diff --git a/.github/ISSUE_TEMPLATE/issue_template.md b/.github/ISSUE_TEMPLATE/issue_template.md new file mode 100644 index 0000000..0f5c8f2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_template.md @@ -0,0 +1,14 @@ +--- +name: issue_template +about: 이슈 전용 탬플릿 입니다 +title: '' +labels: '' +assignees: '' + +--- + +## ISSUE +- 이슈에 대한 설명 + +## ❗ TO DO +- [ ] 해야 할 일에 대한 설명 diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml new file mode 100644 index 0000000..ba915c4 --- /dev/null +++ b/.github/auto_assign.yml @@ -0,0 +1,6 @@ +addAssignees: author + +addReviewers: true + +reviewers: + - sonms diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..88eaf1e --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,12 @@ +## ISSUE +- closed #이슈번호 + +## ❗ WORK DESCRIPTION +- PR에 대한 설명 + +## Uncompleted Tasks 😅 +- [ ] + +## 📸 SCREENSHOT + + diff --git a/.github/workflows/auto_assign_action.yml b/.github/workflows/auto_assign_action.yml new file mode 100644 index 0000000..721a0f9 --- /dev/null +++ b/.github/workflows/auto_assign_action.yml @@ -0,0 +1,13 @@ +name: "Auto Assign and Reviewers" +on: + pull_request: + types: [opened, ready_for_review] + +jobs: + add-reviewers-and-assignees: + runs-on: ubuntu-latest + steps: + - name: Assign author and add reviewers + uses: kentaro-m/auto-assign-action@v2.0.0 + with: + configuration-path: '.github/auto_assign.yml' \ No newline at end of file diff --git a/.gitignore b/.gitignore index aa724b7..d14fddb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,249 @@ +# Created by https://www.toptal.com/developers/gitignore/api/android,androidstudio,macos,windows,linux +# Edit at https://www.toptal.com/developers/gitignore?templates=android,androidstudio,macos,windows,linux + +### Android ### +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +output.json + +# IntelliJ *.iml -.gradle -/local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml +.idea/* +!.idea/codeInsightSettings.xml +!.idea/icon.svg +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof + +### Android Patch ### +gen-external-apklibs + +# Replacement of .externalNativeBuild directories introduced +# with Android Studio 3.5. + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General .DS_Store -/build -/captures +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files + +# Android Studio +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production +.navigation/ +*.ipr +*.swp + +# Keystores files + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Android Patch + +# External native build folder generated in Android Studio 2.2 and later .externalNativeBuild -.cxx -local.properties + +# NDK +obj/ + +# IntelliJ IDEA +*.iws +/out/ + +# User-specific configurations +.idea/caches/ +.idea/libraries/ +.idea/shelf/ +.idea/workspace.xml +.idea/tasks.xml +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/modules.xml +.idea/scopes/scope_settings.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml +.idea/assetWizardSettings.xml +.idea/gradle.xml +.idea/jarRepositories.xml +.idea/navEditor.xml + +# Legacy Eclipse project files +.classpath +.project +.cproject +.settings/ + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.war +*.ear + +# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) +hs_err_pid* + +## Plugin-specific files: + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Mongo Explorer plugin +.idea/mongoSettings.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### AndroidStudio Patch ### + +!/gradle/wrapper/gradle-wrapper.jar + +# End of https://www.toptal.com/developers/gitignore/api/android,androidstudio,macos,windows,linux + +# Fastlane dependencies +vendor/ + +### Claude ### +# Claude Code +CLAUDE.md diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b86273d..fb7f4a8 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 2de1171..f8a571a 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -1,5 +1,6 @@ + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6f54aee..1a71cb4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,9 +1,13 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { - alias(libs.plugins.android.application) + alias(libs.plugins.focpet.android.application) + alias(libs.plugins.focpet.android.buildconfig) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) + alias(libs.plugins.dagger.hilt) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.devtools.ksp) } android { @@ -44,14 +48,10 @@ android { } dependencies { + // presentation + implementation(projects.presentation.main) - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.lifecycle.runtime.ktx) - implementation(libs.androidx.activity.compose) - implementation(platform(libs.androidx.compose.bom)) - - testImplementation(libs.junit) - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.espresso.core) - androidTestImplementation(platform(libs.androidx.compose.bom)) + implementation(libs.timber) + implementation(libs.androidx.appcompat) + implementation(libs.coil.compose) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d8a5166..756ed0a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> diff --git a/app/src/main/java/com/focpet/FocpetApplication.kt b/app/src/main/java/com/focpet/FocpetApplication.kt new file mode 100644 index 0000000..1df1364 --- /dev/null +++ b/app/src/main/java/com/focpet/FocpetApplication.kt @@ -0,0 +1,34 @@ +package com.focpet + +import android.app.Application +import androidx.appcompat.app.AppCompatDelegate +import coil.ImageLoader +import coil.ImageLoaderFactory +import dagger.hilt.android.HiltAndroidApp +import timber.log.Timber +import javax.inject.Inject + +@HiltAndroidApp +class FocpetApplication: Application(), ImageLoaderFactory { + @Inject + lateinit var imageLoaderProvider: dagger.Lazy + + override fun onCreate() { + super.onCreate() + + setDayMode() + initTimber() + } + + private fun setDayMode() { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + } + + private fun initTimber() { + if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) + } + + override fun newImageLoader(): ImageLoader { + return imageLoaderProvider.get() + } +} diff --git a/app/src/main/java/com/focpet/MainActivity.kt b/app/src/main/java/com/focpet/MainActivity.kt deleted file mode 100644 index f813759..0000000 --- a/app/src/main/java/com/focpet/MainActivity.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.focpet - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.focpet.ui.theme.FocpetTheme - -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContent { - FocpetTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - Greeting( - name = "Android", - modifier = Modifier.padding(innerPadding) - ) - } - } - } - } -} - -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier - ) -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - FocpetTheme { - Greeting("Android") - } -} diff --git a/app/src/main/java/com/focpet/di/CoilModule.kt b/app/src/main/java/com/focpet/di/CoilModule.kt new file mode 100644 index 0000000..3ec2636 --- /dev/null +++ b/app/src/main/java/com/focpet/di/CoilModule.kt @@ -0,0 +1,44 @@ +package com.focpet.di + +import android.content.Context +import coil.ImageLoader +import coil.disk.DiskCache +import coil.memory.MemoryCache +import coil.util.DebugLogger +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object CoilModule { + + @Provides + @Singleton + fun provideImageLoader( + @ApplicationContext context: Context, + okHttpClient: OkHttpClient + ): ImageLoader { + return ImageLoader.Builder(context) + .okHttpClient(okHttpClient) + .memoryCache { + MemoryCache.Builder(context) + .maxSizePercent(0.25) + .strongReferencesEnabled(true) + .build() + } + .diskCache { + DiskCache.Builder() + .directory(context.cacheDir.resolve("image_cache")) + .maxSizeBytes(100L * 1024 * 1024) + .build() + } + .logger(DebugLogger()) + .allowRgb565(true) + .build() + } +} diff --git a/build-logic/.gitignore b/build-logic/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/build-logic/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/build-logic/convention/.gitignore b/build-logic/convention/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/build-logic/convention/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts new file mode 100644 index 0000000..58834d6 --- /dev/null +++ b/build-logic/convention/build.gradle.kts @@ -0,0 +1,80 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + `kotlin-dsl` +} + +group = "com.focpet.buildlogic" + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } +} + +dependencies { + compileOnly(libs.android.gradlePlugin) + compileOnly(libs.kotlin.gradlePlugin) + compileOnly(libs.compose.compiler.gradle.plugin) + compileOnly(libs.ksp.gradlePlugin) + compileOnly(libs.hilt.gradlePlugin) +} + +gradlePlugin { + plugins { + // app 모듈용 + register("androidApplication") { + id = "focpet.android.application" + implementationClass = "AndroidApplicationConventionPlugin" + } + // 일반 library용 + register("androidLibrary") { + id = "focpet.android.library" + implementationClass = "AndroidLibraryConventionPlugin" + } + // BuildConfig library용 + register("androidBuildConfig") { + id = "focpet.android.buildconfig" + implementationClass = "AndroidBuildConfigConventionPlugin" + } + // Compose 설정용 + register("androidCompose") { + id = "focpet.android.compose" + implementationClass = "AndroidComposeConventionPlugin" + } + // presentation(feature) 모듈용 + register("androidPresentation") { + id = "focpet.android.presentation" + implementationClass = "AndroidPresentationConventionPlugin" + } + // data 모듈용 + register("androidData") { + id = "focpet.android.data" + implementationClass = "AndroidDataConventionPlugin" + } + // domain 모듈용 + register("kotlinJvm") { + id = "focpet.kotlin.jvm" + implementationClass = "KotlinJvmConventionPlugin" + } + // Hilt 설정용 + register("hilt") { + id = "focpet.hilt" + implementationClass = "HiltConventionPlugin" + } + // Serialization용 + register("serialization") { + id = "focpet.serialization" + implementationClass = "SerializationConventionPlugin" + } + // Test 설정용 + register("androidTest") { + id = "focpet.android.test" + implementationClass = "AndroidTestConventionPlugin" + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt new file mode 100644 index 0000000..4143c5f --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -0,0 +1,47 @@ +import com.android.build.api.dsl.ApplicationExtension +import com.focpet.buildlogic.configureKotlinAndroid +import com.focpet.buildlogic.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure + +/** + * app 모듈 전용 Convention Plugin. + * 프로젝트에서 오직 :app 모듈에서만 사용. + * + * Library와 다르게 Application은: + * - targetSdk 설정 필요 + * - applicationId 설정 필요 (모듈 build.gradle에서 직접 설정) + * - Compose 포함 (app도 Compose 사용) + * + * 적용되는 설정: + * - com.android.application + * - org.jetbrains.kotlin.android + * - org.jetbrains.kotlin.plugin.compose + * - compileSdk, minSdk, JVM 11 (configureKotlinAndroid 헬퍼) + */ +class AndroidApplicationConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.android.application") + apply("org.jetbrains.kotlin.android") + apply("org.jetbrains.kotlin.plugin.compose") + apply("focpet.hilt") + } + extensions.configure { + configureKotlinAndroid(this) + // targetSdk는 compileSdk와 동일하게 + defaultConfig.targetSdk = libs.findVersion("compileSdk").get().requiredVersion.toInt() + buildFeatures { + compose = true + } + packaging { + resources { + excludes.add("META-INF/**") + } + } + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidBuildConfigConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidBuildConfigConventionPlugin.kt new file mode 100644 index 0000000..bb1acff --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidBuildConfigConventionPlugin.kt @@ -0,0 +1,23 @@ +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.api.dsl.CommonExtension +import com.android.build.api.dsl.LibraryExtension +import com.focpet.buildlogic.configureBuildTypes +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** + * BuildConfig의 사용을 필요한 곳에서만 설정할 수 있도록 설정하는 Convention Plugin + * 불필요한 buildconfig를 만드는 의존성을 분리하여 필요한 곳에서만 사용할 수 있게 구현 + * */ +class AndroidBuildConfigConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + // 현재 모듈이 App인지 Library인지 확인해서 알맞은 Extension을 가져옵니다 + val extension = extensions.findByType(ApplicationExtension::class.java) + ?: extensions.findByType(LibraryExtension::class.java) + ?: error("AndroidBuildConfigConventionPlugin은 안드로이드 애플리케이션 또는 라이브러리 플러그인이 적용된 이후에만 사용할 수 있습니다.") + + configureBuildTypes(extension as CommonExtension<*, *, *, *, *, *>) + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidComposeConventionPlugin.kt new file mode 100644 index 0000000..266ea70 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidComposeConventionPlugin.kt @@ -0,0 +1,34 @@ +import com.focpet.buildlogic.androidExtension +import com.focpet.buildlogic.configureAndroidCompose +import com.focpet.buildlogic.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies + +/** + * Compose 설정용 Convention Plugin + * Compose가 필요한 모듈에 AndroidLibraryConventionPlugin과 함께 적용 + * + * 단독으로 쓰이지 않고 Presentation/Data Plugin 내부에서 조합되어 사용 + * + * 적용되는 설정: + * - org.jetbrains.kotlin.plugin.compose + * - buildFeatures.compose = true + * - ComposeCompiler sourceInformation + */ +class AndroidComposeConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.apply("org.jetbrains.kotlin.plugin.compose") + configureAndroidCompose(androidExtension) + + dependencies { + val bom = libs.findLibrary("androidx.compose.bom").get() + "implementation"(platform(bom)) + "implementation"(libs.findBundle("compose").get()) + "debugImplementation"(libs.findLibrary("androidx.ui.tooling").get()) + "debugImplementation"(libs.findLibrary("androidx.ui.test.manifest").get()) + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidDataConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidDataConventionPlugin.kt new file mode 100644 index 0000000..7d74a5f --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidDataConventionPlugin.kt @@ -0,0 +1,48 @@ +import com.android.build.api.dsl.LibraryExtension +import com.focpet.buildlogic.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies + +/** + * Data 모듈용 Convention Plugin + * + * library + hilt + serialization을 조합한 플러그인 + * network(Retrofit), local(DataStore), repository 구현체가 여기에 위치 + * + * 적용되는 설정: + * - convention.android.library (compileSdk, minSdk, JVM 11) + * - convention.hilt (Hilt 의존성) + * - convention.serialization (Serialization 플러그인) + * - domain 모듈 의존성 자동 추가 + * - Retrofit, OkHttp, DataStore, kotlinx-serialization 의존성 자동 추가 + */ +class AndroidDataConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("focpet.android.library") + apply("focpet.hilt") + apply("focpet.serialization") + } + extensions.configure { + packaging { + resources { + excludes.add("META-INF/**") + } + } + } + dependencies { + "implementation"(project(":core:network")) + + // Local + "implementation"(libs.findLibrary("androidx.datastore.preferences").get()) + + "implementation"(libs.findLibrary("javax.inject").get()) + + "implementation"(libs.findBundle("retrofit").get()) + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt new file mode 100644 index 0000000..83f0ff4 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -0,0 +1,33 @@ +import com.android.build.api.dsl.LibraryExtension +import com.android.build.api.variant.LibraryAndroidComponentsExtension +import com.focpet.buildlogic.configureKotlinAndroid +import com.focpet.buildlogic.disableUnnecessaryAndroidTests +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure + +/** + * 일반 Android Library 모듈용 Convention Plugin + * core:common, core:designsystem, core:navigation, core:type 에서 사용 + * + * 적용되는 설정: + * - com.android.library + * - org.jetbrains.kotlin.android + * - compileSdk, minSdk, JVM 11 (configureKotlinAndroid 헬퍼) + */ +class AndroidLibraryConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.android.library") + apply("org.jetbrains.kotlin.android") + } + extensions.configure { + configureKotlinAndroid(this) + } + extensions.configure { + disableUnnecessaryAndroidTests(target) + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidPresentationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidPresentationConventionPlugin.kt new file mode 100644 index 0000000..531caf8 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidPresentationConventionPlugin.kt @@ -0,0 +1,59 @@ +import com.android.build.api.dsl.LibraryExtension +import com.focpet.buildlogic.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies + +/** + * Presentation(feature) 모듈용 Convention Plugin + * auth, home, main, mypage 모듈에서 사용 + * + * library + compose + hilt를 조합한 플러그인 + * 세 플러그인을 매번 따로 적용하지 않고 이걸 하나만 적용 + * + * 적용되는 설정: + * - focpet.android.library (compileSdk, minSdk, JVM 11) + * - focpet.android.compose (Compose 활성화) + * - focpet.hilt (Hilt 의존성) + * - domain, core 모듈 의존성 자동 추가 + */ +class AndroidPresentationConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("focpet.android.library") + apply("focpet.android.compose") + apply("focpet.hilt") + apply("focpet.serialization") + } + + // 리소스 패키징 옵션을 설정합니다 + extensions.configure { + packaging { + resources { + excludes.add("META-INF/**") + } + } + } + + dependencies { + "implementation"(project(":core:common")) + "implementation"(project(":core:designsystem")) + "implementation"(project(":core:navigation")) + + // AndroidX + "implementation"(libs.findBundle("androidx").get()) + + // Hilt Navigation + "implementation"(libs.findLibrary("hilt-navigation-compose").get()) + + // Timber + "implementation"(libs.findLibrary("timber").get()) + + // Immutable + "implementation"(libs.findLibrary("kotlinx.immutable").get()) + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt new file mode 100644 index 0000000..ac305d9 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt @@ -0,0 +1,27 @@ +import com.focpet.buildlogic.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies + +/** + * Android Test 설정용 Convention Plugin + * 테스트가 필요한 모듈에 추가로 적용 + * + * 적용되는 설정: + * - JUnit, Espresso, Compose UI Test 의존성 자동 추가 + */ +class AndroidTestConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + dependencies { + "testImplementation"(libs.findLibrary("junit").get()) + "androidTestImplementation"(libs.findLibrary("androidx.junit").get()) + "androidTestImplementation"(libs.findLibrary("androidx.espresso.core").get()) + "androidTestImplementation"(platform(libs.findLibrary("androidx.compose.bom").get())) + "androidTestImplementation"(libs.findLibrary("androidx.ui.test.junit4").get()) + "debugImplementation"(libs.findLibrary("androidx.ui.tooling").get()) + "debugImplementation"(libs.findLibrary("androidx.ui.test.manifest").get()) + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt b/build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt new file mode 100644 index 0000000..78f7d24 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/HiltConventionPlugin.kt @@ -0,0 +1,32 @@ +import com.focpet.buildlogic.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies + +/** + * Hilt 의존성 주입 설정용 Convention Plugin + * Hilt가 필요한 모듈에 추가로 적용 + * + * data, presentation 모듈에서 사용 + * domain은 Hilt 불필요 (순수 Kotlin 인터페이스/UseCase만 존재) + * + * 적용되는 설정: + * - com.google.dagger.hilt.android 플러그인 + * - com.google.devtools.ksp 플러그인 (어노테이션 처리) + * - hilt-android implementation + * - hilt-compiler ksp + */ +class HiltConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.google.dagger.hilt.android") + apply("com.google.devtools.ksp") + } + dependencies { + "implementation"(libs.findLibrary("hilt").get()) + "ksp"(libs.findLibrary("hilt.compiler").get()) + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/KotlinJvmConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KotlinJvmConventionPlugin.kt new file mode 100644 index 0000000..ee64ba6 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/KotlinJvmConventionPlugin.kt @@ -0,0 +1,27 @@ +import com.focpet.buildlogic.configureKotlinJvm +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply + +/** + * 순수 Kotlin 모듈(domain)용 Convention Plugin + * + * domain 모듈은 Android 의존성이 없어야 해서 + * com.android.library 대신 org.jetbrains.kotlin.jvm 사용 + * + * 장점: + * - 빌드 속도가 Android 모듈보다 빠름 + * - Android 의존성 실수로 추가하면 컴파일 에러로 바로 감지 가능 + * + * 적용되는 설정: + * - org.jetbrains.kotlin.jvm + * - JVM 11 + */ +class KotlinJvmConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + apply(plugin = "org.jetbrains.kotlin.jvm") + configureKotlinJvm() + } + } +} diff --git a/build-logic/convention/src/main/kotlin/SerializationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/SerializationConventionPlugin.kt new file mode 100644 index 0000000..c694dfd --- /dev/null +++ b/build-logic/convention/src/main/kotlin/SerializationConventionPlugin.kt @@ -0,0 +1,27 @@ +import com.focpet.buildlogic.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies + +/** + * Kotlin Serialization 설정용 Convention Plugin + * 네트워크 통신 시 JSON 직렬화/역직렬화에 사용 + * + * data 모듈에서 Retrofit + kotlinx.serialization 조합으로 사용 + * + * 적용되는 설정: + * - org.jetbrains.kotlin.plugin.serialization 플러그인 + * (실제 kotlinx-serialization-json 라이브러리는 + * AndroidDataConventionPlugin에서 의존성으로 추가) + */ +class SerializationConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.apply("org.jetbrains.kotlin.plugin.serialization") + + dependencies { + "implementation"(libs.findLibrary("kotlinx.serialization.json").get()) + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/com/focpet/buildlogic/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/com/focpet/buildlogic/AndroidCompose.kt new file mode 100644 index 0000000..d4e0344 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/focpet/buildlogic/AndroidCompose.kt @@ -0,0 +1,34 @@ +package com.focpet.buildlogic + +import com.android.build.api.dsl.CommonExtension +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType +import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension + +/** + * Compose 관련 설정 헬퍼 + * AndroidComposeConventionPlugin에서 호출 + * + * Compose를 사용하는 모듈은 두 가지 필요 + * 1. buildFeatures.compose = true → Compose 컴파일러 활성화 + * 2. ComposeCompilerGradlePluginExtension → Compose Compiler 세부 옵션 + * + * AGP 8.x부터는 kotlin.plugin.compose를 적용하면 + * buildFeatures.compose가 자동 활성화되기도 하지만 + * 명시적으로 선언해서 의도를 명확히 하는 게 좋을듯 + */ +internal fun Project.configureAndroidCompose( + commonExtension: CommonExtension<*, *, *, *, *, *>, +) { + commonExtension.apply { + buildFeatures { + compose = true + } + } + + extensions.getByType().apply { + // Compose 소스 정보를 컴파일 결과에 포함시켜 + // Layout Inspector에서 Composable 위치 추적이 가능해짐 + includeSourceInformation.set(true) + } +} diff --git a/build-logic/convention/src/main/kotlin/com/focpet/buildlogic/AndroidInstrumentedTests.kt b/build-logic/convention/src/main/kotlin/com/focpet/buildlogic/AndroidInstrumentedTests.kt new file mode 100644 index 0000000..cfe9326 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/focpet/buildlogic/AndroidInstrumentedTests.kt @@ -0,0 +1,16 @@ +package com.focpet.buildlogic + +import com.android.build.api.variant.LibraryAndroidComponentsExtension +import org.gradle.api.Project + +/** + * `androidTest` 소스셋이 없는 라이브러리 모듈의 경우, + * 불필요한 Android Instrumented Test 작업을 비활성화하여 빌드 시간을 최적화합니다. + * 기기에서 직접 실행해보는 테스트(androidTest)를 위한 준비 작업을 백그라운드에서 진행하는데 그걸 막기 위함 + */ +fun LibraryAndroidComponentsExtension.disableUnnecessaryAndroidTests( + project: Project, +) = beforeVariants { variant -> + variant.androidTest.enable = variant.androidTest.enable + && project.projectDir.resolve("src/androidTest").exists() +} diff --git a/build-logic/convention/src/main/kotlin/com/focpet/buildlogic/BuildType.kt b/build-logic/convention/src/main/kotlin/com/focpet/buildlogic/BuildType.kt new file mode 100644 index 0000000..468cb9a --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/focpet/buildlogic/BuildType.kt @@ -0,0 +1,62 @@ +package com.focpet.buildlogic + +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.api.dsl.CommonExtension +import com.android.build.api.dsl.LibraryExtension +import org.gradle.api.Project +import java.util.Properties + +/** + * Build Type을 세팅 헬퍼 - ApplicationExtension, LibraryExtension 공통 + * */ +fun Project.configureBuildTypes( + commonExtension: CommonExtension<*, *, *, *, *, *>, +) { + val properties = Properties().apply { + // 루트 디렉토리의 local.properties 파일을 읽어옵니다 + project.rootProject.file("local.properties").takeIf { it.exists() }?.inputStream()?.use { load(it) } + } + + fun configureBuildTypeFields( + buildConfigField: (String, String, String) -> Unit, + isDebug: Boolean + ) { + // debug 환경이면 "dev", release 환경이면 "prod" 접두사를 사용합니다 + val prefix = if (isDebug) "dev" else "prod" + + buildConfigField( + "String", + "BASE_URL", + properties.getQuotedProperty("$prefix.base.url") + ) + } + + commonExtension.apply { + when (this) { + is ApplicationExtension -> { + buildFeatures { buildConfig = true } + buildTypes { + getByName("debug") { configureBuildTypeFields(::buildConfigField, true) } + getByName("release") { configureBuildTypeFields(::buildConfigField, false) } + } + } + + is LibraryExtension -> { + buildFeatures { buildConfig = true } + buildTypes { + getByName("debug") { configureBuildTypeFields(::buildConfigField, true) } + getByName("release") { configureBuildTypeFields(::buildConfigField, false) } + } + } + } + } +} + +// local.properties에서 값을 가져올 때, 양끝에 따옴표("")가 없으면 자동으로 붙여주는 안전 장치입니다 +private fun Properties.getQuotedProperty(key: String): String { + val value = getProperty(key) ?: "" + if (value.startsWith("\"") && value.endsWith("\"")) { + return value + } + return "\"$value\"" +} diff --git a/build-logic/convention/src/main/kotlin/com/focpet/buildlogic/Extensions.kt b/build-logic/convention/src/main/kotlin/com/focpet/buildlogic/Extensions.kt new file mode 100644 index 0000000..ddaf72d --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/focpet/buildlogic/Extensions.kt @@ -0,0 +1,31 @@ +package com.focpet.buildlogic + +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.api.dsl.CommonExtension +import com.android.build.api.dsl.LibraryExtension +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalog +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.kotlin.dsl.getByType + +// build-logic Convention Plugin 안에서 libs.versions.toml에 접근하기 위한 확장함수 +// 일반 build.gradle.kts에서는 libs가 자동 주입되지만 +// Plugin 클래스 안(Convention Plugin 클래스 안에서는 build.gradle.kts와 달리)에서는 Project의 extension에서 직접 VersionCatalog를 꺼내는 확장함수가 필요 +// 모든 Convention Plugin에서 libs.findLibrary(), libs.findVersion() 형태로 사용 +val Project.libs: VersionCatalog + get() = extensions.getByType().named("libs") + +/** + * 모듈이 Application인지 Library인지 몰라도 + * 공통 타입인 CommonExtension으로 접근할 수 있게 해주는 확장함수 + * + * 예를 들어 configureKotlinAndroid()는 Application/Library 둘 다 지원해야 하는데 + * 각각 타입이 달라서 CommonExtension으로 추상화해서 받 음 + * + * Library → Application 순으로 시도하고 둘 다 없으면 에러 출력 + */ +val Project.androidExtension: CommonExtension<*, *, *, *, *, *> + get() = runCatching { extensions.getByType() } + .recoverCatching { extensions.getByType() } + .onFailure { println("Could not find Library or Application extension") } + .getOrThrow() diff --git a/build-logic/convention/src/main/kotlin/com/focpet/buildlogic/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/com/focpet/buildlogic/KotlinAndroid.kt new file mode 100644 index 0000000..4c10be3 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/focpet/buildlogic/KotlinAndroid.kt @@ -0,0 +1,75 @@ +package com.focpet.buildlogic + +import com.android.build.api.dsl.CommonExtension +import org.gradle.api.JavaVersion +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +/** + * Android 모듈(Library/Application) 공통 설정 헬퍼 + * AndroidLibraryConventionPlugin, AndroidApplicationConventionPlugin에서 호출돼 + * + * compileSdk, minSdk는 libs.versions.toml에서 읽어와서 + * 버전 변경 시 toml 한 곳만 수정하면 댐 + */ +internal fun Project.configureKotlinAndroid( + commonExtension: CommonExtension<*, *, *, *, *, *>, +) { + commonExtension.apply { + // toml의 [versions]에서 읽어옴 + // 모든 Android 모듈이 동일한 compileSdk를 사용하도록 강제 + compileSdk = libs.findVersion("compileSdk").get().requiredVersion.toInt() + + defaultConfig { + // 지원하는 최소 Android 버전 + minSdk = libs.findVersion("minSdk").get().requiredVersion.toInt() + } + + compileOptions { + // Java 소스 코드 호환성 버전 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + } + + // Kotlin 컴파일 옵션은 Android/JVM 공통이라 별도 함수로 분리 + configureKotlin() +} + +/** + * 순수 Kotlin 모듈(domain) 공통 설정 헬퍼 + * KotlinJvmConventionPlugin에서 호출 + * + * Android 의존성이 없는 domain 모듈은 compileSdk/minSdk 설정이 필요 없어서 + * configureKotlin()만 호출 + */ +internal fun Project.configureKotlinJvm() { + extensions.configure { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + configureKotlin() +} + +/** + * Kotlin 컴파일 옵션 공통 설정 + * Android/JVM 모듈 모두 동일하게 적용되는 설정이라 private으로 분리 + * + * freeCompilerArgs: + * - RequiresOptIn: @OptIn 어노테이션 사용을 위한 opt-in 설정 + * (예: ExperimentalCoroutinesApi 같은 실험적 API 사용 시 필요) + */ +private fun Project.configureKotlin() { + tasks.withType().configureEach { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + freeCompilerArgs.addAll( + listOf("-opt-in=kotlin.RequiresOptIn") + ) + } + } +} diff --git a/build-logic/convention/src/main/kotlin/com/focpet/buildlogic/Namespace.kt b/build-logic/convention/src/main/kotlin/com/focpet/buildlogic/Namespace.kt new file mode 100644 index 0000000..0d02da0 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/focpet/buildlogic/Namespace.kt @@ -0,0 +1,12 @@ +package com.focpet.buildlogic + +import org.gradle.api.Project + +/** + * 모듈의 네임스페이스를 설정합니다. (예: "com.focpet.presentation.home") + */ +fun Project.setNamespace(name: String) { + androidExtension.apply { + namespace = "com.focpet.$name" + } +} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 0000000..af69125 --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,16 @@ +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} + +rootProject.name = "build-logic" +include(":convention") diff --git a/core/common/.gitignore b/core/common/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/core/common/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts new file mode 100644 index 0000000..28c3d5d --- /dev/null +++ b/core/common/build.gradle.kts @@ -0,0 +1,10 @@ +import com.focpet.buildlogic.setNamespace + +plugins { + alias(libs.plugins.focpet.android.library) + alias(libs.plugins.focpet.android.compose) +} + +android { + setNamespace("core.common") +} diff --git a/core/common/src/main/AndroidManifest.xml b/core/common/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8bdb7e1 --- /dev/null +++ b/core/common/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/common/src/main/java/com/focpet/core/common/base/UiState.kt b/core/common/src/main/java/com/focpet/core/common/base/UiState.kt new file mode 100644 index 0000000..5d75510 --- /dev/null +++ b/core/common/src/main/java/com/focpet/core/common/base/UiState.kt @@ -0,0 +1,18 @@ +package com.focpet.core.common.base + +import androidx.compose.runtime.Stable + +@Stable +sealed interface UiState { + data object Empty : UiState + + data object Loading : UiState + + data class Success( + val data: T, + ) : UiState + + data class Failure( + val message: String, + ) : UiState +} diff --git a/core/common/src/main/java/com/focpet/core/common/extension/FlowExt.kt b/core/common/src/main/java/com/focpet/core/common/extension/FlowExt.kt new file mode 100644 index 0000000..748d9b3 --- /dev/null +++ b/core/common/src/main/java/com/focpet/core/common/extension/FlowExt.kt @@ -0,0 +1,48 @@ +package com.focpet.core.common.extension + +import android.os.SystemClock +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.flowWithLifecycle +import kotlinx.coroutines.flow.Flow + +@Composable +fun Flow.collectSideEffect( + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + minActiveState: Lifecycle.State = Lifecycle.State.STARTED, + action: suspend (T) -> Unit +) { + val lifecycle = lifecycleOwner.lifecycle + + LaunchedEffect(this, lifecycle) { + flowWithLifecycle(lifecycle, minActiveState) + .collect(action) + } +} + +@Composable +fun Flow.collectSingleEvent( + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + throttleTime: Long = 500L, + action: suspend (T) -> Unit +) { + val lifecycle = lifecycleOwner.lifecycle + var lastEmitTime by remember { mutableLongStateOf(0L) } + + LaunchedEffect(this, lifecycle) { + flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect { value -> + val currentTime = SystemClock.uptimeMillis() + if (currentTime - lastEmitTime > throttleTime) { + lastEmitTime = currentTime + action(value) + } + } + } +} diff --git a/core/common/src/main/java/com/focpet/core/common/extension/ModifierExt.kt b/core/common/src/main/java/com/focpet/core/common/extension/ModifierExt.kt new file mode 100644 index 0000000..7ee0ca5 --- /dev/null +++ b/core/common/src/main/java/com/focpet/core/common/extension/ModifierExt.kt @@ -0,0 +1,26 @@ +package com.focpet.core.common.extension + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.dp + +fun Modifier.noRippleClickable(onClick: () -> Unit): Modifier = composed { + this.clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + onClick() + } +} + +fun Modifier.forcePixelToDp(painter: Painter): Modifier { + return this.size( + width = painter.intrinsicSize.width.dp, + height = painter.intrinsicSize.height.dp + ) +} diff --git a/core/common/src/main/java/com/focpet/core/common/extension/StateFlowExt.kt b/core/common/src/main/java/com/focpet/core/common/extension/StateFlowExt.kt new file mode 100644 index 0000000..9d926fa --- /dev/null +++ b/core/common/src/main/java/com/focpet/core/common/extension/StateFlowExt.kt @@ -0,0 +1,18 @@ +package com.focpet.core.common.extension + +import android.provider.SyncStateContract.Helpers.update +import com.kiero.core.model.UiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +inline fun MutableStateFlow>.updateSuccess( + crossinline onUpdate: (T) -> T +) { + update { currentState -> + if (currentState is UiState.Success) { + currentState.copy(data = onUpdate(currentState.data)) + } else { + currentState + } + } +} diff --git a/core/common/src/main/java/com/focpet/core/common/extension/StringExt.kt b/core/common/src/main/java/com/focpet/core/common/extension/StringExt.kt new file mode 100644 index 0000000..e69de29 diff --git a/core/common/src/main/java/com/focpet/core/common/extension/ThrowableExt.kt b/core/common/src/main/java/com/focpet/core/common/extension/ThrowableExt.kt new file mode 100644 index 0000000..c3b077e --- /dev/null +++ b/core/common/src/main/java/com/focpet/core/common/extension/ThrowableExt.kt @@ -0,0 +1,16 @@ +package com.focpet.core.common.extension + +import java.io.IOException +import java.net.SocketTimeoutException +import java.net.UnknownHostException +import java.util.concurrent.TimeoutException + +fun Throwable.toHandleErrorMessage(): String { + return when (this) { + is TimeoutException -> "네트워크 시간이 초과되었습니다. 다시 시도해주세요." + is SocketTimeoutException -> "네트워크 시간이 초과되었습니다. 다시 시도해주세요" + is UnknownHostException -> "서버에 연결할 수 없습니다. 인터넷 연결을 확인하세요." + is IOException -> "네트워크 연결에 문제가 발생했습니다. 다시 시도하세요." + else -> "알 수 없는 오류가 발생했습니다. 잠시 후 다시 시도하세요." + } +} diff --git a/core/common/src/main/java/com/focpet/core/common/model/DialogState.kt b/core/common/src/main/java/com/focpet/core/common/model/DialogState.kt new file mode 100644 index 0000000..d7cca53 --- /dev/null +++ b/core/common/src/main/java/com/focpet/core/common/model/DialogState.kt @@ -0,0 +1,17 @@ +package com.focpet.core.common.model + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable + +@Immutable +data class DialogState( + val isVisible: Boolean = false, + val onClickAction: () -> Unit = {} +) + +@Stable +class DialogTrigger( + val show: (() -> Unit) -> Unit, + val dismiss: () -> Unit +) + diff --git a/core/common/src/main/java/com/focpet/core/common/model/GlobalUiEventHolder.kt b/core/common/src/main/java/com/focpet/core/common/model/GlobalUiEventHolder.kt new file mode 100644 index 0000000..2e81221 --- /dev/null +++ b/core/common/src/main/java/com/focpet/core/common/model/GlobalUiEventHolder.kt @@ -0,0 +1,10 @@ +package com.focpet.core.common.model + +import androidx.compose.runtime.Stable + +@Stable +class GlobalUiEventHolder( + val dialogTrigger: DialogTrigger, + val showToast: (String) -> Unit, + val showSnackbar: (SnackbarState) -> Unit, +) diff --git a/core/common/src/main/java/com/focpet/core/common/model/SnackbarState.kt b/core/common/src/main/java/com/focpet/core/common/model/SnackbarState.kt new file mode 100644 index 0000000..efe64ca --- /dev/null +++ b/core/common/src/main/java/com/focpet/core/common/model/SnackbarState.kt @@ -0,0 +1,9 @@ +package com.focpet.core.common.model + +import androidx.compose.runtime.Immutable + +@Immutable +data class SnackbarState( + val message: String = "", + val bottomPadding: Int = 30 +) diff --git a/core/common/src/main/java/com/focpet/core/common/trigger/LocalGlobalUiEventTrigger.kt b/core/common/src/main/java/com/focpet/core/common/trigger/LocalGlobalUiEventTrigger.kt new file mode 100644 index 0000000..e4e220c --- /dev/null +++ b/core/common/src/main/java/com/focpet/core/common/trigger/LocalGlobalUiEventTrigger.kt @@ -0,0 +1,8 @@ +package com.focpet.core.common.trigger + +import androidx.compose.runtime.staticCompositionLocalOf +import com.focpet.core.common.model.GlobalUiEventHolder + +val LocalGlobalUiEventTrigger = staticCompositionLocalOf { + error("No GlobalUiEvent Trigger provided") +} diff --git a/core/common/src/main/java/com/focpet/core/common/util/SuspendRunCatching.kt b/core/common/src/main/java/com/focpet/core/common/util/SuspendRunCatching.kt new file mode 100644 index 0000000..64d239c --- /dev/null +++ b/core/common/src/main/java/com/focpet/core/common/util/SuspendRunCatching.kt @@ -0,0 +1,19 @@ +package com.focpet.core.common.util + +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.ensureActive +import kotlin.coroutines.cancellation.CancellationException +import kotlin.coroutines.coroutineContext + +suspend inline fun suspendRunCatching(block: suspend () -> R): Result { + return try { + Result.success(block()) + } catch (t: TimeoutCancellationException) { + Result.failure(t) + } catch (c: CancellationException) { + throw c + } catch (e: Throwable) { + coroutineContext.ensureActive() + Result.failure(e) + } +} diff --git a/core/designsystem/.gitignore b/core/designsystem/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/core/designsystem/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts new file mode 100644 index 0000000..0a41800 --- /dev/null +++ b/core/designsystem/build.gradle.kts @@ -0,0 +1,10 @@ +import com.focpet.buildlogic.setNamespace + +plugins { + alias(libs.plugins.focpet.android.library) + alias(libs.plugins.focpet.android.compose) +} + +android { + setNamespace("core.designsystem") +} diff --git a/core/designsystem/src/main/AndroidManifest.xml b/core/designsystem/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8bdb7e1 --- /dev/null +++ b/core/designsystem/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/java/com/focpet/ui/theme/Color.kt b/core/designsystem/src/main/java/com/focpet/core/designsystem/theme/Color.kt similarity index 74% rename from app/src/main/java/com/focpet/ui/theme/Color.kt rename to core/designsystem/src/main/java/com/focpet/core/designsystem/theme/Color.kt index ef9d894..441e2c4 100644 --- a/app/src/main/java/com/focpet/ui/theme/Color.kt +++ b/core/designsystem/src/main/java/com/focpet/core/designsystem/theme/Color.kt @@ -1,4 +1,4 @@ -package com.focpet.ui.theme +package com.focpet.core.designsystem.theme import androidx.compose.ui.graphics.Color @@ -8,4 +8,4 @@ val Pink80 = Color(0xFFEFB8C8) val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file +val Pink40 = Color(0xFF7D5260) diff --git a/app/src/main/java/com/focpet/ui/theme/Theme.kt b/core/designsystem/src/main/java/com/focpet/core/designsystem/theme/Theme.kt similarity index 97% rename from app/src/main/java/com/focpet/ui/theme/Theme.kt rename to core/designsystem/src/main/java/com/focpet/core/designsystem/theme/Theme.kt index d9db1ed..e895863 100644 --- a/app/src/main/java/com/focpet/ui/theme/Theme.kt +++ b/core/designsystem/src/main/java/com/focpet/core/designsystem/theme/Theme.kt @@ -1,4 +1,4 @@ -package com.focpet.ui.theme +package com.focpet.core.designsystem.theme import android.app.Activity import android.os.Build diff --git a/app/src/main/java/com/focpet/ui/theme/Type.kt b/core/designsystem/src/main/java/com/focpet/core/designsystem/theme/Type.kt similarity index 95% rename from app/src/main/java/com/focpet/ui/theme/Type.kt rename to core/designsystem/src/main/java/com/focpet/core/designsystem/theme/Type.kt index 329f7a0..898e1b8 100644 --- a/app/src/main/java/com/focpet/ui/theme/Type.kt +++ b/core/designsystem/src/main/java/com/focpet/core/designsystem/theme/Type.kt @@ -1,4 +1,4 @@ -package com.focpet.ui.theme +package com.focpet.core.designsystem.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle @@ -31,4 +31,4 @@ val Typography = Typography( letterSpacing = 0.5.sp ) */ -) \ No newline at end of file +) diff --git a/core/designsystem/src/main/res/font/pretendard_bold.otf b/core/designsystem/src/main/res/font/pretendard_bold.otf new file mode 100644 index 0000000..8e5e30a Binary files /dev/null and b/core/designsystem/src/main/res/font/pretendard_bold.otf differ diff --git a/core/designsystem/src/main/res/font/pretendard_medium.otf b/core/designsystem/src/main/res/font/pretendard_medium.otf new file mode 100644 index 0000000..0575069 Binary files /dev/null and b/core/designsystem/src/main/res/font/pretendard_medium.otf differ diff --git a/core/designsystem/src/main/res/font/pretendard_regular.otf b/core/designsystem/src/main/res/font/pretendard_regular.otf new file mode 100644 index 0000000..08bf4cf Binary files /dev/null and b/core/designsystem/src/main/res/font/pretendard_regular.otf differ diff --git a/core/designsystem/src/main/res/font/pretendard_semibold.otf b/core/designsystem/src/main/res/font/pretendard_semibold.otf new file mode 100644 index 0000000..e7e36ab Binary files /dev/null and b/core/designsystem/src/main/res/font/pretendard_semibold.otf differ diff --git a/core/localstorage/.gitignore b/core/localstorage/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/core/localstorage/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/localstorage/build.gradle.kts b/core/localstorage/build.gradle.kts new file mode 100644 index 0000000..e69de29 diff --git a/core/localstorage/src/main/AndroidManifest.xml b/core/localstorage/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8bdb7e1 --- /dev/null +++ b/core/localstorage/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/navigation/.gitignore b/core/navigation/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/core/navigation/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts new file mode 100644 index 0000000..8c8b8a4 --- /dev/null +++ b/core/navigation/build.gradle.kts @@ -0,0 +1,9 @@ +import com.focpet.buildlogic.setNamespace + +plugins { + alias(libs.plugins.focpet.android.library) +} + +android { + setNamespace("core.navigation") +} diff --git a/core/navigation/src/main/java/com/focpet/core/navigation/MainTabRoute.kt b/core/navigation/src/main/java/com/focpet/core/navigation/MainTabRoute.kt new file mode 100644 index 0000000..5c31a92 --- /dev/null +++ b/core/navigation/src/main/java/com/focpet/core/navigation/MainTabRoute.kt @@ -0,0 +1,3 @@ +package com.focpet.core.navigation + +interface MainTabRoute : Route diff --git a/core/navigation/src/main/java/com/focpet/core/navigation/Route.kt b/core/navigation/src/main/java/com/focpet/core/navigation/Route.kt new file mode 100644 index 0000000..ce989bc --- /dev/null +++ b/core/navigation/src/main/java/com/focpet/core/navigation/Route.kt @@ -0,0 +1,3 @@ +package com.focpet.core.navigation + +interface Route diff --git a/core/network/.gitignore b/core/network/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/core/network/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts new file mode 100644 index 0000000..9fec6e9 --- /dev/null +++ b/core/network/build.gradle.kts @@ -0,0 +1,20 @@ +import com.focpet.buildlogic.setNamespace + +plugins { + alias(libs.plugins.focpet.android.library) + alias(libs.plugins.focpet.hilt) + alias(libs.plugins.focpet.serialization) + alias(libs.plugins.focpet.android.buildconfig) +} + +android { + setNamespace("core.network") +} + +dependencies { + implementation(platform(libs.okhttp.bom)) + implementation(libs.bundles.okhttp) + implementation(libs.bundles.retrofit) + implementation(libs.kotlinx.serialization.json) + implementation(libs.timber) +} diff --git a/core/network/consumer-rules.pro b/core/network/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/core/network/proguard-rules.pro b/core/network/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/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/core/network/src/main/AndroidManifest.xml b/core/network/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8bdb7e1 --- /dev/null +++ b/core/network/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/network/src/main/java/com/focpet/core/network/di/NetworkModule.kt b/core/network/src/main/java/com/focpet/core/network/di/NetworkModule.kt new file mode 100644 index 0000000..e46d0f5 --- /dev/null +++ b/core/network/src/main/java/com/focpet/core/network/di/NetworkModule.kt @@ -0,0 +1,55 @@ +package com.focpet.core.network.di + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Converter +import retrofit2.Retrofit +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + + @Provides + @Singleton + fun providesLoggingInterceptor() = HttpLoggingInterceptor().apply { + level = if (BuildConfig.DEBUG) { + HttpLoggingInterceptor.Level.BODY + } else { + HttpLoggingInterceptor.Level.NONE + } + } + + @Provides + @Singleton + fun providesOkHttpClient( + loggingInterceptor: HttpLoggingInterceptor, + ): OkHttpClient = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .addInterceptor(loggingInterceptor) + .build() + + @Provides + @Singleton + fun providesConverterFactory(): Converter.Factory = Json.Default.asConverterFactory("application/json".toMediaType()) + + @Provides + @Singleton + fun providesRetrofit( + client: OkHttpClient, + converterFactory: Converter.Factory + ): Retrofit = Retrofit.Builder() + .baseUrl(BuildConfig.BASE_URL) + .addConverterFactory(converterFactory) + .client(client) + .build() +} diff --git a/core/network/src/main/java/com/focpet/core/network/model/BaseResponse.kt b/core/network/src/main/java/com/focpet/core/network/model/BaseResponse.kt new file mode 100644 index 0000000..3a7b3d4 --- /dev/null +++ b/core/network/src/main/java/com/focpet/core/network/model/BaseResponse.kt @@ -0,0 +1,17 @@ +package com.focpet.core.network.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/* +@Serializable +data class BaseResponse( + @SerialName("status") + val status: Int, + @SerialName("message") + val message: String, + @SerialName("data") + val data: T? = null, +) +*/ + diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/domain/.gitkeep b/domain/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/gradle.properties b/gradle.properties index 132244e..3e88ee2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,23 +1,29 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. For more details, visit -# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects -# org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app's APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn +# Gradle Daemon JVM arguments and file encoding settings +org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8 + +# Enable parallel task execution +org.gradle.parallel=true + +# Enable build cache +org.gradle.caching=true + +# Enable configuration cache +org.gradle.configuration-cache=true + +# Enable parallel configuration cache +org.gradle.configuration-cache.parallel=true + +# Use AndroidX libraries android.useAndroidX=true -# Kotlin code style for this project: "official" or "obsolete": + +# Kotlin code style settings kotlin.code.style=official -# Enables namespacing of each library's R class so that its R class includes only the -# resources declared in the library itself and none from the library's dependencies, -# thereby reducing the size of the R class for that library + +# Use non-transitive R classes (improves build speed) android.nonTransitiveRClass=true + +# Use AGP built-in Kotlin support +android.builtInKotlin=true + +# Enforce unique package names +android.uniquePackageNames=true diff --git a/presentation/.gitignore b/presentation/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/presentation/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts new file mode 100644 index 0000000..f3bd531 --- /dev/null +++ b/presentation/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("java-library") + alias(libs.plugins.jetbrains.kotlin.jvm) +} +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11 + } +} diff --git a/presentation/main/.gitignore b/presentation/main/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/presentation/main/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/presentation/main/build.gradle.kts b/presentation/main/build.gradle.kts new file mode 100644 index 0000000..8793dfd --- /dev/null +++ b/presentation/main/build.gradle.kts @@ -0,0 +1,16 @@ +import com.focpet.buildlogic.setNamespace + +plugins { + alias(libs.plugins.focpet.android.presentation) +} + +android { + setNamespace("presentation.main") + + buildFeatures { + buildConfig = true + } +} + +dependencies { +} diff --git a/presentation/main/consumer-rules.pro b/presentation/main/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/presentation/main/proguard-rules.pro b/presentation/main/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/presentation/main/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/presentation/main/src/main/AndroidManifest.xml b/presentation/main/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8bdb7e1 --- /dev/null +++ b/presentation/main/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/presentation/main/src/main/java/com/focpet/presentation/main/MainActivity.kt b/presentation/main/src/main/java/com/focpet/presentation/main/MainActivity.kt new file mode 100644 index 0000000..166c189 --- /dev/null +++ b/presentation/main/src/main/java/com/focpet/presentation/main/MainActivity.kt @@ -0,0 +1,22 @@ +package com.focpet.presentation.main + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import com.focpet.core.designsystem.theme.FocpetTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + FocpetTheme { + MainScreen() + } + } + } +} + diff --git a/presentation/main/src/main/java/com/focpet/presentation/main/MainScreen.kt b/presentation/main/src/main/java/com/focpet/presentation/main/MainScreen.kt new file mode 100644 index 0000000..2df1b00 --- /dev/null +++ b/presentation/main/src/main/java/com/focpet/presentation/main/MainScreen.kt @@ -0,0 +1,84 @@ +package com.focpet.presentation.main + +import android.widget.Toast +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import com.focpet.core.common.model.DialogTrigger +import com.focpet.core.common.model.GlobalUiEventHolder +import com.focpet.core.common.model.SnackbarState +import com.focpet.core.common.trigger.LocalGlobalUiEventTrigger +import com.focpet.presentation.main.state.MainAppState +import com.focpet.presentation.main.state.rememberDialogStateHolder +import com.focpet.presentation.main.state.rememberMainAppState +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Composable +internal fun MainScreen( + appState: MainAppState = rememberMainAppState(), +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + + val dialogState = rememberDialogStateHolder() + + val snackBarHostState = remember { SnackbarHostState() } + var currentSnackbarState by remember { mutableStateOf(null) } + + val onShowToast: (String) -> Unit = remember { + { message -> + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } + } + + val onShowSnackbar: (SnackbarState) -> Unit = remember(scope, snackBarHostState) { + { state -> + currentSnackbarState = state + scope.launch { + snackBarHostState.currentSnackbarData?.dismiss() + + val job = launch { + snackBarHostState.showSnackbar( + message = state.message, + ) + } + job.invokeOnCompletion { + if (currentSnackbarState == state) { + currentSnackbarState = null + } + } + delay(2000L) + job.cancel() + } + } + } + + val eventHolder = remember(dialogState, onShowToast, onShowSnackbar) { + GlobalUiEventHolder( + dialogTrigger = DialogTrigger( + show = { onConfirm -> + dialogState.showDialog(onConfirm) + }, + dismiss = { + dialogState.dismissDialog() + } + ), + showToast = onShowToast, + showSnackbar = onShowSnackbar, + ) + } + + CompositionLocalProvider( + LocalGlobalUiEventTrigger provides eventHolder, + ) { + + } +} diff --git a/presentation/main/src/main/java/com/focpet/presentation/main/MainTab.kt b/presentation/main/src/main/java/com/focpet/presentation/main/MainTab.kt new file mode 100644 index 0000000..27a5570 --- /dev/null +++ b/presentation/main/src/main/java/com/focpet/presentation/main/MainTab.kt @@ -0,0 +1,19 @@ +package com.focpet.presentation.main + +import androidx.annotation.DrawableRes +import com.focpet.core.navigation.MainTabRoute +import com.focpet.core.navigation.Route + +internal enum class MainTab( + @param:DrawableRes val iconRes: Int, + val route: MainTabRoute, + val label: String, +) { + ; + + companion object { + fun find(predicate: (MainTabRoute) -> Boolean): MainTab? = entries.find { predicate(it.route) } + + fun contains(predicate: (Route) -> Boolean): Boolean = entries.map { it.route }.any { predicate(it) } + } +} diff --git a/presentation/main/src/main/java/com/focpet/presentation/main/state/DialogStateHolder.kt b/presentation/main/src/main/java/com/focpet/presentation/main/state/DialogStateHolder.kt new file mode 100644 index 0000000..9462bf7 --- /dev/null +++ b/presentation/main/src/main/java/com/focpet/presentation/main/state/DialogStateHolder.kt @@ -0,0 +1,28 @@ +package com.focpet.presentation.main.state + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.focpet.core.common.model.DialogState + +@Stable +class DialogStateHolder { + var dialogState by mutableStateOf(DialogState()) + private set + + fun showDialog(onClick: () -> Unit) { + dialogState = DialogState(isVisible = true, onClickAction = onClick) + } + + fun dismissDialog() { + dialogState = dialogState.copy(isVisible = false) + } +} + +@Composable +fun rememberDialogStateHolder(): DialogStateHolder = remember { + DialogStateHolder() +} diff --git a/presentation/main/src/main/java/com/focpet/presentation/main/state/MainAppState.kt b/presentation/main/src/main/java/com/focpet/presentation/main/state/MainAppState.kt new file mode 100644 index 0000000..1af99ab --- /dev/null +++ b/presentation/main/src/main/java/com/focpet/presentation/main/state/MainAppState.kt @@ -0,0 +1,110 @@ +package com.focpet.presentation.main.state + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.navigation.NavDestination.Companion.hasRoute +import androidx.navigation.NavHostController +import androidx.navigation.NavOptions +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navOptions +import com.focpet.presentation.main.MainTab +import java.time.LocalDate +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +@Stable +internal class MainAppState( + val navController: NavHostController, + coroutineScope: CoroutineScope, +) { + /*val startDestination = Splash + + val isOffline: StateFlow = networkMonitor.isOnline + .map(Boolean::not) + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + )*/ + + private val currentDestination = navController.currentBackStackEntryFlow + .map { it.destination } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + + val currentTab: StateFlow = currentDestination + .map { destination -> + MainTab.find { tab -> + destination?.hasRoute(tab::class) == true + } + } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + + val isBottomBarVisible: StateFlow = currentDestination + .map { destination -> + MainTab.contains { tab -> + destination?.hasRoute(tab::class) == true + } + } + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = null + ) + + fun navigate(tab: MainTab) { + val navOptions = navOptions { + navController.currentDestination?.route?.let { + popUpTo(it) { + inclusive = true + saveState = true + } + restoreState = true + launchSingleTop = true + } + } + + val refreshNavOptions = navOptions { + popUpTo(0) { + inclusive = true + } + launchSingleTop + } + + when (tab) { + + } + } + + private val clearStackNavOptions = navOptions { + popUpTo(0) { + inclusive = true + } + launchSingleTop = true + } + + fun navigateUp() { + navController.navigateUp() + } +} + +@Composable +internal fun rememberMainAppState( + //networkMonitor: NetworkMonitor, + navController: NavHostController = rememberNavController(), + coroutineScope: CoroutineScope = rememberCoroutineScope(), +): MainAppState = remember(navController, coroutineScope) { + MainAppState(navController, coroutineScope) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index ae7281f..c1c943e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,3 +22,20 @@ dependencyResolutionManagement { rootProject.name = "Focpet" include(":app") + +// core +include(":core:designsystem") +include(":core:common") +include(":core:navigation") +include(":core:network") +include(":core:localstorage") + +// data +include(":data") + +// domain +include(":domain") + +// presentation +include(":presentation:main") +