diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 98daea7f..01a3d8d5 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -72,6 +72,7 @@ dependencies {
implementation(project(":features:base_mvvm_lifecycle"))
implementation(project(":features:base_mvvm_bottom_sheet"))
implementation(project(":features:compose_permissions_result"))
+ implementation(project(":features:compose_navigation"))
/**
* Network
@@ -108,14 +109,6 @@ dependencies {
implementation(Rx.java)
implementation(Rx.kotlin)
- /**
- * Glide
- */
- implementation(Glide.base)
- implementation(Glide.compiler)
- implementation(Glide.okhttp)
- kapt(Glide.compiler)
-
/**
* Timber
*/
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index f694e25e..eba794e4 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -10,7 +10,7 @@
- @color/purple_500
- @color/purple_500
- - @color/white
+ - @color/white
- false
- true
diff --git a/build.gradle.kts b/build.gradle.kts
index 40423e40..6090ec51 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -12,7 +12,7 @@ buildscript {
classpath("com.android.tools.build:gradle:8.2.2")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22")
classpath("org.jetbrains.kotlin:kotlin-serialization:1.8.22")
- classpath("com.google.dagger:hilt-android-gradle-plugin:2.45")
+ classpath("com.google.dagger:hilt-android-gradle-plugin:2.48")
}
}
diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt
index 1f389bcd..0f108aa2 100644
--- a/buildSrc/src/main/java/Dependencies.kt
+++ b/buildSrc/src/main/java/Dependencies.kt
@@ -6,7 +6,7 @@ object Apps {
object Versions {
const val retrofit = "2.9.0"
const val lifecycle = "2.6.1"
- const val hilt = "2.45"
+ const val hilt = "2.48"
}
object AndroidX {
@@ -59,10 +59,21 @@ object Retrofit {
}
object Glide {
- const val base = "com.github.bumptech.glide:glide:4.16.0"
- const val okhttp = "com.github.bumptech.glide:okhttp3-integration:4.16.0"
- const val compiler = "com.github.bumptech.glide:ksp:4.16.0"
+ const val base = "com.github.bumptech.glide:glide:4.13.2"
+ const val okhttp = "com.github.bumptech.glide:okhttp3-integration:4.13.2"
+ const val compiler = "com.github.bumptech.glide:compiler:4.13.2"
+ const val annotations = "com.github.bumptech.glide:annotations:4.13.2"
const val compose = "com.github.bumptech.glide:compose:1.0.0-beta01"
+// const val base = "com.github.bumptech.glide:glide:4.16.0"
+// const val okhttp = "com.github.bumptech.glide:okhttp3-integration:4.16.0"
+// const val compiler = "com.github.bumptech.glide:ksp:4.16.0"
+// const val annotations = "com.github.bumptech.glide:annotations:4.16.0"
+// const val compose = "com.github.bumptech.glide:compose:1.0.0-beta01"
+// const val base = "com.github.bumptech.glide:glide:5.0.0-rc01"
+// const val okhttp = "com.github.bumptech.glide:okhttp3-integration:5.0.0-rc01"
+// const val compiler = "com.github.bumptech.glide:ksp:5.0.0-rc01"
+// const val annotations = "com.github.bumptech.glide:annotations:5.0.0-rc01"
+// const val compose = "com.github.bumptech.glide:compose:1.0.0-beta01"
}
object KotlinX {
@@ -90,6 +101,8 @@ object Compose {
const val viewModel = "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"
const val constraint = "androidx.constraintlayout:constraintlayout-compose:1.0.1"
const val runtime = "androidx.compose.runtime:runtime"
+ const val navigation = "androidx.navigation:navigation-compose:2.7.7"
+ const val navigationViewModel = "androidx.hilt:hilt-navigation-compose:1.0.0"
}
object UnitTest {
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index 516a05f0..ec8eb90e 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -53,6 +53,8 @@ dependencies {
implementation(Glide.base)
implementation(Glide.compiler)
implementation(Glide.okhttp)
+ implementation(Glide.annotations)
+ implementation(Glide.compose)
kapt(Glide.compiler)
/**
@@ -106,6 +108,7 @@ dependencies {
implementation(Compose.preview)
implementation(Compose.ui)
implementation(Compose.runtime)
+ implementation(Compose.navigation)
/**
* Unit Test
diff --git a/core/src/main/java/com/hmju/core/compose/Extensions.kt b/core/src/main/java/com/hmju/core/compose/Extensions.kt
index 39c8691c..4db198fe 100644
--- a/core/src/main/java/com/hmju/core/compose/Extensions.kt
+++ b/core/src/main/java/com/hmju/core/compose/Extensions.kt
@@ -1,12 +1,20 @@
package com.hmju.core.compose
+import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.SavedStateHandle
+import androidx.navigation.NavController
+import androidx.navigation.NavHostController
import com.hmju.core.compose.ComposeLifecycleState.Companion.from
@@ -35,3 +43,45 @@ fun rememberLifecycleUpdatedState(
}
return currentState
}
+
+/**
+ * 다른데 터치할때 키보드 내리기 위한 유틸 함수
+ * @param focusManager LocalFocusManager
+ */
+fun Modifier.addFocusCleaner(
+ focusManager: FocusManager,
+ doOnClear: () -> Unit = {}
+): Modifier {
+ return this.pointerInput(Unit) {
+ detectTapGestures(onTap = {
+ doOnClear()
+ focusManager.clearFocus()
+ })
+ }
+}
+
+fun NavHostController.backPressed() {
+ if (!popBackStack()) {
+ val activity = this.context as? FragmentActivity
+ activity?.finish()
+ }
+}
+
+inline fun NavController.getBundleData(key: String): T? {
+ val savedStateHandle = currentBackStackEntry?.savedStateHandle ?: return null
+ return savedStateHandle.get(key)
+}
+
+inline fun NavController.putBundle(
+ predicate: SavedStateHandle.() -> Unit
+) {
+ val savedStateHandle = currentBackStackEntry?.savedStateHandle ?: return
+ predicate.invoke(savedStateHandle)
+}
+
+inline fun NavController.prevPutBundle(
+ predicate: SavedStateHandle.() -> Unit
+) {
+ val savedStateHandle = previousBackStackEntry?.savedStateHandle ?: return
+ predicate.invoke(savedStateHandle)
+}
diff --git a/core/src/main/java/com/hmju/core/compose/MutableStateAdapter.kt b/core/src/main/java/com/hmju/core/compose/MutableStateAdapter.kt
new file mode 100644
index 00000000..bdb55497
--- /dev/null
+++ b/core/src/main/java/com/hmju/core/compose/MutableStateAdapter.kt
@@ -0,0 +1,37 @@
+package com.hmju.core.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
+import androidx.compose.runtime.collectAsState
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+/**
+ * Description :
+ *
+ * Created by juhongmin on 4/7/24
+ */
+class MutableStateAdapter(
+ private val state: State,
+ private val mutate: (T) -> Unit
+) : MutableState {
+
+ override var value: T
+ get() = state.value
+ set(value) {
+ mutate(value)
+ }
+
+ override fun component1(): T = value
+ override fun component2(): (T) -> Unit = { value = it }
+}
+
+@Composable
+fun MutableStateFlow.collectAsMutableState(
+ context: CoroutineContext = EmptyCoroutineContext
+): MutableState = MutableStateAdapter(
+ state = collectAsState(context),
+ mutate = { value = it }
+)
diff --git a/core/src/main/java/com/hmju/core/compose/MutableStateFlowList.kt b/core/src/main/java/com/hmju/core/compose/MutableStateFlowList.kt
new file mode 100644
index 00000000..a1ea3c74
--- /dev/null
+++ b/core/src/main/java/com/hmju/core/compose/MutableStateFlowList.kt
@@ -0,0 +1,37 @@
+package com.hmju.core.compose
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/**
+ * Description : Compose + Flow 전용 List 형태의 Flow
+ *
+ * Created by juhongmin on 5/6/24
+ */
+class MutableStateFlowList {
+
+ private val _list = mutableListOf()
+ private val _stateFlow = MutableStateFlow>(emptyList())
+
+ val stateFlow = _stateFlow.asStateFlow()
+
+ fun add(item: T) {
+ _list.add(item)
+ notifyObserver()
+ }
+
+ fun addAll(items: List?) {
+ if (items == null) return
+ _list.addAll(items)
+ notifyObserver()
+ }
+
+ fun remove(item: T) {
+ _list.remove(item)
+ notifyObserver()
+ }
+
+ private fun notifyObserver() {
+ _stateFlow.value = _list.toList()
+ }
+}
diff --git a/core/src/main/java/com/hmju/core/compose/TilColor.kt b/core/src/main/java/com/hmju/core/compose/TilColor.kt
index db1b6865..99775373 100644
--- a/core/src/main/java/com/hmju/core/compose/TilColor.kt
+++ b/core/src/main/java/com/hmju/core/compose/TilColor.kt
@@ -22,7 +22,8 @@ class TilColor internal constructor(
val gray3: Color,
val gray3Light: Color,
val gray4: Color,
- val gray5: Color
+ val gray5: Color,
+ val defBgColor: Color
) {
constructor() : this(
white = Color(255, 255, 255),
@@ -36,6 +37,7 @@ class TilColor internal constructor(
gray3 = Color(204, 204, 204),
gray3Light = Color(229, 229, 229),
gray4 = Color(240, 240, 240),
- gray5 = Color(247, 247, 247)
+ gray5 = Color(247, 247, 247),
+ defBgColor = Color(245,243,244)
)
}
diff --git a/core/src/main/java/com/hmju/core/compose/TilComponent.kt b/core/src/main/java/com/hmju/core/compose/TilComponent.kt
new file mode 100644
index 00000000..6431d5d6
--- /dev/null
+++ b/core/src/main/java/com/hmju/core/compose/TilComponent.kt
@@ -0,0 +1,237 @@
+package com.hmju.core.compose
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.painter.ColorPainter
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
+import com.bumptech.glide.integration.compose.GlideImage
+import com.bumptech.glide.integration.compose.placeholder
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import com.hmju.core.R
+
+/**
+ * Description : Compose TIL Component
+ *
+ * Created by juhongmin on 4/10/24
+ */
+object TilComponent {
+
+ @SuppressLint("ModifierParameter")
+ @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
+ @Composable
+ fun EditText(
+ text: MutableState,
+ labelText: String,
+ placeHolderText: String,
+ keyboardOptions: KeyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
+ nextAction: FocusDirection = FocusDirection.Exit,
+ maxLines: Int = 1,
+ focusModifier: Modifier = Modifier
+ .fillMaxWidth()
+ .border(2.dp, TilTheme.color.black, shape = RoundedCornerShape(15.dp)),
+ unFocusModifier: Modifier = Modifier
+ .fillMaxWidth()
+ .border(2.dp, TilTheme.color.gray3, shape = RoundedCornerShape(15.dp))
+ ) {
+ var isFocused by remember { mutableStateOf(false) }
+ val focusManager = LocalFocusManager.current
+ val modifier = if (isFocused) focusModifier else unFocusModifier
+ Box(
+ modifier = Modifier
+ .onFocusChanged { isFocused = it.isFocused }
+ .then(modifier)
+ ) {
+ TextField(
+ value = text.value,
+ onValueChange = { text.value = it },
+ textStyle = TilTheme.text.h4,
+ label = {
+ if (isFocused || text.value.isNotEmpty()) {
+ Text(text = labelText, color = TilTheme.color.black)
+ } else {
+ Text(text = placeHolderText)
+ }
+ },
+ placeholder = { Text(text = placeHolderText) },
+ keyboardOptions = keyboardOptions,
+ keyboardActions = KeyboardActions { focusManager.moveFocus(nextAction) },
+ singleLine = maxLines <= 1,
+ maxLines = maxLines,
+ colors = TextFieldDefaults.textFieldColors(
+ containerColor = Color.Transparent,
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent
+ )
+ )
+
+// if (isFocused) {
+// Box(modifier = focusBg)
+// } else {
+// Box(modifier = unFocusBg)
+// }
+ }
+ }
+
+ @Composable
+ fun HeaderBackButton(
+ title: String,
+ backClick: () -> Unit,
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentSize()
+ .background(TilTheme.color.white)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier
+ .width(50.dp)
+ .fillMaxHeight()
+ .clickable { backClick() },
+ contentAlignment = Alignment.Center
+ ) {
+ Image(
+ painter = painterResource(R.drawable.ic_arrow_left),
+ contentDescription = null
+ )
+ }
+ Text(
+ text = title,
+ style = TilTheme.text.h4_B,
+ modifier = Modifier.weight(1F),
+ textAlign = TextAlign.Center
+ )
+ Spacer(Modifier.width(50.dp))
+ }
+
+ Spacer(
+ Modifier
+ .fillMaxWidth()
+ .height(1.dp)
+ .background(TilTheme.color.gray4)
+ )
+ }
+ }
+
+ @SuppressLint("ModifierParameter")
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ fun HeaderAndContentsBox(
+ title: String,
+ backClick: () -> Unit,
+ modifier: Modifier = Modifier
+ .fillMaxSize(),
+ content: @Composable BoxScope.() -> Unit
+ ) {
+ Scaffold(
+ modifier = Modifier
+ .fillMaxSize(),
+ topBar = { HeaderBackButton(title, backClick) }
+ ) { paddings ->
+ Box(
+ modifier = modifier
+ .padding(paddings)
+ ) { content() }
+ }
+ }
+
+ @SuppressLint("ModifierParameter")
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ fun HeaderAndContentsColumn(
+ title: String,
+ backClick: () -> Unit,
+ modifier: Modifier = Modifier
+ .fillMaxSize(),
+ contentAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
+ scrollState: ScrollState? = null,
+ content: @Composable ColumnScope.() -> Unit
+ ) {
+
+ val verticalScrollState = scrollState ?: rememberScrollState()
+
+ Scaffold(
+ modifier = Modifier
+ .fillMaxSize(),
+ topBar = { HeaderBackButton(title, backClick) }
+ ) { paddings ->
+ Column(
+ modifier = modifier
+ .padding(paddings)
+ .verticalScroll(verticalScrollState),
+ horizontalAlignment = contentAlignment
+ ) { content() }
+ }
+ }
+
+ @OptIn(ExperimentalGlideComposeApi::class)
+ @SuppressLint("ModifierParameter")
+ @Composable
+ fun ImageLoader(
+ imageUrl: String,
+ contentScale: ContentScale = ContentScale.Crop,
+ modifier: Modifier = Modifier,
+ ) {
+ GlideImage(
+ model = imageUrl,
+ contentDescription = null,
+ modifier = modifier,
+ loading = placeholder(ColorPainter(TilTheme.color.gray3Light)),
+ failure = placeholder(R.drawable.ic_error),
+ contentScale = contentScale
+ ) { requestBuilder ->
+ requestBuilder.diskCacheStrategy(DiskCacheStrategy.NONE)
+ }
+ }
+}
diff --git a/core/src/main/java/com/hmju/core/glide/GlideModule.kt b/core/src/main/java/com/hmju/core/glide/GlideModule.kt
index 368e379e..b5c3a3fe 100644
--- a/core/src/main/java/com/hmju/core/glide/GlideModule.kt
+++ b/core/src/main/java/com/hmju/core/glide/GlideModule.kt
@@ -1,39 +1,40 @@
package com.hmju.core.glide
import android.content.Context
+import android.util.Log
import com.bumptech.glide.Glide
+import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
-import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader.Factory
+import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.module.AppGlideModule
+import hmju.http.tracking_interceptor.TrackingHttpInterceptor
import okhttp3.OkHttpClient
-import timber.log.Timber
import java.io.InputStream
/**
* Description : 이미지 로더 모듈
- *
+ * @see v4.16.0 안먹히는 이슈
+ * @see 해결은 했지만 릴리즈는 안나옴
* Created by juhongmin on 3/9/24
*/
-@Suppress("unused")
@GlideModule
class GlideModule : AppGlideModule() {
+ override fun applyOptions(context: Context, builder: GlideBuilder) {
+ super.applyOptions(context, builder)
+ builder.setLogLevel(Log.VERBOSE)
+ }
+
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
- super.registerComponents(context, glide, registry)
val client = OkHttpClient.Builder()
- // .addInterceptor(TrackingHttpInterceptor())
- .addInterceptor { chain ->
- val req = chain.request()
- val res = chain.proceed(req)
-
- if (!res.isSuccessful) {
- Timber.tag("ImageLoader").d("Error Url ${req.url}")
- }
- return@addInterceptor res
- }
+ .addInterceptor(TrackingHttpInterceptor())
.build()
- registry.replace(GlideUrl::class.java, InputStream::class.java, Factory(client))
+ registry.replace(
+ GlideUrl::class.java,
+ InputStream::class.java,
+ OkHttpUrlLoader.Factory(client)
+ )
}
}
\ No newline at end of file
diff --git a/core/src/main/java/com/hmju/core/network/NetworkExtensions.kt b/core/src/main/java/com/hmju/core/network/NetworkExtensions.kt
new file mode 100644
index 00000000..3143a50d
--- /dev/null
+++ b/core/src/main/java/com/hmju/core/network/NetworkExtensions.kt
@@ -0,0 +1,103 @@
+package com.hmju.core.network
+
+import com.hmju.core.models.base.ApiResponse
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+
+/**
+ * Description : Network Exception
+ *
+ * Created by juhongmin on 5/6/24
+ */
+object NetworkExtensions {
+
+ /**
+ * Network -> Simple Mapper
+ * @param predicate ApiResponse -> Mapping Function
+ * @param defValue 네트워크 에러 발생시 기본값 처리
+ */
+ inline fun ApiResponse.toMap(
+ crossinline predicate: (I) -> O,
+ defValue: I? = null
+ ): Result {
+ return when (this) {
+ is ApiResponse.Success -> {
+ Result.success(predicate(this.data))
+ }
+
+ is ApiResponse.Fail -> {
+ if (defValue == null) {
+ Result.failure(this.err)
+ } else {
+ Result.success(predicate(defValue))
+ }
+ }
+ }
+ }
+
+ @OptIn(DelicateCoroutinesApi::class)
+ suspend inline fun zip(
+ crossinline a: suspend () -> ApiResponse,
+ crossinline b: suspend () -> ApiResponse,
+ crossinline predicate: (A, B) -> R
+ ): Result {
+ return coroutineScope {
+ val job1 = GlobalScope.async { a() }
+ val job2 = GlobalScope.async { b() }
+ val res1 = job1.await()
+ val res2 = job2.await()
+ if (res1 is ApiResponse.Success &&
+ res2 is ApiResponse.Success
+ ) {
+ try {
+ Result.success(predicate(res1.data, res2.data))
+ } catch (ex: Exception) {
+ Result.failure(ex)
+ }
+ } else if (res1 is ApiResponse.Fail) {
+ Result.failure(res1.err)
+ } else if (res2 is ApiResponse.Fail) {
+ Result.failure(res2.err)
+ } else {
+ Result.failure(IllegalStateException("Not Found Data Instance"))
+ }
+ }
+ }
+
+ @OptIn(DelicateCoroutinesApi::class)
+ suspend inline fun zip(
+ crossinline a: suspend () -> ApiResponse,
+ crossinline b: suspend () -> ApiResponse,
+ crossinline c: suspend () -> ApiResponse,
+ crossinline predicate: (A, B, C) -> R
+ ): Result {
+ return coroutineScope {
+ val job1 = GlobalScope.async { a() }
+ val job2 = GlobalScope.async { b() }
+ val job3 = GlobalScope.async { c() }
+ val res1 = job1.await()
+ val res2 = job2.await()
+ val res3 = job3.await()
+ if (res1 is ApiResponse.Success &&
+ res2 is ApiResponse.Success &&
+ res3 is ApiResponse.Success
+ ) {
+ try {
+ Result.success(predicate(res1.data, res2.data, res3.data))
+ } catch (ex: Exception) {
+ Result.failure(ex)
+ }
+ } else if (res1 is ApiResponse.Fail) {
+ Result.failure(res1.err)
+ } else if (res2 is ApiResponse.Fail) {
+ Result.failure(res2.err)
+ } else if (res3 is ApiResponse.Fail) {
+ Result.failure(res3.err)
+ } else {
+ Result.failure(IllegalStateException("Not Found Data Instance"))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/java/com/hmju/core/ui/Extensions.kt b/core/src/main/java/com/hmju/core/ui/Extensions.kt
index 6a956ab2..ec8bf609 100644
--- a/core/src/main/java/com/hmju/core/ui/Extensions.kt
+++ b/core/src/main/java/com/hmju/core/ui/Extensions.kt
@@ -4,6 +4,12 @@ import android.content.ContextWrapper
import android.view.View
import androidx.fragment.app.FragmentActivity
import dagger.hilt.android.internal.managers.ViewComponentManager
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.transform
/**
* FragmentActivity 가져오는 View 기반 확장 함수
@@ -35,3 +41,50 @@ fun View.getFragmentActivity(): FragmentActivity? {
}
return null
}
+
+/**
+ * StateFlow Observer 처리하는 함수
+ * [SharingStarted.WhileSubscribed] 정책
+ * stopTimeoutMillis Collector 가 모두 사라진 이후 정치할 Delay
+ * replayExpirationMillis 캐싱할 값을 유지할 시간
+ * ex.)
+ * private val _uiStateFlow = MutableStateFlow()
+ * val successMessage: StateFlow = _uiStateFlow
+ * .observer({ if (it is UiState.Success) it.value else "none" }, "default")
+ *
+ * @param transform Map 기능을 하는 함수 [Transformations.map] 동일
+ * @param initValue NotNull 초기 값
+ */
+inline fun Flow.observer(
+ crossinline transform: suspend (value: I) -> T,
+ initValue: T,
+ scope: CoroutineScope
+): StateFlow {
+ return transform { value ->
+ return@transform emit(transform(value))
+ }.stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(
+ stopTimeoutMillis = 0,
+ replayExpirationMillis = 3000
+ ),
+ initValue
+ )
+}
+
+/**
+ * StateFlow Simple 함수
+ */
+inline fun Flow.stateIn(
+ initValue: T,
+ scope: CoroutineScope
+): StateFlow {
+ return stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(
+ stopTimeoutMillis = 0,
+ replayExpirationMillis = 3000
+ ),
+ initValue
+ )
+}
\ No newline at end of file
diff --git a/core/src/main/java/com/hmju/core/ui/base/BaseViewModel.kt b/core/src/main/java/com/hmju/core/ui/base/BaseViewModel.kt
index 4316ccd8..4425b988 100644
--- a/core/src/main/java/com/hmju/core/ui/base/BaseViewModel.kt
+++ b/core/src/main/java/com/hmju/core/ui/base/BaseViewModel.kt
@@ -4,10 +4,16 @@ import android.os.Bundle
import androidx.annotation.CallSuper
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.kotlin.addTo
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.transform
import timber.log.Timber
import javax.inject.Inject
@@ -83,6 +89,36 @@ open class BaseViewModel @Inject constructor() : ViewModel() {
compositeDisposable.clear()
}
+ /**
+ * StateFlow Observer 처리하는 함수
+ * [SharingStarted.WhileSubscribed] 정책
+ * stopTimeoutMillis Collector 가 모두 사라진 이후 정치할 Delay
+ * replayExpirationMillis 캐싱할 값을 유지할 시간
+ * ex.)
+ * private val _uiStateFlow = MutableStateFlow()
+ * val successMessage: StateFlow = _uiStateFlow
+ * .observer({ if (it is UiState.Success) it.value else "none" }, "default")
+ *
+ * @param transform Map 기능을 하는 함수 [Transformations.map] 동일
+ * @param initValue NotNull 초기 값
+ */
+ protected inline fun Flow.observer(
+ crossinline transform: suspend (value: I) -> T,
+ initValue: T
+ ): StateFlow {
+ return transform { value ->
+ return@transform emit(transform(value))
+ }.stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(
+ stopTimeoutMillis = 0,
+ replayExpirationMillis = 3000
+ ),
+ initValue
+ )
+ }
+
+
override fun onCleared() {
super.onCleared()
if (!compositeDisposable.isDisposed) {
diff --git a/core/src/main/java/com/hmju/core/ui/binding/GlideBindingAdapter.kt b/core/src/main/java/com/hmju/core/ui/binding/GlideBindingAdapter.kt
index 045449a3..2648262d 100644
--- a/core/src/main/java/com/hmju/core/ui/binding/GlideBindingAdapter.kt
+++ b/core/src/main/java/com/hmju/core/ui/binding/GlideBindingAdapter.kt
@@ -4,6 +4,7 @@ import android.view.View
import androidx.appcompat.widget.AppCompatImageView
import androidx.databinding.BindingAdapter
import com.bumptech.glide.RequestManager
+import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.transition.DrawableCrossFadeFactory
@@ -43,6 +44,7 @@ object GlideBindingAdapter {
requestManager.load(url)
.transition(crossFadeTransition)
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
.into(iv)
}
}
diff --git a/core/src/main/res/drawable/ic_arrow_left.xml b/core/src/main/res/drawable/ic_arrow_left.xml
new file mode 100644
index 00000000..8565f294
--- /dev/null
+++ b/core/src/main/res/drawable/ic_arrow_left.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/features/compose-ui/build.gradle.kts b/features/compose-ui/build.gradle.kts
index 481ecf20..e407d782 100644
--- a/features/compose-ui/build.gradle.kts
+++ b/features/compose-ui/build.gradle.kts
@@ -65,7 +65,6 @@ dependencies {
implementation(Compose.constraint)
implementation(Compose.tracing)
implementation(Compose.activity)
- implementation(Glide.compose)
testImplementation(UnitTest.junit)
androidTestImplementation(platform(Compose.base))
diff --git a/features/compose-ui/src/main/java/com/features/compose_ui/ComposeUiActivity.kt b/features/compose-ui/src/main/java/com/features/compose_ui/ComposeUiActivity.kt
index ae2293e5..c7adff19 100644
--- a/features/compose-ui/src/main/java/com/features/compose_ui/ComposeUiActivity.kt
+++ b/features/compose-ui/src/main/java/com/features/compose_ui/ComposeUiActivity.kt
@@ -6,8 +6,11 @@ import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -49,40 +52,46 @@ class ComposeUiActivity :
@Preview
@Composable
fun TestMessageCard() {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(15.dp)
- .background(Color.Gray)
- ) {
- Text(
- text = "Hello H0",
- style = TilTheme.text.h0,
- modifier = Modifier.clickable {
- val intent = Intent(this@ComposeUiActivity, GeneralComposeActivity::class.java)
- this@ComposeUiActivity.startActivity(intent)
+ MaterialTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(15.dp)
+ ) {
+ Text(
+ text = "Hello H0",
+ style = TilTheme.text.h0,
+ modifier = Modifier.clickable {
+ val intent = Intent(this@ComposeUiActivity, GeneralComposeActivity::class.java)
+ this@ComposeUiActivity.startActivity(intent)
+ }
+ )
+ Text(
+ text = "Hello H1",
+ style = TilTheme.text.h1
+ )
+ Text(
+ text = "Hello H2",
+ style = TilTheme.text.h2
+ )
+ Text(
+ text = "Hello H3",
+ style = TilTheme.text.h3
+ )
+ Text(
+ text = "Hello h4",
+ style = TilTheme.text.h4
+ )
+ Text(
+ text = "Hello h5",
+ style = TilTheme.text.h5
+ )
}
- )
- Text(
- text = "Hello H1",
- style = TilTheme.text.h1
- )
- Text(
- text = "Hello H2",
- style = TilTheme.text.h2
- )
- Text(
- text = "Hello H3",
- style = TilTheme.text.h3
- )
- Text(
- text = "Hello h4",
- style = TilTheme.text.h4
- )
- Text(
- text = "Hello h5",
- style = TilTheme.text.h5
- )
+ }
}
}
}
diff --git a/features/compose-ui/src/main/java/com/features/compose_ui/models/MemoUiModel.kt b/features/compose-ui/src/main/java/com/features/compose_ui/models/MemoUiModel.kt
index ec0ac0ce..61c61b1b 100644
--- a/features/compose-ui/src/main/java/com/features/compose_ui/models/MemoUiModel.kt
+++ b/features/compose-ui/src/main/java/com/features/compose_ui/models/MemoUiModel.kt
@@ -1,6 +1,5 @@
package com.features.compose_ui.models
-import android.graphics.drawable.GradientDrawable
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -21,16 +20,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
-import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
-import com.bumptech.glide.integration.compose.GlideImage
-import com.bumptech.glide.integration.compose.placeholder
-import com.hmju.core.compose.TilTheme
import com.hmju.core.R
+import com.hmju.core.compose.TilComponent
+import com.hmju.core.compose.TilTheme
/**
* Description : Compose Memo UiModel
@@ -210,7 +206,6 @@ sealed interface MemoUiModel {
return "ImageAndInfo"
}
- @OptIn(ExperimentalGlideComposeApi::class)
@Composable
override fun GetUi() {
Row(
@@ -219,21 +214,12 @@ sealed interface MemoUiModel {
.wrapContentHeight()
.padding(bottom = 15.dp)
) {
- GlideImage(
- model = imageUrl,
- contentDescription = null,
+ TilComponent.ImageLoader(
+ imageUrl = imageUrl,
modifier = Modifier
.width(100.dp)
.height(100.dp)
- .clip(RoundedCornerShape(5.dp)),
- contentScale = ContentScale.Crop,
- loading = placeholder(
- GradientDrawable(
- GradientDrawable.Orientation.BL_TR,
- intArrayOf(R.color.gray3, R.color.gray3)
- )
- ),
- failure = placeholder(R.drawable.ic_error)
+ .clip(RoundedCornerShape(5.dp))
)
Column(
@@ -276,24 +262,14 @@ sealed interface MemoUiModel {
return "ImageThumb"
}
- @ExperimentalGlideComposeApi
@Composable
override fun GetUi() {
- GlideImage(
- model = imageUrl,
- contentDescription = null,
+ TilComponent.ImageLoader(
+ imageUrl = imageUrl,
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
- .border(1.dp, TilTheme.color.gray3),
- contentScale = ContentScale.Crop,
- loading = placeholder(
- GradientDrawable(
- GradientDrawable.Orientation.BL_TR,
- intArrayOf(R.color.gray3, R.color.gray3)
- )
- ),
- failure = placeholder(R.drawable.ic_error)
+ .border(1.dp, TilTheme.color.gray3)
)
}
}
diff --git a/features/compose_navigation/.gitignore b/features/compose_navigation/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/features/compose_navigation/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/features/compose_navigation/build.gradle.kts b/features/compose_navigation/build.gradle.kts
new file mode 100644
index 00000000..ca8d2f79
--- /dev/null
+++ b/features/compose_navigation/build.gradle.kts
@@ -0,0 +1,58 @@
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("dagger.hilt.android.plugin")
+ id("kotlinx-serialization")
+ kotlin("kapt")
+}
+
+android {
+ namespace = "com.features.compose_navigation"
+ buildFeatures { compose = true }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Compose.compile
+ }
+}
+
+dependencies {
+ implementation(project(":core"))
+ implementation(project(":features:compose_navigation_bridge"))
+
+ implementation(AndroidX.appCompat)
+
+ /**
+ * Network
+ */
+ implementation(Retrofit.base)
+
+ /**
+ * Kotlinx Serialization
+ */
+ implementation(KotlinX.serialization)
+
+ /**
+ * Timber
+ */
+ implementation(Log.timber)
+
+ /**
+ * Hilt
+ */
+ implementation(Hilt.android)
+ kapt(Hilt.compiler)
+
+ /**
+ * Compose
+ */
+ implementation(platform(Compose.base))
+ implementation(Compose.material)
+ implementation(Compose.ui)
+ implementation(Compose.preview)
+ implementation(Compose.viewModel)
+ implementation(Compose.constraint)
+ implementation(Compose.tracing)
+ implementation(Compose.activity)
+ implementation(Compose.navigation)
+ implementation(Compose.navigationViewModel)
+}
\ No newline at end of file
diff --git a/features/compose_navigation/consumer-rules.pro b/features/compose_navigation/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/features/compose_navigation/proguard-rules.pro b/features/compose_navigation/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/features/compose_navigation/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/features/compose_navigation/src/main/AndroidManifest.xml b/features/compose_navigation/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..655dd532
--- /dev/null
+++ b/features/compose_navigation/src/main/AndroidManifest.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/features/compose_navigation/src/main/java/com/features/compose_navigation/ApiService.kt b/features/compose_navigation/src/main/java/com/features/compose_navigation/ApiService.kt
new file mode 100644
index 00000000..3d9d6fce
--- /dev/null
+++ b/features/compose_navigation/src/main/java/com/features/compose_navigation/ApiService.kt
@@ -0,0 +1,25 @@
+package com.features.compose_navigation
+
+import com.features.compose_navigation.models.entity.FileEntity
+import com.features.compose_navigation.models.entity.MemoEntity
+import com.hmju.core.models.base.ApiResponse
+import com.hmju.core.models.base.JSendList
+import retrofit2.http.GET
+import retrofit2.http.QueryMap
+
+/**
+ * Description :
+ *
+ * Created by juhongmin on 5/6/24
+ */
+interface ApiService {
+ @GET("/api/v1/memo")
+ suspend fun fetchMemo(
+ @QueryMap(encoded = true) params: Map
+ ): ApiResponse>
+
+ @GET("/api/v1/uploads")
+ suspend fun fetchUpload(
+ @QueryMap(encoded = true) params: Map
+ ): ApiResponse>
+}
diff --git a/features/compose_navigation/src/main/java/com/features/compose_navigation/ComposeNavigationActivity.kt b/features/compose_navigation/src/main/java/com/features/compose_navigation/ComposeNavigationActivity.kt
new file mode 100644
index 00000000..d071a171
--- /dev/null
+++ b/features/compose_navigation/src/main/java/com/features/compose_navigation/ComposeNavigationActivity.kt
@@ -0,0 +1,62 @@
+package com.features.compose_navigation
+
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.rememberNavController
+import com.features.compose_navigation.screens.login.LoginScreen
+import com.features.compose_navigation.screens.login.SignUpScreen
+import com.features.compose_navigation.screens.memo.MemoScreen
+import com.hmju.core.compose.TilTheme
+import com.hmju.core.compose.addFocusCleaner
+import dagger.hilt.android.AndroidEntryPoint
+
+/**
+ * Description :
+ *
+ * Created by juhongmin on 4/6/24
+ */
+@AndroidEntryPoint
+internal class ComposeNavigationActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ MaterialTheme {
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .addFocusCleaner(LocalFocusManager.current),
+ color = TilTheme.color.white
+ ) {
+ InitNavigation()
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun InitNavigation(
+ navController: NavHostController = rememberNavController()
+ ) {
+ NavHost(navController, Screens.SIGNUP.destination) {
+ Screens.SIGNUP.getNavGraph(this) {
+ SignUpScreen(navController)
+ }
+ Screens.LOGIN.getNavGraph(this) {
+ LoginScreen(navController)
+ }
+ Screens.MEMO.getNavGraph(this) {
+ MemoScreen(navController)
+ }
+ }
+ }
+}
diff --git a/features/compose_navigation/src/main/java/com/features/compose_navigation/Screens.kt b/features/compose_navigation/src/main/java/com/features/compose_navigation/Screens.kt
new file mode 100644
index 00000000..7a661776
--- /dev/null
+++ b/features/compose_navigation/src/main/java/com/features/compose_navigation/Screens.kt
@@ -0,0 +1,103 @@
+package com.features.compose_navigation
+
+import androidx.compose.animation.AnimatedContentScope
+import androidx.compose.animation.AnimatedContentTransitionScope
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.runtime.Composable
+import androidx.navigation.NamedNavArgument
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavType
+import androidx.navigation.compose.composable
+import androidx.navigation.navArgument
+
+/**
+ * Description :
+ *
+ * Created by juhongmin on 4/7/24
+ */
+enum class Screens(
+ val destination: String,
+ val arguments: List = listOf() // type == StringType 만 가능
+) {
+ SIGNUP("signup"),
+ LOGIN(
+ destination = "login",
+ arguments = listOf(
+ navArgument("user_id") {
+ type = NavType.StringType
+ nullable = true
+ },
+ navArgument("user_pw") {
+ type = NavType.StringType
+ nullable = true
+ }
+ )
+ ),
+ MEMO(
+ destination = "memo",
+ arguments = listOf(
+ navArgument("user_id") {
+ type = NavType.StringType
+ }
+ )
+ );
+
+ fun getNavGraph(
+ builder: NavGraphBuilder,
+ content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit
+ ) {
+ val route = StringBuilder(destination)
+ if (arguments.isNotEmpty()) {
+ route.append("?")
+ route.append(arguments.joinToString("&") { "${it.name}={${it.name}}" })
+ }
+ return builder.composable(
+ route = route.toString(),
+ arguments = arguments,
+ content = content,
+ enterTransition = {
+ slideIntoContainer(
+ AnimatedContentTransitionScope.SlideDirection.Left,
+ tween(400)
+ )
+ },
+ exitTransition = {
+ fadeOut(tween(400))
+ },
+ popEnterTransition = {
+ fadeIn(tween(400))
+ },
+ popExitTransition = {
+ slideOutOfContainer(
+ AnimatedContentTransitionScope.SlideDirection.Right,
+ tween(400)
+ )
+ }
+ )
+ }
+
+ /**
+ * 화면에 정의된 Argument 스펙 기준으로 파라미터 셋팅해서 URL 형식으로 전달하는 함수
+ * @param argumentsMap 다음 화면에 전달할 파라미터 데이터
+ */
+ fun getNavigation(
+ argumentsMap: Map = mapOf()
+ ): String {
+ val route = StringBuilder(destination)
+ if (arguments.isNotEmpty()) {
+ route.append("?")
+ route.append(arguments.mapNotNull {
+ val value = argumentsMap[it.name]
+ if (value != null) {
+ "${it.name}=$value"
+ } else {
+ null
+ }
+ }.joinToString("&"))
+ }
+ return route.toString()
+ }
+}
diff --git a/features/compose_navigation/src/main/java/com/features/compose_navigation/di/FeatureModule.kt b/features/compose_navigation/src/main/java/com/features/compose_navigation/di/FeatureModule.kt
new file mode 100644
index 00000000..e8a90ade
--- /dev/null
+++ b/features/compose_navigation/src/main/java/com/features/compose_navigation/di/FeatureModule.kt
@@ -0,0 +1,35 @@
+package com.features.compose_navigation.di
+
+import android.content.Context
+import com.features.compose_navigation.ApiService
+import com.features.compose_navigation.impl.ComposeNavigationBridgeImpl
+import com.features.compose_navigation_bridge.ComposeNavigationBridge
+import com.hmju.core.network.NetworkProvider
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+
+/**
+ * Description :
+ *
+ * Created by juhongmin on 4/6/24
+ */
+@InstallIn(SingletonComponent::class)
+@Module
+internal object FeatureModule {
+ @Provides
+ fun provideBridge(
+ @ApplicationContext context: Context
+ ): ComposeNavigationBridge {
+ return ComposeNavigationBridgeImpl(context)
+ }
+
+ @Provides
+ fun provideApiService(
+ provider: NetworkProvider
+ ): ApiService {
+ return provider.createApiService(ApiService::class.java)
+ }
+}
diff --git a/features/compose_navigation/src/main/java/com/features/compose_navigation/impl/ComposeNavigationBridgeImpl.kt b/features/compose_navigation/src/main/java/com/features/compose_navigation/impl/ComposeNavigationBridgeImpl.kt
new file mode 100644
index 00000000..11ee7e01
--- /dev/null
+++ b/features/compose_navigation/src/main/java/com/features/compose_navigation/impl/ComposeNavigationBridgeImpl.kt
@@ -0,0 +1,22 @@
+package com.features.compose_navigation.impl
+
+import android.content.Context
+import android.content.Intent
+import com.features.compose_navigation.ComposeNavigationActivity
+import com.features.compose_navigation_bridge.ComposeNavigationBridge
+
+/**
+ * Description :
+ *
+ * Created by juhongmin on 4/6/24
+ */
+internal class ComposeNavigationBridgeImpl(
+ private val context: Context
+) : ComposeNavigationBridge {
+ override fun moveToPage() {
+ Intent(context, ComposeNavigationActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(this)
+ }
+ }
+}
diff --git a/features/compose_navigation/src/main/java/com/features/compose_navigation/models/MemoClickEvent.kt b/features/compose_navigation/src/main/java/com/features/compose_navigation/models/MemoClickEvent.kt
new file mode 100644
index 00000000..9a383318
--- /dev/null
+++ b/features/compose_navigation/src/main/java/com/features/compose_navigation/models/MemoClickEvent.kt
@@ -0,0 +1,15 @@
+package com.features.compose_navigation.models
+
+/**
+ * Description :
+ *
+ * Created by juhongmin on 5/6/24
+ */
+sealed class MemoClickEvent {
+
+ object Init : MemoClickEvent()
+
+ data class Item(
+ val msg: String
+ )
+}
diff --git a/features/compose_navigation/src/main/java/com/features/compose_navigation/models/MemoUiModel.kt b/features/compose_navigation/src/main/java/com/features/compose_navigation/models/MemoUiModel.kt
new file mode 100644
index 00000000..7efb7594
--- /dev/null
+++ b/features/compose_navigation/src/main/java/com/features/compose_navigation/models/MemoUiModel.kt
@@ -0,0 +1,118 @@
+package com.features.compose_navigation.models
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.features.compose_navigation.models.entity.FileEntity
+import com.features.compose_navigation.models.entity.MemoEntity
+import com.hmju.core.compose.TilComponent
+import com.hmju.core.compose.TilTheme
+
+/**
+ * Description : 메모 리스트 UiModel
+ *
+ * Created by juhongmin on 5/6/24
+ */
+sealed interface MemoUiModel {
+
+ fun getType(): String
+
+ @Composable
+ fun GetUi(clickEvent: (MemoClickEvent) -> Unit)
+
+ data class Item(
+ val id: Int,
+ val title: String,
+ val contents: String,
+ val imagePath: String? = null
+ ) : MemoUiModel {
+
+ constructor(
+ memoEntity: MemoEntity,
+ fileEntity: FileEntity?
+ ) : this(
+ id = memoEntity.id,
+ title = memoEntity.title,
+ contents = memoEntity.contents,
+ imagePath = fileEntity?.imageUrl
+ )
+
+ override fun getType(): String {
+ return "ItemType"
+ }
+
+ @Composable
+ override fun GetUi(clickEvent: (MemoClickEvent) -> Unit) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(PaddingValues(horizontal = 20.dp, vertical = 20.dp))
+ .shadow(
+ elevation = 6.dp,
+ shape = RoundedCornerShape(8.dp)
+ )
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(Color.White)
+ .clip(RoundedCornerShape(8.dp))
+ ) {
+ TilComponent.ImageLoader(
+ imageUrl = imagePath ?: "",
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(150.dp)
+ )
+ Text(
+ text = title,
+ style = TilTheme.text.h3_B,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 10.dp, horizontal = 15.dp)
+ )
+
+ Text(
+ text = contents,
+ style = TilTheme.text.h4_M,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 10.dp, horizontal = 15.dp)
+ )
+ }
+ }
+ }
+ }
+
+ object Empty : MemoUiModel {
+ override fun getType(): String {
+ return "Empty"
+ }
+
+ @Composable
+ override fun GetUi(clickEvent: (MemoClickEvent) -> Unit) {
+ Spacer(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp)
+ .background(TilTheme.color.gray3)
+ )
+ }
+ }
+}
diff --git a/features/compose_navigation/src/main/java/com/features/compose_navigation/models/entity/FileEntity.kt b/features/compose_navigation/src/main/java/com/features/compose_navigation/models/entity/FileEntity.kt
new file mode 100644
index 00000000..2354f815
--- /dev/null
+++ b/features/compose_navigation/src/main/java/com/features/compose_navigation/models/entity/FileEntity.kt
@@ -0,0 +1,20 @@
+package com.features.compose_navigation.models.entity
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * Description : FileEntity
+ *
+ * Created by juhongmin on 5/6/24
+ */
+@Serializable
+data class FileEntity(
+ val id: Int = 0,
+ @SerialName("original_name")
+ val originalName: String = "",
+ @SerialName("path")
+ val imageUrl: String = "",
+ @SerialName("mime_type")
+ val mimeType: String = ""
+)
\ No newline at end of file
diff --git a/features/compose_navigation/src/main/java/com/features/compose_navigation/models/entity/MemoEntity.kt b/features/compose_navigation/src/main/java/com/features/compose_navigation/models/entity/MemoEntity.kt
new file mode 100644
index 00000000..fcb4e43c
--- /dev/null
+++ b/features/compose_navigation/src/main/java/com/features/compose_navigation/models/entity/MemoEntity.kt
@@ -0,0 +1,35 @@
+package com.features.compose_navigation.models.entity
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+/**
+ * Description :
+ *
+ * Created by juhongmin on 5/6/24
+ */
+@Serializable
+data class MemoEntity(
+ val id: Int = 0,
+ val userId: String = "",
+ val tag: Int = 0,
+ val title: String = "",
+ val contents: String = "",
+ @SerialName("register_date")
+ val registerDate: String = ""
+) {
+ companion object {
+ val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.KOREA)
+ }
+
+ fun getDate(): Date? {
+ return try {
+ format.parse(registerDate)
+ } catch (ex: Exception) {
+ null
+ }
+ }
+}
diff --git a/features/compose_navigation/src/main/java/com/features/compose_navigation/screens/login/LoginScreen.kt b/features/compose_navigation/src/main/java/com/features/compose_navigation/screens/login/LoginScreen.kt
new file mode 100644
index 00000000..338e729c
--- /dev/null
+++ b/features/compose_navigation/src/main/java/com/features/compose_navigation/screens/login/LoginScreen.kt
@@ -0,0 +1,126 @@
+package com.features.compose_navigation.screens.login
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.rememberNavController
+import com.features.compose_navigation.Screens
+import com.hmju.core.compose.TilComponent
+import com.hmju.core.compose.TilTheme
+import com.hmju.core.compose.addFocusCleaner
+import com.hmju.core.compose.backPressed
+import com.hmju.core.compose.collectAsMutableState
+
+/**
+ * Description : 로그인 화면
+ *
+ * Created by juhongmin on 4/7/24
+ */
+@Composable
+fun LoginScreen(
+ navigator: NavHostController,
+ viewModel: LoginViewModel = hiltViewModel()
+) {
+ TilComponent.HeaderAndContentsColumn(
+ title = "로그인",
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(start = 16.dp, end = 16.dp),
+ backClick = { navigator.backPressed() }
+ ) {
+ TilComponent.ImageLoader(
+ imageUrl = "https://til.qtzz.synology.me/resources/img/20240507/1715084116936.png",
+ modifier = Modifier
+ .size(150.dp, 150.dp)
+ .padding(30.dp)
+ .clip(RoundedCornerShape(150.dp))
+ )
+ val id = viewModel.id.collectAsMutableState()
+ val pw = viewModel.password.collectAsMutableState()
+ val isEnable = viewModel.loginEnable.collectAsState()
+ TilComponent.EditText(
+ text = id,
+ labelText = "이름",
+ placeHolderText = "입력해주세요.",
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
+ nextAction = FocusDirection.Next
+ )
+ Spacer(Modifier.height(10.dp))
+ TilComponent.EditText(
+ text = pw,
+ labelText = "비밀번호",
+ placeHolderText = "입력해주세요"
+ )
+ Spacer(Modifier.height(50.dp))
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp)
+ .clip(RoundedCornerShape(6.dp))
+ .then(
+ if (isEnable.value) {
+ Modifier.background(TilTheme.color.blue)
+ } else {
+ Modifier.background(TilTheme.color.gray4)
+ }
+ )
+ .clickable(enabled = isEnable.value) {
+ val route = Screens.MEMO.getNavigation(
+ mapOf("user_id" to id.value)
+ )
+ navigator.navigate(route)
+ },
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = "로그인",
+ style = TilTheme.text.h4_B,
+ color = TilTheme.color.white
+ )
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ viewModel.start()
+
+ }
+}
+
+// @Preview(showBackground = true)
+@Composable
+private fun Example() {
+ MaterialTheme {
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .addFocusCleaner(LocalFocusManager.current),
+ color = TilTheme.color.white
+ ) {
+ LoginScreen(navigator = rememberNavController())
+ }
+ }
+}
+
diff --git a/features/compose_navigation/src/main/java/com/features/compose_navigation/screens/login/LoginViewModel.kt b/features/compose_navigation/src/main/java/com/features/compose_navigation/screens/login/LoginViewModel.kt
new file mode 100644
index 00000000..9b1db538
--- /dev/null
+++ b/features/compose_navigation/src/main/java/com/features/compose_navigation/screens/login/LoginViewModel.kt
@@ -0,0 +1,45 @@
+package com.features.compose_navigation.screens.login
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.hmju.core.ui.stateIn
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+/**
+ * Description : 로그인 화면 관련 ViewModel
+ *
+ * Created by juhongmin on 4/7/24
+ */
+@HiltViewModel
+class LoginViewModel @Inject constructor(
+ private val savedStateHandle: SavedStateHandle
+) : ViewModel() {
+
+ val id: MutableStateFlow by lazy { MutableStateFlow("") }
+ val password: MutableStateFlow by lazy { MutableStateFlow("") }
+ val loginEnable: StateFlow
+ get() = id.combine(password) { id, pw ->
+ isValidateId(id) && isValidatePw(pw)
+ }.stateIn(false, viewModelScope)
+
+ fun start() {
+ viewModelScope.launch {
+ savedStateHandle.get("user_id")?.let { id.emit(it) }
+ savedStateHandle.get("user_pw")?.let { password.emit(it) }
+ }
+ }
+
+ private fun isValidateId(id: String): Boolean {
+ return id.length > 4
+ }
+
+ private fun isValidatePw(pw: String): Boolean {
+ return pw.length > 8
+ }
+}
diff --git a/features/compose_navigation/src/main/java/com/features/compose_navigation/screens/login/SignUpScreen.kt b/features/compose_navigation/src/main/java/com/features/compose_navigation/screens/login/SignUpScreen.kt
new file mode 100644
index 00000000..303365cb
--- /dev/null
+++ b/features/compose_navigation/src/main/java/com/features/compose_navigation/screens/login/SignUpScreen.kt
@@ -0,0 +1,122 @@
+package com.features.compose_navigation.screens.login
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavHostController
+import com.features.compose_navigation.Screens
+import com.hmju.core.compose.TilComponent
+import com.hmju.core.compose.TilTheme
+import com.hmju.core.compose.backPressed
+import com.hmju.core.compose.collectAsMutableState
+
+/**
+ * Description : 회원 가입
+ *
+ * Created by juhongmin on 4/22/24
+ */
+
+@Composable
+fun SignUpScreen(
+ navigator: NavHostController,
+ viewModel: SignUpViewModel = hiltViewModel()
+) {
+ val id = viewModel.id.collectAsMutableState()
+ val pw = viewModel.password.collectAsMutableState()
+ val pwConfirm = viewModel.passwordConfirm.collectAsMutableState()
+ val isSignUpEnable = viewModel.isSignUpEnable.collectAsState()
+ val scrollState = rememberScrollState()
+
+ TilComponent.HeaderAndContentsColumn(
+ title = "회원 가입",
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(start = 16.dp, end = 16.dp),
+ backClick = { navigator.backPressed() },
+ scrollState = scrollState
+ ) {
+ TilComponent.ImageLoader(
+ imageUrl = "https://til.qtzz.synology.me/resources/img/20240507/1715084116936.png",
+ modifier = Modifier
+ .size(150.dp, 150.dp)
+ .padding(30.dp)
+ .clip(RoundedCornerShape(150.dp))
+ )
+ TilComponent.EditText(
+ text = id,
+ labelText = "아이디",
+ placeHolderText = "아이디 입력",
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
+ nextAction = FocusDirection.Next
+ )
+ Spacer(Modifier.height(20.dp))
+ TilComponent.EditText(
+ text = pw,
+ labelText = "비밀번호",
+ placeHolderText = "비밀번호 입력",
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
+ nextAction = FocusDirection.Next
+ )
+ Spacer(Modifier.height(5.dp))
+ TilComponent.EditText(
+ text = pwConfirm,
+ labelText = "비밀번호 확인",
+ placeHolderText = "비밀번호 재 입력",
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
+ )
+ Spacer(Modifier.height(50.dp))
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp)
+ .clip(RoundedCornerShape(6.dp))
+ .then(
+ if (isSignUpEnable.value) {
+ Modifier.background(TilTheme.color.blue)
+ } else {
+ Modifier.background(TilTheme.color.gray4)
+ }
+ )
+ .clickable(isSignUpEnable.value) {
+ val route = Screens.LOGIN.getNavigation(
+ mapOf(
+ "user_id" to id.value,
+ "user_pw" to pw.value
+ )
+ )
+ navigator.navigate(route) {
+// popUpTo(Screens.SIGNUP.destination) {
+// inclusive = true
+// }
+ }
+ },
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = "회원 가입",
+ style = TilTheme.text.h4_B,
+ color = if (isSignUpEnable.value) TilTheme.color.white else TilTheme.color.black
+ )
+ }
+ Spacer(Modifier.height(50.dp))
+ }
+}
\ No newline at end of file
diff --git a/features/compose_navigation/src/main/java/com/features/compose_navigation/screens/login/SignUpViewModel.kt b/features/compose_navigation/src/main/java/com/features/compose_navigation/screens/login/SignUpViewModel.kt
new file mode 100644
index 00000000..cb78b644
--- /dev/null
+++ b/features/compose_navigation/src/main/java/com/features/compose_navigation/screens/login/SignUpViewModel.kt
@@ -0,0 +1,38 @@
+package com.features.compose_navigation.screens.login
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.hmju.core.ui.stateIn
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import javax.inject.Inject
+
+/**
+ * Description : 회원 가입 ViewModel
+ *
+ * Created by juhongmin on 4/22/24
+ */
+@HiltViewModel
+class SignUpViewModel @Inject constructor(
+
+) : ViewModel() {
+
+ val id: MutableStateFlow by lazy { MutableStateFlow("qewrt") }
+ val password: MutableStateFlow by lazy { MutableStateFlow("123456789") }
+ val passwordConfirm: MutableStateFlow by lazy { MutableStateFlow("123456789") }
+ val isSignUpEnable: StateFlow
+ get() = combine(id, password, passwordConfirm) { id, pw, pwConfig ->
+ isValidateId(id) && isValidatePw(pw) && pw == pwConfig
+ }.stateIn(false, viewModelScope)
+
+ private fun isValidateId(id: String): Boolean {
+ return id.length > 4
+ }
+
+ private fun isValidatePw(pw: String): Boolean {
+ return pw.length > 8
+ }
+}
diff --git a/features/compose_navigation/src/main/java/com/features/compose_navigation/screens/memo/MemoScreen.kt b/features/compose_navigation/src/main/java/com/features/compose_navigation/screens/memo/MemoScreen.kt
new file mode 100644
index 00000000..aaaf503d
--- /dev/null
+++ b/features/compose_navigation/src/main/java/com/features/compose_navigation/screens/memo/MemoScreen.kt
@@ -0,0 +1,162 @@
+package com.features.compose_navigation.screens.memo
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavHostController
+import com.hmju.core.compose.TilComponent
+import com.hmju.core.compose.TilTheme
+import com.hmju.core.compose.backPressed
+import timber.log.Timber
+import kotlin.math.roundToInt
+
+/**
+ * Description : 메모 화면
+ *
+ * Created by juhongmin on 5/6/24
+ */
+@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
+@Composable
+fun MemoScreen(
+ navigator: NavHostController,
+ viewModel: MemoViewModel = hiltViewModel()
+) {
+ val scrollState = rememberLazyListState()
+ val headerTitle = viewModel.userId.collectAsState()
+ val uiList = viewModel.dataList.collectAsState()
+ val density = LocalDensity.current
+
+ // 헤더에서 스크롤
+ var appBarHeight = remember { 0.dp } // 전체 헤더 높이값
+ val collapseHeight = 60.dp // 접혔을때 높이값
+ var expandHeightPx = 0F
+ var appbarOffsetHeightPx by remember { mutableFloatStateOf(0f) }
+
+ val nestedScrollConnection = remember {
+ object : NestedScrollConnection {
+
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ appbarOffsetHeightPx += available.y
+ // Timber.d("onPreScroll ${available.y} $appbarOffsetHeightPx")
+ return Offset.Zero
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ appbarOffsetHeightPx -= available.y
+ return Offset.Zero
+ }
+ }
+ }
+
+ TilComponent.HeaderAndContentsBox(
+ title = "메모",
+ backClick = { navigator.backPressed() },
+ modifier = Modifier
+ .fillMaxWidth()
+ .nestedScroll(nestedScrollConnection)
+ ) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize(),
+ state = scrollState,
+ contentPadding = PaddingValues(top = appBarHeight.plus(30.dp))
+ ) {
+ itemsIndexed(
+ items = uiList.value,
+ key = { idx, _ -> idx },
+ contentType = { _, item -> item.getType() },
+ itemContent = { _, item -> item.GetUi { viewModel.setClickEvent(it) } }
+ )
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .onGloballyPositioned { coordinates ->
+ if (appBarHeight == 0.dp) {
+ appBarHeight = with(density) { coordinates.size.height.toDp() }
+ expandHeightPx = with(density) {
+ appBarHeight
+ .minus(collapseHeight)
+ .roundToPx()
+ .toFloat()
+ }
+ }
+ }
+ .offset {
+ IntOffset(
+ x = 0,
+ y = appbarOffsetHeightPx
+ .coerceIn(-expandHeightPx, 0f)
+ .roundToInt()
+ )
+ }
+ .background(TilTheme.color.defBgColor)
+ ) {
+ TilComponent.ImageLoader(
+ imageUrl = "https://til.qtzz.synology.me/resources/img/20240507/1715084116936.png",
+ modifier = Modifier
+ .padding(30.dp)
+ .size(150.dp, 150.dp)
+ .clip(RoundedCornerShape(150.dp))
+ )
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(collapseHeight)
+ .background(TilTheme.color.defBgColor),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = headerTitle.value,
+ modifier = Modifier
+ .fillMaxWidth(),
+ color = TilTheme.color.black,
+ textAlign = TextAlign.Center
+ )
+ }
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ viewModel.start()
+ }
+}
\ No newline at end of file
diff --git a/features/compose_navigation/src/main/java/com/features/compose_navigation/screens/memo/MemoViewModel.kt b/features/compose_navigation/src/main/java/com/features/compose_navigation/screens/memo/MemoViewModel.kt
new file mode 100644
index 00000000..aac157ec
--- /dev/null
+++ b/features/compose_navigation/src/main/java/com/features/compose_navigation/screens/memo/MemoViewModel.kt
@@ -0,0 +1,88 @@
+package com.features.compose_navigation.screens.memo
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.features.compose_navigation.models.MemoClickEvent
+import com.features.compose_navigation.models.MemoUiModel
+import com.features.compose_navigation.usecase.GetMemoUseCase
+import com.hmju.core.compose.MutableStateFlowList
+import com.hmju.core.ui.observer
+import com.hmju.core.ui.stateIn
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import java.text.SimpleDateFormat
+import java.util.Locale
+import javax.inject.Inject
+
+/**
+ * Description :
+ *
+ * Created by juhongmin on 5/6/24
+ */
+@HiltViewModel
+class MemoViewModel @Inject constructor(
+ private val getMemoUseCase: GetMemoUseCase,
+ private val savedStateHandle: SavedStateHandle
+) : ViewModel() {
+
+ private val _userId: MutableStateFlow by lazy {
+ MutableStateFlow(savedStateHandle["id"] ?: "")
+ }
+ val userId: StateFlow
+ get() = _userId.observer({
+ "반갑습니다. $it 님"
+ }, "로딩중..", viewModelScope)
+
+ private val _dataList: MutableStateFlowList by lazy { MutableStateFlowList() }
+ val dataList: StateFlow> get() = _dataList.stateFlow
+ private val _clickEvent: MutableStateFlow by lazy {
+ MutableStateFlow(MemoClickEvent.Init)
+ }
+
+ @OptIn(FlowPreview::class)
+ val clickEvent: StateFlow
+ get() = _clickEvent
+ .debounce(200)
+ .stateIn(MemoClickEvent.Init, viewModelScope)
+
+ fun start() {
+ handleRandomTitle()
+ reqMemoList()
+ }
+
+ private fun handleRandomTitle() {
+ val originId = savedStateHandle.get("user_id") ?: return
+ val sdf = SimpleDateFormat("mm분 ss초", Locale.getDefault())
+
+ viewModelScope.launch {
+ repeat(300) {
+ delay(1000)
+ _userId.emit("$originId ${sdf.format(System.currentTimeMillis())}")
+ }
+ }
+ }
+
+ private fun reqMemoList() {
+ // _dataList.add(MemoUiModel.Empty)
+ getMemoUseCase()
+ .onStart { }
+ .onEach { _dataList.addAll(it) }
+ .catch { Timber.d("ERROR $it") }
+ .launchIn(viewModelScope)
+ }
+
+ fun setClickEvent(newEvent: MemoClickEvent) {
+ _clickEvent.value = newEvent
+ }
+}
diff --git a/features/compose_navigation/src/main/java/com/features/compose_navigation/usecase/GetMemoUseCase.kt b/features/compose_navigation/src/main/java/com/features/compose_navigation/usecase/GetMemoUseCase.kt
new file mode 100644
index 00000000..5d39b3a5
--- /dev/null
+++ b/features/compose_navigation/src/main/java/com/features/compose_navigation/usecase/GetMemoUseCase.kt
@@ -0,0 +1,35 @@
+package com.features.compose_navigation.usecase
+
+import com.features.compose_navigation.ApiService
+import com.features.compose_navigation.models.MemoUiModel
+import com.hmju.core.models.base.JSendList
+import com.hmju.core.models.base.getOrDefault
+import com.hmju.core.models.params.PagingQueryParams
+import com.hmju.core.network.NetworkExtensions.zip
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import javax.inject.Inject
+
+/**
+ * Description :
+ *
+ * Created by juhongmin on 5/6/24
+ */
+class GetMemoUseCase @Inject constructor(
+ private val apiService: ApiService
+) {
+ operator fun invoke(): Flow> {
+ return flow {
+ val queryMap = PagingQueryParams()
+ zip(
+ { apiService.fetchMemo(queryMap.getQueryMap()) },
+ { apiService.fetchUpload(queryMap.getQueryMap()) }
+ ) { memoRes, fileRes ->
+ val fileList = fileRes.list.filter { it.mimeType.startsWith("image") }
+ memoRes.list.mapIndexed { idx, entity ->
+ MemoUiModel.Item(entity, fileList.getOrNull(idx))
+ }
+ }.onSuccess { emit(it) }.onFailure { error(it) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/compose_navigation_bridge/.gitignore b/features/compose_navigation_bridge/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/features/compose_navigation_bridge/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/features/compose_navigation_bridge/build.gradle.kts b/features/compose_navigation_bridge/build.gradle.kts
new file mode 100644
index 00000000..8341a55c
--- /dev/null
+++ b/features/compose_navigation_bridge/build.gradle.kts
@@ -0,0 +1,8 @@
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ namespace = "com.features.compose_navigation_bridge"
+}
\ No newline at end of file
diff --git a/features/compose_navigation_bridge/consumer-rules.pro b/features/compose_navigation_bridge/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/features/compose_navigation_bridge/proguard-rules.pro b/features/compose_navigation_bridge/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/features/compose_navigation_bridge/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/features/compose_navigation_bridge/src/main/AndroidManifest.xml b/features/compose_navigation_bridge/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/features/compose_navigation_bridge/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/features/compose_navigation_bridge/src/main/java/com/features/compose_navigation_bridge/ComposeNavigationBridge.kt b/features/compose_navigation_bridge/src/main/java/com/features/compose_navigation_bridge/ComposeNavigationBridge.kt
new file mode 100644
index 00000000..5b732c30
--- /dev/null
+++ b/features/compose_navigation_bridge/src/main/java/com/features/compose_navigation_bridge/ComposeNavigationBridge.kt
@@ -0,0 +1,10 @@
+package com.features.compose_navigation_bridge
+
+/**
+ * Description :
+ *
+ * Created by juhongmin on 4/6/24
+ */
+interface ComposeNavigationBridge {
+ fun moveToPage()
+}
diff --git a/features/compose_permissions_result/build.gradle.kts b/features/compose_permissions_result/build.gradle.kts
index 9ff5a3bd..c273b112 100644
--- a/features/compose_permissions_result/build.gradle.kts
+++ b/features/compose_permissions_result/build.gradle.kts
@@ -42,7 +42,6 @@ dependencies {
implementation(Compose.constraint)
implementation(Compose.tracing)
implementation(Compose.activity)
- implementation(Glide.compose)
// implementation("com.google.accompanist:accompanist-permissions:0.23.1")
/**
diff --git a/features/main/build.gradle.kts b/features/main/build.gradle.kts
index 1fd19e41..0d07cbdd 100644
--- a/features/main/build.gradle.kts
+++ b/features/main/build.gradle.kts
@@ -20,6 +20,7 @@ dependencies {
implementation(project(":features:async_migrate_bridge"))
implementation(project(":features:compose-ui-bridge"))
implementation(project(":features:compose_permissions_result_bridge"))
+ implementation(project(":features:compose_navigation_bridge"))
/**
* Android X
diff --git a/features/main/src/main/java/com/features/main/MainViewModel.kt b/features/main/src/main/java/com/features/main/MainViewModel.kt
index f1657739..718f0215 100644
--- a/features/main/src/main/java/com/features/main/MainViewModel.kt
+++ b/features/main/src/main/java/com/features/main/MainViewModel.kt
@@ -2,6 +2,7 @@ package com.features.main
import com.features.async_migrate_bridge.AsyncMigrateBridge
import com.features.base_mvvm_bridge.BaseMvvmBridge
+import com.features.compose_navigation_bridge.ComposeNavigationBridge
import com.features.compose_ui_bridge.ComposeUiBridge
import com.features.network_bridge.NetworkBridge
import com.features.recyclerview_bridge.RecyclerViewBridge
@@ -22,7 +23,8 @@ class MainViewModel @Inject constructor(
private val mvvmRequirements: BaseMvvmBridge,
private val asyncMigrateBridge: AsyncMigrateBridge,
private val composeUiBridge: ComposeUiBridge,
- private val composePermissionsResultBridge: ComposePermissionsResultBridge
+ private val composePermissionsResultBridge: ComposePermissionsResultBridge,
+ private val composeNavigationBridge: ComposeNavigationBridge
) : ActivityViewModel() {
fun moveToNetworkPage() {
@@ -52,4 +54,8 @@ class MainViewModel @Inject constructor(
fun moveToPermissionsResultPage() {
composePermissionsResultBridge.moveToPage()
}
+
+ fun moveToComposeNavigationPage() {
+ composeNavigationBridge.moveToPage()
+ }
}
diff --git a/features/main/src/main/res/layout/activity_main.xml b/features/main/src/main/res/layout/activity_main.xml
index 403fe7b5..7b50444b 100644
--- a/features/main/src/main/res/layout/activity_main.xml
+++ b/features/main/src/main/res/layout/activity_main.xml
@@ -71,6 +71,13 @@
android:onClick="@{()->vm.moveToPermissionsResultPage()}"
android:text="Compose Permissions" />
+
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 2ceeddda..004074f8 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -42,3 +42,5 @@ include(":features:base_mvvm_bottom_sheet")
include(":features:base_mvvm_bottom_sheet_bridge")
include(":features:compose_permissions_result")
include(":features:compose_permissions_result_bridge")
+include(":features:compose_navigation")
+include(":features:compose_navigation_bridge")
diff --git a/test/build.gradle.kts b/test/build.gradle.kts
index b3b9647b..231354aa 100644
--- a/test/build.gradle.kts
+++ b/test/build.gradle.kts
@@ -55,6 +55,8 @@ dependencies {
androidTestImplementation(project(":features:base_mvvm_lifecycle_bridge"))
androidTestImplementation(project(":features:base_mvvm_bottom_sheet"))
androidTestImplementation(project(":features:base_mvvm_bottom_sheet_bridge"))
+ androidTestImplementation(project(":features:compose_navigation"))
+ androidTestImplementation(project(":features:compose_navigation_bridge"))
/**
* Android X
diff --git a/test/src/test/java/com/hmju/test/ExampleUnitTest.kt b/test/src/test/java/com/hmju/test/ExampleUnitTest.kt
index bd346159..c5a08a0b 100644
--- a/test/src/test/java/com/hmju/test/ExampleUnitTest.kt
+++ b/test/src/test/java/com/hmju/test/ExampleUnitTest.kt
@@ -78,4 +78,9 @@ class ExampleUnitTest {
println("Kotlinx ${sdf.format(res.getDate())}")
return res.getDate().time
}
+
+ @Test
+ fun calculateTest(){
+ // var offset = Math.min(1F,1- )
+ }
}
\ No newline at end of file