From 41976b8d1201b185c2a1ff692a11b830b6fa0589 Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Sat, 8 Nov 2025 04:16:55 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[FEAT]=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=ED=9A=8C=EC=9B=90=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 1 + .../sampoom/android/MainActivityViewModel.kt | 4 +- .../android/app/navigation/AppNavHost.kt | 20 +- .../core/network/TokenRefreshService.kt | 2 +- .../core/preferences/AuthPreferences.kt | 2 +- .../domain => core/util}/AuthValidator.kt | 2 +- .../feature/auth/data/mapper/AuthMappers.kt | 25 +- .../feature/auth/data/remote/api/AuthApi.kt | 17 +- .../remote/dto/UpdateProfileRequestDto.kt | 8 - .../remote/dto/UpdateProfileResponseDto.kt | 9 - .../data/repository/AuthRepositoryImpl.kt | 35 +- .../auth/domain/repository/AuthRepository.kt | 3 +- .../domain/usecase/GetStoredUserUseCase.kt | 14 - .../auth/domain/usecase/LoginUseCase.kt | 2 +- .../auth/domain/usecase/SignUpUseCase.kt | 2 +- .../android/feature/auth/ui/LoginViewModel.kt | 25 +- .../feature/auth/ui/SignUpViewModel.kt | 36 +- .../feature/dashboard/ui/DashboardScreen.kt | 62 ++-- .../dashboard/ui/DashboardViewModel.kt | 11 +- .../ui/SettingScreen.kt | 49 ++- .../ui/SettingUiEvent.kt | 3 +- .../ui/SettingUiState.kt | 4 +- .../ui/SettingViewModel.kt | 32 +- .../sample/data/local/database/.gitkeep | 0 .../sample/data/local/preferences/.gitkeep | 0 .../feature/sample/data/mapper/.gitkeep | 0 .../feature/sample/data/remote/api/.gitkeep | 0 .../feature/sample/data/remote/dto/.gitkeep | 0 .../feature/sample/data/repository/.gitkeep | 0 .../android/feature/sample/di/.gitkeep | 0 .../feature/sample/domain/model/.gitkeep | 0 .../feature/sample/domain/repository/.gitkeep | 0 .../feature/sample/domain/usecase/.gitkeep | 0 .../android/feature/sample/ui/.gitkeep | 0 .../feature/user/data/mapper/UserMappers.kt | 73 ++++ .../user/data/paging/EmployeePagingSource.kt | 47 +++ .../feature/user/data/remote/api/UserApi.kt | 37 ++ .../data/remote/dto/EditEmployeeRequestDto.kt | 5 + .../remote/dto/EditEmployeeResponseDto.kt | 8 + .../user/data/remote/dto/EmployeeDto.kt | 16 + .../user/data/remote/dto/EmployeeListDto.kt | 15 + .../data/remote/dto/GetProfileResponseDto.kt | 4 +- .../remote/dto/UpdateProfileRequestDto.kt | 5 + .../remote/dto/UpdateProfileResponseDto.kt | 6 + .../data/repository/UserRepositoryImpl.kt | 133 +++++++ .../android/feature/user/di/UserModules.kt | 26 ++ .../feature/user/domain/model/Employee.kt | 16 + .../feature/user/domain/model/EmployeeList.kt | 11 + .../{auth => user}/domain/model/User.kt | 4 +- .../user/domain/repository/UserRepository.kt | 14 + .../domain/usecase/EditEmployeeUseCase.kt | 11 + .../user/domain/usecase/GetEmployeeUseCase.kt | 13 + .../user/domain/usecase/GetProfileUseCase.kt | 11 + .../domain/usecase/GetStoredUserUseCase.kt | 11 + .../domain/usecase/UpdateProfileUseCase.kt | 11 + .../user/ui/EditEmployeeBottomSheet.kt | 113 ++++++ .../feature/user/ui/EditEmployeeUiEvent.kt | 10 + .../feature/user/ui/EditEmployeeUiState.kt | 10 + .../feature/user/ui/EditEmployeeViewModel.kt | 105 ++++++ .../feature/user/ui/EmployeeListScreen.kt | 339 ++++++++++++++++++ .../feature/user/ui/EmployeeListUiEvent.kt | 10 + .../feature/user/ui/EmployeeListUiState.kt | 10 + .../feature/user/ui/EmployeeListViewModel.kt | 47 +++ .../user/ui/UpdateProfileBottomSheet.kt | 84 +++++ .../feature/user/ui/UpdateProfileUiEvent.kt | 9 + .../feature/user/ui/UpdateProfileUiState.kt | 10 + .../feature/user/ui/UpdateProfileViewModel.kt | 100 ++++++ app/src/main/res/values/strings.xml | 12 + gradle/libs.versions.toml | 2 + 69 files changed, 1505 insertions(+), 201 deletions(-) rename app/src/main/java/com/sampoom/android/{feature/auth/domain => core/util}/AuthValidator.kt (98%) delete mode 100644 app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/UpdateProfileRequestDto.kt delete mode 100644 app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/UpdateProfileResponseDto.kt delete mode 100644 app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/GetStoredUserUseCase.kt rename app/src/main/java/com/sampoom/android/feature/{setting => dashboard}/ui/SettingScreen.kt (84%) rename app/src/main/java/com/sampoom/android/feature/{setting => dashboard}/ui/SettingUiEvent.kt (62%) rename app/src/main/java/com/sampoom/android/feature/{setting => dashboard}/ui/SettingUiState.kt (77%) rename app/src/main/java/com/sampoom/android/feature/{setting => dashboard}/ui/SettingViewModel.kt (76%) delete mode 100644 app/src/main/java/com/sampoom/android/feature/sample/data/local/database/.gitkeep delete mode 100644 app/src/main/java/com/sampoom/android/feature/sample/data/local/preferences/.gitkeep delete mode 100644 app/src/main/java/com/sampoom/android/feature/sample/data/mapper/.gitkeep delete mode 100644 app/src/main/java/com/sampoom/android/feature/sample/data/remote/api/.gitkeep delete mode 100644 app/src/main/java/com/sampoom/android/feature/sample/data/remote/dto/.gitkeep delete mode 100644 app/src/main/java/com/sampoom/android/feature/sample/data/repository/.gitkeep delete mode 100644 app/src/main/java/com/sampoom/android/feature/sample/di/.gitkeep delete mode 100644 app/src/main/java/com/sampoom/android/feature/sample/domain/model/.gitkeep delete mode 100644 app/src/main/java/com/sampoom/android/feature/sample/domain/repository/.gitkeep delete mode 100644 app/src/main/java/com/sampoom/android/feature/sample/domain/usecase/.gitkeep delete mode 100644 app/src/main/java/com/sampoom/android/feature/sample/ui/.gitkeep create mode 100644 app/src/main/java/com/sampoom/android/feature/user/data/mapper/UserMappers.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/data/paging/EmployeePagingSource.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/data/remote/api/UserApi.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EditEmployeeRequestDto.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EditEmployeeResponseDto.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeDto.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeListDto.kt rename app/src/main/java/com/sampoom/android/feature/{auth => user}/data/remote/dto/GetProfileResponseDto.kt (82%) create mode 100644 app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateProfileRequestDto.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateProfileResponseDto.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/di/UserModules.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/domain/model/Employee.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/domain/model/EmployeeList.kt rename app/src/main/java/com/sampoom/android/feature/{auth => user}/domain/model/User.kt (87%) create mode 100644 app/src/main/java/com/sampoom/android/feature/user/domain/repository/UserRepository.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/domain/usecase/EditEmployeeUseCase.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetEmployeeUseCase.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetProfileUseCase.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetStoredUserUseCase.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/domain/usecase/UpdateProfileUseCase.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeBottomSheet.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeUiEvent.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeUiState.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeViewModel.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiEvent.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiState.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListViewModel.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileBottomSheet.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileUiEvent.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileUiState.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7d05d41..4bf2b53 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -69,6 +69,7 @@ kotlin { dependencies { // hilt implementation(libs.hilt.android) + implementation(libs.androidx.material3) ksp(libs.hilt.android.compiler) implementation(libs.androidx.hilt.lifecycle.viewmodel.compose) implementation(libs.androidx.hilt.navigation.compose) diff --git a/app/src/main/java/com/sampoom/android/MainActivityViewModel.kt b/app/src/main/java/com/sampoom/android/MainActivityViewModel.kt index 798b771..e1da791 100644 --- a/app/src/main/java/com/sampoom/android/MainActivityViewModel.kt +++ b/app/src/main/java/com/sampoom/android/MainActivityViewModel.kt @@ -3,8 +3,8 @@ package com.sampoom.android import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sampoom.android.core.util.GlobalMessageHandler -import com.sampoom.android.feature.auth.domain.model.User -import com.sampoom.android.feature.auth.domain.usecase.GetStoredUserUseCase +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.domain.usecase.GetStoredUserUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow 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 e8f66bc..44eb2fb 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,9 +1,7 @@ package com.sampoom.android.app.navigation import android.annotation.SuppressLint -import androidx.activity.ComponentActivity import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.CircularProgressIndicator @@ -25,11 +23,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.core.view.WindowInsetsControllerCompat import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController @@ -49,18 +44,19 @@ import com.sampoom.android.core.ui.theme.Main500 import com.sampoom.android.core.ui.theme.backgroundCardColor import com.sampoom.android.core.ui.theme.backgroundColor import com.sampoom.android.core.ui.theme.textColor -import com.sampoom.android.feature.auth.domain.model.User import com.sampoom.android.feature.auth.ui.AuthViewModel import com.sampoom.android.feature.auth.ui.LoginScreen import com.sampoom.android.feature.auth.ui.SignUpScreen import com.sampoom.android.feature.cart.ui.CartListScreen import com.sampoom.android.feature.dashboard.ui.DashboardScreen +import com.sampoom.android.feature.dashboard.ui.SettingScreen import com.sampoom.android.feature.order.ui.OrderDetailScreen import com.sampoom.android.feature.order.ui.OrderListScreen import com.sampoom.android.feature.outbound.ui.OutboundListScreen import com.sampoom.android.feature.part.ui.PartListScreen import com.sampoom.android.feature.part.ui.PartScreen -import com.sampoom.android.feature.setting.ui.SettingScreen +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.ui.EmployeeListScreen import kotlinx.coroutines.flow.filterNotNull // Auth Screen @@ -216,6 +212,13 @@ fun AppNavHost( } ) } + composable(ROUTE_EMPLOYEE) { + EmployeeListScreen( + onNavigateBack = { + navController.navigateUp() + } + ) + } } TopSnackBarHost(hostState = snackBarHostState, isError = currentMessage?.isError ?: false) } @@ -238,6 +241,9 @@ fun MainScreen( composable(ROUTE_DASHBOARD) { DashboardScreen( paddingValues = innerPadding, + onEmployeeClick = { + parentNavController.navigate(ROUTE_EMPLOYEE) + }, onSettingClick = { parentNavController.navigate(ROUTE_SETTINGS) }, diff --git a/app/src/main/java/com/sampoom/android/core/network/TokenRefreshService.kt b/app/src/main/java/com/sampoom/android/core/network/TokenRefreshService.kt index d2db843..1cf693d 100644 --- a/app/src/main/java/com/sampoom/android/core/network/TokenRefreshService.kt +++ b/app/src/main/java/com/sampoom/android/core/network/TokenRefreshService.kt @@ -3,7 +3,7 @@ package com.sampoom.android.core.network import com.sampoom.android.core.preferences.AuthPreferences import com.sampoom.android.feature.auth.data.remote.api.AuthApi import com.sampoom.android.feature.auth.data.remote.dto.RefreshRequestDto -import com.sampoom.android.feature.auth.domain.model.User +import com.sampoom.android.feature.user.domain.model.User import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import javax.inject.Inject diff --git a/app/src/main/java/com/sampoom/android/core/preferences/AuthPreferences.kt b/app/src/main/java/com/sampoom/android/core/preferences/AuthPreferences.kt index 5b3faeb..d4c0473 100644 --- a/app/src/main/java/com/sampoom/android/core/preferences/AuthPreferences.kt +++ b/app/src/main/java/com/sampoom/android/core/preferences/AuthPreferences.kt @@ -7,7 +7,7 @@ import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.sampoom.android.core.model.UserPosition -import com.sampoom.android.feature.auth.domain.model.User +import com.sampoom.android.feature.user.domain.model.User import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/java/com/sampoom/android/feature/auth/domain/AuthValidator.kt b/app/src/main/java/com/sampoom/android/core/util/AuthValidator.kt similarity index 98% rename from app/src/main/java/com/sampoom/android/feature/auth/domain/AuthValidator.kt rename to app/src/main/java/com/sampoom/android/core/util/AuthValidator.kt index b7c4516..e6019f6 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/domain/AuthValidator.kt +++ b/app/src/main/java/com/sampoom/android/core/util/AuthValidator.kt @@ -1,4 +1,4 @@ -package com.sampoom.android.feature.auth.domain +package com.sampoom.android.core.util import com.sampoom.android.R diff --git a/app/src/main/java/com/sampoom/android/feature/auth/data/mapper/AuthMappers.kt b/app/src/main/java/com/sampoom/android/feature/auth/data/mapper/AuthMappers.kt index 206707c..6b130cc 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/data/mapper/AuthMappers.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/data/mapper/AuthMappers.kt @@ -1,11 +1,10 @@ package com.sampoom.android.feature.auth.data.mapper import com.sampoom.android.core.model.UserPosition -import com.sampoom.android.feature.auth.data.remote.dto.GetProfileResponseDto import com.sampoom.android.feature.auth.data.remote.dto.GetVendorsResponseDto import com.sampoom.android.feature.auth.data.remote.dto.LoginResponseDto -import com.sampoom.android.feature.auth.domain.model.User import com.sampoom.android.feature.auth.domain.model.Vendor +import com.sampoom.android.feature.user.domain.model.User fun LoginResponseDto.toModel(): User = User( userId = userId, @@ -23,28 +22,6 @@ fun LoginResponseDto.toModel(): User = User( endedAt = null ) -fun GetProfileResponseDto.toModel(): User = User( - userId = userId, - userName = userName, - email = email, - role = role, - accessToken = "", - refreshToken = "", - expiresIn = 0L, - position = position.toUserPosition(), - workspace = workspace, - branch = branch, - agencyId = organizationId, - startedAt = startedAt, - endedAt = endedAt -) - -private fun String.toUserPosition(): UserPosition = try { - UserPosition.valueOf(this.uppercase()) -} catch (_: IllegalArgumentException) { - UserPosition.STAFF -} - fun GetVendorsResponseDto.toModel(): Vendor = Vendor( id = id, vendorCode = vendorCode, 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 fbbf1fb..d20d25d 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 @@ -2,22 +2,17 @@ package com.sampoom.android.feature.auth.data.remote.api import com.sampoom.android.core.model.ApiResponse import com.sampoom.android.core.model.ApiSuccessResponse +import com.sampoom.android.feature.auth.data.remote.dto.GetVendorsResponseDto import com.sampoom.android.feature.auth.data.remote.dto.LoginRequestDto -import com.sampoom.android.feature.auth.data.remote.dto.SignUpRequestDto -import com.sampoom.android.feature.auth.data.remote.dto.SignUpResponseDto import com.sampoom.android.feature.auth.data.remote.dto.LoginResponseDto import com.sampoom.android.feature.auth.data.remote.dto.RefreshRequestDto import com.sampoom.android.feature.auth.data.remote.dto.RefreshResponseDto -import com.sampoom.android.feature.auth.data.remote.dto.UpdateProfileRequestDto -import com.sampoom.android.feature.auth.data.remote.dto.UpdateProfileResponseDto -import com.sampoom.android.feature.auth.data.remote.dto.GetProfileResponseDto -import com.sampoom.android.feature.auth.data.remote.dto.GetVendorsResponseDto +import com.sampoom.android.feature.auth.data.remote.dto.SignUpRequestDto +import com.sampoom.android.feature.auth.data.remote.dto.SignUpResponseDto import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Headers -import retrofit2.http.PATCH import retrofit2.http.POST -import retrofit2.http.Query interface AuthApi { @POST("auth/signup") @@ -34,12 +29,6 @@ interface AuthApi { @Headers("X-No-Auth: true") suspend fun login(@Body body: LoginRequestDto): ApiResponse - @GET("user/profile") - suspend fun getProfile(@Query("workspace") workspace: String): ApiResponse - - @PATCH("user/profile") - suspend fun updateProfile(@Body body: UpdateProfileRequestDto): ApiResponse - @GET("site/vendors") suspend fun getVendors(): ApiResponse> } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/UpdateProfileRequestDto.kt b/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/UpdateProfileRequestDto.kt deleted file mode 100644 index 8db66be..0000000 --- a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/UpdateProfileRequestDto.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.sampoom.android.feature.auth.data.remote.dto - -data class UpdateProfileRequestDto( - val userName: String, - val position: String, - val workspace: String, - val branch: String -) diff --git a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/UpdateProfileResponseDto.kt b/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/UpdateProfileResponseDto.kt deleted file mode 100644 index 12e2ab6..0000000 --- a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/UpdateProfileResponseDto.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.sampoom.android.feature.auth.data.remote.dto - -data class UpdateProfileResponseDto( - val userId: Long, - val userName: String, - val position: String, - val workspace: String, - val branch: String -) diff --git a/app/src/main/java/com/sampoom/android/feature/auth/data/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/sampoom/android/feature/auth/data/repository/AuthRepositoryImpl.kt index cdc8642..f1e5106 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/data/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/data/repository/AuthRepositoryImpl.kt @@ -7,10 +7,9 @@ import com.sampoom.android.feature.auth.data.remote.api.AuthApi import com.sampoom.android.feature.auth.data.remote.dto.LoginRequestDto import com.sampoom.android.feature.auth.data.remote.dto.RefreshRequestDto import com.sampoom.android.feature.auth.data.remote.dto.SignUpRequestDto -import com.sampoom.android.feature.auth.domain.model.User import com.sampoom.android.feature.auth.domain.model.VendorList import com.sampoom.android.feature.auth.domain.repository.AuthRepository -import com.sampoom.android.feature.outbound.data.mapper.toModel +import com.sampoom.android.feature.user.domain.model.User import kotlinx.coroutines.delay import javax.inject.Inject @@ -63,29 +62,7 @@ class AuthRepositoryImpl @Inject constructor( val loginUser = loginDto.data.toModel() preferences.saveUser(loginUser) - - val profileUser = retry(times = 5, initialDelay = 300) { - getProfile("AGENCY").getOrThrow() - } - - val user = User( - userId = loginUser.userId, - userName = profileUser.userName, - email = profileUser.email, - role = profileUser.role, - accessToken = loginUser.accessToken, - refreshToken = loginUser.refreshToken, - expiresIn = loginUser.expiresIn, - position = profileUser.position, - workspace = profileUser.workspace, - branch = profileUser.branch, - agencyId = profileUser.agencyId, - startedAt = profileUser.startedAt, - endedAt = profileUser.endedAt - ) - - preferences.saveUser(user) - user + loginUser } } @@ -126,14 +103,6 @@ class AuthRepositoryImpl @Inject constructor( override suspend fun isSignedIn(): Boolean = preferences.hasToken() - override suspend fun getProfile(workspace: String): Result { - return runCatching { - val dto = api.getProfile(workspace) - if (!dto.success) throw Exception(dto.message) - dto.data.toModel() - } - } - override suspend fun getVendorList(): Result { return runCatching { val dto = api.getVendors() diff --git a/app/src/main/java/com/sampoom/android/feature/auth/domain/repository/AuthRepository.kt b/app/src/main/java/com/sampoom/android/feature/auth/domain/repository/AuthRepository.kt index 7d9e833..10e95e1 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/domain/repository/AuthRepository.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/domain/repository/AuthRepository.kt @@ -1,7 +1,7 @@ package com.sampoom.android.feature.auth.domain.repository -import com.sampoom.android.feature.auth.domain.model.User import com.sampoom.android.feature.auth.domain.model.VendorList +import com.sampoom.android.feature.user.domain.model.User interface AuthRepository { suspend fun signUp( @@ -18,6 +18,5 @@ interface AuthRepository { suspend fun refreshToken(): Result suspend fun clearTokens(): Result suspend fun isSignedIn(): Boolean - suspend fun getProfile(workspace: String): Result suspend fun getVendorList(): Result } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/GetStoredUserUseCase.kt b/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/GetStoredUserUseCase.kt deleted file mode 100644 index 9401624..0000000 --- a/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/GetStoredUserUseCase.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.sampoom.android.feature.auth.domain.usecase - -import com.sampoom.android.core.preferences.AuthPreferences -import com.sampoom.android.feature.auth.domain.model.User -import javax.inject.Inject - -class GetStoredUserUseCase @Inject constructor( - private val preferences: AuthPreferences -) { - suspend operator fun invoke(): User? = preferences.getStoredUser() -} - - - diff --git a/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/LoginUseCase.kt b/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/LoginUseCase.kt index 4c47220..af9b253 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/LoginUseCase.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/LoginUseCase.kt @@ -1,6 +1,6 @@ package com.sampoom.android.feature.auth.domain.usecase -import com.sampoom.android.feature.auth.domain.model.User +import com.sampoom.android.feature.user.domain.model.User import com.sampoom.android.feature.auth.domain.repository.AuthRepository import javax.inject.Inject diff --git a/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/SignUpUseCase.kt b/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/SignUpUseCase.kt index 9e5ec23..5dc6d2b 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/SignUpUseCase.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/SignUpUseCase.kt @@ -1,6 +1,6 @@ package com.sampoom.android.feature.auth.domain.usecase -import com.sampoom.android.feature.auth.domain.model.User +import com.sampoom.android.feature.user.domain.model.User import com.sampoom.android.feature.auth.domain.repository.AuthRepository import javax.inject.Inject 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 9e06634..baa24dc 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 @@ -6,9 +6,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sampoom.android.core.network.serverMessageOrNull import com.sampoom.android.core.util.GlobalMessageHandler -import com.sampoom.android.feature.auth.domain.AuthValidator -import com.sampoom.android.feature.auth.domain.ValidationResult +import com.sampoom.android.core.util.AuthValidator +import com.sampoom.android.core.util.ValidationResult import com.sampoom.android.feature.auth.domain.usecase.LoginUseCase +import com.sampoom.android.feature.user.domain.usecase.GetProfileUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -20,6 +21,7 @@ import javax.inject.Inject class LoginViewModel @Inject constructor( private val messageHandler: GlobalMessageHandler, private val singIn: LoginUseCase, + private val getProfile: GetProfileUseCase, private val application: Application ) : ViewModel() { @@ -86,9 +88,21 @@ class LoginViewModel @Inject constructor( _uiState.update { it.copy(loading = true, success = false) } singIn(s.email, s.password) .onSuccess { - _uiState.update { - it.copy(loading = false, success = true) - } + getProfile("AGENCY") + .onSuccess { + _uiState.update { + it.copy(loading = false, success = true) + } + } + .onFailure { throwable -> + val backendMessage = throwable.serverMessageOrNull() + val error = backendMessage ?: (throwable.message ?: errorLabel) + messageHandler.showMessage(message = error, isError = true) + + _uiState.update { + it.copy(loading = false, success = false) + } + } } .onFailure { throwable -> val backendMessage = throwable.serverMessageOrNull() @@ -98,6 +112,7 @@ class LoginViewModel @Inject constructor( _uiState.update { it.copy(loading = false, success = false) } + return@launch } Log.d(TAG, "submit: ${_uiState.value}") } diff --git a/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt b/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt index a448e7b..3c43a25 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt @@ -7,13 +7,14 @@ import androidx.lifecycle.viewModelScope import com.sampoom.android.R import com.sampoom.android.core.network.serverMessageOrNull import com.sampoom.android.core.util.GlobalMessageHandler -import com.sampoom.android.feature.auth.domain.AuthValidator -import com.sampoom.android.feature.auth.domain.AuthValidator.validateEmail -import com.sampoom.android.feature.auth.domain.AuthValidator.validatePassword -import com.sampoom.android.feature.auth.domain.AuthValidator.validatePasswordCheck -import com.sampoom.android.feature.auth.domain.ValidationResult +import com.sampoom.android.core.util.AuthValidator +import com.sampoom.android.core.util.AuthValidator.validateEmail +import com.sampoom.android.core.util.AuthValidator.validatePassword +import com.sampoom.android.core.util.AuthValidator.validatePasswordCheck +import com.sampoom.android.core.util.ValidationResult import com.sampoom.android.feature.auth.domain.usecase.GetVendorUseCase import com.sampoom.android.feature.auth.domain.usecase.SignUpUseCase +import com.sampoom.android.feature.user.domain.usecase.GetProfileUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -26,6 +27,7 @@ class SignUpViewModel @Inject constructor( private val messageHandler: GlobalMessageHandler, private val singUp: SignUpUseCase, private val getVendorUseCase: GetVendorUseCase, + private val getProfileUseCase: GetProfileUseCase, private val application: Application ) : ViewModel() { @@ -118,21 +120,21 @@ class SignUpViewModel @Inject constructor( } private fun validateEmail() { - val result = AuthValidator.validateEmail(_state.value.email) + val result = validateEmail(_state.value.email) _state.value = _state.value.copy( emailError = result.toErrorMessage() ) } private fun validatePassword() { - val result = AuthValidator.validatePassword(_state.value.password) + val result = validatePassword(_state.value.password) _state.value = _state.value.copy( passwordError = result.toErrorMessage() ) } private fun validatePasswordCheck() { - val result = AuthValidator.validatePasswordCheck(_state.value.password, _state.value.passwordCheck) + val result = validatePasswordCheck(_state.value.password, _state.value.passwordCheck) _state.value = _state.value.copy( passwordCheckError = result.toErrorMessage() ) @@ -167,9 +169,21 @@ class SignUpViewModel @Inject constructor( position = s.position!!.name ) .onSuccess { - _state.update { - it.copy(loading = false, success = true) - } + getProfileUseCase("AGENCY") + .onSuccess { + _state.update { + it.copy(loading = false, success = true) + } + } + .onFailure { throwable -> + val backendMessage = throwable.serverMessageOrNull() + val error = backendMessage ?: (throwable.message ?: errorLabel) + messageHandler.showMessage(message = error, isError = true) + + _state.update { + it.copy(loading = false, success = false) + } + } } .onFailure { throwable -> val backendMessage = throwable.serverMessageOrNull() diff --git a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardScreen.kt b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardScreen.kt index 4971cc0..8fc4a11 100644 --- a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardScreen.kt @@ -47,7 +47,6 @@ import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import com.sampoom.android.R -import com.sampoom.android.core.model.UserPosition import com.sampoom.android.core.ui.component.EmptyContent import com.sampoom.android.core.ui.component.ErrorContent import com.sampoom.android.core.ui.theme.FailRed @@ -56,15 +55,16 @@ import com.sampoom.android.core.ui.theme.SuccessGreen 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.auth.domain.model.User import com.sampoom.android.feature.dashboard.domain.model.Dashboard import com.sampoom.android.feature.dashboard.domain.model.WeeklySummary import com.sampoom.android.feature.order.domain.model.Order import com.sampoom.android.feature.order.ui.OrderItem +import com.sampoom.android.feature.user.domain.model.User @Composable fun DashboardScreen( paddingValues: PaddingValues, + onEmployeeClick: () -> Unit, onSettingClick: () -> Unit, onNavigateOrderDetail: (Order) -> Unit, onNavigationOrder: () -> Unit, @@ -75,20 +75,7 @@ fun DashboardScreen( val user by viewModel.user.collectAsStateWithLifecycle() val pullRefreshState = rememberPullToRefreshState() val orderListPaged = viewModel.orderListPaged.collectAsLazyPagingItems() - val isManager = when (user?.position) { - UserPosition.STAFF, - UserPosition.SENIOR_STAFF, - UserPosition.ASSISTANT_MANAGER, - UserPosition.MANAGER, - UserPosition.DEPUTY_GENERAL_MANAGER, - UserPosition.GENERAL_MANAGER, - UserPosition.DIRECTOR, - UserPosition.VICE_PRESIDENT, - UserPosition.PRESIDENT, - UserPosition.CHAIRMAN -> true - - else -> false - } + val isManager = user?.role == "ADMIN" LaunchedEffect(errorLabel) { viewModel.bindLabel(errorLabel) @@ -99,6 +86,7 @@ fun DashboardScreen( onRefresh = { viewModel.onEvent(DashboardUiEvent.LoadDashboard) orderListPaged.refresh() + viewModel.refreshUser() }, state = pullRefreshState, modifier = Modifier.fillMaxSize(), @@ -135,7 +123,7 @@ fun DashboardScreen( Row { if (isManager) { IconButton( - onClick = { } + onClick = { onEmployeeClick() } ) { Icon( painter = painterResource(R.drawable.employee), @@ -163,7 +151,13 @@ fun DashboardScreen( ) { item { TitleSection(user) } - item { ButtonSection(isManager, uiState.dashboard) } + item { + ButtonSection( + isManager = isManager, + dashboard = uiState.dashboard, + onEmployeeClick = { onEmployeeClick() } + ) + } item { OrderListSection( @@ -234,6 +228,7 @@ fun TitleSection( @Composable fun ButtonSection( + onEmployeeClick: () -> Unit, isManager: Boolean, dashboard: Dashboard? ) { @@ -254,7 +249,7 @@ fun ButtonSection( painterDescription = stringResource(R.string.dashboard_employee), text = 45.toString(), // TODO : API 연동 subText = stringResource(R.string.dashboard_employee), - onClick = { } + onClick = { onEmployeeClick() } ) } @@ -267,17 +262,16 @@ fun ButtonSection( painter = painterResource(R.drawable.car), painterDescription = stringResource(R.string.dashboard_parts_all), text = (dashboard?.totalParts ?: stringResource(R.string.common_slash)).toString(), - subText = stringResource(R.string.dashboard_parts_all), - onClick = { } + subText = stringResource(R.string.dashboard_parts_all) ) ButtonCard( modifier = Modifier.weight(1f), painter = painterResource(R.drawable.block), painterDescription = stringResource(R.string.dashboard_parts_out_of_stock), - text = (dashboard?.outOfStockParts ?: stringResource(R.string.common_slash)).toString(), - subText = stringResource(R.string.dashboard_parts_out_of_stock), - onClick = { } + text = (dashboard?.outOfStockParts + ?: stringResource(R.string.common_slash)).toString(), + subText = stringResource(R.string.dashboard_parts_out_of_stock) ) } @@ -289,18 +283,18 @@ fun ButtonSection( modifier = Modifier.weight(1f), painter = painterResource(R.drawable.warning), painterDescription = stringResource(R.string.dashboard_parts_low_stock), - text = (dashboard?.lowStockParts ?: stringResource(R.string.common_slash)).toString(), - subText = stringResource(R.string.dashboard_parts_low_stock), - onClick = { } + text = (dashboard?.lowStockParts + ?: stringResource(R.string.common_slash)).toString(), + subText = stringResource(R.string.dashboard_parts_low_stock) ) ButtonCard( modifier = Modifier.weight(1f), painter = painterResource(R.drawable.parts), painterDescription = stringResource(R.string.dashboard_parts_on_hand), - text = (dashboard?.totalQuantity ?: stringResource(R.string.common_slash)).toString(), - subText = stringResource(R.string.dashboard_parts_on_hand), - onClick = { } + text = (dashboard?.totalQuantity + ?: stringResource(R.string.common_slash)).toString(), + subText = stringResource(R.string.dashboard_parts_on_hand) ) } } @@ -313,7 +307,7 @@ fun ButtonCard( painterDescription: String, text: String, subText: String, - onClick: () -> Unit + onClick: () -> Unit = {} ) { Card( modifier = modifier @@ -484,7 +478,8 @@ fun WeeklySummarySection( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = (weeklySummary?.inStockParts ?: stringResource(R.string.common_slash)).toString(), + text = (weeklySummary?.inStockParts + ?: stringResource(R.string.common_slash)).toString(), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, color = SuccessGreen @@ -501,7 +496,8 @@ fun WeeklySummarySection( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - text = (weeklySummary?.outStockParts ?: stringResource(R.string.common_slash)).toString(), + text = (weeklySummary?.outStockParts + ?: stringResource(R.string.common_slash)).toString(), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, color = FailRed diff --git a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardViewModel.kt b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardViewModel.kt index f0e6e4e..afc5613 100644 --- a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardViewModel.kt @@ -6,14 +6,13 @@ import androidx.paging.PagingData import androidx.paging.cachedIn import com.sampoom.android.core.network.serverMessageOrNull import com.sampoom.android.core.util.GlobalMessageHandler -import com.sampoom.android.feature.auth.domain.model.User -import com.sampoom.android.feature.auth.domain.usecase.GetStoredUserUseCase import com.sampoom.android.feature.dashboard.domain.usecase.GetDashboardUseCase import com.sampoom.android.feature.dashboard.domain.usecase.WeeklySummaryUseCase import com.sampoom.android.feature.order.domain.model.Order import com.sampoom.android.feature.order.domain.usecase.GetOrderUseCase +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.domain.usecase.GetStoredUserUseCase import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -127,4 +126,10 @@ class DashboardViewModel @Inject constructor( } } } + + fun refreshUser() { + viewModelScope.launch { + _user.value = getStoredUserUseCase() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/setting/ui/SettingScreen.kt b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingScreen.kt similarity index 84% rename from app/src/main/java/com/sampoom/android/feature/setting/ui/SettingScreen.kt rename to app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingScreen.kt index 46b47e0..90f51a0 100644 --- a/app/src/main/java/com/sampoom/android/feature/setting/ui/SettingScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingScreen.kt @@ -1,4 +1,4 @@ -package com.sampoom.android.feature.setting.ui +package com.sampoom.android.feature.dashboard.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -16,6 +16,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -23,11 +24,13 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -36,14 +39,19 @@ import androidx.compose.ui.res.stringResource 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.backgroundCardColor import com.sampoom.android.core.ui.theme.disableColor import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.core.ui.theme.textSecondaryColor import com.sampoom.android.core.util.formatDate import com.sampoom.android.core.util.positionToKorean -import com.sampoom.android.feature.auth.domain.model.User +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.ui.UpdateProfileBottomSheet +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -52,12 +60,15 @@ fun SettingScreen( onNavigateBack: () -> Unit = {}, onLogoutClick: () -> Unit = {} ) { + val coroutineScope = rememberCoroutineScope() val errorLabel = stringResource(R.string.common_error) val nameLabel = stringResource(R.string.signup_title_name) val uiState by viewModel.uiState.collectAsStateWithLifecycle() val user by viewModel.user.collectAsStateWithLifecycle() val pullRefreshState = rememberPullToRefreshState() var showLogoutDialog by remember { mutableStateOf(false) } + var showEditProfileSheet by remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState(true) LaunchedEffect(errorLabel, nameLabel) { viewModel.bindLabel(errorLabel, nameLabel) @@ -72,7 +83,10 @@ fun SettingScreen( PullToRefreshBox( isRefreshing = false, - onRefresh = { viewModel.onEvent(SettingUiEvent.LoadProfile) }, + onRefresh = { + viewModel.onEvent(SettingUiEvent.LoadProfile) + viewModel.refreshUser() + }, state = pullRefreshState, modifier = Modifier.fillMaxSize(), indicator = { @@ -114,7 +128,7 @@ fun SettingScreen( } item { SettingSection( - onEditProfileClick = { }, + onEditProfileClick = { showEditProfileSheet = true }, onLogoutClick = { showLogoutDialog = true } ) } @@ -122,6 +136,31 @@ fun SettingScreen( } } + if (showEditProfileSheet && user != null) { + ModalBottomSheet( + onDismissRequest = { + coroutineScope.launch { + showEditProfileSheet = false + sheetState.hide() + } + }, + sheetState = sheetState + ) { + UpdateProfileBottomSheet( + user = user!!, + onProfileUpdated = { + viewModel.refreshUser() + }, + onDismiss = { + coroutineScope.launch { + showEditProfileSheet = false + sheetState.hide() + } + } + ) + } + } + if (showLogoutDialog) { AlertDialog( onDismissRequest = { showLogoutDialog = false }, @@ -247,7 +286,7 @@ private fun SettingSection( Text( text = stringResource(R.string.setting_logout), style = MaterialTheme.typography.titleMedium, - color = textColor() + color = FailRed ) Icon( diff --git a/app/src/main/java/com/sampoom/android/feature/setting/ui/SettingUiEvent.kt b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingUiEvent.kt similarity index 62% rename from app/src/main/java/com/sampoom/android/feature/setting/ui/SettingUiEvent.kt rename to app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingUiEvent.kt index 4845ecd..6cae162 100644 --- a/app/src/main/java/com/sampoom/android/feature/setting/ui/SettingUiEvent.kt +++ b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingUiEvent.kt @@ -1,7 +1,6 @@ -package com.sampoom.android.feature.setting.ui +package com.sampoom.android.feature.dashboard.ui sealed interface SettingUiEvent { object LoadProfile : SettingUiEvent data class NameChanged(val userName: String) : SettingUiEvent - object EditProfile : SettingUiEvent } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/setting/ui/SettingUiState.kt b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingUiState.kt similarity index 77% rename from app/src/main/java/com/sampoom/android/feature/setting/ui/SettingUiState.kt rename to app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingUiState.kt index caad258..9076f30 100644 --- a/app/src/main/java/com/sampoom/android/feature/setting/ui/SettingUiState.kt +++ b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingUiState.kt @@ -1,6 +1,6 @@ -package com.sampoom.android.feature.setting.ui +package com.sampoom.android.feature.dashboard.ui -import com.sampoom.android.feature.auth.domain.model.User +import com.sampoom.android.feature.user.domain.model.User data class SettingUiState( val profile: User? = null, diff --git a/app/src/main/java/com/sampoom/android/feature/setting/ui/SettingViewModel.kt b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingViewModel.kt similarity index 76% rename from app/src/main/java/com/sampoom/android/feature/setting/ui/SettingViewModel.kt rename to app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingViewModel.kt index 2cab7bc..4d61c17 100644 --- a/app/src/main/java/com/sampoom/android/feature/setting/ui/SettingViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingViewModel.kt @@ -1,13 +1,13 @@ -package com.sampoom.android.feature.setting.ui +package com.sampoom.android.feature.dashboard.ui import android.app.Application import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sampoom.android.core.util.GlobalMessageHandler -import com.sampoom.android.feature.auth.domain.AuthValidator -import com.sampoom.android.feature.auth.domain.ValidationResult -import com.sampoom.android.feature.auth.domain.model.User -import com.sampoom.android.feature.auth.domain.usecase.GetStoredUserUseCase +import com.sampoom.android.core.util.AuthValidator +import com.sampoom.android.core.util.ValidationResult +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.domain.usecase.GetStoredUserUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -48,12 +48,13 @@ class SettingViewModel @Inject constructor( fun onEvent(event: SettingUiEvent) { when (event) { - is SettingUiEvent.LoadProfile -> {} + is SettingUiEvent.LoadProfile -> { + refreshUser() + } is SettingUiEvent.NameChanged -> { _uiState.value = _uiState.value.copy(userName = event.userName) validateName() } - is SettingUiEvent.EditProfile -> editProfile() } } @@ -72,18 +73,13 @@ class SettingViewModel @Inject constructor( } } - private fun editProfile() = viewModelScope.launch { - validateName() - - if (!_uiState.value.isValid) return@launch - - val s = _uiState.value - _uiState.update { it.copy(loading = true) } - - // TODO : Edit Profile 연동 - } - fun clearSuccess() { _uiState.update { it.copy(profileChangeSuccess = false, logoutSuccess = false) } } + + fun refreshUser() { + viewModelScope.launch { + _user.value = getStoredUserUseCase() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/sample/data/local/database/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/data/local/database/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/sample/data/local/preferences/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/data/local/preferences/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/sample/data/mapper/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/data/mapper/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/sample/data/remote/api/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/data/remote/api/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/sample/data/remote/dto/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/data/remote/dto/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/sample/data/repository/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/data/repository/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/sample/di/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/di/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/sample/domain/model/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/domain/model/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/sample/domain/repository/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/domain/repository/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/sample/domain/usecase/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/domain/usecase/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/sample/ui/.gitkeep b/app/src/main/java/com/sampoom/android/feature/sample/ui/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/mapper/UserMappers.kt b/app/src/main/java/com/sampoom/android/feature/user/data/mapper/UserMappers.kt new file mode 100644 index 0000000..62ec635 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/mapper/UserMappers.kt @@ -0,0 +1,73 @@ +package com.sampoom.android.feature.user.data.mapper + +import com.sampoom.android.core.model.UserPosition +import com.sampoom.android.feature.user.data.remote.dto.EditEmployeeResponseDto +import com.sampoom.android.feature.user.data.remote.dto.EmployeeDto +import com.sampoom.android.feature.user.data.remote.dto.GetProfileResponseDto +import com.sampoom.android.feature.user.data.remote.dto.UpdateProfileResponseDto +import com.sampoom.android.feature.user.domain.model.Employee +import com.sampoom.android.feature.user.domain.model.User + +fun GetProfileResponseDto.toModel(): User = User( + userId = userId, + userName = userName, + email = email, + role = role, + accessToken = "", + refreshToken = "", + expiresIn = 0L, + position = position.toUserPosition(), + workspace = workspace, + branch = branch, + agencyId = organizationId, + startedAt = startedAt, + endedAt = endedAt +) + +private fun String.toUserPosition(): UserPosition = try { + UserPosition.valueOf(this.uppercase()) +} catch (_: IllegalArgumentException) { + UserPosition.STAFF +} + +fun UpdateProfileResponseDto.toModel(): User = User( + userId = userId, + userName = userName, + email = "", + role = "", + accessToken = "", + refreshToken = "", + expiresIn = 0L, + position = UserPosition.STAFF, + workspace = "", + branch = "", + agencyId = 0, + startedAt = null, + endedAt = null +) + +fun EditEmployeeResponseDto.toModel(): Employee = Employee( + userId = userId, + email = "", + role = "", + userName = userName, + workspace = workspace, + organizationId = 0, + branch = "", + position = position.toUserPosition(), + startedAt = null, + endedAt = null +) + +fun EmployeeDto.toModel(): Employee = Employee( + userId, + email, + role, + userName, + workspace, + organizationId, + branch, + position, + startedAt, + endedAt +) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/paging/EmployeePagingSource.kt b/app/src/main/java/com/sampoom/android/feature/user/data/paging/EmployeePagingSource.kt new file mode 100644 index 0000000..621f4ba --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/paging/EmployeePagingSource.kt @@ -0,0 +1,47 @@ +package com.sampoom.android.feature.user.data.paging + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.sampoom.android.core.preferences.AuthPreferences +import com.sampoom.android.feature.user.data.mapper.toModel +import com.sampoom.android.feature.user.data.remote.api.UserApi +import com.sampoom.android.feature.user.domain.model.Employee +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +class EmployeePagingSource @AssistedInject constructor( + private val api: UserApi, + private val authPreferences: AuthPreferences +) : PagingSource() { + + @AssistedFactory + interface Factory { + fun create(): EmployeePagingSource + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) + ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) + } + } + + override suspend fun load(params: LoadParams): LoadResult { + return try { + val agencyId = authPreferences.getStoredUser()?.agencyId ?: throw Exception() + val page = params.key ?: 0 + val pageSize = params.loadSize + val response = api.getEmployeeList("AGENCY", agencyId, page, pageSize) + val employee = response.data.users.map { it.toModel() } + val meta = response.data.meta + + LoadResult.Page( + data = employee, + prevKey = if (meta.hasPrevious) page - 1 else null, + nextKey = if (meta.hasNext) page + 1 else null + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/api/UserApi.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/api/UserApi.kt new file mode 100644 index 0000000..e54ee76 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/api/UserApi.kt @@ -0,0 +1,37 @@ +package com.sampoom.android.feature.user.data.remote.api + +import com.sampoom.android.core.model.ApiResponse +import com.sampoom.android.feature.user.data.remote.dto.GetProfileResponseDto +import com.sampoom.android.feature.user.data.remote.dto.EditEmployeeRequestDto +import com.sampoom.android.feature.user.data.remote.dto.EditEmployeeResponseDto +import com.sampoom.android.feature.user.data.remote.dto.EmployeeListDto +import com.sampoom.android.feature.user.data.remote.dto.UpdateProfileRequestDto +import com.sampoom.android.feature.user.data.remote.dto.UpdateProfileResponseDto +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.PATCH +import retrofit2.http.Path +import retrofit2.http.Query + +interface UserApi { + @GET("user/profile") + suspend fun getProfile(@Query("workspace") workspace: String): ApiResponse + + @PATCH("user/profile") + suspend fun updateProfile(@Body body: UpdateProfileRequestDto): ApiResponse + + @PATCH("user/profile/{userId}") + suspend fun editEmployee( + @Path("userId") userId: Long, + @Query("workspace") workspace: String, + @Body body: EditEmployeeRequestDto + ): ApiResponse + + @GET("user/info") + suspend fun getEmployeeList( + @Query("workspace") workspace: String, + @Query("organizationId") organizationId: Long, + @Query("page") page: Int = 0, + @Query("size") size: Int = 20 + ): ApiResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EditEmployeeRequestDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EditEmployeeRequestDto.kt new file mode 100644 index 0000000..07b2a36 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EditEmployeeRequestDto.kt @@ -0,0 +1,5 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class EditEmployeeRequestDto( + val position: String +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EditEmployeeResponseDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EditEmployeeResponseDto.kt new file mode 100644 index 0000000..700cdce --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EditEmployeeResponseDto.kt @@ -0,0 +1,8 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class EditEmployeeResponseDto( + val userId: Long, + val userName: String, + val workspace: String, + val position: String +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeDto.kt new file mode 100644 index 0000000..31d78fd --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeDto.kt @@ -0,0 +1,16 @@ +package com.sampoom.android.feature.user.data.remote.dto + +import com.sampoom.android.core.model.UserPosition + +data class EmployeeDto( + val userId: Long, + val email: String, + val role: String, + val userName: String, + val workspace: String, + val organizationId: Long, + val branch: String, + val position: UserPosition, + val startedAt: String?, + val endedAt: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeListDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeListDto.kt new file mode 100644 index 0000000..8ca47a8 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeListDto.kt @@ -0,0 +1,15 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class EmployeeListDto( + val users: List, + val meta: EmployeeMetaDto +) + +data class EmployeeMetaDto( + val currentPage: Int, + val totalPages: Int, + val totalElements: Int, + val size: Int, + val hasNext: Boolean, + val hasPrevious: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/GetProfileResponseDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/GetProfileResponseDto.kt similarity index 82% rename from app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/GetProfileResponseDto.kt rename to app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/GetProfileResponseDto.kt index aa46f7d..b54aaef 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/data/remote/dto/GetProfileResponseDto.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/GetProfileResponseDto.kt @@ -1,4 +1,4 @@ -package com.sampoom.android.feature.auth.data.remote.dto +package com.sampoom.android.feature.user.data.remote.dto data class GetProfileResponseDto( val userId: Long, @@ -11,4 +11,4 @@ data class GetProfileResponseDto( val organizationId: Long, val startedAt: String, val endedAt: String? -) +) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateProfileRequestDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateProfileRequestDto.kt new file mode 100644 index 0000000..d87dc5a --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateProfileRequestDto.kt @@ -0,0 +1,5 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class UpdateProfileRequestDto( + val userName: String +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateProfileResponseDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateProfileResponseDto.kt new file mode 100644 index 0000000..ba1b245 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateProfileResponseDto.kt @@ -0,0 +1,6 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class UpdateProfileResponseDto( + val userId: Long, + val userName: String +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt b/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt new file mode 100644 index 0000000..b629d67 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt @@ -0,0 +1,133 @@ +package com.sampoom.android.feature.user.data.repository + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import com.sampoom.android.core.preferences.AuthPreferences +import com.sampoom.android.core.util.retry +import com.sampoom.android.feature.user.data.mapper.toModel +import com.sampoom.android.feature.user.data.paging.EmployeePagingSource +import com.sampoom.android.feature.user.data.remote.api.UserApi +import com.sampoom.android.feature.user.data.remote.dto.EditEmployeeRequestDto +import com.sampoom.android.feature.user.data.remote.dto.UpdateProfileRequestDto +import com.sampoom.android.feature.user.domain.model.Employee +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.domain.repository.UserRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class UserRepositoryImpl @Inject constructor( + private val api: UserApi, + private val preferences: AuthPreferences, + private val pagingSourceFactory: EmployeePagingSource.Factory +) : UserRepository { + override suspend fun getStoredUser(): User? { + return preferences.getStoredUser() + } + + override suspend fun getProfile(workspace: String): Result { + return runCatching { + retry(times = 5, initialDelay = 300) { + val dto = api.getProfile(workspace) + if (!dto.success) throw Exception(dto.message) + val profileUser = dto.data.toModel() + val loginUser = preferences.getStoredUser() + + val completeUser = if (loginUser != null) { + User( + userId = profileUser.userId, + userName = profileUser.userName, + email = profileUser.email, + role = profileUser.role, + accessToken = loginUser.accessToken, // 저장된 토큰 + refreshToken = loginUser.refreshToken, // 저장된 토큰 + expiresIn = loginUser.expiresIn, // 저장된 토큰 + position = profileUser.position, + workspace = profileUser.workspace, + branch = profileUser.branch, + agencyId = profileUser.agencyId, + startedAt = profileUser.startedAt, + endedAt = profileUser.endedAt + ) + } else { + throw Exception() + } + + preferences.saveUser(completeUser) + completeUser + } + } + } + + override suspend fun updateProfile(user: User): Result { + return runCatching { + val requestDto = UpdateProfileRequestDto( + userName = user.userName + ) + val dto = api.updateProfile(requestDto) + if (!dto.success) throw Exception(dto.message) + val updatedProfile = dto.data.toModel() + val storedUser = preferences.getStoredUser() + val completeUser = if (storedUser != null) { + User( + userId = updatedProfile.userId, + userName = updatedProfile.userName, + email = user.email, + role = user.role, + accessToken = storedUser.accessToken, + refreshToken = storedUser.refreshToken, + expiresIn = storedUser.expiresIn, + position = user.position, + workspace = user.workspace, + branch = user.branch, + agencyId = user.agencyId, + startedAt = user.startedAt, + endedAt = user.endedAt + ) + } else throw Exception() + + preferences.saveUser(completeUser) + completeUser + } + } + + override fun getEmployeeList(): Flow> { + return Pager( + config = PagingConfig(pageSize = 20), + pagingSourceFactory = { pagingSourceFactory.create() } + ).flow + } + + override suspend fun editEmployee( + employee: Employee, + workspace: String + ): Result { + return runCatching { + val requestDto = EditEmployeeRequestDto( + position = employee.position.name + ) + val dto = api.editEmployee( + userId = employee.userId, + workspace = workspace, + body = requestDto + ) + if (!dto.success) throw Exception(dto.message) + + val updatedEmployee = dto.data.toModel() + val completeEmployee = Employee( + userId = updatedEmployee.userId, + email = employee.email, + role = employee.role, + userName = updatedEmployee.userName.takeIf { it.isNotBlank() } ?: employee.userName, + workspace = updatedEmployee.workspace.takeIf { it.isNotBlank() } ?: employee.workspace, + organizationId = employee.organizationId, + branch = employee.branch, + position = updatedEmployee.position, + startedAt = employee.startedAt, + endedAt = employee.endedAt + ) + + completeEmployee + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/di/UserModules.kt b/app/src/main/java/com/sampoom/android/feature/user/di/UserModules.kt new file mode 100644 index 0000000..3127f0f --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/di/UserModules.kt @@ -0,0 +1,26 @@ +package com.sampoom.android.feature.user.di + +import com.sampoom.android.feature.user.data.remote.api.UserApi +import com.sampoom.android.feature.user.data.repository.UserRepositoryImpl +import com.sampoom.android.feature.user.domain.repository.UserRepository +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class UserBinModule { + @Binds @Singleton + abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository +} + +@Module +@InstallIn(SingletonComponent::class) +object UserProvideModule { + @Provides @Singleton + fun provideUserApi(retrofit: Retrofit): UserApi = retrofit.create(UserApi::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/model/Employee.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/model/Employee.kt new file mode 100644 index 0000000..7c1a8b1 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/model/Employee.kt @@ -0,0 +1,16 @@ +package com.sampoom.android.feature.user.domain.model + +import com.sampoom.android.core.model.UserPosition + +data class Employee( + val userId: Long, + val email: String, + val role: String, + val userName: String, + val workspace: String, + val organizationId: Long, + val branch: String, + val position: UserPosition, + val startedAt: String?, + val endedAt: String? +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/model/EmployeeList.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/model/EmployeeList.kt new file mode 100644 index 0000000..45f8475 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/model/EmployeeList.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.feature.user.domain.model + +data class EmployeeList( + val items: List, + val totalCount: Int = items.size, + val isEmpty: Boolean = items.isEmpty() +) { + companion object Companion { + fun empty() = EmployeeList(emptyList()) + } +} diff --git a/app/src/main/java/com/sampoom/android/feature/auth/domain/model/User.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/model/User.kt similarity index 87% rename from app/src/main/java/com/sampoom/android/feature/auth/domain/model/User.kt rename to app/src/main/java/com/sampoom/android/feature/user/domain/model/User.kt index 984500a..00abdf8 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/domain/model/User.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/model/User.kt @@ -1,4 +1,4 @@ -package com.sampoom.android.feature.auth.domain.model +package com.sampoom.android.feature.user.domain.model import com.sampoom.android.core.model.UserPosition @@ -16,4 +16,4 @@ data class User( val agencyId: Long, val startedAt: String?, val endedAt: String? -) +) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/repository/UserRepository.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/repository/UserRepository.kt new file mode 100644 index 0000000..6d826b3 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/repository/UserRepository.kt @@ -0,0 +1,14 @@ +package com.sampoom.android.feature.user.domain.repository + +import androidx.paging.PagingData +import com.sampoom.android.feature.user.domain.model.Employee +import com.sampoom.android.feature.user.domain.model.User +import kotlinx.coroutines.flow.Flow + +interface UserRepository { + suspend fun getStoredUser(): User? + suspend fun getProfile(workspace: String): Result + suspend fun updateProfile(user: User): Result + fun getEmployeeList(): Flow> + suspend fun editEmployee(employee: Employee, workspace: String): Result +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/EditEmployeeUseCase.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/EditEmployeeUseCase.kt new file mode 100644 index 0000000..d81d82d --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/EditEmployeeUseCase.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.feature.user.domain.usecase + +import com.sampoom.android.feature.user.domain.model.Employee +import com.sampoom.android.feature.user.domain.repository.UserRepository +import javax.inject.Inject + +class EditEmployeeUseCase @Inject constructor( + private val repository: UserRepository +) { + suspend operator fun invoke(employee: Employee, workspace: String): Result = repository.editEmployee(employee, workspace) +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetEmployeeUseCase.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetEmployeeUseCase.kt new file mode 100644 index 0000000..542179e --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetEmployeeUseCase.kt @@ -0,0 +1,13 @@ +package com.sampoom.android.feature.user.domain.usecase + +import androidx.paging.PagingData +import com.sampoom.android.feature.user.domain.model.Employee +import com.sampoom.android.feature.user.domain.repository.UserRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class GetEmployeeUseCase @Inject constructor( + private val repository: UserRepository +) { + operator fun invoke(): Flow> = repository.getEmployeeList() +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetProfileUseCase.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetProfileUseCase.kt new file mode 100644 index 0000000..3b7e43c --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetProfileUseCase.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.feature.user.domain.usecase + +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.domain.repository.UserRepository +import javax.inject.Inject + +class GetProfileUseCase @Inject constructor( + private val repository: UserRepository +) { + suspend operator fun invoke(workspace: String): Result = repository.getProfile(workspace) +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetStoredUserUseCase.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetStoredUserUseCase.kt new file mode 100644 index 0000000..672956c --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetStoredUserUseCase.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.feature.user.domain.usecase + +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.domain.repository.UserRepository +import javax.inject.Inject + +class GetStoredUserUseCase @Inject constructor( + private val repository: UserRepository +) { + suspend operator fun invoke(): User? = repository.getStoredUser() +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/UpdateProfileUseCase.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/UpdateProfileUseCase.kt new file mode 100644 index 0000000..e3ed1e1 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/UpdateProfileUseCase.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.feature.user.domain.usecase + +import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.domain.repository.UserRepository +import javax.inject.Inject + +class UpdateProfileUseCase @Inject constructor( + private val repository: UserRepository +) { + suspend operator fun invoke(user: User): Result = repository.updateProfile(user) +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeBottomSheet.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeBottomSheet.kt new file mode 100644 index 0000000..0b578ec --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeBottomSheet.kt @@ -0,0 +1,113 @@ +package com.sampoom.android.feature.user.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +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.model.UserPosition +import com.sampoom.android.core.ui.component.CommonButton +import com.sampoom.android.core.ui.component.CommonTextField +import com.sampoom.android.core.util.positionToKorean +import com.sampoom.android.feature.user.domain.model.Employee + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EditEmployeeBottomSheet( + employee: Employee, + onDismiss: () -> Unit, + viewModel: EditEmployeeViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + var selectedPosition by rememberSaveable { mutableStateOf(employee.position) } + var positionMenuExpanded by remember { mutableStateOf(false) } + + val errorLabel = stringResource(R.string.common_error) + val editEmployeeLabel = stringResource(R.string.employee_edit_edited) + val deleteEmployeeLabel = stringResource(R.string.employee_edit_deleted) + + LaunchedEffect(employee) { + viewModel.onEvent(EditEmployeeUiEvent.Initialize(employee)) + selectedPosition = employee.position + } + + LaunchedEffect(errorLabel, editEmployeeLabel, deleteEmployeeLabel) { + viewModel.bindLabel(errorLabel, editEmployeeLabel, deleteEmployeeLabel) + } + + LaunchedEffect(uiState.isSuccess) { + if (uiState.isSuccess) { + viewModel.clearStatus() + onDismiss() + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 32.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + ExposedDropdownMenuBox( + expanded = positionMenuExpanded, + onExpandedChange = { positionMenuExpanded = it } + ) { + CommonTextField( + modifier = Modifier + .fillMaxWidth() + .menuAnchor( + type = ExposedDropdownMenuAnchorType.PrimaryNotEditable, + enabled = true + ), + readOnly = true, + value = positionToKorean(selectedPosition), + onValueChange = {}, + placeholder = stringResource(R.string.signup_placeholder_position), + singleLine = true + ) + ExposedDropdownMenu( + expanded = positionMenuExpanded, + onDismissRequest = { positionMenuExpanded = false } + ) { + UserPosition.entries.forEach { position -> + androidx.compose.material3.DropdownMenuItem( + text = { Text(positionToKorean(position)) }, + onClick = { + selectedPosition = position + positionMenuExpanded = false + } + ) + } + } + } + + Spacer(Modifier.height(8.dp)) + + CommonButton( + modifier = Modifier.fillMaxWidth(), + enabled = !uiState.isLoading && selectedPosition != employee.position, + onClick = { viewModel.onEvent(EditEmployeeUiEvent.EditEmployee(selectedPosition)) } + ) { + Text(stringResource(R.string.common_confirm)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeUiEvent.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeUiEvent.kt new file mode 100644 index 0000000..b8cf9e9 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeUiEvent.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.user.ui + +import com.sampoom.android.core.model.UserPosition +import com.sampoom.android.feature.user.domain.model.Employee + +interface EditEmployeeUiEvent { + data class Initialize(val employee: Employee) : EditEmployeeUiEvent + data class EditEmployee(val position: UserPosition) : EditEmployeeUiEvent + object Dismiss : EditEmployeeUiEvent +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeUiState.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeUiState.kt new file mode 100644 index 0000000..71a0597 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeUiState.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.user.ui + +import com.sampoom.android.feature.user.domain.model.Employee + +data class EditEmployeeUiState( + val employee: Employee? = null, + val isLoading: Boolean = false, + val error: String? = null, + val isSuccess: Boolean = false +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeViewModel.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeViewModel.kt new file mode 100644 index 0000000..a4a4df3 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeViewModel.kt @@ -0,0 +1,105 @@ +package com.sampoom.android.feature.user.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.sampoom.android.core.model.UserPosition +import com.sampoom.android.core.network.serverMessageOrNull +import com.sampoom.android.core.util.GlobalMessageHandler +import com.sampoom.android.feature.user.domain.usecase.EditEmployeeUseCase +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 EditEmployeeViewModel @Inject constructor( + private val messageHandler: GlobalMessageHandler, + private val editEmployeeUseCase: EditEmployeeUseCase +) : ViewModel() { + + private companion object { + private const val TAG = "EditEmployeeViewModel" + } + + private val _uiState = MutableStateFlow(EditEmployeeUiState()) + val uiState: StateFlow = _uiState + + private var errorLabel: String = "" + private var editEmployeeLabel: String = "" + private var deleteEmployeeLabel: String = "" + + fun bindLabel(error: String, editEmployee: String, deleteEmployee: String) { + errorLabel = error + editEmployeeLabel = editEmployee + deleteEmployeeLabel = deleteEmployee + } + + fun onEvent(event: EditEmployeeUiEvent) { + when (event) { + is EditEmployeeUiEvent.Initialize -> { + _uiState.update { + it.copy( + employee = event.employee, + isLoading = false, + isSuccess = false + ) + } + } + is EditEmployeeUiEvent.EditEmployee -> { + editEmployee(event.position) + } + is EditEmployeeUiEvent.Dismiss -> { + _uiState.update { + it.copy( + employee = null, + isLoading = false, + error = null + ) + } + } + } + } + + private fun editEmployee(newPosition: UserPosition) { + viewModelScope.launch { + val currentEmployee = _uiState.value.employee ?: run { + messageHandler.showMessage(message = errorLabel, isError = true) + return@launch + } + + val updateEmployee = currentEmployee.copy(position = newPosition) + _uiState.update { it.copy(isLoading = true, error = null) } + + editEmployeeUseCase(updateEmployee, "AGENCY") + .onSuccess { employee -> + _uiState.update { + it.copy( + employee = employee, + isLoading = false, + error = null, + isSuccess = true + ) + } + messageHandler.showMessage(message = editEmployeeLabel, isError = false) + } + .onFailure { throwable -> + val backendMessage = throwable.serverMessageOrNull() + val error = backendMessage ?: (throwable.message ?: errorLabel) + messageHandler.showMessage(message = error, isError = true) + + _uiState.update { + it.copy( + isLoading = false, + error = error + ) + } + } + } + } + + fun clearStatus() { + _uiState.update { it.copy(isSuccess = false, error = null) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt new file mode 100644 index 0000000..92d1ff6 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt @@ -0,0 +1,339 @@ +package com.sampoom.android.feature.user.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.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.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +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 androidx.paging.LoadState +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemKey +import com.sampoom.android.R +import com.sampoom.android.core.ui.component.ButtonSize +import com.sampoom.android.core.ui.component.ButtonVariant +import com.sampoom.android.core.ui.component.CommonButton +import com.sampoom.android.core.ui.component.EmptyContent +import com.sampoom.android.core.ui.component.ErrorContent +import com.sampoom.android.core.ui.theme.FailRed +import com.sampoom.android.core.ui.theme.backgroundCardColor +import com.sampoom.android.core.ui.theme.textColor +import com.sampoom.android.core.ui.theme.textSecondaryColor +import com.sampoom.android.core.util.formatDate +import com.sampoom.android.core.util.positionToKorean +import com.sampoom.android.feature.user.domain.model.Employee +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EmployeeListScreen( + onNavigateBack: () -> Unit = {}, + viewModel: EmployeeListViewModel = hiltViewModel() +) { + val coroutineScope = rememberCoroutineScope() + val errorLabel = stringResource(R.string.common_error) + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val employeeListPaged = viewModel.employeeListPaged.collectAsLazyPagingItems() + val pullRefreshState = rememberPullToRefreshState() + val listState = rememberSaveable(saver = LazyListState.Saver) { LazyListState() } + val sheetState = rememberModalBottomSheetState(true) + val selectedEmployee = uiState.selectedEmployee + + LaunchedEffect(errorLabel) { + viewModel.bindLabel(errorLabel) + } + + LaunchedEffect(Unit) { + employeeListPaged.refresh() + } + + LaunchedEffect(selectedEmployee) { + if (selectedEmployee != null && !sheetState.isVisible) { + sheetState.show() + } else if (selectedEmployee == null && sheetState.isVisible) { + sheetState.hide() + } + } + + PullToRefreshBox( + isRefreshing = false, + onRefresh = { employeeListPaged.refresh() }, + state = pullRefreshState, + modifier = Modifier.fillMaxSize(), + indicator = { + Indicator( + modifier = Modifier.align(Alignment.TopCenter), + isRefreshing = uiState.employeeLoading, + containerColor = MaterialTheme.colorScheme.primaryContainer, + color = MaterialTheme.colorScheme.onPrimaryContainer, + state = pullRefreshState + ) + } + ) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.employee_title)) }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + painter = painterResource(R.drawable.ic_arrow_back), + contentDescription = stringResource(R.string.nav_back) + ) + } + } + ) + } + ) { innerPadding -> + when (employeeListPaged.loadState.refresh) { + is LoadState.Loading -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + is LoadState.Error -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + contentAlignment = Alignment.Center + ) { + ErrorContent( + onRetry = { viewModel.onEvent(EmployeeListUiEvent.RetryEmployeeList) }, + modifier = Modifier.height(200.dp) + ) + } + } + + else -> { + if (employeeListPaged.loadState.refresh !is LoadState.Loading && employeeListPaged.itemCount == 0) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + EmptyContent( + message = stringResource(R.string.employee_empty_employee), + modifier = Modifier.height(200.dp) + ) + } + } else { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items( + count = employeeListPaged.itemCount, + key = employeeListPaged.itemKey { it.userId } + ) { index -> + val employee = employeeListPaged[index] + if (employee != null) { + EmployeeListItemCard( + employee = employee, + onDeleteClick = { + + }, + onEditClick = { + viewModel.onEvent(EmployeeListUiEvent.ShowBottomSheet(employee)) + } + ) + } + } + + // 로딩 상태 처리 + item { + when (employeeListPaged.loadState.append) { + is LoadState.Loading -> { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + is LoadState.Error -> { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.common_error), + color = FailRed + ) + } + } + + else -> {} + } + } + + item { Spacer(Modifier.height(100.dp)) } + } + } + } + } + } + } + + if (selectedEmployee != null) { + uiState.selectedEmployee?.let { selectedEmployee -> + ModalBottomSheet( + onDismissRequest = { + coroutineScope.launch { + viewModel.onEvent(EmployeeListUiEvent.DismissBottomSheet) + sheetState.hide() + } + }, + sheetState = sheetState + ) { + EditEmployeeBottomSheet( + employee = selectedEmployee, + onDismiss = { + coroutineScope.launch { + viewModel.onEvent(EmployeeListUiEvent.DismissBottomSheet) + sheetState.hide() + } + } + ) + } + } + } +} + +@Composable +private fun EmployeeListItemCard( + employee: Employee, + onDeleteClick: () -> Unit, + onEditClick: () -> Unit +) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = backgroundCardColor()) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = employee.userName, + color = textColor(), + style = MaterialTheme.typography.titleLarge + ) + Text( + text = positionToKorean(employee.position), + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Light + ) + + Spacer(Modifier.height(8.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(R.string.employee_email), + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = employee.email, + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(R.string.employee_startedAt), + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = formatDate(employee.startedAt ?: stringResource(R.string.common_slash)), + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium + ) + } + + Spacer(Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth() + ) { + CommonButton( + modifier = Modifier.weight(1F), + variant = ButtonVariant.Error, + size = ButtonSize.Large, + onClick = { onDeleteClick() } + ) { + Text(stringResource(R.string.employee_delete)) + } + Spacer(Modifier.width(8.dp)) + CommonButton( + modifier = Modifier.weight(1F), + variant = ButtonVariant.Primary, + size = ButtonSize.Large, + onClick = { onEditClick() } + ) { + Text(stringResource(R.string.employee_edit)) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiEvent.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiEvent.kt new file mode 100644 index 0000000..eabb110 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiEvent.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.user.ui + +import com.sampoom.android.feature.user.domain.model.Employee + +interface EmployeeListUiEvent { + object LoadEmployeeList : EmployeeListUiEvent + object RetryEmployeeList : EmployeeListUiEvent + data class ShowBottomSheet(val employee: Employee) : EmployeeListUiEvent + object DismissBottomSheet : EmployeeListUiEvent +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiState.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiState.kt new file mode 100644 index 0000000..648efbf --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiState.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.user.ui + +import com.sampoom.android.feature.user.domain.model.Employee + +data class EmployeeListUiState( + val employeeList: List = emptyList(), + val employeeLoading: Boolean = false, + val employeeError: String? = null, + val selectedEmployee: Employee? = null +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListViewModel.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListViewModel.kt new file mode 100644 index 0000000..0383392 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListViewModel.kt @@ -0,0 +1,47 @@ +package com.sampoom.android.feature.user.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn +import com.sampoom.android.core.util.GlobalMessageHandler +import com.sampoom.android.feature.user.domain.model.Employee +import com.sampoom.android.feature.user.domain.usecase.GetEmployeeUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +class EmployeeListViewModel @Inject constructor( + private val messageHandler: GlobalMessageHandler, + private val getEmployeeUseCase: GetEmployeeUseCase +) : ViewModel() { + + private companion object { + private const val TAG = "EmployeeListViewModel" + } + + private val _uiState = MutableStateFlow(EmployeeListUiState()) + val uiState: StateFlow = _uiState + + private var errorLabel: String = "" + + fun bindLabel(error: String) { + errorLabel = error + } + + val employeeListPaged : Flow> = getEmployeeUseCase() + .cachedIn(viewModelScope) + + fun onEvent(event: EmployeeListUiEvent) { + when (event) { + is EmployeeListUiEvent.LoadEmployeeList -> {} + is EmployeeListUiEvent.RetryEmployeeList -> {} + is EmployeeListUiEvent.ShowBottomSheet -> _uiState.update { it.copy(selectedEmployee = event.employee) } + is EmployeeListUiEvent.DismissBottomSheet -> _uiState.update { it.copy(selectedEmployee = null) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileBottomSheet.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileBottomSheet.kt new file mode 100644 index 0000000..1c836f2 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileBottomSheet.kt @@ -0,0 +1,84 @@ +package com.sampoom.android.feature.user.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +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 +import com.sampoom.android.feature.user.domain.model.User + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun UpdateProfileBottomSheet( + user: User, + onDismiss: () -> Unit, + onProfileUpdated: (User) -> Unit = {}, + viewModel: UpdateProfileViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + var userName by rememberSaveable { mutableStateOf(user.userName) } + + val errorLabel = stringResource(R.string.common_error) + val updateProfileLabel = stringResource(R.string.setting_edit_profile_edited) + + LaunchedEffect(user) { + viewModel.onEvent(UpdateProfileUiEvent.Initialize(user)) + userName = user.userName + } + + LaunchedEffect(errorLabel, updateProfileLabel) { + viewModel.bindLabel(errorLabel, updateProfileLabel) + } + + LaunchedEffect(uiState.isSuccess) { + if (uiState.isSuccess) { + val updatedUser = uiState.user + if (updatedUser != null) onProfileUpdated(updatedUser) + viewModel.clearStatus() + onDismiss() + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 32.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + CommonTextField( + modifier = Modifier.fillMaxWidth(), + value = userName, + onValueChange = { userName = it }, + placeholder = stringResource(R.string.setting_edit_profile_placeholder_username), + singleLine = true + ) + + Spacer(Modifier.height(8.dp)) + + CommonButton( + modifier = Modifier.fillMaxWidth(), + enabled = !uiState.isLoading && userName.isNotBlank() && userName != user.userName, + onClick = { viewModel.onEvent(UpdateProfileUiEvent.UpdateProfile(userName)) } + ) { + Text(stringResource(R.string.setting_edit_profile)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileUiEvent.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileUiEvent.kt new file mode 100644 index 0000000..d58c24e --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileUiEvent.kt @@ -0,0 +1,9 @@ +package com.sampoom.android.feature.user.ui + +import com.sampoom.android.feature.user.domain.model.User + +interface UpdateProfileUiEvent { + data class Initialize(val user: User) : UpdateProfileUiEvent + data class UpdateProfile(val userName: String) : UpdateProfileUiEvent + object Dismiss : UpdateProfileUiEvent +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileUiState.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileUiState.kt new file mode 100644 index 0000000..93aa73f --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileUiState.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.user.ui + +import com.sampoom.android.feature.user.domain.model.User + +data class UpdateProfileUiState( + val user: User? = null, + val isLoading: Boolean = false, + val error: String? = null, + val isSuccess: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileViewModel.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileViewModel.kt new file mode 100644 index 0000000..83d91a1 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateProfileViewModel.kt @@ -0,0 +1,100 @@ +package com.sampoom.android.feature.user.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.sampoom.android.core.network.serverMessageOrNull +import com.sampoom.android.core.util.GlobalMessageHandler +import com.sampoom.android.feature.user.domain.usecase.UpdateProfileUseCase +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 UpdateProfileViewModel @Inject constructor( + private val messageHandler: GlobalMessageHandler, + private val updateProfileUseCase: UpdateProfileUseCase +) : ViewModel() { + + private companion object Companion { + private const val TAG = "UpdateProfileViewModel" + } + + private val _uiState = MutableStateFlow(UpdateProfileUiState()) + val uiState: StateFlow = _uiState + + private var errorLabel: String = "" + private var updateProfileLabel: String = "" + + fun bindLabel(error: String, updateProfile: String) { + errorLabel = error + updateProfileLabel = updateProfile + } + + fun onEvent(event: UpdateProfileUiEvent) { + when (event) { + is UpdateProfileUiEvent.Initialize -> { + _uiState.update { + it.copy( + user = event.user, + isLoading = false, + isSuccess = false + ) + } + } + is UpdateProfileUiEvent.UpdateProfile -> { + updateProfile(event.userName) + } + is UpdateProfileUiEvent.Dismiss -> { + _uiState.update { + it.copy( + user = null + ) + } + } + } + } + + private fun updateProfile(newUserName: String) { + viewModelScope.launch { + val currentUser = _uiState.value.user ?: run { + messageHandler.showMessage(message = errorLabel, isError = true) + return@launch + } + + val updatedUser = currentUser.copy(userName = newUserName) + _uiState.update { it.copy(isLoading = true, error = null) } + + updateProfileUseCase(updatedUser) + .onSuccess { user -> + _uiState.update { + it.copy( + user = user, + isLoading = false, + error = null, + isSuccess = true + ) + } + messageHandler.showMessage(message = updateProfileLabel, isError = false) + } + .onFailure { throwable -> + val backendMessage = throwable.serverMessageOrNull() + val error = backendMessage ?: (throwable.message ?: errorLabel) + messageHandler.showMessage(message = error, isError = true) + + _uiState.update { + it.copy( + isLoading = false, + error = error + ) + } + } + } + } + + fun clearStatus() { + _uiState.update { it.copy(isSuccess = false, error = null) } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1001f02..0f18d62 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -114,6 +114,16 @@ 입고 처리되었습니다 총 가격 + + 직원관리 + 직원이 없습니다. + 이메일 + 가입일 + 삭제 + 수정 + 직원 수정이 완료되었습니다. + 직원 삭제가 완료되었습니다. + 대기중 주문확인 @@ -127,6 +137,8 @@ 설정 프로필 수정 + 이름 입력 + 프로필 수정이 완료되었습니다. 로그아웃 로그아웃 하시겠습니까? diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a1a524e..653678d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,6 +19,7 @@ materialIconsCore = "1.7.8" navigationCompose = "2.9.5" retrofitVersion = "3.0.0" paging = "3.3.6" +material3 = "1.4.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -51,6 +52,7 @@ androidx-paging-runtime = { group = "androidx.paging", name = "paging-runtime", androidx-paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "paging" } logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofitVersion" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From 592da0ea249cdee076ac6c0edcc37e6f62cae6d3 Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Sat, 8 Nov 2025 04:35:57 +0900 Subject: [PATCH 2/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 --- app/build.gradle.kts | 1 - .../com/sampoom/android/feature/user/ui/EmployeeListScreen.kt | 2 +- gradle/libs.versions.toml | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4bf2b53..7d05d41 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -69,7 +69,6 @@ kotlin { dependencies { // hilt implementation(libs.hilt.android) - implementation(libs.androidx.material3) ksp(libs.hilt.android.compiler) implementation(libs.androidx.hilt.lifecycle.viewmodel.compose) implementation(libs.androidx.hilt.navigation.compose) diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt index 92d1ff6..204d2fb 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt @@ -140,7 +140,7 @@ fun EmployeeListScreen( contentAlignment = Alignment.Center ) { ErrorContent( - onRetry = { viewModel.onEvent(EmployeeListUiEvent.RetryEmployeeList) }, + onRetry = { employeeListPaged.refresh() }, modifier = Modifier.height(200.dp) ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 653678d..a1a524e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,6 @@ materialIconsCore = "1.7.8" navigationCompose = "2.9.5" retrofitVersion = "3.0.0" paging = "3.3.6" -material3 = "1.4.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -52,7 +51,6 @@ androidx-paging-runtime = { group = "androidx.paging", name = "paging-runtime", androidx-paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "paging" } logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofitVersion" } -androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From 96205fb09102fb9f9e11488729a2b6a612f335ff Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Sat, 8 Nov 2025 21:34:44 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[FEAT]=20=EB=8C=80=EC=8B=9C=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=EC=A7=81=EC=9B=90=20=EC=88=98=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/dashboard/ui/DashboardScreen.kt | 7 +++- .../feature/dashboard/ui/DashboardUiState.kt | 3 ++ .../dashboard/ui/DashboardViewModel.kt | 37 ++++++++++++++++++- .../data/repository/UserRepositoryImpl.kt | 18 +++++++++ .../user/domain/repository/UserRepository.kt | 1 + .../domain/usecase/GetEmployeeCountUseCase.kt | 10 +++++ 6 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetEmployeeCountUseCase.kt diff --git a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardScreen.kt b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardScreen.kt index 8fc4a11..6586893 100644 --- a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardScreen.kt @@ -1,5 +1,6 @@ package com.sampoom.android.feature.dashboard.ui +import android.R.attr.onClick import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -155,6 +156,7 @@ fun DashboardScreen( ButtonSection( isManager = isManager, dashboard = uiState.dashboard, + employeeCount = uiState.employeeCount, onEmployeeClick = { onEmployeeClick() } ) } @@ -230,7 +232,8 @@ fun TitleSection( fun ButtonSection( onEmployeeClick: () -> Unit, isManager: Boolean, - dashboard: Dashboard? + dashboard: Dashboard?, + employeeCount: Int? ) { Column( modifier = Modifier @@ -247,7 +250,7 @@ fun ButtonSection( ), painter = painterResource(R.drawable.employee), painterDescription = stringResource(R.string.dashboard_employee), - text = 45.toString(), // TODO : API 연동 + text = employeeCount?.toString() ?: stringResource(R.string.common_slash), subText = stringResource(R.string.dashboard_employee), onClick = { onEmployeeClick() } ) diff --git a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardUiState.kt b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardUiState.kt index 4986b22..cd5733a 100644 --- a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardUiState.kt +++ b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardUiState.kt @@ -12,4 +12,7 @@ data class DashboardUiState( val dashboardError: String? = null, val weeklySummaryLoading: Boolean = false, val weeklySummaryError: String? = null, + val employeeCount: Int? = null, + val employeeCountLoading: Boolean = false, + val employeeCountError: String? = null ) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardViewModel.kt b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardViewModel.kt index afc5613..eb463b6 100644 --- a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardViewModel.kt @@ -11,6 +11,8 @@ import com.sampoom.android.feature.dashboard.domain.usecase.WeeklySummaryUseCase import com.sampoom.android.feature.order.domain.model.Order import com.sampoom.android.feature.order.domain.usecase.GetOrderUseCase import com.sampoom.android.feature.user.domain.model.User +import com.sampoom.android.feature.user.domain.usecase.GetEmployeeCountUseCase +import com.sampoom.android.feature.user.domain.usecase.GetEmployeeUseCase import com.sampoom.android.feature.user.domain.usecase.GetStoredUserUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow @@ -26,7 +28,8 @@ class DashboardViewModel @Inject constructor( private val getOrderListUseCase: GetOrderUseCase, private val getStoredUserUseCase: GetStoredUserUseCase, private val getDashboardUseCase: GetDashboardUseCase, - private val getWeeklySummaryUseCase: WeeklySummaryUseCase + private val getWeeklySummaryUseCase: WeeklySummaryUseCase, + private val getEmployeeCountUseCase: GetEmployeeCountUseCase ): ViewModel() { private companion object { @@ -51,6 +54,7 @@ class DashboardViewModel @Inject constructor( init { loadDashboard() loadWeeklySummary() + loadEmployeeCount() viewModelScope.launch { _user.value = getStoredUserUseCase() } @@ -61,10 +65,12 @@ class DashboardViewModel @Inject constructor( is DashboardUiEvent.LoadDashboard -> { loadDashboard() loadWeeklySummary() + loadEmployeeCount() } is DashboardUiEvent.RetryDashboard -> { loadDashboard() loadWeeklySummary() + loadEmployeeCount() } } } @@ -127,6 +133,35 @@ class DashboardViewModel @Inject constructor( } } + private fun loadEmployeeCount() { + viewModelScope.launch { + _uiState.update { it.copy(employeeCountLoading = true, employeeCountError = null) } + + getEmployeeCountUseCase() + .onSuccess { count -> + _uiState.update { + it.copy( + employeeCount = count, + employeeCountLoading = false, + employeeCountError = null + ) + } + } + .onFailure { throwable -> + val backendMessage = throwable.serverMessageOrNull() + val error = backendMessage ?: (throwable.message ?: errorLabel) + messageHandler.showMessage(message = error, isError = true) + + _uiState.update { + it.copy( + weeklySummaryLoading = false, + weeklySummaryError = error + ) + } + } + } + } + fun refreshUser() { viewModelScope.launch { _user.value = getStoredUserUseCase() diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt b/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt index b629d67..0f1dfb5 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt @@ -130,4 +130,22 @@ class UserRepositoryImpl @Inject constructor( completeEmployee } } + + override suspend fun getEmployeeCount(): Result { + return runCatching { + val user = preferences.getStoredUser() ?: throw Exception() + val workspace = user.workspace + val organizationId = user.agencyId + + val dto = api.getEmployeeList( + workspace = workspace, + organizationId = organizationId, + page = 0, + size = 1 + ) + + if (!dto.success) throw Exception(dto.message) + dto.data.meta.totalElements + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/repository/UserRepository.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/repository/UserRepository.kt index 6d826b3..52c64c3 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/domain/repository/UserRepository.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/repository/UserRepository.kt @@ -11,4 +11,5 @@ interface UserRepository { suspend fun updateProfile(user: User): Result fun getEmployeeList(): Flow> suspend fun editEmployee(employee: Employee, workspace: String): Result + suspend fun getEmployeeCount(): Result } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetEmployeeCountUseCase.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetEmployeeCountUseCase.kt new file mode 100644 index 0000000..c8bf5ed --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/GetEmployeeCountUseCase.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.user.domain.usecase + +import com.sampoom.android.feature.user.domain.repository.UserRepository +import javax.inject.Inject + +class GetEmployeeCountUseCase @Inject constructor( + private val repository: UserRepository +) { + suspend operator fun invoke(): Result = repository.getEmployeeCount() +} \ No newline at end of file From 9f32aafcf1a10d929228cf57dd366d715472359d Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Sun, 9 Nov 2025 03:21:43 +0900 Subject: [PATCH 4/6] =?UTF-8?q?[FEAT]=20=EC=A7=81=EC=9B=90=20=EC=9E=AC?= =?UTF-8?q?=EC=A7=81=20=EC=83=81=ED=83=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/core/model/EmployeeStatus.kt | 7 ++ .../android/core/ui/component/CommonButton.kt | 11 +- .../core/util/EmployeeStatusToKorean.kt | 10 ++ .../android/feature/cart/ui/CartListScreen.kt | 1 + .../feature/outbound/ui/OutboundListScreen.kt | 7 +- .../feature/part/ui/PartDetailBottomSheet.kt | 4 +- .../android/feature/part/ui/PartListScreen.kt | 6 +- .../android/feature/part/ui/PartScreen.kt | 8 +- .../feature/user/data/mapper/UserMappers.kt | 26 +++- .../feature/user/data/remote/api/UserApi.kt | 9 ++ .../user/data/remote/dto/EmployeeDto.kt | 2 + .../dto/UpdateEmployeeStatusRequestDto.kt | 5 + .../dto/UpdateEmployeeStatusResponseDto.kt | 8 ++ .../data/repository/UserRepositoryImpl.kt | 37 ++++++ .../feature/user/domain/model/Employee.kt | 2 + .../user/domain/repository/UserRepository.kt | 1 + .../usecase/UpdateEmployeeStatusUseCase.kt | 11 ++ .../user/ui/EditEmployeeBottomSheet.kt | 10 +- .../feature/user/ui/EditEmployeeViewModel.kt | 4 +- .../user/ui/EmployeeBottomSheetType.kt | 6 + .../feature/user/ui/EmployeeListScreen.kt | 95 +++++++++------ .../feature/user/ui/EmployeeListUiEvent.kt | 3 +- .../feature/user/ui/EmployeeListUiState.kt | 3 +- .../feature/user/ui/EmployeeListViewModel.kt | 3 +- .../ui/UpdateEmployeeStatusBottomSheet.kt | 115 ++++++++++++++++++ .../user/ui/UpdateEmployeeStatusUiEvent.kt | 10 ++ .../user/ui/UpdateEmployeeStatusUiState.kt | 10 ++ .../user/ui/UpdateEmployeeStatusViewModel.kt | 103 ++++++++++++++++ app/src/main/res/values/strings.xml | 7 +- 29 files changed, 465 insertions(+), 59 deletions(-) create mode 100644 app/src/main/java/com/sampoom/android/core/model/EmployeeStatus.kt create mode 100644 app/src/main/java/com/sampoom/android/core/util/EmployeeStatusToKorean.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateEmployeeStatusRequestDto.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateEmployeeStatusResponseDto.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/domain/usecase/UpdateEmployeeStatusUseCase.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeBottomSheetType.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusBottomSheet.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusUiEvent.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusUiState.kt create mode 100644 app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusViewModel.kt diff --git a/app/src/main/java/com/sampoom/android/core/model/EmployeeStatus.kt b/app/src/main/java/com/sampoom/android/core/model/EmployeeStatus.kt new file mode 100644 index 0000000..1fb518d --- /dev/null +++ b/app/src/main/java/com/sampoom/android/core/model/EmployeeStatus.kt @@ -0,0 +1,7 @@ +package com.sampoom.android.core.model + +enum class EmployeeStatus { + ACTIVE, // 재직 + LEAVE, // 휴직 + RETIRED // 퇴직 +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt b/app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt index ce4ed82..5a4d7ea 100644 --- a/app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt +++ b/app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.sampoom.android.core.ui.theme.FailRed +import com.sampoom.android.core.ui.theme.Main300 import com.sampoom.android.core.ui.theme.Main500 import com.sampoom.android.core.ui.theme.White import com.sampoom.android.core.ui.theme.disableColor @@ -81,15 +82,15 @@ fun CommonButton( // Light/secondary (tonal) filled button ButtonVariant.Secondary -> { - FilledTonalButton( + OutlinedButton( onClick = onClick, enabled = enabled, shape = shape, modifier = modifier.height(height), - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = Main500, - contentColor = White, - disabledContainerColor = disableColor(), + border = BorderStroke(1.dp, Main500), + colors = ButtonDefaults.outlinedButtonColors( + containerColor = Main300.copy(alpha = 0.3f), + contentColor = Main500, disabledContentColor = textSecondaryColor() ) ) { diff --git a/app/src/main/java/com/sampoom/android/core/util/EmployeeStatusToKorean.kt b/app/src/main/java/com/sampoom/android/core/util/EmployeeStatusToKorean.kt new file mode 100644 index 0000000..6b41bc1 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/core/util/EmployeeStatusToKorean.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.core.util + +import com.sampoom.android.core.model.EmployeeStatus + +fun employeeStatusToKorean(status: EmployeeStatus?): String = when (status) { + EmployeeStatus.ACTIVE -> "재직" + EmployeeStatus.LEAVE -> "휴직" + EmployeeStatus.RETIRED -> "퇴직" + else -> "-" +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListScreen.kt b/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListScreen.kt index d17be0e..dce9cd5 100644 --- a/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp diff --git a/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListScreen.kt b/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListScreen.kt index dabc090..ca3f83d 100644 --- a/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -96,7 +97,9 @@ fun OutboundListScreen( ) } ) { - Column(Modifier.fillMaxSize().padding(paddingValues)) { + Column(Modifier + .fillMaxSize() + .padding(paddingValues)) { Row( modifier = Modifier .fillMaxWidth() @@ -198,7 +201,7 @@ fun OutboundListScreen( .align(Alignment.BottomEnd) .padding(16.dp) .padding(end = 72.dp), - variant = ButtonVariant.Error, + variant = ButtonVariant.Secondary, size = ButtonSize.Large, onClick = { showConfirmDialog = true } ) { Text("${formatWon(uiState.totalCost)} ${stringResource(R.string.outbound_order_parts)}") } diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartDetailBottomSheet.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartDetailBottomSheet.kt index 2afbcfa..561fa71 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/ui/PartDetailBottomSheet.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartDetailBottomSheet.kt @@ -1,5 +1,6 @@ package com.sampoom.android.feature.part.ui +import android.R.attr.onClick import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -24,6 +25,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -203,7 +205,7 @@ fun PartDetailBottomSheet( Row(modifier = Modifier.fillMaxWidth()) { CommonButton( modifier = Modifier.weight(1F), - variant = ButtonVariant.Error, + variant = ButtonVariant.Secondary, size = ButtonSize.Large, leadingIcon = { Icon( diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt index cadb1ae..44a690d 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt @@ -5,10 +5,12 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Card @@ -252,10 +254,12 @@ private fun PartListItemCard( ) } + Spacer(Modifier.width(8.dp)) + Icon( painterResource(R.drawable.chevron_right), contentDescription = stringResource(R.string.common_detail), - tint = disableColor() + tint = textSecondaryColor() ) } } 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 2791fed..47bebf1 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,5 +1,6 @@ package com.sampoom.android.feature.part.ui +import android.widget.Space import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -11,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -519,7 +521,7 @@ private fun PartItemCard( Icon( painterResource(R.drawable.chevron_right), contentDescription = stringResource(R.string.common_detail), - tint = disableColor() + tint = textSecondaryColor() ) } } @@ -644,10 +646,12 @@ private fun SearchPartItem( ) } + Spacer(Modifier.width(8.dp)) + Icon( painterResource(R.drawable.chevron_right), contentDescription = stringResource(R.string.common_detail), - tint = disableColor() + tint = textSecondaryColor() ) } } diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/mapper/UserMappers.kt b/app/src/main/java/com/sampoom/android/feature/user/data/mapper/UserMappers.kt index 62ec635..664d280 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/data/mapper/UserMappers.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/data/mapper/UserMappers.kt @@ -1,9 +1,11 @@ package com.sampoom.android.feature.user.data.mapper +import com.sampoom.android.core.model.EmployeeStatus import com.sampoom.android.core.model.UserPosition import com.sampoom.android.feature.user.data.remote.dto.EditEmployeeResponseDto import com.sampoom.android.feature.user.data.remote.dto.EmployeeDto import com.sampoom.android.feature.user.data.remote.dto.GetProfileResponseDto +import com.sampoom.android.feature.user.data.remote.dto.UpdateEmployeeStatusResponseDto import com.sampoom.android.feature.user.data.remote.dto.UpdateProfileResponseDto import com.sampoom.android.feature.user.domain.model.Employee import com.sampoom.android.feature.user.domain.model.User @@ -55,6 +57,21 @@ fun EditEmployeeResponseDto.toModel(): Employee = Employee( organizationId = 0, branch = "", position = position.toUserPosition(), + employeeStatus = EmployeeStatus.ACTIVE, + startedAt = null, + endedAt = null +) + +fun UpdateEmployeeStatusResponseDto.toModel(): Employee = Employee( + userId = userId, + email = "", + role = "", + userName = userName, + workspace = workspace, + organizationId = 0, + branch = "", + position = UserPosition.STAFF, + employeeStatus = employeeStatus.toEmployeeStatus(), startedAt = null, endedAt = null ) @@ -68,6 +85,13 @@ fun EmployeeDto.toModel(): Employee = Employee( organizationId, branch, position, + employeeStatus ?: EmployeeStatus.ACTIVE, startedAt, endedAt -) \ No newline at end of file +) + +private fun String.toEmployeeStatus(): EmployeeStatus = try { + EmployeeStatus.valueOf(this.uppercase()) +} catch (_: IllegalArgumentException) { + EmployeeStatus.ACTIVE +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/api/UserApi.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/api/UserApi.kt index e54ee76..c815bfa 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/data/remote/api/UserApi.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/api/UserApi.kt @@ -5,6 +5,8 @@ import com.sampoom.android.feature.user.data.remote.dto.GetProfileResponseDto import com.sampoom.android.feature.user.data.remote.dto.EditEmployeeRequestDto import com.sampoom.android.feature.user.data.remote.dto.EditEmployeeResponseDto import com.sampoom.android.feature.user.data.remote.dto.EmployeeListDto +import com.sampoom.android.feature.user.data.remote.dto.UpdateEmployeeStatusRequestDto +import com.sampoom.android.feature.user.data.remote.dto.UpdateEmployeeStatusResponseDto import com.sampoom.android.feature.user.data.remote.dto.UpdateProfileRequestDto import com.sampoom.android.feature.user.data.remote.dto.UpdateProfileResponseDto import retrofit2.http.Body @@ -34,4 +36,11 @@ interface UserApi { @Query("page") page: Int = 0, @Query("size") size: Int = 20 ): ApiResponse + + @PATCH("user/status/{userId}") + suspend fun updateEmployeeStatus( + @Path("userId") userId: Long, + @Query("workspace") workspace: String, + @Body body: UpdateEmployeeStatusRequestDto + ): ApiResponse } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeDto.kt index 31d78fd..500f514 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeDto.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeDto.kt @@ -1,5 +1,6 @@ package com.sampoom.android.feature.user.data.remote.dto +import com.sampoom.android.core.model.EmployeeStatus import com.sampoom.android.core.model.UserPosition data class EmployeeDto( @@ -11,6 +12,7 @@ data class EmployeeDto( val organizationId: Long, val branch: String, val position: UserPosition, + val employeeStatus: EmployeeStatus?, val startedAt: String?, val endedAt: String? ) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateEmployeeStatusRequestDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateEmployeeStatusRequestDto.kt new file mode 100644 index 0000000..c945218 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateEmployeeStatusRequestDto.kt @@ -0,0 +1,5 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class UpdateEmployeeStatusRequestDto( + val employeeStatus: String +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateEmployeeStatusResponseDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateEmployeeStatusResponseDto.kt new file mode 100644 index 0000000..22be469 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/UpdateEmployeeStatusResponseDto.kt @@ -0,0 +1,8 @@ +package com.sampoom.android.feature.user.data.remote.dto + +data class UpdateEmployeeStatusResponseDto( + val userId: Long, + val userName: String, + val workspace: String, + val employeeStatus: String +) diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt b/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt index 0f1dfb5..7dea968 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt @@ -3,12 +3,14 @@ package com.sampoom.android.feature.user.data.repository import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData +import com.sampoom.android.core.model.EmployeeStatus import com.sampoom.android.core.preferences.AuthPreferences import com.sampoom.android.core.util.retry import com.sampoom.android.feature.user.data.mapper.toModel import com.sampoom.android.feature.user.data.paging.EmployeePagingSource import com.sampoom.android.feature.user.data.remote.api.UserApi import com.sampoom.android.feature.user.data.remote.dto.EditEmployeeRequestDto +import com.sampoom.android.feature.user.data.remote.dto.UpdateEmployeeStatusRequestDto import com.sampoom.android.feature.user.data.remote.dto.UpdateProfileRequestDto import com.sampoom.android.feature.user.domain.model.Employee import com.sampoom.android.feature.user.domain.model.User @@ -123,6 +125,7 @@ class UserRepositoryImpl @Inject constructor( organizationId = employee.organizationId, branch = employee.branch, position = updatedEmployee.position, + employeeStatus = employee.employeeStatus, startedAt = employee.startedAt, endedAt = employee.endedAt ) @@ -131,6 +134,40 @@ class UserRepositoryImpl @Inject constructor( } } + override suspend fun updateEmployeeStatus( + employee: Employee, + workspace: String + ): Result { + return runCatching { + val requestDto = UpdateEmployeeStatusRequestDto( + employeeStatus = employee.employeeStatus?.name ?: "-" + ) + val dto = api.updateEmployeeStatus( + userId = employee.userId, + workspace = workspace, + body = requestDto + ) + if (!dto.success) throw Exception(dto.message) + + val updateEmployeeStatus = dto.data.toModel() + val completedEmployeeStatus = Employee( + userId = updateEmployeeStatus.userId, + email = employee.email, + role = employee.role, + userName = updateEmployeeStatus.userName.takeIf { it.isNotBlank() } ?: employee.userName, + workspace = updateEmployeeStatus.workspace.takeIf { it.isNotBlank() } ?: employee.workspace, + organizationId = employee.organizationId, + branch = employee.branch, + position = employee.position, + employeeStatus = updateEmployeeStatus.employeeStatus, + startedAt = employee.startedAt, + endedAt = employee.endedAt + ) + + completedEmployeeStatus + } + } + override suspend fun getEmployeeCount(): Result { return runCatching { val user = preferences.getStoredUser() ?: throw Exception() diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/model/Employee.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/model/Employee.kt index 7c1a8b1..d7001a2 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/domain/model/Employee.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/model/Employee.kt @@ -1,5 +1,6 @@ package com.sampoom.android.feature.user.domain.model +import com.sampoom.android.core.model.EmployeeStatus import com.sampoom.android.core.model.UserPosition data class Employee( @@ -11,6 +12,7 @@ data class Employee( val organizationId: Long, val branch: String, val position: UserPosition, + val employeeStatus: EmployeeStatus, val startedAt: String?, val endedAt: String? ) diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/repository/UserRepository.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/repository/UserRepository.kt index 52c64c3..6c278c0 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/domain/repository/UserRepository.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/repository/UserRepository.kt @@ -11,5 +11,6 @@ interface UserRepository { suspend fun updateProfile(user: User): Result fun getEmployeeList(): Flow> suspend fun editEmployee(employee: Employee, workspace: String): Result + suspend fun updateEmployeeStatus(employee: Employee, workspace: String): Result suspend fun getEmployeeCount(): Result } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/UpdateEmployeeStatusUseCase.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/UpdateEmployeeStatusUseCase.kt new file mode 100644 index 0000000..72b08bc --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/usecase/UpdateEmployeeStatusUseCase.kt @@ -0,0 +1,11 @@ +package com.sampoom.android.feature.user.domain.usecase + +import com.sampoom.android.feature.user.domain.model.Employee +import com.sampoom.android.feature.user.domain.repository.UserRepository +import javax.inject.Inject + +class UpdateEmployeeStatusUseCase @Inject constructor( + private val repository: UserRepository +) { + suspend operator fun invoke(employee: Employee, workspace: String): Result = repository.updateEmployeeStatus(employee, workspace) +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeBottomSheet.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeBottomSheet.kt index 0b578ec..52d46a1 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeBottomSheet.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeBottomSheet.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuAnchorType import androidx.compose.material3.ExposedDropdownMenuBox @@ -34,6 +35,7 @@ import com.sampoom.android.feature.user.domain.model.Employee fun EditEmployeeBottomSheet( employee: Employee, onDismiss: () -> Unit, + onEmployeeUpdated: (Employee) -> Unit = {}, viewModel: EditEmployeeViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -42,19 +44,19 @@ fun EditEmployeeBottomSheet( val errorLabel = stringResource(R.string.common_error) val editEmployeeLabel = stringResource(R.string.employee_edit_edited) - val deleteEmployeeLabel = stringResource(R.string.employee_edit_deleted) LaunchedEffect(employee) { viewModel.onEvent(EditEmployeeUiEvent.Initialize(employee)) selectedPosition = employee.position } - LaunchedEffect(errorLabel, editEmployeeLabel, deleteEmployeeLabel) { - viewModel.bindLabel(errorLabel, editEmployeeLabel, deleteEmployeeLabel) + LaunchedEffect(errorLabel, editEmployeeLabel) { + viewModel.bindLabel(errorLabel, editEmployeeLabel) } LaunchedEffect(uiState.isSuccess) { if (uiState.isSuccess) { + uiState.employee?.let(onEmployeeUpdated) viewModel.clearStatus() onDismiss() } @@ -89,7 +91,7 @@ fun EditEmployeeBottomSheet( onDismissRequest = { positionMenuExpanded = false } ) { UserPosition.entries.forEach { position -> - androidx.compose.material3.DropdownMenuItem( + DropdownMenuItem( text = { Text(positionToKorean(position)) }, onClick = { selectedPosition = position diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeViewModel.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeViewModel.kt index a4a4df3..27df54a 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EditEmployeeViewModel.kt @@ -28,12 +28,10 @@ class EditEmployeeViewModel @Inject constructor( private var errorLabel: String = "" private var editEmployeeLabel: String = "" - private var deleteEmployeeLabel: String = "" - fun bindLabel(error: String, editEmployee: String, deleteEmployee: String) { + fun bindLabel(error: String, editEmployee: String) { errorLabel = error editEmployeeLabel = editEmployee - deleteEmployeeLabel = deleteEmployee } fun onEvent(event: EditEmployeeUiEvent) { diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeBottomSheetType.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeBottomSheetType.kt new file mode 100644 index 0000000..8b3af46 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeBottomSheetType.kt @@ -0,0 +1,6 @@ +package com.sampoom.android.feature.user.ui + +enum class EmployeeBottomSheetType { + STATUS, + EDIT +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt index 204d2fb..268b75f 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt @@ -1,5 +1,6 @@ package com.sampoom.android.feature.user.ui +import android.R.attr.text import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -54,6 +55,7 @@ import com.sampoom.android.core.ui.theme.FailRed import com.sampoom.android.core.ui.theme.backgroundCardColor import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.core.ui.theme.textSecondaryColor +import com.sampoom.android.core.util.employeeStatusToKorean import com.sampoom.android.core.util.formatDate import com.sampoom.android.core.util.positionToKorean import com.sampoom.android.feature.user.domain.model.Employee @@ -73,6 +75,7 @@ fun EmployeeListScreen( val listState = rememberSaveable(saver = LazyListState.Saver) { LazyListState() } val sheetState = rememberModalBottomSheetState(true) val selectedEmployee = uiState.selectedEmployee + val bottomSheetType = uiState.bottomSheetType LaunchedEffect(errorLabel) { viewModel.bindLabel(errorLabel) @@ -82,10 +85,10 @@ fun EmployeeListScreen( employeeListPaged.refresh() } - LaunchedEffect(selectedEmployee) { - if (selectedEmployee != null && !sheetState.isVisible) { + LaunchedEffect(selectedEmployee, bottomSheetType) { + if (selectedEmployee != null && bottomSheetType != null && !sheetState.isVisible) { sheetState.show() - } else if (selectedEmployee == null && sheetState.isVisible) { + } else if ((selectedEmployee == null || bottomSheetType == null) && sheetState.isVisible) { sheetState.hide() } } @@ -175,11 +178,11 @@ fun EmployeeListScreen( if (employee != null) { EmployeeListItemCard( employee = employee, - onDeleteClick = { - + onStatusClick = { + viewModel.onEvent(EmployeeListUiEvent.ShowStatusBottomSheet(employee)) }, onEditClick = { - viewModel.onEvent(EmployeeListUiEvent.ShowBottomSheet(employee)) + viewModel.onEvent(EmployeeListUiEvent.ShowEditBottomSheet(employee)) } ) } @@ -225,26 +228,38 @@ fun EmployeeListScreen( } } - if (selectedEmployee != null) { - uiState.selectedEmployee?.let { selectedEmployee -> - ModalBottomSheet( - onDismissRequest = { - coroutineScope.launch { - viewModel.onEvent(EmployeeListUiEvent.DismissBottomSheet) - sheetState.hide() - } - }, - sheetState = sheetState - ) { - EditEmployeeBottomSheet( - employee = selectedEmployee, - onDismiss = { - coroutineScope.launch { + if (selectedEmployee != null && bottomSheetType != null) { + ModalBottomSheet( + onDismissRequest = { + viewModel.onEvent(EmployeeListUiEvent.DismissBottomSheet) + }, + sheetState = sheetState + ) { + when (bottomSheetType) { + EmployeeBottomSheetType.STATUS -> { + UpdateEmployeeStatusBottomSheet( + employee = selectedEmployee, + onDismiss = { + viewModel.onEvent(EmployeeListUiEvent.DismissBottomSheet) + }, + onStatusUpdated = { updatedEmployee -> viewModel.onEvent(EmployeeListUiEvent.DismissBottomSheet) - sheetState.hide() + employeeListPaged.refresh() } - } - ) + ) + } + EmployeeBottomSheetType.EDIT -> { + EditEmployeeBottomSheet( + employee = selectedEmployee, + onDismiss = { + viewModel.onEvent(EmployeeListUiEvent.DismissBottomSheet) + }, + onEmployeeUpdated = { updatedEmployee -> + viewModel.onEvent(EmployeeListUiEvent.DismissBottomSheet) + employeeListPaged.refresh() + } + ) + } } } } @@ -253,7 +268,7 @@ fun EmployeeListScreen( @Composable private fun EmployeeListItemCard( employee: Employee, - onDeleteClick: () -> Unit, + onStatusClick: () -> Unit, onEditClick: () -> Unit ) { Card( @@ -265,11 +280,23 @@ private fun EmployeeListItemCard( .fillMaxWidth() .padding(16.dp) ) { - Text( - text = employee.userName, - color = textColor(), - style = MaterialTheme.typography.titleLarge - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = employee.userName, + color = textColor(), + style = MaterialTheme.typography.titleLarge + ) + Text( + text = employeeStatusToKorean(employee.employeeStatus), + color = textColor(), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Light + ) + } + Text( text = positionToKorean(employee.position), color = textSecondaryColor(), @@ -318,11 +345,11 @@ private fun EmployeeListItemCard( ) { CommonButton( modifier = Modifier.weight(1F), - variant = ButtonVariant.Error, + variant = ButtonVariant.Outlined, size = ButtonSize.Large, - onClick = { onDeleteClick() } + onClick = { onStatusClick() } ) { - Text(stringResource(R.string.employee_delete)) + Text(stringResource(R.string.employee_status_edit)) } Spacer(Modifier.width(8.dp)) CommonButton( @@ -331,7 +358,7 @@ private fun EmployeeListItemCard( size = ButtonSize.Large, onClick = { onEditClick() } ) { - Text(stringResource(R.string.employee_edit)) + Text(stringResource(R.string.employee_position_edit)) } } } diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiEvent.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiEvent.kt index eabb110..09e936e 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiEvent.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiEvent.kt @@ -5,6 +5,7 @@ import com.sampoom.android.feature.user.domain.model.Employee interface EmployeeListUiEvent { object LoadEmployeeList : EmployeeListUiEvent object RetryEmployeeList : EmployeeListUiEvent - data class ShowBottomSheet(val employee: Employee) : EmployeeListUiEvent + data class ShowEditBottomSheet(val employee: Employee) : EmployeeListUiEvent + data class ShowStatusBottomSheet(val employee: Employee) : EmployeeListUiEvent object DismissBottomSheet : EmployeeListUiEvent } \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiState.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiState.kt index 648efbf..134bcb8 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiState.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListUiState.kt @@ -6,5 +6,6 @@ data class EmployeeListUiState( val employeeList: List = emptyList(), val employeeLoading: Boolean = false, val employeeError: String? = null, - val selectedEmployee: Employee? = null + val selectedEmployee: Employee? = null, + val bottomSheetType: EmployeeBottomSheetType? = null ) diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListViewModel.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListViewModel.kt index 0383392..7747f03 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListViewModel.kt @@ -40,7 +40,8 @@ class EmployeeListViewModel @Inject constructor( when (event) { is EmployeeListUiEvent.LoadEmployeeList -> {} is EmployeeListUiEvent.RetryEmployeeList -> {} - is EmployeeListUiEvent.ShowBottomSheet -> _uiState.update { it.copy(selectedEmployee = event.employee) } + is EmployeeListUiEvent.ShowEditBottomSheet -> _uiState.update { it.copy(selectedEmployee = event.employee, bottomSheetType = EmployeeBottomSheetType.EDIT) } + is EmployeeListUiEvent.ShowStatusBottomSheet -> _uiState.update { it.copy(selectedEmployee = event.employee, bottomSheetType = EmployeeBottomSheetType.STATUS) } is EmployeeListUiEvent.DismissBottomSheet -> _uiState.update { it.copy(selectedEmployee = null) } } } diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusBottomSheet.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusBottomSheet.kt new file mode 100644 index 0000000..5dd4dfc --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusBottomSheet.kt @@ -0,0 +1,115 @@ +package com.sampoom.android.feature.user.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +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.model.EmployeeStatus +import com.sampoom.android.core.ui.component.CommonButton +import com.sampoom.android.core.ui.component.CommonTextField +import com.sampoom.android.core.util.employeeStatusToKorean +import com.sampoom.android.feature.user.domain.model.Employee + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun UpdateEmployeeStatusBottomSheet( + employee: Employee, + onDismiss: () -> Unit, + onStatusUpdated: (Employee) -> Unit = {}, + viewModel: UpdateEmployeeStatusViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + var selectedStatus by rememberSaveable { mutableStateOf(employee.employeeStatus) } + var employeeStatusMenuExpanded by remember { mutableStateOf(false) } + + val errorLabel = stringResource(R.string.common_error) + val editEmployeeLabel = stringResource(R.string.employee_edit_status_edited) + + LaunchedEffect(employee) { + viewModel.onEvent(UpdateEmployeeStatusUiEvent.Initialize(employee)) + selectedStatus = employee.employeeStatus + } + + LaunchedEffect(errorLabel, editEmployeeLabel) { + viewModel.bindLabel(errorLabel, editEmployeeLabel) + } + + LaunchedEffect(uiState.isSuccess) { + if (uiState.isSuccess) { + uiState.employee?.let(onStatusUpdated) + viewModel.clearStatus() + onDismiss() + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 32.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + ExposedDropdownMenuBox( + expanded = employeeStatusMenuExpanded, + onExpandedChange = { employeeStatusMenuExpanded = it } + ) { + CommonTextField( + modifier = Modifier + .fillMaxWidth() + .menuAnchor( + type = ExposedDropdownMenuAnchorType.PrimaryNotEditable, + enabled = true + ), + readOnly = true, + value = employeeStatusToKorean(selectedStatus), + onValueChange = {}, + placeholder = stringResource(R.string.employee_placeholder_status_edit), + singleLine = true + ) + ExposedDropdownMenu( + expanded = employeeStatusMenuExpanded, + onDismissRequest = { employeeStatusMenuExpanded = false } + ) { + EmployeeStatus.entries.forEach { status -> + DropdownMenuItem( + text = { Text(employeeStatusToKorean(status)) }, + onClick = { + selectedStatus = status + employeeStatusMenuExpanded = false + } + ) + } + } + } + + Spacer(Modifier.height(8.dp)) + + CommonButton( + modifier = Modifier.fillMaxWidth(), + enabled = !uiState.isLoading && selectedStatus != employee.employeeStatus, + onClick = { viewModel.onEvent(UpdateEmployeeStatusUiEvent.EditEmployeeStatus(selectedStatus)) } + ) { + Text(stringResource(R.string.common_confirm)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusUiEvent.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusUiEvent.kt new file mode 100644 index 0000000..62fdbbd --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusUiEvent.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.user.ui + +import com.sampoom.android.core.model.EmployeeStatus +import com.sampoom.android.feature.user.domain.model.Employee + +interface UpdateEmployeeStatusUiEvent { + data class Initialize(val employee: Employee) : UpdateEmployeeStatusUiEvent + data class EditEmployeeStatus(val employeeStatus: EmployeeStatus) : UpdateEmployeeStatusUiEvent + object Dismiss : UpdateEmployeeStatusUiEvent +} \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusUiState.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusUiState.kt new file mode 100644 index 0000000..ca7456c --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusUiState.kt @@ -0,0 +1,10 @@ +package com.sampoom.android.feature.user.ui + +import com.sampoom.android.feature.user.domain.model.Employee + +data class UpdateEmployeeStatusUiState( + val employee: Employee? = null, + val isLoading: Boolean = false, + val error: String? = null, + val isSuccess: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusViewModel.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusViewModel.kt new file mode 100644 index 0000000..de82665 --- /dev/null +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusViewModel.kt @@ -0,0 +1,103 @@ +package com.sampoom.android.feature.user.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.sampoom.android.core.model.EmployeeStatus +import com.sampoom.android.core.network.serverMessageOrNull +import com.sampoom.android.core.util.GlobalMessageHandler +import com.sampoom.android.feature.user.domain.usecase.UpdateEmployeeStatusUseCase +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 UpdateEmployeeStatusViewModel @Inject constructor( + private val messageHandler: GlobalMessageHandler, + private val updateEmployeeStatusUseCase: UpdateEmployeeStatusUseCase +) : ViewModel() { + + private companion object { + private const val TAG = "UpdateEmployeeStatusViewModel" + } + + private val _uiState = MutableStateFlow(UpdateEmployeeStatusUiState()) + val uiState: StateFlow = _uiState + + private var errorLabel: String = "" + private var editEmployeeLabel: String = "" + + fun bindLabel(error: String, editEmployee: String) { + errorLabel = error + editEmployeeLabel = editEmployee + } + + fun onEvent(event: UpdateEmployeeStatusUiEvent) { + when (event) { + is UpdateEmployeeStatusUiEvent.Initialize -> { + _uiState.update { + it.copy( + employee = event.employee, + isLoading = false, + isSuccess = false + ) + } + } + is UpdateEmployeeStatusUiEvent.EditEmployeeStatus -> { + editEmployeeStatus(event.employeeStatus) + } + is UpdateEmployeeStatusUiEvent.Dismiss -> { + _uiState.update { + it.copy( + employee = null, + isLoading = false, + error = null + ) + } + } + } + } + + private fun editEmployeeStatus(newEmployeeStatus: EmployeeStatus) { + viewModelScope.launch { + val currentEmployee = _uiState.value.employee ?: run { + messageHandler.showMessage(message = errorLabel, isError = true) + return@launch + } + + val updateEmployee = currentEmployee.copy(employeeStatus = newEmployeeStatus) + _uiState.update { it.copy(isLoading = true, error = null) } + + updateEmployeeStatusUseCase(updateEmployee, "AGENCY") + .onSuccess { employee -> + _uiState.update { + it.copy( + employee = employee, + isLoading = false, + error = null, + isSuccess = true + ) + } + messageHandler.showMessage(message = editEmployeeLabel, isError = false) + } + .onFailure { throwable -> + val backendMessage = throwable.serverMessageOrNull() + val error = backendMessage ?: (throwable.message ?: errorLabel) + messageHandler.showMessage(message = error, isError = true) + + _uiState.update { + it.copy( + isLoading = false, + error = error + ) + } + } + } + } + + fun clearStatus() { + _uiState.update { it.copy(isSuccess = false, error = null) } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0f18d62..f7eff45 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -119,10 +119,11 @@ 직원이 없습니다. 이메일 가입일 - 삭제 - 수정 + 재직상태 변경 + 재직상태 선택 + 직급 변경 직원 수정이 완료되었습니다. - 직원 삭제가 완료되었습니다. + 직원 상태 수정이 완료되었습니다. 대기중 From 80a748bb80558e2807f36e08dbd9938345aed22e Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Sun, 9 Nov 2025 03:44:33 +0900 Subject: [PATCH 5/6] =?UTF-8?q?[FIX]=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20impor?= =?UTF-8?q?t=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sampoom/android/core/di/NetworkModule.kt | 2 +- .../android/core/preferences/AuthPreferences.kt | 2 +- .../android/core/ui/component/CommonButton.kt | 3 +-- .../core/ui/component/CommonTextField.kt | 17 +++++++++++++++-- .../com/sampoom/android/core/ui/theme/Type.kt | 1 - .../feature/auth/domain/usecase/LoginUseCase.kt | 2 +- .../auth/domain/usecase/SignUpUseCase.kt | 2 +- .../android/feature/auth/ui/LoginViewModel.kt | 2 +- .../android/feature/auth/ui/SignUpScreen.kt | 1 - .../android/feature/auth/ui/SignUpViewModel.kt | 2 +- .../android/feature/cart/ui/CartListScreen.kt | 1 - .../feature/dashboard/ui/DashboardScreen.kt | 1 - .../feature/dashboard/ui/DashboardViewModel.kt | 1 - .../feature/dashboard/ui/SettingScreen.kt | 2 -- .../feature/dashboard/ui/SettingViewModel.kt | 2 +- .../feature/order/ui/OrderDetailContent.kt | 2 -- .../android/feature/order/ui/OrderItem.kt | 1 - .../feature/outbound/di/OutboundModules.kt | 1 - .../feature/outbound/ui/OutboundListScreen.kt | 4 ---- .../outbound/ui/OutboundListViewModel.kt | 1 - .../feature/part/ui/PartDetailBottomSheet.kt | 2 -- .../android/feature/part/ui/PartListScreen.kt | 1 - .../feature/part/ui/PartListViewModel.kt | 1 - .../android/feature/part/ui/PartScreen.kt | 2 -- .../android/feature/part/ui/PartUiState.kt | 1 - .../android/feature/part/ui/PartViewModel.kt | 1 - .../feature/user/data/remote/api/UserApi.kt | 2 +- .../user/data/repository/UserRepositoryImpl.kt | 1 - .../feature/user/ui/EmployeeListScreen.kt | 2 -- 29 files changed, 24 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/com/sampoom/android/core/di/NetworkModule.kt b/app/src/main/java/com/sampoom/android/core/di/NetworkModule.kt index 31dff36..d1bf160 100644 --- a/app/src/main/java/com/sampoom/android/core/di/NetworkModule.kt +++ b/app/src/main/java/com/sampoom/android/core/di/NetworkModule.kt @@ -3,10 +3,10 @@ package com.sampoom.android.core.di import com.google.gson.FieldNamingPolicy import com.google.gson.GsonBuilder import com.sampoom.android.BuildConfig -import com.sampoom.android.core.preferences.AuthPreferences import com.sampoom.android.core.network.TokenAuthenticator import com.sampoom.android.core.network.TokenInterceptor import com.sampoom.android.core.network.TokenRefreshService +import com.sampoom.android.core.preferences.AuthPreferences import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/app/src/main/java/com/sampoom/android/core/preferences/AuthPreferences.kt b/app/src/main/java/com/sampoom/android/core/preferences/AuthPreferences.kt index d4c0473..e6787fe 100644 --- a/app/src/main/java/com/sampoom/android/core/preferences/AuthPreferences.kt +++ b/app/src/main/java/com/sampoom/android/core/preferences/AuthPreferences.kt @@ -9,9 +9,9 @@ import androidx.datastore.preferences.preferencesDataStore import com.sampoom.android.core.model.UserPosition import com.sampoom.android.feature.user.domain.model.User import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.first import javax.inject.Inject import javax.inject.Singleton -import kotlinx.coroutines.flow.first // Per official guidance, DataStore instance should be single and at top-level. private val Context.authDataStore by preferencesDataStore(name = "auth_prefs") diff --git a/app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt b/app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt index 5a4d7ea..976a1aa 100644 --- a/app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt +++ b/app/src/main/java/com/sampoom/android/core/ui/component/CommonButton.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.height import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton @@ -80,7 +79,7 @@ fun CommonButton( } } - // Light/secondary (tonal) filled button + // Light/secondary outlined button with semi-transparent background ButtonVariant.Secondary -> { OutlinedButton( onClick = onClick, diff --git a/app/src/main/java/com/sampoom/android/core/ui/component/CommonTextField.kt b/app/src/main/java/com/sampoom/android/core/ui/component/CommonTextField.kt index 33769b5..baad166 100644 --- a/app/src/main/java/com/sampoom/android/core/ui/component/CommonTextField.kt +++ b/app/src/main/java/com/sampoom/android/core/ui/component/CommonTextField.kt @@ -9,8 +9,21 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.input.ImeAction diff --git a/app/src/main/java/com/sampoom/android/core/ui/theme/Type.kt b/app/src/main/java/com/sampoom/android/core/ui/theme/Type.kt index bc62309..3dfbdac 100644 --- a/app/src/main/java/com/sampoom/android/core/ui/theme/Type.kt +++ b/app/src/main/java/com/sampoom/android/core/ui/theme/Type.kt @@ -4,7 +4,6 @@ import androidx.compose.material3.Typography import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.sampoom.android.R diff --git a/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/LoginUseCase.kt b/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/LoginUseCase.kt index af9b253..d4a288a 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/LoginUseCase.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/LoginUseCase.kt @@ -1,7 +1,7 @@ package com.sampoom.android.feature.auth.domain.usecase -import com.sampoom.android.feature.user.domain.model.User import com.sampoom.android.feature.auth.domain.repository.AuthRepository +import com.sampoom.android.feature.user.domain.model.User import javax.inject.Inject class LoginUseCase @Inject constructor( diff --git a/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/SignUpUseCase.kt b/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/SignUpUseCase.kt index 5dc6d2b..aedc1a9 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/SignUpUseCase.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/domain/usecase/SignUpUseCase.kt @@ -1,7 +1,7 @@ package com.sampoom.android.feature.auth.domain.usecase -import com.sampoom.android.feature.user.domain.model.User import com.sampoom.android.feature.auth.domain.repository.AuthRepository +import com.sampoom.android.feature.user.domain.model.User import javax.inject.Inject class SignUpUseCase @Inject constructor( 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 baa24dc..dcb9ef7 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 @@ -5,8 +5,8 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sampoom.android.core.network.serverMessageOrNull -import com.sampoom.android.core.util.GlobalMessageHandler import com.sampoom.android.core.util.AuthValidator +import com.sampoom.android.core.util.GlobalMessageHandler import com.sampoom.android.core.util.ValidationResult import com.sampoom.android.feature.auth.domain.usecase.LoginUseCase import com.sampoom.android.feature.user.domain.usecase.GetProfileUseCase 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 9ead5b9..d7fe3a3 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 @@ -19,7 +19,6 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuAnchorType import androidx.compose.material3.ExposedDropdownMenuBox -import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold diff --git a/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt b/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt index 3c43a25..8430b26 100644 --- a/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/auth/ui/SignUpViewModel.kt @@ -6,11 +6,11 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sampoom.android.R import com.sampoom.android.core.network.serverMessageOrNull -import com.sampoom.android.core.util.GlobalMessageHandler import com.sampoom.android.core.util.AuthValidator import com.sampoom.android.core.util.AuthValidator.validateEmail import com.sampoom.android.core.util.AuthValidator.validatePassword import com.sampoom.android.core.util.AuthValidator.validatePasswordCheck +import com.sampoom.android.core.util.GlobalMessageHandler import com.sampoom.android.core.util.ValidationResult import com.sampoom.android.feature.auth.domain.usecase.GetVendorUseCase import com.sampoom.android.feature.auth.domain.usecase.SignUpUseCase diff --git a/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListScreen.kt b/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListScreen.kt index dce9cd5..d17be0e 100644 --- a/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/cart/ui/CartListScreen.kt @@ -31,7 +31,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp diff --git a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardScreen.kt b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardScreen.kt index 6586893..9245950 100644 --- a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardScreen.kt @@ -1,6 +1,5 @@ package com.sampoom.android.feature.dashboard.ui -import android.R.attr.onClick import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border diff --git a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardViewModel.kt b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardViewModel.kt index eb463b6..4232364 100644 --- a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/DashboardViewModel.kt @@ -12,7 +12,6 @@ import com.sampoom.android.feature.order.domain.model.Order import com.sampoom.android.feature.order.domain.usecase.GetOrderUseCase import com.sampoom.android.feature.user.domain.model.User import com.sampoom.android.feature.user.domain.usecase.GetEmployeeCountUseCase -import com.sampoom.android.feature.user.domain.usecase.GetEmployeeUseCase import com.sampoom.android.feature.user.domain.usecase.GetStoredUserUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow diff --git a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingScreen.kt b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingScreen.kt index 90f51a0..f6d3f82 100644 --- a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingScreen.kt @@ -39,7 +39,6 @@ import androidx.compose.ui.res.stringResource 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.backgroundCardColor @@ -50,7 +49,6 @@ import com.sampoom.android.core.util.formatDate import com.sampoom.android.core.util.positionToKorean import com.sampoom.android.feature.user.domain.model.User import com.sampoom.android.feature.user.ui.UpdateProfileBottomSheet -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) diff --git a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingViewModel.kt b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingViewModel.kt index 4d61c17..9426de8 100644 --- a/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/dashboard/ui/SettingViewModel.kt @@ -3,8 +3,8 @@ package com.sampoom.android.feature.dashboard.ui import android.app.Application import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.sampoom.android.core.util.GlobalMessageHandler import com.sampoom.android.core.util.AuthValidator +import com.sampoom.android.core.util.GlobalMessageHandler import com.sampoom.android.core.util.ValidationResult import com.sampoom.android.feature.user.domain.model.User import com.sampoom.android.feature.user.domain.usecase.GetStoredUserUseCase diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailContent.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailContent.kt index 6109771..5141d9f 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailContent.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderDetailContent.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.unit.dp import com.sampoom.android.R import com.sampoom.android.core.ui.component.StatusChip import com.sampoom.android.core.ui.theme.backgroundCardColor -import com.sampoom.android.core.ui.theme.disableColor import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.core.ui.theme.textSecondaryColor import com.sampoom.android.core.util.formatWon @@ -33,7 +32,6 @@ import com.sampoom.android.feature.order.domain.model.Order import com.sampoom.android.feature.order.domain.model.OrderPart import com.sampoom.android.feature.order.domain.model.subtotal import com.sampoom.android.feature.order.domain.model.totalCost -import kotlin.collections.forEach @Composable fun OrderDetailContent( diff --git a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderItem.kt b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderItem.kt index 8614242..e7776d4 100644 --- a/app/src/main/java/com/sampoom/android/feature/order/ui/OrderItem.kt +++ b/app/src/main/java/com/sampoom/android/feature/order/ui/OrderItem.kt @@ -15,7 +15,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.sampoom.android.R diff --git a/app/src/main/java/com/sampoom/android/feature/outbound/di/OutboundModules.kt b/app/src/main/java/com/sampoom/android/feature/outbound/di/OutboundModules.kt index 7aa5276..9b71dd9 100644 --- a/app/src/main/java/com/sampoom/android/feature/outbound/di/OutboundModules.kt +++ b/app/src/main/java/com/sampoom/android/feature/outbound/di/OutboundModules.kt @@ -10,7 +10,6 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import retrofit2.Retrofit import javax.inject.Singleton -import kotlin.jvm.java @Module @InstallIn(SingletonComponent::class) diff --git a/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListScreen.kt b/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListScreen.kt index ca3f83d..a36e706 100644 --- a/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListScreen.kt @@ -1,6 +1,5 @@ package com.sampoom.android.feature.outbound.ui -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -33,8 +32,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -51,7 +48,6 @@ import com.sampoom.android.core.ui.theme.backgroundCardColor import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.core.ui.theme.textSecondaryColor import com.sampoom.android.core.util.formatWon -import com.sampoom.android.feature.cart.domain.model.subtotal import com.sampoom.android.feature.outbound.domain.model.OutboundPart import com.sampoom.android.feature.outbound.domain.model.subtotal diff --git a/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListViewModel.kt b/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListViewModel.kt index 6eb5820..b64ea15 100644 --- a/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/outbound/ui/OutboundListViewModel.kt @@ -11,7 +11,6 @@ import com.sampoom.android.feature.outbound.domain.usecase.GetOutboundUseCase import com.sampoom.android.feature.outbound.domain.usecase.ProcessOutboundUseCase import com.sampoom.android.feature.outbound.domain.usecase.UpdateOutboundQuantityUseCase import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartDetailBottomSheet.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartDetailBottomSheet.kt index 561fa71..9e9e949 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/ui/PartDetailBottomSheet.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartDetailBottomSheet.kt @@ -1,6 +1,5 @@ package com.sampoom.android.feature.part.ui -import android.R.attr.onClick import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -25,7 +24,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource diff --git a/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt index 44a690d..919e173 100644 --- a/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/part/ui/PartListScreen.kt @@ -49,7 +49,6 @@ import com.sampoom.android.R import com.sampoom.android.core.ui.component.EmptyContent import com.sampoom.android.core.ui.component.ErrorContent import com.sampoom.android.core.ui.theme.backgroundCardColor -import com.sampoom.android.core.ui.theme.disableColor import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.core.ui.theme.textSecondaryColor import com.sampoom.android.core.util.formatWon 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 811fc25..ffa4bcd 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 @@ -8,7 +8,6 @@ import com.sampoom.android.core.network.serverMessageOrNull import com.sampoom.android.core.util.GlobalMessageHandler import com.sampoom.android.feature.part.domain.usecase.GetPartUseCase import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update 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 47bebf1..48fcd67 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.widget.Space import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -62,7 +61,6 @@ 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.backgroundColor -import com.sampoom.android.core.ui.theme.disableColor import com.sampoom.android.core.ui.theme.textColor import com.sampoom.android.core.ui.theme.textSecondaryColor import com.sampoom.android.core.util.formatWon 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 19d8842..a6a7064 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 @@ -2,7 +2,6 @@ package com.sampoom.android.feature.part.ui 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.SearchResult data class PartUiState( // Part 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 7ecbd5e..9c45c81 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 @@ -13,7 +13,6 @@ import com.sampoom.android.feature.part.domain.usecase.GetCategoryUseCase import com.sampoom.android.feature.part.domain.usecase.GetGroupUseCase import com.sampoom.android.feature.part.domain.usecase.SearchPartsUseCase import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/api/UserApi.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/api/UserApi.kt index c815bfa..a885502 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/data/remote/api/UserApi.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/api/UserApi.kt @@ -1,10 +1,10 @@ package com.sampoom.android.feature.user.data.remote.api import com.sampoom.android.core.model.ApiResponse -import com.sampoom.android.feature.user.data.remote.dto.GetProfileResponseDto import com.sampoom.android.feature.user.data.remote.dto.EditEmployeeRequestDto import com.sampoom.android.feature.user.data.remote.dto.EditEmployeeResponseDto import com.sampoom.android.feature.user.data.remote.dto.EmployeeListDto +import com.sampoom.android.feature.user.data.remote.dto.GetProfileResponseDto import com.sampoom.android.feature.user.data.remote.dto.UpdateEmployeeStatusRequestDto import com.sampoom.android.feature.user.data.remote.dto.UpdateEmployeeStatusResponseDto import com.sampoom.android.feature.user.data.remote.dto.UpdateProfileRequestDto diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt b/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt index 7dea968..674af08 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt @@ -3,7 +3,6 @@ package com.sampoom.android.feature.user.data.repository import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData -import com.sampoom.android.core.model.EmployeeStatus import com.sampoom.android.core.preferences.AuthPreferences import com.sampoom.android.core.util.retry import com.sampoom.android.feature.user.data.mapper.toModel diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt index 268b75f..cabdea0 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt @@ -1,6 +1,5 @@ package com.sampoom.android.feature.user.ui -import android.R.attr.text import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -59,7 +58,6 @@ import com.sampoom.android.core.util.employeeStatusToKorean import com.sampoom.android.core.util.formatDate import com.sampoom.android.core.util.positionToKorean import com.sampoom.android.feature.user.domain.model.Employee -import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable From 479a3f6063dcc96f081922760145cd791dc93702 Mon Sep 17 00:00:00 2001 From: Sangyoon Date: Sun, 9 Nov 2025 14:35:27 +0900 Subject: [PATCH 6/6] =?UTF-8?q?[FIX]=20=EC=A7=81=EC=9B=90=20=EC=9E=AC?= =?UTF-8?q?=EC=A7=81=20=EC=83=81=ED=83=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/user/data/mapper/UserMappers.kt | 18 ++++--- .../user/data/remote/dto/EmployeeDto.kt | 6 ++- .../data/repository/UserRepositoryImpl.kt | 14 ++++-- .../feature/user/domain/model/Employee.kt | 6 ++- .../feature/user/ui/EmployeeListScreen.kt | 50 ++++++++++++++++++- .../ui/UpdateEmployeeStatusBottomSheet.kt | 6 +-- .../user/ui/UpdateEmployeeStatusViewModel.kt | 2 +- app/src/main/res/values/strings.xml | 5 +- 8 files changed, 86 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/mapper/UserMappers.kt b/app/src/main/java/com/sampoom/android/feature/user/data/mapper/UserMappers.kt index 664d280..e6b7df3 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/data/mapper/UserMappers.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/data/mapper/UserMappers.kt @@ -57,9 +57,11 @@ fun EditEmployeeResponseDto.toModel(): Employee = Employee( organizationId = 0, branch = "", position = position.toUserPosition(), - employeeStatus = EmployeeStatus.ACTIVE, + status = EmployeeStatus.ACTIVE, + createdAt = null, startedAt = null, - endedAt = null + endedAt = null, + deletedAt = null ) fun UpdateEmployeeStatusResponseDto.toModel(): Employee = Employee( @@ -71,9 +73,11 @@ fun UpdateEmployeeStatusResponseDto.toModel(): Employee = Employee( organizationId = 0, branch = "", position = UserPosition.STAFF, - employeeStatus = employeeStatus.toEmployeeStatus(), + status = employeeStatus.toEmployeeStatus(), + createdAt = null, startedAt = null, - endedAt = null + endedAt = null, + deletedAt = null ) fun EmployeeDto.toModel(): Employee = Employee( @@ -85,9 +89,11 @@ fun EmployeeDto.toModel(): Employee = Employee( organizationId, branch, position, - employeeStatus ?: EmployeeStatus.ACTIVE, + status ?: EmployeeStatus.ACTIVE, + createdAt, startedAt, - endedAt + endedAt, + deletedAt ) private fun String.toEmployeeStatus(): EmployeeStatus = try { diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeDto.kt b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeDto.kt index 500f514..02b7ed1 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeDto.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/data/remote/dto/EmployeeDto.kt @@ -12,7 +12,9 @@ data class EmployeeDto( val organizationId: Long, val branch: String, val position: UserPosition, - val employeeStatus: EmployeeStatus?, + val status: EmployeeStatus?, + val createdAt: String?, val startedAt: String?, - val endedAt: String? + val endedAt: String?, + val deletedAt: String? ) \ No newline at end of file diff --git a/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt b/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt index 674af08..25469c2 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/data/repository/UserRepositoryImpl.kt @@ -124,9 +124,11 @@ class UserRepositoryImpl @Inject constructor( organizationId = employee.organizationId, branch = employee.branch, position = updatedEmployee.position, - employeeStatus = employee.employeeStatus, + status = employee.status, + createdAt = employee.createdAt, startedAt = employee.startedAt, - endedAt = employee.endedAt + endedAt = employee.endedAt, + deletedAt = employee.deletedAt ) completeEmployee @@ -139,7 +141,7 @@ class UserRepositoryImpl @Inject constructor( ): Result { return runCatching { val requestDto = UpdateEmployeeStatusRequestDto( - employeeStatus = employee.employeeStatus?.name ?: "-" + employeeStatus = employee.status.name ) val dto = api.updateEmployeeStatus( userId = employee.userId, @@ -158,9 +160,11 @@ class UserRepositoryImpl @Inject constructor( organizationId = employee.organizationId, branch = employee.branch, position = employee.position, - employeeStatus = updateEmployeeStatus.employeeStatus, + status = updateEmployeeStatus.status, + createdAt = employee.createdAt, startedAt = employee.startedAt, - endedAt = employee.endedAt + endedAt = employee.endedAt, + deletedAt = employee.deletedAt ) completedEmployeeStatus diff --git a/app/src/main/java/com/sampoom/android/feature/user/domain/model/Employee.kt b/app/src/main/java/com/sampoom/android/feature/user/domain/model/Employee.kt index d7001a2..da6bbdb 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/domain/model/Employee.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/domain/model/Employee.kt @@ -12,7 +12,9 @@ data class Employee( val organizationId: Long, val branch: String, val position: UserPosition, - val employeeStatus: EmployeeStatus, + val status: EmployeeStatus, + val createdAt: String?, val startedAt: String?, - val endedAt: String? + val endedAt: String?, + val deletedAt: String? ) diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt index cabdea0..cab05cf 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/EmployeeListScreen.kt @@ -288,7 +288,7 @@ private fun EmployeeListItemCard( style = MaterialTheme.typography.titleLarge ) Text( - text = employeeStatusToKorean(employee.employeeStatus), + text = employeeStatusToKorean(employee.status), color = textColor(), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Light @@ -320,6 +320,22 @@ private fun EmployeeListItemCard( ) } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(R.string.employee_createdAt), + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = formatDate(employee.createdAt ?: stringResource(R.string.common_slash)), + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium + ) + } + Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween @@ -336,6 +352,38 @@ private fun EmployeeListItemCard( ) } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(R.string.employee_endedAt), + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = formatDate(employee.endedAt ?: stringResource(R.string.common_slash)), + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(R.string.employee_deletedAt), + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = formatDate(employee.deletedAt ?: stringResource(R.string.common_slash)), + color = textSecondaryColor(), + style = MaterialTheme.typography.bodyMedium + ) + } + Spacer(Modifier.height(16.dp)) Row( diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusBottomSheet.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusBottomSheet.kt index 5dd4dfc..0685d3a 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusBottomSheet.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusBottomSheet.kt @@ -39,7 +39,7 @@ fun UpdateEmployeeStatusBottomSheet( viewModel: UpdateEmployeeStatusViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - var selectedStatus by rememberSaveable { mutableStateOf(employee.employeeStatus) } + var selectedStatus by rememberSaveable { mutableStateOf(employee.status) } var employeeStatusMenuExpanded by remember { mutableStateOf(false) } val errorLabel = stringResource(R.string.common_error) @@ -47,7 +47,7 @@ fun UpdateEmployeeStatusBottomSheet( LaunchedEffect(employee) { viewModel.onEvent(UpdateEmployeeStatusUiEvent.Initialize(employee)) - selectedStatus = employee.employeeStatus + selectedStatus = employee.status } LaunchedEffect(errorLabel, editEmployeeLabel) { @@ -106,7 +106,7 @@ fun UpdateEmployeeStatusBottomSheet( CommonButton( modifier = Modifier.fillMaxWidth(), - enabled = !uiState.isLoading && selectedStatus != employee.employeeStatus, + enabled = !uiState.isLoading && selectedStatus != employee.status, onClick = { viewModel.onEvent(UpdateEmployeeStatusUiEvent.EditEmployeeStatus(selectedStatus)) } ) { Text(stringResource(R.string.common_confirm)) diff --git a/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusViewModel.kt b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusViewModel.kt index de82665..e08ede8 100644 --- a/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusViewModel.kt +++ b/app/src/main/java/com/sampoom/android/feature/user/ui/UpdateEmployeeStatusViewModel.kt @@ -67,7 +67,7 @@ class UpdateEmployeeStatusViewModel @Inject constructor( return@launch } - val updateEmployee = currentEmployee.copy(employeeStatus = newEmployeeStatus) + val updateEmployee = currentEmployee.copy(status = newEmployeeStatus) _uiState.update { it.copy(isLoading = true, error = null) } updateEmployeeStatusUseCase(updateEmployee, "AGENCY") diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f7eff45..0e6f7a0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -118,7 +118,10 @@ 직원관리 직원이 없습니다. 이메일 - 가입일 + 최초생성일 + 근무시작일 + 근무종료일 + 퇴사일 재직상태 변경 재직상태 선택 직급 변경