diff --git a/.github/workflows/assign-issue-creator.yml b/.github/workflows/assign-issue-creator.yml new file mode 100644 index 0000000..6dde730 --- /dev/null +++ b/.github/workflows/assign-issue-creator.yml @@ -0,0 +1,14 @@ +# 각 레포지토리의 .github/workflows/assign-issue-creator.yml + +name: Assign issue creator + +on: + issues: + types: [opened] + +jobs: + call-reusable-workflow: + # @main 은 .github 레포지토리의 main 브랜치를 사용한다는 의미입니다. 버전을 위해 @v1과 같이 태그를 사용하는 것을 권장합니다. + uses: 33-Auto/.github/.github/workflows/reusable-assign-issue-creator.yml@main + # 이 워크플로우는 secrets를 전달할 필요가 없지만, 필요 시 아래와 같이 전달합니다. + # secrets: inherit \ No newline at end of file diff --git a/.github/workflows/close-issues-on-dev-merge.yml b/.github/workflows/close-issues-on-dev-merge.yml new file mode 100644 index 0000000..325a846 --- /dev/null +++ b/.github/workflows/close-issues-on-dev-merge.yml @@ -0,0 +1,19 @@ +# 각 레포지토리의 .github/workflows/close-issues-on-dev-merge.yml + +name: Auto Close Issues on dev merge + +on: + pull_request: + types: [closed] + +jobs: + call-reusable-workflow: + if: > + github.event.pull_request.merged == true && + github.event.pull_request.base.ref == 'dev' + uses: 33-Auto/.github/.github/workflows/reusable-close-linked-issues.yml@main + # with를 통해 재사용 워크플로우의 inputs에 값을 전달합니다. + with: + pr-body: ${{ github.event.pull_request.body }} + issue-number: ${{ github.event.pull_request.number }} + secrets: inherit # 재사용 워크플로우가 GITHUB_TOKEN을 사용할 수 있도록 전달 \ No newline at end of file diff --git a/.github/workflows/request-pr-review.yml b/.github/workflows/request-pr-review.yml new file mode 100644 index 0000000..66a31b9 --- /dev/null +++ b/.github/workflows/request-pr-review.yml @@ -0,0 +1,17 @@ +# 각 레포지토리의 .github/workflows/request-pr-review.yml + +name: PR Assignee & Team Review Request + +on: + pull_request: + types: [opened, reopened, ready_for_review] + +jobs: + call-reusable-workflow: + uses: 33-Auto/.github/.github/workflows/reusable-pr-assign-and-review.yml@main + with: + team-slug-for-review: "review_avengers" # 여기에 리뷰를 요청할 팀의 slug를 입력합니다. + pr-author: ${{ github.event.pull_request.user.login }} + pr-number: ${{ github.event.pull_request.number }} + secrets: + ORGANIZATION_TOKEN: ${{ secrets.ORGANIZATION_TOKEN }} # 재사용 워크플로우가 ORGANIZATION_TOKEN을 사용할 수 있도록 전달 diff --git a/SampoomManagement.xcodeproj/project.pbxproj b/SampoomManagement.xcodeproj/project.pbxproj index 1f2d7c1..ea94f0a 100644 --- a/SampoomManagement.xcodeproj/project.pbxproj +++ b/SampoomManagement.xcodeproj/project.pbxproj @@ -6,13 +6,31 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + 533528342E8BD99400F38FD1 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 533528332E8BD99400F38FD1 /* Alamofire */; }; + 5387CA3A2E8F676E005A3936 /* Swinject in Frameworks */ = {isa = PBXBuildFile; productRef = 5387CA392E8F676E005A3936 /* Swinject */; }; +/* End PBXBuildFile section */ + /* Begin PBXFileReference section */ 53A7B4BF2E8A43AF00BC946E /* SampoomManagement.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SampoomManagement.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 533528372E8BDAB300F38FD1 /* Exceptions for "SampoomManagement" folder in "SampoomManagement" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Resources/Info.plist, + ); + target = 53A7B4BE2E8A43AF00BC946E /* SampoomManagement */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + /* Begin PBXFileSystemSynchronizedRootGroup section */ 53A7B4C12E8A43AF00BC946E /* SampoomManagement */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 533528372E8BDAB300F38FD1 /* Exceptions for "SampoomManagement" folder in "SampoomManagement" target */, + ); path = SampoomManagement; sourceTree = ""; }; @@ -23,6 +41,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 533528342E8BD99400F38FD1 /* Alamofire in Frameworks */, + 5387CA3A2E8F676E005A3936 /* Swinject in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -65,6 +85,8 @@ ); name = SampoomManagement; packageProductDependencies = ( + 533528332E8BD99400F38FD1 /* Alamofire */, + 5387CA392E8F676E005A3936 /* Swinject */, ); productName = SampoomManagement; productReference = 53A7B4BF2E8A43AF00BC946E /* SampoomManagement.app */; @@ -94,6 +116,10 @@ ); mainGroup = 53A7B4B62E8A43AF00BC946E; minimizedProjectReferenceProxies = 1; + packageReferences = ( + 533528322E8BD99400F38FD1 /* XCRemoteSwiftPackageReference "Alamofire" */, + 5387CA382E8F676E005A3936 /* XCRemoteSwiftPackageReference "Swinject" */, + ); preferredProjectObjectVersion = 77; productRefGroup = 53A7B4C02E8A43AF00BC946E /* Products */; projectDirPath = ""; @@ -256,11 +282,13 @@ DEVELOPMENT_TEAM = B9PUAVBBKX; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "삼품관리"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -288,11 +316,13 @@ DEVELOPMENT_TEAM = B9PUAVBBKX; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "삼품관리"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -332,6 +362,38 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 533528322E8BD99400F38FD1 /* XCRemoteSwiftPackageReference "Alamofire" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Alamofire/Alamofire.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.10.2; + }; + }; + 5387CA382E8F676E005A3936 /* XCRemoteSwiftPackageReference "Swinject" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Swinject/Swinject.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.10.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 533528332E8BD99400F38FD1 /* Alamofire */ = { + isa = XCSwiftPackageProductDependency; + package = 533528322E8BD99400F38FD1 /* XCRemoteSwiftPackageReference "Alamofire" */; + productName = Alamofire; + }; + 5387CA392E8F676E005A3936 /* Swinject */ = { + isa = XCSwiftPackageProductDependency; + package = 5387CA382E8F676E005A3936 /* XCRemoteSwiftPackageReference "Swinject" */; + productName = Swinject; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 53A7B4B72E8A43AF00BC946E /* Project object */; } diff --git a/SampoomManagement.xcodeproj/xcshareddata/xcschemes/SampoomManagement.xcscheme b/SampoomManagement.xcodeproj/xcshareddata/xcschemes/SampoomManagement.xcscheme new file mode 100644 index 0000000..e823c3b --- /dev/null +++ b/SampoomManagement.xcodeproj/xcshareddata/xcschemes/SampoomManagement.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SampoomManagement/App/ContentView.swift b/SampoomManagement/App/ContentView.swift new file mode 100644 index 0000000..f22696f --- /dev/null +++ b/SampoomManagement/App/ContentView.swift @@ -0,0 +1,152 @@ +// +// ContentView.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import SwiftUI + +enum Tabs { + case dashboard, delivery, cart, orders, parts +} + +struct ContentView: View { + @StateObject private var partViewModel: PartViewModel + @State private var selectedTab: Tabs = .dashboard + + init() { + // DI Container에서 ViewModel 주입 + guard let viewModel = DIContainer.shared.resolve(PartViewModel.self) else { + fatalError("PartViewModel을 DIContainer에서 찾을 수 없습니다.") + } + _partViewModel = StateObject(wrappedValue: viewModel) + } + + var body: some View { + TabView(selection: $selectedTab) { + // Dashboard 탭 (임시) + Tab(value: .dashboard) { + NavigationStack { + VStack(spacing: 20) { + Spacer() + Text(StringResources.Tabs.dashboard) + .font(.largeTitle) + .fontWeight(.bold) + Text(StringResources.Placeholders.inventoryDescription) + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + Spacer() + } + .navigationTitle(StringResources.Tabs.dashboard) + } + } label: { + Label { + Text(StringResources.Tabs.dashboard) + } icon: { + Image("dashboard") + .renderingMode(.template) + .foregroundStyle(Color.text) + } + } + + // Delivery 탭 (임시) + Tab(value: .delivery) { + NavigationStack { + VStack(spacing: 20) { + Spacer() + Text(StringResources.Tabs.delivery) + .font(.largeTitle) + .fontWeight(.bold) + Text(StringResources.Placeholders.inventoryDescription) + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + Spacer() + } + .navigationTitle(StringResources.Tabs.delivery) + } + } label: { + Label { + Text(StringResources.Tabs.delivery) + } icon: { + Image("delivery") + .renderingMode(.template) + .foregroundStyle(Color.text) + } + } + + // Cart 탭 (임시) + Tab(value: .cart) { + NavigationStack { + VStack(spacing: 20) { + Spacer() + Text(StringResources.Tabs.cart) + .font(.largeTitle) + .fontWeight(.bold) + Text(StringResources.Placeholders.inventoryDescription) + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + Spacer() + } + .navigationTitle(StringResources.Tabs.cart) + } + } label: { + Label { + Text(StringResources.Tabs.cart) + } icon: { + Image("cart") + .renderingMode(.template) + .foregroundStyle(Color.text) + } + } + + // Orders 탭 (임시) + Tab(value: .orders) { + NavigationStack { + VStack(spacing: 20) { + Spacer() + Text(StringResources.Tabs.orders) + .font(.largeTitle) + .fontWeight(.bold) + Text(StringResources.Placeholders.inventoryDescription) + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + Spacer() + } + .navigationTitle(StringResources.Tabs.orders) + } + } label: { + Label { + Text(StringResources.Tabs.orders) + } icon: { + Image("orders") + .renderingMode(.template) + .foregroundStyle(Color.text) + } + } + + // PartView 탭 + Tab(value: .parts, role: .search) { + PartView() + .environmentObject(partViewModel) + } label: { + Label { + Text(StringResources.Tabs.parts) + } icon: { + Image("parts") + .renderingMode(.template) + .foregroundStyle(Color.text) + } + } + } + .accentColor(.blue) + } +} diff --git a/SampoomManagement/SampoomManagementApp.swift b/SampoomManagement/App/SampoomManagementApp.swift similarity index 72% rename from SampoomManagement/SampoomManagementApp.swift rename to SampoomManagement/App/SampoomManagementApp.swift index 0f7d72c..c04c678 100644 --- a/SampoomManagement/SampoomManagementApp.swift +++ b/SampoomManagement/App/SampoomManagementApp.swift @@ -9,6 +9,12 @@ import SwiftUI @main struct SampoomManagementApp: App { + + init() { + // DI Container 초기화 + _ = DIContainer.shared + } + var body: some Scene { WindowGroup { ContentView() diff --git a/SampoomManagement/ContentView.swift b/SampoomManagement/ContentView.swift deleted file mode 100644 index eee12f3..0000000 --- a/SampoomManagement/ContentView.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ContentView.swift -// SampoomManagement -// -// Created by 채상윤 on 9/29/25. -// - -import SwiftUI - -struct ContentView: View { - var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") - } - .padding() - } -} - -#Preview { - ContentView() -} diff --git a/SampoomManagement/Core/DI/CoreDIModule.swift b/SampoomManagement/Core/DI/CoreDIModule.swift new file mode 100644 index 0000000..898877c --- /dev/null +++ b/SampoomManagement/Core/DI/CoreDIModule.swift @@ -0,0 +1,20 @@ +// +// CoreDIModule.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import Foundation +import Swinject + +final class CoreDIModule: Assembly { + func assemble(container: Container) { + // MARK: - Core Layer Dependencies + + // NetworkManager 등록 + container.register(NetworkManager.self) { _ in + NetworkManager() + }.inObjectScope(.container) + } +} diff --git a/SampoomManagement/Core/DI/DIContainer.swift b/SampoomManagement/Core/DI/DIContainer.swift new file mode 100644 index 0000000..8813450 --- /dev/null +++ b/SampoomManagement/Core/DI/DIContainer.swift @@ -0,0 +1,32 @@ +// +// DIContainer.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import Foundation +import Swinject + +final class DIContainer { + static let shared = DIContainer() + + private let container: Container + private let assembler: Assembler + + private init() { + container = Container() + assembler = Assembler([ + CoreDIModule(), // Core 레벨 의존성 + PartDIModule() // Part Feature 의존성 + ], container: container) + } + + func resolve(_ type: T.Type) -> T? { + return container.resolve(type) + } + + func resolve(_ type: T.Type, name: String) -> T? { + return container.resolve(type, name: name) + } +} diff --git a/SampoomManagement/Core/Network/APIResponse.swift b/SampoomManagement/Core/Network/APIResponse.swift new file mode 100644 index 0000000..907cf15 --- /dev/null +++ b/SampoomManagement/Core/Network/APIResponse.swift @@ -0,0 +1,15 @@ +// +// APIResponse.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import Foundation + +struct APIResponse: Codable { + let status: Int + let success: Bool + let message: String + let data: T +} diff --git a/SampoomManagement/Core/Network/NetworkError.swift b/SampoomManagement/Core/Network/NetworkError.swift new file mode 100644 index 0000000..4333563 --- /dev/null +++ b/SampoomManagement/Core/Network/NetworkError.swift @@ -0,0 +1,31 @@ +// +// NetworkError.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import Foundation + +enum NetworkError: Error, LocalizedError { + case networkError(Error) + case decodingError(Error) + case invalidURL + case noData + case serverError(Int) + + var errorDescription: String? { + switch self { + case .networkError(let error): + return "네트워크 오류: \(error.localizedDescription)" + case .decodingError(let error): + return "데이터 파싱 오류: \(error.localizedDescription)" + case .invalidURL: + return "잘못된 URL" + case .noData: + return "데이터가 없습니다" + case .serverError(let code): + return "서버 오류: \(code)" + } + } +} diff --git a/SampoomManagement/Core/Network/NetworkManager.swift b/SampoomManagement/Core/Network/NetworkManager.swift new file mode 100644 index 0000000..ee13375 --- /dev/null +++ b/SampoomManagement/Core/Network/NetworkManager.swift @@ -0,0 +1,51 @@ +// +// NetworkManager.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import Foundation +import Alamofire + +class NetworkManager { + static let shared = NetworkManager() + + private let baseURL = "http://localhost:8080/api/" + + init() {} + + func request( + endpoint: String, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + responseType: T.Type, + completion: @escaping (Result, NetworkError>) -> Void + ) { + let url = baseURL + endpoint + + AF.request( + url, + method: method, + parameters: parameters, + encoding: JSONEncoding.default + ) + .responseData { response in + switch response.result { + case .success(let data): + Task { @MainActor in + do { + let apiResponse = try JSONDecoder().decode(APIResponse.self, from: data) + completion(.success(apiResponse)) + } catch { + completion(.failure(.decodingError(error))) + } + } + case .failure(let error): + Task { @MainActor in + completion(.failure(.networkError(error))) + } + } + } + } +} diff --git a/SampoomManagement/Core/Resources/StringResources.swift b/SampoomManagement/Core/Resources/StringResources.swift new file mode 100644 index 0000000..874dd17 --- /dev/null +++ b/SampoomManagement/Core/Resources/StringResources.swift @@ -0,0 +1,73 @@ +// +// StringResources.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import Foundation + +struct StringResources { + + // MARK: - App + struct App { + static let title = "SampoomManagement" + } + + // MARK: - Tabs + struct Tabs { + static let dashboard = "대시보드" + static let delivery = "출고목록" + static let cart = "장바구니" + static let orders = "주문관리" + static let parts = "부품조회" + static let employee = "직원관리" + static let setting = "설정" + } + + // MARK: - Messages + struct Messages { + static let loading = "로딩 중..." + static let errorTitle = "오류가 발생했습니다" + static let retryButton = "다시 시도" + static let emptyInventory = "인벤토리가 비어있습니다" + static let emptyStateMessage = "데이터가 없습니다" + } + + // MARK: - Navigation + struct Navigation { + static let back = "뒤로" + static let close = "닫기" + static let detail = "상세 보기" + } + + // MARK: - Placeholders + struct Placeholders { + static let inventoryDescription = "인벤토리 관리 기능이 들어갈 예정입니다" + static let profileDescription = "사용자 프로필 기능이 들어갈 예정입니다" + static let settingsDescription = "앱 설정 기능이 들어갈 예정입니다" + static let searchDescription = "검색 기능이 들어갈 예정입니다" + } + + // MARK: - Search + struct Search { + static let title = "검색" + } + + // MARK: - Detail + struct Detail { + static let title = "상세" + static let screenTitle = "상세 화면" + static let description = "이 화면은 탭바가 완전히 숨겨집니다!" + } + + // MARK: - Common + struct Common { + static let ok = "확인" + static let cancel = "취소" + static let save = "저장" + static let delete = "삭제" + static let edit = "편집" + static let done = "완료" + } +} diff --git a/SampoomManagement/Core/UI/Components/AppHeader.swift b/SampoomManagement/Core/UI/Components/AppHeader.swift new file mode 100644 index 0000000..54799c8 --- /dev/null +++ b/SampoomManagement/Core/UI/Components/AppHeader.swift @@ -0,0 +1,57 @@ +// +// AppHeader.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import SwiftUI + +struct AppHeader: View { + let title: String + let showBackButton: Bool + let onBackPressed: (() -> Void)? + + init( + title: String, + showBackButton: Bool = false, + onBackPressed: (() -> Void)? = nil + ) { + self.title = title + self.showBackButton = showBackButton + self.onBackPressed = onBackPressed + } + + var body: some View { + HStack { + if showBackButton { + Button(action: { + onBackPressed?() + }) { + Image(systemName: "chevron.left") + .font(.title2) + .foregroundColor(.primary) + } + } + + Text(title) + .font(.title2) + .fontWeight(.bold) + + Spacer() + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background(Color(.systemBackground)) + } +} + +#Preview { + VStack { + AppHeader(title: "인벤토리") + AppHeader(title: "상세보기", showBackButton: true) { + print("Back pressed") + } + Spacer() + } +} diff --git a/SampoomManagement/Core/UI/Components/CommonButton.swift b/SampoomManagement/Core/UI/Components/CommonButton.swift new file mode 100644 index 0000000..cb1f91a --- /dev/null +++ b/SampoomManagement/Core/UI/Components/CommonButton.swift @@ -0,0 +1,228 @@ +// +// CommonButton.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import SwiftUI + +// MARK: - Button Types +enum ButtonType { + case filled // 채워진 버튼 + case outlined // 테두리만 있는 버튼 +} + +// MARK: - Button Sizes +enum ButtonSize { + case small + case medium + case large + + var height: CGFloat { + switch self { + case .small: return 32 + case .medium: return 44 + case .large: return 52 + } + } + + var font: Font { + switch self { + case .small: return .system(size: 14, weight: .medium) + case .medium: return .system(size: 16, weight: .medium) + case .large: return .system(size: 18, weight: .semibold) + } + } +} + +// MARK: - CommonButton +struct CommonButton: View { + let title: String + let type: ButtonType + let size: ButtonSize + let icon: String? + let iconPosition: IconPosition + let isEnabled: Bool + let backgroundColor: Color? + let textColor: Color? + let borderColor: Color? + let action: () -> Void + + init( + _ title: String, + type: ButtonType = .filled, + size: ButtonSize = .medium, + icon: String? = nil, + iconPosition: IconPosition = .leading, + isEnabled: Bool = true, + backgroundColor: Color? = nil, + textColor: Color? = nil, + borderColor: Color? = nil, + action: @escaping () -> Void + ) { + self.title = title + self.type = type + self.size = size + self.icon = icon + self.iconPosition = iconPosition + self.isEnabled = isEnabled + self.backgroundColor = backgroundColor + self.textColor = textColor + self.borderColor = borderColor + self.action = action + } + + var body: some View { + Button(action: action) { + HStack(spacing: 8) { + if let icon = icon, iconPosition == .leading { + Image(systemName: icon) + .font(size.font) + } + + Text(title) + .font(size.font) + + if let icon = icon, iconPosition == .trailing { + Image(systemName: icon) + .font(size.font) + } + } + .frame(height: size.height) + .frame(maxWidth: .infinity) + .foregroundColor(buttonTextColor) + .background(buttonBackgroundColor) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(buttonBorderColor, lineWidth: borderWidth) + ) + .cornerRadius(8) + } + .disabled(!isEnabled) + .opacity(isEnabled ? 1.0 : 0.6) + .animation(.easeInOut(duration: 0.2), value: isEnabled) + } + + // MARK: - Button Styling + private var buttonBackgroundColor: Color { + if !isEnabled { + return .gray + } + + if let customColor = backgroundColor { + return customColor + } + + switch type { + case .filled: + return Color(red: 0.5, green: 0.2, blue: 0.8) // 기본 보라색 + case .outlined: + return .clear + } + } + + private var buttonTextColor: Color { + if !isEnabled { + return .white + } + + if let customColor = textColor { + return customColor + } + + switch type { + case .filled: + return .white + case .outlined: + return borderColor ?? .blue + } + } + + private var buttonBorderColor: Color { + if !isEnabled { + return .clear + } + + if let customColor = borderColor { + return customColor + } + + switch type { + case .filled: + return .clear + case .outlined: + return .blue + } + } + + private var borderWidth: CGFloat { + switch type { + case .filled: + return 0 + case .outlined: + return 1 + } + } +} + +// MARK: - Icon Position +enum IconPosition { + case leading + case trailing +} + +// MARK: - Preview +#Preview { + VStack(spacing: 16) { + // Filled Button (기본 보라색) + CommonButton("Button", type: .filled) { + print("Filled button tapped") + } + + // Filled Button with Custom Color + CommonButton("Button", type: .filled, backgroundColor: .blue, textColor: .white) { + print("Custom filled button tapped") + } + + // Filled Button with Icon + CommonButton("Button", type: .filled, icon: "phone.fill", backgroundColor: .green, textColor: .white) { + print("Filled button with icon tapped") + } + + // Outlined Button (기본 파란색) + CommonButton("Button", type: .outlined) { + print("Outlined button tapped") + } + + // Outlined Button with Custom Color + CommonButton("Button", type: .outlined, textColor: .red, borderColor: .red) { + print("Custom outlined button tapped") + } + + // Outlined Button (Gray) + CommonButton("Button", type: .outlined, textColor: .gray, borderColor: .gray) { + print("Gray outlined button tapped") + } + + // Disabled Button + CommonButton("Button", isEnabled: false) { + print("Disabled button tapped") + } + + // Size Examples + HStack(spacing: 16) { + CommonButton("Small", size: .small, backgroundColor: .orange) { } + CommonButton("Medium", size: .medium, backgroundColor: .purple) { } + CommonButton("Large", size: .large, backgroundColor: .pink) { } + } + + // Icon Position Examples + HStack(spacing: 16) { + CommonButton("Leading", icon: "star.fill", iconPosition: .leading, backgroundColor: .yellow, textColor: .black) { } + CommonButton("Trailing", type: .outlined, icon: "arrow.right", iconPosition: .trailing, textColor: .cyan, borderColor: .cyan) { } + } + } + .padding() + .background(Color.black) +} diff --git a/SampoomManagement/Core/UI/Components/CommonTextField.swift b/SampoomManagement/Core/UI/Components/CommonTextField.swift new file mode 100644 index 0000000..1225005 --- /dev/null +++ b/SampoomManagement/Core/UI/Components/CommonTextField.swift @@ -0,0 +1,265 @@ +// +// CommonTextField.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import SwiftUI + +// MARK: - TextField Types +enum TextFieldType { + case email + case password + case text + case number +} + +// MARK: - TextField Sizes +enum TextFieldSize { + case small + case medium + case large + + var height: CGFloat { + switch self { + case .small: return 36 + case .medium: return 44 + case .large: return 52 + } + } + + var font: Font { + switch self { + case .small: return .system(size: 14, weight: .regular) + case .medium: return .system(size: 16, weight: .regular) + case .large: return .system(size: 18, weight: .regular) + } + } + + var padding: EdgeInsets { + switch self { + case .small: return EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12) + case .medium: return EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) + case .large: return EdgeInsets(top: 16, leading: 20, bottom: 16, trailing: 20) + } + } +} + +// MARK: - CommonTextField +struct CommonTextField: View { + @Environment(\.colorScheme) private var colorScheme + @State private var isPasswordVisible = false + @State private var text = "" + + let placeholder: String + let type: TextFieldType + let size: TextFieldSize + let textColor: Color? + let backgroundColor: Color? + let borderColor: Color? + let onTextChange: (String) -> Void + + init( + placeholder: String, + type: TextFieldType = .text, + size: TextFieldSize = .medium, + textColor: Color? = nil, + backgroundColor: Color? = nil, + borderColor: Color? = nil, + onTextChange: @escaping (String) -> Void = { _ in } + ) { + self.placeholder = placeholder + self.type = type + self.size = size + self.textColor = textColor + self.backgroundColor = backgroundColor + self.borderColor = borderColor + self.onTextChange = onTextChange + } + + var body: some View { + HStack { + // Text Field + Group { + if type == .password && !isPasswordVisible { + SecureField(placeholder, text: $text) + .textFieldStyle(PlainTextFieldStyle()) + } else { + TextField(placeholder, text: $text) + .keyboardType(keyboardType) + .textInputAutocapitalization(autocapitalization) + .disableAutocorrection(disableAutocorrection) + .textFieldStyle(PlainTextFieldStyle()) + } + } + .font(size.font) + .foregroundColor(buttonTextColor) + + // Password Toggle Button (inside TextField) + if type == .password { + Button(action: { + isPasswordVisible.toggle() + }) { + Image(systemName: isPasswordVisible ? "eye.slash" : "eye") + .foregroundColor(iconColor) + .font(.system(size: 16, weight: .medium)) + } + .padding(.trailing, 8) + } + } + .padding(size.padding) + .background(buttonBackgroundColor) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(buttonBorderColor, lineWidth: 1) + ) + .onChange(of: text) { oldValue, newValue in + onTextChange(newValue) + } + } + + // MARK: - Computed Properties + private var buttonTextColor: Color { + if let textColor = textColor { + return textColor + } + + // 다크모드 고려한 기본 색상 + switch colorScheme { + case .dark: + return text.isEmpty ? .gray : .white + case .light: + return text.isEmpty ? .gray : .black + @unknown default: + return text.isEmpty ? .gray : .primary + } + } + + private var buttonBackgroundColor: Color { + if let backgroundColor = backgroundColor { + return backgroundColor + } + + // 다크모드 고려한 기본 배경색 + switch colorScheme { + case .dark: + return Color(red: 0.1, green: 0.1, blue: 0.1) // 매우 어두운 회색 + case .light: + return .white + @unknown default: + return Color(.systemBackground) + } + } + + private var buttonBorderColor: Color { + if let borderColor = borderColor { + return borderColor + } + + // 다크모드 고려한 기본 테두리색 + switch colorScheme { + case .dark: + return .gray.opacity(0.3) + case .light: + return .gray.opacity(0.3) + @unknown default: + return .gray.opacity(0.3) + } + } + + private var iconColor: Color { + switch colorScheme { + case .dark: + return .gray + case .light: + return .gray + @unknown default: + return .gray + } + } + + private var keyboardType: UIKeyboardType { + switch type { + case .email: + return .emailAddress + case .number: + return .numberPad + case .password, .text: + return .default + } + } + + private var autocapitalization: TextInputAutocapitalization { + switch type { + case .email: + return .never + case .password: + return .never + case .text: + return .sentences + case .number: + return .never + } + } + + private var disableAutocorrection: Bool { + switch type { + case .email, .password, .number: + return true + case .text: + return false + } + } +} + +// MARK: - Preview +#Preview { + VStack(spacing: 16) { + // Email Input (Placeholder) + CommonTextField( + placeholder: "이메일 입력", + type: .email + ) { text in + print("Email: \(text)") + } + + // Email Input (Filled) + CommonTextField( + placeholder: "이메일 입력", + type: .email + ) { text in + print("Email: \(text)") + } + + // Password Input (Placeholder) + CommonTextField( + placeholder: "비밀번호 입력", + type: .password + ) { text in + print("Password: \(text)") + } + + // Password Input (Filled) + CommonTextField( + placeholder: "비밀번호 입력", + type: .password + ) { text in + print("Password: \(text)") + } + + // Custom Colors + CommonTextField( + placeholder: "커스텀 색상", + type: .text, + textColor: .blue, + backgroundColor: .yellow.opacity(0.1), + borderColor: .blue + ) { text in + print("Custom: \(text)") + } + } + .padding() + .background(Color(.systemBackground)) +} + diff --git a/SampoomManagement/Core/UI/Components/EmptyView.swift b/SampoomManagement/Core/UI/Components/EmptyView.swift new file mode 100644 index 0000000..95e4a6d --- /dev/null +++ b/SampoomManagement/Core/UI/Components/EmptyView.swift @@ -0,0 +1,56 @@ +// +// EmptyView.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import SwiftUI + +struct EmptyStateView: View { + let icon: String + let title: String + let message: String? + + init( + icon: String = "tray", + title: String = "데이터가 없습니다", + message: String? = nil + ) { + self.icon = icon + self.title = title + self.message = message + } + + var body: some View { + VStack(spacing: 16) { + Spacer() + + Image(systemName: icon) + .font(.system(size: 48)) + .foregroundColor(.secondary) + + Text(title) + .font(.headline) + .foregroundColor(.secondary) + + if let message = message { + Text(message) + .font(.caption) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + } + + Spacer() + } + } +} + +#Preview { + EmptyStateView( + icon: "tray", + title: "인벤토리가 비어있습니다", + message: "새로운 부품을 추가해보세요" + ) +} diff --git a/SampoomManagement/Core/UI/Components/ErrorView.swift b/SampoomManagement/Core/UI/Components/ErrorView.swift new file mode 100644 index 0000000..898e1b1 --- /dev/null +++ b/SampoomManagement/Core/UI/Components/ErrorView.swift @@ -0,0 +1,46 @@ +// +// ErrorView.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import SwiftUI + +struct ErrorView: View { + let error: String + let onRetry: () -> Void + + var body: some View { + VStack(spacing: 16) { + Spacer() + + Image(systemName: "exclamationmark.triangle") + .font(.system(size: 48)) + .foregroundColor(.red) + + Text("오류가 발생했습니다") + .font(.headline) + .foregroundColor(.red) + + Text(error) + .font(.caption) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal, 32) + + Button("다시 시도") { + onRetry() + } + .buttonStyle(.borderedProminent) + + Spacer() + } + } +} + +#Preview { + ErrorView(error: "네트워크 연결을 확인해주세요") { + print("Retry tapped") + } +} diff --git a/SampoomManagement/Core/UI/Components/LoadingView.swift b/SampoomManagement/Core/UI/Components/LoadingView.swift new file mode 100644 index 0000000..1717ac0 --- /dev/null +++ b/SampoomManagement/Core/UI/Components/LoadingView.swift @@ -0,0 +1,35 @@ +// +// LoadingView.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import SwiftUI + +struct LoadingView: View { + let message: String + + init(message: String = "로딩 중...") { + self.message = message + } + + var body: some View { + VStack(spacing: 16) { + Spacer() + + ProgressView() + .scaleEffect(1.2) + + Text(message) + .font(.caption) + .foregroundColor(.secondary) + + Spacer() + } + } +} + +#Preview { + LoadingView() +} diff --git a/SampoomManagement/Core/UI/State/UIState.swift b/SampoomManagement/Core/UI/State/UIState.swift new file mode 100644 index 0000000..834e9c0 --- /dev/null +++ b/SampoomManagement/Core/UI/State/UIState.swift @@ -0,0 +1,42 @@ +// +// UIState.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import Foundation + +protocol UIState { + var loading: Bool { get } + var error: String? { get } + var success: Bool { get } +} + +struct BaseUIState: UIState { + let loading: Bool + let error: String? + let success: Bool + + init( + loading: Bool = false, + error: String? = nil, + success: Bool = false + ) { + self.loading = loading + self.error = error + self.success = success + } + + func copy( + loading: Bool? = nil, + error: String? = nil, + success: Bool? = nil + ) -> BaseUIState { + return BaseUIState( + loading: loading ?? self.loading, + error: error ?? self.error, + success: success ?? self.success + ) + } +} diff --git a/SampoomManagement/Features/Part/DI/PartDIModule.swift b/SampoomManagement/Features/Part/DI/PartDIModule.swift new file mode 100644 index 0000000..b628e6c --- /dev/null +++ b/SampoomManagement/Features/Part/DI/PartDIModule.swift @@ -0,0 +1,35 @@ +// +// PartDIModule.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import Foundation +import Swinject + +final class PartDIModule: Assembly { + func assemble(container: Container) { + // MARK: - Part Feature Dependencies + + // PartAPI 등록 + container.register(PartAPI.self) { resolver in + PartAPI(networkManager: resolver.resolve(NetworkManager.self)!) + }.inObjectScope(.container) + + // PartRepository 등록 (Interface -> Implementation) + container.register(PartRepository.self) { resolver in + PartRepositoryImpl(api: resolver.resolve(PartAPI.self)!) + }.inObjectScope(.container) + + // GetPartUseCase 등록 + container.register(GetPartUseCase.self) { resolver in + GetPartUseCase(repository: resolver.resolve(PartRepository.self)!) + }.inObjectScope(.container) + + // PartViewModel 등록 + container.register(PartViewModel.self) { resolver in + PartViewModel(getPartUseCase: resolver.resolve(GetPartUseCase.self)!) + }.inObjectScope(.container) + } +} diff --git a/SampoomManagement/Features/Part/Data/Mappers/PartMappers.swift b/SampoomManagement/Features/Part/Data/Mappers/PartMappers.swift new file mode 100644 index 0000000..c27e5f5 --- /dev/null +++ b/SampoomManagement/Features/Part/Data/Mappers/PartMappers.swift @@ -0,0 +1,18 @@ +// +// PartMappers.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import Foundation + +extension PartDTO { + func toModel() -> Part { + return Part( + id: self.id, + name: self.name, + count: self.count + ) + } +} diff --git a/SampoomManagement/Features/Part/Data/Remote/API/PartAPI.swift b/SampoomManagement/Features/Part/Data/Remote/API/PartAPI.swift new file mode 100644 index 0000000..9def440 --- /dev/null +++ b/SampoomManagement/Features/Part/Data/Remote/API/PartAPI.swift @@ -0,0 +1,34 @@ +// +// PartAPI.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import Foundation + +class PartAPI { + private let networkManager: NetworkManager + + init(networkManager: NetworkManager) { + self.networkManager = networkManager + } + + func getPartList() async throws -> PartList { + return try await withCheckedThrowingContinuation { continuation in + networkManager.request( + endpoint: "part", + responseType: [PartDTO].self + ) { result in + switch result { + case .success(let response): + let parts = response.data.map { $0.toModel() } + let partList = PartList(items: parts) + continuation.resume(returning: partList) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } +} diff --git a/SampoomManagement/Features/Part/Data/Remote/DTO/PartDTO.swift b/SampoomManagement/Features/Part/Data/Remote/DTO/PartDTO.swift new file mode 100644 index 0000000..4f3c96c --- /dev/null +++ b/SampoomManagement/Features/Part/Data/Remote/DTO/PartDTO.swift @@ -0,0 +1,14 @@ +// +// PartDTO.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import Foundation + +struct PartDTO: Codable { + let id: Int + let name: String + let count: Int +} diff --git a/SampoomManagement/Features/Part/Data/Repository/PartRepositoryImpl.swift b/SampoomManagement/Features/Part/Data/Repository/PartRepositoryImpl.swift new file mode 100644 index 0000000..740f47f --- /dev/null +++ b/SampoomManagement/Features/Part/Data/Repository/PartRepositoryImpl.swift @@ -0,0 +1,20 @@ +// +// PartRepositoryImpl.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import Foundation + +class PartRepositoryImpl: PartRepository { + private let api: PartAPI + + init(api: PartAPI) { + self.api = api + } + + func getPartList() async throws -> PartList { + return try await api.getPartList() + } +} diff --git a/SampoomManagement/Features/Part/Domain/Models/Part.swift b/SampoomManagement/Features/Part/Domain/Models/Part.swift new file mode 100644 index 0000000..3f3a96a --- /dev/null +++ b/SampoomManagement/Features/Part/Domain/Models/Part.swift @@ -0,0 +1,14 @@ +// +// Part.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import Foundation + +struct Part: Identifiable, Codable, Equatable { + let id: Int + let name: String + let count: Int +} diff --git a/SampoomManagement/Features/Part/Domain/Models/PartList.swift b/SampoomManagement/Features/Part/Domain/Models/PartList.swift new file mode 100644 index 0000000..832c1e2 --- /dev/null +++ b/SampoomManagement/Features/Part/Domain/Models/PartList.swift @@ -0,0 +1,23 @@ +// +// PartList.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import Foundation + +struct PartList: Codable, Equatable { + let items: [Part] + var totalCount: Int { items.count } + var isEmpty: Bool { items.isEmpty } + + init(items: [Part]) { + self.items = items + } + + static func empty() -> PartList { + return PartList(items: []) + } +} + diff --git a/SampoomManagement/Features/Part/Domain/Repository/PartRepository.swift b/SampoomManagement/Features/Part/Domain/Repository/PartRepository.swift new file mode 100644 index 0000000..3a703b1 --- /dev/null +++ b/SampoomManagement/Features/Part/Domain/Repository/PartRepository.swift @@ -0,0 +1,12 @@ +// +// PartRepository.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import Foundation + +protocol PartRepository { + func getPartList() async throws -> PartList +} diff --git a/SampoomManagement/Features/Part/Domain/UseCase/GetPartUseCase.swift b/SampoomManagement/Features/Part/Domain/UseCase/GetPartUseCase.swift new file mode 100644 index 0000000..1e9fa32 --- /dev/null +++ b/SampoomManagement/Features/Part/Domain/UseCase/GetPartUseCase.swift @@ -0,0 +1,20 @@ +// +// GetPartUseCase.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import Foundation + +class GetPartUseCase { + private let repository: PartRepository + + init(repository: PartRepository) { + self.repository = repository + } + + func execute() async throws -> PartList { + return try await repository.getPartList() + } +} diff --git a/SampoomManagement/Features/Part/UI/PartItemView.swift b/SampoomManagement/Features/Part/UI/PartItemView.swift new file mode 100644 index 0000000..6092fd8 --- /dev/null +++ b/SampoomManagement/Features/Part/UI/PartItemView.swift @@ -0,0 +1,39 @@ +// +// PartItemView.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import SwiftUI + +struct PartItemView: View { + let part: Part + + var body: some View { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(part.name) + .font(.headline) + .fontWeight(.semibold) + + Text("ID: \(part.id)") + .font(.caption) + .foregroundColor(.secondary) + } + + Spacer() + + Text("\(part.count)개") + .font(.title2) + .fontWeight(.bold) + .foregroundColor(.blue) + } + .padding(16) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Color(.systemBackground)) + .shadow(color: .black.opacity(0.1), radius: 2, x: 0, y: 1) + ) + } +} diff --git a/SampoomManagement/Features/Part/UI/PartUIState.swift b/SampoomManagement/Features/Part/UI/PartUIState.swift new file mode 100644 index 0000000..8f5e289 --- /dev/null +++ b/SampoomManagement/Features/Part/UI/PartUIState.swift @@ -0,0 +1,41 @@ +// +// PartUIState.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import Foundation + +struct PartUIState: UIState { + let loading: Bool + let error: String? + let success: Bool + let partList: [Part] + + init( + loading: Bool = false, + error: String? = nil, + success: Bool = false, + partList: [Part] = [] + ) { + self.loading = loading + self.error = error + self.success = success + self.partList = partList + } + + func copy( + loading: Bool? = nil, + error: String? = nil, + success: Bool? = nil, + partList: [Part]? = nil + ) -> PartUIState { + return PartUIState( + loading: loading ?? self.loading, + error: error ?? self.error, + success: success ?? self.success, + partList: partList ?? self.partList + ) + } +} diff --git a/SampoomManagement/Features/Part/UI/PartView.swift b/SampoomManagement/Features/Part/UI/PartView.swift new file mode 100644 index 0000000..aee7f01 --- /dev/null +++ b/SampoomManagement/Features/Part/UI/PartView.swift @@ -0,0 +1,65 @@ +// +// PartView.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import SwiftUI + +struct PartView: View { + @EnvironmentObject var viewModel: PartViewModel + @State var searchString = "" + + var body: some View { + NavigationStack { + VStack(spacing: 0) { + contentView + } + .navigationBarTitle(Text("부품")) + .searchable(text: $searchString) + } + } + + @ViewBuilder + private var contentView: some View { + if viewModel.uiState.loading { + loadingView + } else if let error = viewModel.uiState.error { + errorView(error: error) + } else if viewModel.uiState.partList.isEmpty { + emptyView + } else { + listView + } + } + + private var loadingView: some View { + LoadingView() + } + + private func errorView(error: String) -> some View { + ErrorView(error: error) { + viewModel.refreshPart() + } + } + + private var emptyView: some View { + EmptyStateView( + icon: "tray", + title: "인벤토리가 비어있습니다" + ) + } + + private var listView: some View { + ScrollView { + LazyVStack(spacing: 8) { + ForEach(viewModel.uiState.partList) { part in + PartItemView(part: part) + } + } + .padding(.horizontal, 16) + .padding(.vertical, 8) + } + } +} diff --git a/SampoomManagement/Features/Part/UI/PartViewModel.swift b/SampoomManagement/Features/Part/UI/PartViewModel.swift new file mode 100644 index 0000000..1297624 --- /dev/null +++ b/SampoomManagement/Features/Part/UI/PartViewModel.swift @@ -0,0 +1,47 @@ +// +// PartViewModel.swift +// SampoomManagement +// +// Created by 채상윤 on 9/29/25. +// + +import Foundation +import SwiftUI +import Combine + +@MainActor +class PartViewModel: ObservableObject { + @Published var uiState = PartUIState() + + private let getPartUseCase: GetPartUseCase + + init(getPartUseCase: GetPartUseCase) { + self.getPartUseCase = getPartUseCase + loadPart() + } + + private func loadPart() { + Task { + uiState = uiState.copy(loading: true, error: nil) + + do { + let partList = try await getPartUseCase.execute() + uiState = uiState.copy( + loading: false, + success: true, + partList: partList.items + ) + } catch { + uiState = uiState.copy( + loading: false, + error: error.localizedDescription + ) + } + } + } + + func refreshPart() { + loadPart() + } +} + diff --git a/SampoomManagement/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/SampoomManagement/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..4c7c8aa --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0x80", + "red" : "0x80" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SampoomManagement/Assets.xcassets/AppIcon.appiconset/Contents.json b/SampoomManagement/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 81% rename from SampoomManagement/Assets.xcassets/AppIcon.appiconset/Contents.json rename to SampoomManagement/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index 2305880..a93b0d7 100644 --- a/SampoomManagement/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/SampoomManagement/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "sampoom_1024_light.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" @@ -12,6 +13,7 @@ "value" : "dark" } ], + "filename" : "sampoom_1024_dark.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" @@ -23,6 +25,7 @@ "value" : "tinted" } ], + "filename" : "sampoom_1024_light 1.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/SampoomManagement/Resources/Assets.xcassets/AppIcon.appiconset/sampoom_1024_dark.png b/SampoomManagement/Resources/Assets.xcassets/AppIcon.appiconset/sampoom_1024_dark.png new file mode 100644 index 0000000..a270505 Binary files /dev/null and b/SampoomManagement/Resources/Assets.xcassets/AppIcon.appiconset/sampoom_1024_dark.png differ diff --git a/SampoomManagement/Resources/Assets.xcassets/AppIcon.appiconset/sampoom_1024_light 1.png b/SampoomManagement/Resources/Assets.xcassets/AppIcon.appiconset/sampoom_1024_light 1.png new file mode 100644 index 0000000..0544cad Binary files /dev/null and b/SampoomManagement/Resources/Assets.xcassets/AppIcon.appiconset/sampoom_1024_light 1.png differ diff --git a/SampoomManagement/Resources/Assets.xcassets/AppIcon.appiconset/sampoom_1024_light.png b/SampoomManagement/Resources/Assets.xcassets/AppIcon.appiconset/sampoom_1024_light.png new file mode 100644 index 0000000..0544cad Binary files /dev/null and b/SampoomManagement/Resources/Assets.xcassets/AppIcon.appiconset/sampoom_1024_light.png differ diff --git a/SampoomManagement/Resources/Assets.xcassets/Background.colorset/Contents.json b/SampoomManagement/Resources/Assets.xcassets/Background.colorset/Contents.json new file mode 100644 index 0000000..12e74dd --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/Background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF5", + "green" : "0xF5", + "red" : "0xF5" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x1B", + "green" : "0x18", + "red" : "0x17" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SampoomManagement/Resources/Assets.xcassets/Background_Card.colorset/Contents.json b/SampoomManagement/Resources/Assets.xcassets/Background_Card.colorset/Contents.json new file mode 100644 index 0000000..8a7172d --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/Background_Card.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x3F", + "green" : "0x39", + "red" : "0x36" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SampoomManagement/Assets.xcassets/Contents.json b/SampoomManagement/Resources/Assets.xcassets/Contents.json similarity index 100% rename from SampoomManagement/Assets.xcassets/Contents.json rename to SampoomManagement/Resources/Assets.xcassets/Contents.json diff --git a/SampoomManagement/Resources/Assets.xcassets/Text.colorset/Contents.json b/SampoomManagement/Resources/Assets.xcassets/Text.colorset/Contents.json new file mode 100644 index 0000000..9b2acbc --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/Text.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x3F", + "green" : "0x39", + "red" : "0x36" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SampoomManagement/Assets.xcassets/AccentColor.colorset/Contents.json b/SampoomManagement/Resources/Assets.xcassets/cart.imageset/Contents.json similarity index 70% rename from SampoomManagement/Assets.xcassets/AccentColor.colorset/Contents.json rename to SampoomManagement/Resources/Assets.xcassets/cart.imageset/Contents.json index eb87897..c3c69df 100644 --- a/SampoomManagement/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/SampoomManagement/Resources/Assets.xcassets/cart.imageset/Contents.json @@ -1,6 +1,7 @@ { - "colors" : [ + "images" : [ { + "filename" : "cart.svg", "idiom" : "universal" } ], diff --git a/SampoomManagement/Resources/Assets.xcassets/cart.imageset/cart.svg b/SampoomManagement/Resources/Assets.xcassets/cart.imageset/cart.svg new file mode 100644 index 0000000..6df8577 --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/cart.imageset/cart.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/SampoomManagement/Resources/Assets.xcassets/dashboard.imageset/Contents.json b/SampoomManagement/Resources/Assets.xcassets/dashboard.imageset/Contents.json new file mode 100644 index 0000000..9475a51 --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/dashboard.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "dashboard.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SampoomManagement/Resources/Assets.xcassets/dashboard.imageset/dashboard.svg b/SampoomManagement/Resources/Assets.xcassets/dashboard.imageset/dashboard.svg new file mode 100644 index 0000000..6374b08 --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/dashboard.imageset/dashboard.svg @@ -0,0 +1,3 @@ + + + diff --git a/SampoomManagement/Resources/Assets.xcassets/delivery.imageset/Contents.json b/SampoomManagement/Resources/Assets.xcassets/delivery.imageset/Contents.json new file mode 100644 index 0000000..b5b9b10 --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/delivery.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "delivery.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SampoomManagement/Resources/Assets.xcassets/delivery.imageset/delivery.svg b/SampoomManagement/Resources/Assets.xcassets/delivery.imageset/delivery.svg new file mode 100644 index 0000000..224eaca --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/delivery.imageset/delivery.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/SampoomManagement/Resources/Assets.xcassets/employee.imageset/Contents.json b/SampoomManagement/Resources/Assets.xcassets/employee.imageset/Contents.json new file mode 100644 index 0000000..89cc598 --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/employee.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "employee.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SampoomManagement/Resources/Assets.xcassets/employee.imageset/employee.svg b/SampoomManagement/Resources/Assets.xcassets/employee.imageset/employee.svg new file mode 100644 index 0000000..13048c4 --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/employee.imageset/employee.svg @@ -0,0 +1,3 @@ + + + diff --git a/SampoomManagement/Resources/Assets.xcassets/orders.imageset/Contents.json b/SampoomManagement/Resources/Assets.xcassets/orders.imageset/Contents.json new file mode 100644 index 0000000..9db145f --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/orders.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "orders.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SampoomManagement/Resources/Assets.xcassets/orders.imageset/orders.svg b/SampoomManagement/Resources/Assets.xcassets/orders.imageset/orders.svg new file mode 100644 index 0000000..02401b0 --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/orders.imageset/orders.svg @@ -0,0 +1,3 @@ + + + diff --git a/SampoomManagement/Resources/Assets.xcassets/parts.imageset/Contents.json b/SampoomManagement/Resources/Assets.xcassets/parts.imageset/Contents.json new file mode 100644 index 0000000..b08d848 --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/parts.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "parts.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SampoomManagement/Resources/Assets.xcassets/parts.imageset/parts.svg b/SampoomManagement/Resources/Assets.xcassets/parts.imageset/parts.svg new file mode 100644 index 0000000..e8b992e --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/parts.imageset/parts.svg @@ -0,0 +1,3 @@ + + + diff --git a/SampoomManagement/Resources/Assets.xcassets/search.imageset/Contents.json b/SampoomManagement/Resources/Assets.xcassets/search.imageset/Contents.json new file mode 100644 index 0000000..4a3f4da --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/search.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "search.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SampoomManagement/Resources/Assets.xcassets/search.imageset/search.svg b/SampoomManagement/Resources/Assets.xcassets/search.imageset/search.svg new file mode 100644 index 0000000..35a89bd --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/search.imageset/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/SampoomManagement/Resources/Assets.xcassets/settings.imageset/Contents.json b/SampoomManagement/Resources/Assets.xcassets/settings.imageset/Contents.json new file mode 100644 index 0000000..8446dd8 --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/settings.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "settings.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SampoomManagement/Resources/Assets.xcassets/settings.imageset/settings.svg b/SampoomManagement/Resources/Assets.xcassets/settings.imageset/settings.svg new file mode 100644 index 0000000..4ca7228 --- /dev/null +++ b/SampoomManagement/Resources/Assets.xcassets/settings.imageset/settings.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/SampoomManagement/Resources/Info.plist b/SampoomManagement/Resources/Info.plist new file mode 100644 index 0000000..781d890 --- /dev/null +++ b/SampoomManagement/Resources/Info.plist @@ -0,0 +1,7 @@ + + + + + + +