diff --git a/SampoomManagement.xcodeproj/project.pbxproj b/SampoomManagement.xcodeproj/project.pbxproj index d3251d5..986609b 100644 --- a/SampoomManagement.xcodeproj/project.pbxproj +++ b/SampoomManagement.xcodeproj/project.pbxproj @@ -214,6 +214,7 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; @@ -271,6 +272,7 @@ MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; VALIDATE_PRODUCT = YES; }; diff --git a/SampoomManagement/App/ContentView.swift b/SampoomManagement/App/ContentView.swift index acda9cc..a90ec76 100644 --- a/SampoomManagement/App/ContentView.swift +++ b/SampoomManagement/App/ContentView.swift @@ -11,6 +11,10 @@ enum Tabs { case dashboard, outbound, cart, orders, parts } +enum SettingNavigation: Hashable { + case settings +} + struct ContentView: View { // MARK: - Properties let dependencies: AppDependencies @@ -19,6 +23,7 @@ struct ContentView: View { @State private var selectedTab: Tabs = .dashboard @State private var ordersNavigationPath = NavigationPath() @State private var partsNavigationPath = NavigationPath() + @State private var dashboardNavigationPath = NavigationPath() // MARK: - Initialization init(dependencies: AppDependencies) { @@ -37,7 +42,8 @@ struct ContentView: View { TabView(selection: $selectedTab) { // Dashboard 탭 (DashboardView directly) Tab(value: .dashboard) { - NavigationStack { + NavigationStack(path: $dashboardNavigationPath) { + let user = try? dependencies.authPreferences.getStoredUser() DashboardView( viewModel: dashboardViewModel, onLogoutClick: { @@ -52,9 +58,29 @@ struct ContentView: View { onNavigateOrderList: { selectedTab = .orders }, - userName: ((try? dependencies.authPreferences.getStoredUser())?.name) ?? "", - branch: ((try? dependencies.authPreferences.getStoredUser())?.branch) ?? "" + onSettingClick: { + dashboardNavigationPath.append(SettingNavigation.settings) + }, + userName: user?.name ?? "", + branch: user?.branch ?? "", + userRole: user?.role ?? .user ) + .navigationDestination(for: SettingNavigation.self) { destination in + switch destination { + case .settings: + SettingView( + viewModel: dependencies.makeSettingViewModel(), + onNavigateBack: { + if !dashboardNavigationPath.isEmpty { + dashboardNavigationPath.removeLast() + } + }, + onLogoutClick: { + dependencies.authViewModel.handleSignedOutState() + } + ) + } + } } } label: { Label { diff --git a/SampoomManagement/Core/DI/AppDependencies.swift b/SampoomManagement/Core/DI/AppDependencies.swift index a0868f4..a2dd555 100644 --- a/SampoomManagement/Core/DI/AppDependencies.swift +++ b/SampoomManagement/Core/DI/AppDependencies.swift @@ -62,6 +62,7 @@ class AppDependencies { let getOrderUseCase: GetOrderUseCase let createOrderUseCase: CreateOrderUseCase let getOrderDetailUseCase: GetOrderDetailUseCase + let completeOrderUseCase: CompleteOrderUseCase let receiveOrderUseCase: ReceiveOrderUseCase let cancelOrderUseCase: CancelOrderUseCase @@ -103,7 +104,7 @@ class AppDependencies { // Part partAPI = PartAPI(networkManager: networkManager) - partRepository = PartRepositoryImpl(api: partAPI) + partRepository = PartRepositoryImpl(api: partAPI, preferences: authPreferences) getCategoryUseCase = GetCategoryUseCase(repository: partRepository) getGroupUseCase = GetGroupUseCase(repository: partRepository) getPartUseCase = GetPartUseCase(repository: partRepository) @@ -111,7 +112,7 @@ class AppDependencies { // Outbound outboundAPI = OutboundAPI(networkManager: networkManager) - outboundRepository = OutboundRepositoryImpl(api: outboundAPI) + outboundRepository = OutboundRepositoryImpl(api: outboundAPI, preferences: authPreferences) getOutboundUseCase = GetOutboundUseCase(repository: outboundRepository) addOutboundUseCase = AddOutboundUseCase(repository: outboundRepository) deleteOutboundUseCase = DeleteOutboundUseCase(repository: outboundRepository) @@ -121,7 +122,7 @@ class AppDependencies { // Cart cartAPI = CartAPI(networkManager: networkManager) - cartRepository = CartRepositoryImpl(api: cartAPI) + cartRepository = CartRepositoryImpl(api: cartAPI, preferences: authPreferences) getCartUseCase = GetCartUseCase(repository: cartRepository) addCartUseCase = AddCartUseCase(repository: cartRepository) deleteCartUseCase = DeleteCartUseCase(repository: cartRepository) @@ -134,6 +135,7 @@ class AppDependencies { getOrderUseCase = GetOrderUseCase(repository: orderRepository) createOrderUseCase = CreateOrderUseCase(repository: orderRepository) getOrderDetailUseCase = GetOrderDetailUseCase(repository: orderRepository) + completeOrderUseCase = CompleteOrderUseCase(repository: orderRepository) receiveOrderUseCase = ReceiveOrderUseCase(repository: orderRepository) cancelOrderUseCase = CancelOrderUseCase(repository: orderRepository) } @@ -206,10 +208,19 @@ class AppDependencies { return OrderDetailViewModel( getOrderDetailUseCase: getOrderDetailUseCase, cancelOrderUseCase: cancelOrderUseCase, + completeOrderUseCase: completeOrderUseCase, receiveOrderUseCase: receiveOrderUseCase, globalMessageHandler: globalMessageHandler, orderId: orderId ) } + + func makeSettingViewModel() -> SettingViewModel { + return SettingViewModel( + authPreferences: authPreferences, + signOutUseCase: signOutUseCase, + globalMessageHandler: globalMessageHandler + ) + } } diff --git a/SampoomManagement/Core/Network/TokenRefreshService.swift b/SampoomManagement/Core/Network/TokenRefreshService.swift index 583612c..27735ea 100644 --- a/SampoomManagement/Core/Network/TokenRefreshService.swift +++ b/SampoomManagement/Core/Network/TokenRefreshService.swift @@ -51,13 +51,17 @@ class TokenRefreshService { let updatedUser = User( id: existingUser.id, name: existingUser.name, + email: existingUser.email, role: existingUser.role, accessToken: dto.accessToken, refreshToken: dto.refreshToken, expiresIn: dto.expiresIn, position: existingUser.position, workspace: existingUser.workspace, - branch: existingUser.branch + branch: existingUser.branch, + agencyId: existingUser.agencyId, + startedAt: existingUser.startedAt, + endedAt: existingUser.endedAt ) try authPreferences.saveUser(updatedUser) diff --git a/SampoomManagement/Core/Resources/StringResources.swift b/SampoomManagement/Core/Resources/StringResources.swift index 90d1180..aaba4ba 100644 --- a/SampoomManagement/Core/Resources/StringResources.swift +++ b/SampoomManagement/Core/Resources/StringResources.swift @@ -21,7 +21,7 @@ struct StringResources { static let intro = "오늘도 효율적인 재고 관리를 시작해보세요." static let employee = "직원 관리" static let partsOnHand = "보유 부품" - static let partsInProgress = "진행중 주문" + static let partsInProgress = "진행중 부품" static let shortageOfParts = "부족 부품" static let orderAmount = "주문 금액" static let recentOrdersTitle = "최근 주문" @@ -77,6 +77,7 @@ struct StringResources { // MARK: - Common struct Common { static let ok = "확인" + static let confirm = "확인" static let cancel = "취소" static let save = "저장" static let delete = "삭제" @@ -85,6 +86,10 @@ struct StringResources { static let error = "오류" static let retry = "다시 시도" static let loadMore = "더 보기" + static let close = "닫기" + static let detail = "상세 보기" + static let EA = "EA" + static let slash = "-" } // MARK: - Search @@ -169,11 +174,24 @@ struct StringResources { static let detailToastOrderReceive = "입고 처리되었습니다" // Order Status - static let statusPending = "승인대기" + static let statusPending = "대기중" + static let statusConfirmed = "주문확인" + static let statusShipping = "배송중" + static let statusDelayed = "배송지연" + static let statusProducing = "생산중" + static let statusArrived = "배송완료" static let statusCompleted = "입고완료" static let statusCanceled = "주문취소" } + // MARK: - Setting + struct Setting { + static let title = "설정" + static let editProfile = "프로필 수정" + static let logout = "로그아웃" + static let dialogLogout = "로그아웃 하시겠습니까?" + } + // MARK: - Auth struct Auth { // Login @@ -203,14 +221,16 @@ struct StringResources { // Validation Messages static func fieldRequired(_ field: String) -> String { - return "\(field)을(를) 입력해주세요." + return "\(field)을(를) 입력해주세요" } - static let emailRequired = "이메일을 입력해주세요." - static let emailInvalid = "올바른 이메일 형식이 아닙니다." - static let passwordRequired = "비밀번호를 입력해주세요." - static let passwordTooShort = "비밀번호는 8자 이상이어야 합니다." + static let emailRequired = "이메일을 입력해주세요" + static let emailInvalid = "올바른 이메일 형식이 아닙니다" + static let passwordRequired = "비밀번호를 입력해주세요" + static let passwordTooShort = "비밀번호는 최소 8자 이상이어야 합니다" + static let passwordMaxLength = "비밀번호는 최대 30자까지 가능합니다" + static let passwordComplexity = "영문, 숫자, 특수문자를 각각 1개 이상 포함해야 합니다" static let passwordInvalid = "비밀번호는 영문과 숫자를 포함해야 합니다." - static let passwordCheckRequired = "비밀번호 확인을 입력해주세요." - static let passwordCheckMismatch = "비밀번호가 일치하지 않습니다." + static let passwordCheckRequired = "비밀번호 확인을 입력해주세요" + static let passwordCheckMismatch = "비밀번호가 일치하지 않습니다" } } diff --git a/SampoomManagement/Core/UI/Components/OrderItem.swift b/SampoomManagement/Core/UI/Components/OrderItem.swift index 5896eee..0f5ddfd 100644 --- a/SampoomManagement/Core/UI/Components/OrderItem.swift +++ b/SampoomManagement/Core/UI/Components/OrderItem.swift @@ -28,7 +28,7 @@ struct OrderItem: View { Text(order.createdAt ?? "-") .font(.gmarketCaption) .foregroundColor(.textSecondary) - StatusChip(status: order.status.rawValue) + StatusChip(status: order.status) } } .padding(16) diff --git a/SampoomManagement/Core/UI/Components/StatusChip.swift b/SampoomManagement/Core/UI/Components/StatusChip.swift index 8a44429..7883a48 100644 --- a/SampoomManagement/Core/UI/Components/StatusChip.swift +++ b/SampoomManagement/Core/UI/Components/StatusChip.swift @@ -10,7 +10,7 @@ import Foundation /// 주문 상태를 표시하는 칩 컴포넌트 struct StatusChip: View { - let status: String // 임시로 String 사용 + let status: OrderStatus var body: some View { let (text, color) = statusDisplayInfo @@ -25,24 +25,35 @@ struct StatusChip: View { } private var statusDisplayInfo: (text: String, color: Color) { - switch status.lowercased() { - case "pending": - return ("승인대기", Color(.waitYellow)) - case "completed": - return ("입고완료", Color(.successGreen)) - case "canceled": - return ("주문취소", Color(.failRed)) - default: - return ("승인대기", Color(.waitYellow)) + switch status { + case .pending: + return (StringResources.Order.statusPending, Color(.waitYellow)) + case .confirmed: + return (StringResources.Order.statusConfirmed, Color(.waitYellow)) + case .shipping: + return (StringResources.Order.statusShipping, Color(.waitYellow)) + case .delayed: + return (StringResources.Order.statusDelayed, Color(.waitYellow)) + case .producing: + return (StringResources.Order.statusProducing, Color(.waitYellow)) + case .arrived: + return (StringResources.Order.statusArrived, Color(.waitYellow)) + case .completed: + return (StringResources.Order.statusCompleted, Color(.successGreen)) + case .canceled: + return (StringResources.Order.statusCanceled, Color(.failRed)) } } } #Preview { VStack(spacing: 16) { - StatusChip(status: "pending") - StatusChip(status: "completed") - StatusChip(status: "canceled") + StatusChip(status: .pending) + StatusChip(status: .confirmed) + StatusChip(status: .shipping) + StatusChip(status: .arrived) + StatusChip(status: .completed) + StatusChip(status: .canceled) } .padding() } diff --git a/SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift b/SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift index d0b656b..09ce969 100644 --- a/SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift +++ b/SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift @@ -15,11 +15,15 @@ class AuthPreferences { static let refreshToken = "auth.refreshToken" static let userId = "auth.userId" static let userName = "auth.userName" + static let userEmail = "auth.userEmail" static let userRole = "auth.userRole" static let expiresIn = "auth.expiresIn" static let position = "auth.position" static let workspace = "auth.workspace" static let branch = "auth.branch" + static let agencyId = "auth.agencyId" + static let startedAt = "auth.startedAt" + static let endedAt = "auth.endedAt" } func saveUser(_ user: User) throws { @@ -28,22 +32,30 @@ class AuthPreferences { try keychain.save(user.refreshToken, for: Keys.refreshToken) try keychain.save(String(user.id), for: Keys.userId) try keychain.save(user.name, for: Keys.userName) - try keychain.save(user.role, for: Keys.userRole) + try keychain.save(user.email, for: Keys.userEmail) + try keychain.save(user.role.rawValue, for: Keys.userRole) try keychain.save(String(user.expiresIn), for: Keys.expiresIn) try keychain.save(user.position, for: Keys.position) try keychain.save(user.workspace, for: Keys.workspace) try keychain.save(user.branch, for: Keys.branch) + try keychain.save(String(user.agencyId), for: Keys.agencyId) + try keychain.save(user.startedAt ?? "", for: Keys.startedAt) + try keychain.save(user.endedAt ?? "", for: Keys.endedAt) } catch { // 부분 저장 실패 시 롤백 try? keychain.delete(Keys.accessToken) try? keychain.delete(Keys.refreshToken) try? keychain.delete(Keys.userId) try? keychain.delete(Keys.userName) + try? keychain.delete(Keys.userEmail) try? keychain.delete(Keys.userRole) try? keychain.delete(Keys.expiresIn) try? keychain.delete(Keys.position) try? keychain.delete(Keys.workspace) try? keychain.delete(Keys.branch) + try? keychain.delete(Keys.agencyId) + try? keychain.delete(Keys.startedAt) + try? keychain.delete(Keys.endedAt) throw error } } @@ -62,6 +74,7 @@ class AuthPreferences { func getStoredUser() throws -> User? { do { + // Require only core identifiers and tokens; allow missing new fields for migration guard let userIdString = try keychain.get(Keys.userId), let userId = Int(userIdString), let userName = try keychain.get(Keys.userName), @@ -72,21 +85,29 @@ class AuthPreferences { let expiresIn = Int(expiresInString) else { return nil } - // Tolerate missing profile keys by defaulting to empty strings + // Tolerate missing profile keys by defaulting to safe values let position = (try? keychain.get(Keys.position)) ?? "" let workspace = (try? keychain.get(Keys.workspace)) ?? "" let branch = (try? keychain.get(Keys.branch)) ?? "" + let userEmail = (try? keychain.get(Keys.userEmail)) ?? "" + let agencyId = Int((try? keychain.get(Keys.agencyId)) ?? "0") ?? 0 + let startedAt = try? keychain.get(Keys.startedAt) + let endedAt = try? keychain.get(Keys.endedAt) return User( id: userId, name: userName, - role: userRole, + email: userEmail, + role: UserRole(rawValue: userRole) ?? .user, accessToken: accessToken, refreshToken: refreshToken, expiresIn: expiresIn, position: position, workspace: workspace, - branch: branch + branch: branch, + agencyId: agencyId, + startedAt: startedAt?.isEmpty == false ? startedAt : nil, + endedAt: endedAt?.isEmpty == false ? endedAt : nil ) } catch { print("AuthPreferences - 사용자 정보 조회 실패: \(error)") @@ -127,11 +148,15 @@ class AuthPreferences { try keychain.delete(Keys.refreshToken) try keychain.delete(Keys.userId) try keychain.delete(Keys.userName) + try keychain.delete(Keys.userEmail) try keychain.delete(Keys.userRole) try keychain.delete(Keys.expiresIn) try keychain.delete(Keys.position) try keychain.delete(Keys.workspace) try keychain.delete(Keys.branch) + try keychain.delete(Keys.agencyId) + try keychain.delete(Keys.startedAt) + try keychain.delete(Keys.endedAt) } catch { // 로그아웃 시에는 실패해도 에러를 던지지 않음 (이미 로그아웃 상태로 간주) print("AuthPreferences - 키체인 삭제 실패: \(error)") diff --git a/SampoomManagement/Features/Auth/Data/Mappers/AuthMappers.swift b/SampoomManagement/Features/Auth/Data/Mappers/AuthMappers.swift index 95d0c6a..1376715 100644 --- a/SampoomManagement/Features/Auth/Data/Mappers/AuthMappers.swift +++ b/SampoomManagement/Features/Auth/Data/Mappers/AuthMappers.swift @@ -11,14 +11,18 @@ extension LoginResponseDTO { func toModel() -> User { return User( id: self.userId, - name: self.userName ?? "", - role: self.role, + name: "", + email: "", + role: .user, accessToken: self.accessToken, refreshToken: self.refreshToken, expiresIn: self.expiresIn, position: "", workspace: "", - branch: "" + branch: "", + agencyId: 0, + startedAt: nil, + endedAt: nil ) } } @@ -27,14 +31,18 @@ extension GetProfileResponseDTO { func toModel() -> User { return User( id: self.userId, - name: self.userName ?? "", - role: "", + name: self.userName, + email: self.email, + role: UserRole(rawValue: self.role) ?? .user, accessToken: "", refreshToken: "", expiresIn: 0, - position: self.position ?? "", - workspace: self.workspace ?? "", - branch: self.branch ?? "" + position: self.position, + workspace: self.workspace, + branch: self.branch, + agencyId: self.organizationId, + startedAt: self.startedAt.isEmpty ? nil : self.startedAt, + endedAt: self.endedAt ) } } @@ -44,13 +52,17 @@ extension User { return User( id: self.id, name: profile.name, - role: self.role, + email: profile.email, + role: profile.role, accessToken: self.accessToken, refreshToken: self.refreshToken, expiresIn: self.expiresIn, position: profile.position, workspace: profile.workspace, - branch: profile.branch + branch: profile.branch, + agencyId: profile.agencyId, + startedAt: profile.startedAt, + endedAt: profile.endedAt ) } } diff --git a/SampoomManagement/Features/Auth/Data/Remote/API/AuthAPI.swift b/SampoomManagement/Features/Auth/Data/Remote/API/AuthAPI.swift index eb5e434..ae022d6 100644 --- a/SampoomManagement/Features/Auth/Data/Remote/API/AuthAPI.swift +++ b/SampoomManagement/Features/Auth/Data/Remote/API/AuthAPI.swift @@ -17,9 +17,10 @@ class AuthAPI { // 로그인 func login(email: String, password: String) async throws -> APIResponse { - let requestDTO = LoginRequestDTO(email: email, password: password) + let requestDTO = LoginRequestDTO(workspace: "AGENCY", email: email, password: password) let parameters: [String: Any] = [ + "workspace": requestDTO.workspace, "email": requestDTO.email, "password": requestDTO.password ] @@ -94,9 +95,9 @@ class AuthAPI { } // 프로필 조회 - func getProfile() async throws -> APIResponse { + func getProfile(workspace: String = "AGENCY") async throws -> APIResponse { return try await networkManager.request( - endpoint: "user/profile", + endpoint: "user/profile?workspace=\(workspace)", method: .get, parameters: nil, responseType: GetProfileResponseDTO.self diff --git a/SampoomManagement/Features/Auth/Data/Remote/DTO/GetProfileResponseDTO.swift b/SampoomManagement/Features/Auth/Data/Remote/DTO/GetProfileResponseDTO.swift index 2deb3b8..9c86aff 100644 --- a/SampoomManagement/Features/Auth/Data/Remote/DTO/GetProfileResponseDTO.swift +++ b/SampoomManagement/Features/Auth/Data/Remote/DTO/GetProfileResponseDTO.swift @@ -9,10 +9,15 @@ import Foundation struct GetProfileResponseDTO: Codable { let userId: Int - let userName: String? - let workspace: String? - let branch: String? - let position: String? + let userName: String + let email: String + let role: String + let position: String + let workspace: String + let branch: String + let organizationId: Int + let startedAt: String + let endedAt: String? } diff --git a/SampoomManagement/Features/Auth/Data/Remote/DTO/LoginRequestDTO.swift b/SampoomManagement/Features/Auth/Data/Remote/DTO/LoginRequestDTO.swift index 9b219f9..9a9922f 100644 --- a/SampoomManagement/Features/Auth/Data/Remote/DTO/LoginRequestDTO.swift +++ b/SampoomManagement/Features/Auth/Data/Remote/DTO/LoginRequestDTO.swift @@ -8,6 +8,7 @@ import Foundation struct LoginRequestDTO: Codable { + let workspace: String let email: String let password: String } diff --git a/SampoomManagement/Features/Auth/Data/Remote/DTO/LoginResponseDTO.swift b/SampoomManagement/Features/Auth/Data/Remote/DTO/LoginResponseDTO.swift index cf55bbf..6d22ed4 100644 --- a/SampoomManagement/Features/Auth/Data/Remote/DTO/LoginResponseDTO.swift +++ b/SampoomManagement/Features/Auth/Data/Remote/DTO/LoginResponseDTO.swift @@ -9,8 +9,6 @@ import Foundation struct LoginResponseDTO: Codable { let userId: Int - let userName: String? - let role: String let accessToken: String let refreshToken: String let expiresIn: Int diff --git a/SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift b/SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift index 2304aa9..f44b5ed 100644 --- a/SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift +++ b/SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift @@ -114,13 +114,17 @@ class AuthRepositoryImpl: AuthRepository { let updatedUser = User( id: existingUser.id, name: existingUser.name, + email: existingUser.email, role: existingUser.role, accessToken: dto.accessToken, refreshToken: dto.refreshToken, expiresIn: dto.expiresIn, position: existingUser.position, workspace: existingUser.workspace, - branch: existingUser.branch + branch: existingUser.branch, + agencyId: existingUser.agencyId, + startedAt: existingUser.startedAt, + endedAt: existingUser.endedAt ) do { diff --git a/SampoomManagement/Features/Auth/Domain/Models/User.swift b/SampoomManagement/Features/Auth/Domain/Models/User.swift index 7b3ea01..1955740 100644 --- a/SampoomManagement/Features/Auth/Domain/Models/User.swift +++ b/SampoomManagement/Features/Auth/Domain/Models/User.swift @@ -7,10 +7,29 @@ import Foundation +enum UserRole: String, Codable, Equatable { + case admin = "ADMIN" + case user = "USER" + case manager = "MANAGER" + // Additional roles provided + case staff = "STAFF" + case seniorStaff = "SENIOR_STAFF" + case assistantManager = "ASSISTANT_MANAGER" + case deputyGeneralManager = "DEPUTY_GENERAL_MANAGER" + case generalManager = "GENERAL_MANAGER" + case director = "DIRECTOR" + case vicePresident = "VICE_PRESIDENT" + case president = "PRESIDENT" + case chairman = "CHAIRMAN" + + var isAdmin: Bool { self == .admin } +} + struct User: Equatable { let id: Int let name: String - let role: String + let email: String + let role: UserRole let accessToken: String let refreshToken: String let expiresIn: Int @@ -18,4 +37,7 @@ struct User: Equatable { let position: String let workspace: String let branch: String + let agencyId: Int + let startedAt: String? + let endedAt: String? } diff --git a/SampoomManagement/Features/Auth/UI/AuthViewModel.swift b/SampoomManagement/Features/Auth/UI/AuthViewModel.swift index 99609f1..5691676 100644 --- a/SampoomManagement/Features/Auth/UI/AuthViewModel.swift +++ b/SampoomManagement/Features/Auth/UI/AuthViewModel.swift @@ -45,8 +45,7 @@ class AuthViewModel: ObservableObject { } // 로그아웃 성공/실패 관계없이 로컬 상태 업데이트 - isLoggedIn = false - shouldNavigateToLogin = true + handleSignedOutState() } func handleTokenExpired() async { @@ -65,4 +64,9 @@ class AuthViewModel: ObservableObject { func resetNavigationState() { shouldNavigateToLogin = false } + + func handleSignedOutState() { + isLoggedIn = false + shouldNavigateToLogin = true + } } diff --git a/SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift b/SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift index 294d63b..6bbc341 100644 --- a/SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift +++ b/SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift @@ -16,9 +16,9 @@ class CartAPI { } // 장바구니 목록 조회 - func getCartList() async throws -> [CartDto] { + func getCartList(agencyId: Int) async throws -> [CartDto] { let response = try await networkManager.request( - endpoint: "agency/1/cart", + endpoint: "agency/\(agencyId)/cart", method: .get, responseType: [CartDto].self ) @@ -29,10 +29,10 @@ class CartAPI { } // 장바구니에 부품 추가 - func addCart(request: AddCartRequestDto) async throws { + func addCart(agencyId: Int, request: AddCartRequestDto) async throws { guard let params = request.toDictionary() else { throw NetworkError.invalidParameters } let response = try await networkManager.request( - endpoint: "agency/1/cart", + endpoint: "agency/\(agencyId)/cart", method: .post, parameters: params, responseType: EmptyResponse.self @@ -43,9 +43,9 @@ class CartAPI { } // 장바구니 항목 삭제 - func deleteCart(cartItemId: Int) async throws { + func deleteCart(agencyId: Int, cartItemId: Int) async throws { let response = try await networkManager.request( - endpoint: "agency/1/cart/\(cartItemId)", + endpoint: "agency/\(agencyId)/cart/\(cartItemId)", method: .delete, responseType: EmptyResponse.self ) @@ -55,10 +55,10 @@ class CartAPI { } // 장바구니 수량 변경 - func updateCart(cartItemId: Int, request: UpdateCartRequestDto) async throws { + func updateCart(agencyId: Int, cartItemId: Int, request: UpdateCartRequestDto) async throws { guard let params = request.toDictionary() else { throw NetworkError.invalidParameters } let response = try await networkManager.request( - endpoint: "agency/1/cart/\(cartItemId)", + endpoint: "agency/\(agencyId)/cart/\(cartItemId)", method: .put, parameters: params, responseType: EmptyResponse.self @@ -69,9 +69,9 @@ class CartAPI { } // 장바구니 전체 비우기 - func deleteAllCart() async throws { + func deleteAllCart(agencyId: Int) async throws { let response = try await networkManager.request( - endpoint: "agency/1/cart/clear", + endpoint: "agency/\(agencyId)/cart/clear", method: .delete, responseType: EmptyResponse.self ) diff --git a/SampoomManagement/Features/Cart/Data/Repository/CartRepositoryImpl.swift b/SampoomManagement/Features/Cart/Data/Repository/CartRepositoryImpl.swift index f3cd7aa..199ac4f 100644 --- a/SampoomManagement/Features/Cart/Data/Repository/CartRepositoryImpl.swift +++ b/SampoomManagement/Features/Cart/Data/Repository/CartRepositoryImpl.swift @@ -9,33 +9,50 @@ import Foundation class CartRepositoryImpl: CartRepository { private let api: CartAPI + private let preferences: AuthPreferences - init(api: CartAPI) { + init(api: CartAPI, preferences: AuthPreferences) { self.api = api + self.preferences = preferences } func getCartList() async throws -> CartList { - let data: [CartDto] = try await api.getCartList() + guard let user = try preferences.getStoredUser() else { + throw NetworkError.unauthorized + } + let data: [CartDto] = try await api.getCartList(agencyId: user.agencyId) let cartItems = data.map { $0.toModel() } return CartList(items: cartItems) } func addCart(partId: Int, quantity: Int) async throws { + guard let user = try preferences.getStoredUser() else { + throw NetworkError.unauthorized + } let request = AddCartRequestDto(partId: partId, quantity: quantity) - try await api.addCart(request: request) + try await api.addCart(agencyId: user.agencyId, request: request) } func deleteCart(cartItemId: Int) async throws { - try await api.deleteCart(cartItemId: cartItemId) + guard let user = try preferences.getStoredUser() else { + throw NetworkError.unauthorized + } + try await api.deleteCart(agencyId: user.agencyId, cartItemId: cartItemId) } func deleteAllCart() async throws { - try await api.deleteAllCart() + guard let user = try preferences.getStoredUser() else { + throw NetworkError.unauthorized + } + try await api.deleteAllCart(agencyId: user.agencyId) } func updateCartQuantity(cartItemId: Int, quantity: Int) async throws { + guard let user = try preferences.getStoredUser() else { + throw NetworkError.unauthorized + } let request = UpdateCartRequestDto(quantity: quantity) - try await api.updateCart(cartItemId: cartItemId, request: request) + try await api.updateCart(agencyId: user.agencyId, cartItemId: cartItemId, request: request) } } diff --git a/SampoomManagement/Features/Dashboard/UI/DashboardView.swift b/SampoomManagement/Features/Dashboard/UI/DashboardView.swift index eec1789..6abda9e 100644 --- a/SampoomManagement/Features/Dashboard/UI/DashboardView.swift +++ b/SampoomManagement/Features/Dashboard/UI/DashboardView.swift @@ -12,9 +12,10 @@ struct DashboardView: View { let onLogoutClick: () -> Void let onNavigateOrderDetail: (Order) -> Void let onNavigateOrderList: () -> Void + let onSettingClick: () -> Void let userName: String let branch: String - var isManager = true + let userRole: UserRole var body: some View { VStack(spacing: 0) { @@ -26,13 +27,12 @@ struct DashboardView: View { .frame(height: 24) Spacer() HStack(spacing: 12) { - // TODO: role-based employee button - if (isManager) { + if userRole.isAdmin { Button(action: {}) { Image("employee").renderingMode(.template).foregroundStyle(.text) } } - Button(action: {}) { + Button(action: onSettingClick) { Image("settings").renderingMode(.template).foregroundStyle(.text) } } @@ -42,11 +42,6 @@ struct DashboardView: View { ScrollView { VStack(spacing: 16) { - // Logout (temporary) - CommonButton(StringResources.Auth.logoutButton, backgroundColor: .red, textColor: .white) { - onLogoutClick() - } - titleSection buttonSection orderListSection @@ -85,7 +80,7 @@ struct DashboardView: View { private var buttonSection: some View { VStack(spacing: 16) { - if (isManager) { + if userRole.isAdmin { buttonCard(iconName: "employee", valueText: "45", subText: StringResources.Dashboard.employee, bordered: true) {} } HStack(spacing: 16) { diff --git a/SampoomManagement/Features/Order/Data/Remote/API/OrderAPI.swift b/SampoomManagement/Features/Order/Data/Remote/API/OrderAPI.swift index dbd0989..7fdfa07 100644 --- a/SampoomManagement/Features/Order/Data/Remote/API/OrderAPI.swift +++ b/SampoomManagement/Features/Order/Data/Remote/API/OrderAPI.swift @@ -45,10 +45,20 @@ class OrderAPI { return data } - /// 주문 입고 처리 - func receiveOrder(orderId: Int) async throws { + /// 주문 완료 처리 + func completeOrder(orderId: Int) async throws { let response: APIResponse = try await networkManager.request( - endpoint: "/agency/1/orders/\(orderId)/receive", + endpoint: "order/complete/\(orderId)", + method: .patch, + responseType: EmptyResponse.self + ) + if !response.success { throw NetworkError.serverError(response.status, message: response.message) } + } + + /// 주문 입고 처리 (대리점) + func receiveOrder(agencyId: Int, orderId: Int) async throws { + let response: APIResponse = try await networkManager.request( + endpoint: "agency/\(agencyId)/orders/\(orderId)/receive", method: .patch, responseType: EmptyResponse.self ) diff --git a/SampoomManagement/Features/Order/Data/Repository/OrderRepositoryImpl.swift b/SampoomManagement/Features/Order/Data/Repository/OrderRepositoryImpl.swift index f123031..a62d957 100644 --- a/SampoomManagement/Features/Order/Data/Repository/OrderRepositoryImpl.swift +++ b/SampoomManagement/Features/Order/Data/Repository/OrderRepositoryImpl.swift @@ -59,8 +59,19 @@ class OrderRepositoryImpl: OrderRepository { return dto.toModel() } + func completeOrder(orderId: Int) async throws { + // 인증 확인 (다른 상태 변경 메서드와 동일한 패턴) + guard (try preferences.getStoredUser()) != nil else { + throw NetworkError.unauthorized + } + try await api.completeOrder(orderId: orderId) + } + func receiveOrder(orderId: Int) async throws { - try await api.receiveOrder(orderId: orderId) + guard let user = try preferences.getStoredUser() else { + throw NetworkError.unauthorized + } + try await api.receiveOrder(agencyId: user.agencyId, orderId: orderId) } func getOrderDetail(orderId: Int) async throws -> Order { diff --git a/SampoomManagement/Features/Order/Domain/Models/OrderStatus.swift b/SampoomManagement/Features/Order/Domain/Models/OrderStatus.swift index 14da3e6..bc2f3f8 100644 --- a/SampoomManagement/Features/Order/Domain/Models/OrderStatus.swift +++ b/SampoomManagement/Features/Order/Domain/Models/OrderStatus.swift @@ -9,6 +9,11 @@ import Foundation enum OrderStatus: String, CaseIterable, Codable { case pending = "PENDING" + case confirmed = "CONFIRMED" + case shipping = "SHIPPING" + case delayed = "DELAYED" + case producing = "PRODUCING" + case arrived = "ARRIVED" case completed = "COMPLETED" case canceled = "CANCELED" @@ -18,6 +23,16 @@ enum OrderStatus: String, CaseIterable, Codable { switch rawValue { case "PENDING": return .pending + case "CONFIRMED": + return .confirmed + case "SHIPPING": + return .shipping + case "DELAYED": + return .delayed + case "PRODUCING": + return .producing + case "ARRIVED": + return .arrived case "COMPLETED": return .completed case "CANCELED": diff --git a/SampoomManagement/Features/Order/Domain/Repository/OrderRepository.swift b/SampoomManagement/Features/Order/Domain/Repository/OrderRepository.swift index 5f06e0f..9d3fa37 100644 --- a/SampoomManagement/Features/Order/Domain/Repository/OrderRepository.swift +++ b/SampoomManagement/Features/Order/Domain/Repository/OrderRepository.swift @@ -10,6 +10,7 @@ import Foundation protocol OrderRepository { func getOrderList(page: Int, size: Int) async throws -> (items: [Order], hasMore: Bool) func createOrder(cartList: CartList) async throws -> Order + func completeOrder(orderId: Int) async throws func receiveOrder(orderId: Int) async throws func getOrderDetail(orderId: Int) async throws -> Order func cancelOrder(orderId: Int) async throws diff --git a/SampoomManagement/Features/Order/Domain/UseCase/CompleteOrderUseCase.swift b/SampoomManagement/Features/Order/Domain/UseCase/CompleteOrderUseCase.swift new file mode 100644 index 0000000..2270e2b --- /dev/null +++ b/SampoomManagement/Features/Order/Domain/UseCase/CompleteOrderUseCase.swift @@ -0,0 +1,21 @@ +// +// CompleteOrderUseCase.swift +// SampoomManagement +// +// Created by 채상윤 on 10/20/25. +// + +import Foundation + +class CompleteOrderUseCase { + private let repository: OrderRepository + + init(repository: OrderRepository) { + self.repository = repository + } + + func execute(orderId: Int) async throws { + try await repository.completeOrder(orderId: orderId) + } +} + diff --git a/SampoomManagement/Features/Order/UI/OrderDetailContent.swift b/SampoomManagement/Features/Order/UI/OrderDetailContent.swift index 78c1827..1992c57 100644 --- a/SampoomManagement/Features/Order/UI/OrderDetailContent.swift +++ b/SampoomManagement/Features/Order/UI/OrderDetailContent.swift @@ -78,7 +78,7 @@ struct OrderInfoCard: View { Spacer() - StatusChip(status: order.status.rawValue) + StatusChip(status: order.status) } .padding(16) } diff --git a/SampoomManagement/Features/Order/UI/OrderDetailViewModel.swift b/SampoomManagement/Features/Order/UI/OrderDetailViewModel.swift index 2802e1c..048d73c 100644 --- a/SampoomManagement/Features/Order/UI/OrderDetailViewModel.swift +++ b/SampoomManagement/Features/Order/UI/OrderDetailViewModel.swift @@ -15,6 +15,7 @@ class OrderDetailViewModel: ObservableObject { private let getOrderDetailUseCase: GetOrderDetailUseCase private let cancelOrderUseCase: CancelOrderUseCase + private let completeOrderUseCase: CompleteOrderUseCase private let receiveOrderUseCase: ReceiveOrderUseCase private let globalMessageHandler: GlobalMessageHandler @@ -23,12 +24,14 @@ class OrderDetailViewModel: ObservableObject { init( getOrderDetailUseCase: GetOrderDetailUseCase, cancelOrderUseCase: CancelOrderUseCase, + completeOrderUseCase: CompleteOrderUseCase, receiveOrderUseCase: ReceiveOrderUseCase, globalMessageHandler: GlobalMessageHandler, orderId: Int = 0 ) { self.getOrderDetailUseCase = getOrderDetailUseCase self.cancelOrderUseCase = cancelOrderUseCase + self.completeOrderUseCase = completeOrderUseCase self.receiveOrderUseCase = receiveOrderUseCase self.globalMessageHandler = globalMessageHandler self.orderId = orderId @@ -103,7 +106,11 @@ class OrderDetailViewModel: ObservableObject { uiState = uiState.copy(isProcessing: true) do { + // 1단계: 주문 입고 처리 try await receiveOrderUseCase.execute(orderId: orderId) + // 2단계: 주문 완료 처리 + try await completeOrderUseCase.execute(orderId: orderId) + globalMessageHandler.showMessage(StringResources.Order.detailToastOrderReceive, isError: false) uiState = uiState.copy( isProcessing: false, diff --git a/SampoomManagement/Features/Order/UI/OrderListView.swift b/SampoomManagement/Features/Order/UI/OrderListView.swift index 2f9a32a..b79b0d9 100644 --- a/SampoomManagement/Features/Order/UI/OrderListView.swift +++ b/SampoomManagement/Features/Order/UI/OrderListView.swift @@ -104,7 +104,7 @@ struct OrderItemCard: View { .font(.gmarketCaption) .foregroundColor(Color("TextSecondary")) - StatusChip(status: order.status.rawValue) + StatusChip(status: order.status) } } .padding(16) diff --git a/SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift b/SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift index 5336b87..e5819ff 100644 --- a/SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift +++ b/SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift @@ -16,9 +16,9 @@ class OutboundAPI { } // 출고 목록 조회 - func getOutboundList() async throws -> [OutboundDto] { + func getOutboundList(agencyId: Int) async throws -> [OutboundDto] { let response = try await networkManager.request( - endpoint: "agency/1/outbound", + endpoint: "agency/\(agencyId)/outbound", method: .get, responseType: [OutboundDto].self ) @@ -27,10 +27,10 @@ class OutboundAPI { } // 출고 목록에 부품 추가 - func addOutbound(request: AddOutboundRequestDto) async throws { + func addOutbound(agencyId: Int, request: AddOutboundRequestDto) async throws { guard let params = request.toDictionary() else { throw NetworkError.invalidParameters } let response = try await networkManager.request( - endpoint: "agency/1/outbound", + endpoint: "agency/\(agencyId)/outbound", method: .post, parameters: params, responseType: EmptyResponse.self @@ -41,9 +41,9 @@ class OutboundAPI { } // 출고 처리 - func processOutbound() async throws { + func processOutbound(agencyId: Int) async throws { let response = try await networkManager.request( - endpoint: "agency/1/outbound/process", + endpoint: "agency/\(agencyId)/outbound/process", method: .post, responseType: EmptyResponse.self ) @@ -53,9 +53,9 @@ class OutboundAPI { } // 출고 항목 삭제 - func deleteOutbound(outboundId: Int) async throws { + func deleteOutbound(agencyId: Int, outboundId: Int) async throws { let response = try await networkManager.request( - endpoint: "agency/1/outbound/\(outboundId)", + endpoint: "agency/\(agencyId)/outbound/\(outboundId)", method: .delete, responseType: EmptyResponse.self ) @@ -65,10 +65,10 @@ class OutboundAPI { } // 출고 수량 변경 - func updateOutbound(outboundId: Int, request: UpdateOutboundRequestDto) async throws { + func updateOutbound(agencyId: Int, outboundId: Int, request: UpdateOutboundRequestDto) async throws { guard let params = request.toDictionary() else { throw NetworkError.invalidParameters } let response = try await networkManager.request( - endpoint: "agency/1/outbound/\(outboundId)", + endpoint: "agency/\(agencyId)/outbound/\(outboundId)", method: .patch, parameters: params, responseType: EmptyResponse.self @@ -79,9 +79,9 @@ class OutboundAPI { } // 출고 목록 전체 비우기 - func deleteAllOutbound() async throws { + func deleteAllOutbound(agencyId: Int) async throws { let response = try await networkManager.request( - endpoint: "agency/1/outbound/clear", + endpoint: "agency/\(agencyId)/outbound/clear", method: .delete, responseType: EmptyResponse.self ) diff --git a/SampoomManagement/Features/Outbound/Data/Repository/OutboundRepositoryImpl.swift b/SampoomManagement/Features/Outbound/Data/Repository/OutboundRepositoryImpl.swift index 02299fd..b9c1ef6 100644 --- a/SampoomManagement/Features/Outbound/Data/Repository/OutboundRepositoryImpl.swift +++ b/SampoomManagement/Features/Outbound/Data/Repository/OutboundRepositoryImpl.swift @@ -9,36 +9,56 @@ import Foundation class OutboundRepositoryImpl: OutboundRepository { private let api: OutboundAPI + private let preferences: AuthPreferences - init(api: OutboundAPI) { + init(api: OutboundAPI, preferences: AuthPreferences) { self.api = api + self.preferences = preferences } func getOutboundList() async throws -> OutboundList { - let data: [OutboundDto] = try await api.getOutboundList() + guard let user = try preferences.getStoredUser() else { + throw NetworkError.unauthorized + } + let data: [OutboundDto] = try await api.getOutboundList(agencyId: user.agencyId) let outboundItems = data.map { $0.toModel() } return OutboundList(items: outboundItems) } func processOutbound() async throws { - try await api.processOutbound() + guard let user = try preferences.getStoredUser() else { + throw NetworkError.unauthorized + } + try await api.processOutbound(agencyId: user.agencyId) } func addOutbound(partId: Int, quantity: Int) async throws { + guard let user = try preferences.getStoredUser() else { + throw NetworkError.unauthorized + } let request = AddOutboundRequestDto(partId: partId, quantity: quantity) - try await api.addOutbound(request: request) + try await api.addOutbound(agencyId: user.agencyId, request: request) } func deleteOutbound(outboundId: Int) async throws { - try await api.deleteOutbound(outboundId: outboundId) + guard let user = try preferences.getStoredUser() else { + throw NetworkError.unauthorized + } + try await api.deleteOutbound(agencyId: user.agencyId, outboundId: outboundId) } func deleteAllOutbound() async throws { - try await api.deleteAllOutbound() + guard let user = try preferences.getStoredUser() else { + throw NetworkError.unauthorized + } + try await api.deleteAllOutbound(agencyId: user.agencyId) } func updateOutboundQuantity(outboundId: Int, quantity: Int) async throws { + guard let user = try preferences.getStoredUser() else { + throw NetworkError.unauthorized + } let request = UpdateOutboundRequestDto(quantity: quantity) - try await api.updateOutbound(outboundId: outboundId, request: request) + try await api.updateOutbound(agencyId: user.agencyId, outboundId: outboundId, request: request) } } \ No newline at end of file diff --git a/SampoomManagement/Features/Part/Data/Remote/API/PartAPI.swift b/SampoomManagement/Features/Part/Data/Remote/API/PartAPI.swift index 4ae5006..dcff90f 100644 --- a/SampoomManagement/Features/Part/Data/Remote/API/PartAPI.swift +++ b/SampoomManagement/Features/Part/Data/Remote/API/PartAPI.swift @@ -33,18 +33,18 @@ class PartAPI { return PartsGroupList(items: groups) } - func getPartList(groupId: Int) async throws -> PartList { + func getPartList(agencyId: Int, groupId: Int) async throws -> PartList { let response = try await networkManager.request( - endpoint: "agency/1/group/\(groupId)", + endpoint: "agency/\(agencyId)/group/\(groupId)", responseType: [PartDTO].self ) let parts = (response.data ?? []).map { $0.toModel() } return PartList(items: parts) } - func searchParts(keyword: String, page: Int = 0, size: Int = 20) async throws -> (results: [SearchResult], hasMore: Bool) { + func searchParts(agencyId: Int, keyword: String, page: Int = 0, size: Int = 20) async throws -> (results: [SearchResult], hasMore: Bool) { let response = try await networkManager.request( - endpoint: "agency/1/search", + endpoint: "agency/\(agencyId)/search", method: .get, parameters: [ "keyword": keyword, diff --git a/SampoomManagement/Features/Part/Data/Repository/PartRepositoryImpl.swift b/SampoomManagement/Features/Part/Data/Repository/PartRepositoryImpl.swift index 7dcc4e8..a679ff8 100644 --- a/SampoomManagement/Features/Part/Data/Repository/PartRepositoryImpl.swift +++ b/SampoomManagement/Features/Part/Data/Repository/PartRepositoryImpl.swift @@ -9,9 +9,11 @@ import Foundation class PartRepositoryImpl: PartRepository { private let api: PartAPI + private let preferences: AuthPreferences - init(api: PartAPI) { + init(api: PartAPI, preferences: AuthPreferences) { self.api = api + self.preferences = preferences } func getCategoryList() async throws -> CategoryList { @@ -23,10 +25,16 @@ class PartRepositoryImpl: PartRepository { } func getPartList(groupId: Int) async throws -> PartList { - return try await api.getPartList(groupId: groupId) + guard let user = try preferences.getStoredUser() else { + throw NetworkError.unauthorized + } + return try await api.getPartList(agencyId: user.agencyId, groupId: groupId) } func searchParts(keyword: String, page: Int) async throws -> (results: [SearchResult], hasMore: Bool) { - return try await api.searchParts(keyword: keyword, page: page) + guard let user = try preferences.getStoredUser() else { + throw NetworkError.unauthorized + } + return try await api.searchParts(agencyId: user.agencyId, keyword: keyword, page: page) } } diff --git a/SampoomManagement/Features/Setting/UI/SettingUiEvent.swift b/SampoomManagement/Features/Setting/UI/SettingUiEvent.swift new file mode 100644 index 0000000..9ed9b3f --- /dev/null +++ b/SampoomManagement/Features/Setting/UI/SettingUiEvent.swift @@ -0,0 +1,15 @@ +// +// SettingUiEvent.swift +// SampoomManagement +// +// Created by Generated. +// + +import Foundation + +enum SettingUiEvent { + case loadProfile + case editProfile + case logout +} + diff --git a/SampoomManagement/Features/Setting/UI/SettingUiState.swift b/SampoomManagement/Features/Setting/UI/SettingUiState.swift new file mode 100644 index 0000000..8be2ab1 --- /dev/null +++ b/SampoomManagement/Features/Setting/UI/SettingUiState.swift @@ -0,0 +1,36 @@ +// +// SettingUiState.swift +// SampoomManagement +// +// Created by Generated. +// + +import Foundation + +struct SettingUiState { + let loading: Bool + let error: String? + + static let initial = SettingUiState( + loading: false, + error: nil + ) + + func copy( + loading: Bool? = nil, + error: String?? = nil + ) -> SettingUiState { + let resolvedError: String? + if let error = error { + resolvedError = error + } else { + resolvedError = self.error + } + + return SettingUiState( + loading: loading ?? self.loading, + error: resolvedError + ) + } +} + diff --git a/SampoomManagement/Features/Setting/UI/SettingView.swift b/SampoomManagement/Features/Setting/UI/SettingView.swift new file mode 100644 index 0000000..98a26da --- /dev/null +++ b/SampoomManagement/Features/Setting/UI/SettingView.swift @@ -0,0 +1,126 @@ +// +// SettingView.swift +// SampoomManagement +// +// Created by Generated. +// + +import SwiftUI + +struct SettingView: View { + @ObservedObject var viewModel: SettingViewModel + let onNavigateBack: () -> Void + let onLogoutClick: () -> Void + @State private var showLogoutDialog = false + + var body: some View { + VStack(spacing: 0) { + ScrollView { + VStack(spacing: 16) { + if let user = viewModel.user { + userSection(user: user) + } + + settingSection() + + Spacer(minLength: 100) + } + .padding(.horizontal, 16) + } + } + .navigationTitle(StringResources.Setting.title) + .navigationBarTitleDisplayMode(.large) + .background(Color.background) + .refreshable { + viewModel.onEvent(.loadProfile) + } + .onAppear { + viewModel.onEvent(.loadProfile) + } + .alert("로그아웃", isPresented: $showLogoutDialog) { + Button(StringResources.Common.cancel, role: .cancel) {} + Button(StringResources.Common.confirm) { + Task { + if await viewModel.logout() { + onLogoutClick() + } + } + } + } message: { + Text(StringResources.Setting.dialogLogout) + } + } + + private func userSection(user: User) -> some View { + HStack { + VStack(alignment: .leading, spacing: 8) { + Text(user.name) + .font(.gmarketTitle) + .foregroundColor(.text) + + Text(user.position) + .font(.gmarketBody) + .foregroundColor(.textSecondary) + } + + Spacer() + + VStack(alignment: .trailing, spacing: 8) { + Text(user.email) + .font(.gmarketBody) + .foregroundColor(.textSecondary) + + if let startedAt = user.startedAt, !startedAt.isEmpty { + Text(DateFormatterUtil.formatDate(startedAt)) + .font(.gmarketBody) + .foregroundColor(.textSecondary) + } + + if let endedAt = user.endedAt, !endedAt.isEmpty { + Text(DateFormatterUtil.formatDate(endedAt)) + .font(.gmarketBody) + .foregroundColor(.textSecondary) + } + } + } + .padding(.vertical, 16) + } + + private func settingSection() -> some View { + VStack(spacing: 8) { + Button(action: { + // TODO: Edit profile + viewModel.onEvent(.editProfile) + }) { + HStack { + Text(StringResources.Setting.editProfile) + .font(.gmarketBody) + .foregroundColor(.text) + Spacer() + Image(systemName: "chevron.right") + .foregroundColor(.textSecondary) + } + .padding(16) + .background(Color.backgroundCard) + .cornerRadius(12) + } + + Button(action: { + showLogoutDialog = true + }) { + HStack { + Text(StringResources.Setting.logout) + .font(.gmarketBody) + .foregroundColor(.text) + Spacer() + Image(systemName: "chevron.right") + .foregroundColor(.textSecondary) + } + .padding(16) + .background(Color.backgroundCard) + .cornerRadius(12) + } + } + } +} + diff --git a/SampoomManagement/Features/Setting/UI/SettingViewModel.swift b/SampoomManagement/Features/Setting/UI/SettingViewModel.swift new file mode 100644 index 0000000..431faae --- /dev/null +++ b/SampoomManagement/Features/Setting/UI/SettingViewModel.swift @@ -0,0 +1,63 @@ +// +// SettingViewModel.swift +// SampoomManagement +// +// Created by Generated. +// + +import Foundation +import SwiftUI +import Combine + +@MainActor +class SettingViewModel: ObservableObject { + @Published var uiState = SettingUiState.initial + @Published var user: User? + + private let authPreferences: AuthPreferences + private let signOutUseCase: SignOutUseCase + private let globalMessageHandler: GlobalMessageHandler + + init( + authPreferences: AuthPreferences, + signOutUseCase: SignOutUseCase, + globalMessageHandler: GlobalMessageHandler + ) { + self.authPreferences = authPreferences + self.signOutUseCase = signOutUseCase + self.globalMessageHandler = globalMessageHandler + } + + func onEvent(_ event: SettingUiEvent) { + switch event { + case .loadProfile: + loadProfile() + case .editProfile: + // TODO: Implement edit profile + break + case .logout: + // Deprecated in favor of explicit async logout() from the View + break + } + } + + private func loadProfile() { + do { + user = try authPreferences.getStoredUser() + } catch { + uiState = uiState.copy(error: error.localizedDescription) + } + } + + func logout() async -> Bool { + do { + try await signOutUseCase.execute() + return true + } catch { + let errorMessage = (error as? NetworkError)?.errorDescription ?? error.localizedDescription + globalMessageHandler.showMessage(errorMessage, isError: true) + return false + } + } +} +