From 868eb249ea897668d1724e2e187baed75cbf5a50 Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Thu, 16 Oct 2025 17:07:11 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[FEAT]=20=EB=B6=80=ED=92=88=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=A1=B0=ED=9A=8C,=20=EB=B6=80?= =?UTF-8?q?=ED=92=88=20=EA=B7=B8=EB=A3=B9=20=EC=A1=B0=ED=9A=8C=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/app/navigation/AppNavHost.kt | 10 +- .../sampoom/android/core/ui/theme/Color.kt | 216 +++++++++++ .../sampoom/android/core/ui/theme/Theme.kt | 81 ++++- .../feature/auth/data/remote/api/AuthApi.kt | 4 +- .../android/feature/auth/ui/LoginScreen.kt | 30 +- .../android/feature/auth/ui/LoginViewModel.kt | 30 +- .../android/feature/auth/ui/SignUpScreen.kt | 2 + .../feature/part/data/local/database/.gitkeep | 0 .../part/data/local/preferences/.gitkeep | 0 .../feature/part/data/mapper/PartMappers.kt | 9 +- .../feature/part/data/remote/api/PartApi.kt | 11 +- .../part/data/remote/dto/CategoryDto.kt | 7 + .../feature/part/data/remote/dto/GroupDto.kt | 8 + .../feature/part/data/remote/dto/PartDto.kt | 7 - .../data/repository/PartRepositoryImpl.kt | 17 +- .../feature/part/domain/model/Category.kt | 7 + .../feature/part/domain/model/CategoryList.kt | 11 + .../feature/part/domain/model/Group.kt | 8 + .../model/{PartList.kt => GroupList.kt} | 6 +- .../android/feature/part/domain/model/Part.kt | 7 - .../part/domain/repository/PartRepository.kt | 6 +- ...etPartUseCase.kt => GetCategoryUseCase.kt} | 6 +- .../part/domain/usecase/GetGroupUseCase.kt | 11 + .../android/feature/part/ui/PartScreen.kt | 342 +++++++++++++++--- .../android/feature/part/ui/PartUiEvent.kt | 6 + .../android/feature/part/ui/PartUiState.kt | 19 +- .../android/feature/part/ui/PartViewModel.kt | 114 ++++-- app/src/main/res/drawable/body.xml | 19 + app/src/main/res/drawable/cart.xml | 2 +- app/src/main/res/drawable/chassis.xml | 22 ++ app/src/main/res/drawable/chevron_right.xml | 5 + app/src/main/res/drawable/dashboard.xml | 2 +- app/src/main/res/drawable/delivery.xml | 2 +- app/src/main/res/drawable/electric.xml | 13 + app/src/main/res/drawable/engine.xml | 13 + app/src/main/res/drawable/orders.xml | 2 +- app/src/main/res/drawable/parts.xml | 2 +- app/src/main/res/drawable/search.xml | 2 +- app/src/main/res/drawable/transmission.xml | 9 + app/src/main/res/drawable/trim.xml | 13 + app/src/main/res/values/strings.xml | 12 +- 41 files changed, 925 insertions(+), 168 deletions(-) delete mode 100644 app/src/main/java/com/sampoom/android/feature/part/data/local/database/.gitkeep delete mode 100644 app/src/main/java/com/sampoom/android/feature/part/data/local/preferences/.gitkeep create mode 100644 app/src/main/java/com/sampoom/android/feature/part/data/remote/dto/CategoryDto.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/part/data/remote/dto/GroupDto.kt delete mode 100644 app/src/main/java/com/sampoom/android/feature/part/data/remote/dto/PartDto.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/part/domain/model/Category.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/part/domain/model/CategoryList.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/part/domain/model/Group.kt rename app/src/main/java/com/sampoom/android/feature/part/domain/model/{PartList.kt => GroupList.kt} (65%) delete mode 100644 app/src/main/java/com/sampoom/android/feature/part/domain/model/Part.kt rename app/src/main/java/com/sampoom/android/feature/part/domain/usecase/{GetPartUseCase.kt => GetCategoryUseCase.kt} (51%) create mode 100644 app/src/main/java/com/sampoom/android/feature/part/domain/usecase/GetGroupUseCase.kt create mode 100644 app/src/main/res/drawable/body.xml create mode 100644 app/src/main/res/drawable/chassis.xml create mode 100644 app/src/main/res/drawable/chevron_right.xml create mode 100644 app/src/main/res/drawable/electric.xml create mode 100644 app/src/main/res/drawable/engine.xml create mode 100644 app/src/main/res/drawable/transmission.xml create mode 100644 app/src/main/res/drawable/trim.xml 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 d167d4b..2c0948d 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,6 @@ package com.sampoom.android.app.navigation +import androidx.compose.foundation.background import androidx.compose.foundation.layout.padding import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon @@ -18,6 +19,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.sampoom.android.R +import com.sampoom.android.core.ui.theme.backgroundColor import com.sampoom.android.feature.auth.ui.LoginScreen import com.sampoom.android.feature.auth.ui.SignUpScreen import com.sampoom.android.feature.part.ui.PartScreen @@ -53,7 +55,7 @@ fun AppNavHost() { val navController = rememberNavController() // TODO: 임시 로그인 상태 확인 -> AuthRepository에서 확인하도록 변경 - val isLoggedIn = false + val isLoggedIn = true NavHost( navController = navController, @@ -82,7 +84,11 @@ fun AppNavHost() { ) } composable(ROUTE_HOME) { MainScreen(navController) } - composable(ROUTE_PARTS) { PartScreen() } + composable(ROUTE_PARTS) { PartScreen( + onNavigateBack = { + navController.navigateUp() + } + ) } } } diff --git a/app/src/main/java/com/sampoom/android/core/ui/theme/Color.kt b/app/src/main/java/com/sampoom/android/core/ui/theme/Color.kt index 72cdc9c..fe07c9a 100644 --- a/app/src/main/java/com/sampoom/android/core/ui/theme/Color.kt +++ b/app/src/main/java/com/sampoom/android/core/ui/theme/Color.kt @@ -4,6 +4,222 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +val primaryLight = Color(0xFF4C4AC8) +val onPrimaryLight = Color(0xFFFFFFFF) +val primaryContainerLight = Color(0xFF6565E2) +val onPrimaryContainerLight = Color(0xFFFFFBFF) +val secondaryLight = Color(0xFF5B5D72) +val onSecondaryLight = Color(0xFFFFFFFF) +val secondaryContainerLight = Color(0xFFE6E6FF) +val onSecondaryContainerLight = Color(0xFF65667B) +val tertiaryLight = Color(0xFF5D5F5F) +val onTertiaryLight = Color(0xFFFFFFFF) +val tertiaryContainerLight = Color(0xFFF5F5F5) +val onTertiaryContainerLight = Color(0xFF6F7070) +val errorLight = Color(0xFFAD3035) +val onErrorLight = Color(0xFFFFFFFF) +val errorContainerLight = Color(0xFFFF6C6C) +val onErrorContainerLight = Color(0xFF6E0011) +val backgroundLight = Color(0xFFFCF8FF) +val onBackgroundLight = Color(0xFF1B1B23) +val surfaceLight = Color(0xFFFCF8F8) +val onSurfaceLight = Color(0xFF1C1B1B) +val surfaceVariantLight = Color(0xFFE0E3E3) +val onSurfaceVariantLight = Color(0xFF444748) +val outlineLight = Color(0xFF747878) +val outlineVariantLight = Color(0xFFC4C7C8) +val scrimLight = Color(0xFF000000) +val inverseSurfaceLight = Color(0xFF313030) +val inverseOnSurfaceLight = Color(0xFFF4F0EF) +val inversePrimaryLight = Color(0xFFC2C1FF) +val surfaceDimLight = Color(0xFFDDD9D9) +val surfaceBrightLight = Color(0xFFFCF8F8) +val surfaceContainerLowestLight = Color(0xFFFFFFFF) +val surfaceContainerLowLight = Color(0xFFF6F3F2) +val surfaceContainerLight = Color(0xFFF1EDEC) +val surfaceContainerHighLight = Color(0xFFEBE7E7) +val surfaceContainerHighestLight = Color(0xFFE5E2E1) + +val primaryLightMediumContrast = Color(0xFF231AA2) +val onPrimaryLightMediumContrast = Color(0xFFFFFFFF) +val primaryContainerLightMediumContrast = Color(0xFF5E5DDA) +val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val secondaryLightMediumContrast = Color(0xFF333548) +val onSecondaryLightMediumContrast = Color(0xFFFFFFFF) +val secondaryContainerLightMediumContrast = Color(0xFF6A6B81) +val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val tertiaryLightMediumContrast = Color(0xFF353637) +val onTertiaryLightMediumContrast = Color(0xFFFFFFFF) +val tertiaryContainerLightMediumContrast = Color(0xFF6C6D6D) +val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val errorLightMediumContrast = Color(0xFF730012) +val onErrorLightMediumContrast = Color(0xFFFFFFFF) +val errorContainerLightMediumContrast = Color(0xFFC13F42) +val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF) +val backgroundLightMediumContrast = Color(0xFFFCF8FF) +val onBackgroundLightMediumContrast = Color(0xFF1B1B23) +val surfaceLightMediumContrast = Color(0xFFFCF8F8) +val onSurfaceLightMediumContrast = Color(0xFF111111) +val surfaceVariantLightMediumContrast = Color(0xFFE0E3E3) +val onSurfaceVariantLightMediumContrast = Color(0xFF333738) +val outlineLightMediumContrast = Color(0xFF4F5354) +val outlineVariantLightMediumContrast = Color(0xFF6A6E6E) +val scrimLightMediumContrast = Color(0xFF000000) +val inverseSurfaceLightMediumContrast = Color(0xFF313030) +val inverseOnSurfaceLightMediumContrast = Color(0xFFF4F0EF) +val inversePrimaryLightMediumContrast = Color(0xFFC2C1FF) +val surfaceDimLightMediumContrast = Color(0xFFC9C6C5) +val surfaceBrightLightMediumContrast = Color(0xFFFCF8F8) +val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF) +val surfaceContainerLowLightMediumContrast = Color(0xFFF6F3F2) +val surfaceContainerLightMediumContrast = Color(0xFFEBE7E7) +val surfaceContainerHighLightMediumContrast = Color(0xFFDFDCDB) +val surfaceContainerHighestLightMediumContrast = Color(0xFFD4D1D0) + +val primaryLightHighContrast = Color(0xFF160299) +val onPrimaryLightHighContrast = Color(0xFFFFFFFF) +val primaryContainerLightHighContrast = Color(0xFF3835B4) +val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF) +val secondaryLightHighContrast = Color(0xFF292B3D) +val onSecondaryLightHighContrast = Color(0xFFFFFFFF) +val secondaryContainerLightHighContrast = Color(0xFF46485C) +val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF) +val tertiaryLightHighContrast = Color(0xFF2A2C2D) +val onTertiaryLightHighContrast = Color(0xFFFFFFFF) +val tertiaryContainerLightHighContrast = Color(0xFF48494A) +val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF) +val errorLightHighContrast = Color(0xFF60000D) +val onErrorLightHighContrast = Color(0xFFFFFFFF) +val errorContainerLightHighContrast = Color(0xFF8F1922) +val onErrorContainerLightHighContrast = Color(0xFFFFFFFF) +val backgroundLightHighContrast = Color(0xFFFCF8FF) +val onBackgroundLightHighContrast = Color(0xFF1B1B23) +val surfaceLightHighContrast = Color(0xFFFCF8F8) +val onSurfaceLightHighContrast = Color(0xFF000000) +val surfaceVariantLightHighContrast = Color(0xFFE0E3E3) +val onSurfaceVariantLightHighContrast = Color(0xFF000000) +val outlineLightHighContrast = Color(0xFF292D2D) +val outlineVariantLightHighContrast = Color(0xFF464A4A) +val scrimLightHighContrast = Color(0xFF000000) +val inverseSurfaceLightHighContrast = Color(0xFF313030) +val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF) +val inversePrimaryLightHighContrast = Color(0xFFC2C1FF) +val surfaceDimLightHighContrast = Color(0xFFBBB8B7) +val surfaceBrightLightHighContrast = Color(0xFFFCF8F8) +val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF) +val surfaceContainerLowLightHighContrast = Color(0xFFF4F0EF) +val surfaceContainerLightHighContrast = Color(0xFFE5E2E1) +val surfaceContainerHighLightHighContrast = Color(0xFFD7D4D3) +val surfaceContainerHighestLightHighContrast = Color(0xFFC9C6C5) + +val primaryDark = Color(0xFFC2C1FF) +val onPrimaryDark = Color(0xFF1B0E9D) +val primaryContainerDark = Color(0xFF8283FF) +val onPrimaryContainerDark = Color(0xFF12008E) +val secondaryDark = Color(0xFFFFFFFF) +val onSecondaryDark = Color(0xFF2D2F42) +val secondaryContainerDark = Color(0xFFE0E0F9) +val onSecondaryContainerDark = Color(0xFF616378) +val tertiaryDark = Color(0xFFFFFFFF) +val onTertiaryDark = Color(0xFF2F3131) +val tertiaryContainerDark = Color(0xFFE2E2E2) +val onTertiaryContainerDark = Color(0xFF636565) +val errorDark = Color(0xFFFFB3B0) +val onErrorDark = Color(0xFF68000F) +val errorContainerDark = Color(0xFFFF6C6C) +val onErrorContainerDark = Color(0xFF6E0011) +val backgroundDark = Color(0xFF13131A) +val onBackgroundDark = Color(0xFFE4E1EC) +val surfaceDark = Color(0xFF141313) +val onSurfaceDark = Color(0xFFE5E2E1) +val surfaceVariantDark = Color(0xFF444748) +val onSurfaceVariantDark = Color(0xFFC4C7C8) +val outlineDark = Color(0xFF8E9192) +val outlineVariantDark = Color(0xFF444748) +val scrimDark = Color(0xFF000000) +val inverseSurfaceDark = Color(0xFFE5E2E1) +val inverseOnSurfaceDark = Color(0xFF313030) +val inversePrimaryDark = Color(0xFF4F4DCA) +val surfaceDimDark = Color(0xFF141313) +val surfaceBrightDark = Color(0xFF3A3939) +val surfaceContainerLowestDark = Color(0xFF0E0E0E) +val surfaceContainerLowDark = Color(0xFF1C1B1B) +val surfaceContainerDark = Color(0xFF201F1F) +val surfaceContainerHighDark = Color(0xFF2A2A2A) +val surfaceContainerHighestDark = Color(0xFF353434) + +val primaryDarkMediumContrast = Color(0xFFDBD9FF) +val onPrimaryDarkMediumContrast = Color(0xFF110088) +val primaryContainerDarkMediumContrast = Color(0xFF8283FF) +val onPrimaryContainerDarkMediumContrast = Color(0xFF000000) +val secondaryDarkMediumContrast = Color(0xFFFFFFFF) +val onSecondaryDarkMediumContrast = Color(0xFF2D2F42) +val secondaryContainerDarkMediumContrast = Color(0xFFE0E0F9) +val onSecondaryContainerDarkMediumContrast = Color(0xFF45475A) +val tertiaryDarkMediumContrast = Color(0xFFFFFFFF) +val onTertiaryDarkMediumContrast = Color(0xFF2F3131) +val tertiaryContainerDarkMediumContrast = Color(0xFFE2E2E2) +val onTertiaryContainerDarkMediumContrast = Color(0xFF464848) +val errorDarkMediumContrast = Color(0xFFFFD2CF) +val onErrorDarkMediumContrast = Color(0xFF54000A) +val errorContainerDarkMediumContrast = Color(0xFFFF6C6C) +val onErrorContainerDarkMediumContrast = Color(0xFF250002) +val backgroundDarkMediumContrast = Color(0xFF13131A) +val onBackgroundDarkMediumContrast = Color(0xFFE4E1EC) +val surfaceDarkMediumContrast = Color(0xFF141313) +val onSurfaceDarkMediumContrast = Color(0xFFFFFFFF) +val surfaceVariantDarkMediumContrast = Color(0xFF444748) +val onSurfaceVariantDarkMediumContrast = Color(0xFFDADDDD) +val outlineDarkMediumContrast = Color(0xFFAFB2B3) +val outlineVariantDarkMediumContrast = Color(0xFF8D9191) +val scrimDarkMediumContrast = Color(0xFF000000) +val inverseSurfaceDarkMediumContrast = Color(0xFFE5E2E1) +val inverseOnSurfaceDarkMediumContrast = Color(0xFF2A2A2A) +val inversePrimaryDarkMediumContrast = Color(0xFF3733B3) +val surfaceDimDarkMediumContrast = Color(0xFF141313) +val surfaceBrightDarkMediumContrast = Color(0xFF454444) +val surfaceContainerLowestDarkMediumContrast = Color(0xFF070707) +val surfaceContainerLowDarkMediumContrast = Color(0xFF1E1D1D) +val surfaceContainerDarkMediumContrast = Color(0xFF282828) +val surfaceContainerHighDarkMediumContrast = Color(0xFF333232) +val surfaceContainerHighestDarkMediumContrast = Color(0xFF3E3D3D) + +val primaryDarkHighContrast = Color(0xFFF1EEFF) +val onPrimaryDarkHighContrast = Color(0xFF000000) +val primaryContainerDarkHighContrast = Color(0xFFBDBCFF) +val onPrimaryContainerDarkHighContrast = Color(0xFF03003B) +val secondaryDarkHighContrast = Color(0xFFFFFFFF) +val onSecondaryDarkHighContrast = Color(0xFF000000) +val secondaryContainerDarkHighContrast = Color(0xFFE0E0F9) +val onSecondaryContainerDarkHighContrast = Color(0xFF27293B) +val tertiaryDarkHighContrast = Color(0xFFFFFFFF) +val onTertiaryDarkHighContrast = Color(0xFF000000) +val tertiaryContainerDarkHighContrast = Color(0xFFE2E2E2) +val onTertiaryContainerDarkHighContrast = Color(0xFF282A2B) +val errorDarkHighContrast = Color(0xFFFFECEA) +val onErrorDarkHighContrast = Color(0xFF000000) +val errorContainerDarkHighContrast = Color(0xFFFFADAA) +val onErrorContainerDarkHighContrast = Color(0xFF220002) +val backgroundDarkHighContrast = Color(0xFF13131A) +val onBackgroundDarkHighContrast = Color(0xFFE4E1EC) +val surfaceDarkHighContrast = Color(0xFF141313) +val onSurfaceDarkHighContrast = Color(0xFFFFFFFF) +val surfaceVariantDarkHighContrast = Color(0xFF444748) +val onSurfaceVariantDarkHighContrast = Color(0xFFFFFFFF) +val outlineDarkHighContrast = Color(0xFFEEF0F1) +val outlineVariantDarkHighContrast = Color(0xFFC0C3C4) +val scrimDarkHighContrast = Color(0xFF000000) +val inverseSurfaceDarkHighContrast = Color(0xFFE5E2E1) +val inverseOnSurfaceDarkHighContrast = Color(0xFF000000) +val inversePrimaryDarkHighContrast = Color(0xFF3733B3) +val surfaceDimDarkHighContrast = Color(0xFF141313) +val surfaceBrightDarkHighContrast = Color(0xFF51504F) +val surfaceContainerLowestDarkHighContrast = Color(0xFF000000) +val surfaceContainerLowDarkHighContrast = Color(0xFF201F1F) +val surfaceContainerDarkHighContrast = Color(0xFF313030) +val surfaceContainerHighDarkHighContrast = Color(0xFF3C3B3B) +val surfaceContainerHighestDarkHighContrast = Color(0xFF474646) + val White = Color(0xFFFFFFFF) val Black = Color(0xFF000000) diff --git a/app/src/main/java/com/sampoom/android/core/ui/theme/Theme.kt b/app/src/main/java/com/sampoom/android/core/ui/theme/Theme.kt index 9c55a3d..88e7334 100644 --- a/app/src/main/java/com/sampoom/android/core/ui/theme/Theme.kt +++ b/app/src/main/java/com/sampoom/android/core/ui/theme/Theme.kt @@ -12,31 +12,84 @@ import androidx.compose.ui.platform.LocalContext private val DarkColorScheme = darkColorScheme( primary = Main500, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, secondary = Main300, - tertiary = Main100 + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = Main100, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = BgBlack, + onBackground = onBackgroundDark, + surface = BgBlack, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, ) private val LightColorScheme = lightColorScheme( primary = Main500, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, secondary = Main300, - tertiary = Main100 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = Main100, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = BgWhite, + onBackground = onBackgroundLight, + surface = BgWhite, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, ) @Composable fun SampoomManagementTheme( darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, + dynamicColor: Boolean = false, content: @Composable () -> Unit ) { val colorScheme = when { diff --git a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/api/AuthApi.kt b/app/src/main/java/com/sampoom/android/feature/auth/data/remote/api/AuthApi.kt index 74ff84f..41ee5bd 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/api/AuthApi.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/data/remote/api/AuthApi.kt @@ -9,9 +9,9 @@ import retrofit2.http.Body import retrofit2.http.POST interface AuthApi { - @POST("login") + @POST("auth/login") suspend fun login(@Body body: LoginRequestDto): ApiResponse - @POST("signup") + @POST("auth/signup") suspend fun signUp(@Body body: SignUpRequestDto): ApiResponse } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/ui/LoginScreen.kt b/app/src/main/java/com/sampoom/android/feature/auth/ui/LoginScreen.kt index 36ed2a9..a894c8d 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/ui/LoginScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/ui/LoginScreen.kt @@ -7,20 +7,16 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -35,6 +31,7 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle 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.CommonButton import com.sampoom.android.core.ui.component.CommonTextField @@ -42,6 +39,7 @@ import com.sampoom.android.core.ui.theme.Main500 import com.sampoom.android.core.ui.component.ShowErrorSnackBar import com.sampoom.android.core.ui.component.rememberCommonSnackBarHostState import com.sampoom.android.core.ui.component.TopSnackBarHost +import com.sampoom.android.core.ui.theme.backgroundColor @Composable fun LoginScreen( @@ -57,15 +55,15 @@ fun LoginScreen( viewModel.bindLabel(emailLabel, passwordLabel, errorLabel) } - val state by viewModel.state.collectAsState() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() - LaunchedEffect(state.success) { - if (state.success) onSuccess() + LaunchedEffect(uiState.success) { + if (uiState.success) onSuccess() } val snackBarHostState = rememberCommonSnackBarHostState() ShowErrorSnackBar( - errorMessage = state.error, + errorMessage = uiState.error, snackBarHostState = snackBarHostState, onConsumed = { viewModel.consumeError() } ) @@ -101,30 +99,30 @@ fun LoginScreen( Spacer(Modifier.height(48.dp)) CommonTextField( modifier = Modifier.fillMaxWidth(), - value = state.email, + value = uiState.email, onValueChange = { viewModel.onEvent(LoginUiEvent.EmailChanged(it)) }, placeholder = stringResource(R.string.login_placeholder_email), - isError = state.emailError != null, - errorMessage = state.emailError + isError = uiState.emailError != null, + errorMessage = uiState.emailError ) Spacer(Modifier.height(8.dp)) CommonTextField( modifier = Modifier.fillMaxWidth(), - value = state.password, + value = uiState.password, onValueChange = { viewModel.onEvent(LoginUiEvent.PasswordChanged(it)) }, placeholder = stringResource(R.string.login_placeholder_password), isPassword = true, - isError = state.passwordError != null, - errorMessage = state.passwordError + isError = uiState.passwordError != null, + errorMessage = uiState.passwordError ) Spacer(Modifier.height(48.dp)) CommonButton( onClick = { viewModel.onEvent(LoginUiEvent.Submit) }, - enabled = state.isValid && !state.loading, + enabled = uiState.isValid && !uiState.loading, modifier = Modifier.fillMaxWidth() ) { Text( - if (state.loading) stringResource(R.string.login_button_login_loading) + if (uiState.loading) stringResource(R.string.login_button_login_loading) else stringResource(R.string.login_button_login) ) } diff --git a/app/src/main/java/com/sampoom/android/feature/auth/ui/LoginViewModel.kt b/app/src/main/java/com/sampoom/android/feature/auth/ui/LoginViewModel.kt index 997312c..83ba328 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/ui/LoginViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/ui/LoginViewModel.kt @@ -20,8 +20,8 @@ class LoginViewModel @Inject constructor( private val singIn: LoginUseCase, private val application: Application ) : ViewModel() { - private val _state = MutableStateFlow(LoginUiState()) - val state: StateFlow = _state + private val _uiState = MutableStateFlow(LoginUiState()) + val uiState: StateFlow = _uiState private var emailLabel: String = "" private var passwordLabel: String = "" @@ -35,26 +35,26 @@ class LoginViewModel @Inject constructor( fun onEvent(e: LoginUiEvent) = when (e) { is LoginUiEvent.EmailChanged -> { - _state.value = _state.value.copy(email = e.email) + _uiState.value = _uiState.value.copy(email = e.email) validateEmail() } is LoginUiEvent.PasswordChanged -> { - _state.value = _state.value.copy(password = e.password) + _uiState.value = _uiState.value.copy(password = e.password) validatePassword() } LoginUiEvent.Submit -> submit() } private fun validateEmail() { - val result = AuthValidator.validateNotEmpty(_state.value.email, emailLabel) - _state.value = _state.value.copy( + val result = AuthValidator.validateNotEmpty(_uiState.value.email, emailLabel) + _uiState.value = _uiState.value.copy( emailError = result.toErrorMessage() ) } private fun validatePassword() { - val result = AuthValidator.validateNotEmpty(_state.value.password, passwordLabel) - _state.value = _state.value.copy( + val result = AuthValidator.validateNotEmpty(_uiState.value.password, passwordLabel) + _uiState.value = _uiState.value.copy( passwordError = result.toErrorMessage() ) } @@ -71,22 +71,22 @@ class LoginViewModel @Inject constructor( validateEmail() validatePassword() - if (!_state.value.isValid) return@launch + if (!_uiState.value.isValid) return@launch - val s = _state.value - _state.update { it.copy(loading = true, error = null) } + val s = _uiState.value + _uiState.update { it.copy(loading = true, error = null) } runCatching { singIn(s.email, s.password) } - .onSuccess { _state.update { it.copy(loading = false, success = true) } } + .onSuccess { _uiState.update { it.copy(loading = false, success = true) } } .onFailure { throwable -> val backendMessage = throwable.serverMessageOrNull() - _state.update { + _uiState.update { it.copy(loading = false, error = backendMessage ?: (throwable.message ?: errorLabel)) } } - Log.d("LoginViewModel", "submit: ${_state.value}") + Log.d("LoginViewModel", "submit: ${_uiState.value}") } fun consumeError() { - _state.update { it.copy(error = null) } + _uiState.update { it.copy(error = null) } } } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpScreen.kt b/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpScreen.kt index 172037c..b252289 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpScreen.kt @@ -1,6 +1,7 @@ package com.sampoom.android.feature.auth.ui import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -41,6 +42,7 @@ import com.sampoom.android.core.ui.component.CommonTextField import com.sampoom.android.core.ui.component.ShowErrorSnackBar import com.sampoom.android.core.ui.component.rememberCommonSnackBarHostState import com.sampoom.android.core.ui.component.TopSnackBarHost +import com.sampoom.android.core.ui.theme.backgroundColor @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/com/sampoom/android/feature/part/data/local/database/.gitkeep b/app/src/main/java/com/sampoom/android/feature/part/data/local/database/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/part/data/local/preferences/.gitkeep b/app/src/main/java/com/sampoom/android/feature/part/data/local/preferences/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/part/data/mapper/PartMappers.kt b/app/src/main/java/com/sampoom/android/feature/part/data/mapper/PartMappers.kt index 0c06f6a..ffd1330 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/data/mapper/PartMappers.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/data/mapper/PartMappers.kt @@ -1,6 +1,9 @@ package com.sampoom.android.feature.part.data.mapper -import com.sampoom.android.feature.part.data.remote.dto.PartDto -import com.sampoom.android.feature.part.domain.model.Part +import com.sampoom.android.feature.part.data.remote.dto.CategoryDto +import com.sampoom.android.feature.part.data.remote.dto.GroupDto +import com.sampoom.android.feature.part.domain.model.Category +import com.sampoom.android.feature.part.domain.model.Group -fun PartDto.toModel(): Part = Part(id, name, count) \ No newline at end of file +fun CategoryDto.toModel(): Category = Category(id, code, name) +fun GroupDto.toModel(): Group = Group(id, code, name, categoryId) 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 e5a4998..a9f44a9 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 @@ -1,10 +1,15 @@ package com.sampoom.android.feature.part.data.remote.api import com.sampoom.android.core.network.ApiResponse -import com.sampoom.android.feature.part.data.remote.dto.PartDto +import com.sampoom.android.feature.part.data.remote.dto.CategoryDto +import com.sampoom.android.feature.part.data.remote.dto.GroupDto import retrofit2.http.GET +import retrofit2.http.Path interface PartApi { - @GET("part") - suspend fun getPartList(): ApiResponse> + @GET("agency/category") + suspend fun getCategoryList(): ApiResponse> + + @GET("agency/category/{categoryId}") + suspend fun getGroupList(@Path("categoryId") categoryId: Long): ApiResponse> } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/part/data/remote/dto/CategoryDto.kt b/app/src/main/java/com/sampoom/android/feature/part/data/remote/dto/CategoryDto.kt new file mode 100644 index 0000000..313b6cb --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/part/data/remote/dto/CategoryDto.kt @@ -0,0 +1,7 @@ +package com.sampoom.android.feature.part.data.remote.dto + +data class CategoryDto( + val id: Long, + val code: String, + val name: String +) diff --git a/app/src/main/java/com/sampoom/android/feature/part/data/remote/dto/GroupDto.kt b/app/src/main/java/com/sampoom/android/feature/part/data/remote/dto/GroupDto.kt new file mode 100644 index 0000000..991e1fe --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/part/data/remote/dto/GroupDto.kt @@ -0,0 +1,8 @@ +package com.sampoom.android.feature.part.data.remote.dto + +data class GroupDto( + val id: Long, + val code: String, + val name: String, + val categoryId: Long +) diff --git a/app/src/main/java/com/sampoom/android/feature/part/data/remote/dto/PartDto.kt b/app/src/main/java/com/sampoom/android/feature/part/data/remote/dto/PartDto.kt deleted file mode 100644 index d7973be..0000000 --- a/app/src/main/java/com/sampoom/android/feature/part/data/remote/dto/PartDto.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.sampoom.android.feature.part.data.remote.dto - -data class PartDto( - val id: Int, - val name: String, - val count: Int -) diff --git a/app/src/main/java/com/sampoom/android/feature/part/data/repository/PartRepositoryImpl.kt b/app/src/main/java/com/sampoom/android/feature/part/data/repository/PartRepositoryImpl.kt index 663cd0a..c36b64b 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/data/repository/PartRepositoryImpl.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/data/repository/PartRepositoryImpl.kt @@ -2,16 +2,23 @@ package com.sampoom.android.feature.part.data.repository import com.sampoom.android.feature.part.data.mapper.toModel import com.sampoom.android.feature.part.data.remote.api.PartApi -import com.sampoom.android.feature.part.domain.model.PartList +import com.sampoom.android.feature.part.domain.model.CategoryList +import com.sampoom.android.feature.part.domain.model.GroupList import com.sampoom.android.feature.part.domain.repository.PartRepository import javax.inject.Inject class PartRepositoryImpl @Inject constructor( private val api: PartApi ) : PartRepository { - override suspend fun getPartList(): PartList { - val response = api.getPartList() - val partItems = response.data.map { it.toModel() } - return PartList(items = partItems) + override suspend fun getCategoryList(): CategoryList { + val dto = api.getCategoryList() + val categoryItems = dto.data.map { it.toModel() } + return CategoryList(items = categoryItems) + } + + override suspend fun getGroupList(categoryId: Long): GroupList { + val response = api.getGroupList(categoryId) + val groupItems = response.data.map { it.toModel() } + return GroupList(items = groupItems) } } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/part/domain/model/Category.kt b/app/src/main/java/com/sampoom/android/feature/part/domain/model/Category.kt new file mode 100644 index 0000000..18834d6 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/part/domain/model/Category.kt @@ -0,0 +1,7 @@ +package com.sampoom.android.feature.part.domain.model + +data class Category( + val id: Long, + val code: String, + val name: String +) diff --git a/app/src/main/java/com/sampoom/android/feature/part/domain/model/CategoryList.kt b/app/src/main/java/com/sampoom/android/feature/part/domain/model/CategoryList.kt new file mode 100644 index 0000000..95d8248 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/part/domain/model/CategoryList.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.feature.part.domain.model + +data class CategoryList( + val items: List, + val totalCount: Int = items.size, + val isEmpty: Boolean = items.isEmpty() +) { + companion object Companion { + fun empty() = CategoryList(emptyList()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/part/domain/model/Group.kt b/app/src/main/java/com/sampoom/android/feature/part/domain/model/Group.kt new file mode 100644 index 0000000..833c004 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/part/domain/model/Group.kt @@ -0,0 +1,8 @@ +package com.sampoom.android.feature.part.domain.model + +data class Group( + val id: Long, + val code: String, + val name: String, + val categoryId: Long +) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/part/domain/model/PartList.kt b/app/src/main/java/com/sampoom/android/feature/part/domain/model/GroupList.kt similarity index 65% rename from app/src/main/java/com/sampoom/android/feature/part/domain/model/PartList.kt rename to app/src/main/java/com/sampoom/android/feature/part/domain/model/GroupList.kt index 74b0891..34bdd22 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/domain/model/PartList.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/domain/model/GroupList.kt @@ -1,11 +1,11 @@ package com.sampoom.android.feature.part.domain.model -data class PartList( - val items: List, +data class GroupList( + val items: List, val totalCount: Int = items.size, val isEmpty: Boolean = items.isEmpty() ) { companion object Companion { - fun empty() = PartList(emptyList()) + fun empty() = GroupList(emptyList()) } } diff --git a/app/src/main/java/com/sampoom/android/feature/part/domain/model/Part.kt b/app/src/main/java/com/sampoom/android/feature/part/domain/model/Part.kt deleted file mode 100644 index 9c1bb23..0000000 --- a/app/src/main/java/com/sampoom/android/feature/part/domain/model/Part.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.sampoom.android.feature.part.domain.model - -data class Part( - val id: Int, - val name: String, - val count: Int -) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/part/domain/repository/PartRepository.kt b/app/src/main/java/com/sampoom/android/feature/part/domain/repository/PartRepository.kt index 06c46d5..e0f05dd 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/domain/repository/PartRepository.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/domain/repository/PartRepository.kt @@ -1,7 +1,9 @@ package com.sampoom.android.feature.part.domain.repository -import com.sampoom.android.feature.part.domain.model.PartList +import com.sampoom.android.feature.part.domain.model.CategoryList +import com.sampoom.android.feature.part.domain.model.GroupList interface PartRepository { - suspend fun getPartList(): PartList + suspend fun getCategoryList(): CategoryList + suspend fun getGroupList(categoryId: Long): GroupList } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/part/domain/usecase/GetPartUseCase.kt b/app/src/main/java/com/sampoom/android/feature/part/domain/usecase/GetCategoryUseCase.kt similarity index 51% rename from app/src/main/java/com/sampoom/android/feature/part/domain/usecase/GetPartUseCase.kt rename to app/src/main/java/com/sampoom/android/feature/part/domain/usecase/GetCategoryUseCase.kt index 9fdd394..3029262 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/domain/usecase/GetPartUseCase.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/domain/usecase/GetCategoryUseCase.kt @@ -1,11 +1,11 @@ package com.sampoom.android.feature.part.domain.usecase -import com.sampoom.android.feature.part.domain.model.PartList +import com.sampoom.android.feature.part.domain.model.CategoryList import com.sampoom.android.feature.part.domain.repository.PartRepository import javax.inject.Inject -class GetPartUseCase @Inject constructor( +class GetCategoryUseCase @Inject constructor( private val repository: PartRepository ) { - suspend operator fun invoke(): PartList = repository.getPartList() + suspend operator fun invoke(): CategoryList = repository.getCategoryList() } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/part/domain/usecase/GetGroupUseCase.kt b/app/src/main/java/com/sampoom/android/feature/part/domain/usecase/GetGroupUseCase.kt new file mode 100644 index 0000000..256be21 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/part/domain/usecase/GetGroupUseCase.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.feature.part.domain.usecase + +import com.sampoom.android.feature.part.domain.model.GroupList +import com.sampoom.android.feature.part.domain.repository.PartRepository +import javax.inject.Inject + +class GetGroupUseCase @Inject constructor( + private val repository: PartRepository +) { + suspend operator fun invoke(categoryId: Long): GroupList = repository.getGroupList(categoryId) +} \ 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 1164887..d7202a2 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 @@ -3,79 +3,250 @@ package com.sampoom.android.feature.part.ui import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Search import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.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.theme.FailRed +import com.sampoom.android.core.ui.theme.Main500 +import com.sampoom.android.core.ui.theme.White +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.Category +import com.sampoom.android.feature.part.domain.model.Group +@OptIn(ExperimentalMaterial3Api::class) @Composable fun PartScreen( + onNavigateBack: () -> Unit = {}, viewModel: PartViewModel = hiltViewModel() ) { + val errorLabel = stringResource(R.string.common_error) + + LaunchedEffect(errorLabel) { + viewModel.bindLabel(errorLabel) + } + val uiState by viewModel.uiState.collectAsStateWithLifecycle() +// val snackBarHostState = rememberCommonSnackBarHostState() +// ShowErrorSnackBar( +// errorMessage = uiState.categoryError, +// snackBarHostState = snackBarHostState, +// onConsumed = { viewModel.consumeError() } +// ) - Scaffold { innerPadding -> + 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) + ) + } + } + ) + } + ) { innerPadding -> Column( modifier = Modifier .fillMaxSize() .padding(innerPadding) .padding(16.dp) ) { + // 검색 바 + OutlinedTextField( + value = "uiState.searchQuery", + onValueChange = { "viewModel.onSearchQueryChanged(it)" }, + placeholder = { Text("부품명으로 검색") }, + trailingIcon = { + Icon( + Icons.Default.Search, + contentDescription = "검색", + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + shape = RoundedCornerShape(24.dp), + singleLine = true, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = Color.Transparent, + unfocusedBorderColor = Color.Transparent, + focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), + ) + ) + + // Category 선택 제목 Text( - text = stringResource(R.string.part_title), - style = MaterialTheme.typography.headlineMedium, + text = stringResource(R.string.part_title_category), + style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, - modifier = Modifier.padding(bottom = 16.dp) + modifier = Modifier.padding(vertical = 16.dp) ) + // Category 섹션 when { - uiState.loading -> { + uiState.categoryLoading -> { Box( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxWidth() + .height(200.dp), contentAlignment = Alignment.Center ) { CircularProgressIndicator() } } - uiState.error != null -> { + uiState.categoryError != null -> { + ErrorContent( + onRetry = { viewModel.onEvent(PartUiEvent.RetryCategories) }, + modifier = Modifier.height(200.dp) + ) + } + + uiState.categoryList.isEmpty() -> { + EmptyContent( + message = stringResource(R.string.part_empty_category), + modifier = Modifier.height(200.dp) + ) + } + + else -> { + // 2x3 그리드로 카테고리 배치 Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { - Text( - text = "${stringResource(R.string.common_error)}: ${uiState.error}", - color = MaterialTheme.colorScheme.error - ) - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = { viewModel.refreshPart() }) { - Text(stringResource((R.string.common_retry))) + // 첫 번째 줄 (3개) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + uiState.categoryList.take(3).forEach { category -> + CategoryItem( + category = category, + isSelected = category.id == uiState.selectedCategory?.id, + onClick = { + viewModel.onEvent( + PartUiEvent.CategorySelected( + category + ) + ) + }, + modifier = Modifier.weight(1f) + ) + } + } + + // 두 번째 줄 (3개) + if (uiState.categoryList.size > 3) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + uiState.categoryList.drop(3).take(3).forEach { category -> + CategoryItem( + category = category, + isSelected = category.id == uiState.selectedCategory?.id, + onClick = { + viewModel.onEvent( + PartUiEvent.CategorySelected( + category + ) + ) + }, + modifier = Modifier.weight(1f) + ) + } + } } } } + } - uiState.partList.isEmpty() -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text(stringResource(R.string.part_empty)) - } + Spacer(Modifier.height(24.dp)) + + // 그룹 리스트 섹션 + if (uiState.selectedCategory == null) { + // 초기 상태: 카테고리 선택 안내 + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + painter = painterResource(R.drawable.search), + contentDescription = null, + modifier = Modifier.size(32.dp), + tint = textSecondaryColor() + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.part_select_category), + style = MaterialTheme.typography.bodyMedium, + color = textSecondaryColor() + ) } + } else { + // 그룹 선택 제목 + Text( + text = stringResource(R.string.part_title_group), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(vertical = 16.dp) + ) - else -> { - LazyColumn( - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - items(uiState.partList) { inventory -> - PartItemCard(part = inventory) + // 그룹 리스트 + when { + uiState.groupLoading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + uiState.groupError != null -> { + ErrorContent( + onRetry = { viewModel.onEvent(PartUiEvent.RetryGroups) }, + modifier = Modifier.height(200.dp) + ) + } + + uiState.groupList.isEmpty() -> { + EmptyContent( + message = stringResource(R.string.part_empty_group), + modifier = Modifier.height(200.dp) + ) + } + + else -> { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(uiState.groupList) { inventory -> + PartItemCard(group = inventory) + } } } } @@ -84,13 +255,62 @@ fun PartScreen( } } +// Category 아이템 +@Composable +fun CategoryItem( + category: Category, + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Card( + onClick = { onClick() }, + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = if (isSelected) Main500 else backgroundCardColor() + ), + modifier = modifier.height(100.dp) + ) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + painter = painterResource(id = resourceMapper(category.code)), + contentDescription = category.name, + modifier = Modifier.size(32.dp), + tint = if (isSelected) White else textColor() + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = category.name, + style = MaterialTheme.typography.bodySmall, + color = if (isSelected) White else textColor() + ) + } + } +} + +private fun resourceMapper(code: String): Int { + return when (code) { + "ENG" -> R.drawable.engine + "TRN" -> R.drawable.transmission + "CHS" -> R.drawable.chassis + "BDY" -> R.drawable.body + "TRM" -> R.drawable.trim + "ELE" -> R.drawable.electric + else -> R.drawable.parts + } +} + @Composable private fun PartItemCard( - part: com.sampoom.android.feature.part.domain.model.Part + group: Group ) { Card( modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + colors = CardDefaults.cardColors(containerColor = backgroundCardColor()), ) { Row( modifier = Modifier @@ -99,25 +319,49 @@ private fun PartItemCard( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Column { - Text( - text = part.name, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - Text( - text = "ID: ${part.id}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - Text( - text = "${part.count}개", - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary + text = group.name, + style = MaterialTheme.typography.titleMedium + ) + + Icon( + painterResource(R.drawable.chevron_right), + contentDescription = stringResource(R.string.common_detail) ) } } +} + +@Composable +fun ErrorContent( + onRetry: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(R.string.common_error), + color = FailRed + ) + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = onRetry) { + Text(stringResource((R.string.common_retry))) + } + } +} + +@Composable +fun EmptyContent( + message: String, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(message) + } } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartUiEvent.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartUiEvent.kt index e6a49d0..7dbf889 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/ui/PartUiEvent.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartUiEvent.kt @@ -1,4 +1,10 @@ package com.sampoom.android.feature.part.ui +import com.sampoom.android.feature.part.domain.model.Category + sealed interface PartUiEvent { + object LoadCategories : PartUiEvent + data class CategorySelected(val category: Category) : PartUiEvent + object RetryCategories : PartUiEvent + object RetryGroups : PartUiEvent } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartUiState.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartUiState.kt index d343dbe..f43a4f6 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/ui/PartUiState.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartUiState.kt @@ -1,10 +1,19 @@ package com.sampoom.android.feature.part.ui -import com.sampoom.android.feature.part.domain.model.Part +import com.sampoom.android.feature.part.domain.model.Category +import com.sampoom.android.feature.part.domain.model.Group data class PartUiState( - val loading: Boolean = false, - val error: String? = null, - val success: Boolean = false, - val partList: List = emptyList() + // Part + val groupList: List = emptyList(), + val groupLoading: Boolean = false, + val groupError: String? = null, + + // 선택된 Category + val selectedCategory: Category? = null, + + // Category + val categoryList: List = emptyList(), + val categoryLoading: Boolean = false, + val categoryError: String? = null ) \ No newline at end of file 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 c979584..98aa727 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 @@ -1,47 +1,115 @@ package com.sampoom.android.feature.part.ui +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.sampoom.android.feature.part.domain.usecase.GetPartUseCase +import com.sampoom.android.core.network.serverMessageOrNull +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.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class PartViewModel @Inject constructor( - private val getPartUseCase: GetPartUseCase + private val getCategoryUseCase: GetCategoryUseCase, + private val getGroupUseCase: GetGroupUseCase ) : ViewModel() { - private val _uiState = MutableStateFlow(PartUiState()) - val uiState: StateFlow = _uiState.asStateFlow() + val uiState: StateFlow = _uiState + + private var errorLabel: String = "" + + fun bindLabel(error: String) { + errorLabel = error + } init { - loadPart() + loadCategory() + } + + fun onEvent(event: PartUiEvent) { + when (event) { + is PartUiEvent.LoadCategories -> loadCategory() + is PartUiEvent.CategorySelected -> selectCategory(event.category) + is PartUiEvent.RetryCategories -> loadCategory() + is PartUiEvent.RetryGroups -> loadGroup() + } } - private fun loadPart() { + private fun loadCategory() { viewModelScope.launch { - _uiState.value = _uiState.value.copy(loading = true, error = null) - try { - val partListResult = getPartUseCase() - _uiState.value = _uiState.value.copy( - loading = false, - partList = partListResult.items, - success = true - ) - } catch (e: Exception) { - _uiState.value = _uiState.value.copy( - loading = false, - error = e.message ?: "Unknown error occurred" - ) - } + _uiState.update { it.copy(categoryLoading = true, categoryError = null) } + + runCatching { getCategoryUseCase() } + .onSuccess { categoryList -> + _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) + ) + } + } + Log.d("PartViewModel", "submit: ${_uiState.value}") } } - fun refreshPart() { - loadPart() + private fun selectCategory(category: Category) { + viewModelScope.launch { + _uiState.update { it.copy(selectedCategory = category) } + loadGroup(category.id) + } } + + private fun loadGroup(categoryId: Long) { + viewModelScope.launch { + _uiState.update { it.copy(groupLoading = true, groupError = null) } + + runCatching { getGroupUseCase(categoryId) } + .onSuccess { groupList -> + _uiState.update { + it.copy( + groupList = groupList.items, + groupLoading = false, + groupError = null + ) + } + } + .onFailure { throwable -> + val backendMessage = throwable.serverMessageOrNull() + _uiState.update { + it.copy( + groupLoading = false, + groupError = backendMessage ?: (throwable.message ?: errorLabel) + ) + } + } + Log.d("PartViewModel", "submit: ${_uiState.value}") + } + } + + private fun loadGroup() { + val selectedCategory = _uiState.value.selectedCategory + if (selectedCategory != null) { + loadGroup(selectedCategory.id) + } + } + +// fun refreshPart() { +// loadCategory() +// } } \ No newline at end of file diff --git a/app/src/main/res/drawable/body.xml b/app/src/main/res/drawable/body.xml new file mode 100644 index 0000000..3ddac4e --- /dev/null +++ b/app/src/main/res/drawable/body.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/cart.xml b/app/src/main/res/drawable/cart.xml index afc4912..12a29bd 100644 --- a/app/src/main/res/drawable/cart.xml +++ b/app/src/main/res/drawable/cart.xml @@ -8,6 +8,6 @@ android:pathData="M0.077,0h24.077v24.333h-24.077z"/> + android:fillColor="@color/text"/> diff --git a/app/src/main/res/drawable/chassis.xml b/app/src/main/res/drawable/chassis.xml new file mode 100644 index 0000000..b73b9dd --- /dev/null +++ b/app/src/main/res/drawable/chassis.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/chevron_right.xml b/app/src/main/res/drawable/chevron_right.xml new file mode 100644 index 0000000..6c71c89 --- /dev/null +++ b/app/src/main/res/drawable/chevron_right.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/dashboard.xml b/app/src/main/res/drawable/dashboard.xml index c2c0e05..f7b65e4 100644 --- a/app/src/main/res/drawable/dashboard.xml +++ b/app/src/main/res/drawable/dashboard.xml @@ -5,5 +5,5 @@ android:viewportHeight="25"> + android:fillColor="@color/text"/> diff --git a/app/src/main/res/drawable/delivery.xml b/app/src/main/res/drawable/delivery.xml index ca411d4..9069781 100644 --- a/app/src/main/res/drawable/delivery.xml +++ b/app/src/main/res/drawable/delivery.xml @@ -8,6 +8,6 @@ android:pathData="M0.615,0h24v24h-24z"/> + android:fillColor="@color/text"/> diff --git a/app/src/main/res/drawable/electric.xml b/app/src/main/res/drawable/electric.xml new file mode 100644 index 0000000..223c5da --- /dev/null +++ b/app/src/main/res/drawable/electric.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/engine.xml b/app/src/main/res/drawable/engine.xml new file mode 100644 index 0000000..3785215 --- /dev/null +++ b/app/src/main/res/drawable/engine.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/orders.xml b/app/src/main/res/drawable/orders.xml index 7521789..41a7732 100644 --- a/app/src/main/res/drawable/orders.xml +++ b/app/src/main/res/drawable/orders.xml @@ -5,5 +5,5 @@ android:viewportHeight="25"> + android:fillColor="@color/text"/> diff --git a/app/src/main/res/drawable/parts.xml b/app/src/main/res/drawable/parts.xml index ff89964..f55ed30 100644 --- a/app/src/main/res/drawable/parts.xml +++ b/app/src/main/res/drawable/parts.xml @@ -5,5 +5,5 @@ android:viewportHeight="25"> + android:fillColor="@color/text"/> diff --git a/app/src/main/res/drawable/search.xml b/app/src/main/res/drawable/search.xml index 519d2d6..c8582cb 100644 --- a/app/src/main/res/drawable/search.xml +++ b/app/src/main/res/drawable/search.xml @@ -8,6 +8,6 @@ android:strokeLineJoin="round" android:strokeWidth="2" android:fillColor="#00000000" - android:strokeColor="#36393F" + android:strokeColor="@color/text" android:strokeLineCap="round"/> diff --git a/app/src/main/res/drawable/transmission.xml b/app/src/main/res/drawable/transmission.xml new file mode 100644 index 0000000..0af0ab7 --- /dev/null +++ b/app/src/main/res/drawable/transmission.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/trim.xml b/app/src/main/res/drawable/trim.xml new file mode 100644 index 0000000..ffb4a25 --- /dev/null +++ b/app/src/main/res/drawable/trim.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5de1348..64f0427 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -37,14 +37,20 @@ 회원가입 회원 가입 중… - - 부품 목록 - 부품 목록이 없습니다. + + 부품조회 + 부품명으로 검색 + 카테고리 선택 + 그룹 선택 + 카테고리 목록이 없습니다. + 카테고리를 선택해주세요. + 그룹 목록이 없습니다. 오류가 발생했습니다 다시 시도 닫기 + 상세 보기 이메일을 입력해주세요 From d53f9100f8587a1d1b89d895eebc6716f2a4cfb9 Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Thu, 16 Oct 2025 19:59:15 +0900 Subject: [PATCH 2/6] =?UTF-8?q?[FEAT]=20=EB=B6=80=ED=92=88=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=ED=99=94=EB=A9=B4=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/app/navigation/AppNavHost.kt | 56 ++++-- .../feature/part/data/mapper/PartMappers.kt | 3 + .../feature/part/data/remote/api/PartApi.kt | 4 + .../feature/part/data/remote/dto/PartDto.kt | 8 + .../data/repository/PartRepositoryImpl.kt | 7 + .../android/feature/part/domain/model/Part.kt | 8 + .../feature/part/domain/model/PartList.kt | 11 ++ .../part/domain/repository/PartRepository.kt | 2 + .../part/domain/usecase/GetPartUseCase.kt | 11 ++ .../android/feature/part/ui/PartListScreen.kt | 168 ++++++++++++++++++ .../feature/part/ui/PartListUiEvent.kt | 6 + .../feature/part/ui/PartListUiState.kt | 9 + .../feature/part/ui/PartListViewModel.kt | 71 ++++++++ .../android/feature/part/ui/PartScreen.kt | 20 ++- app/src/main/res/values/strings.xml | 1 + 15 files changed, 364 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/sampoom/android/feature/part/data/remote/dto/PartDto.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/part/domain/model/Part.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/part/domain/model/PartList.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/part/domain/usecase/GetPartUseCase.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/part/ui/PartListUiEvent.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/part/ui/PartListUiState.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/part/ui/PartListViewModel.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 2c0948d..550b9ec 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 @@ -14,14 +14,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.navigation.NavHostController +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument import com.sampoom.android.R import com.sampoom.android.core.ui.theme.backgroundColor import com.sampoom.android.feature.auth.ui.LoginScreen import com.sampoom.android.feature.auth.ui.SignUpScreen +import com.sampoom.android.feature.part.ui.PartListScreen import com.sampoom.android.feature.part.ui.PartScreen const val ROUTE_LOGIN = "login" @@ -36,6 +39,8 @@ const val ROUTE_ORDERS = "orders" // Detail Screen const val ROUTE_PARTS = "parts" +const val ROUTE_PART_LIST = "{agencyId}/group/{groupId}" +fun routePartList(agencyId: Long, groupId: Long): String = "$agencyId/group/$groupId" const val ROUTE_EMPLOYEE = "employee" const val ROUTE_SETTINGS = "settings" @@ -62,11 +67,12 @@ fun AppNavHost() { startDestination = if (isLoggedIn) ROUTE_HOME else ROUTE_LOGIN ) { composable(ROUTE_LOGIN) { - LoginScreen(onSuccess = { - navController.navigate(ROUTE_HOME) { - popUpTo(ROUTE_LOGIN) { inclusive = true } // 로그인 화면 스택 제거 - } - }, + LoginScreen( + onSuccess = { + navController.navigate(ROUTE_HOME) { + popUpTo(ROUTE_LOGIN) { inclusive = true } // 로그인 화면 스택 제거 + } + }, onNavigateSignUp = { navController.navigate(ROUTE_SIGNUP) }) @@ -84,11 +90,29 @@ fun AppNavHost() { ) } composable(ROUTE_HOME) { MainScreen(navController) } - composable(ROUTE_PARTS) { PartScreen( - onNavigateBack = { - navController.navigateUp() - } - ) } + composable(ROUTE_PARTS) { + PartScreen( + onNavigateBack = { + navController.navigateUp() + }, + onNavigatePartList = { group -> + navController.navigate(routePartList(1, group.id)) + } + ) + } + composable( + ROUTE_PART_LIST, + arguments = listOf( + navArgument("agencyId") { type = NavType.LongType }, + navArgument("groupId") { type = NavType.LongType } + ) + ) { + PartListScreen( + onNavigateBack = { + navController.navigateUp() + } + ) + } } } @@ -128,7 +152,10 @@ fun PartsFab(navController: NavHostController) { } } ) { - Icon(painterResource(R.drawable.parts), contentDescription = stringResource(R.string.part_title)) + Icon( + painterResource(R.drawable.parts), + contentDescription = stringResource(R.string.part_title) + ) } } @@ -147,7 +174,12 @@ fun BottomNavigationBar(navController: NavHostController) { val currentDestination = navBackStackEntry?.destination NavigationBarItem( - icon = { Icon(painterResource(id = item.icon), contentDescription = stringResource(item.title)) }, + icon = { + Icon( + painterResource(id = item.icon), + contentDescription = stringResource(item.title) + ) + }, label = { Text(stringResource(item.title)) }, selected = currentDestination?.route == item.route, onClick = { diff --git a/app/src/main/java/com/sampoom/android/feature/part/data/mapper/PartMappers.kt b/app/src/main/java/com/sampoom/android/feature/part/data/mapper/PartMappers.kt index ffd1330..65800a6 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/data/mapper/PartMappers.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/data/mapper/PartMappers.kt @@ -2,8 +2,11 @@ package com.sampoom.android.feature.part.data.mapper import com.sampoom.android.feature.part.data.remote.dto.CategoryDto import com.sampoom.android.feature.part.data.remote.dto.GroupDto +import com.sampoom.android.feature.part.data.remote.dto.PartDto import com.sampoom.android.feature.part.domain.model.Category import com.sampoom.android.feature.part.domain.model.Group +import com.sampoom.android.feature.part.domain.model.Part fun CategoryDto.toModel(): Category = Category(id, code, name) fun GroupDto.toModel(): Group = Group(id, code, name, categoryId) +fun PartDto.toModel(): Part = Part(partId, code, name, quantity) \ No newline at end of file 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 a9f44a9..06f1004 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 @@ -3,6 +3,7 @@ package com.sampoom.android.feature.part.data.remote.api import com.sampoom.android.core.network.ApiResponse import com.sampoom.android.feature.part.data.remote.dto.CategoryDto import com.sampoom.android.feature.part.data.remote.dto.GroupDto +import com.sampoom.android.feature.part.data.remote.dto.PartDto import retrofit2.http.GET import retrofit2.http.Path @@ -12,4 +13,7 @@ interface PartApi { @GET("agency/category/{categoryId}") suspend fun getGroupList(@Path("categoryId") categoryId: Long): ApiResponse> + + @GET("agency/1/group/{groupId}") + suspend fun getPartList(@Path("groupId") groupId: Long): ApiResponse> } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/part/data/remote/dto/PartDto.kt b/app/src/main/java/com/sampoom/android/feature/part/data/remote/dto/PartDto.kt new file mode 100644 index 0000000..41872f7 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/part/data/remote/dto/PartDto.kt @@ -0,0 +1,8 @@ +package com.sampoom.android.feature.part.data.remote.dto + +data class PartDto( + val partId: Long, + val code: String, + val name: String, + val quantity: Long +) diff --git a/app/src/main/java/com/sampoom/android/feature/part/data/repository/PartRepositoryImpl.kt b/app/src/main/java/com/sampoom/android/feature/part/data/repository/PartRepositoryImpl.kt index c36b64b..f40c510 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/data/repository/PartRepositoryImpl.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/data/repository/PartRepositoryImpl.kt @@ -4,6 +4,7 @@ import com.sampoom.android.feature.part.data.mapper.toModel import com.sampoom.android.feature.part.data.remote.api.PartApi import com.sampoom.android.feature.part.domain.model.CategoryList import com.sampoom.android.feature.part.domain.model.GroupList +import com.sampoom.android.feature.part.domain.model.PartList import com.sampoom.android.feature.part.domain.repository.PartRepository import javax.inject.Inject @@ -21,4 +22,10 @@ class PartRepositoryImpl @Inject constructor( val groupItems = response.data.map { it.toModel() } return GroupList(items = groupItems) } + + override suspend fun getPartList(groupId: Long): PartList { + val response = api.getPartList(groupId) + val partItems = response.data.map { it.toModel() } + return PartList(items = partItems) + } } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/part/domain/model/Part.kt b/app/src/main/java/com/sampoom/android/feature/part/domain/model/Part.kt new file mode 100644 index 0000000..019b63b --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/part/domain/model/Part.kt @@ -0,0 +1,8 @@ +package com.sampoom.android.feature.part.domain.model + +data class Part( + val partId: Long, + val code: String, + val name: String, + val quantity: Long +) diff --git a/app/src/main/java/com/sampoom/android/feature/part/domain/model/PartList.kt b/app/src/main/java/com/sampoom/android/feature/part/domain/model/PartList.kt new file mode 100644 index 0000000..74b0891 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/part/domain/model/PartList.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.feature.part.domain.model + +data class PartList( + val items: List, + val totalCount: Int = items.size, + val isEmpty: Boolean = items.isEmpty() +) { + companion object Companion { + fun empty() = PartList(emptyList()) + } +} diff --git a/app/src/main/java/com/sampoom/android/feature/part/domain/repository/PartRepository.kt b/app/src/main/java/com/sampoom/android/feature/part/domain/repository/PartRepository.kt index e0f05dd..3d761e0 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/domain/repository/PartRepository.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/domain/repository/PartRepository.kt @@ -2,8 +2,10 @@ package com.sampoom.android.feature.part.domain.repository import com.sampoom.android.feature.part.domain.model.CategoryList import com.sampoom.android.feature.part.domain.model.GroupList +import com.sampoom.android.feature.part.domain.model.PartList interface PartRepository { suspend fun getCategoryList(): CategoryList suspend fun getGroupList(categoryId: Long): GroupList + suspend fun getPartList(groupId: Long): PartList } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/part/domain/usecase/GetPartUseCase.kt b/app/src/main/java/com/sampoom/android/feature/part/domain/usecase/GetPartUseCase.kt new file mode 100644 index 0000000..93ca316 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/part/domain/usecase/GetPartUseCase.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.feature.part.domain.usecase + +import com.sampoom.android.feature.part.domain.model.PartList +import com.sampoom.android.feature.part.domain.repository.PartRepository +import javax.inject.Inject + +class GetPartUseCase @Inject constructor( + private val repository: PartRepository +) { + suspend operator fun invoke(groupId: Long): PartList = repository.getPartList(groupId) +} \ No newline at end of file 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 new file mode 100644 index 0000000..61f6997 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt @@ -0,0 +1,168 @@ +package com.sampoom.android.feature.part.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.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.lazy.items +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.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.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.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 + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PartListScreen( + onNavigateBack: () -> Unit = {}, + viewModel: PartListViewModel = hiltViewModel() +) { + val errorLabel = stringResource(R.string.common_error) + + LaunchedEffect(errorLabel) { + viewModel.bindLabel(errorLabel) + } + + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + 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) + ) + } + } + ) + } + ) { innerPadding -> + when { + uiState.partListLoading -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + uiState.partListError != null -> { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + ErrorContent( + onRetry = { viewModel.onEvent(PartListUiEvent.RetryPartList) }, + modifier = Modifier.height(200.dp) + ) + } + } + + 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) + } + } + } + } + } +} + +@Composable +private fun PartListItemCard( + part: Part +) { + 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, + color = textColor(), + style = MaterialTheme.typography.titleMedium + ) + Text( + text = part.code, + color = textSecondaryColor(), + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Light + ) + } + + Text( + text = part.quantity.toString(), + color = textColor(), + style = MaterialTheme.typography.titleMedium + ) + + Icon( + painterResource(R.drawable.chevron_right), + contentDescription = stringResource(R.string.common_detail), + tint = textSecondaryColor() + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartListUiEvent.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListUiEvent.kt new file mode 100644 index 0000000..8026605 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListUiEvent.kt @@ -0,0 +1,6 @@ +package com.sampoom.android.feature.part.ui + +sealed interface PartListUiEvent { + object LoadPartList : PartListUiEvent + object RetryPartList : PartListUiEvent +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartListUiState.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListUiState.kt new file mode 100644 index 0000000..e5bbee1 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListUiState.kt @@ -0,0 +1,9 @@ +package com.sampoom.android.feature.part.ui + +import com.sampoom.android.feature.part.domain.model.Part + +data class PartListUiState( + val partList: List = emptyList(), + val partListLoading: Boolean = false, + val partListError: String? = null +) 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 new file mode 100644 index 0000000..8bd7990 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListViewModel.kt @@ -0,0 +1,71 @@ +package com.sampoom.android.feature.part.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.part.domain.usecase.GetPartUseCase +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 PartListViewModel @Inject constructor( + private val getPartListUseCase: GetPartUseCase, + savedStateHandle: SavedStateHandle +) : ViewModel() { + private val _uiState = MutableStateFlow(PartListUiState()) + val uiState: StateFlow = _uiState + + // Navigation 인자 로드 + private val agencyId: Long = savedStateHandle.get("agencyId") ?: 0L + private val groupId: Long = savedStateHandle.get("groupId") ?: 0L + + private var errorLabel: String = "" + + fun bindLabel(error: String) { + errorLabel = error + } + + init { + loadPartList(groupId) + } + + fun onEvent(event: PartListUiEvent) { + when (event) { + is PartListUiEvent.LoadPartList -> loadPartList(groupId) + is PartListUiEvent.RetryPartList -> loadPartList(groupId) + } + } + + private fun loadPartList(groupId: Long) { + 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 + ) + } + } + .onFailure { throwable -> + val backendMessage = throwable.serverMessageOrNull() + _uiState.update { + it.copy( + partListLoading = false, + partListError = backendMessage ?: (throwable.message ?: errorLabel) + ) + } + } + Log.d("PartListViewModel", "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 d7202a2..0890f12 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 @@ -17,6 +17,7 @@ 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 androidx.lifecycle.viewmodel.compose.viewModel import com.sampoom.android.R import com.sampoom.android.core.ui.theme.FailRed import com.sampoom.android.core.ui.theme.Main500 @@ -26,11 +27,13 @@ import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.core.ui.theme.textSecondaryColor import com.sampoom.android.feature.part.domain.model.Category import com.sampoom.android.feature.part.domain.model.Group +import com.sampoom.android.feature.part.ui.PartItemCard @OptIn(ExperimentalMaterial3Api::class) @Composable fun PartScreen( onNavigateBack: () -> Unit = {}, + onNavigatePartList: (Group) -> Unit, viewModel: PartViewModel = hiltViewModel() ) { val errorLabel = stringResource(R.string.common_error) @@ -40,12 +43,6 @@ fun PartScreen( } val uiState by viewModel.uiState.collectAsStateWithLifecycle() -// val snackBarHostState = rememberCommonSnackBarHostState() -// ShowErrorSnackBar( -// errorMessage = uiState.categoryError, -// snackBarHostState = snackBarHostState, -// onConsumed = { viewModel.consumeError() } -// ) Scaffold( topBar = { @@ -244,8 +241,11 @@ fun PartScreen( LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp) ) { - items(uiState.groupList) { inventory -> - PartItemCard(group = inventory) + items(uiState.groupList) { group -> + PartItemCard( + group = group, + onClick = { onNavigatePartList(group) } + ) } } } @@ -306,9 +306,11 @@ private fun resourceMapper(code: String): Int { @Composable private fun PartItemCard( - group: Group + group: Group, + onClick: () -> Unit ) { Card( + onClick = { onClick() }, modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = backgroundCardColor()), ) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 64f0427..0e06ff4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -43,6 +43,7 @@ 카테고리 선택 그룹 선택 카테고리 목록이 없습니다. + 부품 목록이 없습니다. 카테고리를 선택해주세요. 그룹 목록이 없습니다. From 08b589bcd1b62f76f7144fc35d5101c1866b1794 Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Sat, 18 Oct 2025 09:45:51 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[FIX]=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EA=B7=B8=EB=A6=AC=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/feature/part/ui/PartScreen.kt | 46 ++++++++----------- 1 file changed, 19 insertions(+), 27 deletions(-) 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 0890f12..3d2ffe8 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 @@ -3,13 +3,16 @@ package com.sampoom.android.feature.part.ui import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.focusModifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -63,8 +66,11 @@ fun PartScreen( modifier = Modifier .fillMaxSize() .padding(innerPadding) - .padding(16.dp) + .padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()) ) { + Spacer(Modifier.height(16.dp)) + // 검색 바 OutlinedTextField( value = "uiState.searchQuery", @@ -131,34 +137,12 @@ fun PartScreen( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - // 첫 번째 줄 (3개) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - uiState.categoryList.take(3).forEach { category -> - CategoryItem( - category = category, - isSelected = category.id == uiState.selectedCategory?.id, - onClick = { - viewModel.onEvent( - PartUiEvent.CategorySelected( - category - ) - ) - }, - modifier = Modifier.weight(1f) - ) - } - } - - // 두 번째 줄 (3개) - if (uiState.categoryList.size > 3) { + uiState.categoryList.chunked(3).forEach { categoryChunk -> Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - uiState.categoryList.drop(3).take(3).forEach { category -> + categoryChunk.forEach { category -> CategoryItem( category = category, isSelected = category.id == uiState.selectedCategory?.id, @@ -172,6 +156,11 @@ fun PartScreen( modifier = Modifier.weight(1f) ) } + + // 3개 미만인 경우 빈 공간 채우기 + repeat(3 - categoryChunk.size) { + Spacer(modifier = Modifier.weight(1f)) + } } } } @@ -238,10 +227,11 @@ fun PartScreen( } else -> { - LazyColumn( + Column( + Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - items(uiState.groupList) { group -> + uiState.groupList.forEach { group -> PartItemCard( group = group, onClick = { onNavigatePartList(group) } @@ -251,6 +241,8 @@ fun PartScreen( } } } + + Spacer(Modifier.height(32.dp)) } } } From 2efb8d0e31fb4a4ea256a297ca0b3b3576974111 Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Sat, 18 Oct 2025 12:49:14 +0900 Subject: [PATCH 4/6] =?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 | 4 +- .../android/core/ui/component/EmptyContent.kt | 21 +++++++++ .../android/core/ui/component/ErrorContent.kt | 37 +++++++++++++++ .../feature/part/ui/PartListViewModel.kt | 4 +- .../android/feature/part/ui/PartScreen.kt | 47 ++----------------- .../android/feature/part/ui/PartViewModel.kt | 9 +++- app/src/main/res/values/strings.xml | 3 +- 7 files changed, 78 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/com/sampoom/android/core/ui/component/EmptyContent.kt create mode 100644 app/src/main/java/com/sampoom/android/core/ui/component/ErrorContent.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 550b9ec..049d4ac 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 @@ -39,8 +39,8 @@ const val ROUTE_ORDERS = "orders" // Detail Screen const val ROUTE_PARTS = "parts" -const val ROUTE_PART_LIST = "{agencyId}/group/{groupId}" -fun routePartList(agencyId: Long, groupId: Long): String = "$agencyId/group/$groupId" +const val ROUTE_PART_LIST = "parts/{agencyId}/group/{groupId}" +fun routePartList(agencyId: Long, groupId: Long): String = "parts/$agencyId/group/$groupId" const val ROUTE_EMPLOYEE = "employee" const val ROUTE_SETTINGS = "settings" diff --git a/app/src/main/java/com/sampoom/android/core/ui/component/EmptyContent.kt b/app/src/main/java/com/sampoom/android/core/ui/component/EmptyContent.kt new file mode 100644 index 0000000..ba9ed2a --- /dev/null +++ b/app/src/main/java/com/sampoom/android/core/ui/component/EmptyContent.kt @@ -0,0 +1,21 @@ +package com.sampoom.android.core.ui.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun EmptyContent( + message: String, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text(message) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/core/ui/component/ErrorContent.kt b/app/src/main/java/com/sampoom/android/core/ui/component/ErrorContent.kt new file mode 100644 index 0000000..0d4b7ec --- /dev/null +++ b/app/src/main/java/com/sampoom/android/core/ui/component/ErrorContent.kt @@ -0,0 +1,37 @@ +package com.sampoom.android.core.ui.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Button +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.unit.dp +import com.sampoom.android.R +import com.sampoom.android.core.ui.theme.FailRed + +@Composable +fun ErrorContent( + onRetry: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(R.string.common_error), + color = FailRed + ) + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = onRetry) { + Text(stringResource((R.string.common_retry))) + } + } +} \ No newline at end of file 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 8bd7990..d0c4e80 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 @@ -1,6 +1,7 @@ package com.sampoom.android.feature.part.ui import android.util.Log +import androidx.compose.ui.res.stringResource import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -32,7 +33,8 @@ class PartListViewModel @Inject constructor( } init { - loadPartList(groupId) + if (groupId > 0L) loadPartList(groupId) + else _uiState.update { it.copy(partListError = errorLabel) } } fun onEvent(event: PartListUiEvent) { 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 3d2ffe8..11e1f22 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 @@ -1,8 +1,7 @@ package com.sampoom.android.feature.part.ui +import android.R.attr.singleLine import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll @@ -12,7 +11,6 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.focusModifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -20,9 +18,9 @@ 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 androidx.lifecycle.viewmodel.compose.viewModel import com.sampoom.android.R -import com.sampoom.android.core.ui.theme.FailRed +import com.sampoom.android.core.ui.component.EmptyContent +import com.sampoom.android.core.ui.component.ErrorContent import com.sampoom.android.core.ui.theme.Main500 import com.sampoom.android.core.ui.theme.White import com.sampoom.android.core.ui.theme.backgroundCardColor @@ -30,7 +28,6 @@ import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.core.ui.theme.textSecondaryColor import com.sampoom.android.feature.part.domain.model.Category import com.sampoom.android.feature.part.domain.model.Group -import com.sampoom.android.feature.part.ui.PartItemCard @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -75,11 +72,11 @@ fun PartScreen( OutlinedTextField( value = "uiState.searchQuery", onValueChange = { "viewModel.onSearchQueryChanged(it)" }, - placeholder = { Text("부품명으로 검색") }, + placeholder = { Text(stringResource(R.string.part_placeholder_search)) }, trailingIcon = { Icon( Icons.Default.Search, - contentDescription = "검색", + contentDescription = stringResource(R.string.part_title_search), tint = MaterialTheme.colorScheme.onSurfaceVariant ) }, @@ -324,38 +321,4 @@ private fun PartItemCard( ) } } -} - -@Composable -fun ErrorContent( - onRetry: () -> Unit, - modifier: Modifier = Modifier -) { - Column( - modifier = modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = stringResource(R.string.common_error), - color = FailRed - ) - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = onRetry) { - Text(stringResource((R.string.common_retry))) - } - } -} - -@Composable -fun EmptyContent( - message: String, - modifier: Modifier = Modifier -) { - Box( - modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text(message) - } } \ No newline at end of file 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 98aa727..6bcf62a 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.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -22,6 +23,7 @@ class PartViewModel @Inject constructor( private val _uiState = MutableStateFlow(PartUiState()) val uiState: StateFlow = _uiState + private var groupLoadJob: Job? = null private var errorLabel: String = "" fun bindLabel(error: String) { @@ -71,16 +73,20 @@ class PartViewModel @Inject constructor( private fun selectCategory(category: Category) { viewModelScope.launch { _uiState.update { it.copy(selectedCategory = category) } + groupLoadJob?.cancel() // 기존 그룹 로드 취소 후 새 요청 loadGroup(category.id) } } private fun loadGroup(categoryId: Long) { - viewModelScope.launch { + groupLoadJob?.cancel() + 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, @@ -91,6 +97,7 @@ class PartViewModel @Inject constructor( } .onFailure { throwable -> val backendMessage = throwable.serverMessageOrNull() + if (_uiState.value.selectedCategory?.id != categoryId) return@onFailure _uiState.update { it.copy( groupLoading = false, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0e06ff4..7f7d2ab 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -39,9 +39,10 @@ 부품조회 - 부품명으로 검색 + 검색 카테고리 선택 그룹 선택 + 부품명으로 검색 카테고리 목록이 없습니다. 부품 목록이 없습니다. 카테고리를 선택해주세요. From 2d7606226a6c2146fb745ab15ab685bd66e74512 Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Sat, 18 Oct 2025 12:50:10 +0900 Subject: [PATCH 5/6] =?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/app/navigation/AppNavHost.kt | 2 -- .../com/sampoom/android/feature/part/ui/PartListViewModel.kt | 1 - .../main/java/com/sampoom/android/feature/part/ui/PartScreen.kt | 1 - 3 files changed, 4 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 049d4ac..91cd311 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,6 +1,5 @@ package com.sampoom.android.app.navigation -import androidx.compose.foundation.background import androidx.compose.foundation.layout.padding import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon @@ -21,7 +20,6 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.sampoom.android.R -import com.sampoom.android.core.ui.theme.backgroundColor import com.sampoom.android.feature.auth.ui.LoginScreen import com.sampoom.android.feature.auth.ui.SignUpScreen import com.sampoom.android.feature.part.ui.PartListScreen 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 d0c4e80..999d8a9 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 @@ -1,7 +1,6 @@ package com.sampoom.android.feature.part.ui import android.util.Log -import androidx.compose.ui.res.stringResource import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope 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 11e1f22..97ae617 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 @@ -1,6 +1,5 @@ package com.sampoom.android.feature.part.ui -import android.R.attr.singleLine import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape From 9e73aacc45b9e95946854f6bbf0864355e2727d1 Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Sat, 18 Oct 2025 13:14:25 +0900 Subject: [PATCH 6/6] =?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 --- .../com/sampoom/android/core/ui/component/EmptyContent.kt | 3 +-- .../com/sampoom/android/core/ui/component/ErrorContent.kt | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/sampoom/android/core/ui/component/EmptyContent.kt b/app/src/main/java/com/sampoom/android/core/ui/component/EmptyContent.kt index ba9ed2a..83439f1 100644 --- a/app/src/main/java/com/sampoom/android/core/ui/component/EmptyContent.kt +++ b/app/src/main/java/com/sampoom/android/core/ui/component/EmptyContent.kt @@ -1,7 +1,6 @@ package com.sampoom.android.core.ui.component import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -13,7 +12,7 @@ fun EmptyContent( modifier: Modifier = Modifier ) { Box( - modifier = modifier.fillMaxSize(), + modifier = modifier, contentAlignment = Alignment.Center ) { Text(message) diff --git a/app/src/main/java/com/sampoom/android/core/ui/component/ErrorContent.kt b/app/src/main/java/com/sampoom/android/core/ui/component/ErrorContent.kt index 0d4b7ec..6e102cb 100644 --- a/app/src/main/java/com/sampoom/android/core/ui/component/ErrorContent.kt +++ b/app/src/main/java/com/sampoom/android/core/ui/component/ErrorContent.kt @@ -21,7 +21,7 @@ fun ErrorContent( modifier: Modifier = Modifier ) { Column( - modifier = modifier.fillMaxSize(), + modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { @@ -31,7 +31,7 @@ fun ErrorContent( ) Spacer(modifier = Modifier.height(16.dp)) Button(onClick = onRetry) { - Text(stringResource((R.string.common_retry))) + Text(stringResource(R.string.common_retry)) } } } \ No newline at end of file