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) + } + } +}