From d54ad6682a0fe1a5adda09367ac7256e28b95eb2 Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Mon, 20 Oct 2025 20:03:33 +0900 Subject: [PATCH 01/10] =?UTF-8?q?[FEAT]=20=EC=A3=BC=EB=AC=B8=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=99=94=EB=A9=B4,=20=EC=A3=BC=EB=AC=B8=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EC=83=81=EC=84=B8=20=ED=99=94=EB=A9=B4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/app/navigation/AppNavHost.kt | 32 ++- .../android/core/ui/component/StatusChip.kt | 37 +++ .../sampoom/android/core/util/FormatDate.kt | 11 + .../sampoom/android/core/util/OrderTitle.kt | 26 ++ .../feature/order/data/mapper/OrderMappers.kt | 15 + .../feature/order/data/remote/api/OrderApi.kt | 32 +++ .../feature/order/data/remote/dto/OrderDto.kt | 31 +++ .../data/repository/OrderRepositoryImpl.kt | 43 +++ .../android/feature/order/di/OrderModules.kt | 26 ++ .../feature/order/domain/model/Order.kt | 29 ++ .../feature/order/domain/model/OrderList.kt | 11 + .../feature/order/domain/model/OrderStatus.kt | 14 + .../domain/repository/OrderRepository.kt | 11 + .../domain/usecase/CancelOrderUseCase.kt | 10 + .../domain/usecase/CreateOrderUseCase.kt | 10 + .../domain/usecase/GetOrderDetailUseCase.kt | 10 + .../order/domain/usecase/GetOrderUseCase.kt | 10 + .../domain/usecase/ReceiveOrderUseCase.kt | 10 + .../feature/order/ui/OrderDetailScreen.kt | 262 ++++++++++++++++++ .../feature/order/ui/OrderDetailUiEvent.kt | 6 + .../feature/order/ui/OrderDetailUiState.kt | 9 + .../feature/order/ui/OrderDetailViewModel.kt | 77 +++++ .../feature/order/ui/OrderListScreen.kt | 181 ++++++++++++ .../feature/order/ui/OrderListUiEvent.kt | 6 + .../feature/order/ui/OrderListUiState.kt | 9 + .../feature/order/ui/OrderListViewModel.kt | 70 +++++ .../android/feature/part/ui/PartScreen.kt | 4 +- app/src/main/res/values/strings.xml | 16 ++ 28 files changed, 999 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/sampoom/android/core/ui/component/StatusChip.kt create mode 100644 app/src/main/java/com/sampoom/android/core/util/FormatDate.kt create mode 100644 app/src/main/java/com/sampoom/android/core/util/OrderTitle.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/data/mapper/OrderMappers.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/data/remote/api/OrderApi.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/data/remote/dto/OrderDto.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/data/repository/OrderRepositoryImpl.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/di/OrderModules.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/domain/model/Order.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/domain/model/OrderList.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/domain/model/OrderStatus.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/domain/repository/OrderRepository.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/domain/usecase/CancelOrderUseCase.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/domain/usecase/CreateOrderUseCase.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/domain/usecase/GetOrderDetailUseCase.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/domain/usecase/GetOrderUseCase.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/domain/usecase/ReceiveOrderUseCase.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailUiEvent.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailUiState.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailViewModel.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/ui/OrderListScreen.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/ui/OrderListUiEvent.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/ui/OrderListUiState.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/ui/OrderListViewModel.kt diff --git a/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt b/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt index fdf8411..d01162e 100644 --- a/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt +++ b/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt @@ -23,6 +23,9 @@ import com.sampoom.android.R import com.sampoom.android.feature.auth.ui.LoginScreen import com.sampoom.android.feature.auth.ui.SignUpScreen import com.sampoom.android.feature.cart.ui.CartListScreen +import com.sampoom.android.feature.order.domain.model.OrderList +import com.sampoom.android.feature.order.ui.OrderDetailScreen +import com.sampoom.android.feature.order.ui.OrderListScreen import com.sampoom.android.feature.outbound.ui.OutboundListScreen import com.sampoom.android.feature.part.ui.PartListScreen import com.sampoom.android.feature.part.ui.PartScreen @@ -41,6 +44,8 @@ const val ROUTE_ORDERS = "orders" const val ROUTE_PARTS = "parts" const val ROUTE_PART_LIST = "parts/{agencyId}/group/{groupId}" fun routePartList(agencyId: Long, groupId: Long): String = "parts/$agencyId/group/$groupId" +const val ROUTE_ORDER_DETAIL = "orders/{agencyId}/orders/{orderId}" +fun routeOrderDetail(agencyId: Long, orderId: Long): String = "orders/$agencyId/orders/$orderId" const val ROUTE_EMPLOYEE = "employee" const val ROUTE_SETTINGS = "settings" @@ -134,7 +139,26 @@ fun MainScreen( composable(ROUTE_DASHBOARD) { DashboardScreen() } composable(ROUTE_OUTBOUND) { OutboundListScreen() } composable(ROUTE_CART) { CartListScreen() } - composable(ROUTE_ORDERS) { OrderScreen() } + composable(ROUTE_ORDERS) { + OrderListScreen( + onNavigateOrderDetail = { order -> + navController.navigate(routeOrderDetail(1, order.orderId)) + } + ) + } + composable( + ROUTE_ORDER_DETAIL, + arguments = listOf( + navArgument("agencyId") { type = NavType.LongType }, + navArgument("orderId") { type = NavType.LongType } + ) + ) { + OrderDetailScreen( + onNavigateBack = { + navController.navigateUp() + } + ) + } } } } @@ -201,10 +225,4 @@ fun BottomNavigationBar(navController: NavHostController) { private fun DashboardScreen() { // 홈 화면 구현 Text("대시보드 화면") -} - -@Composable -private fun OrderScreen() { - // 프로필 화면 구현 - Text("Order 화면") } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/core/ui/component/StatusChip.kt b/app/src/main/java/com/sampoom/android/core/ui/component/StatusChip.kt new file mode 100644 index 0000000..47f0272 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/core/ui/component/StatusChip.kt @@ -0,0 +1,37 @@ +package com.sampoom.android.core.ui.component + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.sampoom.android.R +import com.sampoom.android.core.ui.theme.FailRed +import com.sampoom.android.core.ui.theme.SuccessGreen +import com.sampoom.android.core.ui.theme.WaitYellow +import com.sampoom.android.feature.order.domain.model.OrderStatus + +@Composable +fun StatusChip(status: OrderStatus) { + val (text, color) = when (status) { + OrderStatus.PENDING -> stringResource(R.string.order_status_pending) to WaitYellow + OrderStatus.COMPLETED -> stringResource(R.string.order_status_completed) to SuccessGreen + OrderStatus.CANCELED -> stringResource(R.string.order_status_canceled) to FailRed + } + + Surface( + shape = RoundedCornerShape(16.dp), + color = color.copy(alpha = 0.2F) + ) { + Text( + text = text, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), + style = MaterialTheme.typography.bodySmall, + color = color + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/core/util/FormatDate.kt b/app/src/main/java/com/sampoom/android/core/util/FormatDate.kt new file mode 100644 index 0000000..8fc7deb --- /dev/null +++ b/app/src/main/java/com/sampoom/android/core/util/FormatDate.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.core.util + +fun formatDate(dateString: String): String { + return try { + val inFmt = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", java.util.Locale.getDefault()) + val outFmt = java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault()) + outFmt.format(inFmt.parse(dateString) ?: java.util.Date()) + } catch (_: Exception) { + dateString + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/core/util/OrderTitle.kt b/app/src/main/java/com/sampoom/android/core/util/OrderTitle.kt new file mode 100644 index 0000000..d027df7 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/core/util/OrderTitle.kt @@ -0,0 +1,26 @@ +package com.sampoom.android.core.util + +import com.sampoom.android.feature.order.domain.model.Order +import com.sampoom.android.feature.order.domain.model.OrderPart + +fun buildOrderTitle(order: Order): String { + val flattened: List> = + order.items.flatMap { category -> + category.groups.flatMap { group -> + group.parts.map { part -> Triple(category.categoryName, group.groupName, part) } + } + } + + if (flattened.isEmpty()) return "-" + + val first = flattened.first() + val groupName = first.second + val part = first.third + val totalParts = flattened.size + + return if (totalParts == 1) { + "$groupName - ${part.name} ${part.quantity}EA" + } else { + "${part.name} - $groupName ${part.quantity}EA 외 ${totalParts - 1}건" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/data/mapper/OrderMappers.kt b/app/src/main/java/com/sampoom/android/feature/order/data/mapper/OrderMappers.kt new file mode 100644 index 0000000..fd25e78 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/data/mapper/OrderMappers.kt @@ -0,0 +1,15 @@ +package com.sampoom.android.feature.order.data.mapper + +import com.sampoom.android.feature.order.data.remote.dto.OrderCategoryDto +import com.sampoom.android.feature.order.data.remote.dto.OrderDto +import com.sampoom.android.feature.order.data.remote.dto.OrderGroupDto +import com.sampoom.android.feature.order.data.remote.dto.OrderPartDto +import com.sampoom.android.feature.order.domain.model.Order +import com.sampoom.android.feature.order.domain.model.OrderCategory +import com.sampoom.android.feature.order.domain.model.OrderGroup +import com.sampoom.android.feature.order.domain.model.OrderPart + +fun OrderDto.toModel(): Order = Order(orderId, orderNumber, createdAt, status, agencyName, items.map { it.toModel() }) +fun OrderCategoryDto.toModel(): OrderCategory = OrderCategory(categoryId, categoryName, groups.map { it.toModel() }) +fun OrderGroupDto.toModel(): OrderGroup = OrderGroup(groupId, groupName, parts.map { it.toModel() }) +fun OrderPartDto.toModel(): OrderPart = OrderPart(partId, code, name, quantity) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/data/remote/api/OrderApi.kt b/app/src/main/java/com/sampoom/android/feature/order/data/remote/api/OrderApi.kt new file mode 100644 index 0000000..d8fc9fc --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/data/remote/api/OrderApi.kt @@ -0,0 +1,32 @@ +package com.sampoom.android.feature.order.data.remote.api + +import com.sampoom.android.core.network.ApiResponse +import com.sampoom.android.core.network.ApiSuccessResponse +import com.sampoom.android.feature.order.data.remote.dto.OrderDto +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.PATCH +import retrofit2.http.POST +import retrofit2.http.Path + +interface OrderApi { + // 주문 목록 조회 + @GET("agency/1/orders") + suspend fun getOrderList(): ApiResponse> + + // 주문 생성 + @POST("agency/1/orders") + suspend fun createOrder(): ApiResponse> + + // 주문 입고 처리 + @PATCH("agency/1/orders/{orderId}/receive") + suspend fun receiveOrder(@Path("orderId") orderId: Long): ApiSuccessResponse + + // 주문 상세 조회 + @GET("agency/1/orders/{orderId}") + suspend fun getOrderDetail(@Path("orderId") orderId: Long): ApiResponse> + + // 주문 취소 + @DELETE("agency/1/orders/{orderId}") + suspend fun cancelOrder(@Path("orderId") orderId: Long): ApiSuccessResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/data/remote/dto/OrderDto.kt b/app/src/main/java/com/sampoom/android/feature/order/data/remote/dto/OrderDto.kt new file mode 100644 index 0000000..50a9129 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/data/remote/dto/OrderDto.kt @@ -0,0 +1,31 @@ +package com.sampoom.android.feature.order.data.remote.dto + +import com.sampoom.android.feature.order.domain.model.OrderStatus + +data class OrderDto( + val orderId: Long, + val orderNumber: String?, + val createdAt: String?, + val status: OrderStatus, + val agencyName: String?, + val items: List +) + +data class OrderCategoryDto( + val categoryId: Long, + val categoryName: String, + val groups: List +) + +data class OrderGroupDto( + val groupId: Long, + val groupName: String, + val parts: List +) + +data class OrderPartDto( + val partId: Long, + val code: String, + val name: String, + val quantity: Long +) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/data/repository/OrderRepositoryImpl.kt b/app/src/main/java/com/sampoom/android/feature/order/data/repository/OrderRepositoryImpl.kt new file mode 100644 index 0000000..065eeca --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/data/repository/OrderRepositoryImpl.kt @@ -0,0 +1,43 @@ +package com.sampoom.android.feature.order.data.repository + +import com.sampoom.android.feature.order.data.mapper.toModel +import com.sampoom.android.feature.order.data.remote.api.OrderApi +import com.sampoom.android.feature.order.domain.model.OrderList +import com.sampoom.android.feature.order.domain.repository.OrderRepository +import javax.inject.Inject + +class OrderRepositoryImpl @Inject constructor( + private val api: OrderApi +) : OrderRepository { + override suspend fun getOrderList(): OrderList { + val dto = api.getOrderList() + val orderItems = dto.data.map { it.toModel() } + return OrderList(items = orderItems) + } + + override suspend fun createOrder(): OrderList { + val dto = api.createOrder() + val orderItems = dto.data.map { it.toModel() } + return OrderList(items = orderItems) + } + + override suspend fun receiveOrder(orderId: Long): Result { + val dto = api.receiveOrder(orderId) + return runCatching { + if (!dto.success) throw Exception(dto.message) + } + } + + override suspend fun getOrderDetail(orderId: Long): OrderList { + val dto = api.getOrderDetail(orderId) + val orderItems = dto.data.map { it.toModel() } + return OrderList(items = orderItems) + } + + override suspend fun cancelOrder(orderId: Long): Result { + val dto = api.cancelOrder(orderId) + return runCatching { + if (!dto.success) throw Exception(dto.message) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/di/OrderModules.kt b/app/src/main/java/com/sampoom/android/feature/order/di/OrderModules.kt new file mode 100644 index 0000000..fe342bf --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/di/OrderModules.kt @@ -0,0 +1,26 @@ +package com.sampoom.android.feature.order.di + +import com.sampoom.android.feature.order.data.remote.api.OrderApi +import com.sampoom.android.feature.order.data.repository.OrderRepositoryImpl +import com.sampoom.android.feature.order.domain.repository.OrderRepository +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import jakarta.inject.Singleton +import retrofit2.Retrofit + +@Module +@InstallIn(SingletonComponent::class) +abstract class OrderBindModule { + @Binds @Singleton + abstract fun bindOrderRepository(impl: OrderRepositoryImpl): OrderRepository +} + +@Module +@InstallIn(SingletonComponent::class) +object OrderModule { + @Provides @Singleton + fun provideOrderApi(retrofit: Retrofit): OrderApi = retrofit.create(OrderApi::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/domain/model/Order.kt b/app/src/main/java/com/sampoom/android/feature/order/domain/model/Order.kt new file mode 100644 index 0000000..90da7f8 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/domain/model/Order.kt @@ -0,0 +1,29 @@ +package com.sampoom.android.feature.order.domain.model + +data class Order( + val orderId: Long, + val orderNumber: String?, + val createdAt: String?, + val status: OrderStatus, + val agencyName: String?, + val items: List +) + +data class OrderCategory( + val categoryId: Long, + val categoryName: String, + val groups: List +) + +data class OrderGroup( + val groupId: Long, + val groupName: String, + val parts: List +) + +data class OrderPart( + val partId: Long, + val code: String, + val name: String, + val quantity: Long +) diff --git a/app/src/main/java/com/sampoom/android/feature/order/domain/model/OrderList.kt b/app/src/main/java/com/sampoom/android/feature/order/domain/model/OrderList.kt new file mode 100644 index 0000000..94d1431 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/domain/model/OrderList.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.feature.order.domain.model + +data class OrderList( + val items: List, + val totalCount: Int = items.size, + val isEmpty: Boolean = items.isEmpty() +) { + companion object Companion { + fun empty() = OrderList(emptyList()) + } +} diff --git a/app/src/main/java/com/sampoom/android/feature/order/domain/model/OrderStatus.kt b/app/src/main/java/com/sampoom/android/feature/order/domain/model/OrderStatus.kt new file mode 100644 index 0000000..4e31967 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/domain/model/OrderStatus.kt @@ -0,0 +1,14 @@ +package com.sampoom.android.feature.order.domain.model + +enum class OrderStatus { + PENDING, COMPLETED, CANCELED; + + companion object { + fun from(raw: String?): OrderStatus = when (raw?.uppercase()) { + "PENDING" -> PENDING + "COMPLETED" -> COMPLETED + "CANCELED" -> CANCELED + else -> PENDING + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/domain/repository/OrderRepository.kt b/app/src/main/java/com/sampoom/android/feature/order/domain/repository/OrderRepository.kt new file mode 100644 index 0000000..871b70a --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/domain/repository/OrderRepository.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.feature.order.domain.repository + +import com.sampoom.android.feature.order.domain.model.OrderList + +interface OrderRepository { + suspend fun getOrderList(): OrderList + suspend fun createOrder(): OrderList + suspend fun receiveOrder(orderId: Long): Result + suspend fun getOrderDetail(orderId: Long): OrderList + suspend fun cancelOrder(orderId: Long): Result +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/domain/usecase/CancelOrderUseCase.kt b/app/src/main/java/com/sampoom/android/feature/order/domain/usecase/CancelOrderUseCase.kt new file mode 100644 index 0000000..e3e8f86 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/domain/usecase/CancelOrderUseCase.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.order.domain.usecase + +import com.sampoom.android.feature.order.domain.repository.OrderRepository +import javax.inject.Inject + +class CancelOrderUseCase @Inject constructor( + private val repository: OrderRepository +){ + suspend operator fun invoke(orderId: Long) = repository.cancelOrder(orderId) +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/domain/usecase/CreateOrderUseCase.kt b/app/src/main/java/com/sampoom/android/feature/order/domain/usecase/CreateOrderUseCase.kt new file mode 100644 index 0000000..c26aafe --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/domain/usecase/CreateOrderUseCase.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.order.domain.usecase + +import com.sampoom.android.feature.order.domain.repository.OrderRepository +import javax.inject.Inject + +class CreateOrderUseCase @Inject constructor( + private val repository: OrderRepository +){ + suspend operator fun invoke() = repository.createOrder() +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/domain/usecase/GetOrderDetailUseCase.kt b/app/src/main/java/com/sampoom/android/feature/order/domain/usecase/GetOrderDetailUseCase.kt new file mode 100644 index 0000000..1898e1b --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/domain/usecase/GetOrderDetailUseCase.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.order.domain.usecase + +import com.sampoom.android.feature.order.domain.repository.OrderRepository +import javax.inject.Inject + +class GetOrderDetailUseCase @Inject constructor( + private val repository: OrderRepository +) { + suspend operator fun invoke(orderId: Long) = repository.getOrderDetail(orderId) +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/domain/usecase/GetOrderUseCase.kt b/app/src/main/java/com/sampoom/android/feature/order/domain/usecase/GetOrderUseCase.kt new file mode 100644 index 0000000..ad20ec3 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/domain/usecase/GetOrderUseCase.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.order.domain.usecase + +import com.sampoom.android.feature.order.domain.repository.OrderRepository +import javax.inject.Inject + +class GetOrderUseCase @Inject constructor( + private val repository: OrderRepository +){ + suspend operator fun invoke() = repository.getOrderList() +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/domain/usecase/ReceiveOrderUseCase.kt b/app/src/main/java/com/sampoom/android/feature/order/domain/usecase/ReceiveOrderUseCase.kt new file mode 100644 index 0000000..76ce9b1 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/domain/usecase/ReceiveOrderUseCase.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.order.domain.usecase + +import com.sampoom.android.feature.order.domain.repository.OrderRepository +import javax.inject.Inject + +class ReceiveOrderUseCase @Inject constructor( + private val repository: OrderRepository +) { + suspend operator fun invoke(orderId: Long) = repository.receiveOrder(orderId) +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt new file mode 100644 index 0000000..8c8e5ba --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt @@ -0,0 +1,262 @@ +package com.sampoom.android.feature.order.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.sampoom.android.R +import com.sampoom.android.core.ui.component.EmptyContent +import com.sampoom.android.core.ui.component.ErrorContent +import com.sampoom.android.core.ui.component.StatusChip +import com.sampoom.android.core.ui.theme.FailRed +import com.sampoom.android.core.ui.theme.SuccessGreen +import com.sampoom.android.core.ui.theme.WaitYellow +import com.sampoom.android.core.ui.theme.backgroundCardColor +import com.sampoom.android.core.ui.theme.textColor +import com.sampoom.android.core.ui.theme.textSecondaryColor +import com.sampoom.android.feature.order.domain.model.Order +import com.sampoom.android.feature.order.domain.model.OrderPart +import com.sampoom.android.feature.order.domain.model.OrderStatus + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun OrderDetailScreen( + onNavigateBack: () -> Unit = {}, + viewModel: OrderDetailViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + Column(Modifier.fillMaxSize()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + IconButton(onClick = onNavigateBack) { + Icon( + painter = painterResource(R.drawable.ic_arrow_back), + contentDescription = stringResource(R.string.nav_back) + ) + } + Text( + modifier = Modifier + .padding(vertical = 16.dp), + text = stringResource(R.string.order_detail_title), + style = MaterialTheme.typography.titleLarge, + color = textColor() + ) + } + + when { + uiState.orderDetailLoading -> { + Box( + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + uiState.orderDetailError != null -> { + ErrorContent( + onRetry = { viewModel.onEvent(OrderDetailUiEvent.RetryOrder) }, + modifier = Modifier.height(200.dp).fillMaxWidth() + ) + } + + uiState.orderDetail.isEmpty() -> { + EmptyContent( + message = stringResource(R.string.order_empty_list), + modifier = Modifier.height(200.dp).fillMaxWidth() + ) + } + + else -> { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + uiState.orderDetail.forEach { order -> + item { + OrderInfoCard(order = order) + } + item { + Text( + text = stringResource(R.string.order_detail_order_items_title), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = textColor() + ) + } + order.items.forEach { category -> + category.groups.forEach { group -> + item { + OrderSection( + categoryName = category.categoryName, + groupName = group.groupName, + parts = group.parts + ) + } + } + } + } + } + } + } + } +} + +@Composable +private fun OrderInfoCard(order: Order) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = backgroundCardColor()) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + OrderInfoRow( + label = stringResource(R.string.order_detail_order_number), + value = order.orderNumber ?: stringResource(R.string.common_slash) + ) + OrderInfoRow( + label = stringResource(R.string.order_detail_order_date), + value = order.createdAt ?: stringResource(R.string.common_slash) + ) + OrderInfoRow( + label = stringResource(R.string.order_detail_order_agency), + value = order.agencyName ?: stringResource(R.string.common_slash) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.order_detail_order_status), + style = MaterialTheme.typography.bodyMedium, + color = textSecondaryColor() + ) + + StatusChip(status = order.status) + } + } + } +} + +@Composable +private fun OrderInfoRow( + label: String, + value: String +) { + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = label, + style = MaterialTheme.typography.bodyMedium, + color = textSecondaryColor() + ) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + color = textColor() + ) + } +} + +@Composable +private fun OrderSection( + categoryName: String, + groupName: String, + parts: List +) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "$categoryName > $groupName", + style = MaterialTheme.typography.titleMedium, + color = textColor() + ) + + parts.forEach { part -> + OrderPartItem(part = part) + } + } +} + +@Composable +private fun OrderPartItem( + part: OrderPart +) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = backgroundCardColor()) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1F) + ) { + Text( + text = part.name, + style = MaterialTheme.typography.titleMedium, + color = textColor() + ) + Text( + text = part.code, + style = MaterialTheme.typography.bodySmall, + color = textSecondaryColor() + ) + } + + Text( + text = part.quantity.toString(), + style = MaterialTheme.typography.titleMedium, + color = textColor() + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailUiEvent.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailUiEvent.kt new file mode 100644 index 0000000..2caa7c4 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailUiEvent.kt @@ -0,0 +1,6 @@ +package com.sampoom.android.feature.order.ui + +sealed interface OrderDetailUiEvent { + object LoadOrder : OrderDetailUiEvent + object RetryOrder : OrderDetailUiEvent +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailUiState.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailUiState.kt new file mode 100644 index 0000000..6fdbede --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailUiState.kt @@ -0,0 +1,9 @@ +package com.sampoom.android.feature.order.ui + +import com.sampoom.android.feature.order.domain.model.Order + +data class OrderDetailUiState( + val orderDetail: List = emptyList(), + val orderDetailLoading: Boolean = false, + val orderDetailError: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailViewModel.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailViewModel.kt new file mode 100644 index 0000000..e604071 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailViewModel.kt @@ -0,0 +1,77 @@ +package com.sampoom.android.feature.order.ui + +import android.util.Log +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.sampoom.android.core.network.serverMessageOrNull +import com.sampoom.android.feature.order.domain.usecase.GetOrderDetailUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class OrderDetailViewModel @Inject constructor( + private val getOrderDetailUseCase: GetOrderDetailUseCase, + savedStateHandle: SavedStateHandle +) : ViewModel() { + + private companion object { + private const val TAG = "OrderDetailViewModel" + } + + private val _uiState = MutableStateFlow(OrderDetailUiState()) + val uiState: StateFlow = _uiState + + // Navigation 인자 로드 + private val agencyId: Long = savedStateHandle.get("agencyId") ?: 0L + private val orderId: Long = savedStateHandle.get("orderId") ?: 0L + + private var errorLabel: String = "" + + fun bindLabel(error: String) { + errorLabel = error + } + + init { + if (orderId > 0L) loadOrderDetail(orderId) + else _uiState.update { it.copy(orderDetailError = errorLabel) } + } + + fun onEvent(event: OrderDetailUiEvent) { + when (event) { + is OrderDetailUiEvent.LoadOrder -> loadOrderDetail(orderId) + is OrderDetailUiEvent.RetryOrder -> loadOrderDetail(orderId) + } + } + + private fun loadOrderDetail(orderId: Long) { + viewModelScope.launch { + _uiState.update { it.copy(orderDetailLoading = true, orderDetailError = null) } + + runCatching { getOrderDetailUseCase(orderId) } + .onSuccess { orderList -> + _uiState.update { + it.copy( + orderDetail = orderList.items, + orderDetailLoading = false, + orderDetailError = null + ) + } + } + .onFailure { throwable -> + val backendMessage = throwable.serverMessageOrNull() + _uiState.update { + it.copy( + orderDetailLoading = false, + orderDetailError = backendMessage ?: (throwable.message ?: errorLabel) + ) + } + } + Log.d(TAG, "submit: ${_uiState.value}") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListScreen.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListScreen.kt new file mode 100644 index 0000000..8e4d528 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListScreen.kt @@ -0,0 +1,181 @@ +package com.sampoom.android.feature.order.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.sampoom.android.R +import com.sampoom.android.core.ui.component.EmptyContent +import com.sampoom.android.core.ui.component.ErrorContent +import com.sampoom.android.core.ui.component.StatusChip +import com.sampoom.android.core.ui.theme.backgroundCardColor +import com.sampoom.android.core.ui.theme.textColor +import com.sampoom.android.core.ui.theme.textSecondaryColor +import com.sampoom.android.core.util.buildOrderTitle +import com.sampoom.android.core.util.formatDate +import com.sampoom.android.feature.order.domain.model.Order +import com.sampoom.android.feature.order.domain.model.OrderPart + +@Composable +fun OrderListScreen( + onNavigateOrderDetail: (Order) -> Unit, + viewModel: OrderListViewModel = hiltViewModel() +) { + val errorLabel = stringResource(R.string.common_error) + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val listState = rememberSaveable(saver = LazyListState.Saver) { LazyListState() } + + LaunchedEffect(errorLabel) { + viewModel.bindLabel(errorLabel) + viewModel.onEvent(OrderListUiEvent.LoadOrderList) + } + + Column(Modifier.fillMaxSize()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier + .padding(vertical = 16.dp), + text = stringResource(R.string.order_title), + style = MaterialTheme.typography.titleLarge, + color = textColor() + ) + } + + when { + uiState.orderLoading -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + uiState.orderError != null -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + ErrorContent( + onRetry = { viewModel.onEvent(OrderListUiEvent.RetryOrderList) }, + modifier = Modifier.height(200.dp) + ) + } + } + + uiState.orderList.isEmpty() -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + EmptyContent( + message = stringResource(R.string.order_empty_list), + modifier = Modifier.height(200.dp) + ) + } + } + + else -> { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + uiState.orderList.forEach { order -> + item { + OrderItem( + order = order, + onClick = { onNavigateOrderDetail(order) } + ) + } + } + item { Spacer(Modifier.height(100.dp)) } + } + } + } + } +} + +@Composable +private fun OrderItem( + order: Order, + onClick: () -> Unit +) { + Card( + onClick = { onClick() }, + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = backgroundCardColor()) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .weight(1f) + ) { + Text( + text = buildOrderTitle(order), + style = MaterialTheme.typography.bodyMedium, + maxLines = 1 + ) + Spacer(Modifier.height(4.dp)) + Text( + text = order.agencyName ?: stringResource(R.string.common_slash), + style = MaterialTheme.typography.labelMedium, + color = textSecondaryColor() + ) + } + + Spacer(Modifier.width(12.dp)) + + Column(horizontalAlignment = Alignment.End) { + Text( + text = order.createdAt?.let { formatDate(it) } ?: stringResource(R.string.common_slash), + style = MaterialTheme.typography.labelMedium, + color = textSecondaryColor() + ) + Spacer(Modifier.height(6.dp)) + StatusChip(status = order.status) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListUiEvent.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListUiEvent.kt new file mode 100644 index 0000000..dfb3e8c --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListUiEvent.kt @@ -0,0 +1,6 @@ +package com.sampoom.android.feature.order.ui + +sealed interface OrderListUiEvent { + object LoadOrderList : OrderListUiEvent + object RetryOrderList : OrderListUiEvent +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListUiState.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListUiState.kt new file mode 100644 index 0000000..8e7e25b --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListUiState.kt @@ -0,0 +1,9 @@ +package com.sampoom.android.feature.order.ui + +import com.sampoom.android.feature.order.domain.model.Order + +data class OrderListUiState( + val orderList: List = emptyList(), + val orderLoading: Boolean = false, + val orderError: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListViewModel.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListViewModel.kt new file mode 100644 index 0000000..a5fcf74 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListViewModel.kt @@ -0,0 +1,70 @@ +package com.sampoom.android.feature.order.ui + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.sampoom.android.core.network.serverMessageOrNull +import com.sampoom.android.feature.order.domain.usecase.GetOrderUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class OrderListViewModel @Inject constructor( + private val getOrderListUseCase: GetOrderUseCase +) : ViewModel() { + + private companion object { + private const val TAG = "OrderListViewModel" + } + + private val _uiState = MutableStateFlow(OrderListUiState()) + val uiState: StateFlow = _uiState + + private var errorLabel: String = "" + + fun bindLabel(error: String) { + errorLabel = error + } + + init { + loadOrderList() + } + + fun onEvent(event: OrderListUiEvent) { + when (event) { + is OrderListUiEvent.LoadOrderList -> loadOrderList() + is OrderListUiEvent.RetryOrderList -> loadOrderList() + } + } + + private fun loadOrderList() { + viewModelScope.launch { + _uiState.update { it.copy(orderLoading = true, orderError = null) } + + runCatching { getOrderListUseCase() } + .onSuccess { orderList -> + _uiState.update { + it.copy( + orderList = orderList.items, + orderLoading = false, + orderError = null + ) + } + } + .onFailure { throwable -> + val backendMessage = throwable.serverMessageOrNull() + _uiState.update { + it.copy( + orderLoading = false, + orderError = backendMessage ?: (throwable.message ?: errorLabel ) + ) + } + } + Log.d(TAG, "submit: ${_uiState.value}") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartScreen.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartScreen.kt index 97ae617..3585da3 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/ui/PartScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartScreen.kt @@ -116,14 +116,14 @@ fun PartScreen( uiState.categoryError != null -> { ErrorContent( onRetry = { viewModel.onEvent(PartUiEvent.RetryCategories) }, - modifier = Modifier.height(200.dp) + modifier = Modifier.height(200.dp).fillMaxWidth() ) } uiState.categoryList.isEmpty() -> { EmptyContent( message = stringResource(R.string.part_empty_category), - modifier = Modifier.height(200.dp) + modifier = Modifier.height(200.dp).fillMaxWidth() ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7faede2..52953bd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -78,6 +78,21 @@ 추가되었습니다 부품 주문 + + 주문관리 + 주문관리 목록이 없습니다. + 주문정보 + 주문번호 + 주문일자 + 대리점 + 주문상태 + 주문상품 + + + 승인대기 + 배송완료 + 주문실패 + 오류가 발생했습니다 다시 시도 @@ -87,6 +102,7 @@ 삭제 상세 보기 EA + - 이메일을 입력해주세요 From 2d38e1fb03813471023860a4e1fa9b289d029064 Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Mon, 20 Oct 2025 20:19:10 +0900 Subject: [PATCH 02/10] =?UTF-8?q?[FIX]=20Navigation=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/app/navigation/AppNavHost.kt | 28 +++++++-------- .../feature/order/ui/OrderDetailScreen.kt | 35 ++++++++----------- 2 files changed, 28 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt b/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt index d01162e..4531182 100644 --- a/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt +++ b/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt @@ -118,6 +118,19 @@ fun AppNavHost() { } ) } + composable( + ROUTE_ORDER_DETAIL, + arguments = listOf( + navArgument("agencyId") { type = NavType.LongType }, + navArgument("orderId") { type = NavType.LongType } + ) + ) { + OrderDetailScreen( + onNavigateBack = { + navController.navigateUp() + } + ) + } } } @@ -142,20 +155,7 @@ fun MainScreen( composable(ROUTE_ORDERS) { OrderListScreen( onNavigateOrderDetail = { order -> - navController.navigate(routeOrderDetail(1, order.orderId)) - } - ) - } - composable( - ROUTE_ORDER_DETAIL, - arguments = listOf( - navArgument("agencyId") { type = NavType.LongType }, - navArgument("orderId") { type = NavType.LongType } - ) - ) { - OrderDetailScreen( - onNavigateBack = { - navController.navigateUp() + parentNavController.navigate(routeOrderDetail(1, order.orderId)) } ) } diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt index 8c8e5ba..7c0606d 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt @@ -53,29 +53,21 @@ fun OrderDetailScreen( ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - Column(Modifier.fillMaxSize()) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 4.dp), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically - ) { - IconButton(onClick = onNavigateBack) { - Icon( - painter = painterResource(R.drawable.ic_arrow_back), - contentDescription = stringResource(R.string.nav_back) - ) - } - Text( - modifier = Modifier - .padding(vertical = 16.dp), - text = stringResource(R.string.order_detail_title), - style = MaterialTheme.typography.titleLarge, - color = textColor() + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.order_detail_title)) }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + painter = painterResource(R.drawable.ic_arrow_back), + contentDescription = stringResource(R.string.nav_back) + ) + } + } ) } - + ) { innerPadding -> when { uiState.orderDetailLoading -> { Box( @@ -106,6 +98,7 @@ fun OrderDetailScreen( LazyColumn( modifier = Modifier .fillMaxSize() + .padding(innerPadding) .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { From f3c63992bc0394cea2f32f82fd3f9c7a3c2d9f20 Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Tue, 21 Oct 2025 11:04:20 +0900 Subject: [PATCH 03/10] =?UTF-8?q?[FEAT]=20=EC=A3=BC=EB=AC=B8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=ED=98=84,=20=EC=A3=BC=EB=AC=B8=20?= =?UTF-8?q?=EC=B7=A8=EC=86=8C,=20=EC=9E=85=EA=B3=A0=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/core/network/ErrorHandling.kt | 2 + .../data/repository/CartRepositoryImpl.kt | 8 +- .../android/feature/cart/ui/CartListScreen.kt | 20 +- .../feature/cart/ui/CartListUiEvent.kt | 1 + .../feature/cart/ui/CartListUiState.kt | 5 +- .../feature/cart/ui/CartListViewModel.kt | 43 ++- .../data/repository/OrderRepositoryImpl.kt | 4 +- .../feature/order/ui/OrderDetailContent.kt | 188 ++++++++++++ .../feature/order/ui/OrderDetailScreen.kt | 274 +++++++----------- .../feature/order/ui/OrderDetailUiEvent.kt | 3 + .../feature/order/ui/OrderDetailUiState.kt | 6 +- .../feature/order/ui/OrderDetailViewModel.kt | 83 +++++- .../order/ui/OrderResultBottomSheet.kt | 168 +++++++++++ .../data/repository/OutboundRepositoryImpl.kt | 10 +- app/src/main/res/drawable/ic_check_circle.xml | 5 + app/src/main/res/values/strings.xml | 10 +- 16 files changed, 608 insertions(+), 222 deletions(-) create mode 100644 app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailContent.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/order/ui/OrderResultBottomSheet.kt create mode 100644 app/src/main/res/drawable/ic_check_circle.xml diff --git a/app/src/main/java/com/sampoom/android/core/network/ErrorHandling.kt b/app/src/main/java/com/sampoom/android/core/network/ErrorHandling.kt index 23d3ae5..e0a43ca 100644 --- a/app/src/main/java/com/sampoom/android/core/network/ErrorHandling.kt +++ b/app/src/main/java/com/sampoom/android/core/network/ErrorHandling.kt @@ -1,5 +1,6 @@ package com.sampoom.android.core.network +import android.util.Log import com.google.gson.Gson import com.google.gson.JsonSyntaxException import retrofit2.HttpException @@ -12,6 +13,7 @@ data class ApiErrorResponse( fun Throwable.serverMessageOrNull(): String? { if (this is HttpException) { val errorBody = response()?.errorBody()?.string() ?: return null + Log.d("ErrorHandling", "Error body: $errorBody") return try { Gson().fromJson(errorBody, ApiErrorResponse::class.java).message } catch (_: JsonSyntaxException) { diff --git a/app/src/main/java/com/sampoom/android/feature/cart/data/repository/CartRepositoryImpl.kt b/app/src/main/java/com/sampoom/android/feature/cart/data/repository/CartRepositoryImpl.kt index 70021a1..87cf282 100644 --- a/app/src/main/java/com/sampoom/android/feature/cart/data/repository/CartRepositoryImpl.kt +++ b/app/src/main/java/com/sampoom/android/feature/cart/data/repository/CartRepositoryImpl.kt @@ -21,22 +21,22 @@ class CartRepositoryImpl @Inject constructor( partId: Long, quantity: Long ): Result { - val dto = api.addCart(AddCartRequestDto(partId, quantity)) return runCatching { + val dto = api.addCart(AddCartRequestDto(partId, quantity)) if (!dto.success) throw Exception(dto.message) } } override suspend fun deleteCart(cartItemId: Long): Result { - val dto = api.deleteCart(cartItemId) return runCatching { + val dto = api.deleteCart(cartItemId) if (!dto.success) throw Exception(dto.message) } } override suspend fun deleteAllCart(): Result { - val dto = api.deleteAllCart() return runCatching { + val dto = api.deleteAllCart() if (!dto.success) throw Exception(dto.message) } } @@ -45,8 +45,8 @@ class CartRepositoryImpl @Inject constructor( cartItemId: Long, quantity: Long ): Result { - val dto = api.updateCart(cartItemId, UpdateCartRequestDto(quantity)) return runCatching { + val dto = api.updateCart(cartItemId, UpdateCartRequestDto(quantity)) if (!dto.success) throw Exception(dto.message) } } diff --git a/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListScreen.kt b/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListScreen.kt index b48d196..5c6ad52 100644 --- a/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListScreen.kt @@ -45,6 +45,7 @@ import com.sampoom.android.core.ui.theme.backgroundCardColor import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.core.ui.theme.textSecondaryColor import com.sampoom.android.feature.cart.domain.model.CartPart +import com.sampoom.android.feature.order.ui.OrderResultBottomSheet import kotlin.collections.forEach @Composable @@ -55,26 +56,17 @@ fun CartListScreen( val uiState by viewModel.uiState.collectAsStateWithLifecycle() var showEmptyCartDialog by remember { mutableStateOf(false) } var showConfirmDialog by remember { mutableStateOf(false) } - val context = LocalContext.current - - LaunchedEffect(Unit) { - viewModel.clearSuccess() - } LaunchedEffect(errorLabel) { viewModel.bindLabel(errorLabel) viewModel.onEvent(CartListUiEvent.LoadCartList) } - LaunchedEffect(uiState.isOrderSuccess) { - if (uiState.isOrderSuccess) { - Toast.makeText( - context, - context.getString(R.string.cart_toast_order_text), - Toast.LENGTH_SHORT - ).show() - } - viewModel.clearSuccess() + uiState.processedOrder?.let { orders -> + OrderResultBottomSheet( + order = orders, + onDismiss = { viewModel.onEvent(CartListUiEvent.DismissOrderResult)} + ) } Column(Modifier.fillMaxSize()) { diff --git a/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListUiEvent.kt b/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListUiEvent.kt index f96cb48..15682c9 100644 --- a/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListUiEvent.kt +++ b/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListUiEvent.kt @@ -9,4 +9,5 @@ sealed interface CartListUiEvent { object DeleteAllCart : CartListUiEvent object ClearUpdateError : CartListUiEvent object ClearDeleteError : CartListUiEvent + object DismissOrderResult : CartListUiEvent } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListUiState.kt b/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListUiState.kt index d6ae356..d005b17 100644 --- a/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListUiState.kt +++ b/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListUiState.kt @@ -1,6 +1,7 @@ package com.sampoom.android.feature.cart.ui import com.sampoom.android.feature.cart.domain.model.Cart +import com.sampoom.android.feature.order.domain.model.Order data class CartListUiState( val cartList: List = emptyList(), @@ -11,5 +12,7 @@ data class CartListUiState( val updateError: String? = null, val isDeleting: Boolean = false, val deleteError: String? = null, - val isOrderSuccess: Boolean = false + val isProcessing: Boolean = false, + val processError: String? = null, + val processedOrder: List? = null ) diff --git a/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListViewModel.kt b/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListViewModel.kt index 1be0d4f..4f6d7a1 100644 --- a/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListViewModel.kt @@ -1,5 +1,6 @@ package com.sampoom.android.feature.cart.ui +import android.R.attr.order import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -8,6 +9,7 @@ import com.sampoom.android.feature.cart.domain.usecase.DeleteAllCartUseCase import com.sampoom.android.feature.cart.domain.usecase.DeleteCartUseCase import com.sampoom.android.feature.cart.domain.usecase.GetCartUseCase import com.sampoom.android.feature.cart.domain.usecase.UpdateCartQuantityUseCase +import com.sampoom.android.feature.order.domain.usecase.CreateOrderUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -20,7 +22,8 @@ class CartListViewModel @Inject constructor( private val getCartListUseCase: GetCartUseCase, private val updateCartQuantityUseCase: UpdateCartQuantityUseCase, private val deleteCartUseCase: DeleteCartUseCase, - private val deleteAllCartUseCase: DeleteAllCartUseCase + private val deleteAllCartUseCase: DeleteAllCartUseCase, + private val createOrderUseCase: CreateOrderUseCase ) : ViewModel() { private companion object { @@ -50,6 +53,7 @@ class CartListViewModel @Inject constructor( is CartListUiEvent.DeleteAllCart -> deleteAllCart() is CartListUiEvent.ClearUpdateError -> _uiState.update { it.copy(updateError = null) } is CartListUiEvent.ClearDeleteError -> _uiState.update { it.copy(deleteError = null) } + is CartListUiEvent.DismissOrderResult -> _uiState.update { it.copy(processedOrder = null) } } } @@ -81,25 +85,24 @@ class CartListViewModel @Inject constructor( } } - // TODO() : 주문 생성 로직 private fun processOrder() { viewModelScope.launch { -// _uiState.update { it.copy(cartLoading = true, cartError = null) } -// -// processCartUseCase() -// .onSuccess { -// _uiState.update { it.copy(isUpdating = false, isOrderSuccess = true) } -// loadCartList() -// } -// .onFailure { throwable -> -// val backendMessage = throwable.serverMessageOrNull() -// _uiState.update { -// it.copy( -// isUpdating = false, -// updateError = backendMessage ?: (throwable.message ?: errorLabel) -// ) -// } -// } + _uiState.update { it.copy(isProcessing = true, processError = null) } + + runCatching { createOrderUseCase() } + .onSuccess { orderList -> + _uiState.update { it.copy(isProcessing = false, processedOrder = orderList.items) } + loadCartList() + } + .onFailure { throwable -> + val backendMessage = throwable.serverMessageOrNull() + _uiState.update { + it.copy( + isProcessing = false, + processError = backendMessage ?: (throwable.message ?: errorLabel) + ) + } + } Log.d(TAG, "submit: ${_uiState.value}") } } @@ -217,8 +220,4 @@ class CartListViewModel @Inject constructor( currentState.copy(cartList = emptyList()) } } - - fun clearSuccess() { - _uiState.update { it.copy(isOrderSuccess = false) } - } } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/data/repository/OrderRepositoryImpl.kt b/app/src/main/java/com/sampoom/android/feature/order/data/repository/OrderRepositoryImpl.kt index 065eeca..16a74e1 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/data/repository/OrderRepositoryImpl.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/data/repository/OrderRepositoryImpl.kt @@ -22,8 +22,8 @@ class OrderRepositoryImpl @Inject constructor( } override suspend fun receiveOrder(orderId: Long): Result { - val dto = api.receiveOrder(orderId) return runCatching { + val dto = api.receiveOrder(orderId) if (!dto.success) throw Exception(dto.message) } } @@ -35,8 +35,8 @@ class OrderRepositoryImpl @Inject constructor( } override suspend fun cancelOrder(orderId: Long): Result { - val dto = api.cancelOrder(orderId) return runCatching { + val dto = api.cancelOrder(orderId) if (!dto.success) throw Exception(dto.message) } } diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailContent.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailContent.kt new file mode 100644 index 0000000..f30e900 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailContent.kt @@ -0,0 +1,188 @@ +package com.sampoom.android.feature.order.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.sampoom.android.R +import com.sampoom.android.core.ui.component.StatusChip +import com.sampoom.android.core.ui.theme.backgroundCardColor +import com.sampoom.android.core.ui.theme.textColor +import com.sampoom.android.core.ui.theme.textSecondaryColor +import com.sampoom.android.feature.order.domain.model.Order +import com.sampoom.android.feature.order.domain.model.OrderPart +import kotlin.collections.forEach + +@Composable +fun OrderDetailContent( + order: List, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier = modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + order.forEach { order -> + item { + OrderInfoCard(order = order) + } + item { + Text( + text = stringResource(R.string.order_detail_order_items_title), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = textColor() + ) + } + order.items.forEach { category -> + category.groups.forEach { group -> + item { + OrderSection( + categoryName = category.categoryName, + groupName = group.groupName, + parts = group.parts + ) + } + } + } + } + } +} + + +@Composable +private fun OrderInfoCard(order: Order) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = backgroundCardColor()) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + OrderInfoRow( + label = stringResource(R.string.order_detail_order_number), + value = order.orderNumber ?: stringResource(R.string.common_slash) + ) + OrderInfoRow( + label = stringResource(R.string.order_detail_order_date), + value = order.createdAt ?: stringResource(R.string.common_slash) + ) + OrderInfoRow( + label = stringResource(R.string.order_detail_order_agency), + value = order.agencyName ?: stringResource(R.string.common_slash) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.order_detail_order_status), + style = MaterialTheme.typography.bodyMedium, + color = textSecondaryColor() + ) + + StatusChip(status = order.status) + } + } + } +} + +@Composable +private fun OrderInfoRow( + label: String, + value: String +) { + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = label, + style = MaterialTheme.typography.bodyMedium, + color = textSecondaryColor() + ) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + color = textColor() + ) + } +} + +@Composable +private fun OrderSection( + categoryName: String, + groupName: String, + parts: List +) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "$categoryName > $groupName", + style = MaterialTheme.typography.titleMedium, + color = textColor() + ) + + parts.forEach { part -> + OrderPartItem(part = part) + } + } +} + +@Composable +private fun OrderPartItem( + part: OrderPart +) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = backgroundCardColor()) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1F) + ) { + Text( + text = part.name, + style = MaterialTheme.typography.titleMedium, + color = textColor() + ) + Text( + text = part.code, + style = MaterialTheme.typography.bodySmall, + color = textSecondaryColor() + ) + } + + Text( + text = part.quantity.toString(), + style = MaterialTheme.typography.titleMedium, + color = textColor() + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt index 7c0606d..900ed05 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt @@ -1,49 +1,42 @@ package com.sampoom.android.feature.order.ui -import androidx.compose.foundation.layout.Arrangement +import android.widget.Toast import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults +import androidx.compose.foundation.layout.width +import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.sampoom.android.R +import com.sampoom.android.core.ui.component.ButtonVariant +import com.sampoom.android.core.ui.component.CommonButton import com.sampoom.android.core.ui.component.EmptyContent import com.sampoom.android.core.ui.component.ErrorContent -import com.sampoom.android.core.ui.component.StatusChip -import com.sampoom.android.core.ui.theme.FailRed -import com.sampoom.android.core.ui.theme.SuccessGreen -import com.sampoom.android.core.ui.theme.WaitYellow -import com.sampoom.android.core.ui.theme.backgroundCardColor -import com.sampoom.android.core.ui.theme.textColor -import com.sampoom.android.core.ui.theme.textSecondaryColor -import com.sampoom.android.feature.order.domain.model.Order -import com.sampoom.android.feature.order.domain.model.OrderPart -import com.sampoom.android.feature.order.domain.model.OrderStatus +import com.sampoom.android.core.ui.component.ErrorContent @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -52,6 +45,35 @@ fun OrderDetailScreen( viewModel: OrderDetailViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() + var showCancelOrderDialog by remember { mutableStateOf(false) } + var showReceiveOrderDialog by remember { mutableStateOf(false) } + val context = LocalContext.current + + // 성공 시 Toast 표시 후 다이얼로그 닫기 + LaunchedEffect(uiState.isProcessingCancelSuccess) { + if (uiState.isProcessingCancelSuccess) { + Toast.makeText(context, context.getString(R.string.order_detail_toast_order_cancel), Toast.LENGTH_SHORT).show() + } + viewModel.clearSuccess() + viewModel.onEvent(OrderDetailUiEvent.LoadOrder) + } + + // 성공 시 Toast 표시 후 다이얼로그 닫기 + LaunchedEffect(uiState.isProcessingReceiveSuccess) { + if (uiState.isProcessingReceiveSuccess) { + Toast.makeText(context, context.getString(R.string.order_detail_toast_order_receive), Toast.LENGTH_SHORT).show() + } + viewModel.clearSuccess() + viewModel.onEvent(OrderDetailUiEvent.LoadOrder) + } + + // 실패 시 Toast 표시 + LaunchedEffect(uiState.isProcessingError) { + uiState.isProcessingError?.let { error -> + Toast.makeText(context, error, Toast.LENGTH_LONG).show() + viewModel.onEvent(OrderDetailUiEvent.ClearError) + } + } Scaffold( topBar = { @@ -66,6 +88,26 @@ fun OrderDetailScreen( } } ) + }, + bottomBar = { + Row( + modifier = Modifier.fillMaxWidth().padding(16.dp) + ) { + CommonButton( + modifier = Modifier.weight(1f), + variant = ButtonVariant.Error, + onClick = { showCancelOrderDialog = true } + ) { + Text(stringResource(R.string.order_detail_order_cancel)) + } + Spacer(Modifier.width(16.dp)) + CommonButton( + modifier = Modifier.weight(1f), + onClick = { showReceiveOrderDialog = true } + ) { + Text(stringResource(R.string.order_detail_order_receive)) + } + } } ) { innerPadding -> when { @@ -95,161 +137,59 @@ fun OrderDetailScreen( } else -> { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - uiState.orderDetail.forEach { order -> - item { - OrderInfoCard(order = order) - } - item { - Text( - text = stringResource(R.string.order_detail_order_items_title), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - color = textColor() - ) - } - order.items.forEach { category -> - category.groups.forEach { group -> - item { - OrderSection( - categoryName = category.categoryName, - groupName = group.groupName, - parts = group.parts - ) - } - } - } - } - } - } - } - } -} - -@Composable -private fun OrderInfoCard(order: Order) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors(containerColor = backgroundCardColor()) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - OrderInfoRow( - label = stringResource(R.string.order_detail_order_number), - value = order.orderNumber ?: stringResource(R.string.common_slash) - ) - OrderInfoRow( - label = stringResource(R.string.order_detail_order_date), - value = order.createdAt ?: stringResource(R.string.common_slash) - ) - OrderInfoRow( - label = stringResource(R.string.order_detail_order_agency), - value = order.agencyName ?: stringResource(R.string.common_slash) - ) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(R.string.order_detail_order_status), - style = MaterialTheme.typography.bodyMedium, - color = textSecondaryColor() + OrderDetailContent( + order = uiState.orderDetail, + modifier = Modifier.padding(innerPadding) ) - - StatusChip(status = order.status) } } } -} -@Composable -private fun OrderInfoRow( - label: String, - value: String -) { - Row( - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = label, - style = MaterialTheme.typography.bodyMedium, - color = textSecondaryColor() - ) - Text( - text = value, - style = MaterialTheme.typography.bodyMedium, - color = textColor() + if (showCancelOrderDialog) { + AlertDialog( + onDismissRequest = { showCancelOrderDialog = false }, + text = { Text(stringResource(R.string.order_detail_dialog_order_cancel)) }, + confirmButton = { + TextButton( + onClick = { + showCancelOrderDialog = false + viewModel.onEvent(OrderDetailUiEvent.CancelOrder) + } + ) { + Text(stringResource(R.string.common_confirm)) + } + }, + dismissButton = { + TextButton( + onClick = { showCancelOrderDialog = false } + ) { + Text(stringResource(R.string.common_cancel)) + } + } ) } -} - -@Composable -private fun OrderSection( - categoryName: String, - groupName: String, - parts: List -) { - Column( - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = "$categoryName > $groupName", - style = MaterialTheme.typography.titleMedium, - color = textColor() - ) - parts.forEach { part -> - OrderPartItem(part = part) - } - } -} - -@Composable -private fun OrderPartItem( - part: OrderPart -) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors(containerColor = backgroundCardColor()) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier.weight(1F) - ) { - Text( - text = part.name, - style = MaterialTheme.typography.titleMedium, - color = textColor() - ) - Text( - text = part.code, - style = MaterialTheme.typography.bodySmall, - color = textSecondaryColor() - ) + if (showReceiveOrderDialog) { + AlertDialog( + onDismissRequest = { showReceiveOrderDialog = false }, + text = { Text(stringResource(R.string.order_detail_dialog_order_receive)) }, + confirmButton = { + TextButton( + onClick = { + showReceiveOrderDialog = false + viewModel.onEvent(OrderDetailUiEvent.ReceiveOrder) + } + ) { + Text(stringResource(R.string.common_confirm)) + } + }, + dismissButton = { + TextButton( + onClick = { showReceiveOrderDialog = false } + ) { + Text(stringResource(R.string.common_cancel)) + } } - - Text( - text = part.quantity.toString(), - style = MaterialTheme.typography.titleMedium, - color = textColor() - ) - } + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailUiEvent.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailUiEvent.kt index 2caa7c4..90e3fb5 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailUiEvent.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailUiEvent.kt @@ -3,4 +3,7 @@ package com.sampoom.android.feature.order.ui sealed interface OrderDetailUiEvent { object LoadOrder : OrderDetailUiEvent object RetryOrder : OrderDetailUiEvent + object ReceiveOrder : OrderDetailUiEvent + object CancelOrder : OrderDetailUiEvent + object ClearError : OrderDetailUiEvent } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailUiState.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailUiState.kt index 6fdbede..fd4a4a7 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailUiState.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailUiState.kt @@ -5,5 +5,9 @@ import com.sampoom.android.feature.order.domain.model.Order data class OrderDetailUiState( val orderDetail: List = emptyList(), val orderDetailLoading: Boolean = false, - val orderDetailError: String? = null + val orderDetailError: String? = null, + val isProcessing: Boolean = false, + val isProcessingCancelSuccess: Boolean = false, + val isProcessingReceiveSuccess: Boolean = false, + val isProcessingError: String? = null ) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailViewModel.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailViewModel.kt index e604071..648142e 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailViewModel.kt @@ -5,7 +5,9 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sampoom.android.core.network.serverMessageOrNull +import com.sampoom.android.feature.order.domain.usecase.CancelOrderUseCase import com.sampoom.android.feature.order.domain.usecase.GetOrderDetailUseCase +import com.sampoom.android.feature.order.domain.usecase.ReceiveOrderUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -16,6 +18,8 @@ import javax.inject.Inject @HiltViewModel class OrderDetailViewModel @Inject constructor( private val getOrderDetailUseCase: GetOrderDetailUseCase, + private val cancelOrderUseCase: CancelOrderUseCase, + private val receiveOrderUseCase: ReceiveOrderUseCase, savedStateHandle: SavedStateHandle ) : ViewModel() { @@ -28,7 +32,17 @@ class OrderDetailViewModel @Inject constructor( // Navigation 인자 로드 private val agencyId: Long = savedStateHandle.get("agencyId") ?: 0L - private val orderId: Long = savedStateHandle.get("orderId") ?: 0L + private val navOrderId: Long = savedStateHandle.get("orderId") ?: 0L + + private var apiOrderId: Long? = null + + fun setOrderIdFromApi(orderId: Long) { + apiOrderId = orderId + } + + private fun getOrderId(): Long { + return apiOrderId ?: navOrderId + } private var errorLabel: String = "" @@ -37,14 +51,17 @@ class OrderDetailViewModel @Inject constructor( } init { - if (orderId > 0L) loadOrderDetail(orderId) + if (getOrderId() > 0L) loadOrderDetail(getOrderId()) else _uiState.update { it.copy(orderDetailError = errorLabel) } } fun onEvent(event: OrderDetailUiEvent) { when (event) { - is OrderDetailUiEvent.LoadOrder -> loadOrderDetail(orderId) - is OrderDetailUiEvent.RetryOrder -> loadOrderDetail(orderId) + is OrderDetailUiEvent.LoadOrder -> loadOrderDetail(getOrderId()) + is OrderDetailUiEvent.RetryOrder -> loadOrderDetail(getOrderId()) + is OrderDetailUiEvent.CancelOrder -> cancelOrder(getOrderId()) + is OrderDetailUiEvent.ReceiveOrder -> receiveOrder(getOrderId()) + is OrderDetailUiEvent.ClearError -> _uiState.update { it.copy(isProcessingError = null) } } } @@ -74,4 +91,62 @@ class OrderDetailViewModel @Inject constructor( Log.d(TAG, "submit: ${_uiState.value}") } } + + private fun cancelOrder(orderId: Long) { + viewModelScope.launch { + _uiState.update { it.copy(isProcessing = true, isProcessingError = null) } + + cancelOrderUseCase(orderId) + .onSuccess { + _uiState.update { + it.copy( + isProcessing = false, + isProcessingCancelSuccess = true, + isProcessingError = null + ) + } + } + .onFailure { throwable -> + val backendMessage = throwable.serverMessageOrNull() + _uiState.update { + it.copy( + isProcessing = false, + isProcessingError = backendMessage ?: (throwable.message ?: errorLabel) + ) + } + } + Log.d(TAG, "submit: ${_uiState.value}") + } + } + + private fun receiveOrder(orderId: Long) { + viewModelScope.launch { + _uiState.update { it.copy(isProcessing = true, isProcessingError = null) } + + receiveOrderUseCase(orderId) + .onSuccess { + _uiState.update { + it.copy( + isProcessing = false, + isProcessingReceiveSuccess = true, + isProcessingError = null + ) + } + } + .onFailure { throwable -> + val backendMessage = throwable.serverMessageOrNull() + _uiState.update { + it.copy( + isProcessing = false, + isProcessingError = backendMessage ?: (throwable.message ?: errorLabel) + ) + } + } + Log.d(TAG, "submit: ${_uiState.value}") + } + } + + fun clearSuccess() { + _uiState.update { it.copy(isProcessingCancelSuccess = false, isProcessingReceiveSuccess = false) } + } } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderResultBottomSheet.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderResultBottomSheet.kt new file mode 100644 index 0000000..eb2152c --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderResultBottomSheet.kt @@ -0,0 +1,168 @@ +package com.sampoom.android.feature.order.ui + +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.sampoom.android.R +import com.sampoom.android.core.ui.component.ButtonVariant +import com.sampoom.android.core.ui.component.CommonButton +import com.sampoom.android.core.ui.theme.SuccessGreen +import com.sampoom.android.core.ui.theme.textColor +import com.sampoom.android.feature.order.domain.model.Order + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun OrderResultBottomSheet( + order: List, + onDismiss: () -> Unit, + viewModel: OrderDetailViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ) + var showCancelOrderDialog by remember { mutableStateOf(false) } + val context = LocalContext.current + + LaunchedEffect(order) { + if (order.isNotEmpty()) viewModel.setOrderIdFromApi(order.first().orderId) + } + + // 성공 시 Toast 표시 후 다이얼로그 닫기 + LaunchedEffect(uiState.isProcessingCancelSuccess) { + if (uiState.isProcessingCancelSuccess) { + Toast.makeText(context, context.getString(R.string.order_detail_toast_order_cancel), Toast.LENGTH_SHORT).show() + } + viewModel.clearSuccess() + viewModel.onEvent(OrderDetailUiEvent.LoadOrder) + } + + // 실패 시 Toast 표시 + LaunchedEffect(uiState.isProcessingError) { + uiState.isProcessingError?.let { error -> + Toast.makeText(context, error, Toast.LENGTH_LONG).show() + viewModel.onEvent(OrderDetailUiEvent.ClearError) + } + } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState + ) { + Column( + modifier = Modifier + .fillMaxHeight(0.9f) + ) { + // 주문 완료 헤더 + OrderCompleteHeader() + + Spacer(modifier = Modifier.height(16.dp)) + + // OrderDetailContent 재사용 + OrderDetailContent( + order = order, + modifier = Modifier.weight(1f) + ) + + // 하단 고정 버튼들 + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth().padding(16.dp) + ) { + CommonButton( + modifier = Modifier.weight(1f), + variant = ButtonVariant.Error, + onClick = { showCancelOrderDialog = true } + ) { + Text(stringResource(R.string.order_detail_order_cancel)) + } + } + + // 하단 여백 + Spacer(modifier = Modifier.height(16.dp)) + } + } + + if (showCancelOrderDialog) { + AlertDialog( + onDismissRequest = { showCancelOrderDialog = false }, + text = { Text(stringResource(R.string.order_detail_dialog_order_cancel)) }, + confirmButton = { + TextButton( + onClick = { + showCancelOrderDialog = false + viewModel.onEvent(OrderDetailUiEvent.CancelOrder) + } + ) { + Text(stringResource(R.string.common_confirm)) + } + }, + dismissButton = { + TextButton( + onClick = { showCancelOrderDialog = false } + ) { + Text(stringResource(R.string.common_cancel)) + } + } + ) + } +} + +@Composable +private fun OrderCompleteHeader() { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.ic_check_circle), + contentDescription = stringResource(R.string.common_confirm), + tint = SuccessGreen, + modifier = Modifier.size(20.dp) + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = stringResource(R.string.cart_toast_order_text), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = textColor() + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/outbound/data/repository/OutboundRepositoryImpl.kt b/app/src/main/java/com/sampoom/android/feature/outbound/data/repository/OutboundRepositoryImpl.kt index 1139cf5..55c3f6f 100644 --- a/app/src/main/java/com/sampoom/android/feature/outbound/data/repository/OutboundRepositoryImpl.kt +++ b/app/src/main/java/com/sampoom/android/feature/outbound/data/repository/OutboundRepositoryImpl.kt @@ -18,8 +18,8 @@ class OutboundRepositoryImpl @Inject constructor( } override suspend fun processOutbound(): Result { - val dto = api.processOutbound() return runCatching { + val dto = api.processOutbound() if (!dto.success) throw Exception(dto.message) } } @@ -28,22 +28,22 @@ class OutboundRepositoryImpl @Inject constructor( partId: Long, quantity: Long ): Result { - val dto = api.addOutbound(AddOutboundRequestDto(partId, quantity)) return runCatching { + val dto = api.addOutbound(AddOutboundRequestDto(partId, quantity)) if (!dto.success) throw Exception(dto.message) } } override suspend fun deleteOutbound(outboundId: Long): Result { - val dto = api.deleteOutbound(outboundId) return runCatching { + val dto = api.deleteOutbound(outboundId) if (!dto.success) throw Exception(dto.message) } } override suspend fun deleteAllOutbound(): Result { - val dto = api.deleteAllOutbound() return runCatching { + val dto = api.deleteAllOutbound() if (!dto.success) throw Exception(dto.message) } } @@ -52,8 +52,8 @@ class OutboundRepositoryImpl @Inject constructor( outboundId: Long, quantity: Long ): Result { - val dto = api.updateOutbound(outboundId, UpdateOutboundRequestDto(quantity)) return runCatching { + val dto = api.updateOutbound(outboundId, UpdateOutboundRequestDto(quantity)) if (!dto.success) throw Exception(dto.message) } } diff --git a/app/src/main/res/drawable/ic_check_circle.xml b/app/src/main/res/drawable/ic_check_circle.xml new file mode 100644 index 0000000..7f747c6 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 52953bd..dbc472d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -73,9 +73,9 @@ 장바구니 목록이 없습니다. 장바구니 목록에 추가하시겠습니까? 장바구니 목록에 추가되었습니다 - 장바구니에 추가하시겠습니까? + 장바구니 목륵의 상품을 주문하시겠습니까? 장바구니를 비우시겠습니까? - 추가되었습니다 + 주문이 완료되었습니다 부품 주문 @@ -87,6 +87,12 @@ 대리점 주문상태 주문상품 + 주문취소 + 주문 취소처리하시겠습니까? + 주문 취소처리되었습니다 + 입고처리 + 입고 처리하시겠습니까? + 입고 처리되었습니다 승인대기 From 9430d1f6227836ed5514f7150552bd1f39e6c493 Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Tue, 21 Oct 2025 11:07:03 +0900 Subject: [PATCH 04/10] =?UTF-8?q?[REFAC]=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20I?= =?UTF-8?q?mport=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/sampoom/android/feature/cart/ui/CartListScreen.kt | 2 -- .../com/sampoom/android/feature/cart/ui/CartListViewModel.kt | 1 - .../com/sampoom/android/feature/order/ui/OrderDetailScreen.kt | 1 - .../com/sampoom/android/feature/order/ui/OrderListScreen.kt | 3 --- .../sampoom/android/feature/part/ui/PartDetailBottomSheet.kt | 2 -- .../java/com/sampoom/android/feature/part/ui/PartListScreen.kt | 1 - 6 files changed, 10 deletions(-) diff --git a/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListScreen.kt b/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListScreen.kt index 5c6ad52..52e1333 100644 --- a/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListScreen.kt @@ -1,6 +1,5 @@ package com.sampoom.android.feature.cart.ui -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -28,7 +27,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp diff --git a/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListViewModel.kt b/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListViewModel.kt index 4f6d7a1..ff30e54 100644 --- a/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListViewModel.kt @@ -1,6 +1,5 @@ package com.sampoom.android.feature.cart.ui -import android.R.attr.order import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt index 900ed05..4a2e811 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt @@ -36,7 +36,6 @@ import com.sampoom.android.core.ui.component.ButtonVariant import com.sampoom.android.core.ui.component.CommonButton import com.sampoom.android.core.ui.component.EmptyContent import com.sampoom.android.core.ui.component.ErrorContent -import com.sampoom.android.core.ui.component.ErrorContent @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListScreen.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListScreen.kt index 8e4d528..ae9420f 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator @@ -25,7 +24,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -39,7 +37,6 @@ import com.sampoom.android.core.ui.theme.textSecondaryColor import com.sampoom.android.core.util.buildOrderTitle import com.sampoom.android.core.util.formatDate import com.sampoom.android.feature.order.domain.model.Order -import com.sampoom.android.feature.order.domain.model.OrderPart @Composable fun OrderListScreen( diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartDetailBottomSheet.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartDetailBottomSheet.kt index 24419cd..540c120 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/ui/PartDetailBottomSheet.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartDetailBottomSheet.kt @@ -31,7 +31,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign -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 @@ -42,7 +41,6 @@ import com.sampoom.android.core.ui.component.CommonButton import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.core.ui.theme.textSecondaryColor import com.sampoom.android.feature.part.domain.model.Part -import kotlinx.coroutines.delay @Composable fun PartDetailBottomSheet( diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt index 17f7c76..8aa2fae 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt @@ -45,7 +45,6 @@ import com.sampoom.android.core.ui.theme.backgroundCardColor import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.core.ui.theme.textSecondaryColor import com.sampoom.android.feature.part.domain.model.Part -import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable From 1b4b94b4fd22dfe6de15667a3ffcb39a072eb5e4 Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Tue, 21 Oct 2025 12:08:15 +0900 Subject: [PATCH 05/10] =?UTF-8?q?[FIX]=20=EC=BD=94=EB=93=9C=20=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EB=A6=AC=EB=B7=B0=20=EC=82=AC=ED=95=AD=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/app/navigation/AppNavHost.kt | 3 +- .../sampoom/android/core/util/FormatDate.kt | 31 ++++++-- .../sampoom/android/core/util/OrderTitle.kt | 2 +- .../feature/cart/data/remote/api/CartApi.kt | 1 + .../data/repository/CartRepositoryImpl.kt | 30 +++++++- .../feature/cart/ui/CartListViewModel.kt | 37 ++++----- .../feature/order/data/remote/api/OrderApi.kt | 1 + .../data/repository/OrderRepositoryImpl.kt | 15 +++- .../android/feature/order/di/OrderModules.kt | 2 +- .../feature/order/domain/model/OrderStatus.kt | 4 +- .../feature/order/ui/OrderDetailScreen.kt | 25 ++++-- .../feature/order/ui/OrderDetailViewModel.kt | 36 +++++---- .../feature/order/ui/OrderListScreen.kt | 14 ++-- .../feature/order/ui/OrderListViewModel.kt | 36 +++++---- .../order/ui/OrderResultBottomSheet.kt | 11 ++- .../outbound/data/remote/api/OutboundApi.kt | 1 + .../data/repository/OutboundRepositoryImpl.kt | 36 +++++++-- .../feature/outbound/ui/OutboundListScreen.kt | 2 +- .../outbound/ui/OutboundListViewModel.kt | 37 ++++----- .../feature/part/data/remote/api/PartApi.kt | 1 + .../feature/part/ui/PartDetailBottomSheet.kt | 4 +- .../feature/part/ui/PartListViewModel.kt | 36 +++++---- .../android/feature/part/ui/PartViewModel.kt | 77 ++++++++++--------- app/src/main/res/values/strings.xml | 2 +- 24 files changed, 277 insertions(+), 167 deletions(-) diff --git a/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt b/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt index 4531182..d243585 100644 --- a/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt +++ b/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt @@ -23,7 +23,6 @@ import com.sampoom.android.R import com.sampoom.android.feature.auth.ui.LoginScreen import com.sampoom.android.feature.auth.ui.SignUpScreen import com.sampoom.android.feature.cart.ui.CartListScreen -import com.sampoom.android.feature.order.domain.model.OrderList import com.sampoom.android.feature.order.ui.OrderDetailScreen import com.sampoom.android.feature.order.ui.OrderListScreen import com.sampoom.android.feature.outbound.ui.OutboundListScreen @@ -101,6 +100,7 @@ fun AppNavHost() { navController.navigateUp() }, onNavigatePartList = { group -> + // TODO: 실제 사용자의 agencyId 사용 navController.navigate(routePartList(1, group.id)) } ) @@ -155,6 +155,7 @@ fun MainScreen( composable(ROUTE_ORDERS) { OrderListScreen( onNavigateOrderDetail = { order -> + // TODO: 실제 사용자의 agencyId 사용 parentNavController.navigate(routeOrderDetail(1, order.orderId)) } ) diff --git a/app/src/main/java/com/sampoom/android/core/util/FormatDate.kt b/app/src/main/java/com/sampoom/android/core/util/FormatDate.kt index 8fc7deb..272d9e4 100644 --- a/app/src/main/java/com/sampoom/android/core/util/FormatDate.kt +++ b/app/src/main/java/com/sampoom/android/core/util/FormatDate.kt @@ -1,11 +1,28 @@ package com.sampoom.android.core.util +import android.os.Build +import androidx.annotation.RequiresApi +import java.time.format.DateTimeFormatter + +@RequiresApi(Build.VERSION_CODES.O) fun formatDate(dateString: String): String { - return try { - val inFmt = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", java.util.Locale.getDefault()) - val outFmt = java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault()) - outFmt.format(inFmt.parse(dateString) ?: java.util.Date()) - } catch (_: Exception) { - dateString - } + return runCatching { + val out = DateTimeFormatter.ISO_LOCAL_DATE + val hasOffset = + dateString.endsWith("Z") || dateString.contains('+') || dateString.contains('-') + .and(dateString.count { it == ':' } >= 3) + val date = if (hasOffset) { + val inFmt = java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME + java.time.OffsetDateTime.parse(dateString, inFmt).toLocalDate() + } else { + val inFmt = java.time.format.DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd'T'HH:mm:ss") + .optionalStart() + .appendFraction(java.time.temporal.ChronoField.NANO_OF_SECOND, 0, 6, true) + .optionalEnd() + .toFormatter(java.util.Locale.ROOT) + java.time.LocalDateTime.parse(dateString, inFmt).toLocalDate() + } + date.format(out) + }.getOrElse { dateString } } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/core/util/OrderTitle.kt b/app/src/main/java/com/sampoom/android/core/util/OrderTitle.kt index d027df7..631540a 100644 --- a/app/src/main/java/com/sampoom/android/core/util/OrderTitle.kt +++ b/app/src/main/java/com/sampoom/android/core/util/OrderTitle.kt @@ -21,6 +21,6 @@ fun buildOrderTitle(order: Order): String { return if (totalParts == 1) { "$groupName - ${part.name} ${part.quantity}EA" } else { - "${part.name} - $groupName ${part.quantity}EA 외 ${totalParts - 1}건" + "$groupName - ${part.name} ${part.quantity}EA 외 ${totalParts - 1}건" } } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/cart/data/remote/api/CartApi.kt b/app/src/main/java/com/sampoom/android/feature/cart/data/remote/api/CartApi.kt index aafff14..06bacec 100644 --- a/app/src/main/java/com/sampoom/android/feature/cart/data/remote/api/CartApi.kt +++ b/app/src/main/java/com/sampoom/android/feature/cart/data/remote/api/CartApi.kt @@ -12,6 +12,7 @@ import retrofit2.http.POST import retrofit2.http.PUT import retrofit2.http.Path +// TODO: AgencyId 동적 주입 interface CartApi { // 장바구니 목록 조회 @GET("agency/1/cart") diff --git a/app/src/main/java/com/sampoom/android/feature/cart/data/repository/CartRepositoryImpl.kt b/app/src/main/java/com/sampoom/android/feature/cart/data/repository/CartRepositoryImpl.kt index 87cf282..ddc5d07 100644 --- a/app/src/main/java/com/sampoom/android/feature/cart/data/repository/CartRepositoryImpl.kt +++ b/app/src/main/java/com/sampoom/android/feature/cart/data/repository/CartRepositoryImpl.kt @@ -6,7 +6,9 @@ import com.sampoom.android.feature.cart.data.remote.dto.AddCartRequestDto import com.sampoom.android.feature.cart.data.remote.dto.UpdateCartRequestDto import com.sampoom.android.feature.cart.domain.model.CartList import com.sampoom.android.feature.cart.domain.repository.CartRepository +import kotlinx.coroutines.CancellationException import javax.inject.Inject +import kotlin.Result class CartRepositoryImpl @Inject constructor( private val api: CartApi @@ -21,23 +23,38 @@ class CartRepositoryImpl @Inject constructor( partId: Long, quantity: Long ): Result { - return runCatching { + return try { val dto = api.addCart(AddCartRequestDto(partId, quantity)) if (!dto.success) throw Exception(dto.message) + Result.success(Unit) + } catch (ce: CancellationException) { + throw ce + } catch (t : Throwable) { + Result.failure(t) } } override suspend fun deleteCart(cartItemId: Long): Result { - return runCatching { + return try { val dto = api.deleteCart(cartItemId) if (!dto.success) throw Exception(dto.message) + Result.success(Unit) + } catch (ce: CancellationException) { + throw ce + } catch (t : Throwable) { + Result.failure(t) } } override suspend fun deleteAllCart(): Result { - return runCatching { + return try { val dto = api.deleteAllCart() if (!dto.success) throw Exception(dto.message) + Result.success(Unit) + } catch (ce: CancellationException) { + throw ce + } catch (t : Throwable) { + Result.failure(t) } } @@ -45,9 +62,14 @@ class CartRepositoryImpl @Inject constructor( cartItemId: Long, quantity: Long ): Result { - return runCatching { + return try { val dto = api.updateCart(cartItemId, UpdateCartRequestDto(quantity)) if (!dto.success) throw Exception(dto.message) + Result.success(Unit) + } catch (ce: CancellationException) { + throw ce + } catch (t : Throwable) { + Result.failure(t) } } } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListViewModel.kt b/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListViewModel.kt index ff30e54..80506ab 100644 --- a/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListViewModel.kt @@ -10,6 +10,7 @@ import com.sampoom.android.feature.cart.domain.usecase.GetCartUseCase import com.sampoom.android.feature.cart.domain.usecase.UpdateCartQuantityUseCase import com.sampoom.android.feature.order.domain.usecase.CreateOrderUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -60,26 +61,26 @@ class CartListViewModel @Inject constructor( viewModelScope.launch { _uiState.update { it.copy(cartLoading = true, cartError = null) } - runCatching { getCartListUseCase() } - .onSuccess { cartList -> - _uiState.update { - it.copy( - cartList = cartList.items, - cartLoading = false, - cartError = null - ) - } + try { + val cartList = getCartListUseCase() + _uiState.update { + it.copy( + cartList = cartList.items, + cartLoading = false, + cartError = null + ) } - .onFailure { throwable -> - val backendMessage = throwable.serverMessageOrNull() - _uiState.update { - it.copy( - cartLoading = false, - cartError = backendMessage ?: (throwable.message ?: errorLabel) - ) - } - + } catch (ce: CancellationException) { + throw ce + } catch (throwable: Throwable) { + val backendMessage = throwable.serverMessageOrNull() + _uiState.update { + it.copy( + cartLoading = false, + cartError = backendMessage ?: (throwable.message ?: errorLabel) + ) } + } Log.d(TAG, "submit: ${_uiState.value}") } } diff --git a/app/src/main/java/com/sampoom/android/feature/order/data/remote/api/OrderApi.kt b/app/src/main/java/com/sampoom/android/feature/order/data/remote/api/OrderApi.kt index d8fc9fc..6785d0c 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/data/remote/api/OrderApi.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/data/remote/api/OrderApi.kt @@ -9,6 +9,7 @@ import retrofit2.http.PATCH import retrofit2.http.POST import retrofit2.http.Path +// TODO: AgencyId 동적 주입 interface OrderApi { // 주문 목록 조회 @GET("agency/1/orders") diff --git a/app/src/main/java/com/sampoom/android/feature/order/data/repository/OrderRepositoryImpl.kt b/app/src/main/java/com/sampoom/android/feature/order/data/repository/OrderRepositoryImpl.kt index 16a74e1..f43065a 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/data/repository/OrderRepositoryImpl.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/data/repository/OrderRepositoryImpl.kt @@ -5,6 +5,7 @@ import com.sampoom.android.feature.order.data.remote.api.OrderApi import com.sampoom.android.feature.order.domain.model.OrderList import com.sampoom.android.feature.order.domain.repository.OrderRepository import javax.inject.Inject +import kotlin.coroutines.cancellation.CancellationException class OrderRepositoryImpl @Inject constructor( private val api: OrderApi @@ -22,9 +23,14 @@ class OrderRepositoryImpl @Inject constructor( } override suspend fun receiveOrder(orderId: Long): Result { - return runCatching { + return try { val dto = api.receiveOrder(orderId) if (!dto.success) throw Exception(dto.message) + Result.success(Unit) + } catch (ce : CancellationException) { + throw ce + } catch (t : Throwable) { + Result.failure(t) } } @@ -35,9 +41,14 @@ class OrderRepositoryImpl @Inject constructor( } override suspend fun cancelOrder(orderId: Long): Result { - return runCatching { + return try { val dto = api.cancelOrder(orderId) if (!dto.success) throw Exception(dto.message) + Result.success(Unit) + } catch (ce : CancellationException) { + throw ce + } catch (t : Throwable) { + Result.failure(t) } } } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/order/di/OrderModules.kt b/app/src/main/java/com/sampoom/android/feature/order/di/OrderModules.kt index fe342bf..c17f4fb 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/di/OrderModules.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/di/OrderModules.kt @@ -8,8 +8,8 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import jakarta.inject.Singleton import retrofit2.Retrofit +import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) diff --git a/app/src/main/java/com/sampoom/android/feature/order/domain/model/OrderStatus.kt b/app/src/main/java/com/sampoom/android/feature/order/domain/model/OrderStatus.kt index 4e31967..211339b 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/domain/model/OrderStatus.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/domain/model/OrderStatus.kt @@ -1,10 +1,12 @@ package com.sampoom.android.feature.order.domain.model +import java.util.Locale + enum class OrderStatus { PENDING, COMPLETED, CANCELED; companion object { - fun from(raw: String?): OrderStatus = when (raw?.uppercase()) { + fun from(raw: String?): OrderStatus = when (raw?.uppercase(Locale.ROOT)) { "PENDING" -> PENDING "COMPLETED" -> COMPLETED "CANCELED" -> CANCELED diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt index 4a2e811..7f7b5a3 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt @@ -36,6 +36,7 @@ import com.sampoom.android.core.ui.component.ButtonVariant import com.sampoom.android.core.ui.component.CommonButton import com.sampoom.android.core.ui.component.EmptyContent import com.sampoom.android.core.ui.component.ErrorContent +import com.sampoom.android.feature.order.domain.model.OrderStatus @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -52,18 +53,18 @@ fun OrderDetailScreen( LaunchedEffect(uiState.isProcessingCancelSuccess) { if (uiState.isProcessingCancelSuccess) { Toast.makeText(context, context.getString(R.string.order_detail_toast_order_cancel), Toast.LENGTH_SHORT).show() + viewModel.clearSuccess() + viewModel.onEvent(OrderDetailUiEvent.LoadOrder) } - viewModel.clearSuccess() - viewModel.onEvent(OrderDetailUiEvent.LoadOrder) } // 성공 시 Toast 표시 후 다이얼로그 닫기 LaunchedEffect(uiState.isProcessingReceiveSuccess) { if (uiState.isProcessingReceiveSuccess) { Toast.makeText(context, context.getString(R.string.order_detail_toast_order_receive), Toast.LENGTH_SHORT).show() + viewModel.clearSuccess() + viewModel.onEvent(OrderDetailUiEvent.LoadOrder) } - viewModel.clearSuccess() - viewModel.onEvent(OrderDetailUiEvent.LoadOrder) } // 실패 시 Toast 표시 @@ -90,11 +91,15 @@ fun OrderDetailScreen( }, bottomBar = { Row( - modifier = Modifier.fillMaxWidth().padding(16.dp) + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) ) { CommonButton( modifier = Modifier.weight(1f), variant = ButtonVariant.Error, + enabled = uiState.orderDetail.firstOrNull()?.status != OrderStatus.COMPLETED && + uiState.orderDetail.firstOrNull()?.status != OrderStatus.CANCELED, onClick = { showCancelOrderDialog = true } ) { Text(stringResource(R.string.order_detail_order_cancel)) @@ -102,6 +107,8 @@ fun OrderDetailScreen( Spacer(Modifier.width(16.dp)) CommonButton( modifier = Modifier.weight(1f), + enabled = uiState.orderDetail.firstOrNull()?.status != OrderStatus.PENDING && + uiState.orderDetail.firstOrNull()?.status != OrderStatus.CANCELED, onClick = { showReceiveOrderDialog = true } ) { Text(stringResource(R.string.order_detail_order_receive)) @@ -124,14 +131,18 @@ fun OrderDetailScreen( uiState.orderDetailError != null -> { ErrorContent( onRetry = { viewModel.onEvent(OrderDetailUiEvent.RetryOrder) }, - modifier = Modifier.height(200.dp).fillMaxWidth() + modifier = Modifier + .height(200.dp) + .fillMaxWidth() ) } uiState.orderDetail.isEmpty() -> { EmptyContent( message = stringResource(R.string.order_empty_list), - modifier = Modifier.height(200.dp).fillMaxWidth() + modifier = Modifier + .height(200.dp) + .fillMaxWidth() ) } diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailViewModel.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailViewModel.kt index 648142e..a0bd0af 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailViewModel.kt @@ -9,6 +9,7 @@ import com.sampoom.android.feature.order.domain.usecase.CancelOrderUseCase import com.sampoom.android.feature.order.domain.usecase.GetOrderDetailUseCase import com.sampoom.android.feature.order.domain.usecase.ReceiveOrderUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -69,25 +70,26 @@ class OrderDetailViewModel @Inject constructor( viewModelScope.launch { _uiState.update { it.copy(orderDetailLoading = true, orderDetailError = null) } - runCatching { getOrderDetailUseCase(orderId) } - .onSuccess { orderList -> - _uiState.update { - it.copy( - orderDetail = orderList.items, - orderDetailLoading = false, - orderDetailError = null - ) - } + try { + val orderList = getOrderDetailUseCase(orderId) + _uiState.update { + it.copy( + orderDetail = orderList.items, + orderDetailLoading = false, + orderDetailError = null + ) } - .onFailure { throwable -> - val backendMessage = throwable.serverMessageOrNull() - _uiState.update { - it.copy( - orderDetailLoading = false, - orderDetailError = backendMessage ?: (throwable.message ?: errorLabel) - ) - } + } catch (ce : CancellationException) { + throw ce + } catch (throwable : Throwable) { + val backendMessage = throwable.serverMessageOrNull() + _uiState.update { + it.copy( + orderDetailLoading = false, + orderDetailError = backendMessage ?: (throwable.message ?: errorLabel) + ) } + } Log.d(TAG, "submit: ${_uiState.value}") } } diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListScreen.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListScreen.kt index ae9420f..3b38ff8 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListScreen.kt @@ -1,5 +1,6 @@ package com.sampoom.android.feature.order.ui +import android.R.attr.order import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -12,6 +13,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator @@ -114,13 +116,11 @@ fun OrderListScreen( .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - uiState.orderList.forEach { order -> - item { - OrderItem( - order = order, - onClick = { onNavigateOrderDetail(order) } - ) - } + items(uiState.orderList) { order -> + OrderItem( + order = order, + onClick = { onNavigateOrderDetail(order) } + ) } item { Spacer(Modifier.height(100.dp)) } } diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListViewModel.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListViewModel.kt index a5fcf74..a9cd392 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope import com.sampoom.android.core.network.serverMessageOrNull import com.sampoom.android.feature.order.domain.usecase.GetOrderUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -45,25 +46,26 @@ class OrderListViewModel @Inject constructor( viewModelScope.launch { _uiState.update { it.copy(orderLoading = true, orderError = null) } - runCatching { getOrderListUseCase() } - .onSuccess { orderList -> - _uiState.update { - it.copy( - orderList = orderList.items, - orderLoading = false, - orderError = null - ) - } + try { + val orderList = getOrderListUseCase() + _uiState.update { + it.copy( + orderList = orderList.items, + orderLoading = false, + orderError = null + ) } - .onFailure { throwable -> - val backendMessage = throwable.serverMessageOrNull() - _uiState.update { - it.copy( - orderLoading = false, - orderError = backendMessage ?: (throwable.message ?: errorLabel ) - ) - } + } catch (ce: CancellationException) { + throw ce + } catch (throwable: Throwable) { + val backendMessage = throwable.serverMessageOrNull() + _uiState.update { + it.copy( + orderLoading = false, + orderError = backendMessage ?: (throwable.message ?: errorLabel ) + ) } + } Log.d(TAG, "submit: ${_uiState.value}") } } diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderResultBottomSheet.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderResultBottomSheet.kt index eb2152c..b125c62 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderResultBottomSheet.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderResultBottomSheet.kt @@ -40,6 +40,7 @@ import com.sampoom.android.core.ui.component.CommonButton import com.sampoom.android.core.ui.theme.SuccessGreen import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.feature.order.domain.model.Order +import com.sampoom.android.feature.order.domain.model.OrderStatus @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -63,9 +64,9 @@ fun OrderResultBottomSheet( LaunchedEffect(uiState.isProcessingCancelSuccess) { if (uiState.isProcessingCancelSuccess) { Toast.makeText(context, context.getString(R.string.order_detail_toast_order_cancel), Toast.LENGTH_SHORT).show() + viewModel.clearSuccess() + viewModel.onEvent(OrderDetailUiEvent.LoadOrder) } - viewModel.clearSuccess() - viewModel.onEvent(OrderDetailUiEvent.LoadOrder) } // 실패 시 Toast 표시 @@ -99,11 +100,15 @@ fun OrderResultBottomSheet( Spacer(modifier = Modifier.height(16.dp)) Row( - modifier = Modifier.fillMaxWidth().padding(16.dp) + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) ) { CommonButton( modifier = Modifier.weight(1f), variant = ButtonVariant.Error, + enabled = order.firstOrNull()?.status != OrderStatus.COMPLETED && + order.firstOrNull()?.status != OrderStatus.CANCELED, onClick = { showCancelOrderDialog = true } ) { Text(stringResource(R.string.order_detail_order_cancel)) diff --git a/app/src/main/java/com/sampoom/android/feature/outbound/data/remote/api/OutboundApi.kt b/app/src/main/java/com/sampoom/android/feature/outbound/data/remote/api/OutboundApi.kt index 02c13bf..d7ee115 100644 --- a/app/src/main/java/com/sampoom/android/feature/outbound/data/remote/api/OutboundApi.kt +++ b/app/src/main/java/com/sampoom/android/feature/outbound/data/remote/api/OutboundApi.kt @@ -12,6 +12,7 @@ import retrofit2.http.PATCH import retrofit2.http.POST import retrofit2.http.Path +// TODO: AgencyId 동적 주입 interface OutboundApi { // 출고 목록 조회 @GET("agency/1/outbound") diff --git a/app/src/main/java/com/sampoom/android/feature/outbound/data/repository/OutboundRepositoryImpl.kt b/app/src/main/java/com/sampoom/android/feature/outbound/data/repository/OutboundRepositoryImpl.kt index 55c3f6f..71f3245 100644 --- a/app/src/main/java/com/sampoom/android/feature/outbound/data/repository/OutboundRepositoryImpl.kt +++ b/app/src/main/java/com/sampoom/android/feature/outbound/data/repository/OutboundRepositoryImpl.kt @@ -7,6 +7,7 @@ import com.sampoom.android.feature.outbound.data.remote.dto.UpdateOutboundReques import com.sampoom.android.feature.outbound.domain.model.OutboundList import com.sampoom.android.feature.outbound.domain.repository.OutboundRepository import jakarta.inject.Inject +import kotlin.coroutines.cancellation.CancellationException class OutboundRepositoryImpl @Inject constructor( private val api: OutboundApi @@ -18,9 +19,14 @@ class OutboundRepositoryImpl @Inject constructor( } override suspend fun processOutbound(): Result { - return runCatching { + return try { val dto = api.processOutbound() if (!dto.success) throw Exception(dto.message) + Result.success(Unit) + } catch (ce : CancellationException) { + throw ce + } catch (t : Throwable) { + Result.failure(t) } } @@ -28,23 +34,38 @@ class OutboundRepositoryImpl @Inject constructor( partId: Long, quantity: Long ): Result { - return runCatching { + return try { val dto = api.addOutbound(AddOutboundRequestDto(partId, quantity)) if (!dto.success) throw Exception(dto.message) + Result.success(Unit) + } catch (ce : CancellationException) { + throw ce + } catch (t : Throwable) { + Result.failure(t) } } override suspend fun deleteOutbound(outboundId: Long): Result { - return runCatching { + return try { val dto = api.deleteOutbound(outboundId) if (!dto.success) throw Exception(dto.message) + Result.success(Unit) + } catch (ce : CancellationException) { + throw ce + } catch (t : Throwable) { + Result.failure(t) } } override suspend fun deleteAllOutbound(): Result { - return runCatching { + return try { val dto = api.deleteAllOutbound() if (!dto.success) throw Exception(dto.message) + Result.success(Unit) + } catch (ce : CancellationException) { + throw ce + } catch (t : Throwable) { + Result.failure(t) } } @@ -52,9 +73,14 @@ class OutboundRepositoryImpl @Inject constructor( outboundId: Long, quantity: Long ): Result { - return runCatching { + return try { val dto = api.updateOutbound(outboundId, UpdateOutboundRequestDto(quantity)) if (!dto.success) throw Exception(dto.message) + Result.success(Unit) + } catch (ce : CancellationException) { + throw ce + } catch (t : Throwable) { + Result.failure(t) } } } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListScreen.kt b/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListScreen.kt index 4323ec8..a2501d2 100644 --- a/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListScreen.kt @@ -70,8 +70,8 @@ fun OutboundListScreen( LaunchedEffect(uiState.isOrderSuccess) { if (uiState.isOrderSuccess) { Toast.makeText(context, context.getString(R.string.outbound_toast_order_text), Toast.LENGTH_SHORT).show() + viewModel.clearSuccess() } - viewModel.clearSuccess() } Column(Modifier.fillMaxSize()) { diff --git a/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListViewModel.kt b/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListViewModel.kt index 4fa3c09..8a49c79 100644 --- a/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListViewModel.kt @@ -10,6 +10,7 @@ import com.sampoom.android.feature.outbound.domain.usecase.GetOutboundUseCase import com.sampoom.android.feature.outbound.domain.usecase.ProcessOutboundUseCase import com.sampoom.android.feature.outbound.domain.usecase.UpdateOutboundQuantityUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -59,26 +60,26 @@ class OutboundListViewModel @Inject constructor( viewModelScope.launch { _uiState.update { it.copy(outboundLoading = true, outboundError = null) } - runCatching { getOutboundUseCase() } - .onSuccess { outboundList -> - _uiState.update { - it.copy( - outboundList = outboundList.items, - outboundLoading = false, - outboundError = null - ) - } + try { + val outboundList = getOutboundUseCase() + _uiState.update { + it.copy( + outboundList = outboundList.items, + outboundLoading = false, + outboundError = null + ) } - .onFailure { throwable -> - val backendMessage = throwable.serverMessageOrNull() - _uiState.update { - it.copy( - outboundLoading = false, - outboundError = backendMessage ?: (throwable.message ?: errorLabel) - ) - } - + } catch (ce: CancellationException) { + throw ce + } catch (throwable: Throwable) { + val backendMessage = throwable.serverMessageOrNull() + _uiState.update { + it.copy( + outboundLoading = false, + outboundError = backendMessage ?: (throwable.message ?: errorLabel) + ) } + } Log.d(TAG, "submit: ${_uiState.value}") } } diff --git a/app/src/main/java/com/sampoom/android/feature/part/data/remote/api/PartApi.kt b/app/src/main/java/com/sampoom/android/feature/part/data/remote/api/PartApi.kt index 06f1004..eaea882 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/data/remote/api/PartApi.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/data/remote/api/PartApi.kt @@ -7,6 +7,7 @@ import com.sampoom.android.feature.part.data.remote.dto.PartDto import retrofit2.http.GET import retrofit2.http.Path +// TODO: AgencyId 동적 주입 interface PartApi { @GET("agency/category") suspend fun getCategoryList(): ApiResponse> diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartDetailBottomSheet.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartDetailBottomSheet.kt index 540c120..d9a2701 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/ui/PartDetailBottomSheet.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartDetailBottomSheet.kt @@ -70,16 +70,16 @@ fun PartDetailBottomSheet( LaunchedEffect(uiState.isOutboundSuccess) { if (uiState.isOutboundSuccess) { Toast.makeText(context, context.getString(R.string.outbound_toast_success), Toast.LENGTH_SHORT).show() + viewModel.clearSuccess() } - viewModel.clearSuccess() } // 성공 시 Toast 표시 후 다이얼로그 닫기 LaunchedEffect(uiState.isCartSuccess) { if (uiState.isCartSuccess) { Toast.makeText(context, context.getString(R.string.cart_toast_success), Toast.LENGTH_SHORT).show() + viewModel.clearSuccess() } - viewModel.clearSuccess() } // 실패 시 Toast 표시 diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartListViewModel.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListViewModel.kt index f582f31..80897f3 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/ui/PartListViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope import com.sampoom.android.core.network.serverMessageOrNull import com.sampoom.android.feature.part.domain.usecase.GetPartUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -54,25 +55,26 @@ class PartListViewModel @Inject constructor( viewModelScope.launch { _uiState.update { it.copy(partListLoading = true, partListError = null) } - runCatching { getPartListUseCase(groupId) } - .onSuccess { partList -> - _uiState.update { - it.copy( - partList = partList.items, - partListLoading = false, - partListError = null - ) - } + try { + val partList = getPartListUseCase(groupId) + _uiState.update { + it.copy( + partList = partList.items, + partListLoading = false, + partListError = null + ) } - .onFailure { throwable -> - val backendMessage = throwable.serverMessageOrNull() - _uiState.update { - it.copy( - partListLoading = false, - partListError = backendMessage ?: (throwable.message ?: errorLabel) - ) - } + } catch (ce: CancellationException) { + throw ce + } catch (throwable: Throwable) { + val backendMessage = throwable.serverMessageOrNull() + _uiState.update { + it.copy( + partListLoading = false, + partListError = backendMessage ?: (throwable.message ?: errorLabel) + ) } + } Log.d(TAG, "submit: ${_uiState.value}") } } diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartViewModel.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartViewModel.kt index 2a87763..bb8e0c8 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/ui/PartViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartViewModel.kt @@ -8,6 +8,7 @@ import com.sampoom.android.feature.part.domain.model.Category import com.sampoom.android.feature.part.domain.usecase.GetCategoryUseCase import com.sampoom.android.feature.part.domain.usecase.GetGroupUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -52,25 +53,26 @@ class PartViewModel @Inject constructor( viewModelScope.launch { _uiState.update { it.copy(categoryLoading = true, categoryError = null) } - runCatching { getCategoryUseCase() } - .onSuccess { categoryList -> - _uiState.update { - it.copy( - categoryList = categoryList.items, - categoryLoading = false, - categoryError = null - ) - } + try { + val categoryList = getCategoryUseCase() + _uiState.update { + it.copy( + categoryList = categoryList.items, + categoryLoading = false, + categoryError = null + ) } - .onFailure { throwable -> - val backendMessage = throwable.serverMessageOrNull() - _uiState.update { - it.copy( - categoryLoading = false, - categoryError = backendMessage ?: (throwable.message ?: errorLabel) - ) - } + } catch (ce: CancellationException) { + throw ce + } catch (throwable: Throwable) { + val backendMessage = throwable.serverMessageOrNull() + _uiState.update { + it.copy( + categoryLoading = false, + categoryError = backendMessage ?: (throwable.message ?: errorLabel) + ) } + } Log.d(TAG, "loadCategory: ${_uiState.value}") } } @@ -88,28 +90,29 @@ class PartViewModel @Inject constructor( groupLoadJob = viewModelScope.launch { _uiState.update { it.copy(groupLoading = true, groupError = null) } - runCatching { getGroupUseCase(categoryId) } - .onSuccess { groupList -> - // 최신 선택과 불일치하면 무시 - if (_uiState.value.selectedCategory?.id != categoryId) return@onSuccess - _uiState.update { - it.copy( - groupList = groupList.items, - groupLoading = false, - groupError = null - ) - } + try { + val groupList = getGroupUseCase(categoryId) + // 최신 선택과 불일치하면 무시 + if (_uiState.value.selectedCategory?.id != categoryId) return@launch + _uiState.update { + it.copy( + groupList = groupList.items, + groupLoading = false, + groupError = null + ) } - .onFailure { throwable -> - val backendMessage = throwable.serverMessageOrNull() - if (_uiState.value.selectedCategory?.id != categoryId) return@onFailure - _uiState.update { - it.copy( - groupLoading = false, - groupError = backendMessage ?: (throwable.message ?: errorLabel) - ) - } + } catch (ce: CancellationException) { + throw ce + } catch (throwable: Throwable) { + val backendMessage = throwable.serverMessageOrNull() + if (_uiState.value.selectedCategory?.id != categoryId) return@launch + _uiState.update { + it.copy( + groupLoading = false, + groupError = backendMessage ?: (throwable.message ?: errorLabel) + ) } + } Log.d(TAG, "loadGroup: ${_uiState.value}") } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dbc472d..b61421d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -73,7 +73,7 @@ 장바구니 목록이 없습니다. 장바구니 목록에 추가하시겠습니까? 장바구니 목록에 추가되었습니다 - 장바구니 목륵의 상품을 주문하시겠습니까? + 장바구니 목록의 상품을 주문하시겠습니까? 장바구니를 비우시겠습니까? 주문이 완료되었습니다 부품 주문 From bed77cda51715442eda578a56248228ab733906f Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Tue, 21 Oct 2025 13:03:47 +0900 Subject: [PATCH 06/10] =?UTF-8?q?[FIX]=20=EC=BD=94=EB=93=9C=20=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EB=A6=AC=EB=B7=B0=20=EC=82=AC=ED=95=AD=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/feature/order/ui/OrderListScreen.kt | 2 -- .../android/feature/order/ui/OrderListViewModel.kt | 10 ++++++++-- .../android/feature/order/ui/OrderResultBottomSheet.kt | 8 +++++--- app/src/main/res/values/strings.xml | 4 ++-- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListScreen.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListScreen.kt index 3b38ff8..f13f0e6 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListScreen.kt @@ -1,6 +1,5 @@ package com.sampoom.android.feature.order.ui -import android.R.attr.order import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -51,7 +50,6 @@ fun OrderListScreen( LaunchedEffect(errorLabel) { viewModel.bindLabel(errorLabel) - viewModel.onEvent(OrderListUiEvent.LoadOrderList) } Column(Modifier.fillMaxSize()) { diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListViewModel.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListViewModel.kt index a9cd392..a2bfa67 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListViewModel.kt @@ -7,10 +7,14 @@ import com.sampoom.android.core.network.serverMessageOrNull import com.sampoom.android.feature.order.domain.usecase.GetOrderUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.Dispatcher import javax.inject.Inject @HiltViewModel @@ -26,6 +30,7 @@ class OrderListViewModel @Inject constructor( val uiState: StateFlow = _uiState private var errorLabel: String = "" + private var loadJob: Job? = null fun bindLabel(error: String) { errorLabel = error @@ -43,11 +48,12 @@ class OrderListViewModel @Inject constructor( } private fun loadOrderList() { - viewModelScope.launch { + if (loadJob?.isActive == true) return + loadJob = viewModelScope.launch { _uiState.update { it.copy(orderLoading = true, orderError = null) } try { - val orderList = getOrderListUseCase() + val orderList = withContext(Dispatchers.IO) { getOrderListUseCase() } _uiState.update { it.copy( orderList = orderList.items, diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderResultBottomSheet.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderResultBottomSheet.kt index b125c62..abd4407 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderResultBottomSheet.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderResultBottomSheet.kt @@ -1,5 +1,6 @@ package com.sampoom.android.feature.order.ui +import android.R.attr.onClick import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -56,7 +57,7 @@ fun OrderResultBottomSheet( var showCancelOrderDialog by remember { mutableStateOf(false) } val context = LocalContext.current - LaunchedEffect(order) { + LaunchedEffect(order.firstOrNull()?.orderId) { if (order.isNotEmpty()) viewModel.setOrderIdFromApi(order.first().orderId) } @@ -107,8 +108,9 @@ fun OrderResultBottomSheet( CommonButton( modifier = Modifier.weight(1f), variant = ButtonVariant.Error, - enabled = order.firstOrNull()?.status != OrderStatus.COMPLETED && - order.firstOrNull()?.status != OrderStatus.CANCELED, + enabled = order.firstOrNull()?.let { + it.status != OrderStatus.COMPLETED && it.status != OrderStatus.CANCELED + } ?: false, onClick = { showCancelOrderDialog = true } ) { Text(stringResource(R.string.order_detail_order_cancel)) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b61421d..1956094 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -96,8 +96,8 @@ 승인대기 - 배송완료 - 주문실패 + 입고완료 + 주문취소 오류가 발생했습니다 From 7b6058e00ae8248568ed2221a692736f72ec5f37 Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Tue, 21 Oct 2025 14:06:11 +0900 Subject: [PATCH 07/10] =?UTF-8?q?[FIX]=20=EB=B2=84=ED=8A=BC=20=EB=B9=84?= =?UTF-8?q?=ED=99=9C=EC=84=B1=ED=99=94=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sampoom/android/feature/order/ui/OrderDetailScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt index 7f7b5a3..ef3352c 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt @@ -107,7 +107,7 @@ fun OrderDetailScreen( Spacer(Modifier.width(16.dp)) CommonButton( modifier = Modifier.weight(1f), - enabled = uiState.orderDetail.firstOrNull()?.status != OrderStatus.PENDING && + enabled = uiState.orderDetail.firstOrNull()?.status != OrderStatus.COMPLETED && uiState.orderDetail.firstOrNull()?.status != OrderStatus.CANCELED, onClick = { showReceiveOrderDialog = true } ) { From 3ece7b981aca47156364a4fdca3f0e7c49cf3d7b Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Wed, 22 Oct 2025 09:11:58 +0900 Subject: [PATCH 08/10] =?UTF-8?q?[FEAT]=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=8A=A4=EC=99=80=EC=9D=B4=ED=94=84=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=EA=B3=A0=EC=B9=A8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/app/navigation/AppNavHost.kt | 33 ++- .../sampoom/android/core/util/FormatDate.kt | 2 +- .../android/feature/cart/ui/CartListScreen.kt | 203 +++++++++-------- .../feature/order/ui/OrderDetailScreen.kt | 159 ++++++++------ .../feature/order/ui/OrderListScreen.kt | 144 ++++++++----- .../feature/order/ui/OrderListViewModel.kt | 1 + .../order/ui/OrderResultBottomSheet.kt | 7 +- .../feature/outbound/ui/OutboundListScreen.kt | 204 ++++++++++-------- .../android/feature/part/ui/PartListScreen.kt | 142 ++++++------ 9 files changed, 518 insertions(+), 377 deletions(-) diff --git a/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt b/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt index d243585..94d0f63 100644 --- a/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt +++ b/app/src/main/java/com/sampoom/android/app/navigation/AppNavHost.kt @@ -1,5 +1,8 @@ package com.sampoom.android.app.navigation +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon @@ -59,6 +62,7 @@ sealed class BottomNavItem( object Orders : BottomNavItem(ROUTE_ORDERS, R.string.nav_order, R.drawable.orders) } +@RequiresApi(Build.VERSION_CODES.O) @Composable fun AppNavHost() { val navController = rememberNavController() @@ -134,6 +138,7 @@ fun AppNavHost() { } } +@RequiresApi(Build.VERSION_CODES.O) @Composable fun MainScreen( parentNavController: NavHostController @@ -146,14 +151,26 @@ fun MainScreen( ) { innerPadding -> NavHost( navController = navController, - startDestination = ROUTE_DASHBOARD, - modifier = Modifier.padding(innerPadding) + startDestination = ROUTE_DASHBOARD ) { - composable(ROUTE_DASHBOARD) { DashboardScreen() } - composable(ROUTE_OUTBOUND) { OutboundListScreen() } - composable(ROUTE_CART) { CartListScreen() } + composable(ROUTE_DASHBOARD) { + DashboardScreen( + paddingValues = innerPadding + ) + } + composable(ROUTE_OUTBOUND) { + OutboundListScreen( + paddingValues = innerPadding + ) + } + composable(ROUTE_CART) { + CartListScreen( + paddingValues = innerPadding + ) + } composable(ROUTE_ORDERS) { OrderListScreen( + paddingValues = innerPadding, onNavigateOrderDetail = { order -> // TODO: 실제 사용자의 agencyId 사용 parentNavController.navigate(routeOrderDetail(1, order.orderId)) @@ -223,7 +240,9 @@ fun BottomNavigationBar(navController: NavHostController) { // 임시 화면들 (실제로는 각각의 feature 모듈에서 구현) @Composable -private fun DashboardScreen() { +private fun DashboardScreen( + paddingValues: PaddingValues +) { // 홈 화면 구현 - Text("대시보드 화면") + Text("대시보드 화면", modifier = Modifier.padding(paddingValues)) } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/core/util/FormatDate.kt b/app/src/main/java/com/sampoom/android/core/util/FormatDate.kt index 272d9e4..425a09b 100644 --- a/app/src/main/java/com/sampoom/android/core/util/FormatDate.kt +++ b/app/src/main/java/com/sampoom/android/core/util/FormatDate.kt @@ -12,7 +12,7 @@ fun formatDate(dateString: String): String { dateString.endsWith("Z") || dateString.contains('+') || dateString.contains('-') .and(dateString.count { it == ':' } >= 3) val date = if (hasOffset) { - val inFmt = java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME + val inFmt = DateTimeFormatter.ISO_OFFSET_DATE_TIME java.time.OffsetDateTime.parse(dateString, inFmt).toLocalDate() } else { val inFmt = java.time.format.DateTimeFormatterBuilder() diff --git a/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListScreen.kt b/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListScreen.kt index 52e1333..bf83548 100644 --- a/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListScreen.kt @@ -3,6 +3,7 @@ package com.sampoom.android.feature.cart.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -19,6 +20,9 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -43,15 +47,18 @@ import com.sampoom.android.core.ui.theme.backgroundCardColor import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.core.ui.theme.textSecondaryColor import com.sampoom.android.feature.cart.domain.model.CartPart +import com.sampoom.android.feature.order.ui.OrderListUiEvent import com.sampoom.android.feature.order.ui.OrderResultBottomSheet import kotlin.collections.forEach @Composable fun CartListScreen( + paddingValues: PaddingValues, viewModel: CartListViewModel = hiltViewModel() ) { val errorLabel = stringResource(R.string.common_error) val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val pullRefreshState = rememberPullToRefreshState() var showEmptyCartDialog by remember { mutableStateOf(false) } var showConfirmDialog by remember { mutableStateOf(false) } @@ -67,113 +74,129 @@ fun CartListScreen( ) } - Column(Modifier.fillMaxSize()) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - modifier = Modifier - .padding(vertical = 16.dp), - text = stringResource(R.string.cart_title), - style = MaterialTheme.typography.titleLarge, - color = textColor() + PullToRefreshBox( + isRefreshing = uiState.cartLoading, + onRefresh = { viewModel.onEvent(CartListUiEvent.LoadCartList) }, + state = pullRefreshState, + modifier = Modifier.fillMaxSize(), + indicator = { + Indicator( + modifier = Modifier.align(Alignment.TopCenter), + isRefreshing = uiState.cartLoading, + containerColor = MaterialTheme.colorScheme.primaryContainer, + color = MaterialTheme.colorScheme.onPrimaryContainer, + state = pullRefreshState ) + } + ) { + Column(Modifier.fillMaxSize().padding(paddingValues)) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier + .padding(vertical = 16.dp), + text = stringResource(R.string.cart_title), + style = MaterialTheme.typography.titleLarge, + color = textColor() + ) - when { - uiState.cartLoading -> {} - uiState.cartError != null -> {} - uiState.cartList.isEmpty() -> {} - else -> { - TextButton( - onClick = { showEmptyCartDialog = true } - ) { - Text( - text = stringResource(R.string.cart_empty_list), - style = MaterialTheme.typography.titleMedium, - color = FailRed - ) + when { + uiState.cartLoading -> {} + uiState.cartError != null -> {} + uiState.cartList.isEmpty() -> {} + else -> { + TextButton( + onClick = { showEmptyCartDialog = true } + ) { + Text( + text = stringResource(R.string.cart_empty_list), + style = MaterialTheme.typography.titleMedium, + color = FailRed + ) + } } } } - } - when { - uiState.cartLoading -> { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } - - uiState.cartError != null -> { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - ErrorContent( - onRetry = { viewModel.onEvent(CartListUiEvent.RetryCartList) }, - modifier = Modifier.height(200.dp) - ) + when { + uiState.cartLoading -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } } - } - uiState.cartList.isEmpty() -> { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - EmptyContent( - message = stringResource(R.string.cart_empty_outbound), - modifier = Modifier.height(200.dp) - ) + uiState.cartError != null -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + ErrorContent( + onRetry = { viewModel.onEvent(CartListUiEvent.RetryCartList) }, + modifier = Modifier.height(200.dp) + ) + } } - } - else -> { - Box(modifier = Modifier.fillMaxSize()) { - LazyColumn( + uiState.cartList.isEmpty() -> { + Box( modifier = Modifier - .fillMaxSize() - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + .fillMaxSize(), + contentAlignment = Alignment.Center ) { - uiState.cartList.forEach { category -> - category.groups.forEach { group -> - item { - CartSection( - categoryName = category.categoryName, - groupName = group.groupName, - parts = group.parts, - isUpdating = uiState.isUpdating, - isDeleting = uiState.isDeleting, - onEvent = { viewModel.onEvent(it) } - ) + EmptyContent( + message = stringResource(R.string.cart_empty_outbound), + modifier = Modifier.height(200.dp) + ) + } + } + + else -> { + Box(modifier = Modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + uiState.cartList.forEach { category -> + category.groups.forEach { group -> + item { + CartSection( + categoryName = category.categoryName, + groupName = group.groupName, + parts = group.parts, + isUpdating = uiState.isUpdating, + isDeleting = uiState.isDeleting, + onEvent = { viewModel.onEvent(it) } + ) + } } } + item { Spacer(Modifier.height(100.dp)) } } - item { Spacer(Modifier.height(100.dp)) } - } - CommonButton( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomEnd) - .padding(16.dp) - .padding(end = 72.dp), - variant = ButtonVariant.Primary, - size = ButtonSize.Large, - onClick = { showConfirmDialog = true } - ) { Text(stringResource(R.string.cart_order_parts)) } + CommonButton( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomEnd) + .padding(16.dp) + .padding(end = 72.dp), + variant = ButtonVariant.Primary, + size = ButtonSize.Large, + onClick = { showConfirmDialog = true } + ) { Text(stringResource(R.string.cart_order_parts)) } + } } } } diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt index ef3352c..d4bbfcc 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailScreen.kt @@ -1,9 +1,11 @@ package com.sampoom.android.feature.order.ui +import android.R.attr.order import android.widget.Toast import androidx.compose.foundation.layout.Box 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 @@ -13,10 +15,14 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -37,6 +43,7 @@ import com.sampoom.android.core.ui.component.CommonButton import com.sampoom.android.core.ui.component.EmptyContent import com.sampoom.android.core.ui.component.ErrorContent import com.sampoom.android.feature.order.domain.model.OrderStatus +import com.sampoom.android.feature.outbound.ui.OutboundListUiEvent @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -45,6 +52,7 @@ fun OrderDetailScreen( viewModel: OrderDetailViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val pullRefreshState = rememberPullToRefreshState() var showCancelOrderDialog by remember { mutableStateOf(false) } var showReceiveOrderDialog by remember { mutableStateOf(false) } val context = LocalContext.current @@ -75,82 +83,101 @@ fun OrderDetailScreen( } } - Scaffold( - topBar = { - TopAppBar( - title = { Text(stringResource(R.string.order_detail_title)) }, - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon( - painter = painterResource(R.drawable.ic_arrow_back), - contentDescription = stringResource(R.string.nav_back) - ) - } - } + PullToRefreshBox( + isRefreshing = uiState.orderDetailLoading, + onRefresh = { viewModel.onEvent(OrderDetailUiEvent.LoadOrder) }, + state = pullRefreshState, + modifier = Modifier.fillMaxSize(), + indicator = { + Indicator( + modifier = Modifier.align(Alignment.TopCenter), + isRefreshing = uiState.orderDetailLoading, + containerColor = MaterialTheme.colorScheme.primaryContainer, + color = MaterialTheme.colorScheme.onPrimaryContainer, + state = pullRefreshState ) - }, - bottomBar = { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - CommonButton( - modifier = Modifier.weight(1f), - variant = ButtonVariant.Error, - enabled = uiState.orderDetail.firstOrNull()?.status != OrderStatus.COMPLETED && - uiState.orderDetail.firstOrNull()?.status != OrderStatus.CANCELED, - onClick = { showCancelOrderDialog = true } - ) { - Text(stringResource(R.string.order_detail_order_cancel)) - } - Spacer(Modifier.width(16.dp)) - CommonButton( - modifier = Modifier.weight(1f), - enabled = uiState.orderDetail.firstOrNull()?.status != OrderStatus.COMPLETED && - uiState.orderDetail.firstOrNull()?.status != OrderStatus.CANCELED, - onClick = { showReceiveOrderDialog = true } - ) { - Text(stringResource(R.string.order_detail_order_receive)) - } - } } - ) { innerPadding -> - when { - uiState.orderDetailLoading -> { - Box( + ) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.order_detail_title)) }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + painter = painterResource(R.drawable.ic_arrow_back), + contentDescription = stringResource(R.string.nav_back) + ) + } + } + ) + }, + bottomBar = { + val orderStatus = uiState.orderDetail.firstOrNull()?.status + Row( modifier = Modifier .fillMaxWidth() - .height(200.dp), - contentAlignment = Alignment.Center + .padding(16.dp) ) { - CircularProgressIndicator() + CommonButton( + modifier = Modifier.weight(1f), + variant = ButtonVariant.Error, + enabled = orderStatus != null && + !uiState.isProcessing && + orderStatus == OrderStatus.PENDING, + onClick = { showCancelOrderDialog = true } + ) { + Text(stringResource(R.string.order_detail_order_cancel)) + } + Spacer(Modifier.width(16.dp)) + CommonButton( + modifier = Modifier.weight(1f), + enabled = orderStatus != null && + !uiState.isProcessing && + orderStatus == OrderStatus.PENDING, + onClick = { showReceiveOrderDialog = true } + ) { + Text(stringResource(R.string.order_detail_order_receive)) + } } } + ) { innerPadding -> + when { + uiState.orderDetailLoading -> { + Box( + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } - uiState.orderDetailError != null -> { - ErrorContent( - onRetry = { viewModel.onEvent(OrderDetailUiEvent.RetryOrder) }, - modifier = Modifier - .height(200.dp) - .fillMaxWidth() - ) - } + uiState.orderDetailError != null -> { + ErrorContent( + onRetry = { viewModel.onEvent(OrderDetailUiEvent.RetryOrder) }, + modifier = Modifier + .height(200.dp) + .fillMaxWidth() + ) + } - uiState.orderDetail.isEmpty() -> { - EmptyContent( - message = stringResource(R.string.order_empty_list), - modifier = Modifier - .height(200.dp) - .fillMaxWidth() - ) - } + uiState.orderDetail.isEmpty() -> { + EmptyContent( + message = stringResource(R.string.order_empty_list), + modifier = Modifier + .height(200.dp) + .fillMaxWidth() + ) + } - else -> { - OrderDetailContent( - order = uiState.orderDetail, - modifier = Modifier.padding(innerPadding) - ) + else -> { + OrderDetailContent( + order = uiState.orderDetail, + modifier = Modifier.padding(innerPadding) + ) + } } } } diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListScreen.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListScreen.kt index f13f0e6..6125a00 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListScreen.kt @@ -1,8 +1,11 @@ package com.sampoom.android.feature.order.ui +import android.os.Build +import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -13,11 +16,15 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -39,94 +46,115 @@ import com.sampoom.android.core.util.buildOrderTitle import com.sampoom.android.core.util.formatDate import com.sampoom.android.feature.order.domain.model.Order +@RequiresApi(Build.VERSION_CODES.O) +@OptIn(ExperimentalMaterialApi::class) @Composable fun OrderListScreen( + paddingValues: PaddingValues, onNavigateOrderDetail: (Order) -> Unit, viewModel: OrderListViewModel = hiltViewModel() ) { val errorLabel = stringResource(R.string.common_error) val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val pullRefreshState = rememberPullToRefreshState() val listState = rememberSaveable(saver = LazyListState.Saver) { LazyListState() } LaunchedEffect(errorLabel) { viewModel.bindLabel(errorLabel) } - Column(Modifier.fillMaxSize()) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - modifier = Modifier - .padding(vertical = 16.dp), - text = stringResource(R.string.order_title), - style = MaterialTheme.typography.titleLarge, - color = textColor() + PullToRefreshBox( + isRefreshing = uiState.orderLoading, + onRefresh = { viewModel.onEvent(OrderListUiEvent.LoadOrderList) }, + state = pullRefreshState, + modifier = Modifier.fillMaxSize(), + indicator = { + Indicator( + modifier = Modifier.align(Alignment.TopCenter), + isRefreshing = uiState.orderLoading, + containerColor = MaterialTheme.colorScheme.primaryContainer, + color = MaterialTheme.colorScheme.onPrimaryContainer, + state = pullRefreshState ) } - - when { - uiState.orderLoading -> { - Box( + ) { + Column(Modifier.fillMaxSize().padding(paddingValues)) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } + .padding(vertical = 16.dp), + text = stringResource(R.string.order_title), + style = MaterialTheme.typography.titleLarge, + color = textColor() + ) } - uiState.orderError != null -> { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - ErrorContent( - onRetry = { viewModel.onEvent(OrderListUiEvent.RetryOrderList) }, - modifier = Modifier.height(200.dp) - ) + when { + uiState.orderLoading -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } } - } - uiState.orderList.isEmpty() -> { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - EmptyContent( - message = stringResource(R.string.order_empty_list), - modifier = Modifier.height(200.dp) - ) + uiState.orderError != null -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + ErrorContent( + onRetry = { viewModel.onEvent(OrderListUiEvent.RetryOrderList) }, + modifier = Modifier.height(200.dp) + ) + } } - } - else -> { - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - items(uiState.orderList) { order -> - OrderItem( - order = order, - onClick = { onNavigateOrderDetail(order) } + uiState.orderList.isEmpty() -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + EmptyContent( + message = stringResource(R.string.order_empty_list), + modifier = Modifier.height(200.dp) ) } - item { Spacer(Modifier.height(100.dp)) } + } + + else -> { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(uiState.orderList) { order -> + OrderItem( + order = order, + onClick = { onNavigateOrderDetail(order) } + ) + } + item { Spacer(Modifier.height(100.dp)) } + } } } } } } +@RequiresApi(Build.VERSION_CODES.O) @Composable private fun OrderItem( order: Order, diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListViewModel.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListViewModel.kt index a2bfa67..4060fc4 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListViewModel.kt @@ -9,6 +9,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderResultBottomSheet.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderResultBottomSheet.kt index abd4407..e859080 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderResultBottomSheet.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderResultBottomSheet.kt @@ -105,12 +105,13 @@ fun OrderResultBottomSheet( .fillMaxWidth() .padding(16.dp) ) { + val orderStatus = order.firstOrNull()?.status CommonButton( modifier = Modifier.weight(1f), variant = ButtonVariant.Error, - enabled = order.firstOrNull()?.let { - it.status != OrderStatus.COMPLETED && it.status != OrderStatus.CANCELED - } ?: false, + enabled = orderStatus != null && + !uiState.isProcessing && + orderStatus == OrderStatus.PENDING, onClick = { showCancelOrderDialog = true } ) { Text(stringResource(R.string.order_detail_order_cancel)) diff --git a/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListScreen.kt b/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListScreen.kt index a2501d2..21c1664 100644 --- a/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListScreen.kt @@ -4,6 +4,7 @@ import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -21,6 +22,9 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -45,15 +49,18 @@ import com.sampoom.android.core.ui.theme.FailRed import com.sampoom.android.core.ui.theme.backgroundCardColor import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.core.ui.theme.textSecondaryColor +import com.sampoom.android.feature.order.ui.OrderListUiEvent import com.sampoom.android.feature.outbound.domain.model.OutboundPart @OptIn(ExperimentalMaterial3Api::class) @Composable fun OutboundListScreen( + paddingValues: PaddingValues, viewModel: OutboundListViewModel = hiltViewModel() ) { val errorLabel = stringResource(R.string.common_error) val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val pullRefreshState = rememberPullToRefreshState() var showEmptyOutboundDialog by remember { mutableStateOf(false) } var showConfirmDialog by remember { mutableStateOf(false) } val context = LocalContext.current @@ -73,113 +80,128 @@ fun OutboundListScreen( viewModel.clearSuccess() } } - - Column(Modifier.fillMaxSize()) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - modifier = Modifier - .padding(vertical = 16.dp), - text = stringResource(R.string.outbound_title), - style = MaterialTheme.typography.titleLarge, - color = textColor() + PullToRefreshBox( + isRefreshing = uiState.outboundLoading, + onRefresh = { viewModel.onEvent(OutboundListUiEvent.LoadOutboundList) }, + state = pullRefreshState, + modifier = Modifier.fillMaxSize(), + indicator = { + Indicator( + modifier = Modifier.align(Alignment.TopCenter), + isRefreshing = uiState.outboundLoading, + containerColor = MaterialTheme.colorScheme.primaryContainer, + color = MaterialTheme.colorScheme.onPrimaryContainer, + state = pullRefreshState ) - - when { - uiState.outboundLoading -> {} - uiState.outboundError != null -> {} - uiState.outboundList.isEmpty() -> {} - else -> { - TextButton( - onClick = { showEmptyOutboundDialog = true } - ) { - Text( - text = stringResource(R.string.outbound_empty_list), - style = MaterialTheme.typography.titleMedium, - color = FailRed - ) - } - } - } } - - when { - uiState.outboundLoading -> { - Box( + ) { + Column(Modifier.fillMaxSize().padding(paddingValues)) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() + .padding(vertical = 16.dp), + text = stringResource(R.string.outbound_title), + style = MaterialTheme.typography.titleLarge, + color = textColor() + ) + + when { + uiState.outboundLoading -> {} + uiState.outboundError != null -> {} + uiState.outboundList.isEmpty() -> {} + else -> { + TextButton( + onClick = { showEmptyOutboundDialog = true } + ) { + Text( + text = stringResource(R.string.outbound_empty_list), + style = MaterialTheme.typography.titleMedium, + color = FailRed + ) + } + } } } - uiState.outboundError != null -> { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - ErrorContent( - onRetry = { viewModel.onEvent(OutboundListUiEvent.RetryOutboundList) }, - modifier = Modifier.height(200.dp) - ) + when { + uiState.outboundLoading -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } } - } - uiState.outboundList.isEmpty() -> { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - EmptyContent( - message = stringResource(R.string.outbound_empty_outbound), - modifier = Modifier.height(200.dp) - ) + uiState.outboundError != null -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + ErrorContent( + onRetry = { viewModel.onEvent(OutboundListUiEvent.RetryOutboundList) }, + modifier = Modifier.height(200.dp) + ) + } } - } - else -> { - Box(modifier = Modifier.fillMaxSize()) { - LazyColumn( + uiState.outboundList.isEmpty() -> { + Box( modifier = Modifier - .fillMaxSize() - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + .fillMaxSize(), + contentAlignment = Alignment.Center ) { - uiState.outboundList.forEach { category -> - category.groups.forEach { group -> - item { - OutboundSection( - categoryName = category.categoryName, - groupName = group.groupName, - parts = group.parts, - isUpdating = uiState.isUpdating, - isDeleting = uiState.isDeleting, - onEvent = { viewModel.onEvent(it) } - ) + EmptyContent( + message = stringResource(R.string.outbound_empty_outbound), + modifier = Modifier.height(200.dp) + ) + } + } + + else -> { + Box(modifier = Modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + uiState.outboundList.forEach { category -> + category.groups.forEach { group -> + item { + OutboundSection( + categoryName = category.categoryName, + groupName = group.groupName, + parts = group.parts, + isUpdating = uiState.isUpdating, + isDeleting = uiState.isDeleting, + onEvent = { viewModel.onEvent(it) } + ) + } } } + item { Spacer(Modifier.height(100.dp)) } } - item { Spacer(Modifier.height(100.dp)) } - } - CommonButton( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomEnd) - .padding(16.dp) - .padding(end = 72.dp), - variant = ButtonVariant.Error, - size = ButtonSize.Large, - onClick = { showConfirmDialog = true } - ) { Text(stringResource(R.string.outbound_order_parts)) } + CommonButton( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomEnd) + .padding(16.dp) + .padding(end = 72.dp), + variant = ButtonVariant.Error, + size = ButtonSize.Large, + onClick = { showConfirmDialog = true } + ) { Text(stringResource(R.string.outbound_order_parts)) } + } } } } diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt index 8aa2fae..161c8d9 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt @@ -22,6 +22,9 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -44,6 +47,7 @@ import com.sampoom.android.core.ui.component.ErrorContent import com.sampoom.android.core.ui.theme.backgroundCardColor import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.core.ui.theme.textSecondaryColor +import com.sampoom.android.feature.outbound.ui.OutboundListUiEvent import com.sampoom.android.feature.part.domain.model.Part @OptIn(ExperimentalMaterial3Api::class) @@ -59,82 +63,98 @@ fun PartListScreen( } val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val pullRefreshState = rememberPullToRefreshState() // ModalBottomSheet 상태 관리 val sheetState = rememberModalBottomSheetState() - val scope = rememberCoroutineScope() var showBottomSheet by remember { mutableStateOf(false) } - Scaffold( - topBar = { - TopAppBar( - title = { Text(stringResource(R.string.part_title)) }, - navigationIcon = { - IconButton(onClick = onNavigateBack) { - Icon( - painter = painterResource(R.drawable.ic_arrow_back), - contentDescription = stringResource(R.string.nav_back) - ) - } - } + PullToRefreshBox( + isRefreshing = uiState.partListLoading, + onRefresh = { viewModel.onEvent(PartListUiEvent.LoadPartList) }, + state = pullRefreshState, + modifier = Modifier.fillMaxSize(), + indicator = { + Indicator( + modifier = Modifier.align(Alignment.TopCenter), + isRefreshing = uiState.partListLoading, + containerColor = MaterialTheme.colorScheme.primaryContainer, + color = MaterialTheme.colorScheme.onPrimaryContainer, + state = pullRefreshState ) } - ) { innerPadding -> - when { - uiState.partListLoading -> { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } + ) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.part_title)) }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + painter = painterResource(R.drawable.ic_arrow_back), + contentDescription = stringResource(R.string.nav_back) + ) + } + } + ) } - - uiState.partListError != null -> { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - ErrorContent( - onRetry = { viewModel.onEvent(PartListUiEvent.RetryPartList) }, - modifier = Modifier.height(200.dp) - ) + ) { innerPadding -> + when { + uiState.partListLoading -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } } - } - uiState.partList.isEmpty() -> { - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ) { - EmptyContent( - message = stringResource(R.string.part_empty_part), - modifier = Modifier.height(200.dp) - ) + uiState.partListError != null -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + ErrorContent( + onRetry = { viewModel.onEvent(PartListUiEvent.RetryPartList) }, + modifier = Modifier.height(200.dp) + ) + } } - } - else -> { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - items(uiState.partList) { part -> - PartListItemCard( - part = part, - onClick = { - viewModel.onEvent(PartListUiEvent.ShowBottomSheet(part)) - showBottomSheet = true - } + uiState.partList.isEmpty() -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + EmptyContent( + message = stringResource(R.string.part_empty_part), + modifier = Modifier.height(200.dp) ) } } + + else -> { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(uiState.partList) { part -> + PartListItemCard( + part = part, + onClick = { + viewModel.onEvent(PartListUiEvent.ShowBottomSheet(part)) + showBottomSheet = true + } + ) + } + } + } } } } From 81b543f0210cfe0199803b27b37fc1b0e3455ba6 Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Wed, 22 Oct 2025 09:43:55 +0900 Subject: [PATCH 09/10] =?UTF-8?q?[FIX]=20=EC=BD=94=EB=93=9C=20=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EB=A6=AC=EB=B7=B0=20=EC=82=AC=ED=95=AD=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/sampoom/android/core/util/FormatDate.kt | 5 +++-- .../sampoom/android/feature/order/ui/OrderListViewModel.kt | 2 -- .../com/sampoom/android/feature/part/ui/PartListScreen.kt | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/sampoom/android/core/util/FormatDate.kt b/app/src/main/java/com/sampoom/android/core/util/FormatDate.kt index 425a09b..5835cde 100644 --- a/app/src/main/java/com/sampoom/android/core/util/FormatDate.kt +++ b/app/src/main/java/com/sampoom/android/core/util/FormatDate.kt @@ -9,8 +9,9 @@ fun formatDate(dateString: String): String { return runCatching { val out = DateTimeFormatter.ISO_LOCAL_DATE val hasOffset = - dateString.endsWith("Z") || dateString.contains('+') || dateString.contains('-') - .and(dateString.count { it == ':' } >= 3) + dateString.contains("Z") || dateString.lastIndexOf('+') > dateString.indexOf('T') || (dateString.lastIndexOf( + '-' + ) > dateString.indexOf('T') && dateString.count { it == ':' } >= 3) val date = if (hasOffset) { val inFmt = DateTimeFormatter.ISO_OFFSET_DATE_TIME java.time.OffsetDateTime.parse(dateString, inFmt).toLocalDate() diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListViewModel.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListViewModel.kt index 4060fc4..0a4fab1 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderListViewModel.kt @@ -9,13 +9,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import okhttp3.Dispatcher import javax.inject.Inject @HiltViewModel diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt index 161c8d9..b32c2cb 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt @@ -103,7 +103,7 @@ fun PartListScreen( uiState.partListLoading -> { Box( modifier = Modifier - .fillMaxSize(), + .fillMaxSize().padding(innerPadding), contentAlignment = Alignment.Center ) { CircularProgressIndicator() @@ -113,7 +113,7 @@ fun PartListScreen( uiState.partListError != null -> { Box( modifier = Modifier - .fillMaxSize(), + .fillMaxSize().padding(innerPadding), contentAlignment = Alignment.Center ) { ErrorContent( @@ -126,7 +126,7 @@ fun PartListScreen( uiState.partList.isEmpty() -> { Box( modifier = Modifier - .fillMaxSize(), + .fillMaxSize().padding(innerPadding), contentAlignment = Alignment.Center ) { EmptyContent( From 39c473661f73f93a83212bfb137eee64599e6c92 Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Wed, 22 Oct 2025 09:50:17 +0900 Subject: [PATCH 10/10] =?UTF-8?q?[FIX]=20=EC=BD=94=EB=93=9C=20=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EB=A6=AC=EB=B7=B0=20=EC=82=AC=ED=95=AD=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/sampoom/android/core/util/FormatDate.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/sampoom/android/core/util/FormatDate.kt b/app/src/main/java/com/sampoom/android/core/util/FormatDate.kt index 5835cde..7684ba3 100644 --- a/app/src/main/java/com/sampoom/android/core/util/FormatDate.kt +++ b/app/src/main/java/com/sampoom/android/core/util/FormatDate.kt @@ -9,9 +9,9 @@ fun formatDate(dateString: String): String { return runCatching { val out = DateTimeFormatter.ISO_LOCAL_DATE val hasOffset = - dateString.contains("Z") || dateString.lastIndexOf('+') > dateString.indexOf('T') || (dateString.lastIndexOf( - '-' - ) > dateString.indexOf('T') && dateString.count { it == ':' } >= 3) + dateString.contains("Z") || (dateString.contains('T') && (dateString.lastIndexOf('+') > dateString.indexOf( + 'T' + ) || (dateString.lastIndexOf('-') > dateString.indexOf('T') && dateString.count { it == ':' } >= 3))) val date = if (hasOffset) { val inFmt = DateTimeFormatter.ISO_OFFSET_DATE_TIME java.time.OffsetDateTime.parse(dateString, inFmt).toLocalDate()