diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/TopAppBar.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/TopAppBar.kt
index ddc25238..d932fd7f 100644
--- a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/TopAppBar.kt
+++ b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/TopAppBar.kt
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
@@ -21,12 +22,14 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
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.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.skydoves.balloon.ArrowOrientation
import com.skydoves.balloon.BalloonAnimation
@@ -55,6 +58,7 @@ fun UnifestTopAppBar(
contentColor: Color = Color.Black,
onNavigationClick: () -> Unit = {},
onTitleClick: (Boolean) -> Unit = {},
+ elevation: Dp = 0.dp,
) {
CompositionLocalProvider(LocalContentColor provides contentColor) {
val icon: @Composable (Modifier, imageVector: ImageVector) -> Unit =
@@ -73,6 +77,7 @@ fun UnifestTopAppBar(
Box(
modifier = Modifier
.fillMaxWidth()
+ .shadow(elevation, RoundedCornerShape(bottomEnd = 20.dp, bottomStart = 20.dp))
.background(containerColor)
.then(modifier),
) {
diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Font.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Font.kt
index 7181fe33..c9ba24e3 100644
--- a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Font.kt
+++ b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Font.kt
@@ -134,6 +134,18 @@ val Content6 = TextStyle(
fontSize = 12.sp,
)
+val Content7 = TextStyle(
+ fontFamily = pretendardFamily,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 11.sp,
+)
+
+val Content8 = TextStyle(
+ fontFamily = pretendardFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 15.sp,
+)
+
val WaitingTeam = TextStyle(
fontFamily = pretendardFamily,
fontWeight = FontWeight.Bold,
diff --git a/core/designsystem/src/main/res/drawable/ic_admin_mode.xml b/core/designsystem/src/main/res/drawable/ic_admin_mode.xml
new file mode 100644
index 00000000..f30ab67a
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_admin_mode.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/core/designsystem/src/main/res/drawable/ic_inquiry.xml b/core/designsystem/src/main/res/drawable/ic_inquiry.xml
new file mode 100644
index 00000000..7ff3a4a4
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_inquiry.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/core/designsystem/src/main/res/drawable/ic_selected_menu.xml b/core/designsystem/src/main/res/drawable/ic_selected_menu.xml
new file mode 100644
index 00000000..02eca978
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/ic_selected_menu.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml
index 1d9e3e61..d4858f15 100644
--- a/core/designsystem/src/main/res/values/strings.xml
+++ b/core/designsystem/src/main/res/values/strings.xml
@@ -45,6 +45,19 @@
부스 / 주점을 검색해보세요.
여기를 눌러서 학교를 검색해보세요.
+
+ 메뉴
+ 나의 관심 학교
+ 추가하기>
+ 관심 부스
+ 더보기>
+ 이용 문의
+ 운영자 모드 진입
+
+
+ 관심 부스
+
+
학교 / 축제 이름을 검색해보세요.
나의 관심 축제
diff --git a/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/InterestedBoothScreen.kt b/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/InterestedBoothScreen.kt
index fed2552b..4bf87204 100644
--- a/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/InterestedBoothScreen.kt
+++ b/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/InterestedBoothScreen.kt
@@ -1,17 +1,48 @@
package com.unifest.android.feature.interested_booth
-import androidx.compose.foundation.layout.Arrangement
+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.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.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+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.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.unifest.android.core.designsystem.R
+import com.unifest.android.core.designsystem.component.NetworkImage
+import com.unifest.android.core.designsystem.component.TopAppBarNavigationType
+import com.unifest.android.core.designsystem.component.UnifestTopAppBar
+import com.unifest.android.core.designsystem.theme.Title2
+import com.unifest.android.core.designsystem.theme.Title5
import com.unifest.android.core.designsystem.theme.UnifestTheme
import com.unifest.android.core.domain.entity.BoothDetailEntity
+import com.unifest.android.core.domain.entity.MenuEntity
import com.unifest.android.core.ui.DevicePreview
import com.unifest.android.feature.interested_booth.viewmodel.InterestedBoothUiState
import com.unifest.android.feature.interested_booth.viewmodel.InterestedBoothViewModel
@@ -19,23 +50,122 @@ import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun InterestedBoothRoute(
+ padding: PaddingValues,
+ onBackClick: () -> Unit,
viewModel: InterestedBoothViewModel = hiltViewModel(),
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
-
- InterestedBoothScreen(uiState = uiState)
+ InterestedBoothScreen(
+ padding = padding,
+ uiState = uiState,
+ onBackClick = onBackClick,
+ )
}
@Composable
internal fun InterestedBoothScreen(
+ padding: PaddingValues,
uiState: InterestedBoothUiState,
+ onBackClick: () -> Unit,
) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding),
+ ) {
+ Column {
+ UnifestTopAppBar(
+ navigationType = TopAppBarNavigationType.Back,
+ onNavigationClick = onBackClick,
+ title = stringResource(id = R.string.interested_booths_title),
+ elevation = 8.dp,
+ modifier = Modifier
+ .background(
+ Color.White,
+ shape = RoundedCornerShape(bottomEnd = 20.dp, bottomStart = 20.dp),
+ )
+ .padding(top = 13.dp, bottom = 5.dp),
+ )
+ LazyColumn {
+ itemsIndexed(
+ uiState.interestedBooths,
+ key = { _, booth -> booth.id },
+ ) { index, booth ->
+ InterestedBoothsItems(booth, index, uiState.interestedBooths.size)
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun InterestedBoothsItems(booth: BoothDetailEntity, index: Int, total: Int) {
+ var isBookmarked by remember { mutableStateOf(true) }
+ val bookMarkColor = if (isBookmarked) Color(0xFFF5687E) else Color(0xFF4B4B4B)
Column(
- modifier = Modifier.fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center,
+ modifier = Modifier
+ .clickable { /* 클릭 이벤트 처리 */ }
+ .padding(horizontal = 20.dp),
) {
- Text(uiState.interestedBooths.toString())
+ Spacer(modifier = Modifier.height(16.dp))
+ Row(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ NetworkImage(
+ imageUrl = "https://picsum.photos/86",
+ modifier = Modifier
+ .size(86.dp)
+ .clip(RoundedCornerShape(16.dp)),
+ )
+ Spacer(modifier = Modifier.width(14.dp))
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth(),
+ ) {
+ Text(
+ text = booth.name,
+ style = Title2,
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = booth.description,
+ style = Title5,
+ color = Color(0xFF545454),
+ )
+ Spacer(modifier = Modifier.height(13.dp))
+ Row {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_location_green),
+ contentDescription = "Location Icon",
+ tint = Color.Unspecified,
+ )
+ Spacer(modifier = Modifier.width(3.dp))
+ Text(
+ text = booth.location,
+ style = Title5,
+ color = Color(0xFF545454),
+ modifier = Modifier.align(Alignment.CenterVertically),
+ )
+ }
+ }
+ Icon(
+ imageVector = ImageVector.vectorResource(if (isBookmarked) R.drawable.ic_bookmarked else R.drawable.ic_bookmark),
+ contentDescription = if (isBookmarked) "북마크됨" else "북마크하기",
+ tint = bookMarkColor,
+ modifier = Modifier
+ .size(24.dp)
+ .clickable(onClick = { isBookmarked = !isBookmarked }),
+ )
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ if (index < total - 1) {
+ HorizontalDivider(
+ color = Color(0xFFDFDFDF),
+ thickness = 1.dp,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
}
}
@@ -44,13 +174,118 @@ internal fun InterestedBoothScreen(
fun InterestedBoothScreenPreview() {
UnifestTheme {
InterestedBoothScreen(
+ padding = PaddingValues(),
+ onBackClick = {},
uiState = InterestedBoothUiState(
interestedBooths = persistentListOf(
- BoothDetailEntity(1L, "컴공 주점", "컴퓨터공학부 전용 부스"),
- BoothDetailEntity(2L, "학생회 부스", "건국대학교 학생회 부스"),
- BoothDetailEntity(3L, "컴공 주점", "컴퓨터공학부 전용 부스"),
- BoothDetailEntity(4L, "학생회 부스", "건국대학교 학생회 부스"),
- BoothDetailEntity(5L, "컴공 주점", "컴퓨터공학부 전용 부스"),
+ BoothDetailEntity(
+ id = 1,
+ name = "부스 이름",
+ category = "음식",
+ description = "부스 설명",
+ warning = "주의사항",
+ location = "부스 위치",
+ latitude = 0.0f,
+ longitude = 0.0f,
+ menus = listOf(
+ MenuEntity(
+ id = 1,
+ name = "메뉴 이름",
+ price = 1000,
+ imgUrl = "",
+ ),
+ ),
+ ),
+ BoothDetailEntity(
+ id = 2,
+ name = "부스 이름",
+ category = "음식",
+ description = "부스 설명",
+ warning = "주의사항",
+ location = "부스 위치",
+ latitude = 0.0f,
+ longitude = 0.0f,
+ menus = listOf(
+ MenuEntity(
+ id = 1,
+ name = "메뉴 이름",
+ price = 1000,
+ imgUrl = "",
+ ),
+ ),
+ ),
+ BoothDetailEntity(
+ id = 3,
+ name = "부스 이름",
+ category = "음식",
+ description = "부스 설명",
+ warning = "주의사항",
+ location = "부스 위치",
+ latitude = 0.0f,
+ longitude = 0.0f,
+ menus = listOf(
+ MenuEntity(
+ id = 1,
+ name = "메뉴 이름",
+ price = 1000,
+ imgUrl = "",
+ ),
+ ),
+ ),
+ BoothDetailEntity(
+ id = 4,
+ name = "부스 이름",
+ category = "음식",
+ description = "부스 설명",
+ warning = "주의사항",
+ location = "부스 위치",
+ latitude = 0.0f,
+ longitude = 0.0f,
+ menus = listOf(
+ MenuEntity(
+ id = 1,
+ name = "메뉴 이름",
+ price = 1000,
+ imgUrl = "",
+ ),
+ ),
+ ),
+ BoothDetailEntity(
+ id = 5,
+ name = "부스 이름",
+ category = "음식",
+ description = "부스 설명",
+ warning = "주의사항",
+ location = "부스 위치",
+ latitude = 0.0f,
+ longitude = 0.0f,
+ menus = listOf(
+ MenuEntity(
+ id = 1,
+ name = "메뉴 이름",
+ price = 1000,
+ imgUrl = "",
+ ),
+ ),
+ ),
+ BoothDetailEntity(
+ id = 6,
+ name = "부스 이름",
+ category = "음식",
+ description = "부스 설명",
+ warning = "주의사항",
+ location = "부스 위치",
+ latitude = 0.0f,
+ longitude = 0.0f,
+ menus = listOf(
+ MenuEntity(
+ id = 1,
+ name = "메뉴 이름",
+ price = 1000,
+ imgUrl = "",
+ ),
+ ),
+ ),
),
),
)
diff --git a/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/navigation/InterestedBoothNavigation.kt b/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/navigation/InterestedBoothNavigation.kt
index dfd2476d..a4437a5b 100644
--- a/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/navigation/InterestedBoothNavigation.kt
+++ b/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/navigation/InterestedBoothNavigation.kt
@@ -1,5 +1,6 @@
package com.unifest.android.feature.interested_booth.navigation
+import androidx.compose.foundation.layout.PaddingValues
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
@@ -11,8 +12,14 @@ fun NavController.navigateToInterestedBooth() {
navigate(INTERESTED_BOOTH_ROUTE)
}
-fun NavGraphBuilder.interestedBoothNavGraph() {
+fun NavGraphBuilder.interestedBoothNavGraph(
+ padding: PaddingValues,
+ onBackClick: () -> Unit,
+) {
composable(route = INTERESTED_BOOTH_ROUTE) {
- InterestedBoothRoute()
+ InterestedBoothRoute(
+ padding = padding,
+ onBackClick = onBackClick,
+ )
}
}
diff --git a/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/viewmodel/InterestedBoothViewModel.kt b/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/viewmodel/InterestedBoothViewModel.kt
index b7b8a9f9..1f1c06e2 100644
--- a/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/viewmodel/InterestedBoothViewModel.kt
+++ b/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/viewmodel/InterestedBoothViewModel.kt
@@ -2,6 +2,7 @@ package com.unifest.android.feature.interested_booth.viewmodel
import androidx.lifecycle.ViewModel
import com.unifest.android.core.domain.entity.BoothDetailEntity
+import com.unifest.android.core.domain.entity.MenuEntity
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
@@ -19,11 +20,114 @@ class InterestedBoothViewModel @Inject constructor() : ViewModel() {
_uiState.update {
it.copy(
interestedBooths = persistentListOf(
- BoothDetailEntity(1L, "컴공 주점", "컴퓨터공학부 전용 부스"),
- BoothDetailEntity(2L, "학생회 부스", "건국대학교 학생회 부스"),
- BoothDetailEntity(3L, "컴공 주점", "컴퓨터공학부 전용 부스"),
- BoothDetailEntity(4L, "학생회 부스", "건국대학교 학생회 부스"),
- BoothDetailEntity(5L, "컴공 주점", "컴퓨터공학부 전용 부스"),
+ BoothDetailEntity(
+ id = 1,
+ name = "부스 이름",
+ category = "음식",
+ description = "부스 설명",
+ warning = "주의사항",
+ location = "부스 위치",
+ latitude = 0.0f,
+ longitude = 0.0f,
+ menus = listOf(
+ MenuEntity(
+ id = 1,
+ name = "메뉴 이름",
+ price = 1000,
+ imgUrl = "",
+ ),
+ ),
+ ),
+ BoothDetailEntity(
+ id = 2,
+ name = "부스 이름",
+ category = "음식",
+ description = "부스 설명",
+ warning = "주의사항",
+ location = "부스 위치",
+ latitude = 0.0f,
+ longitude = 0.0f,
+ menus = listOf(
+ MenuEntity(
+ id = 1,
+ name = "메뉴 이름",
+ price = 1000,
+ imgUrl = "",
+ ),
+ ),
+ ),
+ BoothDetailEntity(
+ id = 3,
+ name = "부스 이름",
+ category = "음식",
+ description = "부스 설명",
+ warning = "주의사항",
+ location = "부스 위치",
+ latitude = 0.0f,
+ longitude = 0.0f,
+ menus = listOf(
+ MenuEntity(
+ id = 1,
+ name = "메뉴 이름",
+ price = 1000,
+ imgUrl = "",
+ ),
+ ),
+ ),
+ BoothDetailEntity(
+ id = 4,
+ name = "부스 이름",
+ category = "음식",
+ description = "부스 설명",
+ warning = "주의사항",
+ location = "부스 위치",
+ latitude = 0.0f,
+ longitude = 0.0f,
+ menus = listOf(
+ MenuEntity(
+ id = 1,
+ name = "메뉴 이름",
+ price = 1000,
+ imgUrl = "",
+ ),
+ ),
+ ),
+ BoothDetailEntity(
+ id = 5,
+ name = "부스 이름",
+ category = "음식",
+ description = "부스 설명",
+ warning = "주의사항",
+ location = "부스 위치",
+ latitude = 0.0f,
+ longitude = 0.0f,
+ menus = listOf(
+ MenuEntity(
+ id = 1,
+ name = "메뉴 이름",
+ price = 1000,
+ imgUrl = "",
+ ),
+ ),
+ ),
+ BoothDetailEntity(
+ id = 6,
+ name = "부스 이름",
+ category = "음식",
+ description = "부스 설명",
+ warning = "주의사항",
+ location = "부스 위치",
+ latitude = 0.0f,
+ longitude = 0.0f,
+ menus = listOf(
+ MenuEntity(
+ id = 1,
+ name = "메뉴 이름",
+ price = 1000,
+ imgUrl = "",
+ ),
+ ),
+ ),
),
)
}
diff --git a/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainScreen.kt b/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainScreen.kt
index bb0c7615..0032ba79 100644
--- a/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainScreen.kt
+++ b/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainScreen.kt
@@ -39,6 +39,7 @@ import com.unifest.android.core.designsystem.theme.BottomMenuBar
import com.unifest.android.core.designsystem.theme.UnifestTheme
import com.unifest.android.feature.booth.navigation.boothNavGraph
import com.unifest.android.feature.home.navigation.homeNavGraph
+import com.unifest.android.feature.interested_booth.navigation.interestedBoothNavGraph
import com.unifest.android.feature.map.navigation.mapNavGraph
import com.unifest.android.feature.menu.navigation.menuNavGraph
import com.unifest.android.feature.waiting.navigation.waitingNavGraph
@@ -106,6 +107,11 @@ internal fun MainScreen(
)
menuNavGraph(
padding = innerPadding,
+ onNavigateToInterestedBooths = navigator::navigateToInterestedBooth,
+ )
+ interestedBoothNavGraph(
+ padding = innerPadding,
+ onBackClick = navigator::popBackStackIfNotHome,
)
}
}
diff --git a/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainTab.kt b/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainTab.kt
index 0d9920fe..a6403819 100644
--- a/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainTab.kt
+++ b/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainTab.kt
@@ -32,7 +32,7 @@ internal enum class MainTab(
),
MENU(
iconResId = R.drawable.ic_menu,
- selectedIconResId = R.drawable.ic_menu,
+ selectedIconResId = R.drawable.ic_selected_menu,
contentDescription = "Menu Icon",
label = "메뉴",
route = "menu_route",
diff --git a/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/MenuScreen.kt b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/MenuScreen.kt
index 4636c45f..832d2931 100644
--- a/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/MenuScreen.kt
+++ b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/MenuScreen.kt
@@ -1,35 +1,414 @@
package com.unifest.android.feature.menu
+import android.content.pm.PackageManager
+import androidx.compose.foundation.background
+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 com.unifest.android.core.designsystem.theme.UnifestTheme
+import com.unifest.android.core.domain.entity.MenuEntity
+import com.unifest.android.core.ui.DevicePreview
+import kotlinx.collections.immutable.persistentListOf
+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.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.VerticalDivider
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.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import com.unifest.android.core.designsystem.theme.UnifestTheme
-import com.unifest.android.core.ui.DevicePreview
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.unifest.android.core.designsystem.R
+import com.unifest.android.core.designsystem.component.NetworkImage
+import com.unifest.android.core.designsystem.component.TopAppBarNavigationType
+import com.unifest.android.core.designsystem.component.UnifestTopAppBar
+import com.unifest.android.core.designsystem.theme.Content6
+import com.unifest.android.core.designsystem.theme.Content7
+import com.unifest.android.core.designsystem.theme.Content8
+import com.unifest.android.core.designsystem.theme.MenuTitle
+import com.unifest.android.core.designsystem.theme.Title2
+import com.unifest.android.core.designsystem.theme.Title3
+import com.unifest.android.core.designsystem.theme.Title5
+import com.unifest.android.core.domain.entity.BoothDetailEntity
+import com.unifest.android.core.domain.entity.Festival
+import com.unifest.android.core.ui.component.FestivalSearchBottomSheet
+import com.unifest.android.feature.menu.viewmodel.MenuUiState
+import com.unifest.android.feature.menu.viewmodel.MenuViewModel
+import timber.log.Timber
@Composable
-internal fun MenuRoute(padding: PaddingValues) {
- MenuScreen(padding = padding)
+internal fun MenuRoute(
+ padding: PaddingValues,
+ onNavigateToInterestedBooths: () -> Unit,
+ viewModel: MenuViewModel = hiltViewModel(),
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ MenuScreen(
+ padding = padding,
+ uiState = uiState,
+ setFestivalSearchBottomSheetVisible = viewModel::setFestivalSearchBottomSheetVisible,
+ onNavigateToInterestedBooths = onNavigateToInterestedBooths,
+ updateFestivalSearchText = viewModel::updateFestivalSearchText,
+ initSearchText = viewModel::initSearchText,
+ setEnableSearchMode = viewModel::setEnableSearchMode,
+ setEnableEditMode = viewModel::setEnableEditMode,
+ setInterestedFestivalDeleteDialogVisible = viewModel::setInterestedFestivalDeleteDialogVisible,
+ )
}
@Composable
fun MenuScreen(
padding: PaddingValues,
+ uiState: MenuUiState,
+ setFestivalSearchBottomSheetVisible: (Boolean) -> Unit,
+ onNavigateToInterestedBooths: () -> Unit,
+ updateFestivalSearchText: (TextFieldValue) -> Unit,
+ initSearchText: () -> Unit,
+ setEnableSearchMode: (Boolean) -> Unit,
+ setEnableEditMode: () -> Unit,
+ setInterestedFestivalDeleteDialogVisible: (Boolean) -> Unit,
) {
- Column(
+ val context = LocalContext.current
+ val appVersion = remember {
+ try {
+ context.packageManager.getPackageInfo(context.packageName, 0).versionName
+ } catch (e: PackageManager.NameNotFoundException) {
+ Timber.tag("AppVersion").e(e, "Failed to get package info")
+ "Unknown"
+ }
+ }
+ Box(
modifier = Modifier
.fillMaxSize()
.padding(padding),
+ ) {
+ Column {
+ UnifestTopAppBar(
+ navigationType = TopAppBarNavigationType.None,
+ title = stringResource(id = R.string.menu_title),
+ elevation = 8.dp,
+ modifier = Modifier
+ .background(
+ Color.White,
+ shape = RoundedCornerShape(bottomEnd = 20.dp, bottomStart = 20.dp),
+ )
+ .padding(top = 13.dp, bottom = 5.dp),
+ )
+ LazyColumn {
+ item { Spacer(modifier = Modifier.height(5.dp)) }
+ item {
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 10.dp, start = 20.dp),
+ ) {
+ Text(
+ text = stringResource(id = R.string.menu_my_interested_festival),
+ style = Title3,
+ )
+ TextButton(
+ onClick = { setFestivalSearchBottomSheetVisible(true) },
+ modifier = Modifier.padding(end = 8.dp),
+ ) {
+ Text(
+ text = stringResource(id = R.string.menu_add),
+ style = Content7,
+ color = Color(0xFF545454),
+ )
+ }
+ }
+ }
+ item {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(4),
+ modifier = Modifier
+ .padding(horizontal = 20.dp)
+ .height(
+ when {
+ uiState.festivals.isEmpty() -> 0.dp
+ else -> {
+ val rows = ((uiState.festivals.size - 1) / 4 + 1) * 140
+ rows.dp
+ }
+ },
+ ),
+ horizontalArrangement = Arrangement.spacedBy(20.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ items(
+ uiState.festivals.size,
+ key = { index -> uiState.festivals[index].schoolName },
+ ) { index ->
+ FestivalItem(
+ festival = uiState.festivals[index],
+ )
+ }
+ }
+ }
+ item {
+ VerticalDivider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(8.dp)
+ .background(Color(0xFFF1F3F7)),
+ )
+ }
+ item {
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 20.dp, top = 10.dp),
+ ) {
+ Text(text = stringResource(id = R.string.menu_interested_booths))
+ TextButton(
+ onClick = { onNavigateToInterestedBooths() },
+ modifier = Modifier.padding(end = 8.dp),
+ ) {
+ Text(
+ text = stringResource(id = R.string.menu_watch_more),
+ style = Content7,
+ color = Color(0xFF545454),
+ )
+ }
+ }
+ }
+ itemsIndexed(uiState.interestedBooths.take(3), key = { _, booth -> booth.id }) { index, booth ->
+ InterestedBoothsItems(booth, index, uiState.interestedBooths.size)
+ }
+ item {
+ VerticalDivider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(8.dp)
+ .background(Color(0xFFF1F3F7)),
+ )
+ }
+ item {
+ MenuItem(
+ ImageVector.vectorResource(R.drawable.ic_inquiry),
+ title = stringResource(id = R.string.menu_questions),
+ onClick = { /* 구현 */ },
+ )
+ }
+ item {
+ VerticalDivider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(1.dp)
+ .background(Color(0xFFE0E0E0)),
+ )
+ }
+ item {
+ MenuItem(
+ ImageVector.vectorResource(R.drawable.ic_admin_mode),
+ title = stringResource(id = R.string.menu_admin_mode),
+ onClick = { /* 구현 */ },
+ )
+ }
+ item {
+ VerticalDivider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(1.dp)
+ .background(Color(0xFFE0E0E0)),
+ )
+ }
+ item {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 13.dp),
+ ) {
+ Text("UniFest v$appVersion", textAlign = TextAlign.Center, color = Color(0xFFC5C5C5))
+ }
+ }
+ }
+ }
+ if (uiState.isFestivalSearchBottomSheetVisible) {
+ FestivalSearchBottomSheet(
+ searchText = uiState.festivalSearchText,
+ updateSearchText = updateFestivalSearchText,
+ searchTextHintRes = R.string.festival_search_text_field_hint,
+ setFestivalSearchBottomSheetVisible = setFestivalSearchBottomSheetVisible,
+ interestedFestivals = uiState.interestedFestivals,
+ festivalSearchResults = uiState.festivalSearchResults,
+ initSearchText = initSearchText,
+ setEnableSearchMode = setEnableSearchMode,
+ isSearchMode = uiState.isSearchMode,
+ setEnableEditMode = setEnableEditMode,
+ isInterestedFestivalDeleteDialogVisible = uiState.isInterestedFestivalDeleteDialogVisible,
+ setInterestedFestivalDeleteDialogVisible = setInterestedFestivalDeleteDialogVisible,
+ isEditMode = uiState.isEditMode,
+ )
+ }
+ }
+}
+
+@Composable
+fun FestivalItem(
+ festival: Festival,
+) {
+ Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
+ modifier = Modifier.padding(vertical = 10.dp),
+ ) {
+ Box(
+ modifier = Modifier
+ .size(65.dp)
+ .shadow(
+ elevation = 6.dp,
+ shape = CircleShape,
+ )
+ .background(Color.White, CircleShape)
+ .padding(5.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ NetworkImage(
+ imageUrl = "https://picsum.photos/86",
+ modifier = Modifier
+ .size(60.dp)
+ .clip(CircleShape),
+ )
+ }
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = festival.schoolName,
+ color = Color(0xFF545454),
+ style = Content6,
+ )
+ Spacer(modifier = Modifier.height(2.dp))
+ Text(
+ text = festival.festivalName,
+ color = Color.Black,
+ style = MenuTitle,
+ )
+ }
+}
+
+@Composable
+fun InterestedBoothsItems(booth: BoothDetailEntity, index: Int, total: Int) {
+ var isBookmarked by remember { mutableStateOf(true) }
+ val bookMarkColor = if (isBookmarked) Color(0xFFF5687E) else Color(0xFF4B4B4B)
+ Column(
+ modifier = Modifier
+ .clickable { /* 클릭 이벤트 처리 */ }
+ .padding(horizontal = 20.dp),
+ ) {
+ Spacer(modifier = Modifier.height(16.dp))
+ Row(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ NetworkImage(
+ imageUrl = "https://picsum.photos/86",
+ modifier = Modifier
+ .size(86.dp)
+ .clip(RoundedCornerShape(16.dp)),
+ )
+ Spacer(modifier = Modifier.width(14.dp))
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth(),
+ ) {
+ Text(
+ text = booth.name,
+ style = Title2,
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = booth.description,
+ style = Title5,
+ color = Color(0xFF545454),
+ )
+ Spacer(modifier = Modifier.height(13.dp))
+ Row {
+ Icon(
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_location_green),
+ contentDescription = "Location Icon",
+ tint = Color.Unspecified,
+ )
+ Spacer(modifier = Modifier.width(3.dp))
+ Text(
+ text = booth.location,
+ style = Title5,
+ color = Color(0xFF545454),
+ modifier = Modifier.align(Alignment.CenterVertically),
+ )
+ }
+ }
+ Icon(
+ imageVector = ImageVector.vectorResource(if (isBookmarked) R.drawable.ic_bookmarked else R.drawable.ic_bookmark),
+ contentDescription = if (isBookmarked) "북마크됨" else "북마크하기",
+ tint = bookMarkColor,
+ modifier = Modifier
+ .size(24.dp)
+ .clickable(onClick = { isBookmarked = !isBookmarked }),
+ )
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ if (index < total - 1) {
+ HorizontalDivider(
+ color = Color(0xFFDFDFDF),
+ thickness = 1.dp,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ }
+}
+
+@Composable
+fun MenuItem(icon: ImageVector, title: String, onClick: () -> Unit) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = onClick)
+ .padding(25.dp),
) {
- Text("Menu Screen")
+ Icon(
+ icon,
+ contentDescription = null,
+ tint = Color(0xFF7A7A7C),
+ )
+ Spacer(modifier = Modifier.width(16.dp))
+ Text(title, style = Content8)
}
}
@@ -37,6 +416,69 @@ fun MenuScreen(
@Composable
fun MenuScreenPreview() {
UnifestTheme {
- MenuScreen(padding = PaddingValues(0.dp))
+ MenuScreen(
+ padding = PaddingValues(0.dp),
+ setFestivalSearchBottomSheetVisible = { },
+ updateFestivalSearchText = { },
+ initSearchText = { },
+ setEnableSearchMode = { },
+ setEnableEditMode = { },
+ setInterestedFestivalDeleteDialogVisible = { },
+ onNavigateToInterestedBooths = { },
+ uiState = MenuUiState(
+ festivals = persistentListOf(
+ Festival(
+ schoolName = "건국대",
+ festivalName = "녹색지대",
+ festivalDate = "2021.11.11",
+ imgUrl = "",
+ ),
+ Festival(
+ schoolName = "건국대",
+ festivalName = "녹색지대",
+ festivalDate = "2021.11.11",
+ imgUrl = "",
+ ),
+ ),
+ interestedBooths = persistentListOf(
+ BoothDetailEntity(
+ id = 1,
+ name = "부스 이름",
+ category = "음식",
+ description = "부스 설명",
+ warning = "주의사항",
+ location = "부스 위치",
+ latitude = 0.0f,
+ longitude = 0.0f,
+ menus = listOf(
+ MenuEntity(
+ id = 1,
+ name = "메뉴 이름",
+ price = 1000,
+ imgUrl = "",
+ ),
+ ),
+ ),
+ BoothDetailEntity(
+ id = 2,
+ name = "부스 이름",
+ category = "음식",
+ description = "부스 설명",
+ warning = "주의사항",
+ location = "부스 위치",
+ latitude = 0.0f,
+ longitude = 0.0f,
+ menus = listOf(
+ MenuEntity(
+ id = 1,
+ name = "메뉴 이름",
+ price = 1000,
+ imgUrl = "",
+ ),
+ ),
+ ),
+ ),
+ ),
+ )
}
}
diff --git a/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/navigation/MenuNavigation.kt b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/navigation/MenuNavigation.kt
index 1cd2e85e..b4a908dc 100644
--- a/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/navigation/MenuNavigation.kt
+++ b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/navigation/MenuNavigation.kt
@@ -15,8 +15,12 @@ fun NavController.navigateToMenu(navOptions: NavOptions) {
fun NavGraphBuilder.menuNavGraph(
padding: PaddingValues,
+ onNavigateToInterestedBooths: () -> Unit,
) {
composable(route = MENU_ROUTE) {
- MenuRoute(padding = padding)
+ MenuRoute(
+ padding = padding,
+ onNavigateToInterestedBooths = onNavigateToInterestedBooths,
+ )
}
}
diff --git a/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/viewmodel/MenuUiState.kt b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/viewmodel/MenuUiState.kt
new file mode 100644
index 00000000..59b6184d
--- /dev/null
+++ b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/viewmodel/MenuUiState.kt
@@ -0,0 +1,19 @@
+package com.unifest.android.feature.menu.viewmodel
+
+import androidx.compose.ui.text.input.TextFieldValue
+import com.unifest.android.core.domain.entity.BoothDetailEntity
+import com.unifest.android.core.domain.entity.Festival
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+
+data class MenuUiState(
+ val festivals: ImmutableList = persistentListOf(),
+ val interestedBooths: ImmutableList = persistentListOf(),
+ val festivalSearchText: TextFieldValue = TextFieldValue(),
+ val interestedFestivals: MutableList = mutableListOf(),
+ val festivalSearchResults: ImmutableList = persistentListOf(),
+ val isSearchMode: Boolean = false,
+ val isEditMode: Boolean = false,
+ val isFestivalSearchBottomSheetVisible: Boolean = false,
+ val isInterestedFestivalDeleteDialogVisible: Boolean = false,
+)
diff --git a/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/viewmodel/MenuViewModel.kt b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/viewmodel/MenuViewModel.kt
index b8c84732..25000722 100644
--- a/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/viewmodel/MenuViewModel.kt
+++ b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/viewmodel/MenuViewModel.kt
@@ -1,8 +1,118 @@
package com.unifest.android.feature.menu.viewmodel
+import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
+import com.unifest.android.core.domain.entity.BoothDetailEntity
+import com.unifest.android.core.domain.entity.Festival
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
import javax.inject.Inject
@HiltViewModel
-class MenuViewModel @Inject constructor() : ViewModel()
+class MenuViewModel @Inject constructor() : ViewModel() {
+ private val _uiState = MutableStateFlow(MenuUiState())
+ val uiState: StateFlow = _uiState.asStateFlow()
+ init {
+ _uiState.update { currentState ->
+ currentState.copy(
+ interestedFestivals = mutableListOf(
+ Festival("https://picsum.photos/36", "서울대학교", "설대축제", "05.06-05.08"),
+ Festival("https://picsum.photos/36", "연세대학교", "연대축제", "05.06-05.08"),
+ Festival("https://picsum.photos/36", "고려대학교", "고대축제", "05.06-05.08"),
+ Festival("https://picsum.photos/36", "건국대학교", "녹색지대", "05.06-05.08"),
+ Festival("https://picsum.photos/36", "성균관대학교", "성대축제", "05.06-05.08"),
+ ),
+ festivalSearchResults = persistentListOf(
+ Festival("https://picsum.photos/36", "서울대학교", "설대축제", "05.06-05.08"),
+ Festival("https://picsum.photos/36", "연세대학교", "연대축제", "05.06-05.08"),
+ Festival("https://picsum.photos/36", "고려대학교", "고대축제", "05.06-05.08"),
+ Festival("https://picsum.photos/36", "건국대학교", "녹색지대", "05.06-05.08"),
+ Festival("https://picsum.photos/36", "성균관대학교", "성대축제", "05.06-05.08"),
+ ),
+ // 임시 데이터
+ festivals = persistentListOf(
+ Festival("school_image_url_1", "서울대학교", "설대축제", "05.06-05.08"),
+ Festival("school_image_url_2", "연세대학교", "연대축제", "05.06-05.08"),
+ Festival("school_image_url_3", "고려대학교", "고대축제", "05.06-05.08"),
+ Festival("school_image_url_4", "건국대학교", "녹색지대", "05.06-05.08"),
+ Festival("school_image_url_5", "성균관대", "성대축제", "05.06-05.08"),
+ ),
+ interestedBooths = persistentListOf(
+ BoothDetailEntity(
+ id = 1,
+ name = "부스1",
+ category = "카페",
+ description = "부스1 설명",
+ warning = "부스1 주의사항",
+ location = "부스1 위치",
+ latitude = 37.5665f,
+ longitude = 126.9780f,
+ menus = listOf(),
+ ),
+ BoothDetailEntity(
+ id = 2,
+ name = "부스2",
+ category = "카페",
+ description = "부스2 설명",
+ warning = "부스2 주의사항",
+ location = "부스2 위치",
+ latitude = 37.5665f,
+ longitude = 126.9780f,
+ menus = listOf(),
+ ),
+ BoothDetailEntity(
+ id = 3,
+ name = "부스3",
+ category = "카페",
+ description = "부스3 설명",
+ warning = "부스3 주의사항",
+ location = "부스3 위치",
+ latitude = 37.5665f,
+ longitude = 126.9780f,
+ menus = listOf(),
+ ),
+ ),
+ )
+ }
+ }
+
+ fun updateFestivalSearchText(text: TextFieldValue) {
+ _uiState.update {
+ it.copy(festivalSearchText = text)
+ }
+ }
+
+ fun initSearchText() {
+ _uiState.update {
+ it.copy(festivalSearchText = TextFieldValue())
+ }
+ }
+
+ fun setFestivalSearchBottomSheetVisible(flag: Boolean) {
+ _uiState.update {
+ it.copy(isFestivalSearchBottomSheetVisible = flag)
+ }
+ }
+
+ fun setEnableSearchMode(flag: Boolean) {
+ _uiState.update {
+ it.copy(isSearchMode = flag)
+ }
+ }
+
+ fun setEnableEditMode() {
+ _uiState.update {
+ it.copy(isEditMode = !_uiState.value.isEditMode)
+ }
+ }
+
+ fun setInterestedFestivalDeleteDialogVisible(flag: Boolean) {
+ _uiState.update {
+ it.copy(isInterestedFestivalDeleteDialogVisible = flag)
+ }
+ }
+}