diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index c4761c66..3a43d5b9 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -6,7 +6,7 @@
## ๐ท ์คํฌ๋ฆฐ์ท
-
+
## โ๏ธ ์ฌ์ฉ๋ฒ
diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml
new file mode 100644
index 00000000..328bf5a0
--- /dev/null
+++ b/.github/workflows/android-ci.yml
@@ -0,0 +1,47 @@
+name: Android CI
+
+on:
+ push: # ์ฝ๋ ํธ์ ์ด๋ฒคํธ์ ๋ํ ์ค์
+ branches: [ "develop" ] # "develop" ๋ธ๋์น์ ํธ์๋ ๋๋ง ํธ๋ฆฌ๊ฑฐ๋๋ค.
+ pull_request: # ํ ๋ฆฌํ์คํธ ์ด๋ฒคํธ์ ๋ํ ์ค์
+ branches: [ "develop" ] # "develop" ๋ธ๋์น๋ก์ ํ ๋ฆฌํ์คํธ๊ฐ ์์ฑ๋ ๋๋ง ํธ๋ฆฌ๊ฑฐ๋๋ค.
+
+jobs: # CI์์ ์ํํ ์์
์ ์ ์ํ๋ค.
+ ci-build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: gradle
+
+ - name: Cache Gradle packages
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/buildSrc/**/*.kt') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ - name: Generate local.properties
+ run: |
+ echo '${{ secrets.LOCAL_PROPERTIES }}' >> ./local.properties
+
+ - name: Generate google-services.json
+ run: |
+ echo '${{ secrets.GOOGLE_SERVICES }}' >> ./app/google-services.json
+
+# - name: Code style checks
+# run: |
+# ./gradlew detekt
+
+ - name: Run build
+ run: ./gradlew assembleDebug --stacktrace
+
diff --git a/.github/workflows/firebase-app-distribution-debug.yml b/.github/workflows/firebase-app-distribution-debug.yml
new file mode 100644
index 00000000..3e93757b
--- /dev/null
+++ b/.github/workflows/firebase-app-distribution-debug.yml
@@ -0,0 +1,63 @@
+name: Build & upload to Firebase App Distribution
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ cd-build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+
+ - name: Cache Gradle packages
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/buildSrc/**/*.kt') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
+ - name: Prepare keystore dir
+ run: mkdir -p keystore
+
+ - name: Decode And Save Keystore Base64
+ run: |
+ echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > keystore/keystore.jks
+
+ - name: Decode And Save Debug Keystore Base64
+ run: |
+ echo "${{ secrets.DEBUG_KEYSTORE_BASE64 }}" | base64 -d > debug.keystore
+
+ - name: Generate local.properties
+ run: |
+ echo '${{ secrets.LOCAL_PROPERTIES }}' >> ./local.properties
+
+ - name: Generate google-services.json
+ run: |
+ echo '${{ secrets.GOOGLE_SERVICES }}' >> ./app/google-services.json
+
+ - name: Build debug APK
+ run: ./gradlew assembleDebug
+
+ - name: Upload artifact to Firebase App Distribution
+ uses: wzieba/Firebase-Distribution-Github-Action@v1
+ with:
+ appId: ${{ secrets.FIREBASE_APP_ID }}
+ serviceCredentialsFileContent: ${{ secrets.CREDENTIAL_FILE_CONTENT }}
+ groups: testers
+ file: app/build/outputs/apk/debug/app-debug.apk
+# releaseNotes: ${{ steps.firebase_release_note.outputs.notes }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index b7197f62..4a5682ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -168,3 +168,5 @@ fabric.properties
### AndroidStudio Patch ###
!/gradle/wrapper/gradle-wrapper.jar
+/app/debug/output-metadata.json
+/app/build/outputs/**/output-metadata.json
diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml
new file mode 100644
index 00000000..decbbbe0
--- /dev/null
+++ b/.idea/appInsightsSettings.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 00000000..b86273d9
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 00000000..d7fd201e
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 00000000..7b3006b6
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index cde3e199..7061a0d6 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -49,6 +49,10 @@
+
+
+
+
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 00000000..6d0ee1c2
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 00000000..f8051a6f
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 36072dca..fee46db1 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,13 +1,12 @@
-
-<<<<<<< Updated upstream
-
-
+
+
+
+
+
+
-=======
-
->>>>>>> Stashed changes
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 00000000..16660f1d
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000..94a25f7f
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 41d41492..fc4156bb 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,12 +1,22 @@
+import java.util.Properties
+
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.firebase.crashlytics)
+ id("com.google.dagger.hilt.android") // Hilt ํ๋ฌ๊ทธ์ธ ์ถ๊ฐ
+ kotlin("kapt") // Hilt๋ฅผ ์ํ kapt ์ถ๊ฐ
+}
+
+val properties = Properties().apply {
+ load(project.rootProject.file("local.properties").inputStream())
}
android {
namespace = "com.kuit.ourmenu"
- compileSdk = 34
+ compileSdk = 35
defaultConfig {
applicationId = "com.kuit.ourmenu"
@@ -16,6 +26,23 @@ android {
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ buildConfigField("String", "BASE_URL", "\"${properties["BASE_URL"]}\"")
+
+ manifestPlaceholders["KAKAO_APP_KEY"] = properties["KAKAO_APP_KEY"].toString()
+ buildConfigField("String", "KAKAO_APP_KEY", properties["KAKAO_APP_KEY"].toString())
+
+ }
+
+ signingConfigs {
+ val debugKeystore = file("$rootDir/debug.keystore")
+ if (debugKeystore.exists()) {
+ getByName("debug") {
+ storeFile = debugKeystore
+ storePassword = "android"
+ keyAlias = "androiddebugkey"
+ keyPassword = "android"
+ }
+ }
}
buildTypes {
@@ -36,6 +63,7 @@ android {
}
buildFeatures {
compose = true
+ buildConfig = true
}
}
@@ -49,6 +77,12 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
+ implementation(libs.androidx.lifecycle.viewmodel.compose)
+ implementation(libs.androidx.lifecycle.runtime.compose.android)
+ implementation(libs.androidx.navigation.compose)
+ implementation(libs.androidx.datastore.preferences)
+ implementation(libs.androidx.espresso.core)
+ implementation(libs.play.services.location)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
@@ -56,4 +90,41 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
+ implementation(libs.kotlinx.collections.immutable)
+
+ // Dots Indicator
+ implementation("com.tbuonomo:dotsindicator:5.1.0")
+
+ // Hilt
+ implementation(libs.hilt.android)
+ kapt(libs.hilt.compiler)
+ implementation(libs.androidx.hilt.navigation.compose)
+
+ // Network
+ implementation(platform(libs.okhttp.bom))
+ implementation(libs.okhttp)
+ implementation(libs.okhttp.logging.interceptor)
+ implementation(libs.retrofit)
+ implementation(libs.retrofit.kotlin.serialization.converter)
+ implementation(libs.kotlinx.serialization.json)
+
+ // Kakao SDK
+ implementation("com.kakao.sdk:v2-all:2.20.6")
+ implementation("com.kakao.sdk:v2-user:2.20.6") // ์นด์นด์ค ๋ก๊ทธ์ธ API ๋ชจ๋
+ implementation("com.kakao.maps.open:android:2.12.8") // ์นด์นด์ค ๋งต API
+
+ // coil
+ implementation(libs.coil.compose)
+ implementation(libs.coil.network.okhttp)
+ implementation(libs.coil.svg)
+
+ // Firebase
+ implementation(platform(libs.firebase.bom))
+ implementation(libs.firebase.crashlytics.ndk)
+ implementation(libs.firebase.analytics)
+}
+
+// Hilt๋ฅผ ์ฌ์ฉํ ๋ ํ์ํ Annotation Processor
+kapt {
+ correctErrorTypes = true
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ecf4e2b8..c43d8c45 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,8 +2,15 @@
+
+
+
+
+
+
diff --git a/app/src/main/java/com/kuit/ourmenu/MainActivity.kt b/app/src/main/java/com/kuit/ourmenu/MainActivity.kt
index b87fc428..40116d9e 100644
--- a/app/src/main/java/com/kuit/ourmenu/MainActivity.kt
+++ b/app/src/main/java/com/kuit/ourmenu/MainActivity.kt
@@ -4,44 +4,64 @@ 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.foundation.background
+import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.tooling.preview.Preview
+import com.kuit.ourmenu.ui.navigator.MainNavHost
+import com.kuit.ourmenu.ui.navigator.MainTab
+import com.kuit.ourmenu.ui.navigator.component.MainBottomBar
+import com.kuit.ourmenu.ui.navigator.rememberMainNavigator
+import androidx.navigation.compose.rememberNavController
+import coil3.imageLoader
+import com.kuit.ourmenu.ui.onboarding.screen.SplashScreen
+import com.kuit.ourmenu.ui.theme.NeutralWhite
import com.kuit.ourmenu.ui.theme.OurMenuTheme
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.collections.immutable.toPersistentList
+@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
+ var showSplash by remember { mutableStateOf(true) }
+ val navController = rememberMainNavigator()
+
OurMenuTheme {
- Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
- Greeting(
- name = "Android",
- modifier = Modifier.padding(innerPadding)
+ if (showSplash) {
+ SplashScreen(
+ imageLoader = imageLoader,
+ ) {
+ showSplash = false
+ }
+ } else {
+ Scaffold(
+ bottomBar = {
+ MainBottomBar(
+ modifier = Modifier
+ .background(NeutralWhite)
+ .navigationBarsPadding(),
+ visible = navController.shouldShowBottomBar(),
+ tabs = MainTab.entries.toPersistentList(),
+ currentTab = navController.currentTab,
+ onTabSelected = { navController.navigate(it) }
+ )
+ },
+ content = { innerPadding ->
+ MainNavHost(
+ navController = navController,
+ padding = innerPadding
+ )
+ }
)
}
}
}
}
}
-
-@Composable
-fun Greeting(name: String, modifier: Modifier = Modifier) {
- Text(
- text = "Hello $name!",
- modifier = modifier
- )
-}
-
-@Preview(showBackground = true)
-@Composable
-fun GreetingPreview() {
- OurMenuTheme {
- Greeting("Android")
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/OurMenuApp.kt b/app/src/main/java/com/kuit/ourmenu/OurMenuApp.kt
new file mode 100644
index 00000000..fd9f2a6b
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/OurMenuApp.kt
@@ -0,0 +1,38 @@
+package com.kuit.ourmenu
+
+import android.app.Application
+import android.util.Log
+import coil3.ImageLoader
+import coil3.PlatformContext
+import coil3.SingletonImageLoader
+import coil3.memory.MemoryCache
+import coil3.util.DebugLogger
+import com.kakao.sdk.common.KakaoSdk
+import com.kakao.sdk.common.util.Utility
+import com.kakao.vectormap.KakaoMapSdk
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class OurMenuApp : Application(), SingletonImageLoader.Factory {
+ override fun onCreate() {
+ super.onCreate()
+ KakaoSdk.init(this, BuildConfig.KAKAO_APP_KEY)
+ KakaoMapSdk.init(this, BuildConfig.KAKAO_APP_KEY)
+ // ์ด ๋ถ๋ถ์ ์ฃผ์ ํด์ ํด์ keyHash ๊ฐ ์ฝ์ผ์๋ฉด ๋ฉ๋๋ค
+ val keyHash = Utility.getKeyHash(this)
+ Log.d("KeyHash", keyHash)
+
+
+ }
+
+ override fun newImageLoader(context: PlatformContext): ImageLoader {
+ return ImageLoader.Builder(context)
+ .logger(DebugLogger())
+ .memoryCache(
+ MemoryCache.Builder()
+ .maxSizePercent(context, 0.35)
+ .build()
+ )
+ .build()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/di/NetworkModule.kt b/app/src/main/java/com/kuit/ourmenu/data/di/NetworkModule.kt
new file mode 100644
index 00000000..55f57027
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/di/NetworkModule.kt
@@ -0,0 +1,74 @@
+package com.kuit.ourmenu.data.di
+
+import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
+import com.kuit.ourmenu.BuildConfig
+import com.kuit.ourmenu.utils.auth.AuthInterceptor
+import com.kuit.ourmenu.utils.auth.TokenAuthenticator
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.json.Json
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import java.util.concurrent.TimeUnit
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object NetworkModule {
+
+ @OptIn(ExperimentalSerializationApi::class)
+ @Provides
+ @Singleton
+ fun provideJson(): Json =
+ Json {
+ isLenient = true // ์ ์ฐํ JSON ๊ตฌ๋ถ ํ์ฉ
+ prettyPrint = true // ์ถ๋ ฅ JSON ์ ์์๊ฒ ๋ค์ฌ์ฐ๊ธฐํด์ ๊ฐ๋
์ฑ์ ๋์
+ encodeDefaults = true // ํ๋ผ๋ฏธํฐ์ ๊ธฐ๋ณธ๊ฐ(default) ์ JSON ์ผ๋ก ์ธ์ฝ๋ฉ
+ explicitNulls = false // null ๊ฐ์ ๋ช
์์ ์ผ๋ก ํ์ํ์ง ์์
+ ignoreUnknownKeys = true // JSON ์ ์ ์ํ์ง ์์ ํค๊ฐ ์์ด๋ ๋ฌด์ํ๊ณ ํ์ฑ
+ }
+
+ @Provides
+ @Singleton
+ fun providesOkHttpClient(
+ loggingInterceptor: HttpLoggingInterceptor,
+ authInterceptor: AuthInterceptor,
+ authAuthenticator: TokenAuthenticator
+ ): OkHttpClient =
+ OkHttpClient.Builder().apply {
+ connectTimeout(10, TimeUnit.SECONDS)
+ writeTimeout(10, TimeUnit.SECONDS)
+ readTimeout(10, TimeUnit.SECONDS)
+ addInterceptor(loggingInterceptor)
+ addInterceptor(authInterceptor)
+ authenticator(authAuthenticator)
+ }.build()
+
+ @Provides
+ @Singleton
+ fun providesLoggingInterceptor(): HttpLoggingInterceptor =
+ HttpLoggingInterceptor().apply {
+ level = HttpLoggingInterceptor.Level.BODY
+ }
+
+ @Provides
+ @Singleton
+ fun providesRetrofit(
+ json: Json,
+ okHttpClient: OkHttpClient
+ ): Retrofit =
+ Retrofit.Builder()
+ .baseUrl(BuildConfig.BASE_URL)
+ .client(okHttpClient)
+ .addConverterFactory(
+ json.asConverterFactory(requireNotNull("application/json".toMediaType()))
+ )
+ .client(okHttpClient)
+ .build()
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/di/ServiceModule.kt b/app/src/main/java/com/kuit/ourmenu/data/di/ServiceModule.kt
new file mode 100644
index 00000000..21fecad8
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/di/ServiceModule.kt
@@ -0,0 +1,55 @@
+package com.kuit.ourmenu.data.di
+
+import com.kuit.ourmenu.data.service.AuthService
+import com.kuit.ourmenu.data.service.CacheService
+import com.kuit.ourmenu.data.service.DummyService
+import com.kuit.ourmenu.data.service.MapService
+import com.kuit.ourmenu.data.service.MenuFolderService
+import com.kuit.ourmenu.data.service.MenuInfoService
+import com.kuit.ourmenu.data.service.UserService
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import retrofit2.Retrofit
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object ServiceModule {
+
+ @Provides
+ @Singleton
+ fun providesService(retrofit: Retrofit): DummyService =
+ retrofit.create(DummyService::class.java)
+
+ @Provides
+ @Singleton
+ fun providesAuthService(retrofit: Retrofit): AuthService =
+ retrofit.create(AuthService::class.java)
+
+ @Provides
+ @Singleton
+ fun providesUserService(retrofit: Retrofit): UserService =
+ retrofit.create(UserService::class.java)
+
+ @Provides
+ @Singleton
+ fun providesMapService(retrofit: Retrofit): MapService =
+ retrofit.create(MapService::class.java)
+
+ @Provides
+ @Singleton
+ fun providesCacheService(retrofit: Retrofit): CacheService =
+ retrofit.create(CacheService::class.java)
+
+ @Provides
+ @Singleton
+ fun provideMenuFolderService(retrofit: Retrofit): MenuFolderService =
+ retrofit.create(MenuFolderService::class.java)
+
+ @Provides
+ @Singleton
+ fun provideMenuInfoService(retrofit: Retrofit): MenuInfoService =
+ retrofit.create(MenuInfoService::class.java)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/auth/SignInType.kt b/app/src/main/java/com/kuit/ourmenu/data/model/auth/SignInType.kt
new file mode 100644
index 00000000..146f51ff
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/auth/SignInType.kt
@@ -0,0 +1,6 @@
+package com.kuit.ourmenu.data.model.auth
+
+enum class SignInType {
+ EMAIL,
+ KAKAO,
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/auth/request/ConfirmCodeRequest.kt b/app/src/main/java/com/kuit/ourmenu/data/model/auth/request/ConfirmCodeRequest.kt
new file mode 100644
index 00000000..acbdb15c
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/auth/request/ConfirmCodeRequest.kt
@@ -0,0 +1,12 @@
+package com.kuit.ourmenu.data.model.auth.request
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ConfirmCodeRequest(
+ @SerialName("confirmCode")
+ val confirmCode: String,
+ @SerialName("email")
+ val email: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/auth/request/EmailRequest.kt b/app/src/main/java/com/kuit/ourmenu/data/model/auth/request/EmailRequest.kt
new file mode 100644
index 00000000..f857de43
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/auth/request/EmailRequest.kt
@@ -0,0 +1,11 @@
+package com.kuit.ourmenu.data.model.auth.request
+
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class EmailRequest(
+ @SerialName("email")
+ val email: String?,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/auth/request/LoginRequest.kt b/app/src/main/java/com/kuit/ourmenu/data/model/auth/request/LoginRequest.kt
new file mode 100644
index 00000000..96b0e760
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/auth/request/LoginRequest.kt
@@ -0,0 +1,15 @@
+package com.kuit.ourmenu.data.model.auth.request
+
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class LoginRequest(
+ @SerialName("email")
+ val email: String,
+ @SerialName("password")
+ val password: String?,
+ @SerialName("signInType")
+ val signInType: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/auth/request/SignupRequest.kt b/app/src/main/java/com/kuit/ourmenu/data/model/auth/request/SignupRequest.kt
new file mode 100644
index 00000000..56a23189
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/auth/request/SignupRequest.kt
@@ -0,0 +1,16 @@
+package com.kuit.ourmenu.data.model.auth.request
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class SignupRequest(
+ @SerialName("email")
+ val email: String?,
+ @SerialName("mealTime")
+ val mealTime: List,
+ @SerialName("password")
+ val password: String?,
+ @SerialName("signInType")
+ val signInType: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/auth/response/CheckKakaoEmailResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/auth/response/CheckKakaoEmailResponse.kt
new file mode 100644
index 00000000..44239baf
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/auth/response/CheckKakaoEmailResponse.kt
@@ -0,0 +1,10 @@
+package com.kuit.ourmenu.data.model.auth.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class CheckKakaoEmailResponse(
+ @SerialName("existUser")
+ val existUser: Boolean,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/auth/response/EmailResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/auth/response/EmailResponse.kt
new file mode 100644
index 00000000..f1f0b542
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/auth/response/EmailResponse.kt
@@ -0,0 +1,10 @@
+package com.kuit.ourmenu.data.model.auth.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class EmailResponse(
+ @SerialName("code")
+ val code: String,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/auth/response/LoginResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/auth/response/LoginResponse.kt
new file mode 100644
index 00000000..346e3e28
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/auth/response/LoginResponse.kt
@@ -0,0 +1,16 @@
+package com.kuit.ourmenu.data.model.auth.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class LoginResponse(
+ @SerialName("accessToken")
+ val accessToken: String,
+ @SerialName("grantType")
+ val grantType: String,
+ @SerialName("refreshToken")
+ val refreshToken: String,
+ @SerialName("refreshTokenExpiredAt")
+ val refreshTokenExpiredAt: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/auth/response/ReissueTokenResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/auth/response/ReissueTokenResponse.kt
new file mode 100644
index 00000000..d298ad41
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/auth/response/ReissueTokenResponse.kt
@@ -0,0 +1,16 @@
+package com.kuit.ourmenu.data.model.auth.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ReissueTokenResponse(
+ @SerialName("accessToken")
+ val accessToken: String,
+ @SerialName("grantType")
+ val grantType: String,
+ @SerialName("refreshToken")
+ val refreshToken: String,
+ @SerialName("refreshTokenExpiredAt")
+ val refreshTokenExpiredAt: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/auth/response/SignupResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/auth/response/SignupResponse.kt
new file mode 100644
index 00000000..23f0b8e2
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/auth/response/SignupResponse.kt
@@ -0,0 +1,16 @@
+package com.kuit.ourmenu.data.model.auth.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class SignupResponse(
+ @SerialName("accessToken")
+ val accessToken: String,
+ @SerialName("grantType")
+ val grantType: String,
+ @SerialName("refreshToken")
+ val refreshToken: String,
+ @SerialName("refreshTokenExpiredAt")
+ val refreshTokenExpiredAt: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/auth/response/TokenReIssueResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/auth/response/TokenReIssueResponse.kt
new file mode 100644
index 00000000..9aa19515
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/auth/response/TokenReIssueResponse.kt
@@ -0,0 +1,10 @@
+package com.kuit.ourmenu.data.model.auth.response
+
+import kotlinx.serialization.SerialName
+
+data class TokenReIssueResponse(
+ @SerialName("grantType") val grantType: String,
+ @SerialName("accessToken") val accessToken: String,
+ @SerialName("refreshToken") val refreshToken: String,
+ @SerialName("expiredAt") val expiredAt: String
+)
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/base/ApiResponseHandler.kt b/app/src/main/java/com/kuit/ourmenu/data/model/base/ApiResponseHandler.kt
new file mode 100644
index 00000000..462d9cf5
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/base/ApiResponseHandler.kt
@@ -0,0 +1,22 @@
+package com.kuit.ourmenu.data.model.base
+
+import android.R.id.message
+
+fun BaseResponse.handleBaseResponse(): Result =
+ if (isSuccess) {
+ Result.success(response)
+ } else {
+ Result.failure(
+ OurMenuApiFailureException(
+ errorResponse?.status,
+ errorResponse?.code,
+ errorResponse?.message
+ )
+ )
+ }
+
+data class OurMenuApiFailureException(
+ val status: Int? = null,
+ val code: String? = null,
+ override val message: String? = null
+) : Throwable()
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/base/BaseResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/base/BaseResponse.kt
new file mode 100644
index 00000000..8072106c
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/base/BaseResponse.kt
@@ -0,0 +1,18 @@
+package com.kuit.ourmenu.data.model.base
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class BaseResponse(
+ @SerialName("isSuccess") val isSuccess: Boolean,
+ @SerialName("response") val response: T?,
+ @SerialName("errorResponse") val errorResponse: ErrorResponse?,
+)
+
+@Serializable
+data class ErrorResponse(
+ @SerialName("status") val status: Int,
+ @SerialName("code") val code: String,
+ @SerialName("message") val message: String
+)
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/base/type/MenuFolderIconType.kt b/app/src/main/java/com/kuit/ourmenu/data/model/base/type/MenuFolderIconType.kt
new file mode 100644
index 00000000..e64279b6
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/base/type/MenuFolderIconType.kt
@@ -0,0 +1,36 @@
+package com.kuit.ourmenu.data.model.base.type
+
+enum class MenuFolderIconType {
+ ANGRY,
+ BAEKSUK,
+ BASKET,
+ BREAD,
+ CLOUD,
+ COFFEE,
+ CONGRATS,
+ COUPLE,
+ CRY,
+ DICE,
+ DOUGHNUT,
+ FIRE,
+ FISH,
+ FISH_BREAD,
+ HAMBURGER,
+ HEART,
+ ICE_CREAM,
+ JJAMBBONG,
+ LEAF,
+ MAN,
+ MEAT,
+ NOODLE,
+ PEOPLE,
+ RAMEN,
+ RICE,
+ SMILE,
+ SNOWMAN,
+ SPOON_AND_CHOPSTICK,
+ SUN,
+ SUNNY,
+ SUSHI,
+ TABLE,
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/base/type/SortOrderType.kt b/app/src/main/java/com/kuit/ourmenu/data/model/base/type/SortOrderType.kt
new file mode 100644
index 00000000..44097c2d
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/base/type/SortOrderType.kt
@@ -0,0 +1,13 @@
+package com.kuit.ourmenu.data.model.base.type
+
+enum class
+SortOrderType(val apiValue: String, val displayName: String) {
+ TITLE_ASC("TITLE_ASC", "์ด๋ฆ์"),
+// TITLE_DESC("TITLE_DESC", "์ด๋ฆ์ญ์"),
+// CREATED_AT_ASC("CREATED_AT_ASC", "์ต์ ์"),
+ CREATED_AT_DESC("CREATED_AT_DESC", "๋ฑ๋ก์"),
+ PRICE_ASC("PRICE_ASC", "๊ฐ๊ฒฉ์");
+// PRICE_DESC("PRICE_DESC", "๋์ ๊ฐ๊ฒฉ์");
+
+ override fun toString(): String = displayName
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/base/type/TagType.kt b/app/src/main/java/com/kuit/ourmenu/data/model/base/type/TagType.kt
new file mode 100644
index 00000000..2c143479
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/base/type/TagType.kt
@@ -0,0 +1,38 @@
+package com.kuit.ourmenu.data.model.base.type
+
+enum class TagType(val apiValue: String, val displayName: String) {
+ KOREA("KOREA", "ํ์"),
+ CHINA("CHINA", "์ค์"),
+ JAPAN("JAPAN", "์ผ์"),
+ WESTERN("WESTERN", "์์"),
+ ASIA("ASIA", "์์์"),
+ RICE("RICE", "๋ฐฅ"),
+ BREAD("BREAD", "๋นต"),
+ NOODLE("NOODLE", "๋ฉด"),
+ MEAT("MEAT", "๊ณ ๊ธฐ"),
+ FISH("FISH", "์์ "),
+ DESSERT("DESSERT", "๋์ ํธ"),
+ CAFE("CAFE", "์นดํ"),
+ FAST_FOOD("FAST_FOOD", "ํจ์คํธํธ๋"),
+ SPICY("SPICY", "๋งค์ฝคํจ"),
+ SWEET("SWEET", "๋ฌ๋ฌํจ"),
+ COOL("COOL", "์์ํจ"),
+ HOT("HOT", "๋จ๋ํจ"),
+ HOT_SPICY("HOT_SPICY", "์ผํฐํจ"),
+ SOLO("SOLO", "ํผ๋ฐฅ"),
+ BUSINESS("BUSINESS", "๋น์ฆ๋์ค ๋ฏธํ
"),
+ PROMISE("PROMISE", "์น๊ตฌ ์ฝ์"),
+ DATE("DATE", "๋ฐ์ดํธ"),
+ BUY_FOOD("BUY_FOOD", "๋ฐฅ์ฝ"),
+ ORGANIZATION("ORGANIZATION", "๋จ์ฒด");
+
+ override fun toString(): String = displayName
+
+ companion object {
+ fun fromDisplayName(displayName: String): TagType? =
+ entries.firstOrNull { it.displayName == displayName }
+
+ fun toApiValues(tags: List): List =
+ tags.map { it.apiValue }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/cache/response/CacheInfoResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/cache/response/CacheInfoResponse.kt
new file mode 100644
index 00000000..a19abf4a
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/cache/response/CacheInfoResponse.kt
@@ -0,0 +1,55 @@
+package com.kuit.ourmenu.data.model.cache.response
+
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class CacheInfoResponse(
+ @SerialName("homeImgs")
+ val homeImgs: List,
+ @SerialName("menuFolderIcons")
+ val menuFolderIcons: List,
+ @SerialName("menuPins")
+ val menuPins: List,
+ @SerialName("tags")
+ val tags: List
+) {
+ @Serializable
+ data class HomeImg(
+ @SerialName("homeImg")
+ val homeImg: String,
+ @SerialName("homeImgUrl")
+ val homeImgUrl: String
+ )
+
+ @Serializable
+ data class MenuFolderIcon(
+ @SerialName("menuFolderIcon")
+ val menuFolderIcon: String,
+ @SerialName("menuFolderIconUrl")
+ val menuFolderIconUrl: String
+ )
+
+ @Serializable
+ data class MenuPin(
+ @SerialName("menuPin")
+ val menuPin: String,
+ @SerialName("menuPinAddDisableImgUrl")
+ val menuPinAddDisableImgUrl: String?,
+ @SerialName("menuPinAddImgUrl")
+ val menuPinAddImgUrl: String?,
+ @SerialName("menuPinMapImgUrl")
+ val menuPinMapImgUrl: String
+ )
+
+ @Serializable
+ data class Tag(
+ @SerialName("orangeTagImgUrl")
+ val orangeTagImgUrl: String,
+ @SerialName("tag")
+ val tag: String,
+ @SerialName("whiteTagImgUrl")
+ val whiteTagImgUrl: String
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/dummy/request/DummyRequest.kt b/app/src/main/java/com/kuit/ourmenu/data/model/dummy/request/DummyRequest.kt
new file mode 100644
index 00000000..9ea1823c
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/dummy/request/DummyRequest.kt
@@ -0,0 +1,9 @@
+package com.kuit.ourmenu.data.model.dummy.request
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class DummyRequest(
+ @SerialName("dummyString") val dummyString: String
+)
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/dummy/response/DummyResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/dummy/response/DummyResponse.kt
new file mode 100644
index 00000000..8349a9eb
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/dummy/response/DummyResponse.kt
@@ -0,0 +1,9 @@
+package com.kuit.ourmenu.data.model.dummy.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class DummyResponse(
+ @SerialName("dummyString") val dummyString: String
+)
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/map/response/CrawlingHistoryResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/map/response/CrawlingHistoryResponse.kt
new file mode 100644
index 00000000..d3037fd0
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/map/response/CrawlingHistoryResponse.kt
@@ -0,0 +1,14 @@
+package com.kuit.ourmenu.data.model.map.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class CrawlingHistoryResponse(
+ @SerialName("menuTitle")
+ val menuTitle: String,
+ @SerialName("storeAddress")
+ val storeAddress: String,
+ @SerialName("modifiedAt")
+ val modifiedAt: String
+)
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/map/response/CrawlingStoreDetailResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/map/response/CrawlingStoreDetailResponse.kt
new file mode 100644
index 00000000..515449e5
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/map/response/CrawlingStoreDetailResponse.kt
@@ -0,0 +1,30 @@
+package com.kuit.ourmenu.data.model.map.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class CrawlingStoreDetailResponse(
+ @SerialName("storeId")
+ val storeId: String,
+ @SerialName("storeTitle")
+ val storeTitle: String,
+ @SerialName("storeAddress")
+ val storeAddress: String,
+ @SerialName("storeImgs")
+ val storeImgs: List,
+ @SerialName("menus")
+ val menus: List,
+ @SerialName("storeMapX")
+ val storeMapX: Double,
+ @SerialName("storeMapY")
+ val storeMapY: Double,
+)
+
+@Serializable
+data class CrawlingMenuDetail(
+ @SerialName("menuTitle")
+ val menuTitle: String,
+ @SerialName("menuPrice")
+ val menuPrice: String
+)
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/map/response/CrawlingStoreInfoResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/map/response/CrawlingStoreInfoResponse.kt
new file mode 100644
index 00000000..55393e6e
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/map/response/CrawlingStoreInfoResponse.kt
@@ -0,0 +1,19 @@
+package com.kuit.ourmenu.data.model.map.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class CrawlingStoreInfoResponse(
+ @SerialName("storeTitle")
+ val storeTitle: String,
+
+ @SerialName("storeAddress")
+ val storeAddress: String,
+
+ @SerialName("storeId")
+ val storeId: String,
+
+ @SerialName("crawled")
+ val crawled: Boolean
+)
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapDetailResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapDetailResponse.kt
new file mode 100644
index 00000000..c616bb64
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapDetailResponse.kt
@@ -0,0 +1,40 @@
+package com.kuit.ourmenu.data.model.map.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MapDetailResponse(
+ @SerialName("menuId")
+ val menuId: Long,
+ @SerialName("menuTitle")
+ val menuTitle: String,
+ @SerialName("menuPrice")
+ val menuPrice: Int,
+ @SerialName("storeTitle")
+ val storeTitle: String,
+ @SerialName("menuPinImgUrl")
+ val menuPinImgUrl: String,
+ @SerialName("menuTagImgUrls")
+ val menuTagImgUrls: List,
+ @SerialName("menuImgUrls")
+ val menuImgUrls: List,
+ @SerialName("menuFolderInfo")
+ val menuFolderInfo: MenuFolderInfo,
+ @SerialName("mapId")
+ val mapId: Long,
+ @SerialName("mapX")
+ val mapX: Double,
+ @SerialName("mapY")
+ val mapY: Double
+)
+
+@Serializable
+data class MenuFolderInfo(
+ @SerialName("menuFolderTitle")
+ val menuFolderTitle: String,
+ @SerialName("menuFolderIconImgUrl")
+ val menuFolderIconImgUrl: String,
+ @SerialName("menuFolderCount")
+ val menuFolderCount: Int
+)
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapMenuDetailResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapMenuDetailResponse.kt
new file mode 100644
index 00000000..7c068d4f
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapMenuDetailResponse.kt
@@ -0,0 +1,33 @@
+package com.kuit.ourmenu.data.model.map.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MapMenuDetailResponse(
+ @SerialName("menuId")
+ val menuId: Long,
+ @SerialName("menuTitle")
+ val menuTitle: String,
+ @SerialName("storeTitle")
+ val storeTitle: String,
+ @SerialName("menuPrice")
+ val menuPrice: Int,
+ @SerialName("menuPinImgUrl")
+ val menuPinImgUrl: String,
+ @SerialName("menuTagImgUrls")
+ val menuTagImgUrls: List,
+ @SerialName("menuImgUrls")
+ val menuImgUrls: List,
+ @SerialName("menuFolderInfo")
+ val menuFolderInfo: MenuFolderInfo,
+ @SerialName("mapId")
+ val mapId: Long,
+ @SerialName("mapX")
+ val mapX: Double,
+ @SerialName("mapY")
+ val mapY: Double,
+)
+
+
+
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapResponse.kt
new file mode 100644
index 00000000..c4feeb0b
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapResponse.kt
@@ -0,0 +1,18 @@
+package com.kuit.ourmenu.data.model.map.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MapResponse(
+ @SerialName("mapId")
+ val mapId: Long,
+ @SerialName("menuPinImgUrl")
+ val menuPinImgUrl: String,
+ @SerialName("menuPinDisableImgUrl")
+ val menuPinDisableImgUrl: String,
+ @SerialName("mapX")
+ val mapX: Double,
+ @SerialName("mapY")
+ val mapY: Double
+)
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapSearchHistoryResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapSearchHistoryResponse.kt
new file mode 100644
index 00000000..40346cb8
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapSearchHistoryResponse.kt
@@ -0,0 +1,16 @@
+package com.kuit.ourmenu.data.model.map.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MapSearchHistoryResponse(
+ @SerialName("menuId")
+ val menuId: Long,
+ @SerialName("menuTitle")
+ val menuTitle: String,
+ @SerialName("storeTitle")
+ val storeTitle: String,
+ @SerialName("storeAddress")
+ val storeAddress: String
+)
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapSearchResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapSearchResponse.kt
new file mode 100644
index 00000000..e072a888
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/map/response/MapSearchResponse.kt
@@ -0,0 +1,14 @@
+package com.kuit.ourmenu.data.model.map.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MapSearchResponse(
+ @SerialName("menuTitle")
+ val menuTitle: String,
+ @SerialName("storeTitle")
+ val storeTitle: String,
+ @SerialName("storeAddress")
+ val storeAddress: String
+)
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/menuFolder/request/MenuFolderIndexRequest.kt b/app/src/main/java/com/kuit/ourmenu/data/model/menuFolder/request/MenuFolderIndexRequest.kt
new file mode 100644
index 00000000..e37e4f52
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/menuFolder/request/MenuFolderIndexRequest.kt
@@ -0,0 +1,8 @@
+package com.kuit.ourmenu.data.model.menuFolder.request
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MenuFolderIndexRequest(
+ val index: Int,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/menuFolder/response/MenuFolderAllResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/menuFolder/response/MenuFolderAllResponse.kt
new file mode 100644
index 00000000..931673d8
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/menuFolder/response/MenuFolderAllResponse.kt
@@ -0,0 +1,13 @@
+package com.kuit.ourmenu.data.model.menuFolder.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MenuFolderAllResponse(
+ override val menuId: Long,
+ override val menuTitle: String,
+ override val storeTitle: String,
+ override val storeAddress: String,
+ override val menuPrice: Int,
+ override val menuImgUrl: String
+) : MenuFolderMenuItem
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/menuFolder/response/MenuFolderDetailResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/menuFolder/response/MenuFolderDetailResponse.kt
new file mode 100644
index 00000000..09a51668
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/menuFolder/response/MenuFolderDetailResponse.kt
@@ -0,0 +1,22 @@
+package com.kuit.ourmenu.data.model.menuFolder.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MenuFolderDetailResponse(
+ val menuFolderId: Int = 0,
+ val menuFolderTitle: String = "",
+ val menuFolderImgUrl: String = "",
+ val menuFolderIconImgUrl: String = "",
+ val menus: List = emptyList()
+)
+
+@Serializable
+data class MenuFolderDetailMenus(
+ override val menuId: Long = 0,
+ override val menuTitle: String = "",
+ override val storeTitle: String = "",
+ override val storeAddress: String = "",
+ override val menuPrice: Int = 0,
+ override val menuImgUrl: String = ""
+) : MenuFolderMenuItem
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/menuFolder/response/MenuFolderMenuItem.kt b/app/src/main/java/com/kuit/ourmenu/data/model/menuFolder/response/MenuFolderMenuItem.kt
new file mode 100644
index 00000000..caf3923e
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/menuFolder/response/MenuFolderMenuItem.kt
@@ -0,0 +1,10 @@
+package com.kuit.ourmenu.data.model.menuFolder.response
+
+interface MenuFolderMenuItem {
+ val menuId: Long
+ val menuTitle: String
+ val storeTitle: String
+ val storeAddress: String
+ val menuPrice: Int
+ val menuImgUrl: String
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/menuFolder/response/MenuFolderResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/menuFolder/response/MenuFolderResponse.kt
new file mode 100644
index 00000000..ba24a0c9
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/menuFolder/response/MenuFolderResponse.kt
@@ -0,0 +1,19 @@
+package com.kuit.ourmenu.data.model.menuFolder.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MenuFolderResponse(
+ val menuCount: Int,
+ val menuFolders: List
+)
+
+@Serializable
+data class MenuFolderList(
+ val menuFolderId: Long,
+ val menuFolderTitle: String,
+ val menuFolderImgUrl: String,
+ val menuFolderIconImgUrl: String,
+ val menuIds: List,
+ val index: Int,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/menuinfo/response/MenuInfoResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/menuinfo/response/MenuInfoResponse.kt
new file mode 100644
index 00000000..c75e3d3c
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/menuinfo/response/MenuInfoResponse.kt
@@ -0,0 +1,25 @@
+package com.kuit.ourmenu.data.model.menuinfo.response
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class MenuInfoResponse(
+ val menuId: Long = 0,
+ val menuTitle: String = "",
+ val menuPrice: Int = 0,
+ val menuPinImgUrl: String = "",
+ val menuMemoTitle: String = "",
+ val menuMemoContent: String = "",
+ val storeTitle: String = "",
+ val storeAddress: String = "",
+ val tagImgUrls: List = emptyList(),
+ val menuImgUrls: List = emptyList(),
+ val menuFolders: List = emptyList(),
+)
+
+@Serializable
+data class MenuFolder(
+ val menuFolderId: Int = 0,
+ val menuFolderTitle: String = "",
+ val menuFolderIconImgUrl: String = "",
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/user/request/ChangeMealTimeRequest.kt b/app/src/main/java/com/kuit/ourmenu/data/model/user/request/ChangeMealTimeRequest.kt
new file mode 100644
index 00000000..1f3634d5
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/user/request/ChangeMealTimeRequest.kt
@@ -0,0 +1,10 @@
+package com.kuit.ourmenu.data.model.user.request
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ChangeMealTimeRequest(
+ @SerialName("mealTime")
+ val mealTime: List
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/user/request/ChangePasswordRequest.kt b/app/src/main/java/com/kuit/ourmenu/data/model/user/request/ChangePasswordRequest.kt
new file mode 100644
index 00000000..cdfbfd43
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/user/request/ChangePasswordRequest.kt
@@ -0,0 +1,12 @@
+package com.kuit.ourmenu.data.model.user.request
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ChangePasswordRequest(
+ @SerialName("password")
+ val password: String,
+ @SerialName("newPassword")
+ val newPassword: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/user/response/TemporaryPasswordResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/user/response/TemporaryPasswordResponse.kt
new file mode 100644
index 00000000..12044a18
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/user/response/TemporaryPasswordResponse.kt
@@ -0,0 +1,10 @@
+package com.kuit.ourmenu.data.model.auth.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class TemporaryPasswordResponse(
+ @SerialName("temporaryPassword")
+ val temporaryPassword: String,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/user/response/UserInfoResponse.kt b/app/src/main/java/com/kuit/ourmenu/data/model/user/response/UserInfoResponse.kt
new file mode 100644
index 00000000..68940d6d
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/model/user/response/UserInfoResponse.kt
@@ -0,0 +1,29 @@
+package com.kuit.ourmenu.data.model.user.response
+
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class UserInfoResponse(
+ @SerialName("announcementUrl")
+ val announcementUrl: String,
+ @SerialName("appReviewUrl")
+ val appReviewUrl: String,
+ @SerialName("customerServiceUrl")
+ val customerServiceUrl: String,
+ @SerialName("email")
+ val email: String,
+ @SerialName("mealTimeList")
+ val mealTimeList: List,
+ @SerialName("signInType")
+ val signInType: String
+) {
+ @Serializable
+ data class MealTime(
+ @SerialName("isAfter")
+ val isAfter: Boolean,
+ @SerialName("mealTime")
+ val mealTime: String
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/repository/AuthRepository.kt b/app/src/main/java/com/kuit/ourmenu/data/repository/AuthRepository.kt
new file mode 100644
index 00000000..41ab85d6
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/repository/AuthRepository.kt
@@ -0,0 +1,102 @@
+package com.kuit.ourmenu.data.repository
+
+import com.kuit.ourmenu.data.model.auth.SignInType
+import com.kuit.ourmenu.data.model.auth.request.ConfirmCodeRequest
+import com.kuit.ourmenu.data.model.auth.request.EmailRequest
+import com.kuit.ourmenu.data.model.auth.request.LoginRequest
+import com.kuit.ourmenu.data.model.auth.request.SignupRequest
+import com.kuit.ourmenu.data.model.base.handleBaseResponse
+import com.kuit.ourmenu.data.service.AuthService
+import com.kuit.ourmenu.utils.auth.TokenManager
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class AuthRepository @Inject constructor(
+ private val authService: AuthService,
+ private val tokenManager: TokenManager,
+) {
+ suspend fun signup(
+ email: String?,
+ mealTime: List,
+ password: String?,
+ signInType: SignInType
+ ) = runCatching {
+ val response = authService.signup(
+ SignupRequest(
+ email = email,
+ mealTime = mealTime,
+ password = password,
+ signInType = signInType.name
+ )
+ ).handleBaseResponse().getOrThrow()
+
+ response?.let {
+ tokenManager.saveAccessToken(it.accessToken)
+ tokenManager.saveRefreshToken(it.refreshToken)
+ }
+ }
+
+ suspend fun logout() = runCatching {
+ authService.logout().handleBaseResponse().getOrThrow()
+ }
+
+ suspend fun login(
+ email: String,
+ password: String?,
+ signInType: SignInType
+ ) = runCatching {
+ val response = authService.login(
+ LoginRequest(
+ email = email,
+ password = password,
+ signInType = signInType.name
+ )
+ ).handleBaseResponse().getOrThrow()
+
+ response?.let {
+ tokenManager.saveAccessToken(it.accessToken)
+ tokenManager.saveRefreshToken(it.refreshToken)
+ }
+ }
+
+
+ suspend fun checkKakaoEmail(
+ email: String?
+ ) = runCatching {
+ authService.checkKakaoEmail(
+ EmailRequest(
+ email = email
+ )
+ ).handleBaseResponse().getOrThrow()
+ }
+
+
+ suspend fun reissueToken(
+ refreshToken: String
+ ) = runCatching {
+ authService.reissueToken(refreshToken).handleBaseResponse().getOrThrow()
+ }
+
+ suspend fun sendEmail(
+ email: String
+ ) = runCatching {
+ authService.sendEmail(
+ EmailRequest(
+ email = email
+ )
+ ).handleBaseResponse().getOrThrow()
+ }
+
+ suspend fun confirmCode(
+ confirmCode: String,
+ email: String
+ ) = runCatching {
+ authService.confirmCode(
+ ConfirmCodeRequest(
+ confirmCode = confirmCode,
+ email = email
+ )
+ ).handleBaseResponse().getOrThrow()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/repository/CacheRepository.kt b/app/src/main/java/com/kuit/ourmenu/data/repository/CacheRepository.kt
new file mode 100644
index 00000000..b0584682
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/repository/CacheRepository.kt
@@ -0,0 +1,15 @@
+package com.kuit.ourmenu.data.repository
+
+import com.kuit.ourmenu.data.model.base.handleBaseResponse
+import com.kuit.ourmenu.data.service.CacheService
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class CacheRepository @Inject constructor(
+ private val cacheService: CacheService
+) {
+ suspend fun getCacheData() = runCatching {
+ cacheService.getCacheData().handleBaseResponse().getOrThrow()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/repository/DummyRepository.kt b/app/src/main/java/com/kuit/ourmenu/data/repository/DummyRepository.kt
new file mode 100644
index 00000000..d8235b7e
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/repository/DummyRepository.kt
@@ -0,0 +1,16 @@
+package com.kuit.ourmenu.data.repository
+
+import com.kuit.ourmenu.data.model.base.handleBaseResponse
+import com.kuit.ourmenu.data.service.DummyService
+import com.kuit.ourmenu.utils.auth.TokenManager
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class DummyRepository @Inject constructor(
+ private val dummyService: DummyService,
+ private val tokenManager: TokenManager
+) {
+ suspend fun getDummyData() =
+ runCatching { dummyService.getDummyData().handleBaseResponse().getOrThrow() }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/repository/KakaoRepository.kt b/app/src/main/java/com/kuit/ourmenu/data/repository/KakaoRepository.kt
new file mode 100644
index 00000000..d63eb5d2
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/repository/KakaoRepository.kt
@@ -0,0 +1,120 @@
+package com.kuit.ourmenu.data.repository
+
+import android.content.Context
+import android.util.Log
+import com.kakao.sdk.auth.AuthApiClient
+import com.kakao.sdk.auth.model.OAuthToken
+import com.kakao.sdk.common.model.ClientError
+import com.kakao.sdk.common.model.ClientErrorCause
+import com.kakao.sdk.user.UserApiClient
+import kotlinx.coroutines.suspendCancellableCoroutine
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlin.coroutines.resume
+
+@Singleton
+class KakaoRepository @Inject constructor() {
+ fun getKakaoLogin(
+ context: Context,
+ successLogin: () -> Unit,
+ ) {
+ val callback: (OAuthToken?, Throwable?) -> Unit = { token, error ->
+ if (error != null) {
+ Log.e("KakaoModule", "์นด์นด์ค๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ ์คํจ", error)
+ } else if (token != null) {
+ Log.i("KakaoModule", "์นด์นด์ค๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ ์ฑ๊ณต ${token.accessToken}")
+ }
+ }
+
+ if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) {
+ Log.d("KakaoModule", "์นด์นด์คํก์ผ๋ก ๋ก๊ทธ์ธ ๊ฐ๋ฅ")
+ UserApiClient.instance.loginWithKakaoTalk(context) { token, error ->
+ Log.d("KakaoModule", "์นด์นด์คํก์ผ๋ก ๋ก๊ทธ์ธ ์๋")
+ if (error != null) {
+ Log.e("KakaoModule", "์นด์นด์คํก์ผ๋ก ๋ก๊ทธ์ธ ์คํจ", error)
+ if (error is ClientError && error.reason == ClientErrorCause.Cancelled) {
+ return@loginWithKakaoTalk
+ }
+
+ UserApiClient.instance.loginWithKakaoAccount(context, callback = callback)
+ } else if (token != null) {
+ Log.i("KakaoModule", "์นด์นด์คํก์ผ๋ก ๋ก๊ทธ์ธ ์ฑ๊ณต ${token.accessToken}")
+ }
+ UserApiClient.instance.me { user, error ->
+ if (error != null) {
+ Log.e("KakaoModule", "์ฌ์ฉ์ ์ ๋ณด ์์ฒญ ์คํจ", error)
+ }
+ else if (user != null) {
+ Log.i("KakaoModule", "์ฌ์ฉ์ ์ ๋ณด ์์ฒญ ์ฑ๊ณต" +
+ "\nํ์๋ฒํธ: ${user.id}" +
+ "\n์ด๋ฉ์ผ: ${user.kakaoAccount?.email}" +
+ "\n๋๋ค์: ${user.kakaoAccount?.profile?.nickname}" +
+ "\nํ๋กํ์ฌ์ง: ${user.kakaoAccount?.profile?.thumbnailImageUrl}")
+ }
+ }
+
+ successLogin()
+ }
+ } else {
+ Log.d("KakaoModule", "์นด์นด์คํก์ผ๋ก ๋ก๊ทธ์ธ ๋ถ๊ฐ๋ฅ, ์นด์นด์ค๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ ์๋")
+ UserApiClient.instance.loginWithKakaoAccount(context, callback = callback)
+ successLogin()
+ }
+ }
+
+ suspend fun getUserEmail(): String? {
+ if (AuthApiClient.instance.hasToken()) {
+ return suspendCancellableCoroutine { continuation ->
+ UserApiClient.instance.me { user, error ->
+ if (error != null) {
+ Log.e("KakaoModule", "์ฌ์ฉ์ ์ ๋ณด ์์ฒญ ์คํจ", error)
+ continuation.resume(null)
+ } else if (user != null) {
+ val email = user.kakaoAccount?.email
+ Log.e("KakaoModule2", email.toString())
+ continuation.resume(email)
+ } else {
+ continuation.resume(null)
+ }
+ }
+ }
+ } else {// has no token
+ Log.e("KakaoModule", "ํ ํฐ ์คํจ")
+ return null
+ }
+ }
+
+ fun logout(
+ successLogout: () -> Unit,
+ errorLogout: (Throwable) -> Unit,
+ ) {
+ if (AuthApiClient.instance.hasToken()) {
+ UserApiClient.instance.logout { error ->
+ if (error != null) {
+ Log.e("KakaoTag", "๋ก๊ทธ์์ ์คํจ. SDK์์ ํ ํฐ ์ญ์ ๋จ", error)
+ errorLogout(error)
+ } else {
+ Log.i("KakaoTag", "๋ก๊ทธ์์ ์ฑ๊ณต. SDK์์ ํ ํฐ ์ญ์ ๋จ")
+ successLogout()
+ }
+ }
+ }
+ }
+
+ fun unlink(
+ successUnlink: () -> Unit,
+ errorUnlink: (Throwable) -> Unit,
+ ) {
+ if (AuthApiClient.instance.hasToken()) {
+ UserApiClient.instance.unlink { error ->
+ if (error != null) {
+ Log.e("KakaoTag", "ํ์ ํํด ์คํจ. SDK์์ ํ ํฐ ์ญ์ ๋จ", error)
+ errorUnlink(error)
+ } else {
+ Log.i("KakaoTag", "ํ์ ํํด ์ฑ๊ณต. SDK์์ ํ ํฐ ์ญ์ ๋จ")
+ successUnlink()
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/repository/MapRepository.kt b/app/src/main/java/com/kuit/ourmenu/data/repository/MapRepository.kt
new file mode 100644
index 00000000..28521138
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/repository/MapRepository.kt
@@ -0,0 +1,75 @@
+package com.kuit.ourmenu.data.repository
+
+import com.kuit.ourmenu.data.model.base.handleBaseResponse
+import com.kuit.ourmenu.data.service.MapService
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class MapRepository @Inject constructor(
+ private val mapService: MapService
+) {
+ suspend fun getMapDetail(
+ mapId: Long
+ ) = runCatching {
+ mapService.getMapDetail(
+ mapId = mapId
+ ).handleBaseResponse().getOrThrow()
+ }
+
+ suspend fun getMap() = runCatching {
+ mapService.getMap().handleBaseResponse().getOrThrow()
+ }
+
+ suspend fun getMapMenuDetail(
+ menuId: Long
+ ) = runCatching {
+ mapService.getMapMenuDetail(
+ menuId = menuId
+ ).handleBaseResponse().getOrThrow()
+ }
+
+ suspend fun getMapSearch(
+ title: String,
+ longitude: Double?,
+ latitude: Double?
+ ) = runCatching {
+ mapService.getMapSearch(
+ title = title,
+ longitude = longitude,
+ latitude = latitude
+ ).handleBaseResponse().getOrThrow()
+ }
+
+ suspend fun getMapSearchHistory() = runCatching {
+ mapService.getMapSearchHistory().handleBaseResponse().getOrThrow()
+ }
+
+ // ํฌ๋กค๋ง ๊ด๋ จ
+ suspend fun getCrawlingHistory() = runCatching {
+ mapService.getCrawlingHistory().handleBaseResponse().getOrThrow()
+ }
+
+ suspend fun getCrawlingStoreDetail(
+ isCrawled: Boolean,
+ storeId: String
+ ) = runCatching {
+ mapService.getCrawlingStoreDetail(
+ isCrawled = isCrawled,
+ storeId = storeId
+ ).handleBaseResponse().getOrThrow()
+ }
+
+ suspend fun getCrawlingStoreInfo(
+ query: String,
+ longitude: Double,
+ latitude: Double
+ ) = runCatching {
+ mapService.getCrawlingStoreInfo(
+ query = query,
+ longitude = longitude,
+ latitude = latitude
+ ).handleBaseResponse().getOrThrow()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/repository/MenuFolderRepository.kt b/app/src/main/java/com/kuit/ourmenu/data/repository/MenuFolderRepository.kt
new file mode 100644
index 00000000..708eb2e2
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/repository/MenuFolderRepository.kt
@@ -0,0 +1,60 @@
+package com.kuit.ourmenu.data.repository
+
+import com.kuit.ourmenu.data.model.base.handleBaseResponse
+import com.kuit.ourmenu.data.model.menuFolder.request.MenuFolderIndexRequest
+import com.kuit.ourmenu.data.service.MenuFolderService
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class MenuFolderRepository @Inject constructor(
+ private val menuFolderService: MenuFolderService,
+) {
+ suspend fun getMenuFolders() = runCatching {
+ menuFolderService.getMenuFolders().handleBaseResponse().getOrThrow()
+ }
+
+ suspend fun getMenuFolderDetail(
+ menuFolderId: Long,
+ sortOrder: String,
+ ) = runCatching {
+ menuFolderService.getMenuFolderDetails(
+ menuFolderId = menuFolderId,
+ sortOrder = sortOrder
+ ).handleBaseResponse().getOrThrow()
+ }
+
+ suspend fun getMenuFolderAll(
+ tags: List? = null,
+ minPrice: Long? = null,
+ maxPrice: Long? = null,
+ page: Int? = null,
+ size: Int = 10,
+ sortOrder: String,
+ ) = runCatching {
+ menuFolderService.getMenuFolderAll(
+ tags = tags,
+ minPrice = minPrice,
+ maxPrice = maxPrice,
+ page = page,
+ size = size,
+ sortOrder = sortOrder
+ ).handleBaseResponse().getOrThrow()
+ }
+
+ suspend fun deleteMenuFolder(
+ menuFolderId: Long
+ ) = runCatching {
+ menuFolderService.deleteMenuFolder(menuFolderId).handleBaseResponse().getOrThrow()
+ }
+
+ suspend fun updateMenuFolderIndex(
+ menuFolderId: Long,
+ request: MenuFolderIndexRequest
+ ) = runCatching {
+ menuFolderService.updateMenuFolderIndex(
+ menuFolderId = menuFolderId,
+ request = request
+ ).handleBaseResponse().getOrThrow()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/repository/MenuInfoRepository.kt b/app/src/main/java/com/kuit/ourmenu/data/repository/MenuInfoRepository.kt
new file mode 100644
index 00000000..fbe7f0cb
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/repository/MenuInfoRepository.kt
@@ -0,0 +1,17 @@
+package com.kuit.ourmenu.data.repository
+
+import com.kuit.ourmenu.data.model.base.handleBaseResponse
+import com.kuit.ourmenu.data.service.MenuInfoService
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class MenuInfoRepository @Inject constructor(
+ private val menuInfoService: MenuInfoService
+) {
+ suspend fun getMenuInfo(
+ menuId: Long
+ ) = runCatching {
+ menuInfoService.getMenuInfo(menuId).handleBaseResponse().getOrThrow()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/repository/UserRepository.kt b/app/src/main/java/com/kuit/ourmenu/data/repository/UserRepository.kt
new file mode 100644
index 00000000..8fefaba8
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/repository/UserRepository.kt
@@ -0,0 +1,46 @@
+package com.kuit.ourmenu.data.repository
+
+import com.kuit.ourmenu.data.model.base.handleBaseResponse
+import com.kuit.ourmenu.data.model.user.request.ChangeMealTimeRequest
+import com.kuit.ourmenu.data.model.user.request.ChangePasswordRequest
+import com.kuit.ourmenu.data.service.UserService
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class UserRepository @Inject constructor(
+ private val userService: UserService,
+ private val kakaoRepository: KakaoRepository
+) {
+
+ suspend fun sendTemporaryPassword(
+ email: String
+ ) = runCatching {
+ userService.sendTemporaryPassword(email).handleBaseResponse().getOrThrow()
+ }
+
+ suspend fun changePassword(
+ currentPassword: String,
+ newPassword: String
+ ) = runCatching {
+ userService.changePassword(
+ ChangePasswordRequest(currentPassword, newPassword)
+ ).handleBaseResponse().getOrThrow()
+ }
+
+ suspend fun updateMealTimes(
+ newMealTimes: List
+ ) = runCatching {
+ userService.updateMealTimes(
+ ChangeMealTimeRequest(newMealTimes)
+ ).handleBaseResponse().getOrThrow()
+ }
+
+ suspend fun getUserInfo() = runCatching {
+ userService.getUserInfo().handleBaseResponse().getOrThrow()
+ }
+
+ suspend fun deleteUser() = runCatching {
+ userService.deleteUser().handleBaseResponse().getOrThrow()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/service/AuthService.kt b/app/src/main/java/com/kuit/ourmenu/data/service/AuthService.kt
new file mode 100644
index 00000000..798ed8b3
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/service/AuthService.kt
@@ -0,0 +1,49 @@
+package com.kuit.ourmenu.data.service
+
+import com.kuit.ourmenu.data.model.auth.request.ConfirmCodeRequest
+import com.kuit.ourmenu.data.model.auth.request.EmailRequest
+import com.kuit.ourmenu.data.model.auth.request.LoginRequest
+import com.kuit.ourmenu.data.model.auth.request.SignupRequest
+import com.kuit.ourmenu.data.model.auth.response.CheckKakaoEmailResponse
+import com.kuit.ourmenu.data.model.auth.response.EmailResponse
+import com.kuit.ourmenu.data.model.auth.response.LoginResponse
+import com.kuit.ourmenu.data.model.auth.response.ReissueTokenResponse
+import com.kuit.ourmenu.data.model.auth.response.SignupResponse
+import com.kuit.ourmenu.data.model.base.BaseResponse
+import retrofit2.http.Body
+import retrofit2.http.POST
+
+interface AuthService {
+ @POST("api/users/sign-up")
+ suspend fun signup(
+ @Body request: SignupRequest
+ ): BaseResponse
+
+ @POST("api/users/sign-out")
+ suspend fun logout(): BaseResponse
+
+ @POST("api/users/sign-in")
+ suspend fun login(
+ @Body request: LoginRequest
+ ): BaseResponse
+
+ @POST("api/users/reissue-token")
+ suspend fun reissueToken(
+ @Body refreshToken: String
+ ): BaseResponse
+
+ @POST("/api/users/auth/kakao")
+ suspend fun checkKakaoEmail(
+ @Body request: EmailRequest
+ ): BaseResponse
+
+ @POST("api/emails")
+ suspend fun sendEmail(
+ @Body request: EmailRequest
+ ): BaseResponse
+
+ @POST("api/emails/confirm-code")
+ suspend fun confirmCode(
+ @Body request: ConfirmCodeRequest
+ ): BaseResponse
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/service/CacheService.kt b/app/src/main/java/com/kuit/ourmenu/data/service/CacheService.kt
new file mode 100644
index 00000000..79b3d5d7
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/service/CacheService.kt
@@ -0,0 +1,10 @@
+package com.kuit.ourmenu.data.service
+
+import com.kuit.ourmenu.data.model.base.BaseResponse
+import com.kuit.ourmenu.data.model.cache.response.CacheInfoResponse
+import retrofit2.http.GET
+
+interface CacheService {
+ @GET("api/cache-data")
+ suspend fun getCacheData() : BaseResponse
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/service/DummyService.kt b/app/src/main/java/com/kuit/ourmenu/data/service/DummyService.kt
new file mode 100644
index 00000000..d41d9d4d
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/service/DummyService.kt
@@ -0,0 +1,18 @@
+package com.kuit.ourmenu.data.service
+
+import com.kuit.ourmenu.data.model.base.BaseResponse
+import com.kuit.ourmenu.data.model.dummy.request.DummyRequest
+import com.kuit.ourmenu.data.model.dummy.response.DummyResponse
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.POST
+
+interface DummyService {
+ @GET("")
+ suspend fun getDummyData(): BaseResponse
+
+ @POST("")
+ suspend fun postDummyData(
+ @Body dummy: DummyRequest
+ ): BaseResponse
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/service/MapService.kt b/app/src/main/java/com/kuit/ourmenu/data/service/MapService.kt
new file mode 100644
index 00000000..d6c1e2ca
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/service/MapService.kt
@@ -0,0 +1,57 @@
+package com.kuit.ourmenu.data.service
+
+import com.kuit.ourmenu.data.model.base.BaseResponse
+import com.kuit.ourmenu.data.model.map.response.CrawlingHistoryResponse
+import com.kuit.ourmenu.data.model.map.response.CrawlingStoreDetailResponse
+import com.kuit.ourmenu.data.model.map.response.CrawlingStoreInfoResponse
+import com.kuit.ourmenu.data.model.map.response.MapDetailResponse
+import com.kuit.ourmenu.data.model.map.response.MapMenuDetailResponse
+import com.kuit.ourmenu.data.model.map.response.MapResponse
+import com.kuit.ourmenu.data.model.map.response.MapSearchHistoryResponse
+import com.kuit.ourmenu.data.model.map.response.MapSearchResponse
+import retrofit2.http.GET
+import retrofit2.http.Path
+import retrofit2.http.Query
+
+interface MapService {
+
+ @GET("api/users/menus/{mapId}/maps")
+ suspend fun getMapDetail(
+ @Path("mapId") mapId: Long
+ ): BaseResponse>
+
+ @GET("api/users/menus/maps")
+ suspend fun getMap(): BaseResponse>
+
+ @GET("api/users/menus/maps/{menuId}/search")
+ suspend fun getMapMenuDetail(
+ @Path("menuId") menuId: Long
+ ): BaseResponse
+
+ @GET("api/users/menus/maps/search")
+ suspend fun getMapSearch(
+ @Query("title") title: String,
+ @Query("mapX") longitude: Double?,
+ @Query("mapY") latitude: Double?
+ ): BaseResponse>
+
+ @GET("api/users/menus/maps/search-history")
+ suspend fun getMapSearchHistory(): BaseResponse>
+
+ // ํฌ๋กค๋ง ๊ด๋ จ ์์ฒญ
+ @GET("api/priored/users/{userId}/histories")
+ suspend fun getCrawlingHistory(): BaseResponse>
+
+ @GET("api/priored/stores/{storeId}")
+ suspend fun getCrawlingStoreDetail(
+ @Path("storeId") storeId: String,
+ @Query("is-crawled") isCrawled: Boolean
+ ): BaseResponse
+
+ @GET("api/priored/stores/menus")
+ suspend fun getCrawlingStoreInfo(
+ @Query("query") query: String,
+ @Query("mapX") longitude: Double,
+ @Query("mapY") latitude: Double
+ ): BaseResponse>
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/service/MenuFolderService.kt b/app/src/main/java/com/kuit/ourmenu/data/service/MenuFolderService.kt
new file mode 100644
index 00000000..e0da6ec9
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/service/MenuFolderService.kt
@@ -0,0 +1,45 @@
+package com.kuit.ourmenu.data.service
+
+import com.kuit.ourmenu.data.model.base.BaseResponse
+import com.kuit.ourmenu.data.model.menuFolder.request.MenuFolderIndexRequest
+import com.kuit.ourmenu.data.model.menuFolder.response.MenuFolderAllResponse
+import com.kuit.ourmenu.data.model.menuFolder.response.MenuFolderDetailResponse
+import com.kuit.ourmenu.data.model.menuFolder.response.MenuFolderResponse
+import retrofit2.http.Body
+import retrofit2.http.DELETE
+import retrofit2.http.GET
+import retrofit2.http.PATCH
+import retrofit2.http.Path
+import retrofit2.http.Query
+
+interface MenuFolderService {
+ @GET("api/menu-folders")
+ suspend fun getMenuFolders(): BaseResponse
+
+ @GET("api/menu-folders/{menuFolderId}/menus")
+ suspend fun getMenuFolderDetails(
+ @Path("menuFolderId") menuFolderId: Long,
+ @Query("sortOrder") sortOrder: String,
+ ): BaseResponse
+
+ @GET("api/menus")
+ suspend fun getMenuFolderAll(
+ @Query("tags") tags: List? = null,
+ @Query("minPrice") minPrice: Long? = null,
+ @Query("maxPrice") maxPrice: Long? = null,
+ @Query("page") page: Int? = null,
+ @Query("size") size: Int,
+ @Query("sortOrder") sortOrder: String,
+ ): BaseResponse>
+
+ @DELETE("api/menu-folders/{menuFolderId}")
+ suspend fun deleteMenuFolder(
+ @Path("menuFolderId") menuFolderId: Long
+ ): BaseResponse
+
+ @PATCH("api/menu-folders/{menuFolderId}/index")
+ suspend fun updateMenuFolderIndex(
+ @Path("menuFolderId") menuFolderId: Long,
+ @Body request: MenuFolderIndexRequest
+ ): BaseResponse
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/data/service/MenuInfoService.kt b/app/src/main/java/com/kuit/ourmenu/data/service/MenuInfoService.kt
new file mode 100644
index 00000000..f5453009
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/service/MenuInfoService.kt
@@ -0,0 +1,13 @@
+package com.kuit.ourmenu.data.service
+
+import com.kuit.ourmenu.data.model.base.BaseResponse
+import com.kuit.ourmenu.data.model.menuinfo.response.MenuInfoResponse
+import retrofit2.http.GET
+import retrofit2.http.Path
+
+interface MenuInfoService {
+ @GET("api/menus/{menuId}")
+ suspend fun getMenuInfo(
+ @Path("menuId") menuId: Long
+ ): BaseResponse
+}
diff --git a/app/src/main/java/com/kuit/ourmenu/data/service/UserService.kt b/app/src/main/java/com/kuit/ourmenu/data/service/UserService.kt
new file mode 100644
index 00000000..7c58abe5
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/data/service/UserService.kt
@@ -0,0 +1,36 @@
+package com.kuit.ourmenu.data.service
+
+import com.kuit.ourmenu.data.model.auth.response.TemporaryPasswordResponse
+import com.kuit.ourmenu.data.model.base.BaseResponse
+import com.kuit.ourmenu.data.model.user.request.ChangeMealTimeRequest
+import com.kuit.ourmenu.data.model.user.request.ChangePasswordRequest
+import com.kuit.ourmenu.data.model.user.response.UserInfoResponse
+import retrofit2.http.Body
+import retrofit2.http.DELETE
+import retrofit2.http.GET
+import retrofit2.http.PATCH
+import retrofit2.http.POST
+
+interface UserService {
+
+ @POST("api/emails/temporary-password")
+ suspend fun sendTemporaryPassword(
+ @Body email: String
+ ): BaseResponse
+
+ @PATCH("api/users/password")
+ suspend fun changePassword(
+ @Body request: ChangePasswordRequest
+ ): BaseResponse
+
+ @PATCH("api/users/meal-time")
+ suspend fun updateMealTimes(
+ @Body mealTimes: ChangeMealTimeRequest
+ ): BaseResponse
+
+ @GET("api/users")
+ suspend fun getUserInfo(): BaseResponse
+
+ @DELETE("api/users")
+ suspend fun deleteUser(): BaseResponse
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/AddMenuAddImageComponent.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/AddMenuAddImageComponent.kt
new file mode 100644
index 00000000..0f0825b7
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/AddMenuAddImageComponent.kt
@@ -0,0 +1,96 @@
+package com.kuit.ourmenu.ui.addmenu.component
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.addmenu.component.item.AddMenuAddedImageItem
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun AddMenuAddImageComponent(
+ modifier: Modifier = Modifier,
+ imgList: List,
+ onDelete: (Int) -> Unit
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ ) {
+ Button(
+ onClick = { /*TODO: ์ด๋ฏธ์ง ์ถ๊ฐ*/ },
+ modifier = modifier.size(88.dp, 72.dp),
+ shape = RoundedCornerShape(12.dp),
+ contentPadding = PaddingValues(0.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Neutral300
+ )
+ ) {
+ Column(
+ modifier = modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_addmenu_add_photo),
+ contentDescription = "add photo",
+ tint = Color.Unspecified
+ )
+ Text(
+ text = "${imgList.size}/5",
+ style = ourMenuTypography().pretendard_500_12,
+ color = Neutral500
+ )
+ }
+ }
+ Spacer(modifier = modifier.size(8.dp))
+ LazyRow(modifier = modifier.fillMaxWidth()) {
+ itemsIndexed(imgList) { index, item ->
+ if (index == 0) {
+ AddMenuAddedImageItem(img = item, isFirstItem = true) {
+ onDelete(index)
+ }
+ } else {
+ AddMenuAddedImageItem(img = item, isFirstItem = false) {
+ onDelete(index)
+ }
+ }
+ Spacer(modifier = modifier.size(8.dp))
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun AddMenuAddImageComponentPreview() {
+ val imgList = listOf(
+ R.drawable.img_dummy_pizza,
+ R.drawable.img_dummy_pizza,
+ R.drawable.img_dummy_pizza,
+ R.drawable.img_dummy_pizza,
+ )
+ AddMenuAddImageComponent(imgList = imgList) {
+ //onDelete
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/AddMenuSearchBackground.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/AddMenuSearchBackground.kt
new file mode 100644
index 00000000..71b04f1b
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/AddMenuSearchBackground.kt
@@ -0,0 +1,175 @@
+package com.kuit.ourmenu.ui.addmenu.component
+
+import androidx.compose.foundation.layout.Box
+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.foundation.lazy.LazyColumn
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.data.model.map.response.CrawlingHistoryResponse
+import com.kuit.ourmenu.data.model.map.response.CrawlingStoreDetailResponse
+import com.kuit.ourmenu.ui.addmenu.component.item.StoreSearchHistoryItem
+import com.kuit.ourmenu.ui.addmenu.component.item.StoreSearchResultItem
+import com.kuit.ourmenu.ui.common.BottomFullWidthButton
+import com.kuit.ourmenu.ui.theme.Neutral100
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun AddMenuSearchBackground(
+ modifier: Modifier = Modifier,
+ searchActionDone: Boolean,
+ searchHistory: List?, //์ดํ์ ํ์
๋ณ๊ฒฝ
+ searchResults: List?, //์ดํ์ ํ์
๋ณ๊ฒฝ
+ onItemClick: () -> Unit
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize(),
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(bottom = 52.dp)
+ ) {
+ if (searchActionDone) {
+ //๊ฒ์์ ํ ๊ฒฝ์ฐ
+ if (searchResults != null) {
+ if (searchResults.isEmpty()) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 68.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_addmenu_noresult),
+ contentDescription = "no result",
+ tint = Color.Unspecified
+ )
+ Text(
+ text = stringResource(R.string.no_result),
+ style = ourMenuTypography().pretendard_600_14,
+ color = Neutral500,
+ modifier = Modifier.padding(top = 8.dp)
+ )
+ }
+ } else {
+ LazyColumn(modifier = Modifier.padding(top = 68.dp)) {
+ items(searchResults.size) { index ->
+ StoreSearchResultItem(
+ resultItem = searchResults[index]
+ ) {
+ onItemClick()
+ }
+ if (index < searchResults.size - 1) {
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Neutral300,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+ }
+ }
+ }
+ } else {
+ //๊ฒ์์ ํ์ง ์์ ๊ฒฝ์ฐ
+ Text(
+ text = stringResource(R.string.recent_search),
+ style = ourMenuTypography().pretendard_600_14,
+ color = Neutral700,
+ modifier = Modifier.padding(start = 28.dp, top = 68.dp)
+ )
+ if (searchHistory != null) {
+ if (searchHistory.isEmpty()) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 68.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_addmenu_noresult),
+ contentDescription = "no result",
+ tint = Color.Unspecified
+ )
+ Text(
+ text = stringResource(R.string.no_result),
+ style = ourMenuTypography().pretendard_600_14,
+ color = Neutral500,
+ modifier = Modifier.padding(top = 8.dp)
+ )
+ }
+ } else {
+ LazyColumn() {
+ items(searchHistory.size) { index ->
+ StoreSearchHistoryItem(
+ historyItem = searchHistory[index]
+ ) {
+ onItemClick()
+ }
+ if (index < searchHistory.size - 1) {
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Neutral300,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+
+ }
+
+ BottomFullWidthButton(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(start = 20.dp, end = 20.dp, bottom = 20.dp),
+ onClick = { TODO() },
+ containerColor = Neutral100,
+ contentColor = Neutral500,
+ text = stringResource(R.string.add_store_and_menu_by_myself)
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun AddMenuSearchBackgroundPreview() {
+ val searchActionDone by rememberSaveable { mutableStateOf(false) }
+ val recentSearchResults = listOf(
+ CrawlingHistoryResponse(
+ menuTitle = stringResource(R.string.our_ddeokbokki),
+ storeAddress = stringResource(R.string.resaturant_address),
+ modifiedAt = "2023-10-01T12:00:00Z",
+ )
+ )
+ val searchResults = emptyList()
+ AddMenuSearchBackground(
+ searchActionDone = searchActionDone,
+ searchHistory = recentSearchResults,
+ searchResults = searchResults,
+ ){
+ //onItemClick
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/SelectedTagGroup.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/SelectedTagGroup.kt
new file mode 100644
index 00000000..52b679d8
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/SelectedTagGroup.kt
@@ -0,0 +1,55 @@
+package com.kuit.ourmenu.ui.addmenu.component
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.ui.addmenu.component.item.SelectedTagItem
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+fun SelectedTagGroup(
+ modifier: Modifier = Modifier,
+ tags: List,
+ onTagClick: (String) -> Unit
+) {
+ FlowRow(
+ modifier = modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ maxItemsInEachRow = Int.MAX_VALUE
+ ) {
+ tags.forEach { it ->
+ SelectedTagItem(
+ label = it
+ ){
+ onTagClick(it)
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SelectedTagGroupPreview() {
+ SelectedTagGroup(
+ tags = listOf(
+ "๋ฐฅ๋ฐฅ๋ฐฅ",
+ "๋ฐฅ๋ฐฅ๋ฐฅ",
+ "๋ฐฅ๋ฐฅ๋ฐฅ",
+ "๋ฐฅ๋ฐฅ๋ฐฅ",
+ "๋ฐฅ๋ฐฅ๋ฐฅ",
+ "๋ฐฅ๋ฐฅ๋ฐฅ",
+ "๋ฐฅ๋ฐฅ๋ฐฅ",
+ "๋ฐฅ๋ฐฅ๋ฐฅ",
+ "๋ฐฅ๋ฐฅ๋ฐฅ",
+ "๋ฐฅ๋ฐฅ๋ฐฅ",
+ )
+ ) {
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/bottomsheet/AddMenuBottomSheetContent.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/bottomsheet/AddMenuBottomSheetContent.kt
new file mode 100644
index 00000000..4154706f
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/bottomsheet/AddMenuBottomSheetContent.kt
@@ -0,0 +1,206 @@
+package com.kuit.ourmenu.ui.addmenu.component.bottomsheet
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.BottomSheetScaffoldState
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.SheetValue
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberBottomSheetScaffoldState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import coil3.compose.LocalPlatformContext
+import coil3.compose.rememberAsyncImagePainter
+import coil3.request.ImageRequest
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.data.model.map.response.CrawlingStoreDetailResponse
+import com.kuit.ourmenu.ui.addmenu.component.item.SelectMenuItem
+import com.kuit.ourmenu.ui.addmenu.viewmodel.AddMenuViewModel
+import com.kuit.ourmenu.ui.common.BottomFullWidthButton
+import com.kuit.ourmenu.ui.theme.Neutral100
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral400
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AddMenuBottomSheetContent(
+ scaffoldState: BottomSheetScaffoldState,
+ storeInfo: CrawlingStoreDetailResponse,
+ selectedMenuIndex: Int?,
+ onNavigateToAddMenuInfo: () -> Unit,
+ onItemClick: (Int) -> Unit
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val enableNextButton = selectedMenuIndex != null
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(start = 20.dp, end = 20.dp, bottom = 20.dp)
+ ) {
+ Text(
+ text = storeInfo.storeTitle,
+ style = ourMenuTypography().pretendard_700_20
+ )
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_fill_map_20),
+ contentDescription = "location info icon",
+ //์ด ์ฝ๋๋ฅผ ์์ ๋ฉด ์์ด์ฝ์ด ๊ฒ์์์ผ๋ก ๋ณด์ฌ์ ์์ฑ
+ tint = Color.Unspecified
+ )
+ Text(
+ text = storeInfo.storeAddress,
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral700,
+ modifier = Modifier.padding(start = 8.dp)
+ )
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp), horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ // ์ด๋ฏธ์ง 3๊ฐ ๋ฐฐ์น
+ for(i in 0..2) {
+ Image(
+ painter = if (i < storeInfo.storeImgs.size) {
+ rememberAsyncImagePainter(
+ model = ImageRequest.Builder(LocalPlatformContext.current)
+ // ์ ๋ฌ ๋ฐ๋ storeImgs์ ๊ฐ์ https://๊ฐ ์์ด ์ ๋ฌ๋จ
+ .data("https://"+storeInfo.storeImgs[i])
+ .size(104, 80)
+ .build()
+ )
+ } else {
+ painterResource(id = R.drawable.img_dummy_menu)
+ },
+ contentDescription = "img${i + 1}",
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .width(104.dp)
+ .height(80.dp)
+ .clip(RoundedCornerShape(8.dp))
+ )
+ }
+ }
+ //BottomSheet๊ฐ ํ์ฅ๋ ์ํ๊ฐ ์๋ ๋ (AddMenuScreen์์) ๋ณด์ฌ์ค ๋ฒํผ
+ if (scaffoldState.bottomSheetState.targetValue != SheetValue.Expanded) {
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Neutral300,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 12.dp)
+ )
+ BottomFullWidthButton(
+ containerColor = Primary500Main,
+ contentColor = NeutralWhite,
+ text = stringResource(R.string.next)
+ ) {
+ coroutineScope.launch {
+ scaffoldState.bottomSheetState.expand()
+ }
+ }
+ } else {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .padding(top = 12.dp)
+ ) {
+ itemsIndexed(storeInfo.menus) { index, menu ->
+ SelectMenuItem(
+ menu = menu,
+ isSelected = selectedMenuIndex == index,
+ onClick = { onItemClick(index) }
+ )
+ }
+ }
+
+ Column() {
+ BottomFullWidthButton(
+ containerColor = Neutral100,
+ contentColor = Neutral500,
+ text = stringResource(R.string.add_menu_by_myself)
+ ) {
+ //๋ฉ๋ด ์ถ๊ฐํ๋ฉด์ผ๋ก ์ด๋ํ๋ ๋ก์ง
+ }
+ Spacer(modifier = Modifier.padding(10.dp))
+ BottomFullWidthButton(
+ containerColor = if (enableNextButton) Primary500Main else Neutral400,
+ contentColor = NeutralWhite,
+ text = stringResource(R.string.next)
+ ) {
+ if (enableNextButton){
+ //๋ฒํผ ํ์ฑํ ๋ ๊ฒฝ์ฐ์ ๋์
+ onNavigateToAddMenuInfo()
+ }
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview(showBackground = true)
+@Composable
+private fun AddMenuBottomSheetContentPreview() {
+ val scaffoldState = rememberBottomSheetScaffoldState()
+ val viewModel: AddMenuViewModel = viewModel()
+ val storeInfo by viewModel.storeInfo.collectAsStateWithLifecycle()
+ val selectedMenuIndex by viewModel.selectedMenuIndex.collectAsStateWithLifecycle()
+
+ LaunchedEffect(Unit) {
+ //์๋ ์ฃผ์ ํด์ ํ๋ฉด bottom sheet ํ์ฅ๋ ์ํ ํ์ธ ๊ฐ๋ฅ
+ scaffoldState.bottomSheetState.expand()
+ }
+ AddMenuBottomSheetContent(
+ scaffoldState = scaffoldState,
+ storeInfo = storeInfo ?: CrawlingStoreDetailResponse(
+ storeId = "",
+ storeTitle = "",
+ storeAddress = "",
+ storeImgs = emptyList(),
+ menus = emptyList(),
+ storeMapX = 0.0,
+ storeMapY = 0.0
+ ),
+ selectedMenuIndex = selectedMenuIndex,
+ onNavigateToAddMenuInfo = {},
+ onItemClick = { index -> viewModel.updateSelectedMenu(index) }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/bottomsheet/IconSelectBottomSheet.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/bottomsheet/IconSelectBottomSheet.kt
new file mode 100644
index 00000000..932ac9d6
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/bottomsheet/IconSelectBottomSheet.kt
@@ -0,0 +1,105 @@
+package com.kuit.ourmenu.ui.addmenu.component.bottomsheet
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.common.BottomHalfWidthButton
+import com.kuit.ourmenu.ui.common.IconItemGroup
+import com.kuit.ourmenu.ui.theme.Neutral400
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+
+@Composable
+fun IconSelectBottomSheet(
+ modifier: Modifier = Modifier,
+ iconList: List,
+ onCancel: () -> Unit,
+ onConfirm: () -> Unit,
+ onIconClick: (Int) -> Unit
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp, vertical = 20.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ IconItemGroup(
+ modifier = modifier.fillMaxWidth(),
+ groupLabel = "์์ด์ฝ",
+ icons = iconList,
+ onIconSelect = onIconClick
+ )
+
+ Spacer(modifier = modifier.height(28.dp))
+
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ ,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ BottomHalfWidthButton(
+ modifier = modifier.weight(1f),
+ containerColor = Neutral400,
+ contentColor = NeutralWhite,
+ text = stringResource(R.string.cancel)
+ ) {
+ onCancel()
+ }
+ Spacer(modifier = modifier.width(12.dp))
+ BottomHalfWidthButton(
+ modifier = modifier.weight(1f),
+ containerColor = Primary500Main,
+ contentColor = NeutralWhite,
+ text = stringResource(R.string.apply)
+ ) {
+ onConfirm()
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun IconSelectBottomSheetPreview() {
+ val iconList = listOf(
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ )
+ IconSelectBottomSheet(
+ iconList = iconList,
+ onCancel = {},
+ onConfirm = {}
+ ){
+ // ์์ด์ฝ ์ ํ์ ๋์
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/bottomsheet/TagSelectBottomSheet.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/bottomsheet/TagSelectBottomSheet.kt
new file mode 100644
index 00000000..e9aadedc
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/bottomsheet/TagSelectBottomSheet.kt
@@ -0,0 +1,232 @@
+package com.kuit.ourmenu.ui.addmenu.component.bottomsheet
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.width
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.common.BottomHalfWidthButton
+import com.kuit.ourmenu.ui.common.OurSnackbarHost
+import com.kuit.ourmenu.ui.common.chip.TagChipGroup
+import com.kuit.ourmenu.ui.theme.Neutral400
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import kotlinx.coroutines.launch
+
+@Composable
+fun TagSelectBottomSheet(
+ modifier: Modifier = Modifier,
+ categoryTagList: List>,
+ nationalityTagList: List>,
+ tasteTagList: List>,
+ occasionTagList: List>,
+ selectedTagList: List,
+ onSelectedTagsChange: (List) -> Unit,
+ onApplyButtonClick: () -> Unit,
+ onTagClick: (String) -> Unit
+) {
+ // toast๋ฅผ ์ํ context
+ val context = LocalContext.current
+ val snackbarHostState = remember { SnackbarHostState() }
+ val scope = rememberCoroutineScope()
+
+ Box(modifier = Modifier.fillMaxSize()) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 20.dp),
+ verticalArrangement = Arrangement.SpaceBetween
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = stringResource(R.string.food_tag),
+ style = ourMenuTypography().pretendard_700_16
+ )
+ Text(
+ text = stringResource(R.string.multiple_choice_available),
+ style = ourMenuTypography().pretendard_600_14,
+ color = Neutral500
+ )
+ }
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ //์ข
๋ฅ
+ TagChipGroup(
+ groupLabel = stringResource(R.string.type),
+ tags = categoryTagList,
+ selectedTags = selectedTagList,
+ ) { tag ->
+ if (selectedTagList.size >= 12 && tag !in selectedTagList) {
+ scope.launch {
+ snackbarHostState.showSnackbar(context.getString(R.string.tag_number_warning))
+ }
+ } else {
+ onTagClick(tag)
+ }
+ }
+ //๋๋ผ ๋ณ ์์
+ TagChipGroup(
+ groupLabel = stringResource(R.string.nationality),
+ tags = nationalityTagList,
+ selectedTags = selectedTagList,
+ ) { tag ->
+ if (selectedTagList.size >= 12 && tag !in selectedTagList) {
+ scope.launch {
+ snackbarHostState.showSnackbar(context.getString(R.string.tag_number_warning))
+ }
+ } else {
+ onTagClick(tag)
+ }
+ }
+ //๋ง
+ TagChipGroup(
+ groupLabel = stringResource(R.string.taste),
+ tags = tasteTagList,
+ selectedTags = selectedTagList,
+ ) { tag ->
+ if (selectedTagList.size >= 12 && tag !in selectedTagList) {
+ scope.launch {
+ snackbarHostState.showSnackbar(context.getString(R.string.tag_number_warning))
+ }
+ } else {
+ onTagClick(tag)
+ }
+ }
+ //์ํฉ
+ TagChipGroup(
+ groupLabel = stringResource(R.string.occasion),
+ tags = occasionTagList,
+ selectedTags = selectedTagList,
+ ) { tag ->
+ if (selectedTagList.size >= 12 && tag !in selectedTagList) {
+ scope.launch {
+ snackbarHostState.showSnackbar(context.getString(R.string.tag_number_warning))
+ }
+ } else {
+ onTagClick(tag)
+ }
+ }
+ }
+
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(bottom = 20.dp),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ BottomHalfWidthButton(
+ modifier = modifier.weight(1f),
+ containerColor = Neutral400,
+ contentColor = NeutralWhite,
+ text = stringResource(R.string.reset)
+ ) {
+ // List ๋น์ฐ๊ธฐ
+ onSelectedTagsChange(emptyList())
+ }
+ Spacer(modifier = modifier.width(12.dp))
+ BottomHalfWidthButton(
+ modifier = modifier.weight(1f),
+ containerColor = Primary500Main,
+ contentColor = NeutralWhite,
+ text = stringResource(R.string.apply)
+ ) {
+ // TODO: ์์ด์ฝ ์ ํ์ผ๋ก ์ด๋
+ onApplyButtonClick()
+ }
+ }
+ }
+
+ OurSnackbarHost(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(bottom = 60.dp),
+ hostState = snackbarHostState,
+ isChecked = false,
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun TagSelectBottomSheetPreview() {
+ val categoryTags = listOf(
+ R.drawable.ic_tag_rice to "๋ฐฅ",
+ R.drawable.ic_tag_rice to "๋นต",
+ R.drawable.ic_tag_rice to "๋ฉด",
+ R.drawable.ic_tag_rice to "๊ณ ๊ธฐ",
+ R.drawable.ic_tag_rice to "์์ ",
+ R.drawable.ic_tag_rice to "์นดํ",
+ R.drawable.ic_tag_rice to "๋์ ํธ",
+ R.drawable.ic_tag_rice to "ํจ์คํธํธ๋",
+ )
+ val nationalityTags = listOf(
+ R.drawable.ic_tag_rice to "ํ์",
+ R.drawable.ic_tag_rice to "์ค์",
+ R.drawable.ic_tag_rice to "์ผ์",
+ R.drawable.ic_tag_rice to "์์",
+ R.drawable.ic_tag_rice to "์์์",
+ )
+ val tasteTags = listOf(
+ R.drawable.ic_tag_rice to "๋งค์ฝคํจ",
+ R.drawable.ic_tag_rice to "๋ฌ๋ฌํจ",
+ R.drawable.ic_tag_rice to "์์ํจ",
+ R.drawable.ic_tag_rice to "๋จ๋ํจ",
+ R.drawable.ic_tag_rice to "์ผํฐํจ",
+ )
+ val occasionTags = listOf(
+ R.drawable.ic_tag_rice to "ํผ๋ฐฅ",
+ R.drawable.ic_tag_rice to "๋น์ฆ๋์ค ๋ฏธํ
",
+ R.drawable.ic_tag_rice to "์น๊ตฌ ์ฝ์",
+ R.drawable.ic_tag_rice to "๋ฐ์ดํธ",
+ R.drawable.ic_tag_rice to "๋ฐฅ์ฝ",
+ R.drawable.ic_tag_rice to "๋จ์ฒด",
+ )
+ var selectedTags by rememberSaveable { mutableStateOf(listOf()) }
+
+ TagSelectBottomSheet(
+ categoryTagList = categoryTags,
+ nationalityTagList = nationalityTags,
+ tasteTagList = tasteTags,
+ occasionTagList = occasionTags,
+ selectedTagList = selectedTags,
+ onSelectedTagsChange = { newSelectedTags -> selectedTags = newSelectedTags },
+ onApplyButtonClick = {}
+ ) { tag ->
+ if (selectedTags.contains(tag)) {
+ selectedTags -= tag
+ } else {
+ selectedTags += tag
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/AddMenuAddedImageItem.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/AddMenuAddedImageItem.kt
new file mode 100644
index 00000000..2f757b5a
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/AddMenuAddedImageItem.kt
@@ -0,0 +1,95 @@
+package com.kuit.ourmenu.ui.addmenu.component.item
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+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.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun AddMenuAddedImageItem(
+ modifier: Modifier = Modifier,
+ img: Int,
+ isFirstItem: Boolean,
+ onDelete: () -> Unit
+) {
+ Card(
+ modifier = modifier
+ .size(88.dp, 72.dp),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Box(modifier = modifier.fillMaxSize()) {
+ Image(
+ modifier = modifier
+ .fillMaxSize()
+ .clip(RoundedCornerShape(12.dp)),
+ painter = painterResource(img),
+ contentDescription = "menu image",
+ contentScale = ContentScale.Crop
+ )
+
+ if (isFirstItem) {
+ Surface(
+ modifier = modifier
+ .align(Alignment.TopStart)
+ .padding(top = 4.dp, start = 6.dp)
+ .size(40.dp, 24.dp),
+ shape = RoundedCornerShape(8.dp),
+ color = NeutralWhite,
+ ) {
+ Box(modifier = modifier.fillMaxSize()) {
+ Text(
+ text = "๋ํ",
+ color = Primary500Main,
+ style = ourMenuTypography().pretendard_600_14,
+ modifier = modifier.align(Alignment.Center)
+ )
+ }
+ }
+ }
+
+ Image(
+ modifier = modifier
+ .align(Alignment.TopEnd)
+ .padding(top = 7.dp, end = 6.dp)
+ .clickable {
+ onDelete()
+ },
+ painter = painterResource(R.drawable.ic_addmenu_x),
+ contentDescription = "menu image",
+ contentScale = ContentScale.Crop
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun AddMenuAddedImageItemPreview() {
+ Column {
+ AddMenuAddedImageItem(img = R.drawable.img_dummy_pizza, isFirstItem = true) {
+
+ }
+ AddMenuAddedImageItem(img = R.drawable.img_dummy_pizza, isFirstItem = false) {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/AddMenuInfoAddressFieldItem.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/AddMenuInfoAddressFieldItem.kt
new file mode 100644
index 00000000..1f248eea
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/AddMenuInfoAddressFieldItem.kt
@@ -0,0 +1,117 @@
+package com.kuit.ourmenu.ui.addmenu.component.item
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+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.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.common.CustomTextField
+import com.kuit.ourmenu.ui.theme.Neutral100
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun AddMenuInfoAddressFieldItem(
+ autoInput: Boolean,
+ mainAddressText: String,
+ detailedAddressText: String,
+ onMainAddressChange: (String) -> Unit,
+ onDetailedAddressChange: (String) -> Unit,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 20.dp),
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 4.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.resaturant_address),
+ style = ourMenuTypography().pretendard_600_14
+ )
+ Text(
+ text = stringResource(R.string.asterisk),
+ style = ourMenuTypography().pretendard_600_14,
+ color = Primary500Main
+ )
+ }
+ CustomTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(44.dp)
+ .border(1.dp, Neutral300, RoundedCornerShape(8.dp)),
+ text = mainAddressText,
+ onTextChange = onMainAddressChange,
+ shape = RoundedCornerShape(8.dp),
+ paddingValues = PaddingValues(start = 28.dp, top = 12.dp, bottom = 12.dp),
+ containerColor = Neutral100,
+ placeHolder = {
+ Text(
+ text = stringResource(R.string.type_store_address),
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral500
+ )
+ },
+ textStyle = ourMenuTypography().pretendard_700_14.copy(color = Neutral700)
+ )
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ CustomTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(44.dp)
+ .border(1.dp, Neutral300, RoundedCornerShape(8.dp)),
+ text = detailedAddressText,
+ onTextChange = onDetailedAddressChange,
+ shape = RoundedCornerShape(8.dp),
+ paddingValues = PaddingValues(start = 28.dp, top = 12.dp, bottom = 12.dp),
+ containerColor = Neutral100,
+ placeHolder = {
+ Text(
+ text = stringResource(R.string.type_restaurant_detailed_address),
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral500
+ )
+ },
+ textStyle = ourMenuTypography().pretendard_700_14.copy(color = Neutral700)
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun AddMenuInfoAddressFieldItemPreview() {
+ var mainAddressText by rememberSaveable { mutableStateOf("") }
+ var detailedAddressText by rememberSaveable { mutableStateOf("") }
+ AddMenuInfoAddressFieldItem(
+ autoInput = true,
+ mainAddressText = mainAddressText,
+ detailedAddressText = detailedAddressText,
+ onMainAddressChange = { mainAddressText = it },
+ onDetailedAddressChange = { detailedAddressText = it },
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/AddMenuInfoMenuBoardFieldItem.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/AddMenuInfoMenuBoardFieldItem.kt
new file mode 100644
index 00000000..12324bcf
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/AddMenuInfoMenuBoardFieldItem.kt
@@ -0,0 +1,181 @@
+package com.kuit.ourmenu.ui.addmenu.component.item
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+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.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.common.CustomTextField
+import com.kuit.ourmenu.ui.theme.Neutral100
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral400
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun AddMenuInfoMenuBoardFieldItem(
+ modifier: Modifier = Modifier,
+ options: List,
+ selectedOption: String,
+ onSelectedOptionChange: (String) -> Unit
+) {
+ var expanded by remember { mutableStateOf(false) }
+
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(bottom = 20.dp),
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(bottom = 4.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.menuboard),
+ style = ourMenuTypography().pretendard_600_14
+ )
+ Text(
+ text = stringResource(R.string.asterisk),
+ style = ourMenuTypography().pretendard_600_14,
+ color = Primary500Main
+ )
+ }
+
+ Box(modifier = modifier.fillMaxWidth()) {
+ CustomTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(44.dp)
+ .border(1.dp, Neutral300, RoundedCornerShape(8.dp))
+ .padding(end = 6.dp)
+ .clickable {
+ expanded = true
+ },
+ text = selectedOption,
+ onTextChange = {},
+ shape = RoundedCornerShape(8.dp),
+ paddingValues = PaddingValues(start = 28.dp, top = 12.dp, bottom = 12.dp),
+ containerColor = Neutral100,
+ placeHolder = {
+ Text(
+ text = stringResource(R.string.save_menuboard),
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral500
+ )
+ },
+ enabled = false,
+ textStyle = ourMenuTypography().pretendard_700_14.copy(color = Neutral700),
+ trailingIcon = {
+ Button(
+ modifier = modifier.size(44.dp, 32.dp),
+ onClick = {
+ if(expanded) expanded = false
+ },
+ shape = RoundedCornerShape(8.dp),
+ colors = ButtonDefaults.buttonColors(if (selectedOption.isBlank()) Neutral400 else Primary500Main),
+ contentPadding = PaddingValues(0.dp)
+ ) {
+ Text(
+ text = "ํ์ธ",
+ style = ourMenuTypography().pretendard_700_12.copy(
+ color = NeutralWhite
+ )
+ )
+ }
+ }
+ )
+ DropdownMenu(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(color = NeutralWhite, shape = RoundedCornerShape(8.dp)),
+ expanded = expanded,
+ onDismissRequest = { expanded = false }
+ ) {
+ options.forEach { option ->
+ val isSelected = option == selectedOption
+ DropdownMenuItem(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(
+ color = if (isSelected) Neutral300 else NeutralWhite,
+ shape = RoundedCornerShape(8.dp)
+ ),
+ text = {
+ Row(
+ modifier = modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Spacer(modifier = Modifier.size(28.dp))
+ Icon(
+ painter = painterResource(if (isSelected) R.drawable.ic_dropdown_checked else R.drawable.ic_dropdown_unchecked),
+ contentDescription = "check icon",
+ tint = Color.Unspecified
+ )
+ Spacer(modifier = Modifier.size(10.dp))
+ Text(
+ text = option,
+ style = ourMenuTypography().pretendard_700_14.copy(
+ color = if (isSelected) Neutral700 else Neutral500
+ )
+ )
+ }
+ },
+ onClick = {
+ onSelectedOptionChange(option)
+ }
+ )
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun AddMenuInfoMenuBoardFieldItemPreview() {
+ var options = listOf("์ต์
1", "์ต์
2", "์ต์
3")
+ var selectedOption by rememberSaveable { mutableStateOf("") }
+ Column(modifier = Modifier.fillMaxSize()) {
+ AddMenuInfoMenuBoardFieldItem(
+ options = options,
+ selectedOption = selectedOption
+ ){
+ selectedOption = it
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/AddMenuInfoTextFieldItem.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/AddMenuInfoTextFieldItem.kt
new file mode 100644
index 00000000..4f71c364
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/AddMenuInfoTextFieldItem.kt
@@ -0,0 +1,123 @@
+package com.kuit.ourmenu.ui.addmenu.component.item
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+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.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.common.CustomTextField
+import com.kuit.ourmenu.ui.theme.Neutral100
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun AddMenuInfoTextFieldItem(
+ fieldName: String,
+ autoInput: Boolean,
+ text: String,
+ onTextChange: (String) -> Unit,
+ placeholder: String,
+ isPriceInfo: Boolean = false,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 20.dp),
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 4.dp)
+ ) {
+ Text(
+ text = fieldName,
+ style = ourMenuTypography().pretendard_600_14
+ )
+ Text(
+ text = stringResource(R.string.asterisk),
+ style = ourMenuTypography().pretendard_600_14,
+ color = Primary500Main
+ )
+ }
+ CustomTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(44.dp)
+ .border(1.dp, Neutral300, RoundedCornerShape(8.dp)),
+ text = text,
+ onTextChange = onTextChange,
+ shape = RoundedCornerShape(8.dp),
+ paddingValues = PaddingValues(start = 28.dp, end = 28.dp, top = 12.dp, bottom = 12.dp),
+ containerColor = Neutral100,
+ placeHolder = {
+ Text(
+ text = placeholder,
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral500
+ )
+ },
+ textStyle = ourMenuTypography().pretendard_700_14.copy(color = Neutral700),
+ keyboardOptions = if(isPriceInfo) {
+ KeyboardOptions(
+ keyboardType = KeyboardType.Number,
+ )
+ }else{
+ KeyboardOptions.Default
+ }
+ )
+
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun AddMenuInfoTextFieldItemPreview() {
+ var priceText by rememberSaveable { mutableStateOf("") }
+ var storeNameText by rememberSaveable { mutableStateOf("") }
+
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ AddMenuInfoTextFieldItem(
+ fieldName = stringResource(R.string.menu_price),
+ autoInput = false,
+ text = priceText,
+ onTextChange = { priceText = it },
+ placeholder = stringResource(R.string.type_menu_price),
+ isPriceInfo = true
+ )
+ AddMenuInfoTextFieldItem(
+ fieldName = stringResource(R.string.store_name),
+ autoInput = true,
+ text = storeNameText,
+ onTextChange = { storeNameText = it },
+ placeholder = stringResource(R.string.type_store_name)
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/FoodTagItem.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/FoodTagItem.kt
new file mode 100644
index 00000000..8d088695
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/FoodTagItem.kt
@@ -0,0 +1,64 @@
+package com.kuit.ourmenu.ui.addmenu.component.item
+
+import androidx.compose.material3.AssistChip
+import androidx.compose.material3.AssistChipDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun FoodTagItem(
+ label: String,
+ iconId: Int,
+ isSelected: Boolean,
+ onSelected: () -> Unit
+) {
+ AssistChip(
+ onClick = {
+ // ํด๋ฆญ ์ด๋ฒคํธ ์ฒ๋ฆฌ
+ onSelected()
+ },
+ label = {
+ Text(
+ text = label,
+ style = ourMenuTypography().pretendard_700_16
+ )
+ },
+ leadingIcon = {
+ Icon(
+ painterResource(iconId),
+ contentDescription = null,
+ tint = if(isSelected) NeutralWhite else Neutral900
+ )
+ },
+ colors = AssistChipDefaults.assistChipColors(
+ containerColor = if (isSelected) Primary500Main else NeutralWhite,
+ labelColor = if (isSelected) NeutralWhite else Neutral900
+ )
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun FoodTagPreview() {
+ var isSelected by rememberSaveable { mutableStateOf(false) }
+ FoodTagItem(
+ label = "๋ฐฅ",
+ iconId = R.drawable.ic_tag_rice,
+ isSelected = isSelected,
+ onSelected = {
+ isSelected = !isSelected
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/IconItem.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/IconItem.kt
new file mode 100644
index 00000000..821b5b97
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/IconItem.kt
@@ -0,0 +1,55 @@
+package com.kuit.ourmenu.ui.addmenu.component.item
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+
+@Composable
+fun IconItem(
+ iconId: Int,
+ onSelected: () -> Unit
+) {
+ Box(
+ modifier = Modifier.size(36.dp).
+ clickable { onSelected() },
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_icon_ellipse),
+ contentDescription = "icon ellipse",
+ tint = Color.Unspecified
+ )
+ Icon(
+ painter = painterResource(iconId),
+ contentDescription = "icon",
+ tint = NeutralWhite,
+ modifier = Modifier.size(20.dp)
+ )
+ }
+}
+
+@Preview
+@Composable
+private fun IconItemPreview() {
+ var isSelected by rememberSaveable { mutableStateOf(false) }
+ IconItem(
+ iconId = R.drawable.ic_tag_rice,
+ onSelected = {
+ isSelected = !isSelected
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/SelectMenuItem.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/SelectMenuItem.kt
new file mode 100644
index 00000000..ab97d7d6
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/SelectMenuItem.kt
@@ -0,0 +1,118 @@
+package com.kuit.ourmenu.ui.addmenu.component.item
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.data.model.map.response.CrawlingMenuDetail
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun SelectMenuItem(
+ menu: CrawlingMenuDetail,
+ isSelected: Boolean = false,
+ onClick: () -> Unit
+) {
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Neutral300,
+ modifier = Modifier.fillMaxWidth()
+ )
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 18.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column {
+ Text(
+ text = menu.menuTitle,
+ style = ourMenuTypography().pretendard_700_16
+ )
+ Text(
+ text = menu.menuPrice,
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral700,
+ )
+ }
+ if (isSelected) {
+ Button(
+ onClick = onClick,
+ shape = RoundedCornerShape(16.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Primary500Main
+ ),
+ modifier = Modifier.size(44.dp, 28.dp),
+ contentPadding = PaddingValues(0.dp)
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_btn_check_white),
+ contentDescription = "selected button"
+ )
+ }
+ } else {
+ Button(
+ onClick = onClick,
+ shape = RoundedCornerShape(16.dp),
+ border = BorderStroke(1.dp, Primary500Main),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = NeutralWhite,
+ contentColor = Primary500Main
+ ),
+ modifier = Modifier.size(44.dp, 28.dp),
+ contentPadding = PaddingValues(0.dp)
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_btn_plus_orange),
+ contentDescription = "unselected button"
+ )
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SelectedMenuItemPreview() {
+ Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) {
+ SelectMenuItem(
+ menu = CrawlingMenuDetail(
+ menuTitle = "๋ก๋ณถ์ด",
+ menuPrice = "14,000์"
+ ),
+ isSelected = false,
+ onClick = {}
+ )
+ SelectMenuItem(
+ menu = CrawlingMenuDetail(
+ menuTitle = "์นํจ",
+ menuPrice = "18,000์"
+ ),
+ isSelected = true,
+ onClick = {}
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/SelectedTagItem.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/SelectedTagItem.kt
new file mode 100644
index 00000000..81c5bf50
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/SelectedTagItem.kt
@@ -0,0 +1,62 @@
+package com.kuit.ourmenu.ui.addmenu.component.item
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.AssistChip
+import androidx.compose.material3.AssistChipDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun SelectedTagItem(
+ modifier: Modifier = Modifier,
+ label: String,
+ onTagClick: (String) -> Unit
+) {
+ AssistChip(
+ modifier = modifier
+ .height(32.dp),
+ onClick = {
+ onTagClick(label)
+ },
+ label = {
+ Text(
+ text = label,
+ style = ourMenuTypography().pretendard_700_16
+ )
+ },
+ trailingIcon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_addmenu_x),
+ contentDescription = "x",
+ tint = NeutralWhite
+ )
+ },
+ colors = AssistChipDefaults.assistChipColors(
+ containerColor = Primary500Main,
+ labelColor = NeutralWhite,
+ ),
+ border = BorderStroke(0.dp, Primary500Main),
+ shape = RoundedCornerShape(8.dp)
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SelectedTagItemPreview() {
+ SelectedTagItem(
+ label = "๋ฐฅ๋ฐฅ๋ฐฅ"
+ ){
+ // ํด๋ฆญ์ ๋์
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/StoreResultItem.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/StoreResultItem.kt
new file mode 100644
index 00000000..42afe47e
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/StoreResultItem.kt
@@ -0,0 +1,72 @@
+package com.kuit.ourmenu.ui.addmenu.component.item
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.data.model.map.response.CrawlingStoreDetailResponse
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun StoreSearchResultItem(
+ resultItem: CrawlingStoreDetailResponse,
+ onClick: () -> Unit = {}
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = onClick)
+ .padding(vertical = 20.dp, horizontal = 20.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_fill_map_20),
+ contentDescription = "location info",
+ tint = Color.Unspecified
+ )
+ Column(modifier = Modifier.padding(start = 20.dp)) {
+ Text(
+ text = resultItem.storeTitle,
+ style = ourMenuTypography().pretendard_600_16,
+ )
+ Text(
+ text = resultItem.storeAddress,
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral700
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun RestaurantSearchItemPreview() {
+ Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) {
+ StoreSearchResultItem(
+ resultItem = CrawlingStoreDetailResponse(
+ storeTitle = stringResource(R.string.our_ddeokbokki),
+ storeAddress = stringResource(R.string.resaturant_address),
+ storeId = TODO(),
+ storeImgs = TODO(),
+ menus = TODO(),
+ storeMapX = TODO(),
+ storeMapY = TODO(),
+ )
+ )
+ }
+}
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/StoreSearchItem.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/StoreSearchItem.kt
new file mode 100644
index 00000000..6966b725
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/component/item/StoreSearchItem.kt
@@ -0,0 +1,68 @@
+package com.kuit.ourmenu.ui.addmenu.component.item
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.data.model.map.response.CrawlingHistoryResponse
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun StoreSearchHistoryItem(
+ historyItem: CrawlingHistoryResponse,
+ onClick: () -> Unit = {}
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = onClick)
+ .padding(vertical = 20.dp, horizontal = 20.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_fill_map_20),
+ contentDescription = "location info",
+ tint = Color.Unspecified
+ )
+ Column(modifier = Modifier.padding(start = 20.dp)) {
+ Text(
+ text = historyItem.menuTitle,
+ style = ourMenuTypography().pretendard_600_16,
+ )
+ Text(
+ text = historyItem.storeAddress,
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral700
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun RestaurantSearchItemPreview() {
+ Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) {
+ StoreSearchHistoryItem(
+ historyItem = CrawlingHistoryResponse(
+ menuTitle = stringResource(R.string.our_ddeokbokki),
+ storeAddress = stringResource(R.string.resaturant_address),
+ modifiedAt = "2023-10-01T12:00:00Z",
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/navigation/AddMenuNavigation.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/navigation/AddMenuNavigation.kt
new file mode 100644
index 00000000..e0c9403d
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/navigation/AddMenuNavigation.kt
@@ -0,0 +1,28 @@
+package com.kuit.ourmenu.ui.addmenu.navigation
+
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import com.kuit.ourmenu.ui.addmenu.screen.AddMenuInfoScreen
+import com.kuit.ourmenu.ui.addmenu.screen.AddMenuScreen
+import com.kuit.ourmenu.ui.navigator.Routes
+
+
+fun NavController.navigateToAddMenuInfo() {
+ navigate(Routes.AddMenuInfo)
+}
+
+fun NavGraphBuilder.addMenuNavGraph(
+ navigateBack: () -> Unit,
+ navigateToAddMenuInfo: () -> Unit
+) {
+ composable {
+ AddMenuScreen(
+ onNavigateToAddMenuInfo = navigateToAddMenuInfo
+ )
+ }
+
+ composable {
+ AddMenuInfoScreen()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/screen/AddMenuInfoScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/screen/AddMenuInfoScreen.kt
new file mode 100644
index 00000000..caf1511b
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/screen/AddMenuInfoScreen.kt
@@ -0,0 +1,146 @@
+package com.kuit.ourmenu.ui.addmenu.screen
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.addmenu.component.AddMenuAddImageComponent
+import com.kuit.ourmenu.ui.addmenu.component.item.AddMenuInfoAddressFieldItem
+import com.kuit.ourmenu.ui.addmenu.component.item.AddMenuInfoMenuBoardFieldItem
+import com.kuit.ourmenu.ui.addmenu.component.item.AddMenuInfoTextFieldItem
+import com.kuit.ourmenu.ui.common.BottomFullWidthButton
+import com.kuit.ourmenu.ui.common.topappbar.OurMenuBackButtonTopAppBar
+import com.kuit.ourmenu.ui.theme.Neutral400
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+
+@Composable
+fun AddMenuInfoScreen(autoInput: Boolean = true) {
+ var menuBoardText by rememberSaveable { mutableStateOf("") }
+ var menuNameText by rememberSaveable { mutableStateOf("") }
+ var priceText by rememberSaveable { mutableStateOf("") }
+ var storeNameText by rememberSaveable { mutableStateOf("") }
+ var mainAddressText by rememberSaveable { mutableStateOf("") }
+ var detailedAddressText by rememberSaveable { mutableStateOf("") }
+ var imgList by rememberSaveable {
+ mutableStateOf(
+ listOf(
+ R.drawable.img_dummy_pizza,
+ R.drawable.img_dummy_pizza,
+ R.drawable.img_dummy_pizza,
+ R.drawable.img_dummy_pizza,
+ )
+ )
+ }
+ var options = listOf("์ต์
1", "์ต์
2", "์ต์
3")
+ var selectedOption by rememberSaveable { mutableStateOf("") }
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ topBar = {
+ OurMenuBackButtonTopAppBar {
+ Text(
+ text = stringResource(R.string.add_menu),
+ style = ourMenuTypography().pretendard_600_18
+ )
+ }
+ },
+ bottomBar = {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ ) {
+ BottomFullWidthButton(
+ containerColor = Neutral400,
+ contentColor = NeutralWhite,
+ text = stringResource(R.string.next)
+ ) {
+ TODO()
+ }
+ }
+ },
+ content = { paddingValues ->
+ Column(
+ modifier = Modifier
+ .padding(paddingValues)
+ .padding(start = 20.dp, end = 20.dp)
+ .fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 20.dp)
+ ) {
+ AddMenuAddImageComponent(imgList = imgList) { index ->
+ imgList = imgList.toMutableList().apply {
+ removeAt(index)
+ }
+ }
+ }
+
+ Column(modifier = Modifier.fillMaxWidth()) {
+ AddMenuInfoMenuBoardFieldItem(
+ options = options,
+ selectedOption = selectedOption
+ ){
+ selectedOption = it
+ }
+
+ AddMenuInfoTextFieldItem(
+ fieldName = stringResource(R.string.menu_name),
+ autoInput = autoInput,
+ text = menuNameText,
+ onTextChange = { menuNameText = it },
+ placeholder = stringResource(R.string.type_menu_name)
+ )
+ AddMenuInfoTextFieldItem(
+ fieldName = stringResource(R.string.menu_price),
+ autoInput = autoInput,
+ text = priceText,
+ onTextChange = { priceText = it },
+ placeholder = stringResource(R.string.type_menu_price),
+ isPriceInfo = true
+ )
+ AddMenuInfoTextFieldItem(
+ fieldName = stringResource(R.string.store_name),
+ text = storeNameText,
+ autoInput = autoInput,
+ onTextChange = { storeNameText = it },
+ placeholder = stringResource(R.string.type_store_name)
+ )
+
+ AddMenuInfoAddressFieldItem(
+ autoInput = autoInput,
+ mainAddressText = mainAddressText,
+ onMainAddressChange = { mainAddressText = it },
+ detailedAddressText = detailedAddressText,
+ onDetailedAddressChange = { detailedAddressText = it }
+ )
+ }
+ }
+ }
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun AddMenuInfoScreenPreview() {
+ //๊ฐ๊ฒ์ ๋ฉ๋ด ์ง์ ์ถ๊ฐํ๊ธฐ๋ฅผ ํตํด ํด๋น ํ๋ฉด์ผ๋ก ์ค๋ ๊ฒฝ์ฐ์๋ ์ธ์๋ก false๋ฅผ ๋๊ฒจ์ค๋ค
+// AddMenuInfoScreen(true)
+ AddMenuInfoScreen(false)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/screen/AddMenuScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/screen/AddMenuScreen.kt
new file mode 100644
index 00000000..0aa1d420
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/screen/AddMenuScreen.kt
@@ -0,0 +1,266 @@
+package com.kuit.ourmenu.ui.addmenu.screen
+
+import android.Manifest
+import android.util.Log
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.background
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsFocusedAsState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+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.shape.RoundedCornerShape
+import androidx.compose.material3.BottomSheetScaffold
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberBottomSheetScaffoldState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+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.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.data.model.map.response.CrawlingStoreDetailResponse
+import com.kuit.ourmenu.ui.addmenu.component.AddMenuSearchBackground
+import com.kuit.ourmenu.ui.addmenu.component.bottomsheet.AddMenuBottomSheetContent
+import com.kuit.ourmenu.ui.addmenu.viewmodel.AddMenuViewModel
+import com.kuit.ourmenu.ui.common.SearchTextField
+import com.kuit.ourmenu.ui.common.map.mapViewWithLifecycle
+import com.kuit.ourmenu.ui.common.topappbar.OurMenuBackButtonTopAppBar
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import com.kuit.ourmenu.utils.PermissionHandler
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AddMenuScreen(
+ modifier: Modifier = Modifier,
+ viewModel: AddMenuViewModel = hiltViewModel(),
+ onNavigateToAddMenuInfo: () -> Unit
+) {
+ var scaffoldState = rememberBottomSheetScaffoldState()
+ var showBottomSheet by rememberSaveable { mutableStateOf(false) }
+ var showSearchBackground by rememberSaveable { mutableStateOf(false) }
+ var searchText by rememberSaveable { mutableStateOf("") }
+ var searchActionDone by rememberSaveable { mutableStateOf(false) }
+ val interactionSource = remember { MutableInteractionSource() }
+ val searchBarFocused by interactionSource.collectIsFocusedAsState()
+ val focusManager = LocalFocusManager.current
+ val scope = rememberCoroutineScope()
+ val context = LocalContext.current
+
+ val recentSearchResults by viewModel.searchHistory.collectAsStateWithLifecycle()
+ val searchResults by viewModel.searchResult.collectAsStateWithLifecycle()
+ val storeInfo by viewModel.storeInfo.collectAsStateWithLifecycle()
+ val selectedMenuIndex by viewModel.selectedMenuIndex.collectAsStateWithLifecycle() // storeInfo์์ ์ ํ๋ ๋ฉ๋ด์ ์ธ๋ฑ์ค
+ val locationPermissionGranted by viewModel.locationPermissionGranted.collectAsStateWithLifecycle()
+
+ // ์ง๋ ์ค์ฌ ์ขํ ์ํ
+ val currentCenter by viewModel.currentCenter.collectAsState()
+
+ // Collect history data from ViewModel
+ val searchHistory by viewModel.searchHistory.collectAsState()
+
+ // ๊ถํ ํ์ฉ์ด ์๋ ๊ฒฝ์ฐ ๊ถํ ์์ฒญ
+ if (!locationPermissionGranted) {
+ PermissionHandler(
+ permission = Manifest.permission.ACCESS_FINE_LOCATION,
+ rationaleMessage = "Location permission is required to show your current location on the map",
+ onPermissionGranted = {
+ viewModel.updateLocationPermission(true)
+ },
+ onPermissionDenied = {
+ viewModel.updateLocationPermission(false)
+ }
+ )
+ }
+
+ val mapView = mapViewWithLifecycle(
+ mapController = viewModel.mapController
+ ) { kakaoMap ->
+ // ์ด๋ฏธ permission ํ์ฉํ ๊ฒฝ์ฐ
+ if (locationPermissionGranted) {
+ viewModel.initializeMap(kakaoMap, context)
+ }
+ }
+
+ // permission ์ฌ๋ถ ๋ณํ ๊ฒฝ์ฐ์ ๋ฐ์
+ LaunchedEffect(locationPermissionGranted) {
+ if (locationPermissionGranted) {
+ viewModel.mapController.kakaoMap.value?.let { kakaoMap ->
+ viewModel.initializeMap(kakaoMap, context)
+ }
+ }
+ }
+
+ LaunchedEffect(searchBarFocused) {
+ if (searchBarFocused) {
+ showSearchBackground = true
+ showBottomSheet = false
+
+ scope.launch {
+ viewModel.getCrawlingHistory()
+ }
+ }
+ }
+
+ BackHandler(enabled = showSearchBackground) {
+ if (searchBarFocused) focusManager.clearFocus()
+ searchActionDone = false
+ showSearchBackground = false
+ searchText = ""
+ }
+
+ BottomSheetScaffold(
+ scaffoldState = scaffoldState,
+ topBar = {
+ OurMenuBackButtonTopAppBar {
+ Text(
+ text = stringResource(R.string.ourmenu),
+ style = ourMenuTypography().pretendard_600_18,
+ color = Primary500Main,
+ )
+ }
+ },
+ sheetContainerColor = Color.White,
+ sheetContent = {
+ //bottom sheet ๊ตฌ์ฑ
+ AddMenuBottomSheetContent(
+ scaffoldState = scaffoldState,
+ storeInfo = storeInfo ?: CrawlingStoreDetailResponse(
+ storeId = "",
+ storeTitle = "",
+ storeAddress = "",
+ storeImgs = emptyList(),
+ menus = emptyList(),
+ storeMapX = 0.0,
+ storeMapY = 0.0
+ ),
+ selectedMenuIndex = selectedMenuIndex,
+ onNavigateToAddMenuInfo = {
+ onNavigateToAddMenuInfo()
+ },
+ onItemClick = { index -> viewModel.updateSelectedMenu(index) })
+ },
+ //์กฐ๊ฑด ๋ง์กฑํ๋ฉด bottom sheet ๋ณด์ฌ์ฃผ๊ณ , ์๋๋ฉด ํ๋ฉด์ ์๋ณด์ด๋๋ก ์ฒ๋ฆฌ
+ sheetPeekHeight = if (showBottomSheet) 254.dp else 0.dp,
+ sheetDragHandle = {
+ // ์ปค์คํ
ํธ๋ค
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 12.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Box(
+ modifier = Modifier
+ .width(120.dp)
+ .height(4.dp)
+ .clip(RoundedCornerShape(6.dp))
+ .background(Neutral300)
+ )
+ }
+ }
+ ) {
+ Box(modifier = Modifier.fillMaxWidth()) {
+ if (!showSearchBackground) {
+ //์ง๋ ์ปดํฌ๋ํธ
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ AndroidView(
+ modifier = Modifier,
+ factory = { mapView }
+ ) { view ->
+
+ }
+ }
+ } else {
+ //๊ฒ์ ์ปดํฌ๋ํธ
+ AddMenuSearchBackground(
+ searchActionDone = searchActionDone,
+ searchHistory = recentSearchResults,
+ searchResults = searchResults
+ ) {
+ //๊ฒ์๋ ์์ดํ
ํด๋ฆญ์ ์๋ํ ํจ์
+ if (searchBarFocused) focusManager.clearFocus()
+ showSearchBackground = false
+ showBottomSheet = true
+ searchText = ""
+ }
+ }
+
+ SearchTextField(
+ modifier = Modifier.padding(top = 12.dp, start = 20.dp, end = 20.dp),
+ text = searchText,
+ onTextChange = {
+ searchText = it
+ showSearchBackground = true
+ showBottomSheet = false
+ },
+ interactionSource = interactionSource,
+ placeHolder = R.string.search_store_name
+ ) {
+ //onSearch ํจ์
+ if (searchBarFocused) focusManager.clearFocus()
+ searchActionDone = true
+
+ if (searchText.isNotEmpty()){
+ viewModel.updateCurrentCenter()
+
+ val center = viewModel.getCurrentCoordinates()
+ if (center != null) {
+ val (latitude, longitude) = center
+ Log.d("SearchMenuScreen", "๊ฒ์ ์์น: $latitude, $longitude")
+
+ // ๊ฒ์์ด์ ํ์ฌ ์ขํ๋ก ํฌ๋กค๋ง ์คํ ์ด ์ ๋ณด ์์ฒญ
+ viewModel.getCrawlingStoreInfo(
+ query = searchText,
+ long = longitude,
+ lat = latitude
+ )
+
+ showBottomSheet = true
+ showSearchBackground = false
+ }
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun AddMenuScreenPreview() {
+ val viewModel : AddMenuViewModel = hiltViewModel()
+ AddMenuScreen(
+ modifier = Modifier,
+ viewModel = viewModel,
+ onNavigateToAddMenuInfo = {}
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/screen/AddMenuTagScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/screen/AddMenuTagScreen.kt
new file mode 100644
index 00000000..6835cf7c
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/screen/AddMenuTagScreen.kt
@@ -0,0 +1,366 @@
+package com.kuit.ourmenu.ui.addmenu.screen
+
+import android.util.Log
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+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.verticalScroll
+import androidx.compose.material3.BottomSheetScaffold
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberBottomSheetScaffoldState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.addmenu.component.SelectedTagGroup
+import com.kuit.ourmenu.ui.addmenu.component.bottomsheet.IconSelectBottomSheet
+import com.kuit.ourmenu.ui.addmenu.component.bottomsheet.TagSelectBottomSheet
+import com.kuit.ourmenu.ui.common.BottomFullWidthButton
+import com.kuit.ourmenu.ui.common.CustomTextField
+import com.kuit.ourmenu.ui.common.bottomsheet.BottomSheetDragHandle
+import com.kuit.ourmenu.ui.common.topappbar.OurMenuBackButtonTopAppBar
+import com.kuit.ourmenu.ui.theme.Neutral100
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AddMenuTagScreen(modifier: Modifier = Modifier) {
+ var scaffoldState = rememberBottomSheetScaffoldState()
+ var showBottomSheet by remember { mutableStateOf(false) }
+ var showTagBottomSheet by rememberSaveable { mutableStateOf(true) }
+ var memoTitle by rememberSaveable { mutableStateOf("") }
+ var memoBody by rememberSaveable { mutableStateOf("") }
+ val scrollState = rememberScrollState()
+ var enableAddButton by rememberSaveable { mutableStateOf(false) }
+
+ // ์ ํ๋ ํ๊ทธ ๊ฐ์๋ฅผ ์ํ ๋ณ์
+ var selectedTags by rememberSaveable { mutableStateOf(listOf()) }
+ var selectedIcon by rememberSaveable { mutableStateOf(0) }
+
+ // ์์๋ฅผ ์ํ dummy list๋ค
+ val categoryTags = listOf(
+ R.drawable.ic_tag_rice to "๋ฐฅ",
+ R.drawable.ic_tag_rice to "๋นต",
+ R.drawable.ic_tag_rice to "๋ฉด",
+ R.drawable.ic_tag_rice to "๊ณ ๊ธฐ",
+ R.drawable.ic_tag_rice to "์์ ",
+ R.drawable.ic_tag_rice to "์นดํ",
+ R.drawable.ic_tag_rice to "๋์ ํธ",
+ R.drawable.ic_tag_rice to "ํจ์คํธํธ๋",
+ )
+ val nationalityTags = listOf(
+ R.drawable.ic_tag_rice to "ํ์",
+ R.drawable.ic_tag_rice to "์ค์",
+ R.drawable.ic_tag_rice to "์ผ์",
+ R.drawable.ic_tag_rice to "์์",
+ R.drawable.ic_tag_rice to "์์์",
+ )
+ val tasteTags = listOf(
+ R.drawable.ic_tag_rice to "๋งค์ฝคํจ",
+ R.drawable.ic_tag_rice to "๋ฌ๋ฌํจ",
+ R.drawable.ic_tag_rice to "์์ํจ",
+ R.drawable.ic_tag_rice to "๋จ๋ํจ",
+ R.drawable.ic_tag_rice to "์ผํฐํจ",
+ )
+ val occasionTags = listOf(
+ R.drawable.ic_tag_rice to "ํผ๋ฐฅ",
+ R.drawable.ic_tag_rice to "๋น์ฆ๋์ค ๋ฏธํ
",
+ R.drawable.ic_tag_rice to "์น๊ตฌ ์ฝ์",
+ R.drawable.ic_tag_rice to "๋ฐ์ดํธ",
+ R.drawable.ic_tag_rice to "๋ฐฅ์ฝ",
+ R.drawable.ic_tag_rice to "๋จ์ฒด",
+ )
+ // ์์ด์ฝ bottom sheet์ ์ฌ์ฉํ list
+ val iconList = listOf(
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ )
+
+ val coroutineScope = rememberCoroutineScope()
+
+ LaunchedEffect(scaffoldState.bottomSheetState) {
+ snapshotFlow { scaffoldState.bottomSheetState.currentValue }
+ .collect { state ->
+ Log.d("AddMenuTagScreen", "BottomSheetState changed: $state")
+ }
+ }
+
+ //๋ฉ๋ชจ๊ฐ ๋น์ด์์ง ์๊ณ , ํ๊ทธ์ ์์ด์ฝ์ด ์ ํ๋ ๊ฒฝ์ฐ์ ๋ฉ๋ด ๋ฑ๋กํ๊ธฐ ๋ฒํผ ํ์ฑํ
+ enableAddButton = memoTitle.isNotBlank() && memoBody.isNotBlank() && selectedTags.isNotEmpty()
+
+ BottomSheetScaffold(
+ scaffoldState = scaffoldState,
+ topBar = {
+ OurMenuBackButtonTopAppBar {
+ Text(
+ text = stringResource(R.string.add_menu),
+ style = ourMenuTypography().pretendard_600_18,
+ color = Neutral900,
+ )
+ }
+ },
+ sheetContainerColor = Color.White,
+ sheetContent = {
+ if (showTagBottomSheet) {
+ //์์ ํ๊ทธ ์ ํํ๋ bottom sheet
+ TagSelectBottomSheet(
+ categoryTagList = categoryTags,
+ nationalityTagList = nationalityTags,
+ tasteTagList = tasteTags,
+ occasionTagList = occasionTags,
+ selectedTagList = selectedTags,
+ onSelectedTagsChange = { newSelectedTags -> selectedTags = newSelectedTags },
+ onApplyButtonClick = {
+ showTagBottomSheet = false
+ }
+ ) { tag ->
+ if (selectedTags.contains(tag)) {
+ selectedTags -= tag
+ } else {
+ selectedTags += tag
+ }
+ }
+ } else {
+ //์์ด์ฝ ์ ํํ๋ bottom sheet
+ IconSelectBottomSheet(
+ iconList = iconList,
+ onCancel = {
+ showTagBottomSheet = true
+ showBottomSheet = true
+ },
+ onConfirm = {
+ coroutineScope.launch {
+ scaffoldState.bottomSheetState.partialExpand()
+ showBottomSheet = false
+ showTagBottomSheet = true
+ }
+ }
+ ){ index ->
+ selectedIcon = index
+ Log.d("AddMenuTagScreen", "Selected icon index: $index")
+ }
+ }
+ },
+ //ํ๊ทธ ๊ณ ๋ฅด๊ธฐ ๋๋ฌ์ผ ๋ณด์ด๋๋ก
+ sheetPeekHeight = if (showBottomSheet) {
+ if (showTagBottomSheet){
+ (LocalConfiguration.current.screenHeightDp - 20).dp
+ } else 400.dp
+ } else 0.dp,
+ sheetDragHandle = {
+ BottomSheetDragHandle()
+ }
+ ) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(horizontal = 20.dp),
+ verticalArrangement = Arrangement.SpaceBetween
+ ) {
+
+ Column(modifier = modifier) {
+ //ํ๊ทธ
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(top = 28.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = stringResource(R.string.tag),
+ style = ourMenuTypography().pretendard_600_14
+ )
+ Button(
+ onClick = {
+ showTagBottomSheet = true
+ showBottomSheet = true
+ },
+ shape = RoundedCornerShape(12.dp),
+ border = BorderStroke(1.dp, Primary500Main),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Primary500Main,
+ contentColor = NeutralWhite
+ ),
+ modifier = modifier.size(128.dp, 32.dp),
+ contentPadding = PaddingValues(0.dp)
+ ) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_filter_white_16),
+ contentDescription = "select tag",
+ tint = Color.Unspecified
+ )
+ Text(
+ text = stringResource(R.string.choose_tag),
+ style = ourMenuTypography().pretendard_700_16,
+ color = NeutralWhite
+ )
+ }
+ }
+ }
+ SelectedTagGroup(
+ modifier = modifier
+ .padding(
+ vertical = if (selectedTags.isEmpty()) 0.dp else 20.dp,
+ ),
+ tags = selectedTags
+ ) {
+ selectedTags -= it
+ }
+
+ //๋ฉ๋ชจ
+ Spacer(modifier = modifier.height(20.dp))
+ Text(
+ text = stringResource(R.string.memo),
+ style = ourMenuTypography().pretendard_600_14
+ )
+ Spacer(modifier = modifier.height(4.dp))
+ CustomTextField(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(44.dp)
+ .border(1.dp, Neutral300, RoundedCornerShape(8.dp)),
+ text = memoTitle,
+ onTextChange = { memoTitle = it },
+ shape = RoundedCornerShape(8.dp),
+ paddingValues = PaddingValues(28.dp, 12.dp),
+ containerColor = Neutral100,
+ placeHolder = {
+ Text(
+ text = stringResource(R.string.type_title),
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral500
+ )
+ },
+ textStyle = ourMenuTypography().pretendard_500_14.copy(color = Neutral700)
+ )
+ Spacer(modifier = modifier.height(8.dp))
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(88.dp)
+ .background(Neutral100)
+ .border(1.dp, Neutral300, RoundedCornerShape(8.dp))
+ .verticalScroll(scrollState),
+ ) {
+ CustomTextField(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(88.dp),
+ text = memoBody,
+ singleLine = false,
+ onTextChange = { memoBody = it },
+ shape = RoundedCornerShape(8.dp),
+ paddingValues = PaddingValues(28.dp, 12.dp),
+ containerColor = Neutral100,
+ placeHolder = {
+ Text(
+ text = stringResource(R.string.type_body),
+ style = ourMenuTypography().pretendard_500_12,
+ color = Neutral500,
+ )
+ },
+ textStyle = ourMenuTypography().pretendard_500_14.copy(color = Neutral700)
+ )
+ }
+
+ //์์ด์ฝ
+ Spacer(modifier = modifier.height(20.dp))
+ Row(modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ Text(
+ text = stringResource(R.string.icon),
+ style = ourMenuTypography().pretendard_600_14
+ )
+ Icon(
+ modifier = modifier
+ .clickable {
+ showTagBottomSheet = false
+ showBottomSheet = true
+ },
+ painter = painterResource(iconList[selectedIcon]),
+ contentDescription = "selected icon",
+ tint = Color.Unspecified,
+ )
+ }
+ }
+ // TODO: ๋์ด ๋์ด๊ธฐ
+ BottomFullWidthButton(
+ modifier = modifier
+ .padding(bottom = 20.dp),
+ containerColor = if (enableAddButton) Primary500Main else Neutral100,
+ contentColor = if (enableAddButton) NeutralWhite else Neutral500,
+ text = "๋ฉ๋ด ๋ฑ๋กํ๊ธฐ"
+ ) {
+ // TODO: ๋ฑ๋ก api ํธ์ถ
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun AddMenuTagScreenPreview() {
+ AddMenuTagScreen(
+
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/addmenu/viewmodel/AddMenuViewModel.kt b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/viewmodel/AddMenuViewModel.kt
new file mode 100644
index 00000000..59e19e46
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/addmenu/viewmodel/AddMenuViewModel.kt
@@ -0,0 +1,259 @@
+package com.kuit.ourmenu.ui.addmenu.viewmodel
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.google.android.gms.location.LocationServices
+import com.kakao.vectormap.KakaoMap
+import com.kakao.vectormap.LatLng
+import com.kakao.vectormap.camera.CameraUpdateFactory
+import com.kakao.vectormap.label.LabelOptions
+import com.kakao.vectormap.label.LabelStyle
+import com.kakao.vectormap.label.LabelStyles
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.data.model.map.response.CrawlingHistoryResponse
+import com.kuit.ourmenu.data.model.map.response.CrawlingStoreDetailResponse
+import com.kuit.ourmenu.data.model.map.response.CrawlingStoreInfoResponse
+import com.kuit.ourmenu.data.repository.MapRepository
+import com.kuit.ourmenu.ui.common.map.MapController
+import com.kuit.ourmenu.utils.PreferencesManager
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class AddMenuViewModel @Inject constructor(
+ private val mapRepository: MapRepository,
+ private val preferencesManager: PreferencesManager
+) : ViewModel() {
+ // ์ต๊ทผ ๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ ์ ์ฅ
+ private val _searchHistory = MutableStateFlow?>(emptyList())
+ val searchHistory = _searchHistory.asStateFlow()
+
+ //์ค์ ๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ ์ ์ฅ
+ private val _searchResult = MutableStateFlow?>(emptyList())
+ val searchResult = _searchResult.asStateFlow()
+
+ //์๋น ์ ๋ณด
+ private val _storeInfo = MutableStateFlow(null)
+ val storeInfo = _storeInfo.asStateFlow()
+
+ // ์ ํ๋ ๋ฉ๋ด ์ธ๋ฑ์ค
+ private val _selectedMenuIndex = MutableStateFlow(null)
+ val selectedMenuIndex = _selectedMenuIndex.asStateFlow()
+
+ // ํ๋ฉด์ ๋ณด์ด๋ ์ง๋ ์์ ์ค์ฌ์ ํด๋นํ๋ ์ขํ
+ private val _currentCenter = MutableStateFlow(null)
+ val currentCenter: StateFlow = _currentCenter
+
+ val mapController = MapController()
+
+ // Permission state
+ private val _locationPermissionGranted = MutableStateFlow(false)
+ val locationPermissionGranted: StateFlow = _locationPermissionGranted.asStateFlow()
+
+ init {
+ viewModelScope.launch {
+ preferencesManager.locationPermissionGranted.collect { granted ->
+ _locationPermissionGranted.value = granted
+ Log.d("AddMenuViewModel", "location permission : $granted")
+ }
+ }
+ }
+
+ fun updateLocationPermission(granted: Boolean) {
+ viewModelScope.launch {
+ preferencesManager.setLocationPermissionGranted(granted)
+ }
+ }
+
+ // ์ง๋ ์ด๊ธฐํ
+ @SuppressLint("MissingPermission")
+ fun initializeMap(kakaoMap: KakaoMap, context: Context) {
+ // Initial map setup
+ // Get current location and move camera
+ Log.d("AddMenuViewModel", "initialize Map")
+ val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
+ fusedLocationClient.lastLocation.addOnSuccessListener { location ->
+ location?.let {
+ Log.d("AddMenuViewModel", "location success: lat=${it.latitude}, long=${it.longitude}")
+ moveCamera(it.latitude, it.longitude)
+// addMarker(it.latitude, it.longitude, R.drawable.img_popup_dice)
+ } ?: run {
+ Log.d("AddMenuViewModel", "location fail")
+ moveCamera(37.5416, 127.0793)
+// addMarker(37.5416, 127.0793, R.drawable.img_popup_dice)
+ }
+ }
+ }
+
+ fun updateSelectedMenu(index: Int) {
+ Log.d("AddMenuViewModel", "index: $index")
+ viewModelScope.launch {
+ _selectedMenuIndex.value = if (_selectedMenuIndex.value == index) null else index
+ }
+ }
+
+
+ // ์ง๋์์์ ํ๋ฉด ์ด๋
+ fun moveCamera(latitude: Double, longitude: Double) {
+ mapController.kakaoMap.value?.let { map ->
+ val cameraUpdate =
+ CameraUpdateFactory.newCenterPosition(LatLng.from(latitude, longitude))
+ map.moveCamera(cameraUpdate)
+ updateCurrentCenter()
+ }
+ }
+
+ // ์ง๋ ์ค์ ์ขํ ์
๋ฐ์ดํธ
+ fun updateCurrentCenter() {
+ viewModelScope.launch {
+ mapController.kakaoMap.value?.let { map ->
+ val center = map.cameraPosition?.position
+ _currentCenter.value = center
+ if (center != null) {
+ Log.d(
+ "SearchMenuViewModel",
+ "ํ์ฌ ์ง๋ ์ค์ฌ ์ขํ: ${center.latitude}, ${center.longitude}"
+ )
+ }
+ }
+ }
+ }
+
+ // Get the current center coordinates as a Pair
+ fun getCurrentCoordinates(): Pair? {
+ return currentCenter.value?.let {
+ Pair(it.latitude, it.longitude)
+ }
+ }
+
+ // ์ง๋์ ํ ์ถ๊ฐ
+ fun addMarker(latitude: Double, longitude: Double, resourceId: Int) {
+ mapController.kakaoMap.value?.let { map ->
+ val style = map.labelManager?.addLabelStyles(
+ LabelStyles.from(LabelStyle.from(resourceId))
+ )
+ val options = LabelOptions.from(LatLng.from(latitude, longitude)).setStyles(style)
+ map.labelManager?.layer?.addLabel(options)
+ map.setOnLabelClickListener { kakaoMap, labelLayer, label ->
+ // Find the store that matches the clicked label's coordinates
+ val clickedStore = searchResult.value?.find { store ->
+ store.storeMapY == label.position.latitude && store.storeMapX == label.position.longitude
+ }
+ // Update storeInfo with the clicked store
+ clickedStore?.let { store ->
+ _storeInfo.value = store
+ }
+ // Move camera to the clicked location
+ moveCamera(latitude = label.position.latitude, longitude = label.position.longitude)
+ true
+ }
+ }
+ }
+
+ // ์ง๋์ ์ ์ฒด ํ ์ ๊ฑฐ
+ fun clearMarkers() {
+ mapController.kakaoMap.value?.let { map ->
+ map.labelManager?.layer?.removeAll()
+ }
+ }
+
+ fun getCrawlingHistory() {
+ viewModelScope.launch {
+ val response = mapRepository.getCrawlingHistory()
+ response.onSuccess {
+ _searchHistory.value = it
+ Log.d("SearchMenuViewModel", "ํฌ๋กค๋ง ๊ธฐ๋ก ์กฐํ ์ฑ๊ณต")
+ }.onFailure {
+ // Handle error
+ Log.d("SearchMenuViewModel", "ํฌ๋กค๋ง ๊ธฐ๋ก ์กฐํ ์คํจ : ${it.message}")
+ }
+ }
+ }
+
+ fun getCrawlingStoreInfo(
+ query: String,
+ long: Double,
+ lat: Double
+ ) {
+ viewModelScope.launch {
+ Log.d("SearchMenuViewModel", "ํฌ๋กค๋ง ์คํ ์ด ์ ๋ณด ์์ฒญ: $query, ์ขํ($lat, $long)")
+
+ val response = mapRepository.getCrawlingStoreInfo(
+ query = query,
+ longitude = long,
+ latitude = lat
+ )
+
+ response.onSuccess { result ->
+ if (result != null) {
+ Log.d("SearchMenuViewModel", "ํฌ๋กค๋ง ์คํ ์ด ์ ๋ณด ์กฐํ ์ฑ๊ณต: ${result.size}๊ฐ")
+ Log.d("SearchMenuViewModel", "ํฌ๋กค๋ง ์คํ ์ด ์ ๋ณด ์กฐํ ์ฑ๊ณต: ${result[0].storeTitle}")
+ // ๊ฒ์ ๊ฒฐ๊ณผ ์ ์ฅ
+ getCrawlingStoreDetail(result)
+ }
+ }.onFailure {
+ Log.d("SearchMenuViewModel", "ํฌ๋กค๋ง ์คํ ์ด ์ ๋ณด ์กฐํ ์คํจ: ${it.message}")
+ }
+ }
+ }
+
+ // ํฌ๋กค๋ง ํ ์ธ๋ถ ์ ๋ณด๋ฅผ _searchResult์ ์ ์ฅํ๋ ํจ์
+ fun getCrawlingStoreDetail(crawledDatas: List) {
+ val updatedResult = mutableListOf()
+ viewModelScope.launch {
+ crawledDatas.forEach { crawledData ->
+ val response = mapRepository.getCrawlingStoreDetail(
+ isCrawled = true,
+ storeId = crawledData.storeId
+ )
+ response.onSuccess {
+ if (it != null) {
+ updatedResult.add(it)
+ Log.d("SearchMenuViewModel", "ํฌ๋กค๋ง ์คํ ์ด ์ ๋ณด ์
๋ฐ์ดํธ ์ฑ๊ณต: $it")
+ } else {
+ Log.d("SearchMenuViewModel", "ํฌ๋กค๋ง ์คํ ์ด ์ ๋ณด ์
๋ฐ์ดํธ ์คํจ: null")
+ }
+ }.onFailure {
+ Log.d("SearchMenuViewModel", "ํฌ๋กค๋ง ์คํ ์ด ์ ๋ณด ์
๋ฐ์ดํธ ์คํจ: ${it.message}")
+ }
+ }
+ _searchResult.value = updatedResult
+ // Set the first store as the initial storeInfo
+ updatedResult.firstOrNull()?.let { firstStore ->
+ _storeInfo.value = firstStore
+ }
+ showSearchResultOnMap()
+ }
+ }
+
+ // ์ง๋์ ๊ฒ์ ๊ฒฐ๊ณผ ํ ์ถ๊ฐ
+ fun showSearchResultOnMap() {
+ clearMarkers()
+ searchResult.value?.forEach { store ->
+ val latitude = store.storeMapY
+ val longitude = store.storeMapX
+ addMarker(latitude, longitude, R.drawable.img_popup_dice)
+ Log.d(
+ "SearchMenuViewModel",
+ "๋ง์ปค ์ถ๊ฐ: ${store.storeTitle} lat: (${latitude}, long: ${longitude})"
+ )
+ }
+ // ์ฒซ ๋ฒ์งธ ๊ฒ์ ๊ฒฐ๊ณผ๋ก ์นด๋ฉ๋ผ ์ด๋
+ searchResult.value?.get(0)?.let { moveCamera(it.storeMapY, it.storeMapX) }
+ }
+}
+
+// TODO: ์ดํ์ dto ๋ฐ์์ ์ญ์ ์์
+data class AddMenuDummyStoreInfo(
+ val imgList: List = emptyList(),
+ val name: String = "",
+ val address: String = "",
+ val menuList: List = emptyList(),
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/BottomFullWidthButton.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/BottomFullWidthButton.kt
new file mode 100644
index 00000000..ec6bcf0e
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/BottomFullWidthButton.kt
@@ -0,0 +1,76 @@
+package com.kuit.ourmenu.ui.common
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+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.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.Neutral100
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun BottomFullWidthButton(
+ modifier: Modifier = Modifier,
+ containerColor: Color,
+ contentColor: Color,
+ text: String,
+ onClick: () -> Unit
+) {
+ Button(
+ onClick = onClick,
+ modifier = modifier
+ .fillMaxWidth()
+ .size(320.dp, 48.dp)
+ .shadow(elevation = 4.dp, shape = RoundedCornerShape(8.dp)),
+ shape = RoundedCornerShape(8.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = containerColor,
+ contentColor = contentColor
+ ),
+ ) {
+ Text(
+ text = text,
+ style = ourMenuTypography().pretendard_700_16,
+ color = contentColor
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun BottomFullWidthButtonPreview() {
+ Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) {
+ BottomFullWidthButton(
+ containerColor = Neutral100,
+ contentColor = Neutral500,
+ text = stringResource(R.string.add_store_and_menu_by_myself)
+ ) {
+ //onClick ์์ฑ
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ BottomFullWidthButton(
+ containerColor = Primary500Main,
+ contentColor = NeutralWhite,
+ text = stringResource(R.string.add_menu)
+ ) {
+ //onClick ์์ฑ
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/BottomHalfWidthButton.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/BottomHalfWidthButton.kt
new file mode 100644
index 00000000..68411a51
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/BottomHalfWidthButton.kt
@@ -0,0 +1,82 @@
+package com.kuit.ourmenu.ui.common
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.Neutral400
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun BottomHalfWidthButton(
+ modifier: Modifier = Modifier,
+ containerColor: Color,
+ contentColor: Color,
+ text: String,
+ onClick: () -> Unit
+) {
+ Button(
+ modifier = modifier
+ .size(154.dp, 52.dp)
+ .shadow(elevation = 4.dp, shape = RoundedCornerShape(12.dp)),
+ shape = RoundedCornerShape(12.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = containerColor,
+ contentColor = contentColor
+ ),
+ onClick = onClick
+ ) {
+ Text(
+ text = text,
+ style = ourMenuTypography().pretendard_700_14,
+ color = contentColor
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun BottomHalfWidthButtonPreview() {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(20.dp),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ BottomHalfWidthButton(
+ containerColor = Neutral400,
+ contentColor = NeutralWhite,
+ text = stringResource(R.string.cancel)
+ ) {
+
+ }
+ BottomHalfWidthButton(
+ containerColor = Primary500Main,
+ contentColor = NeutralWhite,
+ text = stringResource(R.string.apply)
+ ) {
+
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/CustomTextField.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/CustomTextField.kt
new file mode 100644
index 00000000..a54b6f1b
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/CustomTextField.kt
@@ -0,0 +1,212 @@
+package com.kuit.ourmenu.ui.common
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+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.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextFieldColors
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.NeutralBlack
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+
+/**
+ * padding ๊ฐ์ ์ง์ ์ง์ ํ ์ ์๋ ์ปค์คํ
TextField๋ฅผ ๊ตฌํํ Composable ํจ์์
๋๋ค.
+ * ๊ฐ parameter๋ค์ ๊ธฐ๋ณธ ๊ฐ์ ์ด๊ธฐํ ๋์ด ์๊ณ composable ์ฌ์ฉ์์ ์ง์ ์ค์ ํ์ฌ ์ํ๋ TextField๋ฅผ ๋ง๋ค ์ ์์ต๋๋ค.
+ * ํ์์ ์ผ๋ก ๋๊ฒจ์ฃผ์ด์ผ ํ๋ ํ๋ผ๋ฏธํฐ๋ text, onTextChange ํ๋ผ๋ฏธํฐ์
๋๋ค.
+ *
+ *
+ * @param modifier modifier๋ฅผ ํตํด ์ ์ฒด ์ปดํฌ๋ํธ์ ํฌ๊ธฐ, border ๋ฑ์ ์ค์ ํ๋ค.
+ * modifier์์ ์ ์ํ border๋ ๋ฐฐ๊ฒฝ์ด ์๋ ์ธ๋ถ ํ
๋๋ฆฌ ์ ์๋ง ์ํฅ์ ์ค๋ค
+ *
+ * @param text CustomTextField๋ฅผ ์ฌ์ฉํ ์ปดํฌ๋ํธ๊ฐ ์์นํ๋ ๊ณณ์ text์ ์ํ๋ฅผ ๊ด๋ฆฌํ ์ ์๋ ๋ณ์๋ฅผ ์ ์ธํ๊ณ ,
+ * ํ๋ผ๋ฏธํฐ๋ก ๋๊ฒจ์ฃผ์ด ์ฌ์ฉํฉ๋๋ค. (์ํ ํธ์ด์คํ
์ ์ ์ฉํ์ฌ ์ฌ์ฉ)
+ *
+ * @param onTextChange ์์ text ํ๋ผ๋ฏธํฐ์ ํด๋นํ๋ ๋ณ์๋ฅผ ์ ์ธํ ์ฝ๋์์ ํจ๊ป ์์ฑํ๋ฉด ๋๊ณ , { newText -> text = newText } ๋๋ { text = it }
+ * ์ ํํ๋ก๋ ํจ์๋ฅผ ํ๋ผ๋ฏธํฐ์ ๋๊ฒจ์ฃผ์ด ์ฌ์ฉํฉ๋๋ค. ์ด ์์์์ text๋ CustomTextField์ text ํ๋ผ๋ฏธํฐ์ ๋๊ฒจ์ค ๋ณ์๋ช
์
๋๋ค.
+ *
+ * @param interactionSource TextField์ ์ํ(focus, drag, click ๋ฑ)๋ฅผ ๊ด๋ฆฌํ๊ธฐ ์ํ ํ๋ผ๋ฏธํฐ์
๋๋ค. ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ด๊ธฐํ ๋์ด์๊ณ ,
+ * ํด๋น ์ํ์ ๊ด๋ฆฌ๊ฐ ํ์ํ๋ค๋ฉด remember { MutableInteractionSource() } ์ ๊ฐ์ ๊ฐ์ง๋ ๋ณ์๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ๋๊ฒจ์ฃผ๊ณ ,
+ * val isPressed by interactionSource.collectIsPressedAsState()์ ๊ฐ์ด ๋ณ์๋ฅผ ์ ์ธํ์ฌ ์ํ๋ฅผ ์ถ์ ํ ์ ์์ต๋๋ค.
+ *
+ * @param visualTransformation ์
๋ ฅ๋ ํ
์คํธ์ ๊ฐ์ ์ ์งํ๋ฉด์, ํ๋ฉด์ ๋ณด์ฌ์ง๋ ๋ฐฉ์์ ๋ณ๊ฒฝ์ํฌ ๋ ์ฌ์ฉํ๋ ํ๋ผ๋ฏธํฐ์ด๋ค.
+ * ์๋ฅผ ๋ค์ด์ ๋น๋ฐ๋ฒํธ ์
๋ ฅ ํ๋๋ก ์ฌ์ฉํ๋ค๋ฉด,
+ * PasswordVisualTransformation()์ ์ธ์๋ก ์ฃผ์ด ์ค์ ๊ฐ ๋์ ๋ง์คํน ๋ ๊ฐ์ ๋ณด์ฌ์ฃผ๋๋ก ์ค์ ํ ์ ์๋ค.
+ * VisualTransformation์ ์ง์ ์ ์ํ์ฌ ์ปค์คํ
ํ ์๋ ์๋ค. (์ซ์์์ 3์๋ฆฌ ๋จ์๋ก , ๋ฅผ ์ฐ๋ ๊ฒ๊ณผ ๊ฐ์ ํฌ๋งทํ
๋ฑ)
+ *
+ * @param enabled ํ
์คํธ ํ๋์ ํ์ฑํ ์ฌ๋ถ๋ฅผ ๋ํ๋ด๋ ํ๋ผ๋ฏธํฐ์ด๊ณ , true์ธ ๊ฒฝ์ฐ์๋ ์
๋ ฅ์ด ๊ฐ๋ฅํ๋ค
+ *
+ * @param singleLine ์
๋ ฅ๋ ํ
์คํธ๋ฅผ ํ ์ค๋ก๋ง ํ์ํ ์ง์ ์ฌ๋ถ๋ฅผ ๋ํ๋ธ๋ค. ๊ธฐ๋ณธ๊ฐ์ true
+ *
+ * @param shape ์ปดํฌ๋ํธ์ ๋ด๋ถ ์์ญ์ ๋ชจ์ ์ฒ๋ฆฌ, modifier์ RoundedCornerShape๋ฅผ ์ ์ฉํ๋ฉด ์ฌ๊ธฐ์๋ ์ ์ฉํด์ผ ๋์ผํด์ง๋ค
+ * ex) RoundedCornerShape(8.dp) ๋ฑ
+ *
+ * @param trailingIcon ์ปดํฌ๋ํธ์ ์ฐ์ธก ๋์ ์์นํ๊ฒ ํ Icon ๊ฐ์ฒด๋ฅผ lambda๋ฅผ ํตํด ๋๊ฒจ์ค๋ค
+ *
+ * @param leadingIcon ์ปดํฌ๋ํธ์ ์ผ์ชฝ ๋์ ์์นํ๊ฒ ํ Icon ๊ฐ์ฒด๋ฅผ lambda๋ฅผ ํตํด ๋๊ฒจ์ค๋ค
+ *
+ * @param placeHolder placeholder๋ก ์ฌ์ฉํ Text ๊ฐ์ฒด๋ฅผ lambda๋ฅผ ํตํด ๋๊ฒจ์ค๋ค
+ *
+ * @param textStyle ์
๋ ฅํ ํ
์คํธ๋ฅผ ๋ณด์ฌ์ค ์คํ์ผ์ ๋ํ TextStyle ๊ฐ์ฒด๋ฅผ ํตํด ์ ์ํ๋ค
+ *
+ * @param paddingValues ์ปดํฌ๋ํธ์ ํ
์คํธ ๋ถ๋ถ์ ๋ํ padding ๊ฐ์ PaddingValues ๊ฐ์ฒด๋ฅผ ํตํด ์ ์ํ๋ค
+ *
+ * @param containerColor ์ปดํฌ๋ํธ์ ์์์ Color ๊ฐ์ฒด๋ฅผ ํตํด ์ง์ ํ๋ค
+ * ๊ธฐ๋ณธ ๊ฐ์ผ๋ก๋ focused, unfocused์ ๋ํ ๊ฐ์ ๋์ผํ๊ฒ Color.White๋ก ์ง์
+ *
+ * @param cursorColor ์ปค์์ ๋ํ ์์์ Color ๊ฐ์ฒด๋ฅผ ํตํด ์ง์ ํ๋ค
+ * ๊ธฐ๋ณธ ๊ฐ์ผ๋ก๋ Color.Black์ผ๋ก ์ง์
+ *
+ * @param isError ์๋ฌ ์ฒ๋ฆฌ์ ํ์ํ ์กฐ๊ฑด๋ฌธ์ ์ฌ์ฉํ ๋ณ์, ๊ธฐ๋ณธ๊ฐ์ false๋ก ์ด๊ธฐํ๋์ด ์๋ค
+ * ex) ๋น๋ฐ๋ฒํธ ์ค๋ฅ์ container ์์ ๋ณ๊ฒฝ ๋ฑ
+ *
+ * @param keyboardOptions ํค๋ณด๋์ ๋ํ ์ค์ ์ KeyboardOptions๋ฅผ ํตํด ์ง์ ํ๋ค
+ *
+ * @param keyboardActions ํค๋ณด๋์์ ํน์ ๋ฒํผ์ ๋๋ ์ ๋์ ์์
์ keyboardActions๋ฅผ ํตํด ์ง์ ํ๋ค
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CustomTextField(
+ modifier: Modifier = Modifier,
+ text: String,
+ onTextChange: (String) -> Unit,
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ visualTransformation: VisualTransformation = VisualTransformation.None,
+ enabled: Boolean = true,
+ singleLine: Boolean = true,
+ shape: Shape = TextFieldDefaults.shape,
+ trailingIcon: @Composable (() -> Unit)? = null,
+ leadingIcon: @Composable (() -> Unit)? = null,
+ placeHolder: @Composable (() -> Unit)? = null,
+ textStyle: TextStyle = TextStyle.Default,
+ paddingValues: PaddingValues = PaddingValues(0.dp),
+ containerColor: Color = Color.White,
+ cursorColor: Color = Color.Black,
+ isError: Boolean = false,
+ keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+ keyboardActions: KeyboardActions = KeyboardActions.Default,
+ colors : TextFieldColors = TextFieldDefaults.colors()
+) {
+
+ BasicTextField(
+ value = text,
+ onValueChange = onTextChange,
+ modifier = modifier,
+ visualTransformation = visualTransformation,
+ interactionSource = interactionSource,
+ enabled = enabled,
+ singleLine = singleLine,
+ textStyle = textStyle,
+ keyboardOptions = keyboardOptions,
+ keyboardActions = keyboardActions,
+ cursorBrush = SolidColor(cursorColor)
+ ) { innerTextField ->
+
+ TextFieldDefaults.DecorationBox(
+ value = text,
+ visualTransformation = visualTransformation,
+ innerTextField = innerTextField,
+ singleLine = singleLine,
+ enabled = enabled,
+ interactionSource = interactionSource,
+ contentPadding = paddingValues,
+ trailingIcon = trailingIcon,
+ placeholder = placeHolder,
+ leadingIcon = leadingIcon,
+ shape = shape,
+ colors = colors.copy(
+ /*
+ * ์ด๊ณณ์์ enabled ์ฌ๋ถ, focus ์ฌ๋ถ ๋ฑ์ ๋ฐ๋ฅธ ์์, indicator(๋ฐ์ค)์ ์์ ๋ฑ์ ๋ฐ๋ก ์ง์ ํ ์ ์๋ค.
+ * */
+ focusedContainerColor = if(isError) Color.Red else containerColor,
+ unfocusedContainerColor = containerColor,
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent,
+ disabledContainerColor = containerColor,
+ disabledIndicatorColor = Color.Transparent,
+ )
+
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun CustomTextFieldPreview() {
+ var text by rememberSaveable { mutableStateOf("") }
+ var textForSimple by rememberSaveable { mutableStateOf("") }
+
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ //๊ฐ์ํ ๋ฒ์ (์ต์ ์๊ตฌ์ฌํญ, modifier๊ฐ ์์ด๋ ์๋์ ํ์ง๋ง ๊ฐ์์ฑ์ ์ํด ์ถ๊ฐ)
+ CustomTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .border(0.8.dp, Color.Black, RoundedCornerShape(8.dp)),
+ shape = RoundedCornerShape(8.dp),
+ text = textForSimple,
+ onTextChange = { textForSimple = it },
+ isError = true
+ )
+ Spacer(modifier = Modifier.height(20.dp))
+ //์ถ๊ฐ ๊ธฐ๋ฅ์ด ์๋ ๋ฒ์
+ CustomTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .border(0.8.dp, Neutral500, RoundedCornerShape(8.dp)),
+ text = text,
+ onTextChange = { text = it },
+ shape = RoundedCornerShape(8.dp),
+ placeHolder = {
+ Text(
+ stringResource(R.string.placeholder),
+ style = ourMenuTypography().pretendard_600_18.copy(color = Neutral500)
+ )
+ },
+ textStyle = ourMenuTypography().pretendard_500_20.copy(color = NeutralBlack),
+ trailingIcon = {
+ Icon(
+ imageVector = Icons.Default.Search,
+ contentDescription = "SearchIcon"
+ )
+ },
+ paddingValues = PaddingValues(20.dp, 0.dp),
+ cursorColor = Color.Red,
+ )
+ }
+}
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/DisableBottomFullWidthButton.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/DisableBottomFullWidthButton.kt
new file mode 100644
index 00000000..6bc78509
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/DisableBottomFullWidthButton.kt
@@ -0,0 +1,81 @@
+package com.kuit.ourmenu.ui.common
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+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.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.Neutral400
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun DisableBottomFullWidthButton(
+ enable: Boolean,
+ modifier: Modifier = Modifier,
+ enableContainerColor: Color = Primary500Main,
+ disabledContainerColor: Color = Neutral400,
+ contentColor: Color = NeutralWhite,
+ text: String,
+ onClick: () -> Unit
+) {
+ Button(
+ enabled = enable,
+ onClick = onClick,
+ modifier = modifier
+ .shadow(
+ elevation = 8.dp,
+ spotColor = Color(0x29000000),
+ ambientColor = Color(0x29000000)
+ )
+ .fillMaxWidth()
+ .size(320.dp, 48.dp),
+ shape = RoundedCornerShape(12.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = enableContainerColor,
+ disabledContainerColor = disabledContainerColor,
+ contentColor = contentColor
+ ),
+ ) {
+ Text(
+ text = text,
+ style = ourMenuTypography().pretendard_700_16,
+ color = contentColor
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun DisableBottomFullWidthButtonPreview() {
+ Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) {
+ DisableBottomFullWidthButton(
+ enable = true,
+ text = stringResource(R.string.add_store_and_menu_by_myself)
+ ) {
+ //onClick ์์ฑ
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ DisableBottomFullWidthButton(
+ enable = false,
+ text = stringResource(R.string.add_menu)
+ ) {
+ //onClick ์์ฑ
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/GoToMapButton.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/GoToMapButton.kt
new file mode 100644
index 00000000..465c8f82
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/GoToMapButton.kt
@@ -0,0 +1,82 @@
+package com.kuit.ourmenu.ui.common
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment.Companion.CenterVertically
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+/**
+ * ์ง๋ ์ฑ์ผ๋ก ์ด๋ ํ๋ ๋ฒํผ
+ *
+ * onClick ํจ์๋ง ๊ตฌํํ๋ฉด ์ฌ์ฉ ๊ฐ๋ฅ
+ *
+ * Map ํ๋ฉด, Menu-Info Map ํ๋ฉด ์์ ์ฌ์ฉ๋จ.
+ *
+ * @param modifier Modifier
+ * @param onClick () -> Unit : ๋ฒํผ ํด๋ฆญ ์ ์คํ๋๋ ํจ์
+ *
+ * */
+@Composable
+fun GoToMapButton(
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit = {}
+) {
+ Button(
+ modifier = modifier
+ .shadow(elevation = 8.dp, spotColor = Color(0x29000000), ambientColor = Color(0x29000000))
+ .shadow(elevation = 6.dp, spotColor = Color(0x3D000000), ambientColor = Color(0x3D000000))
+ .width(166.dp)
+ .height(40.dp),
+ shape = RoundedCornerShape(20.dp),
+ onClick = onClick,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Primary500Main,
+ contentColor = NeutralWhite
+ ),
+ contentPadding = PaddingValues()
+ ) {
+ Row {
+ Text(
+ text = stringResource(R.string.go_to_map),
+ style = ourMenuTypography().pretendard_600_16.copy(
+ color = NeutralWhite
+ ),
+ modifier = Modifier.align(CenterVertically)
+ )
+ Icon(
+ painter = painterResource(R.drawable.ic_menu_info_nav_24),
+ contentDescription = null,
+ tint = NeutralWhite,
+ modifier = Modifier
+ .padding(start = 8.dp)
+ .align(CenterVertically)
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun GoToMapButtonPreview() {
+ GoToMapButton()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/IconItemGroup.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/IconItemGroup.kt
new file mode 100644
index 00000000..e1f56207
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/IconItemGroup.kt
@@ -0,0 +1,78 @@
+package com.kuit.ourmenu.ui.common
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+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.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.addmenu.component.item.IconItem
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+fun IconItemGroup(
+ modifier: Modifier = Modifier,
+ groupLabel: String,
+ icons: List,
+ onIconSelect: (Int) -> Unit
+) {
+ Column(modifier = modifier.fillMaxWidth()) {
+ Text(
+ text = groupLabel,
+ style = ourMenuTypography().pretendard_700_16
+ )
+ Spacer(modifier = modifier.height(20.dp))
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(6), // ํ ํ์ 6๊ฐ์ฉ ๋ฐฐ์น
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(20.dp),
+ verticalArrangement = Arrangement.spacedBy(20.dp)
+ ) {
+ items(icons.size) { index ->
+ IconItem(
+ iconId = icons[index],
+ ) {
+ onIconSelect(index)
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun IconItemGroupPreview() {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 20.dp)
+ ) {
+ IconItemGroup(
+ groupLabel = "์์ด์ฝ",
+ icons = listOf(
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ R.drawable.ic_tag_rice,
+ )
+ ) {
+ // ์์ด์ฝ ํด๋ฆญ์ ์ํํ ์์
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/LoginTextField.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/LoginTextField.kt
new file mode 100644
index 00000000..51c4d049
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/LoginTextField.kt
@@ -0,0 +1,92 @@
+package com.kuit.ourmenu.ui.common
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.kuit.ourmenu.ui.theme.Neutral100
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.Primary100
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+// TODO : ๋์ค์ ์๋ฌ ์ฌ๋ถ์ ๊ด๊ณ์์ด ์ธ๋ถ์์ ์ ์ฒด์ ์ธ modifier ๋ฅผ ์ฅ์ด์ฃผ๋ ํํ๋ก ์ค๊ณํ๋ฉด ์ข์ ๋ฏ
+fun LoginTextField(
+ modifier: Modifier = Modifier,
+ error: Boolean = false,
+ placeholder: String,
+ input: String,
+ onTextChange: (String) -> Unit,
+ visualTransformation: VisualTransformation = VisualTransformation.None,
+) {
+ var isFocused by remember { mutableStateOf(false) }
+
+ CustomTextField(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(44.dp)
+ .border(
+ width = 1.dp,
+ color =
+ if (error) Primary500Main
+ else if (isFocused) Neutral500
+ else Neutral300,
+ shape = RoundedCornerShape(8.dp)
+ )
+ .onFocusChanged { focusState ->
+ isFocused = focusState.isFocused
+ },
+ text = input,
+ onTextChange = onTextChange,
+ shape = RoundedCornerShape(8.dp),
+ paddingValues = PaddingValues(28.dp, 12.dp),
+ containerColor = if (error) Primary100 else Neutral100,
+ placeHolder = {
+ Text(
+ text = placeholder,
+ style = ourMenuTypography().pretendard_700_12,
+ color = Neutral500,
+ lineHeight = 18.sp
+ )
+ },
+ textStyle = ourMenuTypography().pretendard_700_14.copy(color = Neutral700),
+ visualTransformation = visualTransformation,
+ cursorColor = Primary500Main,
+ colors = TextFieldDefaults.colors(
+ focusedContainerColor = if (error) Primary100 else Neutral100,
+ unfocusedContainerColor = Neutral100,
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent,
+ disabledContainerColor = Neutral100,
+ disabledIndicatorColor = Color.Transparent,
+ )
+ )
+}
+
+@Preview
+@Composable
+private fun LoginTextFieldPreview() {
+ LoginTextField(
+ placeholder = "Email",
+ input = "",
+ onTextChange = {}
+ )
+}
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/MealTime.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/MealTime.kt
new file mode 100644
index 00000000..27310e54
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/MealTime.kt
@@ -0,0 +1,85 @@
+package com.kuit.ourmenu.ui.common
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.ui.common.model.MealTime
+import com.kuit.ourmenu.ui.theme.Neutral100
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Primary100
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import com.kuit.ourmenu.utils.ExtensionUtil.toMealTime
+import com.kuit.ourmenu.utils.ViewUtil.noRippleClickable
+
+@Composable
+fun MealTimeGrid(
+ modifier: Modifier = Modifier,
+ mealTimes: List,
+ updateSelectedTime: (Int) -> Unit,
+) {
+ val state = rememberLazyGridState()
+
+ LazyVerticalGrid(
+ modifier = modifier,
+ columns = GridCells.Fixed(3),
+ state = state,
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalArrangement = Arrangement.spacedBy(11.dp),
+ ) {
+ items(18) { index ->
+ MealTimeItem(
+ modifier = Modifier
+ .width(66.dp)
+ .height(42.dp),
+ mealTime = mealTimes[index].mealTime,
+ selected = mealTimes[index].selected,
+ updateSelected = { updateSelectedTime(index) }
+ )
+ }
+ }
+}
+
+@Composable
+fun MealTimeItem(
+ modifier: Modifier = Modifier,
+ mealTime: Int = 0,
+ selected: Boolean = false,
+ updateSelected: () -> Unit = { },
+) {
+
+ val containerColor = if (selected) Primary100 else Neutral100
+ val borderColor = if (selected) Primary500Main else Neutral300
+ val textColor = if (selected) Primary500Main else Neutral500
+
+ Box(
+ modifier = modifier
+ .border(
+ width = 1.dp,
+ color = borderColor,
+ shape = RoundedCornerShape(size = 8.dp)
+ )
+ .background(color = containerColor, shape = RoundedCornerShape(size = 8.dp))
+ .noRippleClickable { updateSelected() }
+ ) {
+ Text(
+ text = mealTime.toMealTime(),
+ style = ourMenuTypography().pretendard_500_16,
+ color = textColor,
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/OurSnackbarHost.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/OurSnackbarHost.kt
new file mode 100644
index 00000000..e26cc859
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/OurSnackbarHost.kt
@@ -0,0 +1,109 @@
+package com.kuit.ourmenu.ui.common
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+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.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.Neutral50
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun OurSnackbarHost(
+ modifier: Modifier = Modifier,
+ hostState: SnackbarHostState,
+ isChecked: Boolean = false,
+) {
+ SnackbarHost(
+ hostState = hostState,
+ modifier = modifier
+ ) { data ->
+ OurSnackbar(
+ modifier = modifier,
+ isChecked = isChecked,
+ message = data.visuals.message
+ )
+ }
+}
+
+
+@Composable
+private fun OurSnackbar(
+ modifier: Modifier = Modifier,
+ isChecked: Boolean = false,
+ message: String
+) {
+ Card(
+ modifier = modifier
+ .background(color = Neutral50, shape = RoundedCornerShape(size = 8.dp)),
+ elevation = CardDefaults.cardElevation(6.dp),
+ shape = RoundedCornerShape(size = 8.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = NeutralWhite
+ )
+ ) {
+ Row(
+ modifier = Modifier
+ .clip(shape = RoundedCornerShape(size = 8.dp))
+ .padding(horizontal = 40.dp, vertical = 4.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Icon(
+ painter = if (isChecked) painterResource(R.drawable.ic_snackbar_check)
+ else painterResource(R.drawable.ic_snackbar_error),
+ contentDescription = null,
+ tint = Color.Unspecified
+ )
+ Text(
+ text = message,
+ style = ourMenuTypography().pretendard_500_12.copy(color = Neutral700)
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF)
+@Composable
+private fun OurSnackbarPreview() {
+ Column(
+ modifier = Modifier
+ .width(360.dp)
+ .height(400.dp),
+ verticalArrangement = Arrangement.spacedBy(30.dp)
+ ) {
+ OurSnackbar(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ message = "์ต๋ 10์๊น์ง ๊ฐ๋ฅํด์!",
+ isChecked = false
+ )
+
+ OurSnackbar(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ message = "๊ณ์ ์์ฑ ์๋ฃ",
+ isChecked = true
+ )
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/SearchTextField.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/SearchTextField.kt
new file mode 100644
index 00000000..4c046733
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/SearchTextField.kt
@@ -0,0 +1,111 @@
+package com.kuit.ourmenu.ui.common
+
+import androidx.annotation.StringRes
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+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.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+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.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun SearchTextField(
+ modifier: Modifier = Modifier,
+ text: String,
+ @StringRes placeHolder: Int = R.string.placeholder,
+ borderColor: Color = Neutral500,
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ onTextChange: (String) -> Unit,
+ onSearch: () -> Unit
+) {
+ Card(
+ modifier
+ .shadow(elevation = 2.dp, shape = RoundedCornerShape(8.dp))
+ .fillMaxWidth(),
+ shape = RoundedCornerShape(8.dp)
+ ) {
+ CustomTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .border(0.8.dp, borderColor, RoundedCornerShape(8.dp))
+ .clip(RoundedCornerShape(8.dp))
+ .height(44.dp),
+ text = text,
+ onTextChange = onTextChange,
+ shape = RoundedCornerShape(8.dp),
+ placeHolder = {
+ Text(
+ text = stringResource(placeHolder),
+ style = ourMenuTypography().pretendard_500_14.copy(
+ lineHeight = 20.sp,
+ color = Neutral500
+ )
+ )
+ },
+ textStyle = ourMenuTypography().pretendard_700_14.copy(color = Neutral700),
+ trailingIcon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_searchbar_search),
+ contentDescription = "Search Icon",
+ tint = Color.Unspecified,
+ modifier = Modifier.clickable { onSearch() }
+ )
+ },
+ paddingValues = PaddingValues(start = 28.dp, top = 0.dp, bottom = 0.dp),
+ cursorColor = Primary500Main,
+ interactionSource = interactionSource,
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
+ keyboardActions = KeyboardActions(onSearch = { onSearch() })
+ )
+ }
+
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SearchBarPreview() {
+ var text by rememberSaveable { mutableStateOf("") }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(20.dp), verticalArrangement = Arrangement.Center
+ ) {
+ SearchTextField(
+ text = text,
+ onTextChange = { text = it },
+ onSearch = { }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/bottomsheet/BottomSheetDragHandle.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/bottomsheet/BottomSheetDragHandle.kt
new file mode 100644
index 00000000..fa22e08e
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/bottomsheet/BottomSheetDragHandle.kt
@@ -0,0 +1,46 @@
+package com.kuit.ourmenu.ui.common.bottomsheet
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.ui.theme.Neutral300
+
+
+/**
+ * BottomSheet ์ Drag Handle ์ฉ์ผ๋ก ์ฌ์ฉ๋๋ Composable
+ *
+ * BottomSheetScaffold(sheetDragHandle = BottomSheetDragHandle()) ์ ํํ๋ก ์ฌ์ฉํฉ๋๋ค.
+ *
+ * @param modifier Modifier
+ * @param verticalPadding Dp : Drag Handle ์ ์๋จ๊ณผ ํ๋จ์ Padding ๊ฐ
+ * @param width Dp : Drag Handle ์ ๊ฐ๋ก ๊ธธ์ด default 120.dp
+ * @param height Dp : Drag Handle ์ ์ธ๋ก ๊ธธ์ด default 4.dp
+ * @param color Color : Drag Handle ์ ์์ default Neutral300
+ * @param shape Shape : Drag Handle ์ ๋ชจ์ default RoundedCornerShape(6.dp)
+ *
+ * */
+@Composable
+fun BottomSheetDragHandle(
+ modifier: Modifier = Modifier,
+ verticalPadding: Dp = 12.dp,
+ width: Dp = 120.dp,
+ height: Dp = 4.dp,
+ color: Color = Neutral300,
+ shape: Shape = RoundedCornerShape(6.dp),
+) {
+ Surface(
+ modifier = modifier.padding(vertical = verticalPadding),
+ color = color,
+ shape = shape
+ ) {
+ Box(Modifier.size(width = width, height = height))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/bottomsheet/MenuInfoBottomSheetContent.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/bottomsheet/MenuInfoBottomSheetContent.kt
new file mode 100644
index 00000000..a72e0256
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/bottomsheet/MenuInfoBottomSheetContent.kt
@@ -0,0 +1,214 @@
+package com.kuit.ourmenu.ui.common.bottomsheet
+
+import android.util.Log
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.Row
+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.material3.Text
+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.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import coil3.compose.LocalPlatformContext
+import coil3.compose.rememberAsyncImagePainter
+import coil3.request.ImageRequest
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.data.model.map.response.MapDetailResponse
+import com.kuit.ourmenu.data.model.map.response.MenuFolderInfo
+import com.kuit.ourmenu.ui.common.chip.MenuFolderChip
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import com.kuit.ourmenu.utils.ExtensionUtil.toWon
+
+@Composable
+fun MenuInfoBottomSheetContent(
+ modifier: Modifier = Modifier,
+ menuInfoData: MapDetailResponse,
+ onClick: (Long) -> Unit
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp)
+ .clickable {
+ Log.d("MenuInfoBottomSheetContent", "Menu ID: ${menuInfoData.menuId}")
+ onClick(menuInfoData.menuId)
+ }
+ ) {
+ MenuInfoContent(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 12.dp),
+ menuInfoData = menuInfoData
+ )
+
+ MenuInfoImage(
+ modifier = Modifier
+ .fillMaxWidth()
+ .align(Alignment.CenterHorizontally)
+ .padding(bottom = 8.dp),
+ menuInfoData = menuInfoData
+ )
+
+ MenuInfoTagContent(
+ modifier = Modifier
+ .fillMaxWidth(),
+ menuTags = menuInfoData.menuTagImgUrls
+ )
+ }
+}
+
+@Composable
+fun MenuInfoContent(
+ modifier: Modifier = Modifier,
+ menuInfoData: MapDetailResponse
+) {
+ val menuFolderTitle = menuInfoData.menuFolderInfo.menuFolderTitle
+
+ Column(modifier = modifier) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(32.dp),
+ ) {
+ Row(
+ modifier = Modifier
+ .align(Alignment.CenterStart)
+ ) {
+ Text(
+ text = with(menuInfoData.menuTitle) {
+ if (length > 10) take(10) + "..." else this
+ },
+ style = ourMenuTypography().pretendard_700_20.copy(
+ lineHeight = 32.sp,
+ color = Neutral900,
+ ),
+ )
+ Text(
+ text = menuInfoData.menuPrice.toWon(),
+ style = ourMenuTypography().pretendard_600_32.copy(
+ fontSize = 16.sp,
+ lineHeight = 22.sp,
+ color = Neutral700
+ ),
+ modifier = Modifier
+ .padding(start = 12.dp)
+ .align(Alignment.CenterVertically)
+ )
+ }
+ MenuFolderChip(
+ modifier = Modifier
+ .align(Alignment.CenterEnd),
+ menuFolderIconImgUrl = menuInfoData.menuFolderInfo.menuFolderIconImgUrl,
+ menuFolderTitle = menuFolderTitle
+ )
+ }
+ Text(
+ text = menuInfoData.storeTitle,
+ style = ourMenuTypography().pretendard_600_14.copy(
+ lineHeight = 12.sp,
+ color = Neutral500
+ ),
+ )
+ }
+}
+
+@Composable
+fun MenuInfoImage(
+ modifier: Modifier = Modifier,
+ menuInfoData: MapDetailResponse
+) {
+ val imgUrls = menuInfoData.menuImgUrls
+
+ Row(modifier = modifier, horizontalArrangement = Arrangement.SpaceBetween) {
+ for (i in 0 until 3) {
+ Image(
+ painter = if (i < imgUrls.size && imgUrls[i].isNotEmpty()) {
+ rememberAsyncImagePainter(
+ model = ImageRequest.Builder(LocalPlatformContext.current)
+ .data(imgUrls[i])
+ .size(104, 80)
+ .build()
+ )
+ } else {
+ painterResource(R.drawable.img_dummy_menu)
+ },
+ contentDescription = null,
+ modifier = Modifier
+ .size(104.dp, 80.dp)
+ .clip(shape = RoundedCornerShape(8.dp))
+ )
+// if (i != 2) Spacer(modifier = Modifier.padding(end = 4.dp))
+ }
+ }
+}
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+fun MenuInfoTagContent(
+ modifier: Modifier = Modifier,
+ menuTags: List
+) {
+ FlowRow(modifier = modifier) {
+ menuTags.forEach { tag ->
+ val painter = rememberAsyncImagePainter(
+ model = ImageRequest.Builder(LocalPlatformContext.current)
+ .data(tag)
+ .size(96, 32)
+ .build()
+ )
+
+ Image(
+ painter = painter,
+ contentDescription = null,
+ modifier = Modifier
+ .height(32.dp)
+ .padding(top = 4.dp, end = 4.dp),
+ contentScale = ContentScale.FillHeight
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MenuInfoBottomSheetContentPreview() {
+ MenuInfoBottomSheetContent(
+ menuInfoData = MapDetailResponse(
+ menuId = 1,
+ menuTitle = "Test Menu",
+ storeTitle = "๊ฐ๊ฒ ์ด๋ฆ",
+ menuPrice = 10000,
+ menuPinImgUrl = "pin",
+ menuTagImgUrls = listOf("ํ์", "๋ฐฅ"),
+ menuImgUrls = listOf(),
+ menuFolderInfo = MenuFolderInfo(
+ menuFolderTitle = "Test Store",
+ menuFolderIconImgUrl = "icon",
+ menuFolderCount = 1
+ ),
+ mapId = 1,
+ mapX = 127.0,
+ mapY = 37.0
+ )
+ ){
+ // ํด๋ฆญ์ ๋์
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/chip/MenuFolderChip.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/chip/MenuFolderChip.kt
new file mode 100644
index 00000000..1cb11b16
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/chip/MenuFolderChip.kt
@@ -0,0 +1,67 @@
+package com.kuit.ourmenu.ui.common.chip
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+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.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import coil3.compose.AsyncImage
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun MenuFolderChip(
+ modifier: Modifier = Modifier,
+ menuFolderIconImgUrl: String = "",
+ menuFolderTitle: String = "",
+ onClick: () -> Unit = { },
+) {
+ Row(
+ modifier = modifier
+ .clickable { onClick() }
+ .clip(RoundedCornerShape(12.dp))
+ .border(
+ width = 1.dp,
+ color = Neutral300,
+ shape = RoundedCornerShape(12.dp)
+ )
+ .background(NeutralWhite)
+ .padding(horizontal = 12.dp, vertical = 6.dp)
+ ) {
+ AsyncImage(
+ model = menuFolderIconImgUrl,
+ contentDescription = null,
+ modifier = Modifier.size(14.dp),
+ )
+ Text(
+ modifier = Modifier.padding(start = 8.dp),
+ text = menuFolderTitle,
+ style = ourMenuTypography().pretendard_600_32.copy(
+ fontSize = 12.sp,
+ lineHeight = 12.sp,
+ color = Neutral700
+ )
+ )
+ }
+
+
+}
+
+@Preview
+@Composable
+private fun MenuFolderChipPreview() {
+ MenuFolderChip(
+ menuFolderTitle = "ํ๋ ๋ง์ง"
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/chip/TagChip.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/chip/TagChip.kt
new file mode 100644
index 00000000..779b32b4
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/chip/TagChip.kt
@@ -0,0 +1,111 @@
+package com.kuit.ourmenu.ui.common.chip
+
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.FilterChipDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun TagChip(
+ modifier: Modifier = Modifier,
+ tagIcon: Int,
+ tagName: String,
+ enabled: Boolean = true,
+ selected: Boolean = false,
+ onClick: () -> Unit
+) {
+ FilterChip(
+ enabled = enabled,
+ selected = selected,
+ modifier = modifier
+ .height(32.dp),
+ onClick = onClick,
+ label = {
+ Text(
+ text = tagName,
+ style = ourMenuTypography().pretendard_700_16.copy(
+ lineHeight = 24.sp
+ )
+ )
+ },
+ leadingIcon = {
+ Icon(
+ modifier = Modifier.padding(start = 8.dp),
+ painter = painterResource(tagIcon),
+ contentDescription = null,
+ )
+ },
+ // disabled, selected -> primary500Main, enabled, default -> neutralWhite
+ colors = FilterChipDefaults.filterChipColors().copy(
+ containerColor = NeutralWhite,
+ labelColor = Neutral900,
+ leadingIconColor = Neutral900,
+ disabledContainerColor = Primary500Main,
+ disabledLabelColor = NeutralWhite,
+ disabledLeadingIconColor = NeutralWhite,
+ selectedContainerColor = Primary500Main,
+ disabledSelectedContainerColor = Primary500Main,
+ selectedLabelColor = NeutralWhite,
+ selectedLeadingIconColor = NeutralWhite,
+ ),
+ border = FilterChipDefaults.filterChipBorder(
+ enabled = enabled,
+ selected = selected,
+ borderColor = Neutral300,
+ selectedBorderColor = Primary500Main,
+ disabledBorderColor = Primary500Main
+
+ ),
+ shape = RoundedCornerShape(12.dp)
+ )
+
+}
+
+@Preview
+@Composable
+private fun DefaultTagChipPrev() {
+ TagChip(
+ tagIcon = R.drawable.ic_tag_rice,
+ tagName = "๋ฐฅ",
+ onClick = { },
+ )
+}
+
+@Preview
+@Composable
+private fun SelectedTagChipPrev() {
+ TagChip(
+ tagIcon = R.drawable.ic_tag_rice,
+ tagName = "๋ฐฅ",
+ onClick = { },
+ selected = true,
+ enabled = true
+ )
+}
+
+@Preview
+@Composable
+private fun DisableTagChipPrev() {
+ TagChip(
+ tagIcon = R.drawable.ic_tag_rice,
+ tagName = "๋ฐฅ",
+ onClick = { },
+ selected = true,
+ enabled = false
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/chip/TagChipGroup.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/chip/TagChipGroup.kt
new file mode 100644
index 00000000..b3b06c1b
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/chip/TagChipGroup.kt
@@ -0,0 +1,89 @@
+package com.kuit.ourmenu.ui.common.chip
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+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.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+fun TagChipGroup(
+ modifier: Modifier = Modifier,
+ groupLabel: String,
+ tags: List>,
+ selectedTags: List,
+ onTagClick: (String) -> Unit
+) {
+ Column(modifier = modifier.fillMaxWidth()){
+ Text(
+ text = groupLabel,
+ style = ourMenuTypography().pretendard_400_14,
+ color = Neutral900
+ )
+ Spacer(modifier = modifier.height(8.dp))
+ FlowRow(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(bottom = 24.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ maxItemsInEachRow = Int.MAX_VALUE
+ ) {
+ tags.forEach { (img, name) ->
+ TagChip(
+ tagIcon = img,
+ tagName = name,
+ selected = selectedTags.contains(name),
+ ) {
+ //ํ๊ทธ ํด๋ฆญ์ ์ํํ ์์
+ onTagClick(name)
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun TagChipGroupPreview() {
+ val tags = listOf(
+ R.drawable.ic_tag_rice to "๋ฐฅ",
+ R.drawable.ic_tag_rice to "๋นต",
+ R.drawable.ic_tag_rice to "๋ฉด",
+ R.drawable.ic_tag_rice to "๊ณ ๊ธฐ",
+ R.drawable.ic_tag_rice to "์์ ",
+ R.drawable.ic_tag_rice to "์นดํ",
+ R.drawable.ic_tag_rice to "๋์ ํธ",
+ R.drawable.ic_tag_rice to "ํจ์คํธํธ๋",
+ )
+ var selectedTags by rememberSaveable { mutableStateOf(listOf()) }
+
+ TagChipGroup(
+ groupLabel = stringResource(R.string.type),
+ tags = tags,
+ selectedTags = selectedTags,
+ ){ tag ->
+ if(selectedTags.contains(tag)){
+ selectedTags -= tag
+ }else{
+ selectedTags += tag
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/dialog/DialogBigButton.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/dialog/DialogBigButton.kt
new file mode 100644
index 00000000..ed71fa1a
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/dialog/DialogBigButton.kt
@@ -0,0 +1,115 @@
+package com.kuit.ourmenu.ui.common.dialog
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.Neutral400
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+/**
+ * Dialog ์ ์ฌ์ฉ๋๋ Big Button
+ *
+ * Width : fillMaxWidth
+ *
+ * Height : 48dp
+ *
+ * Padding ๊ฐ ์์
+ *
+ * Round Corner : 12dp
+ *
+ * Text Style : Pretendard 700 20sp
+ *
+ * Text Color : NeutralWhite
+ *
+ * @param buttonText: ๋ฒํผ ํ
์คํธ
+ * @param buttonIcon: ๋ฒํผ ์์ด์ฝ
+ * @param hasIcon: ์์ด์ฝ ์ฌ๋ถ
+ * @param containerColor: ๋ฒํผ ์์
+ * */
+@Composable
+fun DialogBigButton(
+ buttonText: String,
+ modifier: Modifier = Modifier,
+ hasIcon: Boolean = true,
+ buttonIcon: String = "",
+ containerColor: Color,
+ onClick: () -> Unit = { /* TODO */ }
+) {
+ Button(
+ onClick = onClick,
+ modifier = modifier,
+ shape = RoundedCornerShape(12.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = containerColor,
+ contentColor = NeutralWhite
+ )
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (hasIcon) {
+ Icon(
+ painter = painterResource(R.drawable.img_home_popup_thumbsup),
+ contentDescription = null,
+ tint = Color.Unspecified,
+ modifier = Modifier
+ .padding(end = 8.dp)
+ .size(28.dp)
+ )
+ }
+ Text(
+ text = buttonText,
+ style = ourMenuTypography().pretendard_700_20.copy(
+ fontSize = 18.sp
+ )
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun IBBPreview() {
+ Column {
+ DialogBigButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(48.dp),
+ buttonText = "์ข์!",
+ hasIcon = true,
+ containerColor = Primary500Main
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ DialogBigButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(48.dp),
+ buttonText = "ํ์ธ",
+ hasIcon = false,
+ containerColor = Neutral400
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/map/MapViewWithLifecycle.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/map/MapViewWithLifecycle.kt
new file mode 100644
index 00000000..5e3ffe8a
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/map/MapViewWithLifecycle.kt
@@ -0,0 +1,86 @@
+package com.kuit.ourmenu.ui.common.map
+
+import android.util.Log
+import android.view.View
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import com.kakao.vectormap.KakaoMap
+import com.kakao.vectormap.KakaoMapReadyCallback
+import com.kakao.vectormap.MapLifeCycleCallback
+import com.kakao.vectormap.MapView
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class MapController {
+ private val _kakaoMap = MutableStateFlow(null)
+ val kakaoMap: StateFlow = _kakaoMap
+
+ fun setMap(map: KakaoMap) {
+ _kakaoMap.value = map
+ }
+
+ fun resetMap(){
+ _kakaoMap.value = null
+ }
+}
+
+@Composable
+fun mapViewWithLifecycle(
+ mapController: MapController? = null,
+ mapSettings: (kakaoMap : KakaoMap) -> Unit
+): View {
+ val context = LocalContext.current
+ val mapView = remember { MapView(context) }
+ val lifecycle = LocalLifecycleOwner.current.lifecycle
+
+ DisposableEffect(lifecycle) {
+ val observer = object : DefaultLifecycleObserver {
+ override fun onCreate(owner: LifecycleOwner) {
+ mapView.start(
+ object : MapLifeCycleCallback() {
+ override fun onMapDestroy() {
+ // ์ง๋ API๊ฐ ์ ์์ ์ผ๋ก ์ข
๋ฃ๋ ๋ ํธ์ถ๋ฉ๋๋ค.
+ Log.d("MapViewWithLifecycle", "onMapDestroy")
+ mapController?.resetMap()
+ }
+
+ override fun onMapError(error: Exception) {
+ // ์ธ์ฆ ์คํจ ๋ฐ ์ง๋ ์ฌ์ฉ ์ค ์๋ฌ๊ฐ ๋ฐ์ํ ๋ ํธ์ถ๋ฉ๋๋ค.
+ Log.d("MapViewWithLifecycle", "onMapError")
+ }
+ },
+ object : KakaoMapReadyCallback() {
+ override fun onMapReady(kakaoMap: KakaoMap) {
+ // ์ง๋ ์ด๊ธฐํ ๋ฐ ์ค์ ์์
+ Log.d("MapViewWithLifecycle", "onMapReady")
+ mapController?.setMap(kakaoMap)
+ mapSettings(kakaoMap)
+ }
+ }
+ )
+ }
+
+ override fun onResume(owner: LifecycleOwner) {
+ Log.d("MapViewWithLifecycle", "onMapResume")
+ mapView.resume()
+ }
+
+ override fun onPause(owner: LifecycleOwner) {
+ Log.d("MapViewWithLifecycle", "onMapPause")
+ mapView.pause()
+ }
+ }
+
+ lifecycle.addObserver(observer)
+ onDispose {
+ lifecycle.removeObserver(observer)
+ }
+ }
+
+ return mapView
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/model/MealTime.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/model/MealTime.kt
new file mode 100644
index 00000000..70650bdf
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/model/MealTime.kt
@@ -0,0 +1,6 @@
+package com.kuit.ourmenu.ui.common.model
+
+data class MealTime(
+ val mealTime: Int,
+ var selected: Boolean = false
+)
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/model/PasswordState.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/model/PasswordState.kt
new file mode 100644
index 00000000..c08eac87
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/model/PasswordState.kt
@@ -0,0 +1,24 @@
+package com.kuit.ourmenu.ui.common.model
+
+sealed class PasswordState {
+ data object Default : PasswordState()
+ data object NotMeetCondition : PasswordState()
+ data object DifferentPassword : PasswordState()
+ data object Valid : PasswordState()
+ data object IncorrectPassword : PasswordState()
+}
+
+private fun isValidPassword(password: String): Boolean {
+ val regex = "^[a-zA-Z0-9]{8,}$".toRegex()
+ return password.matches(regex)
+}
+
+fun checkPassword(password: String, confirmPassword: String): PasswordState {
+ if (!isValidPassword(password)) {
+ return PasswordState.NotMeetCondition
+ }
+ if (password != confirmPassword) {
+ return PasswordState.DifferentPassword
+ }
+ return PasswordState.Valid
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/topappbar/BackButtonTopAppBar.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/topappbar/BackButtonTopAppBar.kt
new file mode 100644
index 00000000..f75ee916
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/topappbar/BackButtonTopAppBar.kt
@@ -0,0 +1,51 @@
+package com.kuit.ourmenu.ui.common.topappbar
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.Neutral500
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun BackButtonTopAppBar(color: Color, isKebabVisible: Boolean, onBackClick: () -> Unit = {}) {
+ TopAppBar(
+ title = {
+ IconButton(onClick = onBackClick) {
+ Icon(
+ painter = painterResource(R.drawable.ic_back),
+ contentDescription = "Back",
+ tint = color,
+ )
+ }
+ },
+ actions = {
+ if (isKebabVisible) {
+ Icon(
+ painter = painterResource(R.drawable.ic_kebab),
+ modifier = Modifier.padding(end = 18.dp),
+ contentDescription = "Menu",
+ tint = color,
+ )
+ }
+ },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = Color.Transparent,
+ )
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun BackButtonTopAppBarPreview() {
+ BackButtonTopAppBar(Neutral500, true)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/topappbar/OnboardingTopAppBar.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/topappbar/OnboardingTopAppBar.kt
new file mode 100644
index 00000000..97018b50
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/topappbar/OnboardingTopAppBar.kt
@@ -0,0 +1,50 @@
+package com.kuit.ourmenu.ui.common.topappbar
+
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.CenterAlignedTopAppBar
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.Neutral700
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun OnboardingTopAppBar(
+ modifier: Modifier = Modifier,
+ onBackClick: () -> Unit = {}
+) {
+ CenterAlignedTopAppBar(
+ title = {
+ Icon(
+ imageVector = ImageVector.vectorResource(R.drawable.ic_top_bar_logo),
+ contentDescription = "์๋จ๋ฐ ๋ก๊ณ ",
+ tint = Color.Unspecified, // ์ด๊ฑฐ ์ค์ ์ํ๋ฉด ๊ฒ์ ์์ผ๋ก ๋ณด์
+ modifier = Modifier.size(44.dp)
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = onBackClick) {
+ Icon(
+ painter = painterResource(R.drawable.ic_top_bar_back),
+ contentDescription = "๋ค๋ก๊ฐ๊ธฐ ๋ฒํผ",
+ tint = Neutral700
+ )
+ }
+ },
+ )
+}
+
+@Preview
+@Composable
+private fun OnboardingTopAppBarPreview() {
+ OnboardingTopAppBar()
+}
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/topappbar/OurMenuAddButtonTopAppBar.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/topappbar/OurMenuAddButtonTopAppBar.kt
new file mode 100644
index 00000000..28e9d333
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/topappbar/OurMenuAddButtonTopAppBar.kt
@@ -0,0 +1,55 @@
+package com.kuit.ourmenu.ui.common.topappbar
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun OurMenuAddButtonTopAppBar(modifier: Modifier = Modifier) {
+ // ๊ธฐ๋ณธ
+ TopAppBar(
+ modifier = modifier.fillMaxWidth(),
+ colors = TopAppBarDefaults.topAppBarColors().copy(
+ containerColor = NeutralWhite
+ ),
+ title = {
+ Icon(
+ painter = painterResource(R.drawable.ic_ourmenu_text_logo),
+ contentDescription = "Top App Bar Logo",
+ modifier = Modifier.padding(start = 8.dp),
+ tint = Color.Unspecified
+ )
+ },
+ actions = {
+ IconButton(
+ onClick = { /* TODO : Add Menu Button Click Event */ },
+ modifier = Modifier.padding(end = 20.dp)
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_home_plus),
+ contentDescription = "Top App Bar Add Menu Button",
+ tint = Color.Unspecified
+ )
+ }
+ }
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun HomeTopAppBarPreview() {
+ OurMenuAddButtonTopAppBar()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/common/topappbar/OurMenuBackButtonTopAppBar.kt b/app/src/main/java/com/kuit/ourmenu/ui/common/topappbar/OurMenuBackButtonTopAppBar.kt
new file mode 100644
index 00000000..b7c99de0
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/common/topappbar/OurMenuBackButtonTopAppBar.kt
@@ -0,0 +1,56 @@
+package com.kuit.ourmenu.ui.common.topappbar
+
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.NeutralBlack
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun OurMenuBackButtonTopAppBar(topAppbarText: @Composable (() -> Unit)) {
+ TopAppBar(
+ title = {
+ topAppbarText()
+ },
+ navigationIcon = {
+ IconButton(onClick = { TODO("๋ค๋ก๊ฐ๊ธฐ ๊ตฌํ") }) {
+ Icon(
+ painter = painterResource(R.drawable.ic_top_bar_back),
+ contentDescription = "Back"
+ )
+ }
+ },
+ modifier = Modifier
+ .drawBehind {
+ drawRect(
+ color = NeutralBlack
+ )
+ }
+ .shadow(elevation = 4.dp)
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun AddMenuTopAppBarPreview() {
+ OurMenuBackButtonTopAppBar {
+ Text(
+ stringResource(R.string.ourmenu),
+ style = ourMenuTypography().pretendard_600_18,
+ color = Primary500Main
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/dummy/DummyScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/dummy/DummyScreen.kt
new file mode 100644
index 00000000..63a688ac
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/dummy/DummyScreen.kt
@@ -0,0 +1,18 @@
+package com.kuit.ourmenu.ui.dummy
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.kuit.ourmenu.ui.dummy.viewmodel.DummyViewModel
+
+@Composable
+fun DummyScreen(
+ modifier: Modifier = Modifier,
+ viewModel: DummyViewModel = hiltViewModel()
+) {
+
+ val uiState by viewModel.dummyUiState.collectAsStateWithLifecycle() // ์ด ๋ init { } ์์ getDummyData ํธ์ถ๋จ.
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/dummy/state/DummyState.kt b/app/src/main/java/com/kuit/ourmenu/ui/dummy/state/DummyState.kt
new file mode 100644
index 00000000..4e4fb3a6
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/dummy/state/DummyState.kt
@@ -0,0 +1,5 @@
+package com.kuit.ourmenu.ui.dummy.state
+
+data class DummyState(
+ val dummyString: String? = null
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/dummy/viewmodel/DummyViewModel.kt b/app/src/main/java/com/kuit/ourmenu/ui/dummy/viewmodel/DummyViewModel.kt
new file mode 100644
index 00000000..3cc92f8b
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/dummy/viewmodel/DummyViewModel.kt
@@ -0,0 +1,45 @@
+package com.kuit.ourmenu.ui.dummy.viewmodel
+
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import com.kuit.ourmenu.data.repository.DummyRepository
+import com.kuit.ourmenu.ui.dummy.state.DummyState
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class DummyViewModel @Inject constructor(
+ private val dummyRepository: DummyRepository
+) : ViewModel() {
+
+ private var _dummyUiState = MutableStateFlow(DummyState())
+ val dummyUiState: StateFlow =
+ _dummyUiState.asStateFlow() // ๋ช
์์ ํ์
์ ์ธ ์ ํด๋ ๋จ. or asStateFlow() ์๋ถ์ฌ๋๋จ
+
+
+ init {
+ getDummyData()
+ }
+
+ private fun getDummyData() {
+ viewModelScope.launch {
+ dummyRepository.getDummyData().fold(
+ onSuccess = {
+ _dummyUiState.update {
+ it.copy(dummyString = it.dummyString)
+ }
+ },
+ onFailure = { error ->
+ Log.e("DummyViewModel", "getDummyData: $error")
+ }
+ )
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/home/component/dialog/HomeDialog.kt b/app/src/main/java/com/kuit/ourmenu/ui/home/component/dialog/HomeDialog.kt
new file mode 100644
index 00000000..b5cdff6a
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/home/component/dialog/HomeDialog.kt
@@ -0,0 +1,123 @@
+package com.kuit.ourmenu.ui.home.component.dialog
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+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.wrapContentHeight
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.Surface
+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.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import com.kuit.ourmenu.ui.common.dialog.DialogBigButton
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun HomePopUpDialog(modifier: Modifier = Modifier) {
+
+ Dialog(
+ onDismissRequest = {
+ // TODO : Dismiss Dialog Event
+ },
+ ) {
+ Surface(
+ modifier = Modifier
+ .shadow(elevation = 8.dp)
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .background(
+ color = NeutralWhite,
+ shape = RoundedCornerShape(size = 12.dp)
+ )
+ .clip(RoundedCornerShape(12.dp))
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(
+ start = 16.dp,
+ end = 16.dp,
+ top = 16.dp,
+ bottom = 24.dp
+ )
+ .fillMaxWidth()
+ .wrapContentHeight()
+
+ ) {
+ HomeDialogAssets(
+ onDiceClick = {
+ // TODO : Dice Click Event
+ },
+ onCloseClick = {
+ // TODO : Close Click Event
+ }
+ )
+ Text(
+ modifier = Modifier
+ .padding(top = 13.dp)
+ .fillMaxWidth(),
+ text = "์๋
ํ์ธ์!\n" +
+ "์ค๋์ ๊ธฐ๋ถ์ ์ด๋ ์ ๊ฐ์?", // TODO : ์ถ์ฒ ๋ฌธ๊ตฌ ๋ฐ์
+ style = ourMenuTypography().pretendard_700_20.copy(
+ color = Neutral900
+ ),
+ textAlign = TextAlign.Center
+ )
+ Text(
+ modifier = Modifier
+ .padding(top = 13.dp, bottom = 19.dp)
+ .fillMaxWidth(),
+ text = "์ง๋ฌธ์ ๋ตํด ๊ธฐ๋ถ์ ๋ง๋ ๋ฉ๋ด๋ฅผ\n" +
+ "์ถ์ฒ๋ฐ์๋ณด์ธ์!",
+ style = ourMenuTypography().pretendard_500_14.copy(
+ fontWeight = FontWeight(500),
+ color = Neutral500
+ ),
+ textAlign = TextAlign.Center
+ )
+
+ DialogBigButton(
+ modifier = Modifier
+ .padding(bottom = 8.dp)
+ .padding(horizontal = 4.dp)
+ .fillMaxWidth()
+ .height(48.dp),
+ buttonText = "์ข์!",
+ containerColor = Primary500Main
+ )
+
+ DialogBigButton(
+ modifier = Modifier
+ .padding(horizontal = 4.dp)
+ .fillMaxWidth()
+ .height(48.dp),
+ buttonText = "๋ณ๋ก์ผ..",
+ containerColor = Primary500Main
+ )
+ }
+ }
+ }
+}
+
+@Preview(
+ showBackground = true,
+ backgroundColor = 0x111111
+)
+@Composable
+private fun HomePopUpDialogPreview() {
+ HomePopUpDialog()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/home/component/dialog/HomeDialogAssets.kt b/app/src/main/java/com/kuit/ourmenu/ui/home/component/dialog/HomeDialogAssets.kt
new file mode 100644
index 00000000..f66b7077
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/home/component/dialog/HomeDialogAssets.kt
@@ -0,0 +1,72 @@
+package com.kuit.ourmenu.ui.home.component.dialog
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+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.layout.width
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun HomeDialogAssets(
+ onDiceClick: () -> Unit = {},
+ onCloseClick: () -> Unit = {},
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth(),
+ ) {
+ Row {
+ HomeDialogTouchBox(
+ modifier = Modifier
+ .padding(top = 4.dp, start = 4.dp)
+ .height(32.dp)
+ .weight(72f)
+ )
+ Spacer(
+ modifier = Modifier.weight(156f)
+ )
+ Icon(
+ painter = painterResource(R.drawable.ic_close_24_n400),
+ contentDescription = null,
+ tint = Color.Unspecified,
+ modifier = Modifier
+ .aspectRatio(1f)
+ .weight(24f)
+ )
+ }
+ Image(
+ painter = painterResource(R.drawable.img_popup_dice),
+ contentDescription = null,
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(top = 27.dp)
+ .padding(horizontal = 82.dp)
+ .fillMaxWidth()
+ .aspectRatio(1f)
+ )
+ // TODO : Add Dice shadow asset
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun Prev() {
+ HomeDialogAssets()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/home/component/dialog/HomeDialogTouchBox.kt b/app/src/main/java/com/kuit/ourmenu/ui/home/component/dialog/HomeDialogTouchBox.kt
new file mode 100644
index 00000000..313a6e18
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/home/component/dialog/HomeDialogTouchBox.kt
@@ -0,0 +1,52 @@
+package com.kuit.ourmenu.ui.home.component.dialog
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun HomeDialogTouchBox(modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier,
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_home_dialog_touch),
+ contentDescription = null,
+ tint = Color.Unspecified
+ )
+ Row(
+ modifier
+ .padding(horizontal = 11.dp)
+ .padding(top = 3.dp, bottom = 6.dp)
+ ) {
+ Text(
+ text = "ํฐ์น!",
+ style = ourMenuTypography().pretendard_500_14.copy(
+ color = Neutral700
+ ),
+ modifier = Modifier.padding(end = 4.dp)
+ )
+ Text(
+ text = "\uD83D\uDC49",
+ style = ourMenuTypography().pretendard_500_14.copy(
+ color = Neutral700
+ )
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/main/HomeMainRecommendation.kt b/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/main/HomeMainRecommendation.kt
new file mode 100644
index 00000000..e2e93266
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/main/HomeMainRecommendation.kt
@@ -0,0 +1,48 @@
+package com.kuit.ourmenu.ui.home.component.recommendation.main
+
+import androidx.compose.foundation.layout.Column
+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.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.ui.home.dummy.HomeDummyData
+
+@Composable
+fun HomeMainRecommendation(
+ modifier: Modifier = Modifier,
+ homeMainDataList : List
+) {
+ Column(
+ modifier = modifier
+ ) {
+ HomeMainRecommendationText(
+ modifier = Modifier
+ .padding(horizontal = 20.dp)
+ .width(278.dp)
+ .height(144.dp)
+ )
+
+ HomeMainRecommendationList(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 32.dp)
+ .height(244.dp),
+ homeMainDataList = homeMainDataList
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MainRecommendationPreview() {
+ HomeMainRecommendation(
+ modifier = Modifier.padding(top = 16.dp),
+ homeMainDataList = HomeDummyData.dummyData
+ )
+}
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/main/HomeMainRecommendationItem.kt b/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/main/HomeMainRecommendationItem.kt
new file mode 100644
index 00000000..ac655278
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/main/HomeMainRecommendationItem.kt
@@ -0,0 +1,92 @@
+package com.kuit.ourmenu.ui.home.component.recommendation.main
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+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.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+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.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.home.dummy.HomeDummyData
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun HomeMainRecommendationItem(
+ modifier: Modifier = Modifier,
+ recommendData: HomeDummyData
+) {
+ Box(
+ modifier = modifier,
+ contentAlignment = Alignment.BottomStart
+ ) {
+ Image(
+ painter = painterResource(recommendData.imageRes), // TODO : ์ถํ Async Image Loading ์ ์ฉ
+ contentDescription = "Main Recommendation Image",
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .height(244.dp)
+ .width(304.dp)
+ .clip(RoundedCornerShape(32.dp))
+ )
+
+ Column(
+ modifier = Modifier.padding(horizontal = 16.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(42.dp),
+ contentAlignment = Alignment.CenterStart, // ์์ง ๋ฐ ์ํ ์ค์ ์ ๋ ฌ
+ ) {
+ Text(
+ text = recommendData.name,
+ style = ourMenuTypography().pretendard_700_24.copy(
+ shadow = Shadow(
+ color = Color.Black.copy(alpha = 0.2f), // ๊ทธ๋ฆผ์ ์์ ๋ฐ ํฌ๋ช
๋
+ offset = Offset(0f, 2f), // ๊ทธ๋ฆผ์ ์คํ์
(x = 0px, y = 2px)
+ blurRadius = 4f // ๋ธ๋ฌ ๋ฐ๊ฒฝ
+ )
+ ),
+ color = NeutralWhite,
+ )
+ }
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 16.dp)
+ .height(24.dp),
+ contentAlignment = Alignment.CenterStart, // ์์ง ๋ฐ ์ํ ์ค์ ์ ๋ ฌ
+ ) {
+ Text(
+ text = recommendData.store,
+ style = ourMenuTypography().pretendard_600_16.copy(
+ shadow = Shadow(
+ color = Color.Black.copy(alpha = 0.2f), // ๊ทธ๋ฆผ์ ์์ ๋ฐ ํฌ๋ช
๋
+ offset = Offset(0f, 2f), // ๊ทธ๋ฆผ์ ์คํ์
(x = 0px, y = 2px)
+ blurRadius = 4f // ๋ธ๋ฌ ๋ฐ๊ฒฝ
+ ),
+ letterSpacing = (-0.4).sp,
+ fontStyle = FontStyle.Normal,
+ ),
+ color = NeutralWhite,
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/main/HomeMainRecommendationList.kt b/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/main/HomeMainRecommendationList.kt
new file mode 100644
index 00000000..cce7aea0
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/main/HomeMainRecommendationList.kt
@@ -0,0 +1,47 @@
+package com.kuit.ourmenu.ui.home.component.recommendation.main
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.ui.home.dummy.HomeDummyData
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun HomeMainRecommendationList(
+ modifier: Modifier = Modifier,
+ homeMainDataList: List
+) {
+ val state = rememberLazyListState() // TODO : hoisting
+ val startIndex = (Int.MAX_VALUE / 2) - (Int.MAX_VALUE / 2) % homeMainDataList.size
+ LaunchedEffect(Unit) {
+ state.scrollToItem(startIndex)
+ }
+
+ LazyRow(
+ modifier = modifier,
+ contentPadding = PaddingValues(horizontal = 28.dp),
+ flingBehavior = rememberSnapFlingBehavior(lazyListState = state), // Snap Fling Behavior
+ state = state
+ ) {
+ items(Int.MAX_VALUE) { index ->
+ val itemIndex = index % homeMainDataList.size
+
+ HomeMainRecommendationItem(
+ modifier = Modifier
+ .height(244.dp)
+ .width(304.dp)
+ .padding(horizontal = 6.dp),
+ recommendData = homeMainDataList[itemIndex]
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/main/HomeMainRecommendationText.kt b/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/main/HomeMainRecommendationText.kt
new file mode 100644
index 00000000..51e80d03
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/main/HomeMainRecommendationText.kt
@@ -0,0 +1,24 @@
+package com.kuit.ourmenu.ui.home.component.recommendation.main
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+
+@Composable
+fun HomeMainRecommendationText(
+ modifier: Modifier = Modifier,
+ imgUrl: String = ""
+) {
+ Image(
+ modifier = modifier,
+ painter = painterResource(id = R.drawable.ic_home_reco_1),
+ contentDescription = "Home Banner"
+ )
+}
+
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/sub/HomeSubRecommendation.kt b/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/sub/HomeSubRecommendation.kt
new file mode 100644
index 00000000..ff4d9451
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/sub/HomeSubRecommendation.kt
@@ -0,0 +1,57 @@
+package com.kuit.ourmenu.ui.home.component.recommendation.sub
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.home.dummy.HomeDummyData
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun HomeSubRecommendation(
+ modifier: Modifier = Modifier,
+ homeSubDataList: List = listOf()
+) {
+ val state = rememberLazyListState() // TODO : hoisting
+ Column(modifier = modifier) {
+ HomeSubRecommendationText(
+ modifier = Modifier
+ .padding(start = 20.dp, end = 20.dp, bottom = 11.dp),
+ icon = R.drawable.ic_home_sub_reco_1,
+ text = "์ถ์ฒ ๋ฉ๋ด"
+ )
+
+ HomeSubRecommendationList(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ state = state,
+ homeSubDataList = homeSubDataList
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun HomeSubRecommendationListPreview() {
+ HomeSubRecommendation(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ HomeDummyData.dummyData
+ )
+}
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/sub/HomeSubRecommendationItem.kt b/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/sub/HomeSubRecommendationItem.kt
new file mode 100644
index 00000000..414e42b4
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/sub/HomeSubRecommendationItem.kt
@@ -0,0 +1,65 @@
+package com.kuit.ourmenu.ui.home.component.recommendation.sub
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+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.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.home.dummy.HomeDummyData
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+
+@Composable
+fun HomeSubRecommendationItem(
+ recommendData: HomeDummyData
+) {
+ Column(
+ modifier = Modifier.padding(end = 11.dp)
+ ) {
+ Image(
+ painter = painterResource(recommendData.imageRes),
+ contentDescription = "Home Sub Recommendation Image",
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .height(140.dp)
+ .width(180.dp)
+ .padding(bottom = 8.dp)
+ .clip(shape = RoundedCornerShape(12.dp))
+ )
+ Text(
+ text = recommendData.name,
+ style = ourMenuTypography().pretendard_600_18.copy(
+ color = Neutral900,
+ lineHeight = 27.sp
+ )
+ )
+ Text(
+ text = recommendData.store,
+ style = TextStyle(
+ // TODO : Design system ์ ์ฉ
+ fontSize = 14.sp,
+ lineHeight = 21.sp,
+ fontFamily = FontFamily(Font(R.font.medium)),
+ fontWeight = FontWeight(500),
+ color = Neutral700,
+ letterSpacing = (-0.154).sp
+ )
+ )
+ }
+}
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/sub/HomeSubRecommendationList.kt b/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/sub/HomeSubRecommendationList.kt
new file mode 100644
index 00000000..27a93cc2
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/sub/HomeSubRecommendationList.kt
@@ -0,0 +1,32 @@
+package com.kuit.ourmenu.ui.home.component.recommendation.sub
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.ui.home.dummy.HomeDummyData
+
+@Composable
+fun HomeSubRecommendationList(
+ modifier: Modifier = Modifier,
+ state: LazyListState,
+ homeSubDataList: List
+) {
+ LazyRow(
+ modifier = modifier,
+ state = state,
+ contentPadding = PaddingValues(
+ start = 20.dp, end = 9.dp
+ ),
+ ) {
+ items(homeSubDataList.size) { data ->
+ HomeSubRecommendationItem(
+ recommendData = homeSubDataList[data]
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/sub/HomeSubRecommendationText.kt b/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/sub/HomeSubRecommendationText.kt
new file mode 100644
index 00000000..83ce0b53
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/home/component/recommendation/sub/HomeSubRecommendationText.kt
@@ -0,0 +1,45 @@
+package com.kuit.ourmenu.ui.home.component.recommendation.sub
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+
+@Composable
+fun HomeSubRecommendationText(
+ modifier: Modifier = Modifier,
+ icon: Int,
+ text: String,
+) {
+ Row(
+ modifier = modifier,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // TODO : Async Image Loading
+ Icon(
+ painter = painterResource(icon),
+ modifier = Modifier
+ .size(32.dp)
+ .padding(end = 4.dp),
+ contentDescription = "Home Sub Recommendation 1",
+ tint = Color.Unspecified
+ )
+ Text(
+ text = text,
+ style = ourMenuTypography().pretendard_700_24.copy(
+ color = Neutral900
+ )
+ )
+ }
+}
+
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/home/dummy/HomeDummyData.kt b/app/src/main/java/com/kuit/ourmenu/ui/home/dummy/HomeDummyData.kt
new file mode 100644
index 00000000..c89c5c28
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/home/dummy/HomeDummyData.kt
@@ -0,0 +1,30 @@
+package com.kuit.ourmenu.ui.home.dummy
+
+import com.kuit.ourmenu.R
+
+data class HomeDummyData(
+ val imageRes: Int,
+ val name: String,
+ val store: String,
+) {
+
+ companion object {
+ val dummyData = listOf(
+ HomeDummyData(
+ imageRes = R.drawable.img_dummy_pizza,
+ name = "์ด์ฝ ์ํํธ์ฝ 1",
+ store = "์์ด์คํฌ๋ฆผ์ธ๊ณํ ์ธ์ ",
+ ),
+ HomeDummyData(
+ imageRes = R.drawable.img_dummy_pizza,
+ name = "์ด์ฝ ์ํํธ์ฝ 2",
+ store = "์์ด์คํฌ๋ฆผ์ธ๊ณํ ์ธ์ ",
+ ),
+ HomeDummyData(
+ imageRes = R.drawable.img_dummy_pizza,
+ name = "์ด์ฝ ์ํํธ์ฝ 3",
+ store = "์์ด์คํฌ๋ฆผ์ธ๊ณํ ์ธ์ ",
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/home/navigation/HomeNavigation.kt b/app/src/main/java/com/kuit/ourmenu/ui/home/navigation/HomeNavigation.kt
new file mode 100644
index 00000000..2fecea7e
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/home/navigation/HomeNavigation.kt
@@ -0,0 +1,24 @@
+package com.kuit.ourmenu.ui.home.navigation
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavOptions
+import androidx.navigation.compose.composable
+import com.kuit.ourmenu.ui.home.screen.HomeScreen
+import com.kuit.ourmenu.ui.navigator.MainTabRoute
+
+fun NavController.navigateToHome(navOptions: NavOptions) {
+ navigate(MainTabRoute.Home, navOptions)
+}
+
+fun NavGraphBuilder.homeNavGraph(
+ padding: PaddingValues,
+ // navigate ์ด๋ฒคํธ
+) {
+ composable {
+ HomeScreen(
+ // navigate ์ด๋ฒคํธ + ๊ธฐํ ์ด๋ฒคํธ
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/home/screen/HomeScreen.kt
new file mode 100644
index 00000000..13b803d0
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/home/screen/HomeScreen.kt
@@ -0,0 +1,72 @@
+package com.kuit.ourmenu.ui.home.screen
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.navigation.compose.rememberNavController
+import com.kuit.ourmenu.ui.common.topappbar.OurMenuAddButtonTopAppBar
+import com.kuit.ourmenu.ui.home.component.recommendation.main.HomeMainRecommendation
+import com.kuit.ourmenu.ui.home.component.recommendation.sub.HomeSubRecommendation
+import com.kuit.ourmenu.ui.home.dummy.HomeDummyData
+
+@Composable
+fun HomeScreen(modifier: Modifier = Modifier) {
+
+ val scrollState = rememberScrollState()
+
+ Scaffold(
+ topBar = {
+ OurMenuAddButtonTopAppBar()
+ }
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .padding(innerPadding)
+ .verticalScroll(scrollState),
+ horizontalAlignment = Alignment.Start
+ ) {
+
+ HomeMainRecommendation(
+ modifier = Modifier
+ .padding(top = 16.dp, bottom = 29.dp),
+ homeMainDataList = HomeDummyData.dummyData
+ )
+
+
+ HomeSubRecommendation(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(bottom = 25.dp),
+ homeSubDataList = HomeDummyData.dummyData
+ )
+
+ HomeSubRecommendation(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(bottom = 25.dp),
+ homeSubDataList = HomeDummyData.dummyData
+ )
+ }
+ }
+}
+
+@Preview(
+ widthDp = 360,
+ heightDp = 800
+)
+@Composable
+private fun HomeScreenPreview() {
+ val navController = rememberNavController()
+ HomeScreen()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/AddButton.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/AddButton.kt
new file mode 100644
index 00000000..545aa96f
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/AddButton.kt
@@ -0,0 +1,52 @@
+package com.kuit.ourmenu.ui.menuFolder.component
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+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.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.Neutral100
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun AddButton(btnName: String, modifier: Modifier, onClick: () -> Unit) {
+ Card(
+ shape = RoundedCornerShape(12.dp),
+ modifier = modifier.padding(bottom = 20.dp),
+ colors = CardDefaults.cardColors(containerColor = Neutral100),
+ elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
+ onClick = onClick
+ ) {
+ Column(
+ modifier = Modifier
+ .height(52.dp)
+ .fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = btnName,
+ color = Neutral500,
+ style = ourMenuTypography().pretendard_600_14
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun AddButtonPreview() {
+ AddButton(stringResource(R.string.add_menu), modifier = Modifier, onClick = {})
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/DeleteMenuFolderModal.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/DeleteMenuFolderModal.kt
new file mode 100644
index 00000000..a2817e02
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/DeleteMenuFolderModal.kt
@@ -0,0 +1,143 @@
+package com.kuit.ourmenu.ui.menuFolder.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.my.component.LogoutModal
+import com.kuit.ourmenu.ui.theme.Neutral400
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun DeleteMenuFolderModal(
+ onDismiss: () -> Unit,
+ onConfirm: () -> Unit
+) {
+ Dialog(onDismissRequest = onDismiss) {
+ Column(
+ modifier = Modifier
+ .background(NeutralWhite, shape = RoundedCornerShape(16.dp))
+ .padding(20.dp)
+ .width(288.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ // ๋ซ๊ธฐ ์์ด์ฝ
+ Icon(
+ painter = painterResource(R.drawable.ic_close_24_n400),
+ contentDescription = null,
+ tint = Color.Unspecified,
+ modifier = Modifier
+ .align(Alignment.End)
+ .clickable { onDismiss() }
+ .size(24.dp)
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // ์ ๋ชฉ
+ Text(
+ text = stringResource(R.string.delete_menu_folder_modal_title),
+ style = ourMenuTypography().pretendard_700_18,
+ color = Neutral900,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Text(
+ text = stringResource(R.string.delete_menu_folder_modal_content),
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral500,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // ๋ฒํผ Row
+ Column(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+
+ Button(
+ onClick = {
+ onConfirm()
+ onDismiss()
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(48.dp),
+ shape = RoundedCornerShape(8.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Primary500Main,
+ contentColor = NeutralWhite
+ ),
+ ) {
+ Text(
+ text = stringResource(R.string.delete),
+ style = ourMenuTypography().pretendard_700_18,
+ color = NeutralWhite
+ )
+ }
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Button(
+ onClick = {
+ onDismiss()
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(48.dp),
+ shape = RoundedCornerShape(8.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Neutral400,
+ contentColor = NeutralWhite
+ ),
+ ) {
+ Text(
+ text = stringResource(R.string.cancel),
+ style = ourMenuTypography().pretendard_700_18,
+ color = NeutralWhite
+ )
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun DeleteMenuFolderModalPreview() {
+ DeleteMenuFolderModal(
+ onDismiss = {},
+ onConfirm = {}
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/FilterBottomSheet.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/FilterBottomSheet.kt
new file mode 100644
index 00000000..d18fb055
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/FilterBottomSheet.kt
@@ -0,0 +1,316 @@
+package com.kuit.ourmenu.ui.menuFolder.component
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.RangeSlider
+import androidx.compose.material3.SliderDefaults
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+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.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.data.model.base.type.TagType
+import com.kuit.ourmenu.ui.common.BottomHalfWidthButton
+import com.kuit.ourmenu.ui.common.OurSnackbarHost
+import com.kuit.ourmenu.ui.common.chip.TagChipGroup
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral400
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun FilterBottomSheet(
+ modifier: Modifier = Modifier,
+ categoryTagList: List>,
+ nationalityTagList: List>,
+ tasteTagList: List>,
+ occasionTagList: List>,
+ onSelectedTagsChange: (List) -> Unit,
+ onPriceRangeChange: (Long, Long) -> Unit,
+ onApplyButtonClick: () -> Unit,
+) {
+ // toast๋ฅผ ์ํ context
+ val context = LocalContext.current
+ val snackbarHostState = remember { SnackbarHostState() }
+ val scope = rememberCoroutineScope()
+
+ // ๊ฐ๊ฒฉ ๋ฒ์ ์ค์
+ val minPrice = 0f
+ val maxPrice = 50_000f
+ var priceRange by remember { mutableStateOf(minPrice..maxPrice) }
+ val stepSize = 5000f // 5000์ ๋จ์ ์กฐ์
+
+ // ๊ฐ ํญ๋ชฉ๋ณ๋ก ์ ํ๋ ํ๊ทธ ์ํ๋ฅผ ๊ฐ๋ณ์ ์ผ๋ก ๊ด๋ฆฌ
+ var selectedCategoryTag by rememberSaveable { mutableStateOf(null) }
+ var selectedNationalityTag by rememberSaveable { mutableStateOf(null) }
+ var selectedTasteTag by rememberSaveable { mutableStateOf(null) }
+ var selectedOccasionTag by rememberSaveable { mutableStateOf(null) }
+
+ fun updateSelectedTags() {
+ val selectedTags = listOfNotNull(
+ selectedCategoryTag,
+ selectedNationalityTag,
+ selectedTasteTag,
+ selectedOccasionTag
+ )
+ onSelectedTagsChange(selectedTags)
+ }
+
+ Box(modifier = Modifier.fillMaxSize()) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize(),
+ verticalArrangement = Arrangement.SpaceBetween
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = stringResource(R.string.filtering),
+ style = ourMenuTypography().pretendard_700_16
+ )
+ Text(
+ text = stringResource(R.string.select_one),
+ style = ourMenuTypography().pretendard_600_14,
+ color = Neutral500
+ )
+ }
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ LazyColumn(
+ modifier = Modifier
+ .padding(horizontal = 20.dp),
+ ) {
+ // ์ข
๋ฅ
+ item {
+ TagChipGroup(
+ groupLabel = stringResource(R.string.type),
+ tags = categoryTagList.map { it.first to it.second.displayName },
+ selectedTags = listOfNotNull(selectedCategoryTag?.displayName),
+ ) { tagDisplayName ->
+ val matchedTag = TagType.fromDisplayName(tagDisplayName)
+ if (selectedCategoryTag != null && selectedCategoryTag != matchedTag) {
+ scope.launch {
+ snackbarHostState.showSnackbar(context.getString(R.string.tag_max_one))
+ }
+ } else {
+ selectedCategoryTag = if (selectedCategoryTag == matchedTag) null else matchedTag
+ updateSelectedTags()
+ }
+ }
+ }
+
+ // ๋๋ผ ๋ณ ์์
+ item {
+ TagChipGroup(
+ groupLabel = stringResource(R.string.nationality),
+ tags = nationalityTagList.map { it.first to it.second.displayName },
+ selectedTags = listOfNotNull(selectedNationalityTag?.displayName),
+ ) { tagDisplayName ->
+ val matchedTag = TagType.fromDisplayName(tagDisplayName)
+ if (selectedNationalityTag != null && selectedNationalityTag != matchedTag) {
+ scope.launch {
+ snackbarHostState.showSnackbar(context.getString(R.string.tag_max_one))
+ }
+ } else {
+ selectedNationalityTag = if (selectedNationalityTag == matchedTag) null else matchedTag
+ updateSelectedTags()
+ }
+ }
+ }
+
+ // ๋ง
+ item {
+ TagChipGroup(
+ groupLabel = stringResource(R.string.taste),
+ tags = tasteTagList.map { it.first to it.second.displayName },
+ selectedTags = listOfNotNull(selectedTasteTag?.displayName),
+ ) { tagDisplayName ->
+ val matchedTag = TagType.fromDisplayName(tagDisplayName)
+ if (selectedTasteTag != null && selectedTasteTag != matchedTag) {
+ scope.launch {
+ snackbarHostState.showSnackbar(context.getString(R.string.tag_max_one))
+ }
+ } else {
+ selectedTasteTag = if (selectedTasteTag == matchedTag) null else matchedTag
+ updateSelectedTags()
+ }
+ }
+ }
+
+ // ์ํฉ
+ item {
+ TagChipGroup(
+ groupLabel = stringResource(R.string.occasion),
+ tags = occasionTagList.map { it.first to it.second.displayName },
+ selectedTags = listOfNotNull(selectedOccasionTag?.displayName),
+ ) { tagDisplayName ->
+ val matchedTag = TagType.fromDisplayName(tagDisplayName)
+ if (selectedOccasionTag != null && selectedOccasionTag != matchedTag) {
+ scope.launch {
+ snackbarHostState.showSnackbar(context.getString(R.string.tag_max_one))
+ }
+ } else {
+ selectedOccasionTag = if (selectedOccasionTag == matchedTag) null else matchedTag
+ updateSelectedTags()
+ }
+ }
+ }
+
+ item { Spacer(modifier = Modifier.height(12.dp)) }
+
+ // ๊ฐ๊ฒฉ ์ฌ๋ผ์ด๋
+ item {
+ Text(
+ text = stringResource(R.string.price_range_title),
+ style = ourMenuTypography().pretendard_400_14,
+ color = Neutral900
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = stringResource(
+ R.string.price_range,
+ priceRange.start.toInt(),
+ priceRange.endInclusive.toInt()
+ ),
+ style = ourMenuTypography().pretendard_700_18
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ RangeSlider(
+ value = priceRange,
+ onValueChange = { newValue ->
+ val adjustedStart =
+ (Math.round(newValue.start / stepSize) * stepSize).coerceAtLeast(
+ minPrice
+ )
+ val adjustedEnd =
+ (Math.round(newValue.endInclusive / stepSize) * stepSize).coerceAtMost(
+ maxPrice
+ )
+
+ priceRange = adjustedStart..adjustedEnd // 5000 ๋จ์ ๋ฐ์ฌ๋ฆผ ์ ์ฉ
+ },
+ valueRange = minPrice..maxPrice,
+ steps = ((maxPrice - minPrice) / stepSize - 1).toInt(), // 5000์ ๋จ์๋ก ์ด๋
+ colors = SliderDefaults.colors(
+ thumbColor = Primary500Main,
+ activeTrackColor = Primary500Main,
+ inactiveTrackColor = Neutral300,
+ activeTickColor = Primary500Main,
+ inactiveTickColor = Neutral300
+ ),
+ modifier = Modifier.padding(horizontal = 8.dp),
+ startThumb = {
+ SliderDefaults.Thumb(
+ interactionSource = remember { MutableInteractionSource() },
+ colors = SliderDefaults.colors(
+ thumbColor = Primary500Main
+ ),
+ modifier = Modifier
+ .size(28.dp)
+ .clip(CircleShape)
+ )
+ },
+ endThumb = {
+ SliderDefaults.Thumb(
+ interactionSource = remember { MutableInteractionSource() },
+ colors = SliderDefaults.colors(
+ thumbColor = Primary500Main
+ ),
+ modifier = Modifier
+ .size(28.dp)
+ .clip(CircleShape)
+ )
+ },
+ )
+
+ Spacer(modifier = Modifier.height(24.dp))
+ }
+ }
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ }
+
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(bottom = 20.dp, start = 20.dp, end = 20.dp),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ BottomHalfWidthButton(
+ modifier = modifier.weight(1f),
+ containerColor = Neutral400,
+ contentColor = NeutralWhite,
+ text = stringResource(R.string.reset)
+ ) {
+ // ๋ชจ๋ ์ ํ๋ ํํฐ ์ด๊ธฐํ
+ selectedCategoryTag = null
+ selectedNationalityTag = null
+ selectedTasteTag = null
+ selectedOccasionTag = null
+ priceRange = minPrice..maxPrice
+
+ onSelectedTagsChange(emptyList())
+ }
+ Spacer(modifier = modifier.width(12.dp))
+ BottomHalfWidthButton(
+ modifier = modifier.weight(1f),
+ containerColor = Primary500Main,
+ contentColor = NeutralWhite,
+ text = stringResource(R.string.apply)
+ ) {
+ onPriceRangeChange(priceRange.start.toLong(), priceRange.endInclusive.toLong())
+ onApplyButtonClick()
+ }
+ }
+ }
+
+ OurSnackbarHost(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(bottom = 60.dp),
+ hostState = snackbarHostState,
+ isChecked = false,
+ )
+ }
+}
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/MenuFolderButton.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/MenuFolderButton.kt
new file mode 100644
index 00000000..deef4d66
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/MenuFolderButton.kt
@@ -0,0 +1,285 @@
+package com.kuit.ourmenu.ui.menuFolder.component
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.gestures.detectHorizontalDragGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+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.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import coil3.compose.AsyncImage
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.data.model.menuFolder.response.MenuFolderList
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import kotlinx.coroutines.launch
+import kotlin.math.roundToInt
+
+
+@Composable
+fun MenuFolderButton(
+ modifier: Modifier = Modifier,
+ menuFolder: MenuFolderList,
+ isSwiped: Boolean, // ํ์ฌ ๋ฒํผ์ด ์ค์์ดํ๋ ์ํ์ธ์ง ํ์ธ
+ onSwipe: () -> Unit, // ์๋ก์ด ๋ฒํผ์ด ์ค์์ดํ๋ ๋ ํธ์ถ
+ onReset: () -> Unit, // ๋ฒํผ์ด ๋ซํ๋ฉด ํธ์ถ
+ onButtonClick: () -> Unit = {},
+ onEditClick: () -> Unit = {},
+ onDeleteClick: () -> Unit = {}
+) {
+ val density = LocalDensity.current
+ val offset = remember {
+ Animatable(initialValue = 0f)
+ }
+ val scope = rememberCoroutineScope()
+ val maxSwipe = with(density) {
+ 128.dp.toPx() // ์ต๋ ์ค์์ดํ ๋ฒ์๋ฅผ dp์์ px๋ก ๋ณํ
+ }
+
+ LaunchedEffect(isSwiped) {
+ if (!isSwiped) {
+ offset.snapTo(0f)
+ }
+ }
+
+ Box(
+ modifier = modifier
+ .height(132.dp)
+ .fillMaxWidth()
+ .pointerInput(Unit) {
+// detectHorizontalDragGestures(
+// onDragEnd = {
+// scope.launch {
+// if (offsetX < -80f) {
+// onSwipe() // ๋ค๋ฅธ ๋ฒํผ์ ๋ซ๊ณ ์ด ๋ฒํผ๋ง ์ค์์ดํ
+// offsetX = -maxSwipe
+// } else {
+// offsetX = 0f
+// onReset() // ์ค์์ดํ๊ฐ ๋ซํ๋ฉด ์ํ ์ด๊ธฐํ
+// }
+// }
+// }
+// ) { change, dragAmount ->
+// change.consume()
+// offsetX = (offsetX + dragAmount).coerceIn(-maxSwipe, 0f)
+// }
+ detectHorizontalDragGestures(
+ onHorizontalDrag = { _, dragAmount ->
+ scope.launch {
+ val newOffset = (offset.value + dragAmount)
+ .coerceIn(-maxSwipe, 0f)
+ offset.snapTo(newOffset)
+ }
+ },
+ onDragEnd = {
+ if (offset.value < -maxSwipe / 2) {
+ scope.launch {
+ offset.animateTo(-maxSwipe)
+ }
+ onSwipe()
+ } else {
+ scope.launch {
+ offset.animateTo(0f)
+ }
+ onReset()
+ }
+ }
+ )
+ }
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.CenterEnd
+ ) {
+ MenuFolderDeleteButton(onDeleteClick)
+ MenuFolderEditButton(onEditClick)
+ }
+
+ // ์ค์์ดํ ์ํ์ผ ๋๋ง offset ์ ์ฉ
+ Box(
+ modifier = Modifier
+// .offset(x = if (isSwiped) offset.value.dp else 0.dp)
+ .offset { IntOffset(offset.value.roundToInt(), 0) }
+
+ ) {
+ MenuFolderContent(
+ onClick = onButtonClick,
+ menuFolder = menuFolder
+ )
+ }
+ }
+}
+
+@Composable
+fun MenuFolderEditButton(onEditClick: () -> Unit = {}) {
+ Box(
+ modifier = Modifier
+ .padding(end = 64.dp)
+ .width(80.dp)
+ .fillMaxHeight()
+ .clip(RoundedCornerShape(topEnd = 12.dp, bottomEnd = 12.dp))
+ .background(color = Neutral300)
+ .clickable(onClick = onEditClick),
+ contentAlignment = Alignment.CenterEnd
+ ) {
+ Column(modifier = Modifier.padding(end = 20.dp)) {
+ Image(
+ painter = painterResource(id = R.drawable.ic_edit),
+ contentDescription = "Edit",
+ modifier = Modifier.size(24.dp)
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = stringResource(R.string.edit),
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral700,
+ )
+ }
+ }
+}
+
+@Composable
+fun MenuFolderDeleteButton(onDeleteClick: () -> Unit = {}) {
+ Box(
+ modifier = Modifier
+ .width(80.dp)
+ .fillMaxHeight()
+ .clip(RoundedCornerShape(topEnd = 12.dp, bottomEnd = 12.dp))
+ .background(color = Primary500Main)
+ .clickable(onClick = onDeleteClick),
+ contentAlignment = Alignment.CenterEnd,
+ ) {
+ Column(modifier = Modifier.padding(end = 20.dp)) {
+ Image(
+ painter = painterResource(id = R.drawable.ic_trashcan),
+ contentDescription = "Delete",
+ modifier = Modifier.size(24.dp)
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = stringResource(R.string.delete),
+ style = ourMenuTypography().pretendard_500_14,
+ color = NeutralWhite,
+ )
+ }
+ }
+}
+
+@Composable
+fun MenuFolderContent(
+ onClick: () -> Unit = {},
+ menuFolder: MenuFolderList,
+) {
+ val menuCount = menuFolder.menuIds.size
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+ AsyncImage(
+ model = menuFolder.menuFolderImgUrl,
+ contentDescription = "Folder Image",
+ contentScale = ContentScale.FillWidth,
+ modifier = Modifier
+ .fillMaxSize()
+ .clip(RoundedCornerShape(12.dp))
+ .clickable(onClick = onClick)
+ )
+
+ Box(
+ modifier = Modifier
+ .matchParentSize()
+ .clip(RoundedCornerShape(12.dp))
+ .background(brush = gradientBrush())
+ )
+
+ Box(
+ modifier = Modifier
+ .fillMaxHeight()
+ .padding(start = 20.dp, bottom = 12.dp),
+ contentAlignment = Alignment.BottomStart
+ ) {
+ Column {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ AsyncImage(
+ model = menuFolder.menuFolderIconImgUrl,
+ contentDescription = "Folder Icon",
+ modifier = Modifier.size(32.dp),
+ contentScale = ContentScale.Fit,
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ Text(
+ text = menuFolder.menuFolderTitle,
+ color = NeutralWhite,
+ style = ourMenuTypography().pretendard_500_24,
+ )
+ }
+ Spacer(modifier = Modifier.height(2.dp))
+ Text(
+ text = String.format(stringResource(R.string.menu_count), menuCount),
+ color = NeutralWhite,
+ style = ourMenuTypography().pretendard_500_14,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun gradientBrush(): Brush {
+ return Brush.verticalGradient(
+ colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.5f))
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MenuFolderButtonPreview() {
+ val dummyMenuFolder = MenuFolderList(
+ menuFolderId = 1,
+ menuFolderTitle = "์ธ๊ธฐ ๋ฉ๋ด",
+ menuFolderImgUrl = "https://ourmenu-default.s3.ap-northeast-2.amazonaws.com/default_menu_folder_img.svg",
+ menuFolderIconImgUrl = "DICE",
+ menuIds = listOf(1, 2, 3),
+ index = 0
+ )
+
+ MenuFolderButton(
+ modifier = Modifier,
+ menuFolder = dummyMenuFolder,
+ false, {}, {})
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/MenuFolderMenuButton.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/MenuFolderMenuButton.kt
new file mode 100644
index 00000000..9cb80ff8
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/MenuFolderMenuButton.kt
@@ -0,0 +1,151 @@
+package com.kuit.ourmenu.ui.menuFolder.component
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+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.shape.RoundedCornerShape
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Text
+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.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import coil3.compose.AsyncImage
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.data.model.menuFolder.response.MenuFolderDetailMenus
+import com.kuit.ourmenu.data.model.menuFolder.response.MenuFolderMenuItem
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun MenuFolderMenuButton(
+ menuFolderDetail: MenuFolderMenuItem,
+ onMenuClick: () -> Unit = {},
+ onMapClick: () -> Unit = {}
+) {
+ val menuPrice = 12000
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(114.dp)
+ .padding(horizontal = 20.dp, vertical = 12.dp)
+ .clickable(onClick = onMenuClick)
+ ) {
+ AsyncImage(
+ model = menuFolderDetail.menuImgUrl,
+ contentDescription = "Menu Image",
+ contentScale = ContentScale.Crop,
+ modifier = Modifier
+ .fillMaxHeight()
+ .width(120.dp)
+ .clip(RoundedCornerShape(12.dp))
+ )
+
+ Spacer(modifier = Modifier.width(16.dp))
+
+
+ Column {
+ Box(
+ modifier = Modifier.height(24.dp),
+ Alignment.Center
+ ) {
+ Text(
+ text = menuFolderDetail.menuTitle,
+ style = ourMenuTypography().pretendard_700_16,
+ color = Neutral900,
+ )
+ }
+
+ Box(
+ modifier = Modifier.height(20.dp),
+ Alignment.Center
+ ) {
+ Row {
+ Text(
+ text = menuFolderDetail.storeTitle,
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral700
+ )
+
+ Spacer(modifier = Modifier.width(5.dp))
+
+ Text(
+ text = "ยท",
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral700
+ )
+
+ Spacer(modifier = Modifier.width(5.dp))
+
+ Text(
+ text = menuFolderDetail.storeAddress,
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral700
+ )
+ }
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = String.format(stringResource(R.string.menu_price_won), menuFolderDetail.menuPrice),
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral700
+ )
+
+ // TODO: ๋ฒํผ ๋๋ฅด๋ฉด ์ง๋๋ก ์ด๋
+ Image(
+ painter = painterResource(id = R.drawable.ic_to_map),
+ contentDescription = "To Map",
+ modifier = Modifier.clickable(onClick = onMapClick)
+ )
+ }
+ }
+ }
+
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Neutral300,
+ modifier = Modifier
+ .fillMaxWidth()
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MenuFolderMenuButtonPreview() {
+ MenuFolderMenuButton(
+ menuFolderDetail = MenuFolderDetailMenus(
+ menuId = 1,
+ menuTitle = "Menu Title",
+ storeTitle = "Store Title",
+ storeAddress = "Store Address",
+ menuImgUrl = "https://ourmenu-default.s3.ap-northeast-2.amazonaws.com/default_menu_folder_img.svg",
+ menuPrice = 12000
+ ),
+ onMenuClick = {},
+ onMapClick = {}
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/MenuFolderTopAppBar.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/MenuFolderTopAppBar.kt
new file mode 100644
index 00000000..b6d8b8df
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/MenuFolderTopAppBar.kt
@@ -0,0 +1,67 @@
+package com.kuit.ourmenu.ui.menuFolder.component
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.NeutralBlack
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MenuFolderTopAppBar(onClick: () -> Unit) {
+ // ๊ธฐ๋ณธ
+ TopAppBar(
+ modifier = Modifier
+ .fillMaxWidth()
+ .drawBehind {
+ drawRect(
+ color = NeutralBlack
+ )
+ }
+ .shadow(elevation = 4.dp),
+ colors = TopAppBarDefaults.topAppBarColors().copy(
+ containerColor = NeutralWhite
+ ),
+ title = {
+ Icon(
+ painter = painterResource(R.drawable.ic_my_menu_title),
+ contentDescription = "Top App Bar Logo",
+ modifier = Modifier.padding(start = 8.dp),
+ tint = Color.Unspecified
+ )
+ },
+ actions = {
+ IconButton(
+ onClick = onClick,
+ modifier = Modifier.padding(end = 20.dp)
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_home_plus),
+ contentDescription = "Top App Bar Add Menu Button",
+ tint = Color.Unspecified
+ )
+ }
+ }
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun HomeTopAppBarPreview() {
+ MenuFolderTopAppBar(
+ onClick = {}
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/SortDropdown.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/SortDropdown.kt
new file mode 100644
index 00000000..23b98e90
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/SortDropdown.kt
@@ -0,0 +1,115 @@
+package com.kuit.ourmenu.ui.menuFolder.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun SortDropdown(
+ color: Color = Neutral700,
+ options: List,
+ selectedOption: String,
+ onSelectedOptionChange: (String) -> Unit
+) {
+ var expanded by remember { mutableStateOf(false) }
+
+ Box {
+ Row(
+ modifier = Modifier.clickable {
+ expanded = true
+ },
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = selectedOption,
+ style = ourMenuTypography().pretendard_500_14,
+ color = color
+ )
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Icon(
+ painter = if (expanded) painterResource(id = R.drawable.ic_dropdown_btn_up) else painterResource(
+ id = R.drawable.ic_dropdown_btn
+ ),
+ tint = color,
+ modifier = Modifier.size(16.dp),
+ contentDescription = "Expand Down"
+ )
+ }
+
+ DropdownMenu(
+ modifier = Modifier
+ .width(72.dp)
+ .background(color = NeutralWhite, shape = RoundedCornerShape(8.dp)),
+ expanded = expanded,
+ onDismissRequest = { expanded = false }
+ ) {
+ options.forEach { option ->
+ val isSelected = option == selectedOption
+
+ DropdownMenuItem(
+ modifier = Modifier
+ .width(72.dp),
+ text = {
+ Text(
+ text = option,
+ style = ourMenuTypography().pretendard_500_14.copy(
+ color = if (isSelected) Primary500Main else Neutral500
+ )
+ )
+ },
+ onClick = {
+ onSelectedOptionChange(option)
+ }
+ )
+ }
+ }
+ }
+
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SortDropdownPreview() {
+ val options = listOf("์ด๋ฆ์", "๋ฑ๋ก์", "๊ฐ๊ฒฉ์")
+ var selectedOption by rememberSaveable { mutableStateOf("์ด๋ฆ์") }
+
+ Column(modifier = Modifier.fillMaxSize()) {
+ SortDropdown(
+ options = options,
+ selectedOption = selectedOption
+ ) {
+ selectedOption = it
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/navigation/MenuFolderNavigation.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/navigation/MenuFolderNavigation.kt
new file mode 100644
index 00000000..5d0e5a67
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/navigation/MenuFolderNavigation.kt
@@ -0,0 +1,72 @@
+package com.kuit.ourmenu.ui.menuFolder.navigation
+
+import android.R.attr.padding
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavOptions
+import androidx.navigation.compose.composable
+import androidx.navigation.toRoute
+import com.kuit.ourmenu.ui.menuFolder.screen.MenuFolderAllMenuScreen
+import com.kuit.ourmenu.ui.menuFolder.screen.MenuFolderDetailScreen
+import com.kuit.ourmenu.ui.menuFolder.screen.MenuFolderScreen
+import com.kuit.ourmenu.ui.navigator.MainTabRoute
+import com.kuit.ourmenu.ui.navigator.Routes
+
+fun NavController.navigateToMenuFolder(navOptions: NavOptions) {
+ navigate(MainTabRoute.MenuFolder, navOptions)
+}
+
+// ์ด๋ ์ด๋ฒคํธ (menuFolderId ์ ๋ฌ)
+fun NavController.navigateToMenuFolderDetail(menuFolderId: Long) {
+ navigate(Routes.MenuFolderDetail(menuFolderId))
+}
+
+fun NavController.navigateToMenuFolderAllMenu() {
+ navigate(Routes.MenuFolderAllMenu)
+}
+
+fun NavController.navigateToAddMenu() {
+ navigate(Routes.AddMenu)
+}
+
+fun NavController.navigateToMenuInfo(menuId: Long) {
+ navigate(Routes.MenuInfo(menuId))
+}
+
+fun NavGraphBuilder.menuFolderNavGraph(
+ padding: PaddingValues,
+ navigateBack: () -> Unit,
+ navigateToMenuFolderDetail: (Long) -> Unit,
+ navigateToMenuFolderAllMenu: () -> Unit,
+ navigateToMenuInfo: (Long) -> Unit,
+ navigateToAddMenu: () -> Unit,
+) {
+ composable {
+ MenuFolderScreen(
+ padding = padding,
+ onNavigateToDetail = navigateToMenuFolderDetail,
+ onNavigateToAllMenu = navigateToMenuFolderAllMenu,
+ onNavigateToAddMenu = navigateToAddMenu,
+ )
+
+ composable {
+ val menuFolderId = it.toRoute().menuFolderId
+ MenuFolderDetailScreen(
+ menuFolderId = menuFolderId,
+ onNavigateToMenuInfo = navigateToMenuInfo,
+ onNavigateBack = navigateBack,
+ onNavigateToAddMenu = navigateToAddMenu
+ )
+ }
+
+ composable {
+ MenuFolderAllMenuScreen(
+ onNavigateBack = navigateBack,
+ onNavigateToMenuInfo = navigateToMenuFolderDetail,
+// onNavigateToMenuInfoMap = navigateToMenuFolderDetail, // TODO: Map์ผ๋ก ํ๋ฉด ์ด๋ ๊ตฌํ
+ onNavigateToAddMenu = navigateToAddMenu,
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderAllMenuScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderAllMenuScreen.kt
new file mode 100644
index 00000000..00b7987e
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderAllMenuScreen.kt
@@ -0,0 +1,227 @@
+package com.kuit.ourmenu.ui.menuFolder.screen
+
+import android.util.Log
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.BottomSheetScaffold
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberBottomSheetScaffoldState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.data.model.base.type.SortOrderType
+import com.kuit.ourmenu.ui.common.bottomsheet.BottomSheetDragHandle
+import com.kuit.ourmenu.ui.common.topappbar.BackButtonTopAppBar
+import com.kuit.ourmenu.ui.menuFolder.component.AddButton
+import com.kuit.ourmenu.ui.menuFolder.component.FilterBottomSheet
+import com.kuit.ourmenu.ui.menuFolder.component.MenuFolderMenuButton
+import com.kuit.ourmenu.ui.menuFolder.component.SortDropdown
+import com.kuit.ourmenu.ui.menuFolder.viewmodel.MenuFolderAllViewModel
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import com.kuit.ourmenu.utils.TagListProvider
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MenuFolderAllMenuScreen(
+ onNavigateBack: () -> Unit,
+ onNavigateToMenuInfo: (Long) -> Unit,
+// onNavigateToMap: () -> Unit, // TODO: Map์ผ๋ก ํ๋ฉด ์ด๋ ๊ตฌํ
+ onNavigateToAddMenu: () -> Unit,
+ viewModel: MenuFolderAllViewModel = hiltViewModel()
+) {
+ val menus by viewModel.menuFolderAll.collectAsStateWithLifecycle()
+ val selectedSort by viewModel.sortOrder.collectAsStateWithLifecycle()
+ val selectedTags by viewModel.selectedTags.collectAsStateWithLifecycle()
+ val minPrice by viewModel.minPrice.collectAsStateWithLifecycle()
+ val maxPrice by viewModel.maxPrice.collectAsStateWithLifecycle()
+ val menuCount = menus.size
+
+ var filterCount by rememberSaveable { mutableIntStateOf(0) } // ์ ํ๋ ํํฐ ๊ฐ์ ์ํ ๊ด๋ฆฌ
+
+ val scaffoldState = rememberBottomSheetScaffoldState()
+ val coroutineScope = rememberCoroutineScope()
+ LaunchedEffect(scaffoldState.bottomSheetState) {
+ snapshotFlow { scaffoldState.bottomSheetState.currentValue }
+ .collect { state ->
+ Log.d("AddMenuTagScreen", "BottomSheetState changed: $state")
+ }
+ }
+
+ BottomSheetScaffold(
+ scaffoldState = scaffoldState,
+ topBar = {
+ BackButtonTopAppBar(Neutral500, false) {
+ onNavigateBack()
+ }
+ },
+ sheetContainerColor = NeutralWhite,
+ sheetPeekHeight = 0.dp,
+ sheetContent = {
+ FilterBottomSheet(
+ categoryTagList = TagListProvider.categoryTagList,
+ nationalityTagList = TagListProvider.nationalityTagList,
+ tasteTagList = TagListProvider.tasteTagList,
+ occasionTagList = TagListProvider.occasionTagList,
+ onSelectedTagsChange = { newSelectedTags ->
+ viewModel.updateTags(newSelectedTags)
+ },
+ onPriceRangeChange = { min, max ->
+ viewModel.updatePriceRange(min, max)
+ },
+ onApplyButtonClick = {
+ coroutineScope.launch {
+ val tagFilterCount = selectedTags.size
+
+ val isPriceChanged =
+ minPrice != null && minPrice != 0L || maxPrice != null && maxPrice != 50000L
+ val priceFilterCount = if (isPriceChanged) 1 else 0
+
+ filterCount = tagFilterCount + priceFilterCount
+
+ scaffoldState.bottomSheetState.partialExpand()
+ }
+ }
+ )
+ },
+ sheetDragHandle = {
+ BottomSheetDragHandle()
+ }
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .padding(innerPadding)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = stringResource(R.string.all_menu),
+ style = ourMenuTypography().pretendard_600_20,
+ color = Neutral900
+ )
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Text(
+ text = String.format(stringResource(R.string.count), menuCount),
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral700
+ )
+ }
+
+ SortDropdown(
+ options = SortOrderType.entries.map { it.displayName },
+ selectedOption = selectedSort.displayName,
+ ) { selectedDisplayName ->
+ SortOrderType.entries
+ .firstOrNull { it.displayName == selectedDisplayName }
+ ?.let { viewModel.updateSortOrder(it) }
+ }
+ }
+
+ // ํํฐ ๋ฒํผ (ํํฐ ๊ฐ์ ๋ฐ์)
+ Card(
+ shape = RoundedCornerShape(12.dp),
+ modifier = Modifier
+ .padding(top = 24.dp, bottom = 16.dp, start = 20.dp)
+ .clickable {
+ coroutineScope.launch {
+ scaffoldState.bottomSheetState.expand() // ๋ฒํผ ํด๋ฆญ ์ BottomSheet ์ด๊ธฐ
+ }
+ },
+ colors = CardDefaults.cardColors(containerColor = Primary500Main),
+ elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
+ ) {
+ Row(
+ modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ painter = painterResource(R.drawable.ic_filter),
+ contentDescription = "filter button",
+ )
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ Text(
+ text = "$filterCount", // ์ ํ๋ ํํฐ ๊ฐ์ ๋ฐ์
+ color = NeutralWhite,
+ style = ourMenuTypography().pretendard_700_16
+ )
+ }
+ }
+
+ LazyColumn(
+ modifier = Modifier,
+ ) {
+ items(menuCount) { index ->
+ MenuFolderMenuButton(
+ menuFolderDetail = menus[index],
+ onMenuClick = {
+ onNavigateToMenuInfo(menus[index].menuId)
+ },
+ onMapClick = {
+// onNavigateToMap()
+ }
+ )
+ }
+
+ item {
+ AddButton(
+ stringResource(R.string.add_menu),
+ modifier = Modifier.padding(start = 20.dp, end = 20.dp, top = 20.dp)
+ ) {
+ onNavigateToAddMenu()
+ }
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MenuFolderAllMenuScreenPreview() {
+// val navController = rememberNavController()
+//
+// MenuFolderAllMenuScreen(navController)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderDetailScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderDetailScreen.kt
new file mode 100644
index 00000000..1915b83c
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderDetailScreen.kt
@@ -0,0 +1,187 @@
+package com.kuit.ourmenu.ui.menuFolder.screen
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import coil3.compose.AsyncImage
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.data.model.base.type.SortOrderType
+import com.kuit.ourmenu.ui.common.topappbar.BackButtonTopAppBar
+import com.kuit.ourmenu.ui.menuFolder.component.AddButton
+import com.kuit.ourmenu.ui.menuFolder.component.MenuFolderMenuButton
+import com.kuit.ourmenu.ui.menuFolder.component.SortDropdown
+import com.kuit.ourmenu.ui.menuFolder.viewmodel.MenuFolderDetailViewModel
+import com.kuit.ourmenu.ui.theme.Neutral50
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun MenuFolderDetailScreen(
+ menuFolderId: Long,
+ onNavigateToMenuInfo: (Long) -> Unit,
+// onNavigateToMap: () -> Unit, // TODO: Map์ผ๋ก ํ๋ฉด ์ด๋ ๊ตฌํ
+ onNavigateToAddMenu: () -> Unit,
+ onNavigateBack: () -> Unit,
+ viewModel: MenuFolderDetailViewModel = hiltViewModel()
+) {
+ val menuFolderDetail by viewModel.menuFolderDetail.collectAsStateWithLifecycle()
+ val menus = menuFolderDetail.menus
+
+ LaunchedEffect(menuFolderId) {
+ viewModel.getMenuFolderDetail(menuFolderId)
+ }
+
+ val options = SortOrderType.entries
+ var selectedOption by rememberSaveable { mutableStateOf(SortOrderType.TITLE_ASC) }
+
+ Scaffold(
+ topBar = {},
+ content = { innerPadding ->
+ Column(
+ modifier = Modifier
+ .padding(innerPadding)
+ .fillMaxSize()
+ ) {
+ Box(
+ modifier = Modifier
+ .height(192.dp),
+ ) {
+ AsyncImage(
+ model = menuFolderDetail.menuFolderImgUrl,
+ contentDescription = "Menu Folder Image",
+ contentScale = ContentScale.FillWidth,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Box(
+ modifier = Modifier
+ .matchParentSize()
+ .background(
+ brush = Brush.verticalGradient(
+ colors = listOf(
+ Color.Black.copy(alpha = 0f), // ํฌ๋ช
(rgba(0, 0, 0, 0.00))
+ Color.Black.copy(alpha = 0.5f) // ๋ถํฌ๋ช
(rgba(0, 0, 0, 0.70))
+ ),
+ startY = 0f,
+ endY = 400f // ์ ์ ์ด๋์์ง๋ ์์น ์ค์
+ )
+ )
+ )
+
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .padding(top = 144.dp, start = 20.dp, end = 20.dp)
+ .fillMaxWidth()
+ ) {
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ AsyncImage(
+ model = menuFolderDetail.menuFolderIconImgUrl,
+ contentDescription = "Folder Icon",
+ modifier = Modifier.size(32.dp),
+ contentScale = ContentScale.Fit,
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = menuFolderDetail.menuFolderTitle,
+ color = NeutralWhite,
+ style = ourMenuTypography().pretendard_600_20,
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ text = String.format(stringResource(R.string.count), menus.size),
+ color = Neutral50,
+ style = ourMenuTypography().pretendard_500_14,
+ )
+ }
+
+ SortDropdown(
+ options = options.map { it.displayName },
+ selectedOption = selectedOption.displayName,
+ color = NeutralWhite
+ ) { selectedDisplayName ->
+ val newSortOption =
+ options.first { it.displayName == selectedDisplayName }
+ selectedOption = newSortOption
+ viewModel.updateSortOrder(newSortOption, menuFolderId)
+ }
+ }
+ }
+
+ LazyColumn(
+ modifier = Modifier.padding(top = 16.dp),
+ ) {
+ items(menus.size) { index ->
+ MenuFolderMenuButton(
+ menuFolderDetail = menus[index],
+ onMenuClick = {
+ onNavigateToMenuInfo(menus[index].menuId)
+ },
+ onMapClick = {
+// onNavigateToMap()
+ }
+ )
+ }
+
+ item {
+ AddButton(
+ stringResource(R.string.add_menu),
+ modifier = Modifier.padding(start = 20.dp, end = 20.dp, top = 20.dp)
+ ) {
+ onNavigateToAddMenu()
+ }
+ }
+ }
+ }
+
+ BackButtonTopAppBar(NeutralWhite, true) {
+ onNavigateBack()
+ }
+ }
+ )
+
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MenuFolderDetailScreenPreview() {
+ MenuFolderDetailScreen(
+ menuFolderId = 0,
+ onNavigateToMenuInfo = {},
+// onNavigateToMap = {},
+ onNavigateToAddMenu = {},
+ onNavigateBack = {},
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderScreen.kt
new file mode 100644
index 00000000..146c762c
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderScreen.kt
@@ -0,0 +1,210 @@
+package com.kuit.ourmenu.ui.menuFolder.screen
+
+import android.annotation.SuppressLint
+import android.util.Log
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+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.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+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.input.pointer.pointerInput
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.menuFolder.component.AddButton
+import com.kuit.ourmenu.ui.menuFolder.component.DeleteMenuFolderModal
+import com.kuit.ourmenu.ui.menuFolder.component.MenuFolderButton
+import com.kuit.ourmenu.ui.menuFolder.component.MenuFolderTopAppBar
+import com.kuit.ourmenu.ui.menuFolder.viewmodel.MenuFolderViewModel
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import com.kuit.ourmenu.utils.dragndrop.dragModifier
+import com.kuit.ourmenu.utils.dragndrop.rememberDragAndDropListState
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+// https://dev.to/mardsoul/how-to-create-lazycolumn-with-drag-and-drop-elements-in-jetpack-compose-part-1-4bn5
+@SuppressLint("UnnecessaryComposedModifier")
+@Composable
+fun MenuFolderScreen(
+ padding: PaddingValues,
+ onNavigateToDetail: (Long) -> Unit,
+ onNavigateToAllMenu: () -> Unit,
+ onNavigateToAddMenu: () -> Unit,
+ viewModel: MenuFolderViewModel = hiltViewModel()
+) {
+ // ํ์ฌ ์ค์์ดํ๋ ๋ฒํผ์ ์ธ๋ฑ์ค๋ฅผ ๊ด๋ฆฌ (ํ ๋ฒ์ ํ๋๋ง ์ค์์ดํ๋๋๋ก)
+ var swipedIndex by remember { mutableIntStateOf(-1) }
+
+ val menuFolders by viewModel.menuFolders.collectAsStateWithLifecycle()
+ val totalMenuCount by viewModel.menuCount.collectAsStateWithLifecycle()
+ var showDeleteModel by remember { mutableStateOf(false) }
+ var deleteIndex by remember { mutableIntStateOf(-1) }
+ var dragStartFolderId by remember { mutableIntStateOf(-1) }
+
+ val lazyListState = rememberLazyListState()
+ val dragAndDropListState =
+ rememberDragAndDropListState(lazyListState) { from, to ->
+ viewModel.updateMenuFolderList(from, to)
+ }
+
+ val coroutineScope = rememberCoroutineScope()
+ var overscrollJob by remember { mutableStateOf(null) }
+
+ Scaffold(
+ topBar = {
+ MenuFolderTopAppBar(
+ onClick = {
+ onNavigateToAddMenu()
+ }
+ )
+ }
+ ) { innerPadding ->
+ if (showDeleteModel) {
+ DeleteMenuFolderModal(
+ onDismiss = {
+ deleteIndex = -1
+ showDeleteModel = false
+ },
+ onConfirm = {
+ viewModel.deleteMenuFolder(deleteIndex)
+ deleteIndex = -1
+ swipedIndex = -1
+ }
+ )
+ }
+
+ LazyColumn(
+ modifier = Modifier
+ .padding(innerPadding)
+ .padding(horizontal = 20.dp)
+ .pointerInput(Unit) {
+ detectDragGesturesAfterLongPress(
+ onDrag = { change, offset ->
+ change.consume()
+ dragAndDropListState.onDrag(offset)
+
+ if (overscrollJob?.isActive == true) return@detectDragGesturesAfterLongPress
+ dragAndDropListState
+ .checkOverscroll()
+ .takeIf { it != 0f }
+ ?.let {
+ overscrollJob = coroutineScope.launch {
+ dragAndDropListState.lazyListState.scrollBy(it)
+ }
+ } ?: kotlin.run { overscrollJob?.cancel() }
+
+ },
+ onDragStart = { offset ->
+ swipedIndex = -1
+ dragAndDropListState.onDragStart(offset)
+ dragStartFolderId = dragAndDropListState.initialIndex?.let { index ->
+ (menuFolders.getOrNull(index)?.menuFolderId ?: -1).toInt()
+ } ?: -1
+ },
+ onDragEnd = {
+ viewModel.patchMenuFolders(
+ dragStartFolderId,
+ dragAndDropListState.endIndex ?: 0
+ )
+ dragAndDropListState.onDragInterrupted()
+ },
+ onDragCancel = { dragAndDropListState.onDragInterrupted() }
+ )
+ },
+ state = dragAndDropListState.lazyListState,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ item {
+ Column(
+ modifier = Modifier
+ .padding(top = 24.dp)
+ .height(64.dp)
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(12.dp))
+ .background(Primary500Main)
+ .clickable(onClick = {
+ onNavigateToAllMenu()
+ }),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = stringResource(R.string.see_all_menu),
+ color = NeutralWhite,
+ style = ourMenuTypography().pretendard_700_18
+ )
+
+ Text(
+ text = String.format(stringResource(R.string.menu_count), totalMenuCount),
+ color = NeutralWhite,
+ style = ourMenuTypography().pretendard_500_14,
+ )
+ }
+ }
+
+ // TODO: ๋๋๊ทธ ์ค ๋๋กญ ๊ตฌํ
+ itemsIndexed(menuFolders) { index, folder ->
+ MenuFolderButton(
+ modifier = Modifier.dragModifier(index, dragAndDropListState),
+ menuFolder = folder,
+ isSwiped = swipedIndex == index,
+ onSwipe = { swipedIndex = index },
+ onReset = { if (swipedIndex == index) swipedIndex = -1 },
+ onButtonClick = {
+ onNavigateToDetail(folder.menuFolderId)
+ },
+ onDeleteClick = {
+ showDeleteModel = true
+ deleteIndex = folder.menuFolderId.toInt()
+ }
+ )
+ }
+
+ item {
+ AddButton(
+ stringResource(R.string.add_menu_folder),
+ modifier = Modifier
+ ) {
+ // TODO: ๋ฒํผ ๋๋ฅด๋ฉด ๋ฉ๋ดํ ์ถ๊ฐ ํ์ด์ง๋ก ์ด๋
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MenuFolderScreenPreview() {
+ MenuFolderScreen(
+ padding = PaddingValues(0.dp),
+ onNavigateToDetail = {},
+ onNavigateToAllMenu = {},
+ onNavigateToAddMenu = {},
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderAllViewModel.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderAllViewModel.kt
new file mode 100644
index 00000000..754640d1
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderAllViewModel.kt
@@ -0,0 +1,102 @@
+package com.kuit.ourmenu.ui.menuFolder.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.kuit.ourmenu.data.model.menuFolder.response.MenuFolderAllResponse
+import com.kuit.ourmenu.data.model.base.type.SortOrderType
+import com.kuit.ourmenu.data.model.base.type.TagType
+import com.kuit.ourmenu.data.repository.MenuFolderRepository
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class MenuFolderAllViewModel @Inject constructor(
+ private val menuFolderRepository: MenuFolderRepository,
+) : ViewModel() {
+ private val _menuFolderAll = MutableStateFlow>(emptyList())
+ val menuFolderAll = _menuFolderAll.asStateFlow()
+
+ private val _sortOrder = MutableStateFlow(SortOrderType.TITLE_ASC)
+ val sortOrder = _sortOrder.asStateFlow()
+
+ private val _selectedTags = MutableStateFlow>(emptyList())
+ val selectedTags = _selectedTags.asStateFlow()
+
+ private val _minPrice = MutableStateFlow(null)
+ val minPrice = _minPrice.asStateFlow()
+
+ private val _maxPrice = MutableStateFlow(null)
+ val maxPrice = _maxPrice.asStateFlow()
+
+ private val _page = MutableStateFlow(null)
+ val page = _page.asStateFlow()
+
+ private val _size = MutableStateFlow(10)
+ val size = _size.asStateFlow()
+
+ private val _error: MutableStateFlow = MutableStateFlow(null)
+ val error = _error.asStateFlow()
+
+ private val _isLoading = MutableStateFlow(false)
+ val isLoading = _isLoading.asStateFlow()
+
+ init {
+ fetchMenuFolderAll()
+ }
+
+ /** ์ ๋ ฌ ํํฐ ๋ณ๊ฒฝ */
+ fun updateSortOrder(newSortOrder: SortOrderType) {
+ if (_sortOrder.value != newSortOrder) {
+ _sortOrder.value = newSortOrder
+ fetchMenuFolderAll()
+ }
+ }
+
+ /** ํ๊ทธ ํํฐ ๋ณ๊ฒฝ */
+ fun updateTags(tags: List) {
+ _selectedTags.value = TagType.toApiValues(tags)
+ fetchMenuFolderAll()
+ }
+
+ /** ๊ฐ๊ฒฉ ํํฐ ๋ณ๊ฒฝ */
+ fun updatePriceRange(min: Long?, max: Long?) {
+ _minPrice.value = min
+ _maxPrice.value = max
+ fetchMenuFolderAll()
+ }
+
+ /** ํ์ด์ง ๋ณ๊ฒฝ */
+ fun updatePagination(newPage: Int?, newSize: Int) {
+ _page.value = newPage
+ _size.value = newSize
+ fetchMenuFolderAll()
+ }
+
+ private fun fetchMenuFolderAll() {
+ viewModelScope.launch {
+ _isLoading.value = true
+ _error.value = null
+
+ menuFolderRepository.getMenuFolderAll(
+ tags = _selectedTags.value,
+ minPrice = _minPrice.value,
+ maxPrice = _maxPrice.value,
+ page = _page.value,
+ size = _size.value,
+ sortOrder = _sortOrder.value.apiValue
+ ).fold(
+ onSuccess = { response ->
+ _menuFolderAll.value = response ?: emptyList()
+ },
+ onFailure = { throwable ->
+ _error.value = throwable.message ?: "๋ฉ๋ด ํด๋๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."
+ }
+ )
+
+ _isLoading.value = false
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderDetailViewModel.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderDetailViewModel.kt
new file mode 100644
index 00000000..ec799b12
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderDetailViewModel.kt
@@ -0,0 +1,66 @@
+package com.kuit.ourmenu.ui.menuFolder.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.kuit.ourmenu.data.model.base.type.SortOrderType
+import com.kuit.ourmenu.data.model.menuFolder.response.MenuFolderDetailResponse
+import com.kuit.ourmenu.data.repository.MenuFolderRepository
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class MenuFolderDetailViewModel @Inject constructor(
+ private val menuFolderRepository: MenuFolderRepository,
+): ViewModel() {
+ private val _menuFolderDetail = MutableStateFlow(MenuFolderDetailResponse())
+ val menuFolderDetail = _menuFolderDetail.asStateFlow()
+
+ private val _menuFolderId = MutableStateFlow(0)
+ val menuFolderId = _menuFolderId.asStateFlow()
+
+ private val _sortOrder = MutableStateFlow(SortOrderType.TITLE_ASC)
+ val sortOrder = _sortOrder.asStateFlow()
+
+ private val _error: MutableStateFlow = MutableStateFlow(null)
+ val error = _error.asStateFlow()
+
+ private val _isLoading = MutableStateFlow(false)
+ val isLoading = _isLoading.asStateFlow()
+
+ fun getMenuFolderDetail(
+ menuFolderId: Long,
+ sortOrder: SortOrderType = _sortOrder.value
+ ) {
+ _menuFolderId.value = menuFolderId
+ viewModelScope.launch {
+ _isLoading.value = true
+ _error.value = null
+
+ menuFolderRepository.getMenuFolderDetail(menuFolderId, sortOrder.apiValue)
+ .fold(
+ onSuccess = { response ->
+ if (response != null) {
+ _menuFolderDetail.value = response
+ _menuFolderId.value = menuFolderId
+ _sortOrder.value = sortOrder
+ }
+ },
+ onFailure = { throwable ->
+ _error.value = throwable.message ?: "๋ฉ๋ด ํด๋๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."
+ }
+ )
+
+ _isLoading.value = false
+ }
+ }
+
+ fun updateSortOrder(sortOrderType: SortOrderType, menuFolderId: Long) {
+ if (_sortOrder.value != sortOrderType) {
+ _sortOrder.value = sortOrderType
+ getMenuFolderDetail(menuFolderId, sortOrderType)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderViewModel.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderViewModel.kt
new file mode 100644
index 00000000..7c89fcc8
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderViewModel.kt
@@ -0,0 +1,109 @@
+package com.kuit.ourmenu.ui.menuFolder.viewmodel
+
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.kuit.ourmenu.data.model.menuFolder.request.MenuFolderIndexRequest
+import com.kuit.ourmenu.data.model.menuFolder.response.MenuFolderList
+import com.kuit.ourmenu.data.repository.MenuFolderRepository
+import com.kuit.ourmenu.utils.dragndrop.move
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class MenuFolderViewModel @Inject constructor(
+ private val menuFolderRepository: MenuFolderRepository
+) : ViewModel() {
+
+ private val _menuFolders = MutableStateFlow>(emptyList())
+ val menuFolders = _menuFolders.asStateFlow()
+
+ private val _menuCount = MutableStateFlow(0)
+ val menuCount = _menuCount.asStateFlow()
+
+ private val _error: MutableStateFlow = MutableStateFlow(null)
+ val error = _error.asStateFlow()
+
+ private val _isLoading = MutableStateFlow(false)
+ val isLoading = _isLoading.asStateFlow()
+
+ init {
+ getMenuFolders()
+ }
+
+ fun getMenuFolders() {
+ viewModelScope.launch {
+ _isLoading.value = true
+ _error.value = null
+
+ menuFolderRepository.getMenuFolders()
+ .fold(
+ onSuccess = { response ->
+ if (response != null) {
+ _menuFolders.value = response.menuFolders.sortedBy { it.index }
+ _menuCount.value = response.menuCount
+ }
+ },
+ onFailure = { throwable ->
+ _error.value = throwable.message ?: "๋ฉ๋ด ํด๋๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."
+ }
+ )
+
+ _isLoading.value = false
+ }
+ }
+
+ fun deleteMenuFolder(menuFolderId: Int) {
+ viewModelScope.launch {
+ _isLoading.value = true
+ _error.value = null
+
+ menuFolderRepository.deleteMenuFolder(menuFolderId.toLong())
+ .fold(
+ onSuccess = {
+ getMenuFolders() // Refresh the list after deletion
+ },
+ onFailure = { throwable ->
+ _error.value = throwable.message ?: "๋ฉ๋ด ํด๋ ์ญ์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."
+ }
+ )
+
+ _isLoading.value = false
+ }
+ }
+
+ fun updateMenuFolderList(from: Int, to: Int) {
+ val newMenuFolders = _menuFolders.value.toMutableList()
+ newMenuFolders.move(from, to)
+ _menuFolders.update { newMenuFolders }
+ }
+
+ fun patchMenuFolders(fromId: Int, to: Int) {
+
+ val toIndex = to.coerceAtMost(_menuFolders.value.size - 1)
+
+ viewModelScope.launch {
+ _isLoading.value = true
+ _error.value = null
+
+ menuFolderRepository.updateMenuFolderIndex(
+ fromId.toLong(),
+ MenuFolderIndexRequest(toIndex)
+ )
+ .fold(
+ onSuccess = {
+ getMenuFolders() // Refresh the list after patching
+ },
+ onFailure = { throwable ->
+ _error.value = throwable.message ?: "๋ฉ๋ด ํด๋ ์์ ๋ณ๊ฒฝ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."
+ }
+ )
+
+ _isLoading.value = false
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/component/info/MenuInfoAdditionalContent.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/component/info/MenuInfoAdditionalContent.kt
new file mode 100644
index 00000000..d7dd5a00
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/component/info/MenuInfoAdditionalContent.kt
@@ -0,0 +1,125 @@
+package com.kuit.ourmenu.ui.menuinfo.component.info
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.Neutral100
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun MenuInfoAdditionalContent(
+ address: String,
+ memoTitle: String,
+ memoContent: String
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp)
+ .padding(top = 12.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.info),
+ style = ourMenuTypography().pretendard_600_32.copy(
+ fontSize = 12.sp,
+ lineHeight = 12.sp,
+ color = Neutral500
+ ),
+ modifier = Modifier.padding(bottom = 6.dp)
+ )
+ Row {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_fill_map_20),
+ contentDescription = null,
+ tint = Color.Unspecified,
+ modifier = Modifier.align(Alignment.CenterVertically)
+ )
+ Text(
+ text = address,
+ style = ourMenuTypography().pretendard_400_14.copy(
+ lineHeight = 22.sp,
+ color = Neutral700
+ ),
+ modifier = Modifier
+ .align(Alignment.CenterVertically)
+ .padding(start = 8.dp)
+ )
+ }
+
+ Text(
+ text = stringResource(R.string.memo_eng),
+ style = ourMenuTypography().pretendard_600_32.copy(
+ fontSize = 12.sp,
+ lineHeight = 12.sp,
+ color = Neutral500
+ ),
+ modifier = Modifier.padding(
+ top = 13.dp, bottom = 6.dp
+ )
+ )
+
+ Column(
+ modifier = Modifier
+ .width(320.dp)
+ .background(color = Neutral100, shape = RoundedCornerShape(size = 12.dp))
+ .padding(start = 8.dp, top = 12.dp, end = 8.dp, bottom = 12.dp)
+ ) {
+ Row {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_menu_info_meno_20),
+ contentDescription = null,
+ tint = Color.Unspecified,
+ modifier = Modifier.align(Alignment.Top)
+ )
+ Column(
+ modifier = Modifier.padding(start = 10.dp)
+ ) {
+ Text(
+ text = memoTitle,
+ style = ourMenuTypography().pretendard_600_14.copy(
+ lineHeight = 18.sp,
+ color = Neutral700
+ ),
+ modifier = Modifier.padding(bottom = 4.dp)
+ )
+ Text(
+ text = memoContent,
+ style = ourMenuTypography().pretendard_600_12.copy(
+ lineHeight = 18.sp,
+ color = Neutral700
+ ),
+ )
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MenuInfoAdditionalContentPreview() {
+ MenuInfoAdditionalContent(
+ address = "์์ธ ๋งํฌ๊ตฌ ์์ฐ์ฐ๋ก 112",
+ memoTitle = "๋งค์ด ๋ง ์ ํ ๊ฐ๋ฅ!",
+ memoContent = "* ๋์ ์ฐ / ใ
ใ
์ฐ / ํ๋ผ์ฐ \n" +
+ "๊ธฐ๋ณธ๋ ์ ๋ง ๋งค์ฐ๋ ์กฐ์ฌํ๊ธฐ"
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/component/info/MenuInfoChipContent.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/component/info/MenuInfoChipContent.kt
new file mode 100644
index 00000000..6e0ab115
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/component/info/MenuInfoChipContent.kt
@@ -0,0 +1,78 @@
+package com.kuit.ourmenu.ui.menuinfo.component.info
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import coil3.compose.AsyncImage
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.data.model.menuinfo.response.MenuInfoResponse
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun MenuInfoChipContent(
+ onNavigateToMenuFolderDetail: (Int) -> Unit = {},
+ menuInfoData: MenuInfoResponse
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.menu_folder),
+ style = ourMenuTypography().pretendard_600_32.copy(
+ fontSize = 12.sp,
+ lineHeight = 12.sp,
+ color = Neutral500
+ ),
+ )
+
+ MenuInfoFolderChipGrid(
+ onNavigateToMenuFolderDetail = onNavigateToMenuFolderDetail,
+ menuFolderList = menuInfoData.menuFolders
+ )
+
+ Text(
+ text = stringResource(R.string.tag_eng),
+ style = ourMenuTypography().pretendard_600_32.copy(
+ fontSize = 12.sp,
+ lineHeight = 12.sp,
+ color = Neutral500
+ ),
+ modifier = Modifier.padding(top = 12.dp)
+ )
+
+ LazyVerticalGrid(
+ columns = GridCells.Adaptive(minSize = 70.dp),
+ modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ items(menuInfoData.tagImgUrls.size) { index ->
+ AsyncImage(
+ model = menuInfoData.tagImgUrls[index],
+ contentDescription = null
+ )
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MenuInfoChipContentPreview() {
+// MenuInfoChipContent(
+// menuInfoData = MenuInfoDummyData.dummyData
+// )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/component/info/MenuInfoContent.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/component/info/MenuInfoContent.kt
new file mode 100644
index 00000000..f2bfcc1f
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/component/info/MenuInfoContent.kt
@@ -0,0 +1,72 @@
+package com.kuit.ourmenu.ui.menuinfo.component.info
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.kuit.ourmenu.data.model.menuinfo.response.MenuInfoResponse
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import com.kuit.ourmenu.utils.ExtensionUtil.toWon
+
+@Composable
+fun MenuInfoContent(
+ menuInfoData: MenuInfoResponse
+) {
+ Column {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp)
+ ) {
+ Text(
+ text = menuInfoData.menuTitle,
+ style = ourMenuTypography().pretendard_700_48.copy(
+ fontSize = 20.sp,
+ lineHeight = 24.sp,
+ color = Neutral900
+ ),
+ modifier = Modifier
+ .padding(start = 20.dp, top = 1.dp)
+ )
+ Text(
+ text = menuInfoData.menuPrice.toWon(),
+ style = ourMenuTypography().pretendard_600_32.copy(
+ fontSize = 16.sp,
+ lineHeight = 22.sp,
+ color = Neutral700
+ ),
+ modifier = Modifier
+ .padding(end = 20.dp, top = 1.dp)
+ .align(Alignment.TopEnd)
+ )
+ }
+ Text(
+ text = menuInfoData.storeTitle,
+ style = ourMenuTypography().pretendard_600_32.copy(
+ fontSize = 14.sp,
+ lineHeight = 18.sp,
+ color = Neutral500
+ ),
+ modifier = Modifier
+ .padding(start = 20.dp, top = 7.dp)
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MenuInfoContentPreview() {
+// MenuInfoContent(
+// MenuInfoDummyData.dummyData
+// )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/component/info/MenuInfoFolderChipGrid.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/component/info/MenuInfoFolderChipGrid.kt
new file mode 100644
index 00000000..7ef654a3
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/component/info/MenuInfoFolderChipGrid.kt
@@ -0,0 +1,62 @@
+package com.kuit.ourmenu.ui.menuinfo.component.info
+
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.data.model.menuinfo.response.MenuFolder
+import com.kuit.ourmenu.ui.common.chip.MenuFolderChip
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+fun MenuInfoFolderChipGrid(
+ modifier: Modifier = Modifier,
+ onNavigateToMenuFolderDetail: (Int) -> Unit = {},
+ menuFolderList: List = listOf(),
+) {
+
+ FlowRow(
+ modifier = Modifier
+ .padding(top = 2.dp)
+ ) {
+ menuFolderList.forEach { menuFolder ->
+ MenuFolderChip(
+ modifier = Modifier.padding(
+ top = 4.dp,
+ ),
+ menuFolderIconImgUrl = menuFolder.menuFolderIconImgUrl,
+ menuFolderTitle = menuFolder.menuFolderTitle
+ ) {
+ onNavigateToMenuFolderDetail(menuFolder.menuFolderId)
+ }
+ if (menuFolder != menuFolderList.last()) {
+ Spacer(modifier = Modifier.padding(end = 4.dp))
+ }
+ }
+ }
+}
+
+@Preview(
+ showBackground = true,
+ backgroundColor = 0xFFFFFFFF
+)
+@Composable
+private fun MenuInfoFolderChipGridPreview() {
+// MenuInfoFolderChipGrid(
+// menuFolderList = listOf(
+// "๋ฉ๋ดํ1",
+// "๋ฉ๋ดํ22",
+// "๋ฉ๋ดํ333",
+// "๋ฉ๋ดํ4444",
+// "๋ฉ๋ดํ55555",
+// "๋ฉ๋ดํ666666",
+// "๋ฉ๋ดํ7777777",
+// "๋ฉ๋ดํ88888888",
+// "๋ฉ๋ดํ999999999"
+// )
+// )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/component/info/MenuInfoImagePager.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/component/info/MenuInfoImagePager.kt
new file mode 100644
index 00000000..33a5493b
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/component/info/MenuInfoImagePager.kt
@@ -0,0 +1,98 @@
+package com.kuit.ourmenu.ui.menuinfo.component.info
+
+import android.util.Log
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Box
+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.pager.HorizontalPager
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import coil3.compose.AsyncImage
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.tbuonomo.viewpagerdotsindicator.compose.DotsIndicator
+import com.tbuonomo.viewpagerdotsindicator.compose.model.DotGraphic
+import com.tbuonomo.viewpagerdotsindicator.compose.type.SpringIndicatorType
+
+@Composable
+fun MenuInfoImagePager(
+ pagerState: PagerState,
+ imgUrls: List,
+) {
+ val pagerCount = imgUrls.size
+
+ Log.d("MenuInfoImagePager", "Image URLs: $imgUrls")
+
+ if (pagerCount == 0) {
+ // โ
์ด๋ฏธ์ง๊ฐ ์์ ๊ฒฝ์ฐ ๋์ฒด UI ๋๋ ๋น Box ์ฒ๋ฆฌ
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(292.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ // ์: Placeholder ์ด๋ฏธ์ง ๋๋ ํ
์คํธ
+ // AsyncImage(model = R.drawable.placeholder, contentDescription = null)
+ }
+ return
+ }
+
+
+ Box(
+ contentAlignment = Alignment.BottomCenter
+ ) {
+ HorizontalPager(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(292.dp),
+ state = pagerState
+ ) { index ->
+ AsyncImage(
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxSize(),
+ contentScale = ContentScale.Crop,
+ model = imgUrls[index],
+ )
+ }
+ DotsIndicator(
+ modifier = Modifier
+ .padding(bottom = 12.dp),
+ dotCount = pagerCount,
+ type = SpringIndicatorType(
+ dotsGraphic = DotGraphic(
+ 4.dp,
+ color = Neutral500
+ ),
+ selectorDotGraphic = DotGraphic(
+ 4.dp,
+ color = NeutralWhite
+ )
+ ),
+ pagerState = pagerState,
+ dotSpacing = 6.dp
+ )
+ }
+}
+
+
+@OptIn(ExperimentalFoundationApi::class)
+@Preview
+@Composable
+private fun MenuInfoPreview() {
+ val state = rememberPagerState(pageCount = { 3 })
+
+// MenuInfoImagePager(
+// pagerState = state,
+// pageItems = MenuInfoDummyData.dummyData
+// )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/component/info/MenuInfoMapButton.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/component/info/MenuInfoMapButton.kt
new file mode 100644
index 00000000..88c92696
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/component/info/MenuInfoMapButton.kt
@@ -0,0 +1,71 @@
+package com.kuit.ourmenu.ui.menuinfo.component.info
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment.Companion.CenterVertically
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun MenuInfoMapButton(
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit
+) {
+ Button(
+ modifier = modifier
+ .shadow(
+ elevation = 8.dp,
+ spotColor = Color(0x29000000),
+ ambientColor = Color(0x29000000)
+ )
+ .width(124.dp)
+ .height(40.dp),
+ shape = RoundedCornerShape(20.dp),
+ onClick = onClick,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = NeutralWhite,
+ contentColor = Primary500Main
+ ),
+ contentPadding = PaddingValues()
+ ) {
+ Row {
+ Text(
+ text = stringResource(R.string.menu_info_goto_map),
+ style = ourMenuTypography().pretendard_600_16,
+ modifier = Modifier.align(CenterVertically)
+ )
+ Icon(
+ painter = painterResource(R.drawable.ic_menu_info_nav_24),
+ contentDescription = null,
+ tint = Primary500Main,
+ modifier = Modifier
+ .padding(start = 8.dp)
+ .align(CenterVertically)
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MenuInfoMapButtonPreview() {
+ MenuInfoMapButton { }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/dummy/MenuInfoDummyData.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/dummy/MenuInfoDummyData.kt
new file mode 100644
index 00000000..ef3e5e01
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/dummy/MenuInfoDummyData.kt
@@ -0,0 +1,151 @@
+package com.kuit.ourmenu.ui.menuinfo.dummy
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.compose.ui.graphics.Color
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+
+data class MenuInfoDummyData(
+ @DrawableRes val imgRes: List,
+ val menuTitle: String,
+ val menuPrice: Int,
+ val store: String,
+ val menuFolderList: List,
+ val defaultTagList: List = listOf(),
+ val customTagList: List = listOf(),
+ val address: String,
+ val memoTitle: String,
+ val memoContent: String
+) {
+ companion object {
+ private const val MENU_TITLE = "ํ์ฐ๋ผ๋ฉ"
+ private const val STORE = "ํ์ฐ๋ผ๋ฉ ๋ฉ์ผ๋ง์ฏ๋ฆฌ ํ๋์ "
+ private const val ADDRESS = "์์ธ ๋งํฌ๊ตฌ ์์ฐ์ฐ๋ก 112"
+ private const val MEMO_TITLE = "๋งค์ด ๋ง ์ ํ ๊ฐ๋ฅ!"
+ private const val MEMO_CONTENT = "* ๋์ ์ฐ / ใ
ใ
์ฐ / ํ๋ผ์ฐ \n" +
+ "๊ธฐ๋ณธ๋ ์ ๋ง ๋งค์ฐ๋ ์กฐ์ฌํ๊ธฐ"
+ private const val TAG_NAME = "๋ฐฅ"
+
+ val dummyData = MenuInfoDummyData(
+ imgRes = listOf(
+ R.drawable.img_dummy_pizza,
+ R.drawable.img_dummy_pizza,
+ R.drawable.img_dummy_pizza,
+ ),
+ menuTitle = MENU_TITLE,
+ menuPrice = 14000,
+ store = STORE,
+ menuFolderList = listOf(
+ "๋ฉ๋ดํ1",
+ "๋ฉ๋ดํ22",
+ "๋ฉ๋ดํ333",
+ "๋ฉ๋ดํ4444",
+ "๋ฉ๋ดํ55555",
+ "๋ฉ๋ดํ666666",
+ "๋ฉ๋ดํ7777777",
+ "๋ฉ๋ดํ88888888",
+ "๋ฉ๋ดํ999999999"
+ ),
+ defaultTagList = listOf(
+ MenuInfoTag(
+ tagName = TAG_NAME,
+ tagIcon = R.drawable.ic_tag_rice,
+ isCustom = false,
+ containerColor = Neutral300,
+ contentColor = Neutral700
+ ),
+ MenuInfoTag(
+ tagName = TAG_NAME,
+ tagIcon = R.drawable.ic_tag_rice,
+ isCustom = false,
+ containerColor = Neutral300,
+ contentColor = Neutral700
+ ),
+ MenuInfoTag(
+ tagName = TAG_NAME,
+ tagIcon = R.drawable.ic_tag_rice,
+ isCustom = false,
+ containerColor = Neutral300,
+ contentColor = Neutral700
+ ),
+ MenuInfoTag(
+ tagName = TAG_NAME,
+ tagIcon = R.drawable.ic_tag_rice,
+ isCustom = false,
+ containerColor = Neutral300,
+ contentColor = Neutral700
+ ),
+ MenuInfoTag(
+ tagName = TAG_NAME,
+ tagIcon = R.drawable.ic_tag_rice,
+ isCustom = false,
+ containerColor = Neutral300,
+ contentColor = Neutral700
+ ),
+ MenuInfoTag(
+ tagName = TAG_NAME,
+ tagIcon = R.drawable.ic_tag_rice,
+ isCustom = false,
+ containerColor = Neutral300,
+ contentColor = Neutral700
+ ),
+ MenuInfoTag(
+ tagName = TAG_NAME,
+ tagIcon = R.drawable.ic_tag_rice,
+ isCustom = false,
+ containerColor = Neutral300,
+ contentColor = Neutral700
+ ),
+ ),
+ customTagList = listOf(
+ MenuInfoTag(
+ tagName = TAG_NAME,
+ tagIcon = R.drawable.ic_tag_rice,
+ isCustom = false,
+ containerColor = Neutral300,
+ contentColor = Neutral700
+ ),
+ MenuInfoTag(
+ tagName = TAG_NAME,
+ tagIcon = R.drawable.ic_tag_rice,
+ isCustom = false,
+ containerColor = Neutral300,
+ contentColor = Neutral700
+ ),
+ MenuInfoTag(
+ tagName = TAG_NAME,
+ tagIcon = R.drawable.ic_tag_rice,
+ isCustom = false,
+ containerColor = Neutral300,
+ contentColor = Neutral700
+ ),
+ ),
+ address = ADDRESS,
+ memoTitle = MEMO_TITLE,
+ memoContent = MEMO_CONTENT
+
+ )
+ }
+}
+
+data class MenuInfoTag(
+ val tagName: String,
+ @DrawableRes val tagIcon: Int,
+ var containerColor: Color,
+ var contentColor: Color,
+ val isCustom: Boolean,
+) {
+ init {
+ if (isCustom) {
+ contentColor = NeutralWhite
+ containerColor = Primary500Main
+ } else {
+ contentColor = Neutral700
+ containerColor = Neutral300
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/navigation/MenuInfoNavigation.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/navigation/MenuInfoNavigation.kt
new file mode 100644
index 00000000..023204bc
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/navigation/MenuInfoNavigation.kt
@@ -0,0 +1,34 @@
+package com.kuit.ourmenu.ui.menuinfo.navigation
+
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import androidx.navigation.toRoute
+import com.kuit.ourmenu.ui.menuinfo.screen.MenuInfoDefaultScreen
+import com.kuit.ourmenu.ui.navigator.Routes
+
+fun NavController.navigateToMenuInfo(menuId: Long) {
+ navigate(Routes.MenuInfo(menuId))
+}
+
+fun NavGraphBuilder.menuInfoNavGraph(
+ navigateBack: () -> Unit,
+ navigateToMenuFolderDetail: (Long) -> Unit,
+ navigateToMenuInfoMap: () -> Unit
+) {
+ composable {
+ val menuId = it.toRoute().menuId
+ MenuInfoDefaultScreen(
+ menuId = menuId,
+ onNavigateBack = navigateBack,
+ onNavigateToMenuFolderDetail = navigateToMenuFolderDetail,
+// onNavigateToMap = navigateToMenuInfoMap
+ )
+ }
+
+// composable {
+// MenuInfoMapScreen(
+// onNavigateBack = navigateBack
+// )
+// }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/screen/MenuInfoDefaultScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/screen/MenuInfoDefaultScreen.kt
new file mode 100644
index 00000000..630cd742
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/screen/MenuInfoDefaultScreen.kt
@@ -0,0 +1,114 @@
+package com.kuit.ourmenu.ui.menuinfo.screen
+
+import androidx.compose.foundation.layout.Box
+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.foundation.pager.rememberPagerState
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.kuit.ourmenu.ui.common.topappbar.BackButtonTopAppBar
+import com.kuit.ourmenu.ui.menuinfo.component.info.MenuInfoAdditionalContent
+import com.kuit.ourmenu.ui.menuinfo.component.info.MenuInfoChipContent
+import com.kuit.ourmenu.ui.menuinfo.component.info.MenuInfoContent
+import com.kuit.ourmenu.ui.menuinfo.component.info.MenuInfoImagePager
+import com.kuit.ourmenu.ui.menuinfo.component.info.MenuInfoMapButton
+import com.kuit.ourmenu.ui.menuinfo.viewmodel.MenuInfoViewModel
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+
+@Composable
+fun MenuInfoDefaultScreen(
+ menuId: Long,
+ onNavigateBack: () -> Unit,
+ onNavigateToMenuFolderDetail: (Long) -> Unit,
+// onNavigateToMap: () -> Unit,
+ viewModel: MenuInfoViewModel = hiltViewModel()
+) {
+ LaunchedEffect(menuId) {
+ viewModel.getMenuInfo(menuId)
+ }
+
+ val menuInfo by viewModel.menuInfo.collectAsStateWithLifecycle()
+ val pagerState = rememberPagerState(
+ pageCount = { menuInfo.menuImgUrls.size.coerceAtLeast(1) } // ์ต์ 1
+ )
+
+ Scaffold(
+ topBar = {},
+ content = { innerPadding ->
+ Box {
+ Column(
+ modifier = Modifier
+ .padding(innerPadding)
+ .fillMaxSize()
+ ) {
+ Box {
+ MenuInfoImagePager(
+ pagerState = pagerState,
+ imgUrls = menuInfo.menuImgUrls
+ )
+ }
+
+ MenuInfoContent(
+ menuInfoData = menuInfo
+ )
+
+ HorizontalDivider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp),
+ color = Neutral300
+ )
+
+ MenuInfoChipContent(
+ // TODO: ๋ฉ๋ด ํด๋ ์ ๋ณด์ ๋ฐ๋ผ ๋ณ๊ฒฝ ํ์, ์ฌ๋ฌ๊ฐ์ธ ๊ฒฝ์ฐ ๊ฐ ํด๋์ ๋ํ ์ด๋ ๊ตฌํ
+// onNavigateToMenuFolderDetail = onNavigateToMenuFolderDetail(menuInfo.menuFolders.),
+ menuInfoData = menuInfo
+ )
+
+ MenuInfoAdditionalContent(
+ address = menuInfo.storeAddress,
+ memoTitle = menuInfo.menuMemoTitle,
+ memoContent = menuInfo.menuMemoContent
+ )
+ }
+ MenuInfoMapButton(
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(end = 20.dp, bottom = 28.dp),
+ ) { }
+ }
+
+ BackButtonTopAppBar(NeutralWhite, true) {
+ onNavigateBack()
+ }
+ }
+ )
+}
+
+
+@Preview(showBackground = true)
+@Composable
+private fun MenuInfoDefaultPreview() {
+// val navController = rememberNavController()
+//
+// MenuInfoDefaultScreen(navController)
+ val viewModel: MenuInfoViewModel = hiltViewModel()
+ MenuInfoDefaultScreen(
+ menuId = 1,
+ onNavigateBack = {},
+ onNavigateToMenuFolderDetail = {},
+ viewModel = viewModel
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/screen/MenuInfoMapScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/screen/MenuInfoMapScreen.kt
new file mode 100644
index 00000000..c604905c
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/screen/MenuInfoMapScreen.kt
@@ -0,0 +1,110 @@
+package com.kuit.ourmenu.ui.menuinfo.screen
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.BottomSheetScaffold
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.rememberBottomSheetScaffoldState
+import androidx.compose.runtime.Composable
+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.Modifier
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavController
+import androidx.navigation.compose.rememberNavController
+import com.kuit.ourmenu.data.model.map.response.MapDetailResponse
+import com.kuit.ourmenu.data.model.map.response.MenuFolderInfo
+import com.kuit.ourmenu.ui.common.GoToMapButton
+import com.kuit.ourmenu.ui.common.bottomsheet.BottomSheetDragHandle
+import com.kuit.ourmenu.ui.common.bottomsheet.MenuInfoBottomSheetContent
+import com.kuit.ourmenu.ui.common.topappbar.OurMenuAddButtonTopAppBar
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MenuInfoMapScreen(navController: NavController) {
+ val scaffoldState = rememberBottomSheetScaffoldState()
+ var dragHandleHeight by remember { mutableStateOf(0.dp) }
+ var bottomSheetContentHeight by remember { mutableStateOf(0.dp) }
+
+ val density = LocalDensity.current
+
+ BottomSheetScaffold(
+ scaffoldState = scaffoldState,
+ sheetContainerColor = NeutralWhite,
+ sheetSwipeEnabled = false,
+ topBar = { OurMenuAddButtonTopAppBar() },
+ sheetDragHandle = {
+ BottomSheetDragHandle(
+ modifier = Modifier.onGloballyPositioned { coordinates ->
+ dragHandleHeight = density.run { coordinates.size.height.toDp() }
+ }
+ )
+ },
+ sheetShape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp),
+ sheetContent = {
+ MenuInfoBottomSheetContent(
+ modifier = Modifier
+ .onGloballyPositioned { coordinates ->
+ val heightPx = coordinates.size.height
+ bottomSheetContentHeight = density.run {
+ heightPx.toDp() + dragHandleHeight
+ }
+ }
+ .padding(bottom = 20.dp),
+ // TODO: ์ดํ์ ์์ ํ์
+ menuInfoData = MapDetailResponse(
+ menuId = 1,
+ menuTitle = "Test Menu",
+ storeTitle = "๊ฐ๊ฒ ์ด๋ฆ",
+ menuPrice = 10000,
+ menuPinImgUrl = "pin",
+ menuTagImgUrls = listOf("ํ์", "๋ฐฅ"),
+ menuImgUrls = listOf(),
+ menuFolderInfo = MenuFolderInfo(
+ menuFolderTitle = "Test Store",
+ menuFolderIconImgUrl = "icon",
+ menuFolderCount = 1
+ ),
+ mapId = 1,
+ mapX = 127.0,
+ mapY = 37.0
+ )
+ ){
+
+ }
+ },
+ sheetPeekHeight = bottomSheetContentHeight,
+ ) { innerPaddings ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPaddings)
+ ) {
+ // TODO : Map SDK
+ GoToMapButton(
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(bottom = 16.dp, end = 20.dp),
+ onClick = { /* TODO : Go To Map Button Click Event */ },
+ )
+ }
+ }
+
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MenuInfoMapScreenPreview() {
+ val navController = rememberNavController()
+
+ MenuInfoMapScreen(navController)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/viewmodel/MenuInfoViewModel.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/viewmodel/MenuInfoViewModel.kt
new file mode 100644
index 00000000..bb03a149
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/menuinfo/viewmodel/MenuInfoViewModel.kt
@@ -0,0 +1,53 @@
+package com.kuit.ourmenu.ui.menuinfo.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.kuit.ourmenu.data.model.menuinfo.response.MenuInfoResponse
+import com.kuit.ourmenu.data.repository.MenuInfoRepository
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class MenuInfoViewModel @Inject constructor(
+ private val menuInfoRepository: MenuInfoRepository
+) : ViewModel() {
+ private val _menuInfo = MutableStateFlow(MenuInfoResponse())
+ val menuInfo = _menuInfo.asStateFlow()
+
+ private val _menuId = MutableStateFlow(0)
+ val menuId = _menuId.asStateFlow()
+
+ private val _error: MutableStateFlow = MutableStateFlow(null)
+ val error = _error.asStateFlow()
+
+ private val _isLoading = MutableStateFlow(false)
+ val isLoading = _isLoading.asStateFlow()
+
+ fun getMenuInfo(
+ menuId: Long
+ ) {
+ _menuId.value = menuId
+
+ viewModelScope.launch {
+ _isLoading.value = true
+ _error.value = null
+
+ menuInfoRepository.getMenuInfo(menuId)
+ .fold(
+ onSuccess = { response ->
+ if (response != null) {
+ _menuInfo.value = response
+ }
+ },
+ onFailure = { throwable ->
+ _error.value = throwable.message ?: "๋ฉ๋ด ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."
+ }
+ )
+
+ _isLoading.value = false
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/my/component/DeleteAccountModal.kt b/app/src/main/java/com/kuit/ourmenu/ui/my/component/DeleteAccountModal.kt
new file mode 100644
index 00000000..786bc7fe
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/my/component/DeleteAccountModal.kt
@@ -0,0 +1,142 @@
+package com.kuit.ourmenu.ui.my.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.Neutral400
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun DeleteAccountModal(
+ onDismiss: () -> Unit,
+ onConfirm: () -> Unit
+) {
+ Dialog(onDismissRequest = onDismiss) {
+ Column(
+ modifier = Modifier
+ .width(288.dp)
+ .background(NeutralWhite, shape = RoundedCornerShape(16.dp))
+ .padding(20.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ // ๋ซ๊ธฐ ์์ด์ฝ
+ Icon(
+ painter = painterResource(R.drawable.ic_close_24_n400),
+ contentDescription = null,
+ tint = Color.Unspecified,
+ modifier = Modifier
+ .align(Alignment.End)
+ .clickable { onDismiss() }
+ .size(24.dp)
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // ์ ๋ชฉ
+ Text(
+ text = stringResource(R.string.want_to_delete_account),
+ style = ourMenuTypography().pretendard_700_18,
+ color = Neutral900,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // ๊ฒฝ๊ณ ๋ฉ์์ง
+ Text(
+ text = stringResource(R.string.delete_account_warning),
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral500,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // ๋ฒํผ Row
+ Row(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Button(
+ onClick = {
+ onDismiss()
+ },
+ modifier = Modifier
+ .weight(1f)
+ .height(48.dp),
+ shape = RoundedCornerShape(8.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Neutral400,
+ contentColor = NeutralWhite
+ ),
+ ) {
+ Text(
+ text = stringResource(R.string.cancel),
+ style = ourMenuTypography().pretendard_700_18,
+ color = NeutralWhite
+ )
+ }
+
+ Spacer(modifier = Modifier.width(4.dp))
+
+ Button(
+ onClick = {
+ onConfirm()
+ onDismiss()
+ },
+ modifier = Modifier
+ .weight(1f)
+ .height(48.dp),
+ shape = RoundedCornerShape(8.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Primary500Main,
+ contentColor = NeutralWhite
+ ),
+ ) {
+ Text(
+ text = stringResource(R.string.confirm),
+ style = ourMenuTypography().pretendard_700_18,
+ color = NeutralWhite
+ )
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun DeleteAccountModalPreview() {
+ DeleteAccountModal(
+ onDismiss = {},
+ onConfirm = {}
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/my/component/LogoutModal.kt b/app/src/main/java/com/kuit/ourmenu/ui/my/component/LogoutModal.kt
new file mode 100644
index 00000000..b5c36f75
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/my/component/LogoutModal.kt
@@ -0,0 +1,130 @@
+package com.kuit.ourmenu.ui.my.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.Neutral400
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun LogoutModal(
+ onDismiss: () -> Unit,
+ onConfirm: () -> Unit
+) {
+ Dialog(onDismissRequest = onDismiss) {
+ Column(
+ modifier = Modifier
+ .background(NeutralWhite, shape = RoundedCornerShape(16.dp))
+ .padding(20.dp)
+ .width(288.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ // ๋ซ๊ธฐ ์์ด์ฝ
+ Icon(
+ painter = painterResource(R.drawable.ic_close_24_n400),
+ contentDescription = null,
+ tint = Color.Unspecified,
+ modifier = Modifier
+ .align(Alignment.End)
+ .clickable { onDismiss() }
+ .size(24.dp)
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // ์ ๋ชฉ
+ Text(
+ text = stringResource(R.string.want_to_logout),
+ style = ourMenuTypography().pretendard_700_18,
+ color = Neutral900,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // ๋ฒํผ Row
+ Row(
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Button(
+ onClick = {
+ onDismiss()
+ },
+ modifier = Modifier
+ .weight(1f)
+ .height(48.dp),
+ shape = RoundedCornerShape(8.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Neutral400,
+ contentColor = NeutralWhite
+ ),
+ ) {
+ Text(
+ text = stringResource(R.string.cancel),
+ style = ourMenuTypography().pretendard_700_18,
+ color = NeutralWhite
+ )
+ }
+
+ Spacer(modifier = Modifier.width(4.dp))
+
+ Button(
+ onClick = {
+ onConfirm()
+ onDismiss()
+ },
+ modifier = Modifier
+ .weight(1f)
+ .height(48.dp),
+ shape = RoundedCornerShape(8.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Primary500Main,
+ contentColor = NeutralWhite
+ ),
+ ) {
+ Text(
+ text = stringResource(R.string.confirm),
+ style = ourMenuTypography().pretendard_700_18,
+ color = NeutralWhite
+ )
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun LogoutModalPreview() {
+ LogoutModal(
+ onDismiss = {},
+ onConfirm = {}
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/my/component/MyBottomModal.kt b/app/src/main/java/com/kuit/ourmenu/ui/my/component/MyBottomModal.kt
new file mode 100644
index 00000000..ba8353b7
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/my/component/MyBottomModal.kt
@@ -0,0 +1,148 @@
+package com.kuit.ourmenu.ui.my.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+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.shape.RoundedCornerShape
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Text
+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.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.data.model.auth.SignInType
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.OurMenuTypography
+import com.kuit.ourmenu.ui.theme.Primary500Main
+
+@Composable
+fun MyBottomModal(
+ signInType: SignInType = SignInType.EMAIL,
+ onDismissRequest: () -> Unit,
+ onChangePassword: () -> Unit,
+ onLogout: () -> Unit,
+ onDeleteAccount: () -> Unit,
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Black.copy(alpha = 0.4f)) // ํ๋ฆฐ ๋ฐฐ๊ฒฝ
+ .clickable(onClick = onDismissRequest), // ๋ฐ๊นฅ ํด๋ฆญ ์ dismiss
+ contentAlignment = Alignment.BottomCenter
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ ) {
+ // ๋ชจ๋ฌ ๋ณธ๋ฌธ
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(12.dp))
+ .background(
+ NeutralWhite,
+ shape = RoundedCornerShape(12.dp)
+ )
+ ) {
+ if (signInType == SignInType.EMAIL) {
+ SheetItem(
+ text = stringResource(R.string.change_password),
+ textColor = Neutral700,
+ onClick = {
+ onDismissRequest()
+ onChangePassword()
+ }
+ )
+ HorizontalDivider(color = Neutral300)
+ }
+
+ SheetItem(
+ text = stringResource(R.string.logout),
+ onClick = {
+ onDismissRequest()
+ onLogout()
+ }
+ )
+
+ HorizontalDivider(color = Neutral300)
+
+ SheetItem(
+ text = stringResource(R.string.delete_account),
+ onClick = {
+ onDismissRequest()
+ onDeleteAccount()
+ }
+ )
+ }
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // ์ทจ์ ๋ฒํผ
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(12.dp))
+ .clickable(onClick = onDismissRequest)
+ .background(
+ NeutralWhite,
+ shape = RoundedCornerShape(12.dp)
+ )
+ .padding(vertical = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = stringResource(R.string.cancel),
+ style = OurMenuTypography().pretendard_500_16,
+ color = Neutral500
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun SheetItem(
+ text: String,
+ onClick: () -> Unit,
+ textColor: Color = Primary500Main
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = onClick)
+ .padding(vertical = 16.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = text,
+ style = OurMenuTypography().pretendard_500_16,
+ color = textColor,
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MyBottomModalPreview() {
+ MyBottomModal(
+ onDismissRequest = {},
+ onChangePassword = {},
+ onLogout = {},
+ onDeleteAccount = {}
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/my/component/MyCurrentPasswordModal.kt b/app/src/main/java/com/kuit/ourmenu/ui/my/component/MyCurrentPasswordModal.kt
new file mode 100644
index 00000000..2f6c871e
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/my/component/MyCurrentPasswordModal.kt
@@ -0,0 +1,190 @@
+package com.kuit.ourmenu.ui.my.component
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+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.Column
+import androidx.compose.foundation.layout.Row
+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.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.CheckboxDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.common.LoginTextField
+import com.kuit.ourmenu.ui.common.OurSnackbarHost
+import com.kuit.ourmenu.ui.common.model.PasswordState
+import com.kuit.ourmenu.ui.theme.Neutral100
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import kotlin.math.roundToInt
+
+@Composable
+fun MyCurrentPasswordModal(
+ onDismiss: () -> Unit,
+ onConfirm: () -> Unit,
+ currentPassword: String = "",
+ focusRequester: FocusRequester,
+ passwordState: PasswordState = PasswordState.Default,
+ shakeOffset: Animatable,
+ snackbarHostState: SnackbarHostState,
+ isPasswordVisible: Boolean = false,
+ updatePassword: (String) -> Unit = {},
+ updatePasswordVisible: (Boolean) -> Unit = {},
+) {
+ val shakingModifier = Modifier.offset { IntOffset(shakeOffset.value.roundToInt(), 0) }
+
+ Dialog(onDismissRequest = onDismiss) {
+ Box(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ Column(
+ modifier = Modifier
+ .background(NeutralWhite, shape = RoundedCornerShape(16.dp))
+ .padding(20.dp)
+ .width(288.dp)
+ .align(Alignment.Center),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_close_24_n400),
+ contentDescription = null,
+ tint = Color.Unspecified,
+ modifier = Modifier
+ .align(Alignment.End)
+ .clickable { onDismiss() }
+ .size(24.dp)
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ text = stringResource(R.string.enter_current_password),
+ style = ourMenuTypography().pretendard_700_18,
+ color = Neutral900,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ LoginTextField(
+ placeholder = stringResource(R.string.current_password),
+ input = currentPassword,
+ modifier = when (passwordState) {
+ PasswordState.IncorrectPassword ->
+ shakingModifier.focusRequester(focusRequester)
+
+ else -> Modifier.focusRequester(focusRequester)
+ },
+ onTextChange = { updatePassword(it) },
+ visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Checkbox(
+ checked = isPasswordVisible,
+ onCheckedChange = { updatePasswordVisible(it) },
+ modifier =
+ Modifier
+ .border(1.dp, Neutral300, RoundedCornerShape(4.dp))
+ .size(24.dp),
+ colors =
+ CheckboxDefaults.colors(
+ checkmarkColor = NeutralWhite,
+ checkedColor = Primary500Main,
+ uncheckedColor = Neutral100,
+ ),
+ )
+
+ Text(
+ text = stringResource(R.string.see_password),
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral500,
+ modifier = Modifier.padding(start = 8.dp),
+ )
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Button(
+ onClick = {
+ onConfirm()
+ onDismiss()
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(48.dp),
+ shape = RoundedCornerShape(8.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Primary500Main,
+ contentColor = NeutralWhite
+ ),
+ ) {
+ Text(
+ text = stringResource(R.string.confirm),
+ style = ourMenuTypography().pretendard_700_18,
+ color = NeutralWhite
+ )
+ }
+ }
+ OurSnackbarHost(
+ modifier = Modifier
+ .align(Alignment.TopCenter)
+ .padding(top = 100.dp),
+ hostState = snackbarHostState
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MyCurrentPasswordModalPreview() {
+ MyCurrentPasswordModal(
+ onDismiss = {},
+ onConfirm = {},
+ snackbarHostState = SnackbarHostState(),
+ focusRequester = FocusRequester(),
+ shakeOffset = remember { Animatable(0f) },
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/my/component/MyMealTime.kt b/app/src/main/java/com/kuit/ourmenu/ui/my/component/MyMealTime.kt
new file mode 100644
index 00000000..62b65b89
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/my/component/MyMealTime.kt
@@ -0,0 +1,126 @@
+package com.kuit.ourmenu.ui.my.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+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.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.my.viewmodel.UserMealTime
+import com.kuit.ourmenu.ui.theme.Neutral200
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.OurMenuTypography
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import com.kuit.ourmenu.utils.ExtensionUtil.toMealTime
+import kotlin.collections.forEachIndexed
+import kotlin.collections.lastIndex
+
+@Composable
+fun MyMealTime(
+ navigateToEdit: () -> Unit = {},
+ mealTimes: List = listOf(),
+) {
+ Column(
+ modifier = Modifier
+ .background(Neutral200)
+ .padding(20.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.now_meal_time),
+ style = OurMenuTypography().pretendard_600_16,
+ color = Neutral900
+ )
+
+ MyMealTimeBox(mealTimes = mealTimes)
+
+ Spacer(Modifier.height(20.dp))
+
+ Button(
+ onClick = { navigateToEdit() },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(42.dp),
+ shape = RoundedCornerShape(8.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Primary500Main,
+ contentColor = NeutralWhite
+ ),
+ ) {
+ Text(
+ text = stringResource(R.string.change_meal_time),
+ style = ourMenuTypography().pretendard_700_16,
+ color = NeutralWhite
+ )
+ }
+ }
+}
+
+@Composable
+fun MyMealTimeBox(mealTimes: List) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp)
+ .clip(RoundedCornerShape(12.dp))
+ .background(NeutralWhite)
+ .padding(horizontal = 16.dp, vertical = 2.dp)
+ ) {
+ mealTimes.forEachIndexed { index, time ->
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_clock),
+ contentDescription = null,
+ tint = if (time.isAfter) Primary500Main else Color.Unspecified,
+ modifier = Modifier.padding(end = 16.dp)
+ )
+ Text(
+ text = "${time.mealTime}:00",
+ style = OurMenuTypography().pretendard_600_18,
+ color = Neutral900
+ )
+ }
+
+ if (index != mealTimes.lastIndex) {
+ HorizontalDivider(color = Neutral300, thickness = 1.dp)
+ }
+ }
+ }
+
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MyMealTimePreview() {
+ MyMealTime(
+ mealTimes = listOf(
+ UserMealTime(mealTime = 8, isAfter = true),
+ UserMealTime(mealTime = 12, isAfter = true),
+ UserMealTime(mealTime = 18, isAfter = false)
+ )
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/my/component/MyNewPasswordModal.kt b/app/src/main/java/com/kuit/ourmenu/ui/my/component/MyNewPasswordModal.kt
new file mode 100644
index 00000000..12425d09
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/my/component/MyNewPasswordModal.kt
@@ -0,0 +1,256 @@
+package com.kuit.ourmenu.ui.my.component
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+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.Column
+import androidx.compose.foundation.layout.Row
+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.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.CheckboxDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.common.LoginTextField
+import com.kuit.ourmenu.ui.common.OurSnackbarHost
+import com.kuit.ourmenu.ui.common.model.PasswordState
+import com.kuit.ourmenu.ui.theme.Neutral100
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import kotlin.math.roundToInt
+
+@Composable
+fun MyNewPasswordModal(
+ onDismiss: () -> Unit = {},
+ focusRequester: FocusRequester,
+ shakeOffset: Animatable,
+ passwordState: PasswordState = PasswordState.Default,
+ snackbarHostState: SnackbarHostState,
+ onConfirm: () -> Unit = {},
+ passwordConfirmEnabled: Boolean = true,
+ newPassword: String = "",
+ confirmNewPassword: String = "",
+ isPasswordVisible: Boolean = false,
+ updateNewPassWordChange: (String) -> Unit = {},
+ updateConfirmPasswordChange: (String) -> Unit = {},
+ updatePasswordVisibilityChange: () -> Unit = {},
+) {
+ MyNewPasswordModal(
+ onDismiss = onDismiss,
+ onConfirmClick = { onConfirm() },
+ focusRequester = focusRequester,
+ snackbarHostState = snackbarHostState,
+ newPassword = newPassword,
+ passwordState = passwordState,
+ confirmNewPassword = confirmNewPassword,
+ shakeOffset = shakeOffset,
+ isPasswordVisible = isPasswordVisible,
+ enable = passwordConfirmEnabled,
+ updateNewPassWordChange = { updateNewPassWordChange(it) },
+ updateConfirmPasswordChange = { updateConfirmPasswordChange(it) },
+ updatePasswordVisibilityChange = { updatePasswordVisibilityChange() }
+ )
+}
+
+@Composable
+private fun MyNewPasswordModal(
+ onDismiss: () -> Unit,
+ onConfirmClick: () -> Unit,
+ newPassword: String,
+ snackbarHostState: SnackbarHostState,
+ passwordState: PasswordState,
+ focusRequester: FocusRequester,
+ confirmNewPassword: String,
+ shakeOffset: Animatable,
+ isPasswordVisible: Boolean,
+ enable: Boolean,
+ updateNewPassWordChange: (String) -> Unit,
+ updateConfirmPasswordChange: (String) -> Unit,
+ updatePasswordVisibilityChange: () -> Unit,
+) {
+ val shakingModifier = Modifier.offset { IntOffset(shakeOffset.value.roundToInt(), 0) }
+
+ Dialog(onDismissRequest = onDismiss) {
+ Box(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ Column(
+ modifier = Modifier
+ .background(NeutralWhite, shape = RoundedCornerShape(16.dp))
+ .padding(20.dp)
+ .width(288.dp)
+ .align(Alignment.Center),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_close_24_n400),
+ contentDescription = null,
+ tint = Color.Unspecified,
+ modifier = Modifier
+ .align(Alignment.End)
+ .clickable { onDismiss() }
+ .size(24.dp)
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ text = stringResource(R.string.enter_new_password),
+ style = ourMenuTypography().pretendard_700_18,
+ color = Neutral900,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Text(
+ text = stringResource(R.string.password_hint),
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral500,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ LoginTextField(
+ error = when (passwordState) {
+ PasswordState.NotMeetCondition, PasswordState.DifferentPassword -> true
+ else -> false
+ },
+ modifier = when (passwordState) {
+ PasswordState.NotMeetCondition, PasswordState.DifferentPassword ->
+ shakingModifier.focusRequester(focusRequester)
+
+ else -> Modifier.focusRequester(focusRequester)
+ },
+ placeholder = stringResource(R.string.new_password),
+ input = newPassword,
+ onTextChange = { updateNewPassWordChange(it) },
+ visualTransformation =
+ if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ LoginTextField(
+ error = when (passwordState) {
+ PasswordState.DifferentPassword -> true
+ else -> false
+ },
+ modifier = when (passwordState) {
+ PasswordState.NotMeetCondition, PasswordState.DifferentPassword ->
+ shakingModifier
+
+ else -> Modifier
+ },
+ placeholder = stringResource(R.string.confirm_new_password),
+ input = confirmNewPassword,
+ onTextChange = { updateConfirmPasswordChange(it) },
+ visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Checkbox(
+ checked = isPasswordVisible,
+ onCheckedChange = { updatePasswordVisibilityChange() },
+ modifier =
+ Modifier
+ .border(1.dp, Neutral300, RoundedCornerShape(4.dp))
+ .size(24.dp),
+ colors =
+ CheckboxDefaults.colors(
+ checkmarkColor = NeutralWhite,
+ checkedColor = Primary500Main,
+ uncheckedColor = Neutral100,
+ ),
+ )
+
+ Text(
+ text = stringResource(R.string.see_password),
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral500,
+ modifier = Modifier.padding(start = 8.dp),
+ )
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ Button(
+ onClick = {
+ onConfirmClick()
+ },
+ enabled = enable,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(48.dp),
+ shape = RoundedCornerShape(8.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Primary500Main,
+ contentColor = NeutralWhite
+ ),
+ ) {
+ Text(
+ text = stringResource(R.string.confirm),
+ style = ourMenuTypography().pretendard_700_18,
+ color = NeutralWhite
+ )
+ }
+ }
+ OurSnackbarHost(
+ modifier = Modifier.align(Alignment.TopCenter).padding(top = 100.dp),
+ hostState = snackbarHostState
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MyNewPasswordModalPreview() {
+ MyNewPasswordModal(
+ onDismiss = {},
+ focusRequester = FocusRequester(),
+ shakeOffset = remember { Animatable(0f) },
+ snackbarHostState = SnackbarHostState(),
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/my/navigation/MyNavigation.kt b/app/src/main/java/com/kuit/ourmenu/ui/my/navigation/MyNavigation.kt
new file mode 100644
index 00000000..5ed9759a
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/my/navigation/MyNavigation.kt
@@ -0,0 +1,45 @@
+package com.kuit.ourmenu.ui.my.navigation
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavOptions
+import androidx.navigation.compose.composable
+import androidx.navigation.navOptions
+import androidx.navigation.toRoute
+import com.kuit.ourmenu.ui.my.screen.EditMyMealTimeRoute
+import com.kuit.ourmenu.ui.my.screen.MyRoute
+import com.kuit.ourmenu.ui.navigator.MainTabRoute
+import com.kuit.ourmenu.ui.navigator.Routes
+
+fun NavController.navigateToMy(navOptions: NavOptions) {
+ navigate(MainTabRoute.My, navOptions)
+}
+
+fun NavController.navigateToEditMyMealTime(selectedTimes: List) {
+ navigate(Routes.EditMyMealTime(selectedTimes))
+}
+
+fun NavGraphBuilder.myNavGraph(
+ padding: PaddingValues,
+ navigateToLanding: () -> Unit = {},
+ navigateToEdit: (List) -> Unit = {},
+ navigateToBack: () -> Unit = {},
+) {
+ composable {
+ MyRoute(
+ padding = padding,
+ navigateToEdit = navigateToEdit,
+ navigateToLanding = navigateToLanding,
+ )
+ }
+
+ composable { navBackStackEntry ->
+ val selectedTimes = navBackStackEntry.toRoute().selectedTimes
+ EditMyMealTimeRoute(
+ padding = padding,
+ selectedTimes = selectedTimes,
+ navigateToBack = navigateToBack,
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/my/screen/EditMyMealTimeScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/my/screen/EditMyMealTimeScreen.kt
new file mode 100644
index 00000000..4c8a78b3
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/my/screen/EditMyMealTimeScreen.kt
@@ -0,0 +1,135 @@
+package com.kuit.ourmenu.ui.my.screen
+
+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.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.common.DisableBottomFullWidthButton
+import com.kuit.ourmenu.ui.common.topappbar.OnboardingTopAppBar
+import com.kuit.ourmenu.ui.my.viewmodel.EditMyMealTimeUiState
+import com.kuit.ourmenu.ui.my.viewmodel.EditMyMealTimeViewModel
+import com.kuit.ourmenu.ui.common.MealTimeGrid
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun EditMyMealTimeRoute(
+ padding: PaddingValues,
+ selectedTimes: List = listOf(),
+ navigateToBack: () -> Unit = {},
+ viewModel: EditMyMealTimeViewModel = hiltViewModel(),
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ LaunchedEffect(Unit) {
+ viewModel.setSelectedTimes(selectedTimes)
+ }
+
+ LaunchedEffect(uiState.isSuccess) {
+ if (uiState.isSuccess) {
+ navigateToBack()
+ }
+ }
+
+ EditMyMealTimeScreen(
+ padding = padding,
+ uiState = uiState,
+ updateSelectedTime = viewModel::updateSelectedTime,
+ navigateToBack = navigateToBack,
+ onConfirmClick = viewModel::changeMealTime,
+ )
+
+
+}
+
+@Composable
+fun EditMyMealTimeScreen(
+ padding: PaddingValues,
+ uiState: EditMyMealTimeUiState,
+ updateSelectedTime: (Int) -> Unit = {},
+ navigateToBack: () -> Unit = {},
+ onConfirmClick: () -> Unit = {},
+) {
+ val mealTimes = uiState.mealTimes
+ val selectedTimes = uiState.selectedTimes
+
+ val enable = selectedTimes.isNotEmpty()
+
+ Scaffold(
+ topBar = {
+ OnboardingTopAppBar(
+ onBackClick = navigateToBack,
+ )
+ },
+ modifier = Modifier
+ .fillMaxSize()
+ .imePadding()
+ .padding(padding)
+ ) { innerPadding ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .padding(horizontal = 20.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(top = 92.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.input_meal_time),
+ style = ourMenuTypography().pretendard_600_24,
+ color = Neutral900,
+ modifier = Modifier
+ )
+
+ Text(
+ text = stringResource(R.string.input_meal_time_description),
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral500,
+ modifier = Modifier.padding(top = 4.dp),
+ )
+
+ MealTimeGrid(
+ modifier = Modifier.padding(top = 29.dp),
+ mealTimes = mealTimes,
+ updateSelectedTime = updateSelectedTime
+ )
+ }
+
+ DisableBottomFullWidthButton(
+ enable = enable,
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(bottom = 20.dp),
+ text = stringResource(R.string.confirm),
+ onClick = onConfirmClick
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun EditMyMealTimeScreenPreview() {
+ EditMyMealTimeScreen(
+ padding = PaddingValues(),
+ uiState = EditMyMealTimeUiState(),
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/my/screen/MyScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/my/screen/MyScreen.kt
new file mode 100644
index 00000000..3a08b420
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/my/screen/MyScreen.kt
@@ -0,0 +1,457 @@
+package com.kuit.ourmenu.ui.my.screen
+
+import android.content.Intent
+import android.provider.SyncStateContract.Helpers.update
+import android.util.Log
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+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.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.SnackbarResult
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.net.toUri
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.common.OurSnackbarHost
+import com.kuit.ourmenu.ui.common.model.PasswordState
+import com.kuit.ourmenu.ui.common.topappbar.OurMenuAddButtonTopAppBar
+import com.kuit.ourmenu.ui.my.component.DeleteAccountModal
+import com.kuit.ourmenu.ui.my.component.LogoutModal
+import com.kuit.ourmenu.ui.my.component.MyBottomModal
+import com.kuit.ourmenu.ui.my.component.MyCurrentPasswordModal
+import com.kuit.ourmenu.ui.my.component.MyMealTime
+import com.kuit.ourmenu.ui.my.component.MyNewPasswordModal
+import com.kuit.ourmenu.ui.my.viewmodel.MyPageUiState
+import com.kuit.ourmenu.ui.my.viewmodel.MyPageViewModel
+import com.kuit.ourmenu.ui.my.viewmodel.UserMealTime
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.NeutralBlack
+import com.kuit.ourmenu.ui.theme.OurMenuTypography
+import com.kuit.ourmenu.utils.AnimationUtil.shakeErrorInputField
+import com.kuit.ourmenu.utils.AnimationUtil.shakeErrorInputFieldWithFocus
+import com.kuit.ourmenu.utils.ViewUtil.noRippleClickable
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+@Composable
+fun MyRoute(
+ padding: PaddingValues,
+ navigateToEdit: (List) -> Unit = {},
+ navigateToLanding: () -> Unit = {},
+ viewModel: MyPageViewModel = hiltViewModel(),
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ val context = LocalContext.current
+ val snackbarHostState = remember { SnackbarHostState() }
+ val scope = rememberCoroutineScope()
+ val shakeOffset = remember { Animatable(0f) }
+ val focusRequester = remember { FocusRequester() }
+ val passwordConfirmEnabled by remember {
+ derivedStateOf {
+ uiState.newPassword.isNotEmpty() && uiState.confirmNewPassword.isNotEmpty()
+ }
+ }
+
+ LaunchedEffect(
+ uiState.isLogoutSuccess,
+ uiState.isDeleteAccountSuccess
+ ) {
+ if (uiState.isLogoutSuccess || uiState.isDeleteAccountSuccess)
+ navigateToLanding()
+ }
+
+ LaunchedEffect(
+ uiState.showCompleteSnackbar
+ ) {
+ if (uiState.showCompleteSnackbar) {
+ snackbarHostState.showSnackbar(
+ message = "๋น๋ฐ๋ฒํธ๊ฐ ๋ณ๊ฒฝ๋์์ด์.",
+ duration = SnackbarDuration.Short,
+ )
+ viewModel.updateShowCompleteState(false)
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ viewModel.getUserInfo()
+ }
+
+ LaunchedEffect(uiState.passwordState) {
+ when (uiState.passwordState) {
+ PasswordState.NotMeetCondition -> {
+ shakeErrorInputFieldWithFocus(
+ shakeOffset = shakeOffset,
+ focusRequester = focusRequester,
+ message = "๋น๋ฐ๋ฒํธ ์กฐ๊ฑด์ ๋ค์ ํ์ธํด์ฃผ์ธ์.",
+ snackbarHostState = snackbarHostState,
+ scope = scope
+ )
+ scope.launch {
+ delay(1600)
+ viewModel.updatePasswordState(PasswordState.Default)
+ }
+ }
+
+ PasswordState.DifferentPassword -> {
+ shakeErrorInputField(
+ shakeOffset = shakeOffset,
+ message = "๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์์.",
+ snackbarHostState = snackbarHostState,
+ scope = scope
+ )
+ scope.launch {
+ delay(800)
+ viewModel.updatePasswordState(PasswordState.Default)
+ }
+ }
+
+ PasswordState.Valid -> {
+ viewModel.changePassword()
+ }
+
+ PasswordState.IncorrectPassword -> {
+ viewModel.reInputCurrentPassword()
+ shakeErrorInputFieldWithFocus(
+ shakeOffset = shakeOffset,
+ focusRequester = focusRequester,
+ message = "ํ์ฌ ๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์์.",
+ snackbarHostState = snackbarHostState,
+ scope = scope
+ )
+ }
+
+ else -> {}
+ }
+ }
+
+ MyScreen(
+ padding = padding,
+ uiState = uiState,
+ snackbarHostState = snackbarHostState,
+ focusRequester = focusRequester,
+ shakeOffset = shakeOffset,
+ navigateToEdit = { navigateToEdit(uiState.mealTimes.map { it.mealTime }) },
+ updatePasswordState = { viewModel.updatePasswordState(null) },
+ logout = { viewModel.logout(context) },
+ deleteAccount = { viewModel.deleteAccount(context) },
+ updateBottomSheetVisible = viewModel::updateBottomSheetVisible,
+ updateCurrentPasswordModalVisible = viewModel::updateCurrentPasswordModalVisible,
+ updateNewPasswordModalVisible = viewModel::updateNewPasswordModalVisible,
+ updateLogoutModalVisible = viewModel::updateLogoutModalVisible,
+ updateDeleteAccountModalVisible = viewModel::updateDeleteAccountModalVisible,
+ navigateToAnnouncement = {
+ val intent = Intent(
+ Intent.ACTION_VIEW,
+ uiState.announcementUrl.toUri()
+ )
+ context.startActivity(intent)
+ },
+ navigateToCustomerService = {
+ val intent = Intent(
+ Intent.ACTION_VIEW,
+ uiState.customerServiceUrl.toUri()
+ )
+ context.startActivity(intent)
+ },
+ navigateToReview = {
+ val intent = Intent(
+ Intent.ACTION_VIEW,
+ uiState.appReviewUrl.toUri()
+ ).apply { setPackage("com.android.vending") }
+ context.startActivity(intent)
+ },
+ passwordConfirmEnabled = passwordConfirmEnabled,
+ updateCurrentPassword = viewModel::updateCurrentPassword,
+ updateNewPassWordChange = viewModel::updateNewPassword,
+ updateConfirmPasswordChange = viewModel::updateConfirmNewPassword,
+ updatePasswordVisibilityChange = viewModel::updatePasswordVisibility
+ )
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(top = 44.dp),
+ contentAlignment = Alignment.TopCenter
+ ) {
+ OurSnackbarHost(
+ hostState = snackbarHostState,
+ isChecked = true,
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MyScreen(
+ padding: PaddingValues,
+ uiState: MyPageUiState,
+ snackbarHostState: SnackbarHostState,
+ focusRequester: FocusRequester,
+ shakeOffset: Animatable,
+ navigateToEdit: () -> Unit = {},
+ updatePasswordState: () -> Unit = {},
+ logout: () -> Unit = {},
+ deleteAccount: () -> Unit = {},
+ updateBottomSheetVisible: (Boolean) -> Unit = {},
+ updateCurrentPasswordModalVisible: (Boolean) -> Unit = {},
+ updateNewPasswordModalVisible: (Boolean) -> Unit = {},
+ updateLogoutModalVisible: (Boolean) -> Unit = {},
+ updateDeleteAccountModalVisible: (Boolean) -> Unit = {},
+ navigateToAnnouncement: () -> Unit = {},
+ navigateToCustomerService: () -> Unit = {},
+ navigateToReview: () -> Unit = {},
+ passwordConfirmEnabled: Boolean,
+ updateCurrentPassword: (String) -> Unit = {},
+ updateNewPassWordChange: (String) -> Unit = {},
+ updateConfirmPasswordChange: (String) -> Unit = {},
+ updatePasswordVisibilityChange: () -> Unit = {}
+) {
+ Box(
+ modifier = Modifier
+ .padding(bottom = padding.calculateBottomPadding())
+ ) {
+ Scaffold(
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .padding(innerPadding)
+ ) {
+ Row(
+ modifier = Modifier
+ .padding(start = 20.dp, end = 20.dp, top = 32.dp, bottom = 24.dp)
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_profile),
+ contentDescription = null,
+ modifier = Modifier.size(44.dp),
+ tint = Color.Unspecified
+ )
+
+ Spacer(modifier = Modifier.width(16.dp))
+
+ Text(
+ text = uiState.email,
+ style = OurMenuTypography().pretendard_700_14,
+ color = Neutral900
+ )
+ }
+
+ Icon(
+ painter = painterResource(R.drawable.ic_kebab),
+ contentDescription = null,
+ modifier = Modifier
+ .size(24.dp)
+ .noRippleClickable(
+ onClick = { updateBottomSheetVisible(true) }
+ ),
+ tint = Neutral700
+ )
+ }
+
+ MyMealTime(
+ navigateToEdit = navigateToEdit,
+ mealTimes = uiState.mealTimes
+ )
+
+ Column(
+ modifier = Modifier
+ .padding(top = 22.dp)
+ ) {
+ InfoRow(infoTitle = stringResource(R.string.notice)) {
+ navigateToAnnouncement()
+ }
+ InfoRow(infoTitle = stringResource(R.string.customer_service)) {
+ navigateToCustomerService()
+ }
+ InfoRow(infoTitle = stringResource(R.string.app_review)) {
+ navigateToReview()
+ }
+ Text(
+ modifier = Modifier.padding(top = 10.dp, start = 20.dp),
+ text = stringResource(R.string.app_version, "2.0"),
+ style = OurMenuTypography().pretendard_600_16,
+ color = Neutral900
+ )
+ }
+ }
+ }
+
+ // ๋ชจ๋ฌ ์ฒ๋ฆฌ
+ if (uiState.bottomSheetVisible) {
+ MyBottomModal(
+ signInType = uiState.signInType,
+ onDismissRequest = { updateBottomSheetVisible(false) },
+ onChangePassword = {
+ updateCurrentPasswordModalVisible(true)
+ updateBottomSheetVisible(false)
+ },
+ onLogout = {
+ updateLogoutModalVisible(true)
+ updateBottomSheetVisible(false)
+ },
+ onDeleteAccount = {
+ updateDeleteAccountModalVisible(true)
+ updateBottomSheetVisible(false)
+ }
+ )
+ }
+
+ // ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ ๋ชจ๋ฌ
+ if (uiState.showCurrentPasswordModal) {
+ MyCurrentPasswordModal(
+ onDismiss = { updateCurrentPasswordModalVisible(false) },
+ onConfirm = {
+ updateNewPasswordModalVisible(true)
+ },
+ snackbarHostState = snackbarHostState,
+ currentPassword = uiState.currentPassword,
+ isPasswordVisible = uiState.isPasswordViewVisible,
+ updatePassword = { updateCurrentPassword(it) },
+ updatePasswordVisible = { updatePasswordVisibilityChange() },
+ focusRequester = focusRequester,
+ passwordState = uiState.passwordState,
+ shakeOffset = shakeOffset,
+ )
+ }
+
+ // ์ ๋น๋ฐ๋ฒํธ ์
๋ ฅ ๋ชจ๋ฌ
+ if (uiState.showNewPasswordModal) {
+ MyNewPasswordModal(
+ onDismiss = { updateNewPasswordModalVisible(false) },
+ onConfirm = { updatePasswordState() },
+ focusRequester = focusRequester,
+ shakeOffset = shakeOffset,
+ snackbarHostState = snackbarHostState,
+ newPassword = uiState.newPassword,
+ passwordState = uiState.passwordState,
+ confirmNewPassword = uiState.confirmNewPassword,
+ isPasswordVisible = uiState.isPasswordViewVisible,
+ passwordConfirmEnabled = passwordConfirmEnabled,
+ updateNewPassWordChange = { updateNewPassWordChange(it) },
+ updateConfirmPasswordChange = { updateConfirmPasswordChange(it) },
+ updatePasswordVisibilityChange = { updatePasswordVisibilityChange() },
+
+ )
+ }
+
+ // ๋ก๊ทธ์์ ๋ชจ๋ฌ
+ if (uiState.showLogoutModal) {
+ LogoutModal(
+ onDismiss = { updateLogoutModalVisible(false) },
+ onConfirm = {
+ logout()
+ }
+ )
+ }
+
+ // ๊ณ์ ์ญ์ ๋ชจ๋ฌ
+ if (uiState.showDeleteAccountModal) {
+ DeleteAccountModal(
+ onDismiss = { updateDeleteAccountModalVisible(false) },
+ onConfirm = { deleteAccount() }
+ )
+ }
+ }
+}
+
+@Composable
+fun InfoRow(
+ modifier: Modifier = Modifier,
+ infoTitle: String,
+ onClick: () -> Unit
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .noRippleClickable(onClick = onClick)
+ .padding(start = 20.dp, end = 30.dp)
+ .padding(vertical = 10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = infoTitle,
+ style = OurMenuTypography().pretendard_600_16.copy(
+ lineHeight = 24.sp
+ ),
+ color = Neutral900
+ )
+
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
+ contentDescription = null,
+ modifier = Modifier.height(24.dp)
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MyScreenPreview() {
+ MyScreen(
+ uiState = MyPageUiState(
+ email = "ourmenu@gmai.com",
+ mealTimes = listOf(
+ UserMealTime(
+ 10, true
+ ),
+ UserMealTime(
+ 13, true
+ ),
+ UserMealTime(
+ 16, true
+ )
+ )
+ ),
+ padding = PaddingValues(),
+ snackbarHostState = SnackbarHostState(),
+ focusRequester = FocusRequester(),
+ shakeOffset = remember { Animatable(0f) },
+ passwordConfirmEnabled = true,
+ )
+}
+
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/my/viewmodel/EditMyMealTimeUiState.kt b/app/src/main/java/com/kuit/ourmenu/ui/my/viewmodel/EditMyMealTimeUiState.kt
new file mode 100644
index 00000000..9b55811d
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/my/viewmodel/EditMyMealTimeUiState.kt
@@ -0,0 +1,13 @@
+package com.kuit.ourmenu.ui.my.viewmodel
+
+import com.kuit.ourmenu.ui.common.model.MealTime
+
+
+data class EditMyMealTimeUiState(
+ val mealTimes: List = List(18) {
+ MealTime(mealTime = it + 6)
+ },
+ val selectedTimes: List = emptyList(),
+ val isSuccess: Boolean = false,
+)
+
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/my/viewmodel/EditMyMealTimeViewModel.kt b/app/src/main/java/com/kuit/ourmenu/ui/my/viewmodel/EditMyMealTimeViewModel.kt
new file mode 100644
index 00000000..449ef49f
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/my/viewmodel/EditMyMealTimeViewModel.kt
@@ -0,0 +1,74 @@
+package com.kuit.ourmenu.ui.my.viewmodel
+
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.kuit.ourmenu.data.repository.UserRepository
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class EditMyMealTimeViewModel @Inject constructor(
+ private val userRepository: UserRepository
+) : ViewModel() {
+
+ private val _uiState = MutableStateFlow(EditMyMealTimeUiState())
+ val uiState = _uiState.asStateFlow()
+
+ fun updateSelectedTime(index: Int) {
+ _uiState.update {
+ val selected = it.mealTimes[index].selected
+ val currentSelectedTimes = it.selectedTimes.toMutableList()
+ val mealTime = it.mealTimes[index].mealTime
+
+ it.copy(
+ mealTimes = it.mealTimes.toMutableList()
+ .apply {
+ this[index].selected = !selected && currentSelectedTimes.size < 4
+ }.toList(),
+ selectedTimes =
+ run {
+ if (!selected && currentSelectedTimes.size < 4) {
+ currentSelectedTimes.add(mealTime)
+ } else currentSelectedTimes.remove(mealTime)
+ currentSelectedTimes.toList()
+ },
+ )
+ }
+ }
+
+ fun setSelectedTimes(selectedTimes: List) {
+ _uiState.update {
+ it.copy(
+ selectedTimes = selectedTimes,
+ mealTimes = it.mealTimes.map { mealTime ->
+ mealTime.copy(selected = selectedTimes.contains(mealTime.mealTime))
+ }
+ )
+ }
+ }
+
+ fun changeMealTime() {
+ viewModelScope.launch {
+ userRepository.updateMealTimes(
+ newMealTimes = _uiState.value.selectedTimes.sorted().map { "${it.toString().padStart(2, '0')}:00:00" }
+ ).fold(
+ onSuccess = {
+ _uiState.update {
+ it.copy(isSuccess = true)
+ }
+ },
+ onFailure = {
+ Log.e(
+ "EditMyMealTimeViewModel", "changeMealTime: ${it.message}", it
+ )
+ }
+ )
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/my/viewmodel/MyPageUiState.kt b/app/src/main/java/com/kuit/ourmenu/ui/my/viewmodel/MyPageUiState.kt
new file mode 100644
index 00000000..aade3f02
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/my/viewmodel/MyPageUiState.kt
@@ -0,0 +1,32 @@
+package com.kuit.ourmenu.ui.my.viewmodel
+
+import com.kuit.ourmenu.data.model.auth.SignInType
+import com.kuit.ourmenu.ui.common.model.PasswordState
+
+data class MyPageUiState(
+ val email: String = "",
+ val signInType: SignInType = SignInType.EMAIL,
+ val mealTimes: List = emptyList(),
+ val error: String = "",
+ val bottomSheetVisible: Boolean = false,
+ val showCurrentPasswordModal: Boolean = false,
+ val showNewPasswordModal: Boolean = false,
+ val showLogoutModal: Boolean = false,
+ val showDeleteAccountModal: Boolean = false,
+ val isLogoutSuccess: Boolean = false,
+ val isDeleteAccountSuccess: Boolean = false,
+ val announcementUrl: String = "",
+ val customerServiceUrl: String = "",
+ val appReviewUrl: String = "",
+ val currentPassword: String = "",
+ val newPassword: String = "",
+ val confirmNewPassword: String = "",
+ val isPasswordViewVisible: Boolean = false,
+ val showCompleteSnackbar: Boolean = false,
+ val passwordState: PasswordState = PasswordState.Default
+)
+
+data class UserMealTime(
+ val mealTime: Int,
+ val isAfter: Boolean = false,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/my/viewmodel/MyPageViewModel.kt b/app/src/main/java/com/kuit/ourmenu/ui/my/viewmodel/MyPageViewModel.kt
new file mode 100644
index 00000000..f5bd5f0b
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/my/viewmodel/MyPageViewModel.kt
@@ -0,0 +1,274 @@
+package com.kuit.ourmenu.ui.my.viewmodel
+
+import android.content.Context
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.kuit.ourmenu.data.model.auth.SignInType
+import com.kuit.ourmenu.data.repository.KakaoRepository
+import com.kuit.ourmenu.data.repository.AuthRepository
+import com.kuit.ourmenu.data.repository.UserRepository
+import com.kuit.ourmenu.ui.common.model.PasswordState
+import com.kuit.ourmenu.ui.common.model.checkPassword
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import javax.inject.Inject
+
+@HiltViewModel
+class MyPageViewModel @Inject constructor(
+ private val userRepository: UserRepository,
+ private val authRepository: AuthRepository,
+ private val kakaoRepository: KakaoRepository,
+) : ViewModel() {
+
+ private val _uiState = MutableStateFlow(MyPageUiState())
+ val uiState = _uiState.asStateFlow()
+
+ fun getUserInfo() {
+ viewModelScope.launch {
+ userRepository.getUserInfo().fold(
+ onSuccess = { response ->
+ if (response != null) {
+ _uiState.update {
+ it.copy(
+ email = response.email,
+ mealTimes = response.mealTimeList.map { mealTime ->
+ UserMealTime(
+ mealTime = mealTime.mealTime.substring(0, 2).toInt(),
+ isAfter = mealTime.isAfter,
+ )
+ },
+ signInType = SignInType.valueOf(response.signInType),
+ announcementUrl = response.announcementUrl,
+ customerServiceUrl = response.customerServiceUrl,
+ appReviewUrl = response.appReviewUrl,
+ )
+ }
+ } else {
+ Log.d("MyPageViewModel", "getUserInfo: response is null")
+ }
+ },
+ onFailure = {
+ Log.d("MyPageViewModel", "getUserInfo: $it")
+ }
+ )
+ }
+ }
+
+ fun updatePasswordState(
+ passwordState: PasswordState?
+ ) {
+ val newPasswordState = passwordState ?: checkPassword(
+ password = uiState.value.newPassword,
+ confirmPassword = uiState.value.confirmNewPassword,
+ )
+
+ _uiState.update {
+ it.copy(passwordState = newPasswordState)
+ }
+ }
+
+ fun changePassword() {
+ viewModelScope.launch {
+ userRepository.changePassword(
+ currentPassword = uiState.value.currentPassword,
+ newPassword = uiState.value.newPassword,
+ ).fold(
+ onSuccess = { response ->
+ if (response != null) {
+ Log.d("MyPageViewModel", "changePassword: $response")
+ } else {
+ Log.d("MyPageViewModel", "changePassword: response is null")
+ }
+ updateNewPasswordModalVisible(false)
+ updateShowCompleteState(true)
+ },
+ onFailure = {
+ Log.d("MyPageViewModel", "changePassword: $it")
+ updatePasswordState(PasswordState.IncorrectPassword)
+ }
+ )
+ }
+ }
+
+ fun updateShowCompleteState(visible: Boolean) {
+ _uiState.update {
+ it.copy(showCompleteSnackbar = visible)
+ }
+ }
+
+ fun logout(
+ context: Context
+ ) {
+ viewModelScope.launch {
+ var kakaoResult = true
+
+ runBlocking {
+ kakaoRepository.logout(
+ errorLogout = {
+ Log.d("MyPageViewModel", "Kakao logout failed: $it")
+ kakaoResult = false
+ },
+ successLogout = {
+ Log.d("MyPageViewModel", "Kakao logout success")
+ kakaoResult = true
+ }
+ )
+ }
+
+ if (kakaoResult) {
+ authRepository.logout().fold(
+ onSuccess = {
+ Log.d("MyPageViewModel", "logout Success: $it")
+ setLogoutSuccess()
+ },
+ onFailure = {
+ Log.d("MyPageViewModel", "logout Failure: $it")
+ kakaoRepository.getKakaoLogin(
+ context = context,
+ ) { }
+ }
+ )
+ }
+ }
+ }
+
+ fun deleteAccount(
+ context: Context
+ ) {
+ viewModelScope.launch {
+ var kakaoResult = true
+
+ runBlocking {
+ kakaoRepository.unlink(
+ errorUnlink = {
+ Log.d("MyPageViewModel", "Kakao unlink failed: $it")
+ kakaoResult = false
+ },
+ successUnlink = {
+ Log.d("MyPageViewModel", "Kakao unlink success")
+ kakaoResult = true
+ }
+ )
+ }
+ if (kakaoResult) {
+ userRepository.deleteUser().fold(
+ onSuccess = {
+ Log.d("MyPageViewModel", "deleteAccount: $it")
+ setDeleteAccountSuccess()
+ },
+ onFailure = {
+ Log.d("MyPageViewModel", "deleteAccount: $it")
+ kakaoRepository.getKakaoLogin(
+ context = context
+ ) { }
+ }
+ )
+ }
+ }
+ }
+
+ fun updateBottomSheetVisible(visible: Boolean) {
+ _uiState.update {
+ it.copy(bottomSheetVisible = visible)
+ }
+ }
+
+ fun updateCurrentPasswordModalVisible(visible: Boolean) {
+ if (!visible) {
+ _uiState.update {
+ it.copy(
+ isPasswordViewVisible = false,
+ passwordState = PasswordState.Default,
+ showCurrentPasswordModal = false,
+ )
+ }
+ } else {
+ _uiState.update {
+ it.copy(
+ showCurrentPasswordModal = true,
+ )
+ }
+ }
+ }
+
+ fun updateNewPasswordModalVisible(visible: Boolean) {
+ if (!visible) {
+ _uiState.update {
+ it.copy(
+ currentPassword = "",
+ newPassword = "",
+ confirmNewPassword = "",
+ isPasswordViewVisible = false,
+ passwordState = PasswordState.Default,
+ showNewPasswordModal = false,
+ )
+ }
+ } else {
+ _uiState.update {
+ it.copy(
+ showNewPasswordModal = true,
+ )
+ }
+ }
+ }
+
+ fun setLogoutSuccess() {
+ updateLogoutModalVisible(false)
+ _uiState.update {
+ it.copy(isLogoutSuccess = true)
+ }
+ }
+
+ fun setDeleteAccountSuccess() {
+ updateDeleteAccountModalVisible(false)
+ _uiState.update {
+ it.copy(isDeleteAccountSuccess = true)
+ }
+ }
+
+ fun updateLogoutModalVisible(visible: Boolean) {
+ _uiState.update {
+ it.copy(showLogoutModal = visible)
+ }
+ }
+
+ fun updateDeleteAccountModalVisible(visible: Boolean) {
+ _uiState.update {
+ it.copy(showDeleteAccountModal = visible)
+ }
+ }
+
+ fun updateCurrentPassword(currentPassword: String) {
+ _uiState.update {
+ it.copy(currentPassword = currentPassword)
+ }
+ }
+
+ fun updateNewPassword(newPassword: String) {
+ _uiState.update {
+ it.copy(newPassword = newPassword)
+ }
+ }
+
+ fun updateConfirmNewPassword(confirmNewPassword: String) {
+ _uiState.update {
+ it.copy(confirmNewPassword = confirmNewPassword)
+ }
+ }
+
+ fun updatePasswordVisibility() {
+ _uiState.update {
+ it.copy(isPasswordViewVisible = !it.isPasswordViewVisible)
+ }
+ }
+
+ fun reInputCurrentPassword() {
+ updateCurrentPasswordModalVisible(true)
+ updateNewPasswordModalVisible(false)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/navigator/MainNavController.kt b/app/src/main/java/com/kuit/ourmenu/ui/navigator/MainNavController.kt
new file mode 100644
index 00000000..eb706d95
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/navigator/MainNavController.kt
@@ -0,0 +1,136 @@
+package com.kuit.ourmenu.ui.navigator
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.navigation.NavDestination
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
+import androidx.navigation.navOptions
+import com.kuit.ourmenu.ui.addmenu.navigation.navigateToAddMenuInfo
+import com.kuit.ourmenu.ui.home.navigation.navigateToHome
+import com.kuit.ourmenu.ui.menuFolder.navigation.navigateToAddMenu
+import com.kuit.ourmenu.ui.menuFolder.navigation.navigateToMenuFolder
+import com.kuit.ourmenu.ui.menuFolder.navigation.navigateToMenuFolderAllMenu
+import com.kuit.ourmenu.ui.menuFolder.navigation.navigateToMenuFolderDetail
+import com.kuit.ourmenu.ui.menuinfo.navigation.navigateToMenuInfo
+import com.kuit.ourmenu.ui.my.navigation.navigateToEditMyMealTime
+import com.kuit.ourmenu.ui.my.navigation.navigateToMy
+import com.kuit.ourmenu.ui.onboarding.navigation.navigateOnboardingToHome
+import com.kuit.ourmenu.ui.onboarding.navigation.navigateToLanding
+import com.kuit.ourmenu.ui.onboarding.navigation.navigateToLogin
+import com.kuit.ourmenu.ui.searchmenu.navigation.navigateToSearchMenu
+import com.kuit.ourmenu.ui.signup.navigation.navigateToSignupEmail
+import com.kuit.ourmenu.ui.signup.navigation.navigateToSignupMealTime
+import com.kuit.ourmenu.ui.signup.navigation.navigateToSignupPassword
+import com.kuit.ourmenu.ui.signup.navigation.navigateToSignupVerify
+
+class MainNavController(
+ val navController: NavHostController,
+) {
+ private val currentDestination: NavDestination?
+ @Composable get() = navController
+ .currentBackStackEntryAsState().value?.destination
+
+ val startDestination = Routes.Landing
+
+ val currentTab: MainTab?
+ @Composable get() = MainTab.entries.find { tab ->
+ currentDestination?.route == tab.route::class.qualifiedName
+ }
+
+ fun navigate(tab: MainTab) {
+ val navOptions = navOptions {
+ popUpTo(MainTab.HOME.route) {
+ inclusive = false
+ }
+ launchSingleTop = true
+ restoreState = true
+ }
+
+ when (tab) {
+ MainTab.HOME -> navController.navigateToHome(navOptions)
+ MainTab.MAP -> navController.navigateToSearchMenu(navOptions)
+ MainTab.MENU_FOLDER -> navController.navigateToMenuFolder(navOptions)
+ MainTab.MY -> navController.navigateToMy(navOptions)
+ }
+ }
+
+ // Back Pressed
+ fun navigateUp() {
+ navController.navigateUp()
+ }
+
+ // Onboarding
+ fun navigateToLanding() {
+ navController.navigateToLanding()
+ }
+
+ fun navigateToLogin() {
+ navController.navigateToLogin()
+ }
+
+ fun navigateToSignupEmail() {
+ navController.navigateToSignupEmail()
+ }
+
+ fun navigateToSignupPassword() {
+ navController.navigateToSignupPassword()
+ }
+
+ fun navigateToSignupMealTime() {
+ navController.navigateToSignupMealTime()
+ }
+
+ fun navigateToSignupVerify() {
+ navController.navigateToSignupVerify()
+ }
+
+ fun navigateOnboardingToHome() {
+ navController.navigateOnboardingToHome()
+ }
+
+ fun popBackStack() {
+ navController.popBackStack()
+ }
+
+ // Menu Folder
+ fun navigateToMenuFolderDetail(menuFolderId: Long) {
+ navController.navigateToMenuFolderDetail(menuFolderId)
+ }
+
+ fun navigateToMenuFolderAllMenu() {
+ navController.navigateToMenuFolderAllMenu()
+ }
+
+ // My
+ fun navigateToEditMyMealTime(selectedTimes: List) {
+ navController.navigateToEditMyMealTime(selectedTimes)
+ }
+
+ // Add Menu
+ fun navigateToAddMenu() {
+ navController.navigateToAddMenu()
+ }
+
+ fun navigateToAddMenuInfo() {
+ navController.navigateToAddMenuInfo()
+ }
+
+ // Menu Info
+ fun navigateToMenuInfo(menuId: Long) {
+ navController.navigateToMenuInfo(menuId)
+ }
+
+ @Composable
+ fun shouldShowBottomBar(): Boolean = MainTab.contains {
+ currentDestination?.route?.startsWith(it::class.qualifiedName!!) == true
+ }
+}
+
+@Composable
+fun rememberMainNavigator(
+ navController: NavHostController = rememberNavController(),
+): MainNavController = remember(navController) {
+ MainNavController(navController)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/navigator/MainNavHost.kt b/app/src/main/java/com/kuit/ourmenu/ui/navigator/MainNavHost.kt
new file mode 100644
index 00000000..38ecfafd
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/navigator/MainNavHost.kt
@@ -0,0 +1,127 @@
+package com.kuit.ourmenu.ui.navigator
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.toRoute
+import com.kuit.ourmenu.ui.addmenu.navigation.addMenuNavGraph
+import com.kuit.ourmenu.ui.addmenu.screen.AddMenuScreen
+import com.kuit.ourmenu.ui.home.navigation.homeNavGraph
+import com.kuit.ourmenu.ui.menuFolder.navigation.menuFolderNavGraph
+import com.kuit.ourmenu.ui.menuFolder.screen.MenuFolderAllMenuScreen
+import com.kuit.ourmenu.ui.menuFolder.screen.MenuFolderDetailScreen
+import com.kuit.ourmenu.ui.menuinfo.screen.MenuInfoDefaultScreen
+import com.kuit.ourmenu.ui.menuinfo.screen.MenuInfoMapScreen
+import com.kuit.ourmenu.ui.my.navigation.myNavGraph
+import com.kuit.ourmenu.ui.onboarding.navigation.onboardingNavGraph
+import com.kuit.ourmenu.ui.searchmenu.navigation.searchMenuNavGraph
+import com.kuit.ourmenu.ui.signup.navigation.signupNavGraph
+import com.kuit.ourmenu.ui.signup.viewmodel.SignupViewModel
+
+@Composable
+fun MainNavHost(
+ navController: MainNavController,
+ padding: PaddingValues
+) {
+ NavHost(
+ navController = navController.navController,
+ startDestination = navController.startDestination
+ ) {
+ onboardingNavGraph(
+ navigateBack = navController::navigateUp,
+ navigateOnboardingToHome = navController::navigateOnboardingToHome,
+ navigateToLogin = navController::navigateToLogin,
+ navigateToSignupEmail = navController::navigateToSignupEmail,
+ navigateToSignupMealTime = navController::navigateToSignupMealTime,
+ )
+
+ signupNavGraph(
+ navigateBack = navController::navigateUp,
+ navigateOnboardingToHome = navController::navigateOnboardingToHome,
+ navigateToSignupVerify = navController::navigateToSignupVerify,
+ navigateToSignupPassword = navController::navigateToSignupPassword,
+ navigateToSignupMealTime = navController::navigateToSignupMealTime,
+ getSignupViewModel = { navBackStackEntry ->
+ navBackStackEntry.destination.route?.let {
+
+ val parent = remember(navBackStackEntry) {
+ navController.navController.getBackStackEntry(Routes.SignupEmail)
+ }
+ hiltViewModel(parent)
+ } ?: hiltViewModel()
+ }
+ )
+
+ homeNavGraph(
+ padding = padding,
+ )
+
+ menuFolderNavGraph(
+ padding = padding,
+ navigateBack = navController::navigateUp,
+ navigateToMenuFolderDetail = navController::navigateToMenuFolderDetail,
+ navigateToMenuFolderAllMenu = navController::navigateToMenuFolderAllMenu,
+ navigateToMenuInfo = navController::navigateToMenuInfo,
+ navigateToAddMenu = navController::navigateToAddMenu,
+ )
+
+ addMenuNavGraph(
+ navigateBack = navController::navigateUp,
+ navigateToAddMenuInfo = navController::navigateToAddMenuInfo
+ )
+
+ searchMenuNavGraph(
+ navigateToMenuDetail = navController::navigateToMenuInfo,
+ )
+
+ myNavGraph(
+ padding = padding,
+ navigateToEdit = navController::navigateToEditMyMealTime,
+ navigateToLanding = navController::navigateToLanding,
+ navigateToBack = navController::navigateUp,
+ )
+
+ // ๋ฉ๋ดํ
+ composable {
+ val menuFolderId = it.toRoute().menuFolderId
+ MenuFolderDetailScreen(
+ menuFolderId = menuFolderId,
+ onNavigateBack = navController::navigateUp,
+ onNavigateToMenuInfo = navController::navigateToMenuInfo,
+ onNavigateToAddMenu = navController::navigateToAddMenu
+ )
+ }
+ composable {
+ MenuFolderAllMenuScreen(
+ onNavigateBack = navController::navigateUp,
+ onNavigateToMenuInfo = navController::navigateToMenuInfo,
+// onNavigateToMap = navController::navigateToMenuInfoMap,
+ onNavigateToAddMenu = navController::navigateToAddMenu
+ )
+ }
+
+ // ๋ฉ๋ด
+ composable {
+ val menuId = it.toRoute().menuId
+ MenuInfoDefaultScreen(
+ menuId = menuId,
+ onNavigateBack = navController::navigateUp,
+ onNavigateToMenuFolderDetail = navController::navigateToMenuFolderDetail,
+// onNavigateToMap = navController::navigateToMenuInfoMap
+ )
+ }
+ composable {
+ MenuInfoMapScreen(navController = navController.navController)
+ }
+
+ // ๋ฉ๋ด ์ถ๊ฐ
+ composable {
+ AddMenuScreen(
+ onNavigateToAddMenuInfo = navController::navigateToAddMenuInfo
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/navigator/MainTab.kt b/app/src/main/java/com/kuit/ourmenu/ui/navigator/MainTab.kt
new file mode 100644
index 00000000..da459053
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/navigator/MainTab.kt
@@ -0,0 +1,50 @@
+package com.kuit.ourmenu.ui.navigator
+
+import androidx.compose.runtime.Composable
+import com.kuit.ourmenu.R
+
+enum class MainTab(
+ val iconResId: Int,
+ val selectedIconResId: Int,
+ internal val contentDescription: String,
+ val label: String,
+ val route: MainTabRoute,
+) {
+ HOME(
+ iconResId = R.drawable.ic_navigation_bar_home,
+ selectedIconResId = R.drawable.ic_navigation_bar_home_selected,
+ contentDescription = "Home Icon",
+ label = "ํ",
+ route = MainTabRoute.Home,
+ ),
+ MAP(
+ iconResId = R.drawable.ic_navigation_bar_map,
+ selectedIconResId = R.drawable.ic_navigation_bar_map_selected,
+ contentDescription = "Map Icon",
+ label = "์ง๋",
+ route = MainTabRoute.Map,
+ ),
+ MENU_FOLDER(
+ iconResId = R.drawable.ic_navigation_bar_folder,
+ selectedIconResId = R.drawable.ic_navigation_bar_folder_selected,
+ contentDescription = "Menu Folder Icon",
+ label = "๋ฉ๋ดํ",
+ route = MainTabRoute.MenuFolder,
+ ),
+ MY(
+ iconResId = R.drawable.ic_navigation_bar_my,
+ selectedIconResId = R.drawable.ic_navigation_bar_my_selected,
+ contentDescription = "My Icon",
+ label = "๋ง์ด",
+ route = MainTabRoute.My,
+ )
+ ;
+
+ companion object {
+
+ @Composable
+ fun contains(predicate: @Composable (Routes) -> Boolean): Boolean {
+ return entries.map { it.route }.any { predicate(it) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/navigator/MainTabRoute.kt b/app/src/main/java/com/kuit/ourmenu/ui/navigator/MainTabRoute.kt
new file mode 100644
index 00000000..0e0639fa
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/navigator/MainTabRoute.kt
@@ -0,0 +1,14 @@
+package com.kuit.ourmenu.ui.navigator
+
+import kotlinx.serialization.Serializable
+
+sealed interface MainTabRoute : Routes {
+ @Serializable
+ data object Home : MainTabRoute
+ @Serializable
+ data object Map : MainTabRoute
+ @Serializable
+ data object MenuFolder : MainTabRoute
+ @Serializable
+ data object My : MainTabRoute
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/navigator/Routes.kt b/app/src/main/java/com/kuit/ourmenu/ui/navigator/Routes.kt
new file mode 100644
index 00000000..00b7b8e9
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/navigator/Routes.kt
@@ -0,0 +1,46 @@
+package com.kuit.ourmenu.ui.navigator
+
+import kotlinx.serialization.Serializable
+
+sealed interface Routes{
+
+ // ๋ฉ๋ดํ
+ @Serializable
+ data object MenuFolder: Routes
+ @Serializable
+ data class MenuFolderDetail(val menuFolderId: Long): Routes
+ @Serializable
+ data object MenuFolderAllMenu: Routes
+
+ // ๋ฉ๋ด
+ @Serializable
+ data class MenuInfo(val menuId: Long): Routes
+ @Serializable
+ data object MenuInfoMap: Routes
+
+ // ๋ฉ๋ด ์ถ๊ฐ
+ @Serializable
+ data object AddMenu: Routes
+ @Serializable
+ data object AddMenuInfo: Routes
+
+ // Mypage
+ @Serializable
+ data class EditMyMealTime(val selectedTimes: List): Routes
+
+ // Onboarding
+ @Serializable
+ data object Landing: Routes
+ @Serializable
+ data object Login: Routes
+
+ // Signup
+ @Serializable
+ data object SignupEmail: Routes
+ @Serializable
+ data object SignupPassword: Routes
+ @Serializable
+ data object SignupMealTime: Routes
+ @Serializable
+ data object SignupVerify: Routes
+}
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/navigator/component/MainBottomBar.kt b/app/src/main/java/com/kuit/ourmenu/ui/navigator/component/MainBottomBar.kt
new file mode 100644
index 00000000..dda6736a
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/navigator/component/MainBottomBar.kt
@@ -0,0 +1,79 @@
+package com.kuit.ourmenu.ui.navigator.component
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideIn
+import androidx.compose.animation.slideOut
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.ui.navigator.MainTab
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.OurMenuTheme
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toImmutableList
+
+@Composable
+internal fun MainBottomBar(
+ modifier: Modifier = Modifier,
+ visible: Boolean,
+ tabs: ImmutableList,
+ currentTab: MainTab?,
+ onTabSelected: (MainTab) -> Unit,
+) {
+ AnimatedVisibility(
+ visible = visible,
+ enter = fadeIn() + slideIn { IntOffset(0, it.height) },
+ exit = fadeOut() + slideOut { IntOffset(0, it.height) },
+ ) {
+ Row(
+ modifier = modifier
+ .shadow(
+ elevation = 4.dp,
+ spotColor = Color(0x1F000000),
+ ambientColor = Color(0x1F000000)
+ )
+ .background(color = NeutralWhite)
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp)
+ .height(76.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ tabs.forEach { tab ->
+ MainBottomBarItem(
+ tab = tab,
+ selected = tab == currentTab,
+ onClick = {
+ if (tab != currentTab) onTabSelected(tab)
+ }
+ )
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun MainBottomBarPreview() {
+ OurMenuTheme {
+ MainBottomBar(
+ visible = true,
+ tabs = MainTab.entries.toImmutableList(),
+ currentTab = MainTab.HOME,
+ onTabSelected = {}
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/navigator/component/MainBottomBarItem.kt b/app/src/main/java/com/kuit/ourmenu/ui/navigator/component/MainBottomBarItem.kt
new file mode 100644
index 00000000..c7006768
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/navigator/component/MainBottomBarItem.kt
@@ -0,0 +1,60 @@
+package com.kuit.ourmenu.ui.navigator.component
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.selection.selectable
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.ui.navigator.MainTab
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun RowScope.MainBottomBarItem(
+ modifier: Modifier = Modifier,
+ tab: MainTab,
+ selected: Boolean = false,
+ onClick: () -> Unit = {}
+) {
+ Column(
+ modifier = modifier
+ .padding(vertical = 10.dp)
+ .weight(1f)
+ .fillMaxHeight()
+ .selectable(
+ selected = selected,
+ indication = null,
+ role = Role.Tab,
+ interactionSource = remember { MutableInteractionSource() },
+ onClick = onClick,
+ ),
+ verticalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterVertically),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(if (selected) tab.selectedIconResId else tab.iconResId),
+ contentDescription = tab.contentDescription,
+ tint = Color.Unspecified
+ )
+ Text(
+ text = tab.label,
+ style = ourMenuTypography().pretendard_400_12.copy(
+ color = if (selected) Primary500Main else Neutral500
+ )
+ )
+ }
+}
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/onboarding/component/BottomFullWidthBorderButton.kt b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/component/BottomFullWidthBorderButton.kt
new file mode 100644
index 00000000..15f1811c
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/component/BottomFullWidthBorderButton.kt
@@ -0,0 +1,58 @@
+package com.kuit.ourmenu.ui.onboarding.component
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun BottomFullWidthBorderButton(
+ onClick: () -> Unit
+) {
+ Button(
+ onClick = onClick,
+ modifier = Modifier
+ .fillMaxWidth()
+ .size(320.dp, 48.dp)
+ .border(
+ width = (1.2).dp,
+ color = Primary500Main,
+ shape = RoundedCornerShape(8.dp)
+ ),
+ shape = RoundedCornerShape(8.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = NeutralWhite,
+ contentColor = Primary500Main
+ ),
+ ) {
+ Text(text = stringResource(R.string.make_account),
+ style = ourMenuTypography().pretendard_500_16,
+ color = Primary500Main
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun BottomFullWidthButtonPreview() {
+ Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) {
+ BottomFullWidthBorderButton{
+ //onClick ์์ฑ
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/onboarding/model/LandingUiState.kt b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/model/LandingUiState.kt
new file mode 100644
index 00000000..d43f150a
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/model/LandingUiState.kt
@@ -0,0 +1,8 @@
+package com.kuit.ourmenu.ui.onboarding.model
+
+import com.kuit.ourmenu.ui.onboarding.state.KakaoState
+
+data class LandingUiState(
+ var kakaoState: KakaoState = KakaoState.Default,
+ val error: String = "",
+)
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/onboarding/model/LoginUiState.kt b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/model/LoginUiState.kt
new file mode 100644
index 00000000..3282785f
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/model/LoginUiState.kt
@@ -0,0 +1,11 @@
+package com.kuit.ourmenu.ui.onboarding.model
+
+import com.kuit.ourmenu.ui.onboarding.state.LoginState
+
+data class LoginUiState(
+ val email: String = "",
+ val password: String = "",
+ val loginState: LoginState = LoginState.Default,
+ val isPasswordVisible: Boolean = false,
+ val error: String = ""
+)
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/onboarding/navigation/OnboardingNavigation.kt b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/navigation/OnboardingNavigation.kt
new file mode 100644
index 00000000..2a529385
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/navigation/OnboardingNavigation.kt
@@ -0,0 +1,51 @@
+package com.kuit.ourmenu.ui.onboarding.navigation
+
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import com.kuit.ourmenu.ui.navigator.MainTabRoute
+import com.kuit.ourmenu.ui.navigator.Routes
+import com.kuit.ourmenu.ui.onboarding.screen.LandingRoute
+import com.kuit.ourmenu.ui.onboarding.screen.LoginRoute
+
+fun NavController.navigateToLanding() {
+ navigate(Routes.Landing)
+}
+
+fun NavController.navigateToLogin() {
+ navigate(Routes.Login)
+}
+
+fun NavController.navigateOnboardingToHome() {
+ navigate(MainTabRoute.Home) {
+ popUpTo(Routes.Landing) {
+ inclusive = true
+ }
+ }
+}
+
+
+fun NavGraphBuilder.onboardingNavGraph(
+ navigateBack: () -> Unit,
+ navigateOnboardingToHome: () -> Unit,
+ navigateToLogin: () -> Unit,
+ navigateToSignupEmail: () -> Unit,
+ navigateToSignupMealTime: () -> Unit,
+) {
+ composable {
+ LandingRoute(
+ navigateToHome = navigateOnboardingToHome,
+ navigateToLogin = navigateToLogin,
+ navigateToSignupEmail = navigateToSignupEmail,
+ navigateToSignupMealTime = navigateToSignupMealTime,
+ )
+ }
+ composable {
+ LoginRoute(
+ navigateToHome = navigateOnboardingToHome,
+ navigateBack = navigateBack,
+ navigateToSignupEmail = navigateToSignupEmail,
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/onboarding/screen/LandingScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/screen/LandingScreen.kt
new file mode 100644
index 00000000..8695262c
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/screen/LandingScreen.kt
@@ -0,0 +1,243 @@
+package com.kuit.ourmenu.ui.onboarding.screen
+
+import android.app.Activity
+import android.util.Log
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.common.BottomFullWidthButton
+import com.kuit.ourmenu.ui.onboarding.component.BottomFullWidthBorderButton
+import com.kuit.ourmenu.ui.onboarding.state.KakaoState
+import com.kuit.ourmenu.ui.onboarding.viewmodel.LandingViewModel
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import kotlinx.coroutines.launch
+
+@Composable
+fun LandingRoute(
+ navigateToHome: () -> Unit,
+ navigateToLogin: () -> Unit,
+ navigateToSignupEmail: () -> Unit,
+ navigateToSignupMealTime: () -> Unit,
+ viewModel: LandingViewModel = hiltViewModel()
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ val snackbarHostState = remember { SnackbarHostState() }
+ val scope = rememberCoroutineScope()
+ val context = LocalContext.current as Activity
+ LaunchedEffect(Unit) { navigateToHome() }
+
+ LaunchedEffect(uiState.kakaoState) {
+ Log.d("KakaoModule", uiState.kakaoState.toString())
+ when (uiState.kakaoState) {
+ is KakaoState.Login -> navigateToHome()
+
+ is KakaoState.Loading -> viewModel.signInWithKakao()
+
+ is KakaoState.Signup -> navigateToSignupMealTime()
+ is KakaoState.Error -> {
+ // ์๋ฌ์ ๋ฐ๋ผ snackbar ๋ฅผ show ํ๋ฉด ๋จ
+ scope.launch {
+ snackbarHostState.showSnackbar(
+ message = viewModel.error.value ?: "",
+ duration = SnackbarDuration.Short
+ )
+ }
+ }
+
+ else -> {}
+ }
+ }
+
+ LandingScreen(
+ navigateToLogin = navigateToLogin,
+ navigateToSignupEmail = navigateToSignupEmail,
+ onKakaoLoginClick = {
+ viewModel.getKakaoLogin(context = context)
+ }
+ )
+}
+
+@Composable
+fun LandingScreen(
+ navigateToLogin: () -> Unit,
+ navigateToSignupEmail: () -> Unit,
+ onKakaoLoginClick: () -> Unit = { },
+) {
+
+ Box(
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .padding(bottom = 18.dp),
+ ) {
+ Column(
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .padding(horizontal = 20.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Image(
+ painter = painterResource(R.drawable.ic_top_bar_logo),
+ contentDescription = null,
+ modifier = Modifier
+ .padding(top = 120.dp)
+ .size(78.dp),
+ )
+
+ Text(
+ text = stringResource(R.string.ourmenu),
+ style = ourMenuTypography().pretendard_700_32,
+ color = Primary500Main,
+ modifier = Modifier.padding(top = 4.dp),
+ )
+
+ Text(
+ text = stringResource(R.string.landing_introduce_title),
+ style = ourMenuTypography().pretendard_600_18,
+ color = Neutral900,
+ modifier = Modifier.padding(top = 72.dp),
+ )
+ Text(
+ text = stringResource(R.string.landing_introduce_content),
+ style = ourMenuTypography().pretendard_400_14,
+ color = Neutral700,
+ modifier = Modifier.padding(top = 2.dp),
+ )
+
+ Spacer(modifier = Modifier.height(44.dp))
+
+ BottomFullWidthButton(
+ modifier = Modifier,
+ containerColor = Primary500Main,
+ contentColor = NeutralWhite,
+ text = stringResource(R.string.login)
+ ) {
+ navigateToLogin()
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ BottomFullWidthBorderButton {
+ navigateToSignupEmail()
+ }
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 32.dp)
+ ) {
+ HorizontalDivider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .align(Alignment.Center),
+ thickness = 1.dp,
+ color = Neutral300
+ )
+
+ Box(
+ modifier = Modifier
+ .width(75.dp)
+ .height(38.dp)
+ .background(NeutralWhite)
+ .align(Alignment.Center),
+ )
+ Text(
+ modifier = Modifier
+ .align(Alignment.Center),
+ text = "์์
๋ก๊ทธ์ธ",
+ style = ourMenuTypography().pretendard_500_12.copy(
+ color = Neutral500
+ ),
+ textAlign = TextAlign.Center,
+ )
+ }
+
+ Image(
+ painter = painterResource(R.drawable.img_kakao_login_medium_wide),
+ contentDescription = "kakao login",
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(top = 16.dp)
+ .clickable { onKakaoLoginClick() },
+ contentScale = ContentScale.FillWidth
+ )
+ }
+
+ Box(
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .padding(bottom = 65.5.dp),
+ contentAlignment = Alignment.BottomCenter,
+ ) {
+ Row {
+ Text(
+ text = stringResource(R.string.ourmenu),
+ style = ourMenuTypography().pretendard_700_12,
+ color = Primary500Main,
+ )
+
+ Spacer(modifier = Modifier.width(4.dp))
+
+ Text(
+ text = stringResource(R.string.copy_right),
+ style = ourMenuTypography().pretendard_400_12.copy(
+ fontSize = 10.sp
+ ),
+ color = Neutral500,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+private fun LandingScreenPreview() {
+
+ LandingScreen(
+ navigateToLogin = {},
+ navigateToSignupEmail = {},
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/onboarding/screen/LoginScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/screen/LoginScreen.kt
new file mode 100644
index 00000000..58c117e9
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/screen/LoginScreen.kt
@@ -0,0 +1,328 @@
+package com.kuit.ourmenu.ui.onboarding.screen
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.CheckboxDefaults
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.common.BottomFullWidthButton
+import com.kuit.ourmenu.ui.common.OurSnackbarHost
+import com.kuit.ourmenu.ui.onboarding.component.BottomFullWidthBorderButton
+import com.kuit.ourmenu.ui.common.LoginTextField
+import com.kuit.ourmenu.ui.common.topappbar.OnboardingTopAppBar
+import com.kuit.ourmenu.ui.onboarding.model.LoginUiState
+import com.kuit.ourmenu.ui.onboarding.state.LoginState
+import com.kuit.ourmenu.ui.onboarding.viewmodel.LoginViewModel
+import com.kuit.ourmenu.ui.theme.Neutral100
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import com.kuit.ourmenu.utils.AnimationUtil.shakeErrorInputFieldWithFocus
+import kotlinx.coroutines.launch
+import kotlin.math.roundToInt
+
+@Composable
+fun LoginRoute(
+ navigateToHome: () -> Unit,
+ navigateBack: () -> Unit,
+ navigateToSignupEmail: () -> Unit,
+ viewModel: LoginViewModel = hiltViewModel()
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ val snackbarHostState = remember { SnackbarHostState() }
+ val scope = rememberCoroutineScope()
+ val shakeOffset = remember { Animatable(0f) }
+ val emailFocusRequester = remember { FocusRequester() }
+ val passwordFocusRequester = remember { FocusRequester() }
+
+ LaunchedEffect(uiState.loginState) {
+ when (uiState.loginState) {
+ is LoginState.Success -> navigateToHome()
+
+ is LoginState.Error -> {
+ scope.launch {
+ snackbarHostState.showSnackbar(
+ message = uiState.error ?: "",
+ )
+ }
+
+ }
+
+ is LoginState.NotFoundUser -> {
+ shakeErrorInputFieldWithFocus(
+ shakeOffset = shakeOffset,
+ focusRequester = emailFocusRequester,
+ message = "์กด์ฌํ์ง ์๋ ์ด๋ฉ์ผ์
๋๋ค.",
+ snackbarHostState = snackbarHostState,
+ scope = scope
+ )
+ }
+
+ is LoginState.DifferentPassword -> {
+ shakeErrorInputFieldWithFocus(
+ shakeOffset = shakeOffset,
+ focusRequester = passwordFocusRequester,
+ message = "๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์์.",
+ snackbarHostState = snackbarHostState,
+ scope = scope
+ )
+
+ }
+
+
+ else -> {}
+ }
+ }
+
+ LoginScreen(
+ navigateBack = navigateBack,
+ navigateToSignupEmail = navigateToSignupEmail,
+ shakeOffset = shakeOffset,
+ uiState = uiState,
+ emailFocusRequester = emailFocusRequester,
+ passwordFocusRequester = passwordFocusRequester,
+ updatePasswordVisible = viewModel::updatePasswordVisible,
+ signInWithEmail = viewModel::signInWithEmail,
+ updateEmail = viewModel::updateEmail,
+ updatePassword = viewModel::updatePassword,
+ )
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(top = 44.dp),
+ contentAlignment = Alignment.TopCenter
+ ) {
+ OurSnackbarHost(
+ hostState = snackbarHostState
+ )
+ }
+
+
+}
+
+@Composable
+fun LoginScreen(
+ navigateBack: () -> Unit,
+ navigateToSignupEmail: () -> Unit,
+ emailFocusRequester: FocusRequester,
+ passwordFocusRequester: FocusRequester,
+ shakeOffset: Animatable,
+ uiState: LoginUiState,
+ signInWithEmail: () -> Unit,
+ updateEmail: (String) -> Unit,
+ updatePassword: (String) -> Unit,
+ updatePasswordVisible: (Boolean) -> Unit,
+) {
+ val shakingModifier = Modifier.offset { IntOffset(shakeOffset.value.roundToInt(), 0) }
+
+ Scaffold(
+ topBar = {
+ OnboardingTopAppBar(
+ onBackClick = navigateBack,
+ )
+ },
+ content = { innerPadding ->
+ Column(
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .padding(20.dp, 48.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Box(
+ modifier = Modifier.height(48.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = stringResource(R.string.ourmenu),
+ style = ourMenuTypography().pretendard_700_32,
+ color = Primary500Main,
+ )
+ }
+
+ Spacer(modifier = Modifier.height(56.dp))
+
+ LoginTextField(
+ modifier = when (uiState.loginState) {
+ LoginState.NotFoundUser -> shakingModifier.focusRequester(
+ emailFocusRequester
+ )
+
+ else -> Modifier
+ },
+ error = uiState.loginState == LoginState.NotFoundUser,
+ placeholder = stringResource(R.string.email),
+ input = uiState.email,
+ onTextChange = { updateEmail(it) },
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ LoginTextField(
+ modifier = when (uiState.loginState) {
+ LoginState.DifferentPassword -> shakingModifier.focusRequester(
+ passwordFocusRequester
+ )
+
+ else -> Modifier
+ },
+ error = uiState.loginState == LoginState.DifferentPassword,
+ placeholder = stringResource(R.string.password),
+ input = uiState.password,
+ onTextChange = { updatePassword(it) },
+ visualTransformation = if (uiState.isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
+ )
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Checkbox(
+ checked = uiState.isPasswordVisible,
+ onCheckedChange = { updatePasswordVisible(it) },
+ modifier =
+ Modifier
+ .border(1.dp, Neutral300, RoundedCornerShape(4.dp))
+ .size(24.dp),
+ colors =
+ CheckboxDefaults.colors(
+ checkmarkColor = NeutralWhite,
+ checkedColor = Primary500Main,
+ uncheckedColor = Neutral100,
+ ),
+ )
+
+ Text(
+ text = stringResource(R.string.see_password),
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral500,
+ modifier = Modifier.padding(start = 8.dp),
+ )
+ }
+
+ Text(
+ text = stringResource(R.string.forgot_password),
+ style = ourMenuTypography().pretendard_500_12,
+ color = Neutral500,
+ modifier =
+ Modifier.clickable {
+ // TODO: ๋น๋ฐ๋ฒํธ ์ฐพ๊ธฐ ๋์ ๊ตฌํ
+ },
+ )
+ }
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ BottomFullWidthButton(
+ modifier = Modifier,
+ containerColor = Primary500Main,
+ contentColor = NeutralWhite,
+ text = stringResource(R.string.login),
+ ) { signInWithEmail() }
+
+ Spacer(modifier = Modifier.height(104.dp))
+
+ Text(
+ text = stringResource(R.string.have_account),
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral500,
+ )
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ BottomFullWidthBorderButton {
+ navigateToSignupEmail()
+ }
+ }
+
+ Box(
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .padding(bottom = 65.5.dp),
+ contentAlignment = Alignment.BottomCenter,
+ ) {
+ Row {
+ Text(
+ text = stringResource(R.string.ourmenu),
+ style = ourMenuTypography().pretendard_700_12,
+ color = Primary500Main,
+ )
+
+ Spacer(modifier = Modifier.width(4.dp))
+
+ Text(
+ text = stringResource(R.string.copy_right),
+ style = ourMenuTypography().pretendard_400_12.copy(
+ fontSize = 10.sp
+ ),
+ color = Neutral500,
+ )
+ }
+ }
+ },
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun LoginScreenPreview() {
+ LoginScreen(
+ navigateBack = {},
+ navigateToSignupEmail = {},
+ shakeOffset = remember { Animatable(0f) },
+ uiState = LoginUiState(),
+ updatePasswordVisible = { },
+ signInWithEmail = { },
+ updateEmail = { },
+ updatePassword = { },
+ emailFocusRequester = FocusRequester(),
+ passwordFocusRequester = FocusRequester(),
+ )
+}
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/onboarding/screen/SplashScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/screen/SplashScreen.kt
new file mode 100644
index 00000000..2b091f4a
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/screen/SplashScreen.kt
@@ -0,0 +1,80 @@
+package com.kuit.ourmenu.ui.onboarding.screen
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+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.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import coil3.ImageLoader
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.onboarding.viewmodel.SplashViewModel
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import com.kuit.ourmenu.utils.CacheUtil.preloadData
+import kotlinx.coroutines.delay
+
+@Composable
+fun SplashScreen(
+ imageLoader: ImageLoader,
+ viewModel: SplashViewModel = hiltViewModel(),
+ toHome: () -> Unit
+) {
+ val cacheInfoData by viewModel.cacheInfoData.collectAsState()
+
+ preloadData(LocalContext.current, imageLoader, cacheInfoData)
+ LaunchedEffect(Unit) {
+ delay(1000) // TODO : ์ถํ ์์
+ toHome()
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Primary500Main),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Image(
+ painter = painterResource(R.drawable.ic_splash_logo),
+ contentDescription = "splash ์์ด์ฝ",
+ modifier =
+ Modifier
+ .padding(bottom = 12.dp)
+ .size(140.dp),
+ )
+
+ Text(
+ text = stringResource(R.string.ourmenu),
+ style = ourMenuTypography().pretendard_700_32,
+ color = NeutralWhite,
+ )
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+private fun SplashScreenPreview() {
+ var showSplash by remember { mutableStateOf(true) }
+ SplashScreen(
+ imageLoader = ImageLoader.Builder(LocalContext.current).build(),
+ ) { showSplash = true }
+}
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/onboarding/state/CacheState.kt b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/state/CacheState.kt
new file mode 100644
index 00000000..08da6de9
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/state/CacheState.kt
@@ -0,0 +1,25 @@
+package com.kuit.ourmenu.ui.onboarding.state
+
+import com.kuit.ourmenu.data.model.cache.response.CacheInfoResponse
+
+data class CacheState(
+ val menuFolderIcons: List = emptyList(),
+ val menuPinMaps: List = emptyList(),
+ val menuAdds: List = emptyList(),
+ val menuPinAddDisables: List = emptyList(),
+ val homeImgs: List = emptyList(),
+ val orangeTags: List = emptyList(),
+ val whiteTags: List = emptyList()
+)
+
+fun CacheInfoResponse.toState() = CacheState(
+ homeImgs = homeImgs.map { it.homeImgUrl },
+ menuFolderIcons = menuFolderIcons.map { it.menuFolderIconUrl },
+ menuAdds = menuPins.mapNotNull { it.menuPinAddImgUrl },
+ menuPinMaps = menuPins.map { it.menuPinMapImgUrl },
+ menuPinAddDisables = menuPins.mapNotNull { it.menuPinAddDisableImgUrl },
+ orangeTags = tags.map { it.orangeTagImgUrl },
+ whiteTags = tags.map { it.whiteTagImgUrl }
+)
+
+
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/onboarding/state/KakaoState.kt b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/state/KakaoState.kt
new file mode 100644
index 00000000..134518c5
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/state/KakaoState.kt
@@ -0,0 +1,9 @@
+package com.kuit.ourmenu.ui.onboarding.state
+
+sealed class KakaoState {
+ data object Default: KakaoState()
+ data object Loading : KakaoState()
+ data object Login : KakaoState()
+ data object Signup : KakaoState()
+ data object Error : KakaoState()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/onboarding/state/LoginState.kt b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/state/LoginState.kt
new file mode 100644
index 00000000..4be014b1
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/state/LoginState.kt
@@ -0,0 +1,10 @@
+package com.kuit.ourmenu.ui.onboarding.state
+
+sealed class LoginState {
+ data object Default: LoginState()
+ data object Loading : LoginState()
+ data object NotFoundUser : LoginState()
+ data object DifferentPassword : LoginState()
+ data object Success : LoginState()
+ data object Error : LoginState()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/onboarding/viewmodel/LandingViewModel.kt b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/viewmodel/LandingViewModel.kt
new file mode 100644
index 00000000..71958629
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/viewmodel/LandingViewModel.kt
@@ -0,0 +1,115 @@
+package com.kuit.ourmenu.ui.onboarding.viewmodel
+
+import android.content.Context
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.kuit.ourmenu.data.model.auth.SignInType
+import com.kuit.ourmenu.data.repository.AuthRepository
+import com.kuit.ourmenu.data.repository.KakaoRepository
+import com.kuit.ourmenu.ui.onboarding.model.LandingUiState
+import com.kuit.ourmenu.ui.onboarding.state.KakaoState
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class LandingViewModel @Inject constructor(
+ private val authRepository: AuthRepository,
+ private val kakaoRepository: KakaoRepository
+) : ViewModel() {
+
+ private val _uiState = MutableStateFlow(LandingUiState())
+ val uiState = _uiState.asStateFlow()
+
+ private val _error: MutableStateFlow = MutableStateFlow(null)
+ val error = _error.asStateFlow()
+
+// ์นด์นด์ค ๋ก๊ทธ์ธ ํ
์คํธ์ฉ unlink ํจ์
+// init {
+// UserApiClient.instance.unlink { error ->
+// if (error != null) {
+// Log.e("KakaoModule", "์ฐ๊ฒฐ ๋๊ธฐ ์คํจ", error)
+// } else {
+// Log.i("KakaoModule", "์ฐ๊ฒฐ ๋๊ธฐ ์ฑ๊ณต. SDK์์ ํ ํฐ ์ญ์ ๋จ")
+// }
+// }
+// }
+
+ fun updateKakaoState(state: KakaoState) {
+ _uiState.update {
+ it.copy(kakaoState = state)
+ }
+ }
+
+ fun getKakaoLogin(
+ context: Context
+ ) {
+ kakaoRepository.getKakaoLogin(
+ context = context,
+ successLogin = {
+ updateKakaoState(KakaoState.Loading)
+ },
+ )
+ }
+
+ fun signInWithKakao() {
+ viewModelScope.launch {
+ val email = kakaoRepository.getUserEmail()
+ email ?: run {
+ _uiState.update {
+ it.copy(error = "์ด๋ฉ์ผ์ ๊ฐ์ ธ์ค๋๋ฐ ์คํจํ์ต๋๋ค.")
+ }
+ return@launch
+ }
+ _uiState.update {
+ it.copy(kakaoState = KakaoState.Loading)
+ }
+
+ if (checkKakaoEmail()) {
+ authRepository.login(
+ email = email,
+ password = null,
+ signInType = SignInType.KAKAO
+ ).fold(
+ onSuccess = {
+ _uiState.update {
+ it.copy(kakaoState = KakaoState.Login)
+ }
+ },
+ onFailure = { error ->
+ _uiState.update {
+ it.copy(
+ kakaoState = KakaoState.Error,
+ error = error.message ?: "๋ก๊ทธ์ธ ์คํจ"
+ )
+ }
+
+ delay(1000)
+ }
+ )
+ } else {
+ // ์นด์นด์ค ํ์ X -> ํ์๊ฐ์
+ _uiState.update {
+ it.copy(kakaoState = KakaoState.Signup)
+ }
+ }
+
+ }
+ }
+
+ private suspend fun checkKakaoEmail(): Boolean {
+ val email = kakaoRepository.getUserEmail()
+
+ authRepository.checkKakaoEmail(email).fold(
+ onSuccess = { response ->
+ return response?.existUser == true
+ },
+ onFailure = { return false }
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/onboarding/viewmodel/LoginViewModel.kt b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/viewmodel/LoginViewModel.kt
new file mode 100644
index 00000000..a650be89
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/viewmodel/LoginViewModel.kt
@@ -0,0 +1,94 @@
+package com.kuit.ourmenu.ui.onboarding.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.kuit.ourmenu.data.model.auth.SignInType
+import com.kuit.ourmenu.data.model.base.OurMenuApiFailureException
+import com.kuit.ourmenu.data.repository.AuthRepository
+import com.kuit.ourmenu.ui.onboarding.model.LoginUiState
+import com.kuit.ourmenu.ui.onboarding.state.LoginState
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class LoginViewModel @Inject constructor(
+ private val authRepository: AuthRepository,
+) : ViewModel() {
+
+ private val _uiState = MutableStateFlow(LoginUiState())
+ val uiState = _uiState.asStateFlow()
+
+ fun updateEmail(email: String) {
+ _uiState.update {
+ it.copy(email = email)
+ }
+ }
+
+ fun updatePassword(password: String) {
+ _uiState.update {
+ it.copy(password = password)
+ }
+ }
+
+ fun updatePasswordVisible(isPasswordVisible: Boolean) {
+ _uiState.update {
+ it.copy(isPasswordVisible = isPasswordVisible)
+ }
+ }
+
+ fun signInWithEmail() {
+ viewModelScope.launch {
+ _uiState.update { it.copy(loginState = LoginState.Loading) }
+
+ authRepository.login(
+ email = _uiState.value.email,
+ password = _uiState.value.password,
+ signInType = SignInType.EMAIL
+ ).fold(
+ onSuccess = {
+ _uiState.update {
+ it.copy(loginState = LoginState.Success)
+ }
+ },
+ onFailure = { error ->
+ _uiState.update {
+ it.copy(loginState = LoginState.Error)
+ }
+ when (error) {
+ is OurMenuApiFailureException -> {
+ _uiState.update { it.copy(error = error.message ?: "Unknown error") }
+ when (error.status) {
+ 401 -> _uiState.update {
+ it.copy(loginState = LoginState.DifferentPassword)
+ }
+
+ 404 -> _uiState.update {
+ it.copy(loginState = LoginState.NotFoundUser)
+ }
+ }
+ }
+
+ else -> _uiState.update {
+ it.copy(
+ error = error.message ?: "Unknown error"
+ )
+ }
+ }
+
+ delay(1000)
+ _uiState.update {
+ it.copy(loginState = LoginState.Default)
+ }
+ }
+ )
+
+
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/onboarding/viewmodel/SplashViewModel.kt b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/viewmodel/SplashViewModel.kt
new file mode 100644
index 00000000..9f43f807
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/viewmodel/SplashViewModel.kt
@@ -0,0 +1,42 @@
+package com.kuit.ourmenu.ui.onboarding.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.kuit.ourmenu.data.repository.CacheRepository
+import com.kuit.ourmenu.ui.onboarding.state.toState
+import com.kuit.ourmenu.ui.onboarding.state.CacheState
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class SplashViewModel @Inject constructor(
+ private val cacheRepository: CacheRepository
+) : ViewModel() {
+
+ private val _cacheInfoData = MutableStateFlow(CacheState())
+ val cacheInfoData = _cacheInfoData.asStateFlow()
+
+ private val _errorMessage = MutableStateFlow(null)
+ val errorMessage = _errorMessage.asStateFlow()
+
+ init {
+// getCacheData()
+ }
+
+ private fun getCacheData() {
+ viewModelScope.launch {
+ cacheRepository.getCacheData().fold(
+ onSuccess = { data ->
+ _cacheInfoData.value = data?.toState() ?: CacheState()
+ },
+ onFailure = {
+ _errorMessage.value = it.message
+ }
+ )
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/searchmenu/component/SearchBottomSheetContent.kt b/app/src/main/java/com/kuit/ourmenu/ui/searchmenu/component/SearchBottomSheetContent.kt
new file mode 100644
index 00000000..bf894f21
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/searchmenu/component/SearchBottomSheetContent.kt
@@ -0,0 +1,64 @@
+package com.kuit.ourmenu.ui.searchmenu.component
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.data.model.map.response.MapDetailResponse
+import com.kuit.ourmenu.data.model.map.response.MenuFolderInfo
+import com.kuit.ourmenu.ui.common.bottomsheet.MenuInfoBottomSheetContent
+
+@Composable
+fun SearchBottomSheetContent(
+ modifier: Modifier = Modifier,
+ dataList: List,
+ onItemClick: (Long) -> Unit
+) {
+ LazyColumn(
+ modifier = modifier
+ ) {
+ items(dataList.size) { index ->
+ MenuInfoBottomSheetContent(
+ modifier = Modifier.padding(vertical = 20.dp),
+ menuInfoData = dataList[index],
+ onClick = { menuId ->
+ onItemClick(menuId)
+ }
+ )
+ if (index != dataList.size - 1) {
+ HorizontalDivider()
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SearchBottomSheetContentPreview() {
+ SearchBottomSheetContent(
+ dataList = listOf(
+ MapDetailResponse(
+ menuId = 1,
+ menuTitle = "Test Menu",
+ storeTitle = "๊ฐ๊ฒ ์ด๋ฆ",
+ menuPrice = 10000,
+ menuPinImgUrl = "pin",
+ menuTagImgUrls = listOf("ํ์", "๋ฐฅ"),
+ menuImgUrls = listOf(),
+ menuFolderInfo = MenuFolderInfo(
+ menuFolderTitle = "Test Store",
+ menuFolderIconImgUrl = "icon",
+ menuFolderCount = 1
+ ),
+ mapId = 1,
+ mapX = 127.0,
+ mapY = 37.0
+ )
+ )
+ ){
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/searchmenu/component/SearchHistory.kt b/app/src/main/java/com/kuit/ourmenu/ui/searchmenu/component/SearchHistory.kt
new file mode 100644
index 00000000..85743c5b
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/searchmenu/component/SearchHistory.kt
@@ -0,0 +1,184 @@
+package com.kuit.ourmenu.ui.searchmenu.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.data.model.map.response.MapSearchHistoryResponse
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun SearchHistoryList(
+ modifier: Modifier = Modifier,
+ historyList: List?,
+ onClick: (Long) -> Unit = {},
+) {
+ val lazyListState = rememberLazyListState()
+
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(top = 68.dp)
+ ) {
+ if (historyList == null || historyList.isEmpty()) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 68.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_addmenu_noresult),
+ contentDescription = "no result",
+ tint = Color.Unspecified
+ )
+ Text(
+ text = stringResource(R.string.no_result),
+ style = ourMenuTypography().pretendard_600_14,
+ color = Neutral500,
+ modifier = Modifier.padding(top = 8.dp)
+ )
+ }
+ } else {
+ Text(
+ text = "์ต๊ทผ ๊ฒ์",
+ style = ourMenuTypography().pretendard_600_16.copy(
+ lineHeight = 20.sp,
+ color = Neutral700
+ ),
+ modifier = modifier
+ .padding(bottom = 4.dp)
+ .padding(horizontal = 28.dp)
+ )
+ LazyColumn(
+ state = lazyListState
+ ) {
+
+ items(historyList.size) { index ->
+ SearchHistoryItem(
+ historyData = historyList[index],
+ onClick = onClick
+ )
+
+ if (index != historyList.size - 1) {
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = Neutral300,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun SearchHistoryItem(
+ modifier: Modifier = Modifier,
+ historyData: MapSearchHistoryResponse,
+ onClick: (Long) -> Unit
+) {
+
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .clickable(onClick = { onClick(historyData.menuId) })
+ .padding(vertical = 20.dp, horizontal = 28.dp)
+ ) {
+ Text(
+ text = historyData.menuTitle,
+ style = ourMenuTypography().pretendard_600_16.copy(
+ lineHeight = 20.sp,
+ color = Neutral700
+ ),
+ modifier = Modifier.padding(bottom = 8.dp)
+ )
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_fill_map_20),
+ contentDescription = "location info",
+ tint = Color.Unspecified
+ )
+ Column(
+ modifier = Modifier.padding(start = 4.dp)
+ ) {
+ Text(
+ text = historyData.storeTitle,
+ style = ourMenuTypography().pretendard_600_14.copy(
+ lineHeight = 20.sp,
+ color = Neutral500
+ ),
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .height(20.dp)
+ .wrapContentHeight(align = Alignment.CenterVertically)
+ )
+ Text(
+ text = historyData.storeAddress,
+ style = ourMenuTypography().pretendard_600_14.copy(
+ lineHeight = 20.sp,
+ color = Neutral500
+ ),
+ modifier = Modifier
+ .height(20.dp)
+ .wrapContentHeight(align = Alignment.CenterVertically)
+ .padding(top = 2.dp)
+ )
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SearchHistoryPreview() {
+ SearchHistoryList(
+ historyList = listOf(
+ MapSearchHistoryResponse(
+ menuTitle = "ํผ์",
+ storeTitle = "ํผ์ํ",
+ menuId = 1,
+ storeAddress = "์์ธํน๋ณ์ ๊ฐ๋จ๊ตฌ ์ญ์ผ๋ 123-4"
+ ),
+ MapSearchHistoryResponse(
+ menuTitle = "์นํจ",
+ storeTitle = "๊ตฝ๋ค์นํจ",
+ menuId = 2,
+ storeAddress = "์์ธํน๋ณ์ ๊ฐ๋จ๊ตฌ ์ญ์ผ๋ 456-7"
+ ),
+ MapSearchHistoryResponse(
+ menuTitle = "ํ๋ฒ๊ฑฐ",
+ storeTitle = "๋งฅ๋๋ ๋",
+ menuId = 3,
+ storeAddress = "์์ธํน๋ณ์ ๊ฐ๋จ๊ตฌ ์ญ์ผ๋ 987-6"
+ )
+ ),
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/searchmenu/model/SearchHistoryData.kt b/app/src/main/java/com/kuit/ourmenu/ui/searchmenu/model/SearchHistoryData.kt
new file mode 100644
index 00000000..f4d23c51
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/searchmenu/model/SearchHistoryData.kt
@@ -0,0 +1,7 @@
+package com.kuit.ourmenu.ui.searchmenu.model
+
+data class SearchHistoryData(
+ val storeTitle: String,
+ val menuTitle: String,
+ val address: String
+)
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/searchmenu/navigation/SearchMenuNavigation.kt b/app/src/main/java/com/kuit/ourmenu/ui/searchmenu/navigation/SearchMenuNavigation.kt
new file mode 100644
index 00000000..c461a6bb
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/searchmenu/navigation/SearchMenuNavigation.kt
@@ -0,0 +1,23 @@
+package com.kuit.ourmenu.ui.searchmenu.navigation
+
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavOptions
+import androidx.navigation.compose.composable
+import com.kuit.ourmenu.ui.navigator.MainTabRoute
+import com.kuit.ourmenu.ui.searchmenu.screen.SearchMenuScreen
+
+fun NavController.navigateToSearchMenu(navOptions: NavOptions) {
+ navigate(MainTabRoute.Map, navOptions)
+}
+
+fun NavGraphBuilder.searchMenuNavGraph(
+ // navigate ์ด๋ฒคํธ
+ navigateToMenuDetail: (Long) -> Unit,
+) {
+ composable {
+ SearchMenuScreen(
+ onNavigateToMenuDetail = navigateToMenuDetail
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/searchmenu/screen/SearchMenuScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/searchmenu/screen/SearchMenuScreen.kt
new file mode 100644
index 00000000..9111aef6
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/searchmenu/screen/SearchMenuScreen.kt
@@ -0,0 +1,258 @@
+package com.kuit.ourmenu.ui.searchmenu.screen
+
+import android.Manifest
+import android.util.Log
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsFocusedAsState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+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.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.BottomSheetScaffold
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.rememberBottomSheetScaffoldState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.common.GoToMapButton
+import com.kuit.ourmenu.ui.common.SearchTextField
+import com.kuit.ourmenu.ui.common.bottomsheet.BottomSheetDragHandle
+import com.kuit.ourmenu.ui.common.map.mapViewWithLifecycle
+import com.kuit.ourmenu.ui.common.topappbar.OurMenuAddButtonTopAppBar
+import com.kuit.ourmenu.ui.searchmenu.component.SearchBottomSheetContent
+import com.kuit.ourmenu.ui.searchmenu.component.SearchHistoryList
+import com.kuit.ourmenu.ui.searchmenu.viewmodel.SearchMenuViewModel
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.utils.PermissionHandler
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SearchMenuScreen(
+ modifier: Modifier = Modifier,
+ viewModel: SearchMenuViewModel = hiltViewModel(),
+ onNavigateToMenuDetail: (Long) -> Unit
+) {
+
+ val scaffoldState = rememberBottomSheetScaffoldState()
+ var showBottomSheet by rememberSaveable { mutableStateOf(false) }
+ var dragHandleHeight by remember { mutableStateOf(0.dp) }
+ var bottomSheetPeekHeight by remember { mutableStateOf(0.dp) }
+ var showSearchBackground by rememberSaveable { mutableStateOf(false) }
+ var searchText by rememberSaveable { mutableStateOf("") }
+ var searchActionDone by rememberSaveable { mutableStateOf(false) }
+ val interactionSource = remember { MutableInteractionSource() }
+ val searchBarFocused by interactionSource.collectIsFocusedAsState()
+ val scope = rememberCoroutineScope()
+ val focusManager = LocalFocusManager.current
+ val context = LocalContext.current
+ val locationPermissionGranted by viewModel.locationPermissionGranted.collectAsStateWithLifecycle()
+
+ // ์ง๋ ์ค์ฌ ์ขํ
+ val currentCenter by viewModel.currentCenter.collectAsStateWithLifecycle()
+
+ // ๊ฒ์๊ธฐ๋ก
+ val searchHistory by viewModel.searchHistory.collectAsStateWithLifecycle()
+
+ // ํ ์์น์ ํด๋นํ๋ ๋ฉ๋ด๋ค
+ val menusOnPin by viewModel.menusOnPin.collectAsStateWithLifecycle()
+
+ val density = LocalDensity.current
+ val singleItemHeight = 300.dp // Fixed height for each item
+
+ LaunchedEffect(menusOnPin) {
+ if (menusOnPin != null && menusOnPin?.isNotEmpty() == true) {
+ showBottomSheet = true
+ }
+ }
+
+ // ๊ถํ ํ์ฉ์ด ์๋ ๊ฒฝ์ฐ ๊ถํ ์์ฒญ
+ if (!locationPermissionGranted) {
+ PermissionHandler(
+ permission = Manifest.permission.ACCESS_FINE_LOCATION,
+ rationaleMessage = "Location permission is required to show your current location on the map",
+ onPermissionGranted = {
+ viewModel.updateLocationPermission(true)
+ },
+ onPermissionDenied = {
+ viewModel.updateLocationPermission(false)
+ }
+ )
+ }
+
+ val mapView = mapViewWithLifecycle(
+ mapController = viewModel.mapController
+ ) { kakaoMap ->
+ // viewModel์์ kakaoMap์ ์ด๊ธฐํ
+ viewModel.initializeMap(kakaoMap, context)
+ }
+
+ // permission ์ฌ๋ถ ๋ณํ ๊ฒฝ์ฐ์ ๋ฐ์
+ LaunchedEffect(locationPermissionGranted) {
+ if (locationPermissionGranted) {
+ viewModel.mapController.kakaoMap.value?.let { kakaoMap ->
+ viewModel.initializeMap(kakaoMap, context)
+ }
+ }
+ }
+
+ LaunchedEffect(searchBarFocused) {
+ if (searchBarFocused) {
+ showSearchBackground = true
+ showBottomSheet = false
+
+ // ๊ฒ์ ๊ธฐ๋ก ์กฐํ
+ scope.launch {
+ viewModel.getSearchHistory()
+ Log.d("SearchMenuScreen", "๊ฒ์ ๊ธฐ๋ก ์กฐํ: $searchHistory")
+ }
+ }
+ }
+
+ BackHandler(enabled = showSearchBackground) {
+ if (searchBarFocused) focusManager.clearFocus()
+ searchActionDone = false
+ showSearchBackground = false
+ searchText = ""
+ }
+
+ BottomSheetScaffold(
+ scaffoldState = scaffoldState,
+ modifier = Modifier.fillMaxWidth(),
+ topBar = { OurMenuAddButtonTopAppBar() },
+ sheetContent = {
+ SearchBottomSheetContent(
+ modifier = Modifier.fillMaxWidth(),
+ dataList = menusOnPin ?: emptyList(),
+ onItemClick = { menuId ->
+ Log.d("SearchMenuScreen", "๋ฐํ
์ํธ ๋ฉ๋ด ์์ดํ
ํด๋ฆญ: $menuId")
+ onNavigateToMenuDetail(menuId)
+ }
+ )
+ },
+ sheetContainerColor = NeutralWhite,
+ sheetShape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp),
+ sheetDragHandle = {
+ BottomSheetDragHandle(
+ modifier = Modifier.onGloballyPositioned { coordinates ->
+ dragHandleHeight = density.run { coordinates.size.height.toDp() }
+ }
+ )
+ },
+ sheetPeekHeight = if(showBottomSheet) {
+ val itemCount = menusOnPin?.size ?: 0
+ (singleItemHeight * itemCount) + dragHandleHeight
+ } else 0.dp,
+ containerColor = NeutralWhite,
+ ) { innerPadding ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ ) {
+ if (!showSearchBackground) {
+ //์ง๋ ์ปดํฌ๋ํธ
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ AndroidView(
+ modifier = Modifier,
+ factory = { mapView }
+ )
+ }
+ } else {
+ SearchHistoryList(
+ historyList = searchHistory,
+ onClick = { menuId ->
+ // ํฌ๋กค๋ง ๊ธฐ๋ก ์์ดํ
ํด๋ฆญ์ ๋์
+ viewModel.getMapMenuDetail(menuId)
+ Log.d("SearchMenuScreen", "๊ฒ์ ๊ธฐ๋ก ์์ดํ
ํด๋ฆญ: $menuId")
+ showSearchBackground = false
+ showBottomSheet = true
+ }
+ )
+ }
+
+ SearchTextField(
+ modifier = Modifier.padding(top = 12.dp, start = 20.dp, end = 20.dp),
+ text = searchText,
+ onTextChange = {
+ searchText = it
+ showSearchBackground = true
+ showBottomSheet = false
+ },
+ placeHolder = R.string.search_place_holder,
+ interactionSource = interactionSource
+ ) {
+ // onSearch ํจ์
+ if (searchBarFocused) focusManager.clearFocus()
+ searchActionDone = true
+
+ // ๊ฒ์ ์ ํ์ฌ ์ง๋ ์ค์ฌ ์ขํ ์ฌ์ฉ
+ if (searchText.isNotEmpty()) {
+ // ๊ฒ์ ์ง์ ์ ํ์ฌ ์ง๋ ์ค์ฌ ์ขํ ์
๋ฐ์ดํธ
+ viewModel.updateCurrentCenter()
+
+ val center = viewModel.getCurrentCoordinates()
+ if (center != null) {
+ val (latitude, longitude) = center
+ Log.d("SearchMenuScreen", "๊ฒ์ ์์น: $latitude, $longitude")
+
+ // ๊ฒ์์ด์ ํ์ฌ ์ขํ๋ก ์คํ ์ด ์ ๋ณด ์์ฒญ
+ viewModel.getMapSearchResult(
+ query = searchText,
+ long = longitude,
+ lat = latitude
+ )
+
+ showBottomSheet = true
+ showSearchBackground = false
+ }
+ }
+ }
+
+ GoToMapButton(
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(bottom = 16.dp, end = 20.dp),
+ onClick = {
+ // TODO: ์์๋ก ์ค์ ํด๋์ ์นด๋ฉ๋ผ ์ด๋, ์ค์ ๋ก๋ ๋ค์ด๋ฒ ์ง๋์ ํด๋น ๊ฐ๊ฒ ๊ฒ์ ๊ฒฐ๊ณผ๋ก ์ด๋
+ viewModel.moveCamera(37.5416, 127.0793)
+ },
+ )
+ }
+ }
+
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SearchMenuScreenPreview() {
+ SearchMenuScreen(
+ ){
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/searchmenu/viewmodel/SearchMenuViewModel.kt b/app/src/main/java/com/kuit/ourmenu/ui/searchmenu/viewmodel/SearchMenuViewModel.kt
new file mode 100644
index 00000000..e719bc50
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/searchmenu/viewmodel/SearchMenuViewModel.kt
@@ -0,0 +1,351 @@
+package com.kuit.ourmenu.ui.searchmenu.viewmodel
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.util.Log
+import androidx.core.graphics.scale
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.google.android.gms.location.LocationServices
+import com.kakao.vectormap.KakaoMap
+import com.kakao.vectormap.LatLng
+import com.kakao.vectormap.camera.CameraUpdateFactory
+import com.kakao.vectormap.label.LabelOptions
+import com.kakao.vectormap.label.LabelStyle
+import com.kakao.vectormap.label.LabelStyles
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.data.model.map.response.MapDetailResponse
+import com.kuit.ourmenu.data.model.map.response.MapResponse
+import com.kuit.ourmenu.data.model.map.response.MapSearchHistoryResponse
+import com.kuit.ourmenu.data.model.map.response.MapSearchResponse
+import com.kuit.ourmenu.data.repository.MapRepository
+import com.kuit.ourmenu.ui.common.map.MapController
+import com.kuit.ourmenu.utils.PreferencesManager
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.net.URL
+import javax.inject.Inject
+
+@HiltViewModel
+class SearchMenuViewModel @Inject constructor(
+ private val mapRepository: MapRepository,
+ private val preferencesManager: PreferencesManager
+) : ViewModel() {
+ // ๊ฒ์ ๊ธฐ๋ก
+ private val _searchHistory = MutableStateFlow?>(emptyList())
+ val searchHistory = _searchHistory.asStateFlow()
+
+ // ์ ์ฒด ๋ฑ๋ก๋ ๋ฉ๋ด
+ private val _myMenus = MutableStateFlow?>(emptyList())
+ val myMenus = _myMenus.asStateFlow() // TODO: ๋ฆฌํฉํ ๋ง
+
+ // ๊ฒ์ ๊ฒฐ๊ณผ
+ private val _searchResult = MutableStateFlow?>(emptyList())
+ val searchResult = _searchResult.asStateFlow()
+
+ // ์์น์ ๋ฐ๋ฅธ ๋ฉ๋ด ์กฐํ
+ private val _menusOnPin = MutableStateFlow?>(emptyList())
+ val menusOnPin = _menusOnPin.asStateFlow()
+
+ // ํ๋ฉด์ ๋ณด์ด๋ ์ง๋ ์์ ์ค์ฌ์ ํด๋นํ๋ ์ขํ
+ private val _currentCenter = MutableStateFlow(null)
+ val currentCenter: StateFlow = _currentCenter
+
+ // ํ์ฌ ํ์ฑํ๋ ๋ผ๋ฒจ์ mapId๋ฅผ ์ถ์
+ private val _activeMapId = MutableStateFlow(null)
+ val activeMapId = _activeMapId.asStateFlow()
+
+ val mapController = MapController()
+
+ // Permission state
+ private val _locationPermissionGranted = MutableStateFlow(false)
+ val locationPermissionGranted: StateFlow = _locationPermissionGranted.asStateFlow()
+
+ init {
+ viewModelScope.launch {
+ preferencesManager.locationPermissionGranted.collect { granted ->
+ _locationPermissionGranted.value = granted
+ Log.d("AddMenuViewModel", "location permission : $granted")
+ }
+ }
+ }
+
+ fun updateLocationPermission(granted: Boolean) {
+ viewModelScope.launch {
+ preferencesManager.setLocationPermissionGranted(granted)
+ }
+ }
+
+ // ์ง๋ ์ด๊ธฐํ
+ @SuppressLint("MissingPermission")
+ fun initializeMap(kakaoMap: KakaoMap, context: Context) {
+ // Initial map setup
+ // Get current location and move camera
+ Log.d("AddMenuViewModel", "initialize Map")
+ val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
+ fusedLocationClient.lastLocation.addOnSuccessListener { location ->
+ location?.let {
+ Log.d("AddMenuViewModel", "location success: lat=${it.latitude}, long=${it.longitude}")
+ moveCamera(it.latitude, it.longitude)
+// addMarker(it.latitude, it.longitude, R.drawable.img_popup_dice)
+ } ?: run {
+ Log.d("AddMenuViewModel", "location fail")
+ moveCamera(37.5416, 127.0793)
+// addMarker(37.5416, 127.0793, R.drawable.img_popup_dice)
+ }
+ }
+// moveCamera(37.5416, 127.0793)
+ getMyMenus()
+// showSearchResultOnMap()
+ }
+
+ // ์ง๋์์์ ํ๋ฉด ์ด๋
+ fun moveCamera(latitude: Double, longitude: Double) {
+ mapController.kakaoMap.value?.let { map ->
+ val cameraUpdate =
+ CameraUpdateFactory.newCenterPosition(LatLng.from(latitude, longitude))
+ map.moveCamera(cameraUpdate)
+ updateCurrentCenter()
+ }
+ }
+
+ // ์ง๋ ์ค์ ์ขํ ์
๋ฐ์ดํธ
+ fun updateCurrentCenter() {
+ viewModelScope.launch {
+ mapController.kakaoMap.value?.let { map ->
+ val center = map.cameraPosition?.position
+ _currentCenter.value = center
+ if (center != null) {
+ Log.d(
+ "SearchMenuViewModel",
+ "ํ์ฌ ์ง๋ ์ค์ฌ ์ขํ: ${center.latitude}, ${center.longitude}"
+ )
+ }
+ }
+ }
+ }
+
+ // Get the current center coordinates as a Pair
+ fun getCurrentCoordinates(): Pair? {
+ return currentCenter.value?.let {
+ Pair(it.latitude, it.longitude)
+ }
+ }
+
+ // URL์์ ์ด๋ฏธ์ง๋ฅผ ๋นํธ๋งต์ผ๋ก ๋ก๋ํ๋ ํจ์
+ private suspend fun loadImageFromUrl(url: String): Bitmap? = withContext(Dispatchers.IO) {
+ return@withContext try {
+ val connection = URL(url).openConnection()
+ connection.connectTimeout = 5000
+ connection.readTimeout = 5000
+ connection.connect()
+
+ val input = connection.getInputStream()
+ // ์๋ณธ ๋นํธ๋งต ๋ก๋
+ val originalBitmap = BitmapFactory.decodeStream(input)
+
+ // ์ํ๋ ํฌ๊ธฐ๋ก ๋ฆฌ์ฌ์ด์ง (์: ์๋ณธ์ 2๋ฐฐ)
+ originalBitmap?.let { bitmap ->
+ val width = bitmap.width * 2 // ์๋ณธ ๋๋น์ 2๋ฐฐ
+ val height = bitmap.height * 2 // ์๋ณธ ๋์ด์ 2๋ฐฐ
+ bitmap.scale(width, height).also {
+ // ์๋ณธ ๋นํธ๋งต ๋ฉ๋ชจ๋ฆฌ ํด์
+ if (it != bitmap) {
+ bitmap.recycle()
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Log.e("SearchMenuViewModel", "์ด๋ฏธ์ง ๋ก๋ ์คํจ: ${e.message}")
+ null
+ }
+ }
+
+ // ์ง๋์ ํ ์ถ๊ฐ
+ private fun addMarker(store: MapResponse, isActive: Boolean = false) {
+ viewModelScope.launch {
+ val imageUrl = if (isActive) store.menuPinImgUrl else store.menuPinDisableImgUrl
+ val bitmap = loadImageFromUrl(imageUrl)
+
+ mapController.kakaoMap.value?.let { map ->
+ val style = if (bitmap != null) {
+ map.labelManager?.addLabelStyles(
+ LabelStyles.from(
+ LabelStyle.from(bitmap).apply {
+ isApplyDpScale = true
+ setPadding(0f)
+ setAnchorPoint(0.5f, 0.5f)
+ }
+ )
+ )
+ } else {
+ // ์ด๋ฏธ์ง ๋ก๋ ์คํจ์ ๊ธฐ๋ณธ ์ด๋ฏธ์ง ์ฌ์ฉ
+ map.labelManager?.addLabelStyles(
+ LabelStyles.from(
+ LabelStyle.from(R.drawable.img_popup_dice).apply {
+ isApplyDpScale = true
+ setPadding(0f)
+ setAnchorPoint(0.5f, 0.5f)
+ }
+ )
+ )
+ }
+
+ val options = LabelOptions.from(LatLng.from(store.mapY, store.mapX)).setStyles(style)
+ .setClickable(true)
+ map.labelManager?.layer?.addLabel(options)
+ map.setOnLabelClickListener { kakaoMap, labelLayer, label ->
+ // ํ ํด๋ฆญ์ ๋์ ์ ์
+ Log.d("SearchMenuViewModel", "ํ ํด๋ฆญ๋จ")
+ moveCamera(latitude = label.position.latitude, longitude = label.position.longitude)
+
+ // Find the matching menu item and call getMapDetail
+ _myMenus.value?.find { menu ->
+ menu.mapY == label.position.latitude && menu.mapX == label.position.longitude
+ }?.let { matchingMenu ->
+ Log.d("SearchMenuViewModel", "ํ ํด๋ฆญ๋ ๋ฉ๋ด: ${matchingMenu.mapId}")
+ // ํ์ฌ ํ์ฑํ๋ mapId ์
๋ฐ์ดํธ
+ _activeMapId.value = matchingMenu.mapId
+ // ๋ชจ๋ ๋ง์ปค ๋ค์ ๊ทธ๋ฆฌ๊ธฐ
+ refreshMarkers()
+ getMapDetail(matchingMenu.mapId)
+ }
+
+ true
+ }
+ }
+ }
+ }
+
+ // ๋ชจ๋ ๋ง์ปค ๋ค์ ๊ทธ๋ฆฌ๊ธฐ
+ private fun refreshMarkers() {
+ clearMarkers()
+ myMenus.value?.forEach { store ->
+ addMarker(store, store.mapId == _activeMapId.value)
+ }
+ }
+
+ // ์ง๋์ ์ ์ฒด ํ ์ ๊ฑฐ
+ fun clearMarkers() {
+ mapController.kakaoMap.value?.let { map ->
+ map.labelManager?.layer?.removeAll()
+ }
+ }
+
+ fun getSearchHistory() {
+ viewModelScope.launch {
+ val response = mapRepository.getMapSearchHistory()
+ response.onSuccess {
+ _searchHistory.value = it
+ Log.d("SearchMenuViewModel", "๊ฒ์ ๊ธฐ๋ก ์กฐํ ์ฑ๊ณต")
+ }.onFailure {
+ // Handle error
+ Log.d("SearchMenuViewModel", "๊ฒ์ ๊ธฐ๋ก ์กฐํ ์คํจ : ${it.message}")
+ }
+ }
+ }
+
+ fun getMapSearchResult(
+ query: String,
+ long: Double,
+ lat: Double
+ ) {
+ viewModelScope.launch {
+ Log.d("SearchMenuViewModel", "๋ฑ๋ก ๋ฉ๋ด ์ ๋ณด ์์ฒญ: $query, ์ขํ($lat, $long)")
+
+ val response = mapRepository.getMapSearch(
+ title = query,
+ longitude = long,
+ latitude = lat
+ )
+
+ response.onSuccess { result ->
+ if (result != null && result.isNotEmpty()) {
+ Log.d("SearchMenuViewModel", "๋ฑ๋ก ๋ฉ๋ด ์ ๋ณด ์กฐํ ์ฑ๊ณต: ${result.size}๊ฐ")
+ // ๊ฒ์ ๊ฒฐ๊ณผ ์ ์ฅ
+ _searchResult.value = result
+ }
+ }.onFailure {
+ Log.d("SearchMenuViewModel", "๋ฑ๋ก ๋ฉ๋ด ์ ๋ณด ์กฐํ ์คํจ: ${it.message}")
+ }
+ }
+ }
+
+ // ๋ฑ๋กํ ์ ์ฒด ๋ฉ๋ด ์กฐํ(๋น ๊ฒ์์ ์ํ?)
+ fun getMyMenus() {
+ viewModelScope.launch {
+ val response = mapRepository.getMap()
+ response.onSuccess {
+ if (it != null) {
+ _myMenus.value = it
+ Log.d("SearchMenuViewModel", "๋ด ๋ฉ๋ด ์กฐํ ์ฑ๊ณต: ${it.size}๊ฐ")
+ showSearchResultOnMap()
+ } else {
+ Log.d("SearchMenuViewModel", "๋ด ๋ฉ๋ด ์กฐํ ์คํจ: null")
+ }
+ }.onFailure {
+ Log.d("SearchMenuViewModel", "๋ด ๋ฉ๋ด ์กฐํ ์คํจ: ${it.message}")
+ }
+ }
+ }
+
+ // mapId์ ์์นํ๋ ๋ฉ๋ด ์กฐํ(๋ฉ๋ดํ ๋๋ฅธ ๊ฒฝ์ฐ)
+ fun getMapDetail(mapId: Long) {
+ viewModelScope.launch {
+ val response = mapRepository.getMapDetail(mapId)
+ response.onSuccess {
+ if (it != null) {
+ _menusOnPin.value = it
+ Log.d("SearchMenuViewModel", "ํ ์์น์ ๋ฉ๋ด ์กฐํ ์ฑ๊ณต: $it")
+ } else {
+ Log.d("SearchMenuViewModel", "ํ ์์น์ ๋ฉ๋ด ์กฐํ ์คํจ: null")
+ }
+ }.onFailure {
+ Log.d("SearchMenuViewModel", "ํ ์์น์ ๋ฉ๋ด ์กฐํ ์คํจ: ${it.message}")
+ }
+ }
+ }
+
+ fun getMapMenuDetail(menuId: Long) {
+ viewModelScope.launch {
+ val response = mapRepository.getMapMenuDetail(menuId)
+ response.onSuccess { menuDetail ->
+ Log.d("SearchMenuViewModel", "๋ฉ๋ด ์์ธ ์กฐํ ์ฑ๊ณต: $menuDetail")
+
+ // myMenus์์ ํด๋น menuId๋ฅผ ๊ฐ์ง ๋ฉ๋ด์ ์์น ์ ๋ณด ์ฐพ๊ธฐ
+ myMenus.value?.find { it.mapId == menuId }?.let { menu ->
+ // ํด๋น ์์น๋ก ์นด๋ฉ๋ผ ์ด๋
+ moveCamera(menu.mapY, menu.mapX)
+ // ํด๋น ํ์ ํ์ฑํ ์ํ๋ก ๋ณ๊ฒฝ
+ _activeMapId.value = menuId
+ refreshMarkers()
+ // ๋ฉ๋ด ์์ธ ์ ๋ณด๋ฅผ ๋ฐํ
์ํธ์ ํ์ํ๊ธฐ ์ํด ์ค์
+ getMapDetail(menuId)
+ }
+ }.onFailure {
+ Log.d("SearchMenuViewModel", "๋ฉ๋ด ์์ธ ์กฐํ ์คํจ: ${it.message}")
+ }
+ }
+ }
+
+ // ์ง๋์ ๊ฒ์ ๊ฒฐ๊ณผ ํ ์ถ๊ฐ
+ fun showSearchResultOnMap() {
+ clearMarkers()
+ myMenus.value?.forEach { store ->
+ addMarker(store, store.mapId == _activeMapId.value)
+ Log.d(
+ "SearchMenuViewModel",
+ "mapId: ${store.mapId} lat: (${store.mapY}, long: ${store.mapX})"
+ )
+ }
+ // ์ฒซ ๋ฒ์งธ ๊ฒ์ ๊ฒฐ๊ณผ๋ก ์นด๋ฉ๋ผ ์ด๋ TODO: ํ์ฌ ์์น๋ ๊ฐ๊น์ด ๊ฒฐ๊ณผ๋ก ์ด๋
+ myMenus.value?.get(0)?.let { moveCamera(it.mapY, it.mapX) }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/signup/component/EmailSpinner.kt b/app/src/main/java/com/kuit/ourmenu/ui/signup/component/EmailSpinner.kt
new file mode 100644
index 00000000..cb389560
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/signup/component/EmailSpinner.kt
@@ -0,0 +1,187 @@
+package com.kuit.ourmenu.ui.signup.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+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.lazy.LazyColumn
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+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.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.common.CustomTextField
+import com.kuit.ourmenu.ui.theme.Neutral100
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral700
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import com.kuit.ourmenu.utils.ViewUtil.noRippleClickable
+
+@Composable
+fun EmailSpinner(
+ modifier: Modifier = Modifier,
+ domain : String = "",
+ onDomainChange : (String) -> Unit = {}
+) {
+ var expanded by remember { mutableStateOf(false) }
+ var enable by remember { mutableStateOf(false) }
+ val domains =
+ listOf(
+ stringResource(R.string.email_custom),
+ stringResource(R.string.email_daum),
+ stringResource(R.string.email_gmail),
+ stringResource(R.string.email_kakao),
+ stringResource(R.string.email_nate),
+ stringResource(R.string.email_naver),
+ )
+ var isFocused by remember { mutableStateOf(false) }
+
+ Box(
+ modifier = modifier
+ .width(122.dp)
+ .clip(RoundedCornerShape(8.dp))
+ ) {
+ Column {
+ CustomTextField(
+ enabled = enable,
+ containerColor = Neutral100,
+ modifier = modifier
+ .fillMaxWidth()
+ .height(44.dp)
+ .border(
+ width = 1.dp,
+ color = if (isFocused) Neutral500 else Neutral300,
+ shape = RoundedCornerShape(8.dp)
+ )
+ .clip(RoundedCornerShape(8.dp))
+ .clickable { expanded = !expanded }
+ .onFocusChanged { focusState ->
+ isFocused = focusState.isFocused
+ },
+ paddingValues = PaddingValues(start = 17.5.dp),
+ text = domain,
+ textStyle = ourMenuTypography().pretendard_700_12.copy(color = Neutral700),
+ onTextChange = onDomainChange,
+ shape = RoundedCornerShape(8.dp),
+ placeHolder = {
+ Text(
+ text = stringResource(R.string.placeholder_email_domain),
+ style = ourMenuTypography().pretendard_700_12.copy(color = Neutral500)
+ )
+ },
+ cursorColor = Primary500Main
+ )
+ if (expanded) {
+ LazyColumn(
+ modifier = Modifier
+ .width(122.dp)
+ .padding(top = 8.dp)
+ .height(44.dp * domains.size)
+ .clip(RoundedCornerShape(8.dp))
+ .border(1.dp, Neutral300, RoundedCornerShape(8.dp))
+ .background(Neutral100),
+ verticalArrangement = Arrangement.spacedBy(0.dp),
+ ) {
+ items(domains.size) { index ->
+ Column {
+ DomainItem(
+ modifier = Modifier
+ .width(122.dp)
+ .height(44.dp)
+ .clip(RoundedCornerShape(8.dp))
+ .clickable {
+ if (index == 0) {
+ enable = true
+ onDomainChange("")
+ } else {
+ enable = false
+ onDomainChange(domains[index])
+ }
+ expanded = false
+ }
+ .padding(horizontal = 17.5.dp),
+ domain = domains[index]
+ )
+ }
+ }
+ }
+ }
+ }
+ if (!enable || domain.isEmpty())
+ Icon(
+ painter =
+ if (expanded) painterResource(R.drawable.ic_expand_up)
+ else painterResource(R.drawable.ic_expand_down),
+ contentDescription = null,
+ modifier = Modifier
+ .align(Alignment.TopEnd)
+ .padding(end = 17.5.dp, top = 16.dp)
+ .noRippleClickable { expanded = !expanded },
+ tint = Neutral500
+ )
+ }
+}
+
+@Composable
+fun DomainItem(
+ modifier: Modifier = Modifier,
+ domain: String,
+) {
+ Column(
+ modifier = modifier,
+ ) {
+ HorizontalDivider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 11.5.dp),
+ color = Neutral300,
+ thickness = 1.dp
+ )
+
+ Text(
+ text = domain,
+ textAlign = TextAlign.Justify,
+ lineHeight = 18.sp,
+ style = ourMenuTypography().pretendard_700_12.copy(color = Neutral500)
+ )
+ }
+}
+
+
+@Preview(showBackground = true)
+@Composable
+fun EmailSpinnerPreview() {
+ Box(
+ modifier = Modifier
+ .width(375.dp)
+ .height(812.dp)
+ ) {
+ EmailSpinner(
+ modifier = Modifier.align(Alignment.TopCenter)
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/signup/component/VerifyCodeTextField.kt b/app/src/main/java/com/kuit/ourmenu/ui/signup/component/VerifyCodeTextField.kt
new file mode 100644
index 00000000..e62fb564
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/signup/component/VerifyCodeTextField.kt
@@ -0,0 +1,159 @@
+package com.kuit.ourmenu.ui.signup.component
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.KeyEventType
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.input.key.type
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.kuit.ourmenu.ui.common.CustomTextField
+import com.kuit.ourmenu.ui.theme.Neutral100
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.Primary100
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun VerifyCodeTextField(
+ modifier: Modifier = Modifier,
+ error: Boolean = false,
+ input: String,
+ onTextChange: (String) -> Unit,
+ onNext: () -> Unit, // ๋ค์ ์นธ์ผ๋ก ์ด๋ํ๋ ์ฝ๋ฐฑ
+ onPrevious: () -> Unit = {}, // ์ด์ ์นธ์ผ๋ก ์ด๋ํ๋ ์ฝ๋ฐฑ
+ focusRequester: FocusRequester,
+) {
+
+ var isFocused by remember { mutableStateOf(false) }
+
+ CustomTextField(
+ modifier = modifier
+ .focusRequester(focusRequester)
+ .width(36.dp)
+ .height(44.dp)
+ .border(
+ width = 1.dp,
+ color =
+ if (error) Primary500Main
+ else if (isFocused) Neutral500
+ else Neutral300,
+ shape = RoundedCornerShape(8.dp)
+ )
+ .onFocusChanged { focusState ->
+ isFocused = focusState.isFocused
+ }
+ .onKeyEvent { event ->
+ if (event.type == KeyEventType.KeyDown && event.key == Key.Backspace) {
+ onPrevious() // ์ด์ ์นธ์ผ๋ก ์ด๋
+ true // ์ด๋ฒคํธ ์ฒ๋ฆฌ ์๋ฃ
+ } else {
+ false // ๋ค๋ฅธ ํค๋ ์ฒ๋ฆฌํ์ง ์์
+ }
+ },
+ text = input,
+ onTextChange = { newText ->
+// if (newText.isDigitsOnly()) {
+ when (newText.length) {
+ 0 -> {
+ onTextChange(newText)
+ onPrevious()
+ }
+
+ 1 -> {
+ onTextChange(newText)
+ onNext()
+ }
+
+ 2 -> {
+ onTextChange(newText.last().toString())
+ onNext()
+ }
+
+ }
+// }
+ },
+ shape = RoundedCornerShape(8.dp),
+ paddingValues = PaddingValues(12.dp, 12.dp),
+ containerColor = if (error) Primary100 else Neutral100,
+ textStyle = ourMenuTypography().pretendard_700_16.copy(
+ color = Neutral900,
+ textAlign = TextAlign.Center
+ ),
+// keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ cursorColor = Primary500Main,
+ colors = TextFieldDefaults.colors(
+ focusedContainerColor = if (error) Primary100 else Neutral100,
+ unfocusedContainerColor = Neutral100,
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent,
+ disabledContainerColor = Neutral100,
+ disabledIndicatorColor = Color.Transparent,
+ )
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun VerifyCodeTextFieldPreview() {
+ val focusRequesters = List(6) { FocusRequester() }
+ val codes: SnapshotStateList = remember { mutableStateListOf("", "", "", "", "", "") }
+
+ Row(
+ modifier = Modifier.fillMaxSize(),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ for (i in 0 until 6) {
+ VerifyCodeTextField(
+ input = "1",
+ onTextChange = { newText ->
+ if (newText.length <= 1) {
+ codes[i] = newText // Compose์์ ์ํ ๋ณ๊ฒฝ ๊ฐ์ง
+ }
+ },
+ onNext = {
+ if (i < 5) {
+ focusRequesters[i + 1].requestFocus()
+ }
+ },
+ modifier = Modifier.then(
+ if (i == 0 && codes[i].isEmpty()) {
+ Modifier.focusRequester(focusRequesters[i])
+ } else Modifier
+ ),
+ focusRequester = focusRequesters[i],
+ )
+ if (i < 5) {
+ Spacer(modifier = Modifier.size(8.dp))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/signup/model/SignupState.kt b/app/src/main/java/com/kuit/ourmenu/ui/signup/model/SignupState.kt
new file mode 100644
index 00000000..762019bb
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/signup/model/SignupState.kt
@@ -0,0 +1,8 @@
+package com.kuit.ourmenu.ui.signup.model
+
+sealed class SignupState {
+ data object Default : SignupState()
+ data object Loading : SignupState()
+ data object Success : SignupState()
+ data object Error : SignupState()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/signup/navigation/SignupNavigation.kt b/app/src/main/java/com/kuit/ourmenu/ui/signup/navigation/SignupNavigation.kt
new file mode 100644
index 00000000..18466a78
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/signup/navigation/SignupNavigation.kt
@@ -0,0 +1,68 @@
+package com.kuit.ourmenu.ui.signup.navigation
+
+import androidx.compose.runtime.Composable
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.compose.composable
+import com.kuit.ourmenu.ui.navigator.Routes
+import com.kuit.ourmenu.ui.signup.screen.SignupEmailRoute
+import com.kuit.ourmenu.ui.signup.screen.SignupMealTimeRoute
+import com.kuit.ourmenu.ui.signup.screen.SignupPasswordRoute
+import com.kuit.ourmenu.ui.signup.screen.SignupVerifyRoute
+import com.kuit.ourmenu.ui.signup.viewmodel.SignupViewModel
+
+fun NavController.navigateToSignupEmail() {
+ navigate(Routes.SignupEmail)
+}
+
+fun NavController.navigateToSignupPassword() {
+ navigate(Routes.SignupPassword)
+}
+
+fun NavController.navigateToSignupMealTime() {
+ navigate(Routes.SignupMealTime)
+}
+
+fun NavController.navigateToSignupVerify() {
+ navigate(Routes.SignupVerify)
+}
+
+
+fun NavGraphBuilder.signupNavGraph(
+ navigateBack: () -> Unit,
+ navigateOnboardingToHome: () -> Unit,
+ navigateToSignupVerify: () -> Unit,
+ navigateToSignupMealTime: () -> Unit,
+ navigateToSignupPassword: () -> Unit,
+ getSignupViewModel: @Composable (NavBackStackEntry) -> SignupViewModel
+) {
+ composable {
+ SignupEmailRoute(
+ navigateToVerify = navigateToSignupVerify,
+ navigateBack = navigateBack,
+ viewModel = getSignupViewModel(it)
+ )
+ }
+ composable {
+ SignupVerifyRoute(
+ navigateToPassword = navigateToSignupPassword,
+ navigateBack = navigateBack,
+ viewModel = getSignupViewModel(it)
+ )
+ }
+ composable {
+ SignupPasswordRoute(
+ navigateToMealTime = navigateToSignupMealTime,
+ navigateBack = navigateBack,
+ viewModel = getSignupViewModel(it)
+ )
+ }
+ composable {
+ SignupMealTimeRoute(
+ navigateToHome = navigateOnboardingToHome,
+ navigateBack = navigateBack,
+ viewModel = getSignupViewModel(it)
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/signup/screen/SignupEmailScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/signup/screen/SignupEmailScreen.kt
new file mode 100644
index 00000000..77d08b38
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/signup/screen/SignupEmailScreen.kt
@@ -0,0 +1,220 @@
+package com.kuit.ourmenu.ui.signup.screen
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.common.DisableBottomFullWidthButton
+import com.kuit.ourmenu.ui.common.LoginTextField
+import com.kuit.ourmenu.ui.common.OurSnackbarHost
+import com.kuit.ourmenu.ui.common.topappbar.OnboardingTopAppBar
+import com.kuit.ourmenu.ui.signup.component.EmailSpinner
+import com.kuit.ourmenu.ui.signup.model.SignupState
+import com.kuit.ourmenu.ui.signup.uistate.SignupUiState
+import com.kuit.ourmenu.ui.signup.viewmodel.SignupViewModel
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import kotlinx.coroutines.launch
+
+@Composable
+fun SignupEmailRoute(
+ navigateToVerify: () -> Unit,
+ navigateBack: () -> Unit,
+ viewModel: SignupViewModel = hiltViewModel()
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ val snackbarHostState = remember { SnackbarHostState() }
+ val scope = rememberCoroutineScope()
+ val enable by remember {
+ derivedStateOf { uiState.email.isNotEmpty() && uiState.domain.isNotEmpty() }
+ }
+ val shakeOffset = remember { Animatable(0f) }
+
+ LaunchedEffect(uiState.emailState) {
+ when (uiState.emailState) {
+ is SignupState.Success -> navigateToVerify()
+ is SignupState.Error -> {
+ scope.launch {
+ snackbarHostState.showSnackbar(
+ message = uiState.error,
+ duration = SnackbarDuration.Short
+ )
+ }
+ /*
+ TODO : Error Response ์ ๋ฐ๋ผ์ ๊ด๋ฆฌ๋ฅผ ํด์ผํจ.
+ scope.launch {
+ shakeAnimation(
+ offset = shakeOffset,
+ coroutineScope = scope
+ )
+ }
+ */
+ }
+
+ else -> {}
+ }
+ }
+
+ SignupEmailScreen(
+ navigateBack = navigateBack,
+ uiState = uiState,
+ enable = enable,
+ shakeOffset = shakeOffset,
+ sendEmail = viewModel::sendEmail,
+ updateEmail = viewModel::updateEmail,
+ updateDomain = viewModel::updateDomain
+ )
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(top = 44.dp),
+ contentAlignment = Alignment.TopCenter
+ ) {
+ OurSnackbarHost(
+ hostState = snackbarHostState
+ )
+ }
+
+}
+
+@Composable
+fun SignupEmailScreen(
+ navigateBack: () -> Unit,
+ uiState: SignupUiState,
+ shakeOffset: Animatable,
+ enable: Boolean = true,
+ sendEmail: () -> Unit = {},
+ updateEmail: (String) -> Unit,
+ updateDomain: (String) -> Unit
+) {
+
+ Scaffold(
+ topBar = {
+ OnboardingTopAppBar(
+ onBackClick = navigateBack
+ )
+ },
+ modifier = Modifier
+ .fillMaxSize()
+ .imePadding()
+ ) { innerPadding ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .padding(horizontal = 20.dp)
+ ) {
+ DisableBottomFullWidthButton(
+ enable = enable,
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(bottom = 20.dp),
+ text = stringResource(R.string.send_auth_mail)
+ ) { sendEmail() }
+
+ Column(
+ modifier = Modifier
+ .padding(top = 92.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.input_email),
+ style = ourMenuTypography().pretendard_600_24,
+ color = Neutral900,
+ modifier = Modifier
+ )
+
+ Text(
+ text = stringResource(R.string.input_email_description),
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral500,
+ modifier = Modifier.padding(top = 4.dp),
+ )
+
+ EmailInputField(
+ modifier = Modifier
+ .padding(top = 12.dp)
+// .offset { IntOffset(shakeOffset.value.roundToInt(), 0) }
+ ,
+ email = uiState.email,
+ onEmailChange = { updateEmail(it) },
+ domain = uiState.domain,
+ onDomainChange = { updateDomain(it) }
+ )
+ }
+ }
+ }
+}
+
+
+@Composable
+fun EmailInputField(
+ modifier: Modifier = Modifier,
+ email: String,
+ onEmailChange: (String) -> Unit,
+ domain: String,
+ onDomainChange: (String) -> Unit = {},
+) {
+
+
+ Row(
+ modifier = modifier,
+ ) {
+ LoginTextField(
+ modifier = Modifier.weight(1f),
+ placeholder = stringResource(R.string.placeholder_default),
+ input = email,
+ onTextChange = onEmailChange,
+ )
+
+ Text(
+ text = "@",
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral500,
+ modifier = Modifier
+ .padding(horizontal = 6.dp)
+ .padding(top = 11.5.dp)
+ .align(Alignment.Top),
+ )
+
+ EmailSpinner(
+ domain = domain,
+ onDomainChange = onDomainChange
+ )
+ }
+}
+
+@Preview
+@Composable
+private fun SignupEmailScreenPreview() {
+ SignupEmailScreen(
+ navigateBack = { },
+ uiState = SignupUiState(),
+ shakeOffset = remember { Animatable(0f) },
+ updateEmail = { },
+ updateDomain = { }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/signup/screen/SignupMealTimeScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/signup/screen/SignupMealTimeScreen.kt
new file mode 100644
index 00000000..c564faaf
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/signup/screen/SignupMealTimeScreen.kt
@@ -0,0 +1,145 @@
+package com.kuit.ourmenu.ui.signup.screen
+
+import android.util.Log
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.common.DisableBottomFullWidthButton
+import com.kuit.ourmenu.ui.common.topappbar.OnboardingTopAppBar
+import com.kuit.ourmenu.ui.common.MealTimeGrid
+import com.kuit.ourmenu.ui.signup.model.SignupState
+import com.kuit.ourmenu.ui.signup.uistate.SignupUiState
+import com.kuit.ourmenu.ui.signup.viewmodel.SignupViewModel
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+
+@Composable
+fun SignupMealTimeRoute(
+ navigateToHome: () -> Unit,
+ navigateBack: () -> Unit,
+ viewModel: SignupViewModel = hiltViewModel()
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ val enable = remember { derivedStateOf { uiState.selectedTimes.isNotEmpty() } }
+
+ LaunchedEffect(uiState.signupState) {
+ when (uiState.signupState) {
+ is SignupState.Success -> {
+ navigateToHome()
+ }
+
+ is SignupState.Error ->
+ Log.e("SignupVerifyScreen", uiState.error)
+
+ else -> {}
+ }
+ }
+
+ SignupMealTimeScreen(
+ navigateBack = navigateBack,
+ uiState = uiState,
+ enable = enable.value,
+ updateSelectedTime = viewModel::updateSelectedTime,
+ signup = viewModel::signup
+ )
+
+}
+
+@Composable
+fun SignupMealTimeScreen(
+ navigateBack: () -> Unit,
+ uiState: SignupUiState,
+ enable: Boolean,
+ updateSelectedTime: (Int) -> Unit,
+ signup: () -> Unit,
+) {
+ Scaffold(
+ topBar = {
+ OnboardingTopAppBar(
+ onBackClick = navigateBack
+ )
+ },
+ modifier = Modifier
+ .fillMaxSize()
+ .imePadding()
+ ) { innerPadding ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .padding(horizontal = 20.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(top = 92.dp)
+ ) {
+ Text(
+ text = stringResource(R.string.input_meal_time),
+ style = ourMenuTypography().pretendard_600_24,
+ color = Neutral900,
+ modifier = Modifier
+ )
+
+ Text(
+ text = stringResource(R.string.input_meal_time_description),
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral500,
+ modifier = Modifier.padding(top = 4.dp),
+ )
+
+ MealTimeGrid(
+ modifier = Modifier.padding(top = 29.dp),
+ mealTimes = uiState.mealTimes,
+ updateSelectedTime = { index ->
+ updateSelectedTime(index)
+ },
+ )
+ }
+
+ DisableBottomFullWidthButton(
+ enable = enable,
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(bottom = 20.dp),
+ text = stringResource(R.string.confirm)
+ ) {
+ Log.d("okhttp", "SignupMealTimeScreen")
+ signup()
+ }
+
+ }
+ }
+}
+
+@Preview(
+ showBackground = true,
+ widthDp = 360
+)
+@Composable
+private fun SignupMealTimeScreenPreview() {
+ SignupMealTimeScreen(
+ navigateBack = {},
+ uiState = SignupUiState(),
+ enable = true,
+ updateSelectedTime = {},
+ signup = { },
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/signup/screen/SignupPasswordScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/signup/screen/SignupPasswordScreen.kt
new file mode 100644
index 00000000..b15e02b4
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/signup/screen/SignupPasswordScreen.kt
@@ -0,0 +1,292 @@
+package com.kuit.ourmenu.ui.signup.screen
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.imePadding
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.CheckboxDefaults
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.common.DisableBottomFullWidthButton
+import com.kuit.ourmenu.ui.common.LoginTextField
+import com.kuit.ourmenu.ui.common.OurSnackbarHost
+import com.kuit.ourmenu.ui.common.topappbar.OnboardingTopAppBar
+import com.kuit.ourmenu.ui.common.model.PasswordState
+import com.kuit.ourmenu.ui.common.model.checkPassword
+import com.kuit.ourmenu.ui.signup.uistate.SignupUiState
+import com.kuit.ourmenu.ui.signup.viewmodel.SignupViewModel
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import com.kuit.ourmenu.utils.AnimationUtil.shakeErrorInputField
+import com.kuit.ourmenu.utils.AnimationUtil.shakeErrorInputFieldWithFocus
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlin.math.roundToInt
+
+@Composable
+fun SignupPasswordRoute(
+ navigateToMealTime: () -> Unit,
+ navigateBack: () -> Unit,
+ viewModel: SignupViewModel = hiltViewModel()
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ val enable = remember {
+ derivedStateOf { uiState.password.isNotEmpty() && uiState.confirmPassword.isNotEmpty() }
+ }
+
+ val focusRequester = remember { FocusRequester() }
+ val snackbarHostState = remember { SnackbarHostState() }
+ val scope = rememberCoroutineScope()
+ val shakeOffset = remember { Animatable(0f) }
+
+ LaunchedEffect(uiState.passwordState) {
+ when (uiState.passwordState) {
+ PasswordState.NotMeetCondition -> {
+ shakeErrorInputFieldWithFocus(
+ shakeOffset = shakeOffset,
+ focusRequester = focusRequester,
+ message = "๋น๋ฐ๋ฒํธ ์กฐ๊ฑด์ ๋ค์ ํ์ธํด์ฃผ์ธ์.",
+ snackbarHostState = snackbarHostState,
+ scope = scope
+ )
+ scope.launch {
+ delay(1000)
+ viewModel.updatePasswordState(PasswordState.Default)
+ }
+ }
+
+ PasswordState.DifferentPassword -> {
+ shakeErrorInputField(
+ shakeOffset = shakeOffset,
+ message = "๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์์.",
+ snackbarHostState = snackbarHostState,
+ scope = scope
+ )
+ scope.launch {
+ delay(800)
+ viewModel.updatePasswordState(PasswordState.Default)
+ }
+ }
+
+ PasswordState.Valid -> {
+ navigateToMealTime()
+ }
+
+ else -> {}
+ }
+ }
+
+ SignupPasswordScreen(
+ navigateBack = navigateBack,
+ enable = enable.value,
+ uiState = uiState,
+ focusRequester = focusRequester,
+ shakeOffset = shakeOffset,
+ setPasswordVisibility = viewModel::setPasswordVisibility,
+ updatePasswordState = viewModel::updatePasswordState,
+ updatePassword = viewModel::updatePassword,
+ updateConfirmPassword = viewModel::updateConfirmPassword
+ )
+
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(top = 44.dp),
+ contentAlignment = Alignment.TopCenter
+ ) {
+ OurSnackbarHost(
+ hostState = snackbarHostState
+ )
+ }
+
+}
+
+@Composable
+fun SignupPasswordScreen(
+ navigateBack: () -> Unit,
+ enable: Boolean,
+ uiState: SignupUiState,
+ focusRequester: FocusRequester,
+ shakeOffset: Animatable,
+ setPasswordVisibility: (Boolean) -> Unit,
+ updatePasswordState: (PasswordState) -> Unit,
+ updatePassword: (String) -> Unit,
+ updateConfirmPassword: (String) -> Unit,
+) {
+ val shakingModifier = Modifier.offset { IntOffset(shakeOffset.value.roundToInt(), 0) }
+
+ Scaffold(
+ modifier = Modifier
+ .fillMaxSize()
+ .imePadding(),
+ topBar = {
+ OnboardingTopAppBar(onBackClick = navigateBack)
+ },
+ content = { innerPadding ->
+ Column(
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .padding(horizontal = 20.dp),
+ ) {
+ Text(
+ text = stringResource(R.string.enter_password),
+ style = ourMenuTypography().pretendard_600_24,
+ color = Neutral900,
+ modifier = Modifier.padding(top = 92.dp),
+ )
+
+ Text(
+ text = stringResource(R.string.password_hint),
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral500,
+ modifier = Modifier.padding(top = 4.dp),
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ LoginTextField(
+ error = when (uiState.passwordState) {
+ PasswordState.NotMeetCondition, PasswordState.DifferentPassword -> true
+ else -> false
+ },
+ modifier = when (uiState.passwordState) {
+ PasswordState.NotMeetCondition, PasswordState.DifferentPassword ->
+ shakingModifier.focusRequester(focusRequester)
+
+ else -> Modifier.focusRequester(focusRequester)
+ },
+ placeholder = stringResource(R.string.password_placeholder),
+ input = uiState.password,
+ onTextChange = { updatePassword(it) },
+ visualTransformation =
+ if (uiState.passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ LoginTextField(
+ error = when (uiState.passwordState) {
+ PasswordState.DifferentPassword -> true
+ else -> false
+ },
+ modifier = when (uiState.passwordState) {
+ PasswordState.NotMeetCondition, PasswordState.DifferentPassword ->
+ shakingModifier
+
+ else -> Modifier
+ },
+ placeholder = stringResource(R.string.confirm_password_placeholder),
+ input = uiState.confirmPassword,
+ onTextChange = { updateConfirmPassword(it) },
+ visualTransformation = if (uiState.passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Checkbox(
+ checked = uiState.passwordVisible,
+ onCheckedChange = { setPasswordVisibility(it) },
+ modifier =
+ Modifier
+ .size(24.dp),
+ colors =
+ CheckboxDefaults.colors(
+ checkmarkColor = NeutralWhite,
+ checkedColor = Primary500Main,
+ uncheckedColor = Neutral300,
+ ),
+ )
+
+ Text(
+ text = stringResource(R.string.see_password),
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral500,
+ modifier = Modifier.padding(start = 8.dp),
+ )
+ }
+ }
+ },
+ bottomBar = {
+ Column(
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Box(
+ contentAlignment = Alignment.Center,
+ ) {
+ DisableBottomFullWidthButton(
+ enable = enable,
+ text = stringResource(R.string.confirm),
+ onClick = {
+ updatePasswordState(
+ checkPassword(
+ password = uiState.password,
+ confirmPassword = uiState.confirmPassword,
+ )
+ )
+ },
+ )
+ }
+ }
+ },
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SignupPasswordScreenPreview() {
+ SignupPasswordScreen(
+ navigateBack = {},
+ enable = true,
+ uiState = SignupUiState(),
+ focusRequester = FocusRequester(),
+ shakeOffset = remember { Animatable(0f) },
+ setPasswordVisibility = {},
+ updatePasswordState = {},
+ updatePassword = {},
+ updateConfirmPassword = {}
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/signup/screen/SignupVerifyScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/signup/screen/SignupVerifyScreen.kt
new file mode 100644
index 00000000..0d77db2b
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/signup/screen/SignupVerifyScreen.kt
@@ -0,0 +1,276 @@
+package com.kuit.ourmenu.ui.signup.screen
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.imePadding
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.ui.common.DisableBottomFullWidthButton
+import com.kuit.ourmenu.ui.common.OurSnackbarHost
+import com.kuit.ourmenu.ui.common.topappbar.OnboardingTopAppBar
+import com.kuit.ourmenu.ui.signup.component.VerifyCodeTextField
+import com.kuit.ourmenu.ui.signup.model.SignupState
+import com.kuit.ourmenu.ui.signup.uistate.SignupUiState
+import com.kuit.ourmenu.ui.signup.viewmodel.SignupViewModel
+import com.kuit.ourmenu.ui.theme.Neutral300
+import com.kuit.ourmenu.ui.theme.Neutral500
+import com.kuit.ourmenu.ui.theme.Neutral900
+import com.kuit.ourmenu.ui.theme.NeutralWhite
+import com.kuit.ourmenu.ui.theme.Primary500Main
+import com.kuit.ourmenu.ui.theme.ourMenuTypography
+import com.kuit.ourmenu.utils.AnimationUtil.shakeErrorInputField
+import kotlin.math.roundToInt
+
+@Composable
+fun SignupVerifyRoute(
+ navigateToPassword: () -> Unit,
+ navigateBack: () -> Unit,
+ viewModel: SignupViewModel = hiltViewModel()
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ val enabled by remember {
+ derivedStateOf { uiState.codes.all { it.isNotEmpty() } }
+ }
+ val snackbarHostState = remember { SnackbarHostState() }
+ val scope = rememberCoroutineScope()
+ val shakeOffset = remember { Animatable(0f) }
+
+ LaunchedEffect(uiState.verifyState) {
+ when (uiState.verifyState) {
+ is SignupState.Success ->
+ navigateToPassword()
+
+ is SignupState.Error -> {
+ shakeErrorInputField(
+ shakeOffset = shakeOffset,
+ message = "์ธ์ฆ ์ฝ๋๊ฐ ์ผ์นํ์ง ์์ต๋๋ค.",
+ snackbarHostState = snackbarHostState,
+ scope = scope
+ )
+ }
+
+ else -> {}
+ }
+ }
+
+ SignupVerifyScreen(
+ navigateBack = navigateBack,
+ enabled = enabled,
+ uiState = uiState,
+ shakeOffset = shakeOffset,
+ sendEmail = viewModel::sendEmail,
+ verifyCode = viewModel::verifyCode,
+ updateCode = viewModel::updateCode
+ )
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(top = 44.dp),
+ contentAlignment = Alignment.TopCenter
+ ) {
+ OurSnackbarHost(
+ hostState = snackbarHostState
+ )
+ }
+}
+
+@Composable
+fun SignupVerifyScreen(
+ navigateBack: () -> Unit,
+ enabled: Boolean,
+ uiState: SignupUiState,
+ shakeOffset: Animatable,
+ sendEmail: () -> Unit,
+ verifyCode: () -> Unit,
+ updateCode: (Int, String) -> Unit,
+) {
+ val focusRequesters = List(6) { FocusRequester() }
+ val shakingModifier = Modifier.offset { IntOffset(shakeOffset.value.roundToInt(), 0) }
+
+ Scaffold(
+ modifier = Modifier
+ .fillMaxSize()
+ .imePadding(),
+ topBar = {
+ OnboardingTopAppBar(
+ onBackClick = navigateBack
+ )
+ },
+ content = { innerPadding ->
+ Column(
+ modifier =
+ Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .padding(horizontal = 20.dp),
+ ) {
+ Text(
+ text = stringResource(R.string.sent_mail),
+ style = ourMenuTypography().pretendard_600_24,
+ color = Neutral900,
+ modifier = Modifier.padding(top = 92.dp),
+ )
+
+ Row {
+ Text(
+ text = stringResource(R.string.in_5_mins),
+ style = ourMenuTypography().pretendard_700_14,
+ color = Primary500Main,
+ modifier = Modifier.padding(top = 4.dp),
+ )
+
+ Text(
+ text = stringResource(R.string.enter_code),
+ style = ourMenuTypography().pretendard_500_14,
+ color = Neutral500,
+ modifier = Modifier.padding(top = 4.dp, start = 4.dp),
+ )
+ }
+
+ Spacer(Modifier.height(12.dp))
+
+ // TODO: ์๋ฌ ์ฒ๋ฆฌํ๊ธฐ
+ Row {
+ for (i in 0 until 6) {
+ VerifyCodeTextField(
+ modifier = when (uiState.verifyState) {
+ SignupState.Error -> shakingModifier
+ else -> Modifier
+ }.then(
+ if (i == 0 && uiState.codes[i].isEmpty()) {
+ Modifier.focusRequester(focusRequesters[i])
+ } else {
+ Modifier
+ }
+ ),
+ error = when (uiState.verifyState) {
+ SignupState.Error -> true
+ else -> false
+ },
+ input = uiState.codes[i],
+ onTextChange = { newText ->
+ if (newText.length <= 1) {
+ updateCode(i, newText) // Compose์์ ์ํ ๋ณ๊ฒฝ ๊ฐ์ง
+ }
+ },
+ onNext = {
+ if (i < 5) {
+ focusRequesters[i + 1].requestFocus()
+ }
+ },
+ onPrevious = {
+ if (i > 0) {
+ focusRequesters[i - 1].requestFocus()
+ }
+ },
+ focusRequester = focusRequesters[i],
+ )
+ if (i < 5) {
+ Spacer(modifier = Modifier.size(8.dp))
+ }
+ }
+ }
+ }
+
+ },
+ bottomBar = {
+ Column(
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Box(
+ contentAlignment = Alignment.Center,
+ ) {
+ HorizontalDivider(
+ color = Neutral300,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp),
+ )
+
+ Box(
+ contentAlignment = Alignment.Center, // ๋ด์ฉ ์ค์ ์ ๋ ฌ
+ modifier =
+ Modifier
+ .background(NeutralWhite)
+ .height(36.dp)
+ .width(124.dp),
+ ) {
+ Text(
+ text = stringResource(R.string.didnt_receive),
+ style = ourMenuTypography().pretendard_500_12,
+ color = Neutral500,
+ )
+ }
+ }
+
+ Text(
+ text = stringResource(R.string.resend),
+ style = ourMenuTypography().pretendard_600_14,
+ color = Primary500Main,
+ modifier = Modifier
+ .padding(bottom = 36.dp)
+ .clickable {
+ sendEmail()
+ },
+ )
+
+ DisableBottomFullWidthButton(
+ enable = enabled,
+ text = stringResource(R.string.confirm),
+ onClick = { verifyCode() },
+ )
+ }
+ },
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SignupVerifyScreenPreview() {
+ SignupVerifyScreen(
+ navigateBack = { },
+ enabled = true,
+ uiState = SignupUiState(),
+ shakeOffset = remember { Animatable(0f) },
+ sendEmail = { },
+ verifyCode = { },
+ updateCode = { _, _ -> }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/signup/uistate/SignupUiState.kt b/app/src/main/java/com/kuit/ourmenu/ui/signup/uistate/SignupUiState.kt
new file mode 100644
index 00000000..d0238c4b
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/signup/uistate/SignupUiState.kt
@@ -0,0 +1,24 @@
+package com.kuit.ourmenu.ui.signup.uistate
+
+import com.kuit.ourmenu.ui.common.model.MealTime
+import com.kuit.ourmenu.ui.common.model.PasswordState
+import com.kuit.ourmenu.ui.signup.model.SignupState
+
+data class SignupUiState(
+ val email: String = "",
+ val domain: String = "",
+ val password: String = "",
+ val confirmPassword: String = "",
+ val passwordVisible: Boolean = false,
+ val codes: List = listOf("", "", "", "", "", ""),
+ val mealTimes: List = List(18) {
+ MealTime(mealTime = it + 6)
+ },
+ val selectedTimes: List = emptyList(),
+ val emailState: SignupState = SignupState.Default,
+ val verifyState: SignupState = SignupState.Default,
+ val passwordState: PasswordState = PasswordState.Default,
+ val signupState: SignupState = SignupState.Default,
+ val error: String = ""
+)
+
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/signup/viewmodel/SignupViewModel.kt b/app/src/main/java/com/kuit/ourmenu/ui/signup/viewmodel/SignupViewModel.kt
new file mode 100644
index 00000000..cb502a5d
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/ui/signup/viewmodel/SignupViewModel.kt
@@ -0,0 +1,222 @@
+package com.kuit.ourmenu.ui.signup.viewmodel
+
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.kuit.ourmenu.data.model.auth.SignInType
+import com.kuit.ourmenu.data.repository.AuthRepository
+import com.kuit.ourmenu.data.repository.KakaoRepository
+import com.kuit.ourmenu.ui.common.model.PasswordState
+import com.kuit.ourmenu.ui.signup.model.SignupState
+import com.kuit.ourmenu.ui.signup.uistate.SignupUiState
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class SignupViewModel @Inject constructor(
+ private val authRepository: AuthRepository,
+ private val kakaoRepository: KakaoRepository,
+) : ViewModel() {
+
+ private val _uiState = MutableStateFlow(SignupUiState())
+ val uiState = _uiState.asStateFlow()
+
+ fun updateEmail(email: String) {
+ _uiState.update {
+ it.copy(email = email)
+ }
+ }
+
+ fun updateDomain(domain: String) {
+ _uiState.update {
+ it.copy(domain = domain)
+ }
+ }
+
+ fun updateCode(index: Int, code: String) {
+ _uiState.update {
+ it.copy(codes = it.codes.toMutableList().mapIndexed { i, item ->
+ if (i == index) code.uppercase() else item
+ })
+ }
+ }
+
+ fun updatePassword(password: String) {
+ _uiState.update {
+ it.copy(password = password)
+ }
+ }
+
+ fun updateConfirmPassword(confirmPassword: String) {
+ _uiState.update {
+ it.copy(confirmPassword = confirmPassword)
+ }
+ }
+
+ fun setPasswordVisibility(visible: Boolean) {
+ _uiState.update {
+ it.copy(passwordVisible = visible)
+ }
+ }
+
+ fun updatePasswordState(passwordState: PasswordState) {
+ _uiState.update {
+ it.copy(passwordState = passwordState)
+ }
+ }
+
+ fun updateSelectedTime(index: Int) {
+ _uiState.update {
+ val selected = it.mealTimes[index].selected
+ val currentSelectedTimes = it.selectedTimes.toMutableList()
+ val mealTime = it.mealTimes[index].mealTime
+
+ it.copy(
+ mealTimes = it.mealTimes.toMutableList()
+ .apply {
+ this[index].selected = !selected && currentSelectedTimes.size < 4
+ }.toList(),
+ selectedTimes =
+ run {
+ if (!selected && currentSelectedTimes.size < 4) {
+ currentSelectedTimes.add(mealTime)
+ } else currentSelectedTimes.remove(mealTime)
+ currentSelectedTimes.toList()
+ },
+ )
+ }
+ }
+
+ /* Api Section */
+ fun sendEmail() {
+ viewModelScope.launch {
+ authRepository.sendEmail(
+ email = "${_uiState.value.email}@${_uiState.value.domain}"
+ )
+ .fold(
+ onSuccess = {
+ _uiState.update {
+ it.copy(emailState = SignupState.Success)
+ }
+ },
+ onFailure = { error ->
+ _uiState.update {
+ it.copy(
+ emailState = SignupState.Error,
+ error = error.message ?: "Unknown error"
+ )
+ }
+ }
+
+ )
+ delay(1000)
+ _uiState.update {
+ it.copy(emailState = SignupState.Default)
+ }
+ }
+ }
+
+ fun verifyCode() {
+ viewModelScope.launch {
+ authRepository.confirmCode(
+ confirmCode = _uiState.value.codes.joinToString(""),
+ email = "${_uiState.value.email}@${_uiState.value.domain}"
+ )
+ .fold(
+ onSuccess = {
+ _uiState.update {
+ it.copy(verifyState = SignupState.Success)
+ }
+ },
+ onFailure = { error ->
+ _uiState.update {
+ it.copy(
+ verifyState = SignupState.Error,
+ error = error.message ?: "Unknown error"
+ )
+ }
+ }
+ )
+ delay(1000)
+ _uiState.update {
+ it.copy(verifyState = SignupState.Default)
+ }
+ }
+ }
+
+ fun signup() {
+ if (_uiState.value.email == "" || _uiState.value.domain == "") {
+ signupWithKakao()
+ } else {
+ signupWithEmail()
+ }
+ }
+
+ private fun signupWithEmail() {
+ viewModelScope.launch {
+ val completeEmail = "${_uiState.value.email}@${_uiState.value.domain}"
+
+ authRepository.signup(
+ email = completeEmail,
+ mealTime = _uiState.value.selectedTimes.sorted().map {
+ "${it.toString().padStart(2, '0')}:00:00"
+ },
+ password = _uiState.value.password.takeIf { it.isNotEmpty() },
+ signInType = SignInType.EMAIL
+ ).fold(
+ onSuccess = {
+ _uiState.update {
+ it.copy(signupState = SignupState.Success)
+ }
+ Log.d("okhttp2", _uiState.value.signupState.toString())
+ },
+ onFailure = { error ->
+ _uiState.update {
+ it.copy(signupState = SignupState.Error)
+ }
+ _uiState.update {
+ it.copy(error = error.message ?: "Unknown error")
+ }
+ Log.d("okhttp3", error.toString())
+ }
+ )
+ }
+ }
+
+ private fun signupWithKakao() {
+ viewModelScope.launch {
+ val kakaoEmail = kakaoRepository.getUserEmail()
+
+ authRepository.signup(
+ email = kakaoEmail,
+ mealTime = _uiState.value.selectedTimes.sorted().map {
+ "${it.toString().padStart(2, '0')}:00:00"
+ },
+ password = _uiState.value.password.takeIf { it.isNotEmpty() },
+ signInType = SignInType.KAKAO
+ ).fold(
+ onSuccess = {
+ _uiState.update {
+ it.copy(signupState = SignupState.Success)
+ }
+ Log.d("okhttp2", _uiState.value.signupState.toString())
+ },
+ onFailure = { error ->
+ _uiState.update {
+ it.copy(signupState = SignupState.Error)
+ }
+ _uiState.update {
+ it.copy(error = error.message ?: "Unknown error")
+ }
+ Log.d("okhttp3", error.toString())
+ }
+ )
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/theme/Color.kt b/app/src/main/java/com/kuit/ourmenu/ui/theme/Color.kt
index 54b992d4..6328c20d 100644
--- a/app/src/main/java/com/kuit/ourmenu/ui/theme/Color.kt
+++ b/app/src/main/java/com/kuit/ourmenu/ui/theme/Color.kt
@@ -2,10 +2,20 @@ package com.kuit.ourmenu.ui.theme
import androidx.compose.ui.graphics.Color
-val Purple80 = Color(0xFFD0BCFF)
-val PurpleGrey80 = Color(0xFFCCC2DC)
-val Pink80 = Color(0xFFEFB8C8)
+// Primary Color
+val Primary700 = Color(0xFFE64717)
+val Primary500Main = Color(0xFFFF5420)
+val Primary300 = Color(0xFFFF8864)
+val Primary100 = Color(0xFFFFCBBB)
-val Purple40 = Color(0xFF6650a4)
-val PurpleGrey40 = Color(0xFF625b71)
-val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
+// Neutral
+val NeutralWhite = Color(0xFFFFFFFF)
+val Neutral50 = Color(0xFFFBFBFD)
+val Neutral100 = Color(0xFFF7F7F9)
+val Neutral200 = Color(0xFFF5F5F5)
+val Neutral300 = Color(0xFFE5E5E7)
+val Neutral400 = Color(0xFFC2C2C4)
+val Neutral500 = Color(0xFFA4A4A6)
+val Neutral700 = Color(0xFF666668)
+val Neutral900 = Color(0xFF252527)
+val NeutralBlack = Color(0xFF000000)
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/theme/Theme.kt b/app/src/main/java/com/kuit/ourmenu/ui/theme/Theme.kt
index e6eff763..ed800b77 100644
--- a/app/src/main/java/com/kuit/ourmenu/ui/theme/Theme.kt
+++ b/app/src/main/java/com/kuit/ourmenu/ui/theme/Theme.kt
@@ -1,6 +1,5 @@
package com.kuit.ourmenu.ui.theme
-import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
@@ -9,50 +8,73 @@ import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.platform.LocalContext
-private val DarkColorScheme = darkColorScheme(
- primary = Purple80,
- secondary = PurpleGrey80,
- tertiary = Pink80
-)
-
-private val LightColorScheme = lightColorScheme(
- primary = Purple40,
- secondary = PurpleGrey40,
- tertiary = Pink40
-
- /* Other default colors to override
- background = Color(0xFFFFFBFE),
- surface = Color(0xFFFFFBFE),
- onPrimary = Color.White,
- onSecondary = Color.White,
- onTertiary = Color.White,
- onBackground = Color(0xFF1C1B1F),
- onSurface = Color(0xFF1C1B1F),
- */
-)
+private val DarkColorScheme =
+ darkColorScheme(
+ primary = Primary500Main,
+ )
+
+private val LightColorScheme =
+ lightColorScheme(
+ primary = Primary500Main,
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+ )
+
+private val LocalOurMenuTypography =
+ staticCompositionLocalOf {
+ error("No OurMenuTypography provided")
+ }
+
+object OurMenuTheme {
+ val typography: OurMenuTypography
+ @Composable get() = LocalOurMenuTypography.current
+}
+
+@Composable
+fun ProvideOurMenuTypography(
+ typography: OurMenuTypography,
+ content: @Composable () -> Unit,
+) {
+ val provideTypography = remember { typography.copy() }
+ provideTypography.update(typography)
+ CompositionLocalProvider(
+ LocalOurMenuTypography provides provideTypography,
+ content = content,
+ )
+}
@Composable
fun OurMenuTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
- content: @Composable () -> Unit
+ content: @Composable () -> Unit,
) {
- val colorScheme = when {
- dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
- val context = LocalContext.current
- if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
- }
+ val colorScheme =
+ when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
- darkTheme -> DarkColorScheme
- else -> LightColorScheme
- }
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
MaterialTheme(
colorScheme = colorScheme,
- typography = Typography,
- content = content
+ content = content,
)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/kuit/ourmenu/ui/theme/Type.kt b/app/src/main/java/com/kuit/ourmenu/ui/theme/Type.kt
index acdbcfa1..cc62a072 100644
--- a/app/src/main/java/com/kuit/ourmenu/ui/theme/Type.kt
+++ b/app/src/main/java/com/kuit/ourmenu/ui/theme/Type.kt
@@ -1,34 +1,141 @@
package com.kuit.ourmenu.ui.theme
-import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
+import com.kuit.ourmenu.R
-// Set of Material typography styles to start with
-val Typography = Typography(
- bodyLarge = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 16.sp,
- lineHeight = 24.sp,
- letterSpacing = 0.5.sp
- )
- /* Other default text styles to override
- titleLarge = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 22.sp,
- lineHeight = 28.sp,
- letterSpacing = 0.sp
- ),
- labelSmall = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Medium,
- fontSize = 11.sp,
- lineHeight = 16.sp,
- letterSpacing = 0.5.sp
- )
- */
-)
\ No newline at end of file
+val PretendardBold = FontFamily(Font(R.font.bold))
+val PretendardSemiBold = FontFamily(Font(R.font.semibold))
+val PretendardMedium = FontFamily(Font(R.font.medium))
+val PretendardRegular = FontFamily(Font(R.font.regular))
+
+data class OurMenuTypography(
+ val pretendard_700_48: TextStyle =
+ TextStyle(
+ fontFamily = PretendardBold,
+ fontSize = 48.sp,
+ ),
+ val pretendard_700_32: TextStyle =
+ TextStyle(
+ fontFamily = PretendardBold,
+ fontSize = 32.sp,
+ ),
+ val pretendard_700_24: TextStyle =
+ TextStyle(
+ fontFamily = PretendardBold,
+ fontSize = 24.sp,
+ ),
+ val pretendard_700_20: TextStyle =
+ TextStyle(
+ fontFamily = PretendardBold,
+ fontSize = 20.sp,
+ ),
+ val pretendard_700_18: TextStyle =
+ TextStyle(
+ fontFamily = PretendardBold,
+ fontSize = 18.sp,
+ ),
+ val pretendard_700_16: TextStyle =
+ TextStyle(
+ fontFamily = PretendardBold,
+ fontSize = 16.sp,
+ ),
+ val pretendard_700_14: TextStyle =
+ TextStyle(
+ fontFamily = PretendardBold,
+ fontSize = 14.sp,
+ ),
+ val pretendard_700_12: TextStyle =
+ TextStyle(
+ fontFamily = PretendardBold,
+ fontSize = 12.sp,
+ ),
+ val pretendard_600_32: TextStyle =
+ TextStyle(
+ fontFamily = PretendardSemiBold,
+ fontSize = 32.sp,
+ ),
+ val pretendard_600_24: TextStyle =
+ TextStyle(
+ fontFamily = PretendardSemiBold,
+ fontSize = 24.sp,
+ ),
+ val pretendard_600_20: TextStyle =
+ TextStyle(
+ fontFamily = PretendardSemiBold,
+ fontSize = 20.sp,
+ ),
+ val pretendard_600_18: TextStyle =
+ TextStyle(
+ fontFamily = PretendardSemiBold,
+ fontSize = 18.sp,
+ ),
+ val pretendard_600_16: TextStyle =
+ TextStyle(
+ fontFamily = PretendardSemiBold,
+ fontSize = 16.sp,
+ ),
+ val pretendard_600_14: TextStyle =
+ TextStyle(
+ fontFamily = PretendardSemiBold,
+ fontSize = 14.sp,
+ ),
+ val pretendard_600_12: TextStyle =
+ TextStyle(
+ fontFamily = PretendardSemiBold,
+ fontSize = 12.sp,
+ ),
+ val pretendard_500_28: TextStyle =
+ TextStyle(
+ fontFamily = PretendardMedium,
+ fontSize = 28.sp,
+ ),
+ val pretendard_500_24: TextStyle =
+ TextStyle(
+ fontFamily = PretendardMedium,
+ fontSize = 24.sp,
+ ),
+ val pretendard_500_20: TextStyle =
+ TextStyle(
+ fontFamily = PretendardMedium,
+ fontSize = 20.sp,
+ ),
+ val pretendard_500_18: TextStyle =
+ TextStyle(
+ fontFamily = PretendardMedium,
+ fontSize = 18.sp,
+ ),
+ val pretendard_500_16: TextStyle =
+ TextStyle(
+ fontFamily = PretendardMedium,
+ fontSize = 16.sp,
+ ),
+ val pretendard_500_14: TextStyle =
+ TextStyle(
+ fontFamily = PretendardMedium,
+ fontSize = 14.sp,
+ ),
+ val pretendard_500_12: TextStyle =
+ TextStyle(
+ fontFamily = PretendardMedium,
+ fontSize = 12.sp,
+ ),
+ val pretendard_400_14: TextStyle =
+ TextStyle(
+ fontFamily = PretendardRegular,
+ fontSize = 14.sp,
+ ),
+ val pretendard_400_12: TextStyle =
+ TextStyle(
+ fontFamily = PretendardRegular,
+ fontSize = 12.sp,
+ ),
+) {
+ fun copy(): OurMenuTypography = this
+
+ fun update(other: OurMenuTypography) {}
+}
+
+fun ourMenuTypography(): OurMenuTypography = OurMenuTypography()
diff --git a/app/src/main/java/com/kuit/ourmenu/utils/AnimationUtil.kt b/app/src/main/java/com/kuit/ourmenu/utils/AnimationUtil.kt
new file mode 100644
index 00000000..113604d8
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/utils/AnimationUtil.kt
@@ -0,0 +1,79 @@
+package com.kuit.ourmenu.utils
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.FastOutLinearInEasing
+import androidx.compose.animation.core.keyframes
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.ui.focus.FocusRequester
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+object AnimationUtil {
+
+ fun shakeAnimation(
+ offset: Animatable,
+ coroutineScope: CoroutineScope,
+ durationMillis: Int = 800
+ ) {
+ val shakeKeyframes: AnimationSpec = keyframes {
+ val easing = FastOutLinearInEasing
+
+ // ์ข ๋ ๋๋คํ ํ๋ค๋ฆผ ๊ฐ์ ์ค์
+ listOf(-8f, 8f, -6f, 6f, -4f, 4f, -2f, 2f, 0f).forEachIndexed { index, x ->
+ x at (durationMillis / 9 * (index + 1)) using easing
+ }
+ }
+
+ coroutineScope.launch {
+ offset.animateTo(
+ targetValue = 0f,
+ animationSpec = shakeKeyframes,
+ )
+ }
+ }
+
+ fun shakeErrorInputField(
+ shakeOffset: Animatable,
+ message: String,
+ snackbarHostState: SnackbarHostState,
+ scope: CoroutineScope,
+ ) {
+ shakeAnimation(
+ offset = shakeOffset,
+ coroutineScope = scope,
+ )
+ scope.launch {
+ snackbarHostState.showSnackbar(
+ message = message,
+ duration = SnackbarDuration.Short
+ )
+ }
+ }
+
+ fun shakeErrorInputFieldWithFocus(
+ shakeOffset: Animatable,
+ focusRequester: FocusRequester,
+ message: String,
+ snackbarHostState: SnackbarHostState,
+ scope: CoroutineScope,
+ ) {
+ scope.launch {
+ focusRequester.requestFocus()
+ delay(800)
+ }
+ shakeAnimation(
+ offset = shakeOffset,
+ coroutineScope = scope,
+ )
+ scope.launch {
+ snackbarHostState.showSnackbar(
+ message = message,
+ duration = SnackbarDuration.Short
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/utils/CacheUtil.kt b/app/src/main/java/com/kuit/ourmenu/utils/CacheUtil.kt
new file mode 100644
index 00000000..a3c7ace3
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/utils/CacheUtil.kt
@@ -0,0 +1,37 @@
+package com.kuit.ourmenu.utils
+
+import android.content.Context
+import coil3.ImageLoader
+import coil3.request.ImageRequest
+import com.kuit.ourmenu.ui.onboarding.state.CacheState
+
+object CacheUtil {
+
+ fun preloadData(
+ context: Context,
+ imageLoader: ImageLoader,
+ cacheState: CacheState,
+ ) {
+ loadUrls(context, imageLoader, cacheState.menuFolderIcons)
+ loadUrls(context, imageLoader, cacheState.menuPinMaps)
+ loadUrls(context, imageLoader, cacheState.menuAdds)
+ loadUrls(context, imageLoader, cacheState.menuPinAddDisables)
+ loadUrls(context, imageLoader, cacheState.homeImgs)
+ loadUrls(context, imageLoader, cacheState.orangeTags)
+ loadUrls(context, imageLoader, cacheState.whiteTags)
+ }
+
+ private fun loadUrls(
+ context: Context,
+ imageLoader: ImageLoader,
+ urls: List,
+ ) {
+ urls.map {
+ val request = ImageRequest.Builder(context)
+ .data(it)
+ .build()
+
+ imageLoader.enqueue(request)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/utils/ExtensionUtil.kt b/app/src/main/java/com/kuit/ourmenu/utils/ExtensionUtil.kt
new file mode 100644
index 00000000..25638078
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/utils/ExtensionUtil.kt
@@ -0,0 +1,15 @@
+package com.kuit.ourmenu.utils
+
+import java.util.Locale
+
+object ExtensionUtil {
+ fun Int.toWon(): String {
+ val decimalFormat = java.text.DecimalFormat("#,###")
+ val formatted = decimalFormat.format(this)
+
+ return "${formatted}์"
+ }
+
+ fun Int.toMealTime(): String = String.format(Locale.ROOT, "%02d", this) + ":00"
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/utils/PermissionHandler.kt b/app/src/main/java/com/kuit/ourmenu/utils/PermissionHandler.kt
new file mode 100644
index 00000000..54d9cd46
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/utils/PermissionHandler.kt
@@ -0,0 +1,81 @@
+package com.kuit.ourmenu.utils
+
+import android.app.Activity
+import android.content.pm.PackageManager
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.core.content.ContextCompat
+
+@Composable
+fun PermissionHandler(
+ permission: String,
+ rationaleMessage: String,
+ onPermissionGranted: () -> Unit,
+ onPermissionDenied: () -> Unit
+) {
+ val context = LocalContext.current
+ val showRationaleDialog = remember { mutableStateOf(false) }
+
+ val launcher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestPermission()
+ ) { isGranted ->
+ if (isGranted) {
+ onPermissionGranted()
+ } else {
+ onPermissionDenied()
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ when {
+ ContextCompat.checkSelfPermission(
+ context,
+ permission
+ ) == PackageManager.PERMISSION_GRANTED -> {
+ onPermissionGranted()
+ }
+ (context as? Activity)?.shouldShowRequestPermissionRationale(permission) == true -> {
+ showRationaleDialog.value = true
+ }
+ else -> {
+ launcher.launch(permission)
+ }
+ }
+ }
+
+ if (showRationaleDialog.value) {
+ AlertDialog(
+ onDismissRequest = { showRationaleDialog.value = false },
+ title = { Text("Permission Required") },
+ text = { Text(rationaleMessage) },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ showRationaleDialog.value = false
+ launcher.launch(permission)
+ }
+ ) {
+ Text("OK")
+ }
+ },
+ dismissButton = {
+ TextButton(
+ onClick = {
+ showRationaleDialog.value = false
+ onPermissionDenied()
+ }
+ ) {
+ Text("Cancel")
+ }
+ }
+ )
+ }
+}
diff --git a/app/src/main/java/com/kuit/ourmenu/utils/PreferencesManager.kt b/app/src/main/java/com/kuit/ourmenu/utils/PreferencesManager.kt
new file mode 100644
index 00000000..032fa495
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/utils/PreferencesManager.kt
@@ -0,0 +1,37 @@
+package com.kuit.ourmenu.utils
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.preferencesDataStore
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+import javax.inject.Singleton
+
+
+private val Context.dataStore: DataStore by preferencesDataStore(name = "settings")
+
+@Singleton
+class PreferencesManager @Inject constructor(
+ @ApplicationContext private val context: Context
+) {
+ companion object {
+ private val LOCATION_PERMISSION_GRANTED =
+ booleanPreferencesKey("location_permission_granted")
+ }
+
+ val locationPermissionGranted: Flow = context.dataStore.data
+ .map { preferences ->
+ preferences[LOCATION_PERMISSION_GRANTED] ?: false
+ }
+
+ suspend fun setLocationPermissionGranted(granted: Boolean) {
+ context.dataStore.edit { preferences ->
+ preferences[LOCATION_PERMISSION_GRANTED] = granted
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/utils/TagListProvider.kt b/app/src/main/java/com/kuit/ourmenu/utils/TagListProvider.kt
new file mode 100644
index 00000000..5247d9cd
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/utils/TagListProvider.kt
@@ -0,0 +1,42 @@
+package com.kuit.ourmenu.utils
+
+import com.kuit.ourmenu.R
+import com.kuit.ourmenu.data.model.base.type.TagType
+
+object TagListProvider {
+ val categoryTagList = listOf(
+ R.drawable.ic_tag_rice to TagType.RICE,
+ R.drawable.ic_tag_rice to TagType.BREAD,
+ R.drawable.ic_tag_rice to TagType.NOODLE,
+ R.drawable.ic_tag_rice to TagType.MEAT,
+ R.drawable.ic_tag_rice to TagType.FISH,
+ R.drawable.ic_tag_rice to TagType.DESSERT,
+ R.drawable.ic_tag_rice to TagType.CAFE,
+ R.drawable.ic_tag_rice to TagType.FAST_FOOD
+ )
+
+ val nationalityTagList = listOf(
+ R.drawable.ic_tag_rice to TagType.KOREA,
+ R.drawable.ic_tag_rice to TagType.CHINA,
+ R.drawable.ic_tag_rice to TagType.JAPAN,
+ R.drawable.ic_tag_rice to TagType.WESTERN,
+ R.drawable.ic_tag_rice to TagType.ASIA
+ )
+
+ val tasteTagList = listOf(
+ R.drawable.ic_tag_rice to TagType.SPICY,
+ R.drawable.ic_tag_rice to TagType.SWEET,
+ R.drawable.ic_tag_rice to TagType.COOL,
+ R.drawable.ic_tag_rice to TagType.HOT,
+ R.drawable.ic_tag_rice to TagType.HOT_SPICY
+ )
+
+ val occasionTagList = listOf(
+ R.drawable.ic_tag_rice to TagType.SOLO,
+ R.drawable.ic_tag_rice to TagType.BUSINESS,
+ R.drawable.ic_tag_rice to TagType.PROMISE,
+ R.drawable.ic_tag_rice to TagType.DATE,
+ R.drawable.ic_tag_rice to TagType.BUY_FOOD,
+ R.drawable.ic_tag_rice to TagType.ORGANIZATION
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/utils/ViewUtil.kt b/app/src/main/java/com/kuit/ourmenu/utils/ViewUtil.kt
new file mode 100644
index 00000000..a2919bf3
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/utils/ViewUtil.kt
@@ -0,0 +1,19 @@
+package com.kuit.ourmenu.utils
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+
+
+object ViewUtil {
+ fun Modifier.noRippleClickable(onClick: () -> Unit): Modifier = composed {
+ clickable(indication = null,
+ interactionSource = remember {
+ MutableInteractionSource()
+ }) {
+ onClick()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/utils/auth/AuthInterceptor.kt b/app/src/main/java/com/kuit/ourmenu/utils/auth/AuthInterceptor.kt
new file mode 100644
index 00000000..49828aba
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/utils/auth/AuthInterceptor.kt
@@ -0,0 +1,26 @@
+package com.kuit.ourmenu.utils.auth
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.runBlocking
+import okhttp3.Interceptor
+import okhttp3.Response
+import javax.inject.Inject
+
+class AuthInterceptor @Inject constructor(
+ private val tokenManager: TokenManager
+) : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val request = chain.request().newBuilder()
+ val accessToken =
+ runBlocking { tokenManager.getAccessToken().flowOn(Dispatchers.IO).first() }
+
+ accessToken?.let {
+ // header์ ํ ํฐ์ ์ถ๊ฐ
+ request
+ .addHeader("Authorization", "Bearer $it")
+ }
+ return chain.proceed(request.build())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/utils/auth/TokenAuthenticator.kt b/app/src/main/java/com/kuit/ourmenu/utils/auth/TokenAuthenticator.kt
new file mode 100644
index 00000000..fd7c095c
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/utils/auth/TokenAuthenticator.kt
@@ -0,0 +1,62 @@
+package com.kuit.ourmenu.utils.auth
+
+import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
+import com.kuit.ourmenu.BuildConfig
+import com.kuit.ourmenu.data.model.auth.response.ReissueTokenResponse
+import com.kuit.ourmenu.data.service.AuthService
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import kotlinx.serialization.json.Json
+import okhttp3.Authenticator
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.Route
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import javax.inject.Inject
+
+class TokenAuthenticator @Inject constructor(
+ private val tokenManager: TokenManager,
+): Authenticator {
+ override fun authenticate(route: Route?, response: Response): Request? {
+ val refreshToken = runBlocking {
+ tokenManager.getRefreshToken().first()
+ }
+ return runBlocking {
+ if (refreshToken.isNullOrEmpty()){
+ return@runBlocking null
+ }
+ val newToken = getNewToken(refreshToken)
+
+ if (newToken == null) {
+ tokenManager.clearToken()
+ }
+
+ newToken?.let {
+ tokenManager.saveAccessToken(it.accessToken)
+ response.request.newBuilder()
+ .header("Authorization", "Bearer ${it.accessToken}")
+ .build()
+ }
+ }
+ }
+
+ private suspend fun getNewToken(refreshToken: String): ReissueTokenResponse? {
+ val loggingInterceptor = HttpLoggingInterceptor()
+ loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
+ val okHttpClient = OkHttpClient.Builder().addInterceptor(loggingInterceptor).build()
+
+ val retrofit = Retrofit.Builder()
+ .baseUrl(BuildConfig.BASE_URL)
+ .addConverterFactory(
+ Json.asConverterFactory(requireNotNull("application/json".toMediaType()))
+ )
+ .client(okHttpClient)
+ .build()
+ val service = retrofit.create(AuthService::class.java)
+ return service.reissueToken(refreshToken).response
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/utils/auth/TokenManager.kt b/app/src/main/java/com/kuit/ourmenu/utils/auth/TokenManager.kt
new file mode 100644
index 00000000..ce62bc19
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/utils/auth/TokenManager.kt
@@ -0,0 +1,52 @@
+package com.kuit.ourmenu.utils.auth
+
+import android.content.Context
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.preferencesDataStore
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+import javax.inject.Singleton
+
+private val Context.dataStore by preferencesDataStore(name = "user_prefs")
+
+@Singleton
+class TokenManager @Inject constructor(@ApplicationContext private val context: Context) {
+ companion object {
+ private val ACCESS_TOKEN = stringPreferencesKey("access_token")
+ private val REFRESH_TOKEN = stringPreferencesKey("refresh_token")
+ }
+
+ fun getAccessToken(): Flow {
+ return context.dataStore.data.map { preferences ->
+ preferences[ACCESS_TOKEN]
+ }
+ }
+
+ fun getRefreshToken(): Flow {
+ return context.dataStore.data.map { preferences ->
+ preferences[REFRESH_TOKEN]
+ }
+ }
+
+ suspend fun saveAccessToken(accessToken: String) {
+ context.dataStore.edit { preferences ->
+ preferences[ACCESS_TOKEN] = accessToken
+ }
+ }
+
+ suspend fun saveRefreshToken(refreshToken: String) {
+ context.dataStore.edit { preferences ->
+ preferences[REFRESH_TOKEN] = refreshToken
+ }
+ }
+
+ suspend fun clearToken() {
+ context.dataStore.edit { preferences ->
+ preferences.remove(ACCESS_TOKEN)
+ preferences.remove(REFRESH_TOKEN)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/utils/dragndrop/DragAndDropListState.kt b/app/src/main/java/com/kuit/ourmenu/utils/dragndrop/DragAndDropListState.kt
new file mode 100644
index 00000000..b7a055d8
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/utils/dragndrop/DragAndDropListState.kt
@@ -0,0 +1,121 @@
+package com.kuit.ourmenu.utils.dragndrop
+
+import androidx.compose.foundation.lazy.LazyListItemInfo
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.geometry.Offset
+
+@Composable
+fun rememberDragAndDropListState(
+ lazyListState: LazyListState,
+ onMove: (Int, Int) -> Unit
+): DragAndDropListState {
+ return remember { DragAndDropListState(lazyListState, onMove) }
+}
+
+class DragAndDropListState(
+ val lazyListState: LazyListState,
+ private val onMove: (Int, Int) -> Unit
+) {
+ private var draggingDistance by mutableFloatStateOf(0f)
+ private var initialDraggingElement by mutableStateOf(null)
+
+ val initialIndex: Int?
+ get() = initialDraggingElement?.index?.takeIf { it > 0 }?.minus(1) ?: -1
+ val endIndex: Int?
+ get() = currentIndexOfDraggedItem?.takeIf { it >= 0 }
+ var currentIndexOfDraggedItem by mutableStateOf(null)
+ private val initialOffsets: Pair?
+ get() = initialDraggingElement?.let { Pair(it.offset, it.offsetEnd) }
+ val elementDisplacement: Float?
+ get() = currentIndexOfDraggedItem?.let {
+ lazyListState.getVisibleItemInfo(it + 1)
+ }?.let { itemInfo ->
+ (initialDraggingElement?.offset ?: 0f).toFloat() + draggingDistance - itemInfo.offset
+ }
+ private val currentElement: LazyListItemInfo?
+ get() = currentIndexOfDraggedItem?.let {
+ lazyListState.getVisibleItemInfo(it + 1)
+ }
+
+ fun onDragStart(offset: Offset) {
+ lazyListState.layoutInfo.visibleItemsInfo
+ .firstOrNull { item -> offset.y.toInt() in item.offset..item.offsetEnd }
+ ?.also {
+ if (it.index > 0) {
+ initialDraggingElement = it
+ currentIndexOfDraggedItem = it.index - 1
+ }
+ }
+ }
+
+ fun onDragInterrupted() {
+ initialDraggingElement = null
+ currentIndexOfDraggedItem = null
+ draggingDistance = 0f
+ }
+
+ fun onDrag(offset: Offset) {
+ draggingDistance += offset.y
+
+ initialOffsets?.let { (top, bottom) ->
+ val startOffset = top.toFloat() + draggingDistance
+ val endOffset = bottom.toFloat() + draggingDistance
+
+ currentElement?.let { current ->
+ lazyListState.layoutInfo.visibleItemsInfo
+ .filterNot { item ->
+ item.offsetEnd < startOffset || item.offset > endOffset || current.index == item.index
+ }
+ .firstOrNull { item ->
+ val delta = startOffset - current.offset
+ when {
+ delta < 0 -> item.offset > startOffset
+ else -> item.offsetEnd < endOffset
+ }
+ }
+ }?.also { item ->
+ currentIndexOfDraggedItem?.let { current ->
+ if (item.index > 0) {
+ onMove.invoke(current, item.index - 1)
+ }
+ }
+ if (item.index > 0) {
+ currentIndexOfDraggedItem = item.index - 1
+ }
+ }
+ }
+ }
+
+ fun checkOverscroll(): Float {
+ return initialDraggingElement?.let {
+ val startOffset = it.offset + draggingDistance
+ val endOffset = it.offsetEnd + draggingDistance
+
+ return@let when {
+ draggingDistance > 0 -> {
+ (endOffset - lazyListState.layoutInfo.viewportEndOffset).takeIf { diff -> diff > 0 }
+
+ }
+
+ draggingDistance < 0 -> {
+ (startOffset - lazyListState.layoutInfo.viewportStartOffset).takeIf { diff -> diff < 0 }
+ }
+
+ else -> null
+ }
+ } ?: 0f
+ }
+
+ private fun LazyListState.getVisibleItemInfo(itemPosition: Int): LazyListItemInfo? {
+ return this.layoutInfo.visibleItemsInfo.getOrNull(itemPosition - this.firstVisibleItemIndex)
+ }
+
+ private val LazyListItemInfo.offsetEnd: Int
+ get() = this.offset + this.size
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kuit/ourmenu/utils/dragndrop/Ext.kt b/app/src/main/java/com/kuit/ourmenu/utils/dragndrop/Ext.kt
new file mode 100644
index 00000000..f830e58e
--- /dev/null
+++ b/app/src/main/java/com/kuit/ourmenu/utils/dragndrop/Ext.kt
@@ -0,0 +1,32 @@
+package com.kuit.ourmenu.utils.dragndrop
+
+import android.util.Log
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.zIndex
+
+fun MutableList.move(from: Int, to: Int) {
+ if (from == to) return
+ if (to > this.size - 1) return
+ Log.d("DragAndDrop", "Moving item from $from to $to , ${this.size}")
+ val element = this.removeAt(from)
+ try {
+ this.add(to, element)
+ } catch (e: IndexOutOfBoundsException) {
+ this.add(element)
+ }
+}
+
+fun Modifier.dragModifier(index: Int, dragAndDropListState: DragAndDropListState) = composed {
+ val isDragging = index == dragAndDropListState.currentIndexOfDraggedItem
+ val offsetOrNull = dragAndDropListState.elementDisplacement.takeIf { isDragging }
+
+ this
+ .zIndex(if (isDragging) 1f else 0f)
+ .graphicsLayer {
+ translationY = offsetOrNull ?: 0f
+ scaleX = if (isDragging) 1.03f else 1f
+ scaleY = if (isDragging) 1.03f else 1f
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_addmenu_add_photo.xml b/app/src/main/res/drawable/ic_addmenu_add_photo.xml
new file mode 100644
index 00000000..82f508fd
--- /dev/null
+++ b/app/src/main/res/drawable/ic_addmenu_add_photo.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_addmenu_noresult.xml b/app/src/main/res/drawable/ic_addmenu_noresult.xml
new file mode 100644
index 00000000..9b49aeb5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_addmenu_noresult.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_addmenu_x.xml b/app/src/main/res/drawable/ic_addmenu_x.xml
new file mode 100644
index 00000000..edd82a02
--- /dev/null
+++ b/app/src/main/res/drawable/ic_addmenu_x.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_arrow_right.xml b/app/src/main/res/drawable/ic_arrow_right.xml
new file mode 100644
index 00000000..3e7ac2e4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_arrow_right.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml
new file mode 100644
index 00000000..201bea84
--- /dev/null
+++ b/app/src/main/res/drawable/ic_back.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_btn_check_white.xml b/app/src/main/res/drawable/ic_btn_check_white.xml
new file mode 100644
index 00000000..386cdf1e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_btn_check_white.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_btn_plus_orange.xml b/app/src/main/res/drawable/ic_btn_plus_orange.xml
new file mode 100644
index 00000000..1afb6e33
--- /dev/null
+++ b/app/src/main/res/drawable/ic_btn_plus_orange.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_clock.xml b/app/src/main/res/drawable/ic_clock.xml
new file mode 100644
index 00000000..cf123ab7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_clock.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_close_24_n400.xml b/app/src/main/res/drawable/ic_close_24_n400.xml
new file mode 100644
index 00000000..596b6e30
--- /dev/null
+++ b/app/src/main/res/drawable/ic_close_24_n400.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_dropdown_btn.xml b/app/src/main/res/drawable/ic_dropdown_btn.xml
new file mode 100644
index 00000000..29ad6128
--- /dev/null
+++ b/app/src/main/res/drawable/ic_dropdown_btn.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_dropdown_btn_up.xml b/app/src/main/res/drawable/ic_dropdown_btn_up.xml
new file mode 100644
index 00000000..0a7f15f1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_dropdown_btn_up.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_dropdown_checked.xml b/app/src/main/res/drawable/ic_dropdown_checked.xml
new file mode 100644
index 00000000..61e554c9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_dropdown_checked.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_dropdown_unchecked.xml b/app/src/main/res/drawable/ic_dropdown_unchecked.xml
new file mode 100644
index 00000000..f3fdbfa1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_dropdown_unchecked.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml
new file mode 100644
index 00000000..88ad5758
--- /dev/null
+++ b/app/src/main/res/drawable/ic_edit.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_expand_down.xml b/app/src/main/res/drawable/ic_expand_down.xml
new file mode 100644
index 00000000..13e469cc
--- /dev/null
+++ b/app/src/main/res/drawable/ic_expand_down.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_expand_up.xml b/app/src/main/res/drawable/ic_expand_up.xml
new file mode 100644
index 00000000..842a13c4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_expand_up.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_fill_map_20.xml b/app/src/main/res/drawable/ic_fill_map_20.xml
new file mode 100644
index 00000000..734ac1bd
--- /dev/null
+++ b/app/src/main/res/drawable/ic_fill_map_20.xml
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_filter.xml b/app/src/main/res/drawable/ic_filter.xml
new file mode 100644
index 00000000..be4e2826
--- /dev/null
+++ b/app/src/main/res/drawable/ic_filter.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_filter_white_16.xml b/app/src/main/res/drawable/ic_filter_white_16.xml
new file mode 100644
index 00000000..bfbef61e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_filter_white_16.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_home_dialog_touch.xml b/app/src/main/res/drawable/ic_home_dialog_touch.xml
new file mode 100644
index 00000000..52683691
--- /dev/null
+++ b/app/src/main/res/drawable/ic_home_dialog_touch.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_home_plus.xml b/app/src/main/res/drawable/ic_home_plus.xml
new file mode 100644
index 00000000..9d0cd9d7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_home_plus.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_home_reco_1.png b/app/src/main/res/drawable/ic_home_reco_1.png
new file mode 100644
index 00000000..1308c301
Binary files /dev/null and b/app/src/main/res/drawable/ic_home_reco_1.png differ
diff --git a/app/src/main/res/drawable/ic_home_sub_reco_1.png b/app/src/main/res/drawable/ic_home_sub_reco_1.png
new file mode 100644
index 00000000..fabfff49
Binary files /dev/null and b/app/src/main/res/drawable/ic_home_sub_reco_1.png differ
diff --git a/app/src/main/res/drawable/ic_icon_ellipse.xml b/app/src/main/res/drawable/ic_icon_ellipse.xml
new file mode 100644
index 00000000..bd8941f3
--- /dev/null
+++ b/app/src/main/res/drawable/ic_icon_ellipse.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_kebab.xml b/app/src/main/res/drawable/ic_kebab.xml
new file mode 100644
index 00000000..75697d94
--- /dev/null
+++ b/app/src/main/res/drawable/ic_kebab.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_menu_info_meno_20.xml b/app/src/main/res/drawable/ic_menu_info_meno_20.xml
new file mode 100644
index 00000000..88ad5758
--- /dev/null
+++ b/app/src/main/res/drawable/ic_menu_info_meno_20.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_menu_info_nav_24.xml b/app/src/main/res/drawable/ic_menu_info_nav_24.xml
new file mode 100644
index 00000000..467ffb4a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_menu_info_nav_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_my_menu_title.xml b/app/src/main/res/drawable/ic_my_menu_title.xml
new file mode 100644
index 00000000..189a443d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_my_menu_title.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_navigation_bar_folder.xml b/app/src/main/res/drawable/ic_navigation_bar_folder.xml
new file mode 100644
index 00000000..17b52515
--- /dev/null
+++ b/app/src/main/res/drawable/ic_navigation_bar_folder.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_navigation_bar_folder_selected.xml b/app/src/main/res/drawable/ic_navigation_bar_folder_selected.xml
new file mode 100644
index 00000000..454e670c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_navigation_bar_folder_selected.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_navigation_bar_home.xml b/app/src/main/res/drawable/ic_navigation_bar_home.xml
new file mode 100644
index 00000000..f48c5ec2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_navigation_bar_home.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_navigation_bar_home_selected.xml b/app/src/main/res/drawable/ic_navigation_bar_home_selected.xml
new file mode 100644
index 00000000..fea6e40b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_navigation_bar_home_selected.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_navigation_bar_map.xml b/app/src/main/res/drawable/ic_navigation_bar_map.xml
new file mode 100644
index 00000000..7696d134
--- /dev/null
+++ b/app/src/main/res/drawable/ic_navigation_bar_map.xml
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_navigation_bar_map_selected.xml b/app/src/main/res/drawable/ic_navigation_bar_map_selected.xml
new file mode 100644
index 00000000..0888be05
--- /dev/null
+++ b/app/src/main/res/drawable/ic_navigation_bar_map_selected.xml
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_navigation_bar_my.xml b/app/src/main/res/drawable/ic_navigation_bar_my.xml
new file mode 100644
index 00000000..6ca51b24
--- /dev/null
+++ b/app/src/main/res/drawable/ic_navigation_bar_my.xml
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_navigation_bar_my_selected.xml b/app/src/main/res/drawable/ic_navigation_bar_my_selected.xml
new file mode 100644
index 00000000..ad1d5412
--- /dev/null
+++ b/app/src/main/res/drawable/ic_navigation_bar_my_selected.xml
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_ourmenu_text_logo.xml b/app/src/main/res/drawable/ic_ourmenu_text_logo.xml
new file mode 100644
index 00000000..f27991e9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_ourmenu_text_logo.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_profile.xml b/app/src/main/res/drawable/ic_profile.xml
new file mode 100644
index 00000000..034680f6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_profile.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_searchbar_search.xml b/app/src/main/res/drawable/ic_searchbar_search.xml
new file mode 100644
index 00000000..18a88098
--- /dev/null
+++ b/app/src/main/res/drawable/ic_searchbar_search.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_snackbar_check.xml b/app/src/main/res/drawable/ic_snackbar_check.xml
new file mode 100644
index 00000000..388aec9a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_snackbar_check.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_snackbar_error.xml b/app/src/main/res/drawable/ic_snackbar_error.xml
new file mode 100644
index 00000000..be9f0a53
--- /dev/null
+++ b/app/src/main/res/drawable/ic_snackbar_error.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_splash_logo.xml b/app/src/main/res/drawable/ic_splash_logo.xml
new file mode 100644
index 00000000..02035b9c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_splash_logo.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_tag_rice.xml b/app/src/main/res/drawable/ic_tag_rice.xml
new file mode 100644
index 00000000..0f65bba4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_tag_rice.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_to_map.xml b/app/src/main/res/drawable/ic_to_map.xml
new file mode 100644
index 00000000..330c9a23
--- /dev/null
+++ b/app/src/main/res/drawable/ic_to_map.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_top_bar_back.xml b/app/src/main/res/drawable/ic_top_bar_back.xml
new file mode 100644
index 00000000..12cf4730
--- /dev/null
+++ b/app/src/main/res/drawable/ic_top_bar_back.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_top_bar_logo.xml b/app/src/main/res/drawable/ic_top_bar_logo.xml
new file mode 100644
index 00000000..9b91b31f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_top_bar_logo.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_trashcan.xml b/app/src/main/res/drawable/ic_trashcan.xml
new file mode 100644
index 00000000..f83f096c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_trashcan.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/icon_vert_white_24.xml b/app/src/main/res/drawable/icon_vert_white_24.xml
new file mode 100644
index 00000000..429b3cad
--- /dev/null
+++ b/app/src/main/res/drawable/icon_vert_white_24.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/img_dummy_menu.png b/app/src/main/res/drawable/img_dummy_menu.png
new file mode 100644
index 00000000..d5fa26e2
Binary files /dev/null and b/app/src/main/res/drawable/img_dummy_menu.png differ
diff --git a/app/src/main/res/drawable/img_dummy_pizza.jpg b/app/src/main/res/drawable/img_dummy_pizza.jpg
new file mode 100644
index 00000000..f6783e56
Binary files /dev/null and b/app/src/main/res/drawable/img_dummy_pizza.jpg differ
diff --git a/app/src/main/res/drawable/img_home_popup_thumbsup.png b/app/src/main/res/drawable/img_home_popup_thumbsup.png
new file mode 100644
index 00000000..7ffdf019
Binary files /dev/null and b/app/src/main/res/drawable/img_home_popup_thumbsup.png differ
diff --git a/app/src/main/res/drawable/img_kakao_login_medium_wide.png b/app/src/main/res/drawable/img_kakao_login_medium_wide.png
new file mode 100644
index 00000000..2d369e2e
Binary files /dev/null and b/app/src/main/res/drawable/img_kakao_login_medium_wide.png differ
diff --git a/app/src/main/res/drawable/img_menu_folder_dummy.png b/app/src/main/res/drawable/img_menu_folder_dummy.png
new file mode 100644
index 00000000..7d15ed3c
Binary files /dev/null and b/app/src/main/res/drawable/img_menu_folder_dummy.png differ
diff --git a/app/src/main/res/drawable/img_popup_dice.png b/app/src/main/res/drawable/img_popup_dice.png
new file mode 100644
index 00000000..7c3374b2
Binary files /dev/null and b/app/src/main/res/drawable/img_popup_dice.png differ
diff --git a/app/src/main/res/font/black.ttf b/app/src/main/res/font/black.ttf
new file mode 100644
index 00000000..d0c1db81
Binary files /dev/null and b/app/src/main/res/font/black.ttf differ
diff --git a/app/src/main/res/font/bold.ttf b/app/src/main/res/font/bold.ttf
new file mode 100644
index 00000000..fb07fc65
Binary files /dev/null and b/app/src/main/res/font/bold.ttf differ
diff --git a/app/src/main/res/font/extrabold.ttf b/app/src/main/res/font/extrabold.ttf
new file mode 100644
index 00000000..9d5fe072
Binary files /dev/null and b/app/src/main/res/font/extrabold.ttf differ
diff --git a/app/src/main/res/font/extralight.ttf b/app/src/main/res/font/extralight.ttf
new file mode 100644
index 00000000..09e65428
Binary files /dev/null and b/app/src/main/res/font/extralight.ttf differ
diff --git a/app/src/main/res/font/light.ttf b/app/src/main/res/font/light.ttf
new file mode 100644
index 00000000..2e8541d6
Binary files /dev/null and b/app/src/main/res/font/light.ttf differ
diff --git a/app/src/main/res/font/medium.ttf b/app/src/main/res/font/medium.ttf
new file mode 100644
index 00000000..1db67c68
Binary files /dev/null and b/app/src/main/res/font/medium.ttf differ
diff --git a/app/src/main/res/font/regular.ttf b/app/src/main/res/font/regular.ttf
new file mode 100644
index 00000000..01147e99
Binary files /dev/null and b/app/src/main/res/font/regular.ttf differ
diff --git a/app/src/main/res/font/semibold.ttf b/app/src/main/res/font/semibold.ttf
new file mode 100644
index 00000000..9f2690f0
Binary files /dev/null and b/app/src/main/res/font/semibold.ttf differ
diff --git a/app/src/main/res/font/thin.ttf b/app/src/main/res/font/thin.ttf
new file mode 100644
index 00000000..fe9825f1
Binary files /dev/null and b/app/src/main/res/font/thin.ttf differ
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 87da0c4f..edad81bd 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,134 @@
OurMenu
+ ์์๋ก๋ณถ์ด
+ ์์ธ ๊ด์ง๊ตฌ ๋ฅ๋๋ก 112
+ ๋ฉ๋ด ์ง์ ์ถ๊ฐํ๊ธฐ
+ ๋ค์
+ ํ์ธ
+ ๋ฉ๋ด ์ถ๊ฐํ๊ธฐ
+ *
+ ๋ฉ๋ดํ
+ ๋ฉ๋ดํ์ ์ ํํด์ฃผ์ธ์
+ ๋ฉ๋ด ์ด๋ฆ
+ ๋ฉ๋ด ์ด๋ฆ์ ์
๋ ฅํด์ฃผ์ธ์
+ ๋ฉ๋ด ๊ฐ๊ฒฉ
+ ์ง์ ๊ฐ๊ฒฉ์ ์
๋ ฅํด์ฃผ์ธ์
+ ๊ฐ๊ฒ ์ด๋ฆ
+ ๊ฐ๊ฒ ์ด๋ฆ์ ์
๋ ฅํด์ฃผ์ธ์
+ ๊ฐ๊ฒ ์ฃผ์
+ ๊ฐ๊ฒ ์ฃผ์๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.
+ ์์ธ ์ฃผ์๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.
+ OURMENU
+ ๊ฐ๊ฒ์ ๋ฉ๋ด ์ง์ ์ถ๊ฐ
+ ๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์์ด์!
+ ์ต๊ทผ ๊ฒ์
+ ์ทจ์
+ ์ ์ฉํ๊ธฐ
+ placeholder
+ ํ๊ทธ
+ ํ๊ทธ ๊ณ ๋ฅด๊ธฐ
+ ๋ฉ๋ชจ
+ ์์ด์ฝ
+ ์ ๋ชฉ์ ์
๋ ฅํด์ฃผ์ธ์.
+ ๋ณธ๋ฌธ์ ์
๋ ฅํด์ฃผ์ธ์.
+ ์์ ํ๊ทธ
+ ์ค๋ณต ์ ํ ๊ฐ๋ฅ
+ ๊ฐ๊ฒ ์ด๋ฆ ๊ฒ์
+
+
+ ์์๋ฉ๋ด์ ์จ ๊ฑธ ํ์ํด์!
+ ์ ํฌ์ ํจ๊ป ๋งค์ผ ๋จน์ ํ ๋ผ๋ฅผ ๊ณ ๋ฏผํด์.
+ ๋ก๊ทธ์ธ
+ ๊ณ์ ๋ง๋ค๊ธฐ
+ Copyright ยฉ OURMENU Corp. All Rights Reserved.
+ E-mail
+ Password
+ ๋น๋ฐ๋ฒํธ ๋ณด๊ธฐ
+ ๋น๋ฐ๋ฒํธ ์ฐพ๊ธฐ
+ ๊ณ์ ์ด ์์ผ์ ๊ฐ์?
+ ์ด๋ฉ์ผ์ ์
๋ ฅํด์ฃผ์ธ์.
+ ๋ก๊ทธ์ธ ๋ฐ ํ์๊ฐ์
, ๋ณธ์ธํ์ธ์ ํ์ํฉ๋๋ค.
+ default
+ ์ง์ ์
๋ ฅํ๊ธฐ
+ ์ง์ ์
๋ ฅํ๊ธฐ
+ daum.net
+ gmail.com
+ kakao.com
+ nate.com
+ naver.com
+ ๋ฉ์ผ์ ๋ณด๋์ต๋๋ค!
+ 5๋ถ ์์
+ ์ธ์ฆ ์ฝ๋๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.
+ ๋ฉ์ผ์ด ์ค์ง ์์๋์?
+ ๋ฉ์ผ ๋ค์ ๋ณด๋ด๊ธฐ
+ ๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.
+ ์๋ฌธ, ์ซ์ ํฌํจ 8์ ์ด์
+ ๊ฐ๋ ฅํ ๋น๋ฐ๋ฒํธ๋ฅผ ๋ง๋ค์ด์ฃผ์ธ์!
+ ๋น๋ฐ๋ฒํธ๋ฅผ ํ ๋ฒ ๋ ํ์ธํด์ฃผ์ธ์.
+ ์ธ์ฆ ๋ฉ์ผ ๋ณด๋ด๊ธฐ
+ ํ์ ์์ฌ์๊ฐ์ ์
๋ ฅํด์ฃผ์ธ์.
+ ๋์ค์ ์์ ํ ์ ์์ผ๋ ๊ฑฑ์ ๋ง์ธ์!
+ ์ต๋ 10์
+
+
+ ์ง๋ ์ฑ์ผ๋ก ์ด๋
+
+
+ Menu Folder
+ Tag
+ Info
+ Memo
+ ๋งค์ด ๋ง ์ ํ ๊ฐ๋ฅ!
+ * ๋์ ์ฐ / ใ
ใ
์ฐ / ํ๋ผ์ฐ\n๊ธฐ๋ณธ๋ ์ ๋ง ๋งค์ฐ๋ ์กฐ์ฌํ๊ธฐ
+ ์ง๋๋ณด๊ธฐ
+
+
+ ๊ฐ๊ฒ, ๋ฉ๋ด ์ด๋ฆ ๊ฒ์
+ ํ๊ทธ๋ ์ต๋ 12๊ฐ๊น์ง ๋ฑ๋กํ ์ ์์ด์
+ ์ด๊ธฐํ
+
+
+ ์ญ์
+ ์์
+ ์ ์ฒด ๋ฉ๋ด ๋ณด๊ธฐ
+ ๋ฉ๋ด %d๊ฐ
+ ๋ฉ๋ดํ ์ถ๊ฐํ๊ธฐ
+ %d ์
+ ์ ์ฒด ๋ฉ๋ด
+ %d ๊ฐ
+ ํ๊ฐ ๋ทฐ ๋ง์ง
+ ํํฐ๋ง
+ ๊ฐ ํญ๋ชฉ ๋น 1๊ฐ ์ ํ
+ ๊ฐ๊ฒฉ๋
+ %d์ ~ %d์
+ ํ๊ทธ๋ ํญ๋ชฉ ๋น 1๊ฐ๋ง ์ ํํ ์ ์์ด์
+
+ ์ข
๋ฅ
+ ๋๋ผ ๋ณ ์์
+ ๋ง
+ ์ํฉ
+
+ ์ ๋ง๋ก ์ญ์ ํ์๊ฒ ์ด์?
+ ์ญ์ ํ๋ฉด ๋ณต๊ตฌ๊ฐ ์ด๋ ค์ธ ์ ์์ด์.\n๋ค์ ํ ๋ฒ ํ์ธํด ์ฃผ์ธ์.
+
+
+ ํ์ฌ ์ค์ ํ์ ์์ฌ ์๊ฐ์
๋๋ค!
+ ์์ฌ ์๊ฐ ์์ ํ๊ธฐ
+ ๊ณต์ง์ฌํญ
+ ๊ณ ๊ฐ์ผํฐ
+ ์ฑ ๋ฆฌ๋ทฐ ๋จ๊ธฐ๊ธฐ
+ ํ์ฌ ๋ฒ์ : %s
+
+ ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝํ๊ธฐ
+ ๋ก๊ทธ์์
+ ํ์ ํํด
+
+ ๊ธฐ์กด ๋น๋ฐ๋ฒํธ
+ ๊ธฐ์กด ๋น๋ฐ๋ฒํธ๋ฅผ\n์
๋ ฅํด์ฃผ์ธ์.
+ ๋ณ๊ฒฝํ ๋น๋ฐ๋ฒํธ
+ ๋ณ๊ฒฝํ ๋น๋ฐ๋ฒํธ ํ์ธ
+ ๋ณ๊ฒฝํ ๋น๋ฐ๋ฒํธ๋ฅผ\n์
๋ ฅํด์ฃผ์ธ์.
+ ๋ก๊ทธ์์ ํ์๊ฒ ์ต๋๊น?
+ ํํด ํ์๊ฒ ์ต๋๊น?
+ ํํด ์ ๊ณ์ ๋ฐ ์ด์ฉ ๊ธฐ๋ก์ ๋ชจ๋ ์ญ์ ๋๋ฉฐ,\n์ญ์ ๋ ๋ฐ์ดํฐ๋ ๋ณต๊ตฌ๊ฐ ๋ถ๊ฐ๋ฅํฉ๋๋ค.
\ No newline at end of file
diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml
new file mode 100644
index 00000000..36f8d32e
--- /dev/null
+++ b/app/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 952b9306..464702a5 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -3,4 +3,6 @@ plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
-}
\ No newline at end of file
+ alias(libs.plugins.dagger.hilt) apply false
+ alias(libs.plugins.firebase.crashlytics) apply false
+}
diff --git a/gradle.properties b/gradle.properties
index 20e2a015..2661ee69 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -20,4 +20,6 @@ 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
-android.nonTransitiveRClass=true
\ No newline at end of file
+android.nonTransitiveRClass=true
+
+android.defaults.buildfeatures.buildconfig=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 60cfe30c..82cbe271 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,16 +1,44 @@
[versions]
agp = "8.7.3"
+coilCompose = "3.1.0"
+converterGson = "2.11.0"
+firebaseBom = "34.1.0"
+hiltAndroid = "2.52"
+hiltNavigationCompose = "1.2.0"
kotlin = "2.0.0"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
+kotlinxCollectionsImmutable = "0.3.5"
+kotlinxSerializationJson = "1.6.0"
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
composeBom = "2024.04.01"
+lifecycleViewmodel = "2.9.0-alpha08"
+lifecycleViewmodelCompose = "2.9.0-alpha08"
+lifecycleRuntimeComposeAndroid = "2.8.7"
+retrofit = "2.11.0"
+navigationCompose = "2.8.6"
+okhttp = "4.11.0"
+retrofitKotlinSerializationConverter = "1.0.0"
+datastorePreferences = "1.1.3"
+playServicesLocation = "21.3.0"
+firebaseCrashlytics = "3.0.6"
+googleServices = "4.4.3"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
+coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" }
+coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coilCompose" }
+coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coilCompose" }
+converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
+firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
+firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
+firebase-crashlytics-ndk = { module = "com.google.firebase:firebase-crashlytics-ndk" }
+hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
+hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltAndroid" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
@@ -24,9 +52,25 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
+kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
+androidx-lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "lifecycleViewmodel" }
+androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
+androidx-lifecycle-runtime-compose-android = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose-android", version.ref = "lifecycleRuntimeComposeAndroid" }
+kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
+retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
+androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
+okhttp-bom = { group = "com.squareup.okhttp3", name = "okhttp-bom", version.ref = "okhttp" }
+okhttp = { group = "com.squareup.okhttp3", name = "okhttp" }
+okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor" }
+retrofit-kotlin-serialization-converter = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinSerializationConverter" }
+androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastorePreferences" }
+play-services-location = { group = "com.google.android.gms", name = "play-services-location", version.ref = "playServicesLocation" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
-
+dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" }
+firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlytics" }
+google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" }
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 9d590ce6..7f67b36f 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -16,6 +16,8 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
+ maven { url = java.net.URI("https://devrepo.kakao.com/nexus/content/groups/public/") }
+ maven { url = uri("https://devrepo.kakao.com/nexus/repository/kakaomap-releases/") }
}
}