diff --git a/app/src/main/java/sopt/org/starbucks/data/datasource/MyMenuDataSource.kt b/app/src/main/java/sopt/org/starbucks/data/datasource/MyMenuDataSource.kt index 398d8ba..87a9832 100644 --- a/app/src/main/java/sopt/org/starbucks/data/datasource/MyMenuDataSource.kt +++ b/app/src/main/java/sopt/org/starbucks/data/datasource/MyMenuDataSource.kt @@ -1,5 +1,6 @@ package sopt.org.starbucks.data.datasource +import sopt.org.starbucks.data.dto.response.MyMenuDetailDto import sopt.org.starbucks.data.dto.response.MyMenuListDto import sopt.org.starbucks.data.network.BaseResponse import sopt.org.starbucks.data.service.MyMenuService @@ -13,4 +14,6 @@ class MyMenuDataSource private val myMenuService: MyMenuService ) { suspend fun getMyMenuList(): BaseResponse = myMenuService.getMyMenuList() + + suspend fun getMyMenuDetail(menuId: Long): BaseResponse = myMenuService.getMyMenuDetail(menuId) } diff --git a/app/src/main/java/sopt/org/starbucks/data/dto/response/MyMenuDetailDto.kt b/app/src/main/java/sopt/org/starbucks/data/dto/response/MyMenuDetailDto.kt new file mode 100644 index 0000000..13a4ee4 --- /dev/null +++ b/app/src/main/java/sopt/org/starbucks/data/dto/response/MyMenuDetailDto.kt @@ -0,0 +1,58 @@ +package sopt.org.starbucks.data.dto.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MyMenuDetailDto( + @SerialName("categoryName") + val categoryName: String, + @SerialName("myMenuId") + val myMenuId: Long, + @SerialName("hotMenuKr") + val hotMenuKr: String, + @SerialName("hotMenuEng") + val hotMenuEng: String, + @SerialName("hotMenuImageUrl") + val hotMenuImageUrl: String, + @SerialName("iceMenuKr") + val iceMenuKr: String, + @SerialName("iceMenuEng") + val iceMenuEng: String, + @SerialName("iceMenuImageUrl") + val iceMenuImageUrl: String, + @SerialName("info") + val info: String, + @SerialName("price") + val price: Int, + @SerialName("count") + val count: Int, + @SerialName("isHot") + val isHot: Boolean, + @SerialName("size") + val size: String, + @SerialName("sizePrices") + val sizePrices: SizePricesDto, + @SerialName("personalOptions") + val personalOptions: List, + @SerialName("summary") + val summary: String +) + +@Serializable +data class SizePricesDto( + @SerialName("Tall") + val tall: Int, + @SerialName("Grande") + val grande: Int, + @SerialName("Venti") + val venti: Int +) + +@Serializable +data class PersonalOptionDto( + @SerialName("name") + val name: String, + @SerialName("price") + val price: Int +) diff --git a/app/src/main/java/sopt/org/starbucks/data/mapper/MyMenuDetailMapper.kt b/app/src/main/java/sopt/org/starbucks/data/mapper/MyMenuDetailMapper.kt new file mode 100644 index 0000000..c7c7e94 --- /dev/null +++ b/app/src/main/java/sopt/org/starbucks/data/mapper/MyMenuDetailMapper.kt @@ -0,0 +1,46 @@ +package sopt.org.starbucks.data.mapper + +import sopt.org.starbucks.data.dto.response.MyMenuDetailDto +import sopt.org.starbucks.data.dto.response.PersonalOptionDto +import sopt.org.starbucks.data.dto.response.SizePricesDto +import sopt.org.starbucks.data.model.MenuDetailModel +import sopt.org.starbucks.data.model.PersonalOption +import sopt.org.starbucks.data.model.SizePrices + +fun MyMenuDetailDto.toDomain(): MenuDetailModel = + MenuDetailModel( + myMenuId = this.myMenuId, + categoryName = this.categoryName, + hotMenuKr = this.hotMenuKr, + hotMenuEng = this.hotMenuEng, + hotMenuImageUrl = this.hotMenuImageUrl, + iceMenuKr = this.iceMenuKr, + iceMenuEng = this.iceMenuEng, + iceMenuImageUrl = this.iceMenuImageUrl, + info = this.info, + price = this.price, + count = this.count, + isHot = this.isHot, + size = this.size, + sizePrices = this.sizePrices.toDomain(), + personalOptions = this.personalOptions.map { it.toDomain() }, + summary = this.summary, + isNew = true, + notices = listOf( + "* 리치 과육의 숙 캡슐이 있을 수 있지만 안심하고 드세요.", + "* 대체당(스테비아)을 일부 사용하여 당과 칼로리를 낮췄습니다." + ) + ) + +fun SizePricesDto.toDomain(): SizePrices = + SizePrices( + tall = this.tall, + grande = this.grande, + venti = this.venti + ) + +fun PersonalOptionDto.toDomain(): PersonalOption = + PersonalOption( + name = this.name, + price = this.price + ) diff --git a/app/src/main/java/sopt/org/starbucks/data/model/MenuDetailModel.kt b/app/src/main/java/sopt/org/starbucks/data/model/MenuDetailModel.kt index d0d60e0..8615222 100644 --- a/app/src/main/java/sopt/org/starbucks/data/model/MenuDetailModel.kt +++ b/app/src/main/java/sopt/org/starbucks/data/model/MenuDetailModel.kt @@ -1,12 +1,33 @@ package sopt.org.starbucks.data.model data class MenuDetailModel( - val id: String = "", - val koreanName: String = "", - val englishName: String = "", - val description: String = "", - val imageUrl: String? = null, + val myMenuId: Long = 0, + val categoryName: String = "", + val hotMenuKr: String = "", + val hotMenuEng: String = "", + val hotMenuImageUrl: String = "", + val iceMenuKr: String = "", + val iceMenuEng: String = "", + val iceMenuImageUrl: String = "", + val info: String = "", val price: Int = 0, + val count: Int = 0, + val isHot: Boolean = true, + val size: String = "TALL", + val sizePrices: SizePrices = SizePrices(), + val personalOptions: List = emptyList(), + val summary: String = "", val isNew: Boolean = false, val notices: List = emptyList() ) + +data class SizePrices( + val tall: Int = 0, + val grande: Int = 0, + val venti: Int = 0 +) + +data class PersonalOption( + val name: String = "", + val price: Int = 0 +) diff --git a/app/src/main/java/sopt/org/starbucks/data/repository/MyMenuRepository.kt b/app/src/main/java/sopt/org/starbucks/data/repository/MyMenuRepository.kt index 3473142..b765a98 100644 --- a/app/src/main/java/sopt/org/starbucks/data/repository/MyMenuRepository.kt +++ b/app/src/main/java/sopt/org/starbucks/data/repository/MyMenuRepository.kt @@ -2,6 +2,7 @@ package sopt.org.starbucks.data.repository import sopt.org.starbucks.data.datasource.MyMenuDataSource import sopt.org.starbucks.data.mapper.toDomain +import sopt.org.starbucks.data.model.MenuDetailModel import sopt.org.starbucks.data.model.MyMenu import sopt.org.starbucks.data.network.handleApiResponse import sopt.org.starbucks.data.network.safeApiCall @@ -22,4 +23,13 @@ class MyMenuRepository .getOrThrow() .toDomain() } + + suspend fun getMyMenuDetail(menuId: Long): Result = + safeApiCall { + myMenuDataSource + .getMyMenuDetail(menuId) + .handleApiResponse() + .getOrThrow() + .toDomain() + } } diff --git a/app/src/main/java/sopt/org/starbucks/data/service/MyMenuService.kt b/app/src/main/java/sopt/org/starbucks/data/service/MyMenuService.kt index 097dd26..581bb43 100644 --- a/app/src/main/java/sopt/org/starbucks/data/service/MyMenuService.kt +++ b/app/src/main/java/sopt/org/starbucks/data/service/MyMenuService.kt @@ -1,10 +1,17 @@ package sopt.org.starbucks.data.service import retrofit2.http.GET +import retrofit2.http.Path +import sopt.org.starbucks.data.dto.response.MyMenuDetailDto import sopt.org.starbucks.data.dto.response.MyMenuListDto import sopt.org.starbucks.data.network.BaseResponse interface MyMenuService { @GET("/api/v1/mymenu/list") suspend fun getMyMenuList(): BaseResponse + + @GET("/api/v1/mymenu/{menuId}") + suspend fun getMyMenuDetail( + @Path("menuId") menuId: Long + ): BaseResponse } diff --git a/app/src/main/java/sopt/org/starbucks/ui/main/MainNavHost.kt b/app/src/main/java/sopt/org/starbucks/ui/main/MainNavHost.kt index 8e9645d..04f5bc2 100644 --- a/app/src/main/java/sopt/org/starbucks/ui/main/MainNavHost.kt +++ b/app/src/main/java/sopt/org/starbucks/ui/main/MainNavHost.kt @@ -14,6 +14,7 @@ import sopt.org.starbucks.core.navigation.Other import sopt.org.starbucks.core.navigation.Pay import sopt.org.starbucks.core.navigation.Shop import sopt.org.starbucks.ui.home.HomeRoute +import sopt.org.starbucks.ui.mymenu.MyMenuRoute import sopt.org.starbucks.ui.order.OrderRoute @Composable @@ -43,10 +44,11 @@ fun MainNavHost( composable { } composable { backStackEntry -> val args = backStackEntry.toRoute() -// MyMenuRoute( -// paddingValues = paddingValues, -// menuId = args.menuId -// ) + MyMenuRoute( + paddingValues = paddingValues, + menuId = args.menuId, + onBackClick = { navigator.navController.navigateUp() } + ) } } } diff --git a/app/src/main/java/sopt/org/starbucks/ui/mymenu/MyMenuScreen.kt b/app/src/main/java/sopt/org/starbucks/ui/mymenu/MyMenuScreen.kt index 55ef632..ae6d8cb 100644 --- a/app/src/main/java/sopt/org/starbucks/ui/mymenu/MyMenuScreen.kt +++ b/app/src/main/java/sopt/org/starbucks/ui/mymenu/MyMenuScreen.kt @@ -2,6 +2,7 @@ package sopt.org.starbucks.ui.mymenu import androidx.compose.foundation.background 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.height @@ -10,14 +11,16 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll 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.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import sopt.org.starbucks.core.designsystem.theme.StarbucksTheme +import sopt.org.starbucks.core.state.UiState +import sopt.org.starbucks.core.util.onSuccess +import sopt.org.starbucks.core.util.toStringWithFormat import sopt.org.starbucks.data.model.MenuDetailModel import sopt.org.starbucks.ui.mymenu.component.DrinkImageSection import sopt.org.starbucks.ui.mymenu.component.DrinkSize @@ -30,18 +33,55 @@ import sopt.org.starbucks.ui.mymenu.component.SelectCupSection import sopt.org.starbucks.ui.mymenu.component.TabToggle import sopt.org.starbucks.ui.mymenu.component.TabType -// fun MyMenuRoute( -// paddingValues: PaddingValues, -// menuId: Long -// ) { -// MyMenuScreen( -// menuId = menuId, -// modifier = Modifier.padding(bottom = paddingValues.calculateBottomPadding()) -// ) -// } // TODO 추가 예정 +@Composable +fun MyMenuRoute( + paddingValues: PaddingValues, + menuId: Long, + onBackClick: () -> Unit, + viewModel: MyMenuViewModel = hiltViewModel() +) { + val uiState = viewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(menuId) { + viewModel.loadMenu(menuId) + } + + MyMenuScreen( + uiState = uiState.value, + onTabSelected = viewModel::selectTab, + onSizeSelected = viewModel::selectSize, + onPersonalCupToggle = viewModel::togglePersonalCup, + onBackClick = onBackClick, + modifier = Modifier.padding(bottom = paddingValues.calculateBottomPadding()) + ) +} @Composable fun MyMenuScreen( + uiState: MyMenuUiState, + onTabSelected: (TabType) -> Unit, + onSizeSelected: (DrinkSize) -> Unit, + onPersonalCupToggle: () -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier +) { + uiState.menuLoadState.onSuccess { menu -> + MyMenuContent( + menu = menu, + selectedTab = uiState.selectedTab, + selectedSize = uiState.selectedSize, + isPersonalCupChecked = uiState.isPersonalCupChecked, + onTabSelected = onTabSelected, + onSizeSelected = onSizeSelected, + onPersonalCupToggle = onPersonalCupToggle, + onBackClick = onBackClick, + modifier = modifier + ) + } +} + +@Composable +private fun MyMenuContent( menu: MenuDetailModel, selectedTab: TabType, selectedSize: DrinkSize, @@ -52,8 +92,15 @@ fun MyMenuScreen( onBackClick: () -> Unit, modifier: Modifier = Modifier ) { + val sizePrice = when (selectedSize) { + DrinkSize.TALL -> menu.sizePrices.tall + DrinkSize.GRANDE -> menu.sizePrices.grande + DrinkSize.VENTI -> menu.sizePrices.venti + } + val totalPrice = menu.price + sizePrice + Column( - modifier = Modifier + modifier = modifier .fillMaxSize() .background(StarbucksTheme.colors.white) ) { @@ -61,19 +108,25 @@ fun MyMenuScreen( modifier = Modifier .weight(1f) .verticalScroll(rememberScrollState()) + .padding(bottom = 14.dp) ) { + val (imageUrl, koreanName, englishName) = when (selectedTab) { + TabType.HOT -> Triple(menu.hotMenuImageUrl, menu.hotMenuKr, menu.hotMenuEng) + TabType.ICED -> Triple(menu.iceMenuImageUrl, menu.iceMenuKr, menu.iceMenuEng) + } + DrinkImageSection( modifier = Modifier, - imageUrl = menu.imageUrl, + imageUrl = imageUrl, onBackClick = onBackClick ) Spacer(modifier = Modifier.height(20.dp)) DrinkTitleSection( - koreanTitle = menu.koreanName, - englishTitle = menu.englishName, - description = menu.description, + koreanTitle = koreanName, + englishTitle = englishName, + description = menu.info, isNew = menu.isNew, modifier = Modifier.padding(horizontal = 16.dp) ) @@ -81,7 +134,7 @@ fun MyMenuScreen( Spacer(modifier = Modifier.height(11.5.dp)) Text( - text = "${menu.price}원", + text = "${totalPrice.toStringWithFormat()}원", style = StarbucksTheme.typography.bodyBold22, color = StarbucksTheme.colors.black, modifier = Modifier.padding(horizontal = 16.dp) @@ -139,32 +192,13 @@ fun MyMenuScreen( @Preview(showBackground = true) @Composable -private fun EditMenuScreenPreview() { - var selectedTab by remember { mutableStateOf(TabType.ICED) } - var selectedSize by remember { mutableStateOf(DrinkSize.TALL) } - var isPersonalCupChecked by remember { mutableStateOf(false) } - +private fun MyMenuScreenLoadingPreview() { StarbucksTheme { MyMenuScreen( - menu = MenuDetailModel( - id = "1", - koreanName = "아이스 핑크 팝 캐모마일 릴렉서", - englishName = "Iced Pink Pop Chamomile Relaxer", - description = "크리스마스에 어울리는 상큼한 핑크팝과 캐모마일 릴렉서! 리치, 레몬그라스, 캐모마일의 차분하면서도 새콤달콤한 조합", - imageUrl = null, - price = 6500, - isNew = true, - notices = listOf( - "* 리치 과육의 숙 캡슐이 있을 수 있지만 안심하고 드세요.", - "* 대체당(스테비아)을 일부 사용하여 당과 칼로리를 낮췄습니다." - ) - ), - selectedTab = selectedTab, - selectedSize = selectedSize, - isPersonalCupChecked = isPersonalCupChecked, - onTabSelected = { selectedTab = it }, - onSizeSelected = { selectedSize = it }, - onPersonalCupToggle = { isPersonalCupChecked = !isPersonalCupChecked }, + uiState = MyMenuUiState(menuLoadState = UiState.Loading), + onTabSelected = {}, + onSizeSelected = {}, + onPersonalCupToggle = {}, onBackClick = {} ) } diff --git a/app/src/main/java/sopt/org/starbucks/ui/mymenu/MyMenuUiState.kt b/app/src/main/java/sopt/org/starbucks/ui/mymenu/MyMenuUiState.kt index 0a74c9e..478f85d 100644 --- a/app/src/main/java/sopt/org/starbucks/ui/mymenu/MyMenuUiState.kt +++ b/app/src/main/java/sopt/org/starbucks/ui/mymenu/MyMenuUiState.kt @@ -1,11 +1,12 @@ package sopt.org.starbucks.ui.mymenu +import sopt.org.starbucks.core.state.UiState import sopt.org.starbucks.data.model.MenuDetailModel import sopt.org.starbucks.ui.mymenu.component.DrinkSize import sopt.org.starbucks.ui.mymenu.component.TabType data class MyMenuUiState( - val menu: MenuDetailModel = MenuDetailModel(), + val menuLoadState: UiState = UiState.Init, val selectedTab: TabType = TabType.ICED, val selectedSize: DrinkSize = DrinkSize.TALL, val isPersonalCupChecked: Boolean = false diff --git a/app/src/main/java/sopt/org/starbucks/ui/mymenu/MyMenuViewModel.kt b/app/src/main/java/sopt/org/starbucks/ui/mymenu/MyMenuViewModel.kt index 738d005..1fb24a9 100644 --- a/app/src/main/java/sopt/org/starbucks/ui/mymenu/MyMenuViewModel.kt +++ b/app/src/main/java/sopt/org/starbucks/ui/mymenu/MyMenuViewModel.kt @@ -7,7 +7,8 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import sopt.org.starbucks.data.model.MenuDetailModel +import sopt.org.starbucks.core.state.UiState +import sopt.org.starbucks.data.repository.MyMenuRepository import sopt.org.starbucks.ui.mymenu.component.DrinkSize import sopt.org.starbucks.ui.mymenu.component.TabType import javax.inject.Inject @@ -15,30 +16,40 @@ import javax.inject.Inject @HiltViewModel class MyMenuViewModel @Inject - constructor() : ViewModel() { + constructor( + private val myMenuRepository: MyMenuRepository + ) : ViewModel() { private val _uiState = MutableStateFlow(MyMenuUiState()) val uiState = _uiState.asStateFlow() - fun loadMenu(menuId: String) { + fun loadMenu(menuId: Long) { viewModelScope.launch { - // Mock 데이터 - _uiState.update { - it.copy( - menu = MenuDetailModel( - id = menuId, - koreanName = "아이스 핑크 팝 캐모마일 릴렉서", - englishName = "Iced Pink Pop Chamomile Relaxer", - description = "크리스마스에 어울리는 상큼한 핑크팝과 캐모마일 릴렉서! 리치, 레몬그라스, 캐모마일의 차분하면서도 새콤달콤한 조합", - imageUrl = null, - price = 6500, - isNew = true, - notices = listOf( - "* 리치 과육의 숙 캡슐이 있을 수 있지만 안심하고 드세요.", - "* 대체당(스테비아)을 일부 사용하여 당과 칼로리를 낮췄습니다." + _uiState.update { it.copy(menuLoadState = UiState.Loading) } + + myMenuRepository + .getMyMenuDetail(menuId) + .onSuccess { menu -> + _uiState.update { + it.copy( + menuLoadState = UiState.Success(menu), + selectedTab = if (menu.isHot) TabType.HOT else TabType.ICED, + selectedSize = when (menu.size) { + "TALL" -> DrinkSize.TALL + "GRANDE" -> DrinkSize.GRANDE + "VENTI" -> DrinkSize.VENTI + else -> DrinkSize.TALL + } + ) + } + }.onFailure { t -> + _uiState.update { + it.copy( + menuLoadState = UiState.Failure( + t.message ?: "Failed to load menu" + ) ) - ) - ) - } + } + } } } diff --git a/app/src/main/java/sopt/org/starbucks/ui/mymenu/component/DrinkImageSection.kt b/app/src/main/java/sopt/org/starbucks/ui/mymenu/component/DrinkImageSection.kt index 66cfa73..06f71c5 100644 --- a/app/src/main/java/sopt/org/starbucks/ui/mymenu/component/DrinkImageSection.kt +++ b/app/src/main/java/sopt/org/starbucks/ui/mymenu/component/DrinkImageSection.kt @@ -31,6 +31,16 @@ fun DrinkImageSection( .fillMaxWidth() .background(StarbucksTheme.colors.red01) ) { + // 음료 이미지 + AsyncImage( + model = imageUrl, + contentDescription = "중앙 음료 이미자", + contentScale = ContentScale.Fit, + modifier = Modifier + .align(Alignment.Center) + .aspectRatio(1f) + ) + // 뒤로가기 IconButton( onClick = onBackClick, @@ -61,16 +71,6 @@ fun DrinkImageSection( ) } - // 음료 이미지 - AsyncImage( - model = imageUrl, - contentDescription = "중앙 음료 이미자", - contentScale = ContentScale.Fit, - modifier = Modifier - .align(Alignment.Center) - .aspectRatio(1f) - ) - // 이미지 IconButton( onClick = { },