diff --git a/StockMate/StockMate/ContentView.swift b/StockMate/StockMate/ContentView.swift index 882bebe..d576e34 100644 --- a/StockMate/StockMate/ContentView.swift +++ b/StockMate/StockMate/ContentView.swift @@ -16,6 +16,15 @@ struct ContentView: View { Text("임시 화면") } .padding() + HStack(spacing: 20) { + Image(systemName: "gearshape") + .font(.system(size: 40)) + .foregroundColor(.blue) + + Image(systemName: "lightbulb") + .font(.system(size: 40)) + .foregroundColor(.cyan) + } } } diff --git a/StockMate/StockMate/app/core/components/CartCard.swift b/StockMate/StockMate/app/core/components/CartCard.swift new file mode 100644 index 0000000..1d959d0 --- /dev/null +++ b/StockMate/StockMate/app/core/components/CartCard.swift @@ -0,0 +1,128 @@ +// +// CartCard.swift +// StockMate +// +// Created by Admin on 10/27/25. +// + +import SwiftUI + +struct CartCard: View { + let item: CartItem + let quantity: Int + let onIncrease: () -> Void + let onDecrease: () -> Void + let onAddToCart: (() -> Void)? + let onRemoveFromCart: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text(item.categoryName ?? "") + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(.black) + + Divider().frame(height: 0.2).background(Color.textGray2) + + HStack(alignment: .center, spacing: 12) { + AsyncImage(url: URL(string: item.image ?? "")) { image in + image.resizable().scaledToFit() + } placeholder: { + Color.gray.opacity(0.2) + } + .frame(width: 64, height: 64) + .cornerRadius(10) + + VStack(alignment: .leading, spacing: 6) { + Text(item.brand ?? "") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.black) + .lineLimit(2) + + Text("\((item.trim ?? "")) / \((item.model ?? ""))") + .font(.system(size: 13)) + .foregroundColor(.gray) + .lineLimit(1) + + Text("\(item.price ?? 0)원") + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(.black) + +// Text("\(item.trim) / \(item.model)") +// .font(.system(size: 13)) +// .foregroundColor(.gray) +// +// Text("\(item.price)원") +// .font(.system(size: 13, weight: .semibold)) +// .foregroundColor(.black) + } + + Spacer() + + // 🪄 수량에 따른 3단계 분기 + if quantity == 0 { + if let onAddToCart = onAddToCart { + Button(action: onAddToCart) { + Image(systemName: "cart.badge.plus") + .font(.system(size: 18)) + .foregroundColor(.Primary) + .padding(10) + .background(Color.Primary.opacity(0.1)) + .clipShape(Circle()) + } + } + } else if quantity == 1 { + HStack(spacing: 10) { + Button(action: onRemoveFromCart) { + Image(systemName: "trash") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.red) + } + + Text("1") + .font(.system(size: 15, weight: .semibold)) + .frame(width: 24) + + Button(action: onIncrease) { + Image(systemName: "plus") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.Primary) + } + } + .padding(.vertical, 6) + .padding(.horizontal, 10) + .background(Color.white) + .cornerRadius(10) + .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) + + } else { + HStack(spacing: 10) { + Button(action: onDecrease) { + Image(systemName: "minus") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.gray) + } + + Text("\(quantity)") + .font(.system(size: 15, weight: .semibold)) + .frame(width: 24) + + Button(action: onIncrease) { + Image(systemName: "plus") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.Primary) + } + } + .padding(.vertical, 6) + .padding(.horizontal, 10) + .background(Color.white) + .cornerRadius(10) + .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) + } + } + } + .padding() + .background(Color.white) + .cornerRadius(14) + .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) + } +} diff --git a/StockMate/StockMate/app/core/components/CartInfoCard.swift b/StockMate/StockMate/app/core/components/CartInfoCard.swift new file mode 100644 index 0000000..d38e0f1 --- /dev/null +++ b/StockMate/StockMate/app/core/components/CartInfoCard.swift @@ -0,0 +1,86 @@ +// +// CartInfoCard.swift +// StockMate +// +// Created by Admin on 10/27/25. +// + +import SwiftUI + +struct CartInfoCard: View { + let item: CartItem + let quantity: Int + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text(item.categoryName ?? "") + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(.black) + + Divider().frame(height: 0.2).background(Color.textGray2) + + HStack(alignment: .center, spacing: 12) { + AsyncImage(url: URL(string: item.image ?? "")) { image in + image.resizable().scaledToFit() + } placeholder: { + Color.gray.opacity(0.2) + } + .frame(width: 64, height: 64) + .cornerRadius(10) + + + VStack(alignment: .leading, spacing: 6) { + Text(item.brand ?? "") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.black) + .lineLimit(2) + + HStack{ + Text("\((item.trim ?? "")) / \((item.model ?? ""))") + .font(.system(size: 13)) + .foregroundColor(.gray) + .lineLimit(1) + + Spacer() + + Text("\((item.price ?? 0) * item.amount)원") + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(.black) + .lineLimit(1) + + } + Text("\(item.price ?? 0)원 / \(item.amount)개") + .font(.system(size: 13)) + .foregroundColor(.gray) + .lineLimit(1) + } + } + } + .padding() + .background(Color.white) + .cornerRadius(14) +// .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) + } +} + +#Preview { + CartInfoCard( + item: CartItem( + cartItemId: 1, + partId: 101, + amount: 2, + partName: "실린더 어셈블리 브레이크 마스터", + categoryName: "엔진/미션", + brand: "실린더 어셈블리 브레이크 마스터", + model: "아반떼 MD", + trim: "중형", + price: 60000, + stock: 30, + image: "" + ), + quantity: 2 + ) + .previewLayout(.sizeThatFits) + .padding() + .background(Color.Light) +} diff --git a/StockMate/StockMate/app/core/components/CartSummaryBar.swift b/StockMate/StockMate/app/core/components/CartSummaryBar.swift new file mode 100644 index 0000000..b4aea61 --- /dev/null +++ b/StockMate/StockMate/app/core/components/CartSummaryBar.swift @@ -0,0 +1,50 @@ +// +// CartSummaryBar.swift +// StockMate +// +// Created by Admin on 10/26/25. +// + +import Foundation +import SwiftUI + +struct CartSummaryBar: View { + @ObservedObject var cartVM: CartViewModel + + var body: some View { + VStack { + Spacer() + + if let cart = cartVM.cart, + !cart.items.isEmpty { + NavigationLink(destination: OrderCartView(cartViewModel: cartVM)) { + HStack(spacing: 12) { + Circle() + .fill(Color.white) + .frame(width: 20, height: 20) + .overlay( + Text("\(cart.items.count)") + .font(.system(size: 11, weight: .bold)) + .foregroundColor(.Primary) + ) + + Text("장바구니 보기") + .font(.system(size: 16, weight: .bold)) + .foregroundColor(.white) + + Spacer() + + Text("\(cart.totalPrice ?? 0)원") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.white) + } + .padding(.horizontal, 30) + .frame(height: 60) + .background(Color.Primary) + .clipShape(RoundedCorner(radius: 16, corners: [.topLeft, .topRight])) + } + } + } + .ignoresSafeArea(edges: .bottom) + } +} diff --git a/StockMate/StockMate/app/core/components/OrderRequestCardView.swift b/StockMate/StockMate/app/core/components/OrderRequestCardView.swift new file mode 100644 index 0000000..aa29bef --- /dev/null +++ b/StockMate/StockMate/app/core/components/OrderRequestCardView.swift @@ -0,0 +1,118 @@ +// +// OrderRequestCardView.swift +// StockMate +// +// Created by Admin on 10/20/25. +// + +import SwiftUI + +struct OrderRequestCardView: View { + let item: InventoryItem + let quantity: Int + let onIncrease: () -> Void + let onDecrease: () -> Void + let onAddToCart: () -> Void + let onRemoveFromCart: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text(item.categoryName) + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(.black) + + Divider().frame(height: 0.2).background(Color.textGray2) + + HStack(alignment: .center, spacing: 12) { + AsyncImage(url: URL(string: item.image)) { image in + image.resizable().scaledToFit() + } placeholder: { + Color.gray.opacity(0.2) + } + .frame(width: 64, height: 64) + .cornerRadius(10) + + VStack(alignment: .leading, spacing: 6) { + Text(item.korName) + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.black) + .lineLimit(2) + + Text("\(item.trim) / \(item.model)") + .font(.system(size: 13)) + .foregroundColor(.gray) + + Text("\(item.price)원") + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(.black) + } + + Spacer() + + // 🪄 수량에 따른 3단계 분기 + if quantity == 0 { + Button(action: onAddToCart) { + Image(systemName: "cart.badge.plus") + .font(.system(size: 18)) + .foregroundColor(.Primary) + .padding(10) + .background(Color.Primary.opacity(0.1)) + .clipShape(Circle()) + } + + } else if quantity == 1 { + HStack(spacing: 10) { + Button(action: onRemoveFromCart) { + Image(systemName: "trash") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.red) + } + + Text("1") + .font(.system(size: 15, weight: .semibold)) + .frame(width: 24) + + Button(action: onIncrease) { + Image(systemName: "plus") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.Primary) + } + } + .padding(.vertical, 6) + .padding(.horizontal, 10) + .background(Color.white) + .cornerRadius(10) + .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) + + } else { + HStack(spacing: 10) { + Button(action: onDecrease) { + Image(systemName: "minus") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.gray) + } + + Text("\(quantity)") + .font(.system(size: 15, weight: .semibold)) + .frame(width: 24) + + Button(action: onIncrease) { + Image(systemName: "plus") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.Primary) + } + } + .padding(.vertical, 6) + .padding(.horizontal, 10) + .background(Color.white) + .cornerRadius(10) + .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) + } + } + } + .padding() + .background(Color.white) + .cornerRadius(14) + .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) + } +} diff --git a/StockMate/StockMate/app/core/components/RoundedCorner.swift b/StockMate/StockMate/app/core/components/RoundedCorner.swift new file mode 100644 index 0000000..b93b448 --- /dev/null +++ b/StockMate/StockMate/app/core/components/RoundedCorner.swift @@ -0,0 +1,22 @@ +// +// RoundedCorner.swift +// StockMate +// +// Created by Admin on 10/26/25. +// + +import SwiftUI + +struct RoundedCorner: Shape { + var radius: CGFloat = .infinity + var corners: UIRectCorner = .allCorners + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath( + roundedRect: rect, + byRoundingCorners: corners, + cornerRadii: CGSize(width: radius, height: radius) + ) + return Path(path.cgPath) + } +} diff --git a/StockMate/StockMate/app/feature/auth/ui/HomeView.swift b/StockMate/StockMate/app/feature/auth/ui/HomeView.swift index 8a8d395..1094e52 100644 --- a/StockMate/StockMate/app/feature/auth/ui/HomeView.swift +++ b/StockMate/StockMate/app/feature/auth/ui/HomeView.swift @@ -11,16 +11,17 @@ import SwiftUI struct HomeView: View { @EnvironmentObject var authViewModel: AuthViewModel @StateObject private var userViewModel = UserViewModel() + @StateObject private var inventoryViewModel = InventoryViewModel() var body: some View { ScrollView { - VStack(alignment: .leading, spacing: 24) { + VStack(alignment: .leading, spacing: 15) { // 상단 프로필 HStack(spacing: 16) { ProfileCircleView(name: userViewModel.userInfo?.owner ?? "사용자", size: 50) - VStack(alignment: .leading, spacing: 4) { - HStack { + VStack(alignment: .leading, spacing: 1) { + HStack(spacing: 2) { Image("location") .foregroundColor(.gray) Text(userViewModel.userInfo?.storeName ?? "가게명 없음") @@ -37,55 +38,43 @@ struct HomeView: View { Image("notification") .font(.system(size: 20)) .foregroundColor(.gray) + .padding(.trailing, 5) } .padding(.horizontal) - - // 검색창 - HStack { - Image(systemName: "magnifyingglass") - .foregroundColor(Color(hex: "#4B5565")) - Text("Search for Accessories") - .foregroundColor(.gray) - Spacer() + // 🔍 검색창 + NavigationLink(destination: InventorySearchView()) { + HStack { + Image(systemName: "magnifyingglass") + .foregroundColor(.gray) + + Text("부품을 검색하세요.") + .foregroundColor(.gray) + Spacer() + } + .padding() + .background(Color(.white)) + .cornerRadius(9999) + .overlay( + RoundedRectangle(cornerRadius: 9999) + .stroke(Color.gray.opacity(0.4), lineWidth: 1) + ) + .padding(.horizontal) } - .padding() - .background( - RoundedRectangle(cornerRadius: 120) - .fill(Color(hex: "#EEF2F6")) - ) - .overlay( - RoundedRectangle(cornerRadius: 120) - .stroke(Color(hex: "#9AA4B2"), lineWidth: 1) - ) - .padding(.horizontal) - - - - // 상태 요약 카드 - HStack(spacing: 13) { - StatusItem(title: "입고", count: 77, color: .IncomingBg, icon: "incoming") - StatusItem(title: "부족", count: 33, color: .DangerBg, icon: "lack") - StatusItem(title: "승인대기", count: 27, color: .WarningBg, icon: "wait") - StatusItem(title: "반품/불량", count: 4, color: .DefectBg, icon: "defect") - StatusItem(title: "이동요청", count: 33, color: .TransferBg, icon: "transfer") - } - .padding() - .background(Color.white) - .cornerRadius(16) - .padding(.horizontal) + .buttonStyle(.plain) + lackStockSection // 도넛 차트 섹션 VStack(alignment: .leading, spacing: 18) { - Text("제목") + Text("지난달 카테고리 별 지출") .font(.headline) - .padding() + .padding(4) HStack{ DonutChartView() - .frame(height: 170) + .frame(height: 130) .padding() .background(Color.white) .cornerRadius(16) @@ -103,13 +92,12 @@ struct HomeView: View { // 막대그래프 섹션 VStack(alignment: .leading, spacing: 8) { - Text("제목") + Text("지출 현황") .font(.headline) - .padding(.top) - .padding(.leading) + .padding(4) BarChartView() - .frame(height: 200) + .frame(height: 150) // .padding() .background(Color.white) .shadow(color: .gray.opacity(0.1), radius: 4) @@ -122,11 +110,15 @@ struct HomeView: View { .padding(.vertical) } .background(Color.Light) + .task { + // 카테고리 데이터 로드 + await inventoryViewModel.loadLackCountByCategory() + } .onAppear { Task { await userViewModel.loadUserInfo() } } // 화면 디자인 시 잠시 주석처리 -// // ✅ 세션 만료 시 자동으로 로그인 뷰로 이동 + // ✅ 세션 만료 시 자동으로 로그인 뷰로 이동 // .onChange(of: userViewModel.shouldGoToLogin) { shouldGo in // if shouldGo { // print("세션 만료됨 → 로그인 화면으로 이동") @@ -134,11 +126,61 @@ struct HomeView: View { // } // } } + + private var lackStockSection: some View { + VStack(alignment: .leading, spacing: 8) { + Text("재고 부족 조회") + .font(.headline) + HStack(spacing: 13) { + ForEach(inventoryViewModel.lackCounts, id: \.id) { item in + NavigationLink { + LackListView(selectedCategory: item.categoryName) + } label: { + StatusItem( + title: item.categoryName, + count: item.count, + color: colorForCategory(item.categoryName), + icon: iconForCategory(item.categoryName) + ) + } + } + } + .padding(.vertical, 4) + } + .frame(maxWidth: .infinity, minHeight: 70) + .padding() + .background(Color.white) + .cornerRadius(16) + .padding(.horizontal) + } + + } +private func colorForCategory(_ category: String) -> Color { + switch category { + case "전기/램프": return .Hstatus1Bg + case "엔진/미션": return .Hstatus2Bg + case "하체/바디": return .Hstatus3Bg + case "내장/외장": return .Hstatus4Bg + case "기타소모품": return .Hstatus5Bg + default: return .gray.opacity(0.3) + } +} +private func iconForCategory(_ name: String) -> String { + switch name { + case "전기/램프": return "lightbulb" + case "엔진/미션": return "cog" + case "하체/바디": return "spanner" + case "내장/외장": return "chair" + case "기타소모품": return "package" + default: return "questionmark" + } +} + // MARK: - 상태 아이템 컴포넌트 struct StatusItem: View { let title: String @@ -152,13 +194,13 @@ struct StatusItem: View { .resizable() .scaledToFit() .frame(width: 25, height: 25) - .foregroundColor(.white) .padding(12) .background(color) - .clipShape(RoundedRectangle(cornerRadius: 10)) + .clipShape(RoundedRectangle(cornerRadius: 100)) Text(title) - .font(.system(size: 15, weight: .semibold)) + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(.black) .padding(.top, 5) .lineLimit(1) @@ -196,8 +238,8 @@ struct DonutChartView: View { struct BarChartView: View { let values: [CGFloat] = [0.89, 0.5, 0.9, 0.3, 0.7] let colors: [Color] = [ - Color(hex: "6BE6D3"), .black, Color(hex: "7DBBFF"), Color(hex: "B899EB"), Color(hex: "71DD8C")] - // 6BE6D3 + .LightBlue04, .Primary, .LightBlue04, .LightBlue04, .LightBlue04 + ] var body: some View { HStack(alignment: .bottom, spacing: 33) { ForEach(0.. optional +struct CartData: Decodable { + let cartId: Int + let memberId: Int + let items: [CartItem] + let totalPrice: Int? +} + +// put,post는 cartItemId,partId,amount만 필요 -> optional +struct CartItem: Decodable, Identifiable { + let cartItemId: Int + let partId: Int + var amount: Int + let partName: String? + let categoryName: String? + let brand: String? + let model: String? + let trim: String? + let price: Int? + let stock: Int? + let image: String? + +// var id: Int { cartItemId } + var id: Int { partId } + +} + + +// === Request === put,post만 요청값이 있음 +struct CartUpdateRequest: Encodable { + let items: [CartUpdateItem] +} + +struct CartUpdateItem: Encodable { + let partId: Int + let amount: Int +} + +enum CartApi { + // GET - 장바구니 조회 + static func fetchCart() -> DataRequest { + let url = ApiClient.baseURL + "api/v1/order/cart" + return ApiClient.shared.request(url, method: .get) + } + + // POST - 장바구니 등록 (추가) + static func addToCart(_ body: CartUpdateRequest) -> DataRequest { + let url = ApiClient.baseURL + "api/v1/order/cart" + return ApiClient.shared.request(url, + method: .post, + parameters: body, + encoder: JSONParameterEncoder.default) + } + + // PUT - 장바구니 수정 (전체 덮어쓰기 형태) + static func updateCart(_ body: CartUpdateRequest) -> DataRequest { + let url = ApiClient.baseURL + "api/v1/order/cart" + return ApiClient.shared.request(url, + method: .put, + parameters: body, + encoder: JSONParameterEncoder.default) + } + + // DELETE - 장바구니 전체 비우기 (파라미터 없음) + static func clearCart() -> DataRequest { + let url = ApiClient.baseURL + "api/v1/order/cart" + return ApiClient.shared.request(url, method: .delete) + } +} diff --git a/StockMate/StockMate/app/feature/cart/data/CartRepositoryImpl.swift b/StockMate/StockMate/app/feature/cart/data/CartRepositoryImpl.swift new file mode 100644 index 0000000..479f1d2 --- /dev/null +++ b/StockMate/StockMate/app/feature/cart/data/CartRepositoryImpl.swift @@ -0,0 +1,38 @@ +// +// CartRepositoryImpl.swift +// StockMate +// +// Created by Admin on 10/25/25. +// + + +import Foundation +import Alamofire + +final class CartRepositoryImpl: CartRepositoryProtocol { + + func fetchCart() async -> AppResult> { + let req = CartApi.fetchCart() + return await safeApi(req, decodeTo: ApiResponse.self) + } + + func addToCart( + request: CartUpdateRequest + ) async -> AppResult> { + let req = CartApi.addToCart(request) + return await safeApi(req, decodeTo: ApiResponse.self) + } + + func updateCart( + request: CartUpdateRequest + ) async -> AppResult> { + let req = CartApi.updateCart(request) + return await safeApi(req, decodeTo: ApiResponse.self) + } + + func clearCart() async -> AppResult> { + let req = CartApi.clearCart() + return await safeApi(req, decodeTo: ApiResponse.self) + } + +} diff --git a/StockMate/StockMate/app/feature/cart/domain/CartRepositoryProtocol.swift b/StockMate/StockMate/app/feature/cart/domain/CartRepositoryProtocol.swift new file mode 100644 index 0000000..82b44a0 --- /dev/null +++ b/StockMate/StockMate/app/feature/cart/domain/CartRepositoryProtocol.swift @@ -0,0 +1,24 @@ +// +// CartRepositoryProtocol.swift +// StockMate +// +// Created by Admin on 10/25/25. +// + +import Foundation +import Alamofire + +protocol CartRepositoryProtocol { + func fetchCart() async -> AppResult> + + func addToCart( + request: CartUpdateRequest + ) async -> AppResult> + + func updateCart( + request: CartUpdateRequest + ) async -> AppResult> + + func clearCart() async -> AppResult> +} + diff --git a/StockMate/StockMate/app/feature/cart/viewmodel/CartViewModel.swift b/StockMate/StockMate/app/feature/cart/viewmodel/CartViewModel.swift new file mode 100644 index 0000000..28bf1fd --- /dev/null +++ b/StockMate/StockMate/app/feature/cart/viewmodel/CartViewModel.swift @@ -0,0 +1,178 @@ +// +// CartViewModel.swift +// StockMate +// +// Created by Admin on 10/25/25. +// + +import Foundation + +@MainActor +final class CartViewModel: ObservableObject { + + @Published var cart: CartData? + @Published var items: [CartItem] = [] + @Published var message: String = "" + @Published var isLoading: Bool = false + @Published var shouldGoToLogin: Bool = false // 401 대응 + + private let repository: CartRepositoryProtocol + + init(repository: CartRepositoryProtocol = CartRepositoryImpl()) { + self.repository = repository + } + + // MARK: - Fetch + func fetchCart() async { + isLoading = true + defer { isLoading = false } + + switch await repository.fetchCart() { + case .success(let response): + self.cart = response.data + self.items = response.data?.items ?? [] + case .failure(let error): + handleError(error) + } + } + + // MARK: - Add + func addToCart(partId: Int, amount: Int) async { + let req = CartUpdateRequest(items: [CartUpdateItem(partId: partId, amount: amount)]) + switch await repository.addToCart(request: req) { + case .success: + await fetchCart() + updateLocalCartState() + case .failure(let error): + handleError(error) + } + } + + // MARK: - Update Quantity (전체 덮어쓰기) + func updateCart() async { + // ✅ items가 비면 clearCart 호출하고 return + if items.isEmpty { + await clearCart() + return + } + + let requestItems = items.map { CartUpdateItem(partId: $0.partId, amount: $0.amount) } + let req = CartUpdateRequest(items: requestItems) + + switch await repository.updateCart(request: req) { + case .success(let response): + self.cart = response.data + case .failure(let error): + handleError(error) + } + } + + // MARK: - Clear Cart + func clearCart() async { + switch await repository.clearCart() { + case .success: + self.cart = nil + self.items = [] + case .failure(let error): + handleError(error) + } + } + + // MARK: - Private + private func handleError(_ error: AppError) { + message = error.message + print("🚨 Error (\(error.code)): \(error.message)") + + if error.code == 401 { + shouldGoToLogin = true + } + } + + func quantity(for partId: Int) -> Int { + return items.first(where: { $0.partId == partId })?.amount ?? 0 + } + + private func updateLocalCartState() { + let total = items.reduce(0) { result, item in + result + (item.price ?? 0) * item.amount + } + + if let cart = cart { + self.cart = CartData( + cartId: cart.cartId, + memberId: cart.memberId, + items: self.items, + totalPrice: total + ) + } else { + // fallback: cart가 nil일 수 있는 초기 로드 상황 대비 + self.cart = CartData( + cartId: -1, + memberId: -1, + items: self.items, + totalPrice: total + ) + } + + objectWillChange.send() // 중요! SwiftUI에게 “바뀌었어!” 알림 + } + + + func increaseQuantity(for partId: Int) async { + if let index = items.firstIndex(where: { $0.partId == partId }) { + items[index].amount += 1 + } else { + items.append(CartItem( + cartItemId: 0, + partId: partId, + amount: 1, + partName: nil, + categoryName: nil, + brand: nil, + model: nil, + trim: nil, + price: nil, + stock: nil, + image: nil + )) + } + + await syncCart() + updateLocalCartState() + } + + func decreaseQuantity(for partId: Int) async { + guard let index = items.firstIndex(where: { $0.partId == partId }) else { + return + } + + if items[index].amount > 1 { + items[index].amount -= 1 + } else { + items.remove(at: index) + } + + await syncCart() + updateLocalCartState() + } + + private func syncCart() async { + if items.isEmpty { + // ✅ 장바구니가 빈 경우는 clearCart 호출 + await clearCart() + return + } + let updates = items.map { + CartUpdateItem(partId: $0.partId, amount: $0.amount) + } + let request = CartUpdateRequest(items: updates) + + switch await repository.updateCart(request: request) { + case .success: + await fetchCart() + case .failure(let error): + handleError(error) + } + } + +} diff --git a/StockMate/StockMate/app/feature/inventory/data/InventoryApi.swift b/StockMate/StockMate/app/feature/inventory/data/InventoryApi.swift index ed33182..a4ef6e9 100644 --- a/StockMate/StockMate/app/feature/inventory/data/InventoryApi.swift +++ b/StockMate/StockMate/app/feature/inventory/data/InventoryApi.swift @@ -41,6 +41,13 @@ struct InventoryItem: Decodable, Identifiable { let isLack: Bool } +struct LackCountItem: Decodable, Identifiable { + var id: String { categoryName } // SwiftUI ForEach에서 식별자 사용 + let categoryName: String + let count: Int +} + + enum InventoryApi { static func getInventoryList( page: Int, @@ -80,4 +87,9 @@ enum InventoryApi { return ApiClient.shared.request(url, method: .get) } + // ✅ 카테고리별 부족 재고 개수 조회 + static func getLackCountByCategory() -> DataRequest { + let url = ApiClient.baseURL + "api/v1/store/lack-count" + return ApiClient.shared.request(url, method: .get) + } } diff --git a/StockMate/StockMate/app/feature/inventory/data/InventoryRepositoryImpl.swift b/StockMate/StockMate/app/feature/inventory/data/InventoryRepositoryImpl.swift index 914e732..93a8885 100644 --- a/StockMate/StockMate/app/feature/inventory/data/InventoryRepositoryImpl.swift +++ b/StockMate/StockMate/app/feature/inventory/data/InventoryRepositoryImpl.swift @@ -44,6 +44,11 @@ final class InventoryRepositoryImpl: InventoryRepositoryProtocol { let dataReq = InventoryApi.findByName(name: name, page: page, size: size) return await safeApi(dataReq, decodeTo: ApiResponse.self) } - + // 카테고리별 부족 재고 개수 조회 + func getLackCountByCategory() async -> AppResult> { + let dataReq = InventoryApi.getLackCountByCategory() + return await safeApi(dataReq, decodeTo: ApiResponse<[LackCountItem]>.self) + } + } diff --git a/StockMate/StockMate/app/feature/inventory/domain/InventoryRepositoryProtocol.swift b/StockMate/StockMate/app/feature/inventory/domain/InventoryRepositoryProtocol.swift index 3d45edd..fe2a090 100644 --- a/StockMate/StockMate/app/feature/inventory/domain/InventoryRepositoryProtocol.swift +++ b/StockMate/StockMate/app/feature/inventory/domain/InventoryRepositoryProtocol.swift @@ -6,6 +6,7 @@ // import Foundation +import Alamofire protocol InventoryRepositoryProtocol { func getInventoryList( @@ -29,4 +30,6 @@ protocol InventoryRepositoryProtocol { size: Int ) async -> AppResult> + func getLackCountByCategory() async -> AppResult> + } diff --git a/StockMate/StockMate/app/feature/inventory/ui/IncomingScanView.swift b/StockMate/StockMate/app/feature/inventory/ui/IncomingScanView.swift index 706a507..83d0415 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/IncomingScanView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/IncomingScanView.swift @@ -24,17 +24,6 @@ struct IncomingScanView: View { RoundedRectangle(cornerRadius: 8) .stroke(Color.blue, lineWidth: 3) .frame(width: 220, height: 220) - .overlay( - // 모서리 강조 - ZStack { - Color.clear - .overlay( - Rectangle() - .trim(from: 0, to: 0.25) - .stroke(Color.blue, style: StrokeStyle(lineWidth: 5, lineCap: .round)) - ) - } - ) } .padding(.top, 20) diff --git a/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift b/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift index b0d3132..35545c0 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift @@ -89,7 +89,7 @@ struct InventoryView: View { } label: { ZStack { Circle() - .fill(Color.Secondary) // 배경색 + .fill(Color.Primary) // 배경색 .frame(width: 50, height: 50) Image(systemName: "arrow.up") .font( @@ -146,7 +146,7 @@ struct GridMenuView: View { ZStack { // 타원 배경 Rectangle() - .fill(item.1 ? Color.white.opacity(0.2) : Color.Secondary) + .fill(item.1 ? Color.white.opacity(0.2) : Color.Primary) .frame(width: 35, height: 26) .cornerRadius(80) @@ -162,7 +162,7 @@ struct GridMenuView: View { Text(item.0) .font(.system(size: 15, weight: .semibold)) - .foregroundColor(item.1 ? .white : Color.Secondary) + .foregroundColor(item.1 ? .white : Color.Primary) } .padding(.horizontal, 20) .padding(.vertical, 20) @@ -170,7 +170,7 @@ struct GridMenuView: View { .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: 24) - .fill(item.1 ? Color.Secondary : Color.white) + .fill(item.1 ? Color.Primary : Color.white) // 카드 그림자 (Figma 스펙: y=4, blur=4, opacity=25%, black) .shadow(color: .black.opacity(0.35), radius: 2, x: 0, y: 4) ) diff --git a/StockMate/StockMate/app/feature/inventory/ui/LackListView.swift b/StockMate/StockMate/app/feature/inventory/ui/LackListView.swift new file mode 100644 index 0000000..b9ca69a --- /dev/null +++ b/StockMate/StockMate/app/feature/inventory/ui/LackListView.swift @@ -0,0 +1,98 @@ +// +// LackListView.swift +// StockMate +// +// Created by Admin on 10/23/25. +// + +import SwiftUI + +struct LackListView: View { + @StateObject private var inventoryViewModel = InventoryViewModel() + @State private var isFirstAppear = true + + // 전달받는 초기 카테고리 + @State var selectedCategory: String + private let categories = ["전기/램프", "엔진/미션", "하체/바디", "내장/외장", "기타소모품"] + + var body: some View { + VStack(spacing: 0) { + // 상단 카테고리 탭 + HStack(spacing: 8) { + ForEach(categories, id: \.self) { category in + CategoryButton( + title: category, + isSelected: selectedCategory == category + ) { + Task { + selectedCategory = category + inventoryViewModel.selectedCategories = [category] + await inventoryViewModel.loadUnderLimitList(reset: true) + } + } + } + } + .padding(.horizontal) + .padding(.vertical, 10) + + // 리스트 + ScrollView { + LazyVStack(spacing: 12) { + ForEach(inventoryViewModel.underLimitItems) { item in + InventoryCardView(item: item) + .padding(.horizontal) + .onAppear { + // 무한 스크롤 트리거 + if item.id == inventoryViewModel.underLimitItems.last?.id { + Task { + await inventoryViewModel.loadUnderLimitList() + } + } + } + } + + if inventoryViewModel.isLoading { + ProgressView() + .padding(.vertical, 20) + } + } + .padding(.top, 8) + .padding(.bottom, 80) + } + } + .background(Color.Light) + .navigationTitle("부족 재고") + .navigationBarTitleDisplayMode(.inline) + .task { + if isFirstAppear { + isFirstAppear = false + inventoryViewModel.selectedCategories = [selectedCategory] + await inventoryViewModel.loadUnderLimitList(reset: true) + } + } + } +} + +struct CategoryButton: View { + let title: String + let isSelected: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + Text(title) + .font(.system(size: 11, weight: .regular)) + .foregroundColor(isSelected ? .Primary : .black) + .padding(.vertical, 8) + .padding(.horizontal, 12) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(Color.Light) + ) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(isSelected ? Color.Primary : Color.GrayStroke, lineWidth: 1) + ) + } + } +} diff --git a/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift b/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift index 6467147..a9e1329 100644 --- a/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift +++ b/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift @@ -38,6 +38,10 @@ final class InventoryViewModel: ObservableObject { // ====== 공통 상태 ====== @Published var isLoading = false + // ===== 카테고리별 부족 재고 개수 ===== + @Published var lackCounts: [LackCountItem] = [] + + private let repo: InventoryRepositoryProtocol init(repo: InventoryRepositoryProtocol = InventoryRepositoryImpl()) { @@ -270,5 +274,30 @@ final class InventoryViewModel: ObservableObject { await loadInventoryList() } } + + + // MARK: - 카테고리별 부족 재고 개수 로드 + func loadLackCountByCategory() async { + guard !isLoading else { return } + isLoading = true + + let result = await repo.getLackCountByCategory() + + switch result { + case .success(let apiResp): + if let data = apiResp.data { + lackCounts = data + } else { + message = apiResp.message + } + case .failure(let err): + message = err.message + if err.code == 401 || err.code == 403 { + shouldGoToLogin = true + } + } + + isLoading = false + } } diff --git a/StockMate/StockMate/app/feature/orders/data/OrderApi.swift b/StockMate/StockMate/app/feature/orders/data/OrderApi.swift new file mode 100644 index 0000000..7032db0 --- /dev/null +++ b/StockMate/StockMate/app/feature/orders/data/OrderApi.swift @@ -0,0 +1,168 @@ +// +// OrderApi.swift +// StockMate +// +// Created by Admin on 10/21/25. +// + +import Foundation +import Alamofire + +// MARK: - Response Models + +struct OrderListResponse: Decodable { + let status: Int + let success: Bool + let message: String + let data: OrderPageData? +} + +struct OrderDetailResponse: Decodable { + let status: Int + let success: Bool + let message: String + let data: OrderResponseItem? +} + + +struct OrderPageData: Decodable { + let totalElements: Int + let totalPages: Int + let page: Int + let size: Int + let content: [OrderResponseItem] + let last: Bool +} + +struct OrderResponseItem: Decodable, Identifiable { + var id: Int { orderId } + + let orderId: Int + let orderNumber: String + let memberId: Int + let userInfo: OrderUserInfo? + let orderItems: [OrderItem] + let paymentType: String? + let etc: String? + let rejectedMessage: String? + let carrier: String? + let trackingNumber: String? + let requestedShippingDate: String? + let shippingDate: String? + let totalPrice: Int + let orderStatus: String + let createdAt: String + let updatedAt: String +} + +struct OrderUserInfo: Decodable { + let id: Int + let memberId: Int + let email: String + let owner: String + let address: String + let storeName: String + let businessNumber: String + let role: String + let verified: String + let latitude: Double + let longitude: Double +} + +struct OrderItem: Decodable { + let partId: Int + let amount: Int + let partDetail: OrderPartDetail +} + +struct OrderPartDetail: Decodable { + let id: Int + let name: String + let price: Int + let image: String + let trim: String + let model: String + let category: Int + let korName: String + let engName: String + let categoryName: String + let amount: Int +} + + +// MARK: - 주문 생성 Request +struct OrderRequest: Encodable { + let orderItems: [OrderItems] + let requestedShippingDate: String + let paymentType: String + let etc: String +} + +struct OrderItems: Encodable { + let partId: Int + let amount: Int +} + +// Response +struct OrderCreateResponseData: Decodable { + let orderId: Int + let orderNumber: String + let totalPrice: Int + let orderStatus: String +} + + +// MARK: - API Call + +enum OrderApi { + // ✅ 내 주문 리스트 조회 API + static func getMyOrderList( + status: String? = nil, + startDate: String? = nil, + endDate: String? = nil, + page: Int = 0, + size: Int = 20 + ) -> DataRequest { + var url = ApiClient.baseURL + "api/v1/order/list/my?page=\(page)&size=\(size)" + + if let status = status, !status.isEmpty { + url += "&status=\(status.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")" + } + if let startDate = startDate, !startDate.isEmpty { + url += "&startDate=\(startDate.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")" + } + if let endDate = endDate, !endDate.isEmpty { + url += "&endDate=\(endDate.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")" + } + + return ApiClient.shared.request(url, method: .get) + } + + + // ✅ 주문 상세 조회 API + static func getOrderDetail(orderId: Int) -> DataRequest { + let url = ApiClient.baseURL + "api/v1/order/detail?orderId=\(orderId)" + return ApiClient.shared.request(url, method: .get) + } + + + // ✅ 주문 생성 API + static func createOrder(_ requestBody: OrderRequest) -> DataRequest { + let url = ApiClient.baseURL + "api/v1/order" + return ApiClient.shared.request( + url, + method: .post, + parameters: requestBody, + encoder: JSONParameterEncoder.default + ) + } + + // ✅ 주문 취소 API + static func cancelOrder(orderId: Int) -> DataRequest { + let url = ApiClient.baseURL + "api/v1/order/\(orderId)/cancel" + print("🚀 CancelOrder URL:", url) + return ApiClient.shared.request(url, method: .put) + .validate() // ✅ 서버 상태코드 확인 + } + +} diff --git a/StockMate/StockMate/app/feature/orders/data/OrderRepositoryImpl.swift b/StockMate/StockMate/app/feature/orders/data/OrderRepositoryImpl.swift new file mode 100644 index 0000000..0f4d42f --- /dev/null +++ b/StockMate/StockMate/app/feature/orders/data/OrderRepositoryImpl.swift @@ -0,0 +1,93 @@ +// +// OrderRepositoryImpl.swift +// StockMate +// +// Created by Admin on 10/21/25. +// + +import Foundation +import Alamofire + +final class OrderRepositoryImpl: OrderRepositoryProtocol { + func fetchMyOrders( + status: String?, + startDate: String?, + endDate: String?, + page: Int, + size: Int + ) async -> AppResult { + let request = OrderApi.getMyOrderList( + status: status, + startDate: startDate, + endDate: endDate, + page: page, + size: size + ) + + // safeApi 으로 전체 ApiResponse 를 디코딩 + let result = await safeApi(request, decodeTo: OrderListResponse.self) + + switch result { + case .success(let response): + // response.data 는 OrderPageData? 이므로 안전하게 꺼내서 반환 + if let pageData = response.data { + return .success(pageData) + } else { + // 서버가 data를 비워서 보냈다면 메시지로 실패 처리 + return .failure(AppError(code: response.status, message: response.message, underlying: nil)) + } + + case .failure(let error): + return .failure(error) + } + } + + + func fetchOrderDetail(orderId: Int) async -> AppResult { + let request = OrderApi.getOrderDetail(orderId: orderId) + let result = await safeApi(request, decodeTo: OrderDetailResponse.self) // ✅ 올바른 타입으로 변경 + + switch result { + case .success(let response): + if let data = response.data { + return .success(data) + } else { + return .failure(.init(code: -1, message: "주문 상세 데이터를 불러오지 못했습니다.", underlying: nil)) // ✅ 누락된 인자 채움 + } + case .failure(let error): + return .failure(error) + } + } + + func createOrder(request: OrderRequest) async -> AppResult { + let request = OrderApi.createOrder(request) + + let result = await safeApi(request, decodeTo: ApiResponse.self) + + switch result { + case .success(let response): + if let data = response.data { + return .success(data) + } else { + return .failure(.init(code: response.status, message: response.message, underlying: nil)) + } + case .failure(let error): + return .failure(error) + } + } + + + func cancelOrder(orderId: Int) async -> AppResult { + let request = OrderApi.cancelOrder(orderId: orderId) + let result = await safeApi(request, decodeTo: ApiResponse.self) + + switch result { + case .success(let response): + print("✅ 취소 성공:", response) + return .success(response.data ?? "success") + case .failure(let error): + return .failure(error) + } + } + +} diff --git a/StockMate/StockMate/app/feature/orders/domain/OrderRepositoryProtocol.swift b/StockMate/StockMate/app/feature/orders/domain/OrderRepositoryProtocol.swift new file mode 100644 index 0000000..9b98ce6 --- /dev/null +++ b/StockMate/StockMate/app/feature/orders/domain/OrderRepositoryProtocol.swift @@ -0,0 +1,29 @@ +// +// OrderRepositoryProtocol.swift +// StockMate +// +// Created by Admin on 10/21/25. +// + +import Foundation + +protocol OrderRepositoryProtocol { + func fetchMyOrders( + status: String?, + startDate: String?, + endDate: String?, + page: Int, + size: Int + ) async -> AppResult + + /// 주문 상세 조회 + func fetchOrderDetail( + orderId: Int + ) async -> AppResult + + // 주문 생성 + func createOrder(request: OrderRequest) async -> AppResult + + + func cancelOrder(orderId: Int) async -> AppResult +} diff --git a/StockMate/StockMate/app/feature/orders/ui/OrderCartView.swift b/StockMate/StockMate/app/feature/orders/ui/OrderCartView.swift new file mode 100644 index 0000000..b39a506 --- /dev/null +++ b/StockMate/StockMate/app/feature/orders/ui/OrderCartView.swift @@ -0,0 +1,63 @@ +// +// OrderCartView.swift +// StockMate +// +// Created by Admin on 10/20/25. +// + +import SwiftUI + +struct OrderCartView: View { + @ObservedObject var cartViewModel: CartViewModel + + var body: some View { + VStack(spacing: 0) { + + ScrollView { + LazyVStack(spacing: 16) { + ForEach(cartViewModel.items) { cartItem in + + CartCard( + item: cartItem, + quantity: cartItem.amount, + onIncrease: { + Task { await cartViewModel.increaseQuantity(for: cartItem.partId) } + }, + onDecrease: { + Task { await cartViewModel.decreaseQuantity(for: cartItem.partId) } + }, + onAddToCart: nil, + onRemoveFromCart: { + Task { + await cartViewModel.decreaseQuantity(for: cartItem.partId) + } + } + ) + .padding(.horizontal) + } + } + .padding(.vertical) + } + .background(Color.Light) + + + NavigationLink(destination: OrderInfoView(cartViewModel: cartViewModel)) { + Text("\(cartViewModel.cart?.totalPrice ?? 0)원 결제하기") + .font(.system(size: 16, weight: .bold)) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 60) + .background(Color.Primary) + } + + } + .background(Color.Light) + .navigationTitle("장바구니 확인") + .navigationBarTitleDisplayMode(.inline) + .task { + await cartViewModel.fetchCart() + } + .edgesIgnoringSafeArea(.bottom) + } +} + diff --git a/StockMate/StockMate/app/feature/orders/ui/OrderDetailView.swift b/StockMate/StockMate/app/feature/orders/ui/OrderDetailView.swift new file mode 100644 index 0000000..1d8054e --- /dev/null +++ b/StockMate/StockMate/app/feature/orders/ui/OrderDetailView.swift @@ -0,0 +1,280 @@ +// +// OrderDetailView.swift +// StockMate +// +// Created by Admin on 10/22/25. +// + +import SwiftUI + +struct OrderDetailView: View { + let orderId: Int + @ObservedObject var orderViewModel: OrderViewModel + @StateObject private var viewModel = OrderDetailViewModel() + + var body: some View { + ScrollView { + if viewModel.isLoading { + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if let order = viewModel.order { + VStack(spacing: 16) { + + // ✅ 주문 정보 + VStack(alignment: .leading, spacing: 8) { + Text(formatDate(order.createdAt)) + .font(.system(size: 15, weight: .semibold)) + Text("주문번호: \(order.orderNumber)") + .font(.system(size: 14)) + .foregroundColor(.gray) + HStack { + Text("상태:") + .font(.system(size: 14, weight: .semibold)) + Text(statusText(order.orderStatus)) + .font(.system(size: 13, weight: .semibold)) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(statusBdColor(order.orderStatus)) + .foregroundColor(statusColor(order.orderStatus)) + .cornerRadius(12) + } + } + .frame(maxWidth: .infinity, alignment: .leading) // ✅ 이거 추가 + .padding(.all, 20) + .background(Color.white) + .cornerRadius(16) + .shadow(color: .black.opacity(0.05), radius: 3, y: 2) +// .padding(.horizontal, 20) + + + // ✅ 배송 정보 + VStack(alignment: .leading, spacing: 6) { + Text("배송정보") + .font(.system(size: 15, weight: .semibold)) + .padding(.bottom, 4) + Text(order.userInfo?.owner ?? "-") + Text(order.userInfo?.address ?? "-") + if !(order.etc ?? "").isEmpty { + Text(order.etc ?? "") + } + if let email = order.userInfo?.email { + Text(email) + .foregroundColor(.gray) + } + } + .frame(maxWidth: .infinity, alignment: .leading) // ✅ 여기도 추가 + .padding(.all, 20) + .background(Color.white) + .cornerRadius(16) + .shadow(color: .black.opacity(0.05), radius: 3, y: 2) +// .padding(.horizontal, 20) + + // ✅ 주문 상품 + OrderSectionCard { + VStack(alignment: .leading, spacing: 6) { + Text("주문 상품 \(order.orderItems.count)개") + .font(.system(size: 15, weight: .semibold)) + + VStack(alignment: .leading, spacing: 6) { + ForEach(order.orderItems, id: \.partId) { item in + Text(item.partDetail.categoryName) + .font(.system(size: 12, weight: .semibold)) + + HStack(alignment: .top, spacing: 12) { + AsyncImage(url: URL(string: item.partDetail.image)) { img in + img.resizable().scaledToFill() + } placeholder: { + Color.gray.opacity(0.1) + } + .frame(width: 60, height: 60) + .cornerRadius(4) + .clipped() + + VStack(alignment: .leading, spacing: 4) { + Text(item.partDetail.korName) + .font(.system(size: 14, weight: .semibold)) + Text("\(item.partDetail.model) / \(item.partDetail.trim) / \(formatPrice(item.partDetail.price))원 / \(item.amount)개") + .font(.caption) + .foregroundColor(.gray) + Text("\(formatPrice(item.partDetail.price * item.amount))원") + .font(.system(size: 14, weight: .bold)) + } + } + .frame(maxWidth: .infinity, alignment: .leading) // ✅ 왼쪽 정렬 강제 + } + } + .padding(.top, 8) // 위 여백만 살짝 + } + .frame(maxWidth: .infinity, alignment: .leading) // ✅ 섹션 전체도 왼쪽으로 정렬 + } + + + // ✅ 결제 정보 + OrderSectionCard { + VStack(alignment: .leading, spacing: 10) { + Text("결제 정보") + .font(.system(size: 15, weight: .semibold)) + + if order.paymentType == "DEPOSIT" { + infoRow("결제 수단", "예치금") + } else { + infoRow("결제 수단", "카드 결제") + } + + infoRow("상품금액", "\(formatPrice(order.totalPrice))원") + infoRow("배송희망일", formatDateOrDash(order.requestedShippingDate)) + Divider().padding(.vertical, 4) + HStack { + Text("총 결제 금액") + .font(.headline) + Spacer() + Text("\(formatPrice(order.totalPrice))원") + .font(.headline.bold()) + .foregroundColor(.Primary) + } + } + } + + // ✅ 하단 버튼 + HStack(spacing: 12) { + // 왼쪽: 영수증 확인 + Button(action: {}) { + Text("영수증 확인") + .font(.system(size: 15, weight: .semibold)) + .foregroundColor(Color.Primary) + .frame(maxWidth: .infinity) + .frame(height: 48) + .background(Color.white) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.Primary, lineWidth: 1.5) + ) + .cornerRadius(10) + } + + // 오른쪽: 주문 취소 + Button(action: { + // 주문취소 처리 + Task { + await orderViewModel.cancelOrder(orderId: orderId) + } + + }) { + Text("주문 취소") + .font(.system(size: 15, weight: .semibold)) + .frame(maxWidth: .infinity) + .frame(height: 48) + .background(Color(hex: "#1D4ED8")) + .foregroundColor(.white) + .cornerRadius(10) + } + } + .padding(.top, 10) + + } + .padding(.horizontal, 20) // ✅ 전체 섹션 동일 여백 + .padding(.vertical, 16) + + } else if let error = viewModel.errorMessage { + Text(error) + .foregroundColor(.red) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + .background(Color.Light) + .navigationTitle("주문 내역 상세") + .navigationBarTitleDisplayMode(.inline) + .task { + await viewModel.fetchOrderDetail(orderId: orderId) + } + } + + // MARK: - Helper + func infoRow(_ left: String, _ right: String) -> some View { + HStack { + Text(left) + Spacer() + Text(right) + } + .font(.system(size: 14)) + } + + func formatDate(_ isoDate: String) -> String { + let comps = isoDate.split(separator: "T").first?.split(separator: "-") ?? [] + guard comps.count == 3 else { return isoDate } + return "\(comps[0])년 \(comps[1])월 \(comps[2])일" + } + + func statusText(_ status: String) -> String { + switch status { + case "ORDER_COMPLETED": return "주문 완료" + case "PENDING_SHIPPING": return "출고 대기" + case "REJECTED": return "출고 반려" + case "SHIPPING": return "배송 중" + case "DELIVERED": return "배송 완료" + case "RECEIVED": return "입고 완료" + case "CANCELLED": return "주문 취소" + default: return "알 수 없음" + } + } + + func statusColor(_ status: String) -> Color { + switch status { + case "ORDER_COMPLETED": return .StatusGreen + case "PENDING_SHIPPING": return .InvUse + case "REJECTED": return .Danger + case "SHIPPING": return .Transfer + case "DELIVERED": return .Secondary + case "RECEIVED": return .StatusPurple + case "CANCELLED": return .gray + default: return .gray.opacity(0.6) + } + } + + func statusBdColor(_ status: String) -> Color { + switch status { + case "ORDER_COMPLETED": return .StatusGreenBg + case "PENDING_SHIPPING": return .InvUseBg + case "REJECTED": return .DangerBg + case "SHIPPING": return .TransferBg + case "DELIVERED": return .LightBlue04 + case "RECEIVED": return .StatusPurpleBg + case "CANCELLED": return Color(hex: "#EEEEEF") + default: return .gray.opacity(0.6) + } + } +} + +// ✅ 카드 레이아웃 통일용 +struct OrderSectionCard: View { + let content: Content + init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + content + .padding(20) + } + .frame(maxWidth: .infinity) + .background(Color.white) + .cornerRadius(16) + .shadow(color: .black.opacity(0.05), radius: 3, y: 2) + } +} + +func formatPrice(_ value: Int) -> String { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + return formatter.string(from: NSNumber(value: value)) ?? "\(value)" +} + +func formatDateOrDash(_ isoDate: String?) -> String { + guard let isoDate = isoDate, !isoDate.isEmpty else { + return "-" + } + let comps = isoDate.split(separator: "T").first?.split(separator: "-") ?? [] + guard comps.count == 3 else { return "-" } + return "\(comps[0])년 \(comps[1])월 \(comps[2])일" +} diff --git a/StockMate/StockMate/app/feature/orders/ui/OrderInfoView.swift b/StockMate/StockMate/app/feature/orders/ui/OrderInfoView.swift new file mode 100644 index 0000000..b167671 --- /dev/null +++ b/StockMate/StockMate/app/feature/orders/ui/OrderInfoView.swift @@ -0,0 +1,365 @@ +// +// OrderInfoView.swift +// StockMate +// +// Created by Admin on 10/20/25. +// +import SwiftUI + +enum PaymentType: String { + case deposit = "DEPOSIT" + case card = "CARD" +} + +enum ShippingDateOption { + case today + case tomorrow + case specific(Date) +} + +struct OrderInfoView: View { + @ObservedObject var cartViewModel: CartViewModel + @StateObject var orderViewModel = OrderViewModel() + + @State private var paymentType: PaymentType = .deposit + @State private var shippingDateOption: ShippingDateOption = .today + @State private var specificDate = Date() + @State private var requestMessage: String = "" + + @State private var navigateToSuccessPage = false + + private var destinationView: some View { + Group { + if let id = orderViewModel.createdOrderId { + OrderDetailView(orderId: id, orderViewModel: orderViewModel) + } else { + EmptyView() + } + } + } + + func formattedShippingDate() -> String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + formatter.locale = Locale(identifier: "ko_KR") + + switch shippingDateOption { + case .today: + return formatter.string(from: Date()) + case .tomorrow: + let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date()) ?? Date() + return formatter.string(from: tomorrow) +// return formatter.string(from: Calendar.current.date(byAdding: .day, value: 1, to: Date())!) + case .specific(let date): + return formatter.string(from: date) + } + } + + func makeOrderItems() -> [OrderItems] { + return cartViewModel.items.map { + OrderItems(partId: $0.id, amount: $0.amount) + } + } + + var body: some View { + VStack(spacing: 0) { + ScrollView { + contentView + } + .padding(.horizontal) + .padding(.top) + + bottomOrderButton + } + .background(Color.Light) + .navigationTitle("주문/결제") + .navigationBarTitleDisplayMode(.inline) + .task { await cartViewModel.fetchCart() } + .edgesIgnoringSafeArea(.bottom) + .onChange(of: orderViewModel.isOrderSuccess) { success in + if success { + Task { + await cartViewModel.clearCart() + navigateToSuccessPage = true + } + } + } + } +} + +// MARK: - UI 구성 View +extension OrderInfoView { + + private var contentView: some View { + VStack(alignment: .leading, spacing: 17) { + shippingInfoSection + orderListSection + paymentSection + shippingDateSection + totalPriceSection + } + } + + private var shippingInfoSection: some View { + VStack(alignment: .leading, spacing: 8) { + Text("배송 정보") + .font(.headline) + + VStack(alignment: .leading, spacing: 8) { + Text("홍길동").font(.system(size: 15, weight: .medium)) + Text("서울특별시 강남구 테헤란로114길") + .font(.system(size: 14)) + .foregroundColor(.textGray1) + Text("010-1111-2222") + .font(.system(size: 14)) + .foregroundColor(.textGray1) + + Text("요청사항") + .font(.system(size: 14, weight: .medium)) + .padding(.top, 5) + + ZStack(alignment: .topLeading) { + if requestMessage.isEmpty { + Text("요청사항을 입력하세요") + .foregroundColor(.gray) + .font(.system(size: 14)) + .padding(.top, 12) + .padding(.leading, 10) + } + + TextEditor(text: $requestMessage) + .font(.system(size: 14)) + .padding(.top, 4) + .padding(.leading, 6) + .scrollContentBackground(.hidden) + .background(Color.clear) + } + .frame(height: 70) + .background(Color.white) + .overlay(RoundedRectangle(cornerRadius: 10) + .stroke(Color(.systemGray4), lineWidth: 1)) + } + .padding() + .background(Color.white) + .cornerRadius(16) + } + .padding(.leading, 5) + } + + private var orderListSection: some View { + VStack(alignment: .leading) { + Text("주문 목록") + .font(.headline) + .padding(.leading, 5) + + Section { + LazyVStack(spacing: 8) { + ForEach(cartViewModel.items) { cartItem in + CartInfoCard(item: cartItem, quantity: cartItem.amount) + .padding(.horizontal, 5) + } + } + } + .background(Color.white) + .cornerRadius(16) + } + } + + private var paymentSection: some View { + VStack(alignment: .leading) { + Text("결제 수단") + .font(.headline) + .padding(.leading, 5) + + VStack(alignment: .leading, spacing: 5) { + HStack{ + RadioButtonRow(title: "예치금 (잔액 ₩1,200,000)", selected: paymentType == .deposit) { + paymentType = .deposit + } + Spacer() + } + .frame(height: 35) + HStack{ + RadioButtonRow(title: "직접 결제", selected: paymentType == .card) { + paymentType = .card + } + Spacer() + } + .frame(height: 35) + } + .padding() + .frame(maxWidth: .infinity) + .background(Color.white) + .cornerRadius(16) + } + } + + private var shippingDateSection: some View { + VStack(alignment: .leading) { + Text("배송 요청일") + .font(.headline) + .padding(.leading, 5) + + VStack(alignment: .leading, spacing: 5) { + HStack { + RadioButtonRow(title: "오늘", selected: { + if case .today = shippingDateOption { return true } + return false + }()) { + shippingDateOption = .today + } + Spacer() + } + .frame(height: 35) + HStack { + RadioButtonRow(title: "내일", selected: { + if case .tomorrow = shippingDateOption { return true } + return false + }()) { + shippingDateOption = .tomorrow + } + Spacer() + } + .frame(height: 35) + + HStack (spacing: 30){ + RadioButtonRow(title: "날짜 선택", selected: { + if case .specific(_) = shippingDateOption { return true } + return false + }()) { + shippingDateOption = .specific(specificDate) + } + .padding(.trailing, 5) + + if case .specific(_) = shippingDateOption { + CustomDatePickerField(date: Binding { + specificDate + } set: { newValue in + specificDate = newValue + shippingDateOption = .specific(newValue) + }) + } + } + .frame(height: 35) + + } + .padding() + .background(Color.white) + .cornerRadius(16) + } + } + + private var totalPriceSection: some View { + HStack { + Text("결제금액") + .font(.system(size: 16, weight: .semibold)) + Spacer() + Text("\(cartViewModel.cart?.totalPrice ?? 0)원") + .font(.system(size: 16, weight: .bold)) + .foregroundColor(.Primary) + } + .padding() + .background(Color.white) + .cornerRadius(10) + } + + private var bottomOrderButton: some View { + VStack { + Button { + Task { + let orderRequest = OrderRequest( + orderItems: makeOrderItems(), + requestedShippingDate: formattedShippingDate(), + paymentType: paymentType.rawValue, + etc: requestMessage + ) + await orderViewModel.createOrder(request: orderRequest) + } + } label: { + Text("결제하기") + .font(.system(size: 16, weight: .bold)) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 70) + .background(Color.Primary) + } + + NavigationLink(destination: destinationView, isActive: $navigateToSuccessPage) { + EmptyView() + } + } + } +} + +struct RadioButtonRow: View { + let title: String + var selected: Bool + var action: () -> Void + + var body: some View { + HStack { + Image(systemName: selected ? "circle.inset.filled" : "circle") + .foregroundColor(selected ? .Primary : .gray) + Text(title) + // Spacer() + } + .onTapGesture { action() } + } +} + + +struct CustomDatePickerField: View { + @Binding var date: Date + @State private var showPicker: Bool = false + + // 날짜 포맷 변환용 Formatter + private var dateFormatter: DateFormatter { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + return formatter + } + + var body: some View { + Button { + showPicker.toggle() + } label: { + HStack { + Text(dateFormatter.string(from: date)) + .font(.system(size: 14)) + .foregroundColor(.black) + + Spacer() + + Image(systemName: "calendar") + .foregroundColor(.gray) + } + .padding(10) + .frame(width: 140, height: 30) + .background( + RoundedRectangle(cornerRadius: 4) + .stroke(Color(.systemGray4)) + ) + } + .sheet(isPresented: $showPicker) { + VStack { + DatePicker( + "", + selection: $date, + in: Date()..., + displayedComponents: [.date] + ) + .datePickerStyle(.graphical) + .padding() + + Button("완료") { + showPicker = false + } + .font(.headline) + .padding() + .frame(maxWidth: .infinity) + } + .presentationDetents([.medium]) + } + } +} + diff --git a/StockMate/StockMate/app/feature/orders/ui/OrderListView.swift b/StockMate/StockMate/app/feature/orders/ui/OrderListView.swift new file mode 100644 index 0000000..0dc04a5 --- /dev/null +++ b/StockMate/StockMate/app/feature/orders/ui/OrderListView.swift @@ -0,0 +1,197 @@ +// +// OrderListView.swift +// StockMate +// +// Created by Admin on 10/21/25. +// + +import SwiftUI + +struct OrderListView: View { + @StateObject private var orderViewModel = OrderViewModel() + + var body: some View { +// NavigationStack { + VStack(alignment: .leading, spacing: 0) { + + if orderViewModel.isLoading { + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if let error = orderViewModel.errorMessage { + Text(error) + .foregroundColor(.red) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if orderViewModel.orders.isEmpty { + Text("주문 내역이 없습니다.") + .foregroundColor(.gray) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + // 날짜별로 그룹화 (최신순) +// let groupedOrders = Dictionary(grouping: orderViewModel.orders) { order in +// order.createdAt.split(separator: "T").first ?? "" +// } + let groupedOrders = Dictionary(grouping: orderViewModel.orders) { order in + order.createdAt.split(separator: "T").first.map(String.init) ?? "" + } + .sorted { $0.key > $1.key } + + ScrollView { + LazyVStack(alignment: .leading, spacing: 20) { + ForEach(groupedOrders, id: \.key) { date, orders in + VStack(alignment: .leading, spacing: 12) { + Text(formatDate(String(date))) + .font(.headline) + .padding(.leading, 25) + .padding(.top) + + ForEach(orders) { order in + OrderListCardView(order: order, orderViewModel: orderViewModel) + } + } + } + } + .padding(.bottom) + } + .padding(.top) + } + } + .background(Color.Light) + .navigationTitle("주문 내역") + .task { + await orderViewModel.loadOrders() + } +// } + } + + func formatDate(_ dateString: String) -> String { + // yyyy-MM-dd → yyyy / MM / dd + let comps = dateString.split(separator: "-") + guard comps.count == 3 else { return dateString } + return "\(comps[0])년 \(comps[1])월 \(comps[2])일" + } +} + +struct OrderListCardView: View { + let order: OrderResponseItem + @ObservedObject var orderViewModel: OrderViewModel + + var body: some View { + VStack(alignment: .leading, spacing: 7) { + // 상단: 주문번호 + 상세 버튼 + HStack { + Text("주문 번호: \(order.orderNumber)") + .font(.caption) + .foregroundColor(.gray) + Spacer() + NavigationLink(destination: OrderDetailView(orderId: order.id, orderViewModel: orderViewModel)) { + Text("주문 상세 >") + .font(.caption) + .foregroundColor(.gray) + } + } + + Divider() + + HStack(alignment: .center, spacing: 12) { + // 대표 이미지 + AsyncImage(url: URL(string: order.orderItems.first?.partDetail.image ?? "")) { image in + image.resizable().scaledToFit() + } placeholder: { + Color.gray.opacity(0.2) + } + .frame(width: 64, height: 64) + .cornerRadius(10) + + // 제품명 + 개수 + VStack(alignment: .leading, spacing: 4) { + if let first = order.orderItems.first { + Text(first.partDetail.korName) + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.black) + .lineLimit(1) + + if order.orderItems.count > 1 { + Text("외 \(order.orderItems.count - 1)개") + .font(.caption) + .foregroundColor(.gray) + } + } + } + + Spacer() + + // 상태 뱃지 + Text(statusText(order.orderStatus)) + .font(.system(size: 13, weight: .semibold)) + .padding(.horizontal, 12) + .padding(.vertical, 7) + .background(statusBdColor(order.orderStatus)) + .foregroundColor(statusColor(order.orderStatus)) + .cornerRadius(15) + } + + // 주문취소 버튼 (필요 시) + if order.orderStatus == "ORDER_COMPLETED" { + Button(action: { + // 주문취소 처리 + Task { + await orderViewModel.cancelOrder(orderId: order.id) + } + }) { + Text("주문 취소") + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 40) + .background(Color.Primary) + .cornerRadius(6) + } + .padding(.top, 6) + } + } + .padding() + .background(Color.white) + .cornerRadius(12) + .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 2) + .padding(.horizontal) + } + + func statusText(_ status: String) -> String { + switch status { + case "ORDER_COMPLETED": return "주문 완료" + case "PENDING_SHIPPING": return "출고 대기" + case "REJECTED": return "출고 반려" + case "SHIPPING": return "배송 중" + case "DELIVERED": return "배송 완료" + case "RECEIVED": return "입고 완료" + case "CANCELLED": return "주문 취소" + default: return "알 수 없음" + } + } + + func statusColor(_ status: String) -> Color { + switch status { + case "ORDER_COMPLETED": return .StatusGreen + case "PENDING_SHIPPING": return .InvUse + case "REJECTED": return .Danger + case "SHIPPING": return .Transfer + case "DELIVERED": return .Secondary + case "RECEIVED": return .StatusPurple + case "CANCELLED": return .gray + default: return .gray.opacity(0.6) + } + } + + func statusBdColor(_ status: String) -> Color { + switch status { + case "ORDER_COMPLETED": return .StatusGreenBg + case "PENDING_SHIPPING": return .InvUseBg + case "REJECTED": return .DangerBg + case "SHIPPING": return .TransferBg + case "DELIVERED": return .LightBlue04 + case "RECEIVED": return .StatusPurpleBg + case "CANCELLED": return Color(hex: "#EEEEEF") + default: return .gray.opacity(0.6) + } + } +} diff --git a/StockMate/StockMate/app/feature/orders/ui/OrderRequestSearchView.swift b/StockMate/StockMate/app/feature/orders/ui/OrderRequestSearchView.swift new file mode 100644 index 0000000..d75fea6 --- /dev/null +++ b/StockMate/StockMate/app/feature/orders/ui/OrderRequestSearchView.swift @@ -0,0 +1,182 @@ +// +// OrderRequestSearchView.swift +// StockMate +// +// Created by Admin on 10/27/25. +// + +import SwiftUI + +struct OrderRequestSearchView: View { + @ObservedObject var cartViewModel: CartViewModel + + @StateObject var inventoryViewModel = InventoryViewModel() + @State private var searchText = "" + + // 카테고리, 분류, 모델 + private let categories = ["전기/램프", "엔진/미션", "하체/바디", "내장/외장", "기타소모품"] + private let trims = ["준중형/소형", "중형", "대형", "SUV", "화물/트럭/승합", "수소/전기"] + private let trimToModels: [String: [String]] = [ + "준중형/소형": [ "아반떼MD", "아반떼AD", "아반떼CN7", "I30", "엑센트", "아이오닉", "벨로스터", "캐스퍼" ], + "중형": [ "NF소나타", "YF소나타", "LF소나타", "DN8소나타", "그랜저TG", "그랜저HG", "그랜저IG", "그랜저GN7", "I40" ], + "대형": ["제네시스BH", "에쿠스"], + "SUV": [ "베뉴", "코나OS", "코나SX2", "투싼IX", "투싼TL", "투싼NX4", "싼타페CM", "싼타페DM", "싼타페TM", "싼타페MX5", "맥스크루즈", "베라크루즈", "팰리세이드LX2", "팰리세이드LX3" ], + "화물/트럭/승합": [ "스타렉스", "그랜드스타렉스", "스타리아", "포터2", "쏠라티", "마이티", "메가트럭", "카운티" ], + "수소/전기": ["아이오닉5", "아이오닉6", "아이오닉9", "넥쏘FE", "넥쏘NH2"] + ] + + private var filteredModels: [String] { + if inventoryViewModel.selectedTrims.isEmpty { + return trimToModels.values.flatMap { $0 } + } else { + return inventoryViewModel.selectedTrims + .flatMap { trimToModels[$0] ?? [] } + } + } + + var body: some View { + ZStack { + VStack(spacing: 0) { + // 🔍 검색창 + HStack { + Image(systemName: "magnifyingglass") + .foregroundColor(.gray) + + TextField("부품을 검색하세요.", text: $searchText) + .textFieldStyle(PlainTextFieldStyle()) + .onSubmit { + let term = searchText.trimmingCharacters( + in: .whitespacesAndNewlines + ) + guard !term.isEmpty else { return } + Task { + await inventoryViewModel + .searchByName(name: term, reset: true) + } + } + + if !searchText.isEmpty { + Button(action: { + searchText = "" + inventoryViewModel.isSearching = false + }) { + Image(systemName: "xmark") + .foregroundColor(.gray) + } + .buttonStyle(.plain) + .padding(.trailing,3) + } + } + .padding() + .background(Color(.white)) + .cornerRadius(9999) + .overlay( + RoundedRectangle(cornerRadius: 9999) + .stroke(Color.gray.opacity(0.4), lineWidth: 1) + ) + .padding(.horizontal) + .padding(.vertical) + + // 필터 및 초기화 버튼 + HStack(spacing: 10) { + FilterMenu( + title: "카테고리", + items: categories, + selectedItems: inventoryViewModel.selectedCategories, + onTap: { inventoryViewModel.toggleCategory($0) } + ) + + FilterMenu( + title: "분류", + items: trims, + selectedItems: inventoryViewModel.selectedTrims, + onTap: { inventoryViewModel.toggleTrim($0) } + ) + + FilterMenu( + title: "모델", + items: filteredModels, + selectedItems: inventoryViewModel.selectedModels, + onTap: { inventoryViewModel.toggleModel($0) } + ) + + // 🔄 초기화 버튼 + Button(action: { + inventoryViewModel.resetFilters(with: searchText) + }) { + HStack(spacing: 4) { + Image(systemName: "arrow.counterclockwise") + Text("초기화") + } + .font(.system(size: 13, weight: .medium)) + .foregroundColor(.blue) + .padding(.trailing, 8) + } + .frame(maxWidth: .infinity, alignment: .trailing) + + } + .padding(.horizontal) + .padding(.bottom, 16) + + // 📋 재고 리스트 + ScrollView { + LazyVStack(spacing: 10) { + + ForEach( + inventoryViewModel.isSearching + ? inventoryViewModel.filteredSearchResults // 검색 + 필터링 + : inventoryViewModel.inventoryItems + ) { item in + let qty = cartViewModel.quantity(for: item.id) + + OrderRequestCardView( + item: item, + quantity: qty, + onIncrease: { Task { await cartViewModel.increaseQuantity(for: item.id) } }, + onDecrease: { Task { await cartViewModel.decreaseQuantity(for: item.id) }}, + onAddToCart: { Task { await cartViewModel.addToCart(partId: item.id, amount: 1) }}, + onRemoveFromCart: { Task { await cartViewModel.decreaseQuantity(for: item.id) }} + ) + .padding(.horizontal) + .onAppear { + Task { + if inventoryViewModel.isSearching { + if item.id == inventoryViewModel.filteredSearchResults.last?.id, + inventoryViewModel.searchHasMore { + await inventoryViewModel .loadMore(searchText: searchText) + } + } else { + if item.id == inventoryViewModel.inventoryItems.last?.id, + inventoryViewModel.hasMore, + !inventoryViewModel.isLoading + { + await inventoryViewModel.loadMore(searchText: searchText) + } + } + } + } + } + + if inventoryViewModel.isLoading { + ProgressView() + .padding(.vertical) + } + } + } + } + .background(Color.Light) + .navigationTitle("직접 발주") + .task { + await inventoryViewModel.loadInventoryList(reset: true) + await cartViewModel.fetchCart() + } + + VStack { + Spacer() + CartSummaryBar(cartVM: cartViewModel) + } + .ignoresSafeArea(edges: .bottom) + + } + } +} diff --git a/StockMate/StockMate/app/feature/orders/ui/OrderResultView.swift b/StockMate/StockMate/app/feature/orders/ui/OrderResultView.swift new file mode 100644 index 0000000..39fc6bf --- /dev/null +++ b/StockMate/StockMate/app/feature/orders/ui/OrderResultView.swift @@ -0,0 +1,77 @@ +// +// OrderResultView.swift +// StockMate +// +// Created by Admin on 10/20/25. +// + +import SwiftUI + +struct OrderResultView: View { + @Environment(\.dismiss) private var dismiss + + var body: some View { + VStack(spacing: 24) { + + Spacer() + + // ✅ 결제 완료 아이콘 + Image(systemName: "checkmark.circle.fill") + .resizable() + .frame(width: 80, height: 80) + .foregroundColor(.blue) + .padding(.bottom, 8) + + // ✅ 완료 문구 + Text("결제가 완료되었습니다") + .font(.title3) + .bold() + Text("주문 내역은 발주 탭에서 확인할 수 있습니다.") + .font(.subheadline) + .foregroundColor(.gray) + + Spacer() + + // ✅ 하단 버튼 + VStack(spacing: 12) { + Button { + dismiss() // 이전 화면으로 돌아가기 + } label: { + Text("확인") + .font(.headline) + .frame(maxWidth: .infinity) + .padding() + .foregroundColor(.white) + .background(Color.blue) + .cornerRadius(12) + } + + Button { + // 홈으로 이동 (추후 NavigationStack 연결 시) + } label: { + Text("홈으로 이동") + .font(.headline) + .frame(maxWidth: .infinity) + .padding() + .foregroundColor(.blue) + .background(Color.white) + .cornerRadius(12) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.blue, lineWidth: 1) + ) + } + } + .padding(.horizontal) + + Spacer() + } + .padding() + .navigationBarBackButtonHidden(true) + .navigationTitle("결제 완료") + } +} + +#Preview { + OrderResultView() +} diff --git a/StockMate/StockMate/app/feature/orders/ui/OrderView.swift b/StockMate/StockMate/app/feature/orders/ui/OrderView.swift index 95b7f90..c238a30 100644 --- a/StockMate/StockMate/app/feature/orders/ui/OrderView.swift +++ b/StockMate/StockMate/app/feature/orders/ui/OrderView.swift @@ -8,129 +8,125 @@ import SwiftUI struct OrderView: View { + @StateObject var inventoryViewModel = InventoryViewModel() + @ObservedObject var cartViewModel: CartViewModel + var body: some View { - VStack(spacing: 0) { - // 상단 타이틀 - Text("발주 목록") - .font(.headline) - .padding(.top, 16) - - // 필터 탭 버튼 -// HStack(spacing: 10) { -// ForEach(["Product", "Category", "Payment", "status"], id: \.self) { title in -// Text(title) -// .font(.system(size: 14, weight: .medium)) -// .foregroundColor(.black.opacity(0.8)) -// .padding(.horizontal, 14) -// .padding(.vertical, 8) -// .background(Color.white) -// .cornerRadius(10) -// .shadow(color: Color.black.opacity(0.05), radius: 1, x: 0, y: 1) -// } -// } -// .padding(.top, 12) - - // 주문 리스트 + ZStack{ ScrollView { - VStack(alignment: .leading, spacing: 16) { - OrderSection(date: "2025/09/30", orders: [ - OrderItem(image: "bolt.fill", name: "현대 아이오닉5", detail: "브레이크 등/ 5개", price: "₩300,000", status: "배송중", color: .green), - OrderItem(image: "bolt.fill", name: "현대 아이오닉5", detail: "브레이크 등/ 5개", price: "₩300,000", status: "승인완료", color: .green), - OrderItem(image: "bolt.fill", name: "현대 아이오닉5", detail: "브레이크 등/ 5개", price: "₩300,000", status: "승인완료", color: .green), - OrderItem(image: "bolt.fill", name: "현대 아이오닉5", detail: "브레이크 등/ 5개", price: "₩300,000", status: "승인 대기", color: .orange) - ]) - - OrderSection(date: "2025/09/29", orders: [ - OrderItem(image: "bolt.fill", name: "현대 아이오닉5", detail: "브레이크 등/ 5개", price: "₩300,000", status: "승인 거절", color: .red), - OrderItem(image: "bolt.fill", name: "현대 아이오닉5", detail: "브레이크 등/ 5개", price: "₩300,000", status: "입고완료", color: .blue) - ]) - } - .padding(.horizontal) - .padding(.vertical, 10) - } - - // 하단 고정 버튼 - Button(action: { - // 발주 요청 액션 - }) { - Text("발주 요청") - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(.white) - .frame(maxWidth: .infinity) + // 타이틀 + Text("재고 관리") + .font(.title2) + .bold() + .padding(.top, 13) + .padding(.leading, 25) + .frame(maxWidth: .infinity, alignment: .leading) + + Text("직접 발주") + .font(.system(size: 14)) + .bold() + .padding(.top, 13) + .padding(.leading, 25) + .frame(maxWidth: .infinity, alignment: .leading) + + // 🔍 검색창 + NavigationLink(destination: + OrderRequestSearchView( + cartViewModel: cartViewModel + //inventoryViewModel: inventoryViewModel + ) + ) { + HStack { + Image(systemName: "magnifyingglass") + .foregroundColor(.gray) + + Text("부품을 검색하세요.") + .foregroundColor(.gray) + Spacer() + } .padding() - .background(Color(#colorLiteral(red: 0.215, green: 0.318, blue: 0.686, alpha: 1))) // #374EAF - .cornerRadius(12) + .background(Color(.white)) + .cornerRadius(9999) + .overlay( + RoundedRectangle(cornerRadius: 9999) + .stroke(Color.gray.opacity(0.4), lineWidth: 1) + ) .padding(.horizontal) - .padding(.bottom, 12) - } - } - .background(Color(.systemGray6)) - .ignoresSafeArea(edges: .bottom) - } -} - -struct OrderSection: View { - var date: String - var orders: [OrderItem] - - var body: some View { - VStack(alignment: .leading, spacing: 10) { - Text(date) - .font(.system(size: 13, weight: .medium)) - .foregroundColor(.gray) - - ForEach(orders, id: \.id) { order in - HStack { - Image(systemName: order.image) - .resizable() - .scaledToFit() - .frame(width: 40, height: 40) - .padding(6) - .background(Color.white) - .cornerRadius(10) - .shadow(color: .black.opacity(0.05), radius: 2) - - VStack(alignment: .leading, spacing: 2) { - Text(order.name) - .font(.system(size: 15, weight: .semibold)) - Text(order.detail) - .font(.system(size: 13)) - .foregroundColor(.gray) + } + .buttonStyle(.plain) + + // 타이틀 + Text("부족 재고") + .font(.system(size: 14)) + .bold() + .padding(.top, 13) + .padding(.leading, 25) + .frame(maxWidth: .infinity, alignment: .leading) + + LazyVStack(alignment: .leading, spacing: 14) { + ForEach(inventoryViewModel.underLimitItems) { item in + + let qty = cartViewModel.quantity(for: item.id) + + OrderRequestCardView( + item: item, + quantity: qty, + onIncrease: { + Task { + await cartViewModel.increaseQuantity(for: item.id) + } + }, + onDecrease: { + Task { + await cartViewModel.decreaseQuantity(for: item.id) + } + }, + onAddToCart: { + Task { + await cartViewModel.addToCart(partId: item.id, amount: 1) + } + }, + onRemoveFromCart: { + Task { + await cartViewModel.decreaseQuantity(for: item.id) + } + } + ) + .onAppear { + if item.id == inventoryViewModel.underLimitItems.last?.id { + Task { await inventoryViewModel.loadUnderLimitList() } + } + } } - Spacer() - VStack(alignment: .trailing, spacing: 6) { - Text(order.price) - .font(.system(size: 15, weight: .medium)) - .foregroundColor(.purple) - Text(order.status) - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(order.color) - .padding(.horizontal, 10) - .padding(.vertical, 4) - .background(order.color.opacity(0.1)) - .cornerRadius(10) + if inventoryViewModel.isLoading { + ProgressView().padding() } } - .padding() - .background(Color.white) - .cornerRadius(14) - .shadow(color: Color.black.opacity(0.03), radius: 2) + .padding(.horizontal) + + } + .background(Color.Light) + .task { + if inventoryViewModel.underLimitItems.isEmpty { + await inventoryViewModel.loadUnderLimitList(reset: true) + } + await cartViewModel.fetchCart() } + + // OrderView 내부 ScrollView 아래 장바구니 확인 버튼 + VStack { + Spacer() + CartSummaryBar(cartVM: cartViewModel) + } + .ignoresSafeArea(edges: .bottom) + } - } -} -struct OrderItem: Identifiable { - let id = UUID() - var image: String - var name: String - var detail: String - var price: String - var status: String - var color: Color + } } -#Preview { - OrderView() -} +// +//#Preview { +// OrderView() +//} diff --git a/StockMate/StockMate/app/feature/orders/ui/ReceiptView.swift b/StockMate/StockMate/app/feature/orders/ui/ReceiptView.swift new file mode 100644 index 0000000..59aec01 --- /dev/null +++ b/StockMate/StockMate/app/feature/orders/ui/ReceiptView.swift @@ -0,0 +1,182 @@ +// +// ReceiptView.swift +// StockMate +// +// Created by Admin on 10/28/25. +// + +import SwiftUI +import PDFKit + +enum PDFType { + case a4 + case receipt80mm +} +// TODO: API 연결 후 주문 상세 페이지와 연결 +struct ReceiptView: View { + + + @State var paymentType = "예치금" + @State var approvalNumber = "202510300743" + @State var date = "2025/10/30 07:43:54" + @State var orderNumber = "SMO-2" + @State var itemName = "배터리-트랜스미터" + @State var quantity = 1 + @State var price = 5273 + @State var sellerName = "박시영" + @State var businessNumber = "888777776666" + @State var phone = "010-2596-2352" + @State var address = "서울특별시 성동구 동일로 259 3층" + + var vat: Int { Int(Double(price) * 0.1) } + var total: Int { price + vat } + + var body: some View { + ScrollView { + VStack(alignment: .leading) { + receiptContent + + HStack { + pdfButton(type: .a4, title: "A4 PDF") + pdfButton(type: .receipt80mm, title: "영수증 PDF") + } + .padding(.horizontal) + .padding(.bottom) + } + .background(Color.white) + .cornerRadius(12) + .padding() + } + .background(Color.Light) + .navigationTitle("영수증") + .navigationBarTitleDisplayMode(.inline) + + } + + private var receiptContent: some View { + VStack(alignment: .leading, spacing: 16) { + section("결제 정보") { + Divider() + row("거래종류", paymentType) + row("승인번호", approvalNumber) + row("거래일시", date) + } + .padding(4) + .padding(.top,5) + + section("구매정보") { + Divider() + row("주문번호", orderNumber) + row("상품명", "\(itemName), \(quantity)개") + row("공급가액", "\(price)원") + row("부가세액", "\(vat)원") + row("합계금액", "\(total)원", highlight: true) + } + .padding(4) + .padding(.top) + + section("판매자 정보") { + Divider() + row("대표자명", sellerName) + row("사업자등록번호", businessNumber) + row("전화번호", phone) + row("사업장주소", address) + } + .padding(4) + .padding(.top) + + Divider() + + Text(""" + • 비현금성으로 지급되는 예치금 사용 금액의 경우 현금 영수증 발행 대상에서 + 제외될 수 있습니다. + • 발생 정보는 구매확정 또는 거래 완료 이후 전달되기 때문에 국세청 사이트에서 + 즉시 확인되지 않을 수 있습니다. + • 이 영수증은 조세특례제한법 제 126조 3항에 의거 연말정산 시 소득공제혜택 + 부여 목적으로 발행됩니다. (국세청 회원가입 필수) + • 현금 영수증은 구매확정 또는 거래 완료 후 48시간 내로 국세청에서 확인 작업 + 후 최종 확정됩니다. + • 국세청 확인: 홈택스 홈페이지(https://www.hometax.go.kr/) 또는 국세청 + 상담센터(현금영수증 문의 ☎️126-1-1) + """) + .font(.system(size: 10.5)) + .foregroundColor(.textGray1) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + .lineSpacing(4) + .padding(.leading, 2) // ✅ 총알 뒤 문장 들여쓰기 추가 + + } + .padding() + } + + private func section(_ title: String, @ViewBuilder content: () -> some View) -> some View { + VStack(alignment: .leading, spacing: 8) { + Text(title) + .font(.headline) + content() + } + .font(.subheadline) + } + + private func row(_ key: String, _ value: String, highlight: Bool = false) -> some View { + HStack { + Text(key) + Spacer() + Text(value) + .fontWeight(highlight ? .bold : .regular) + .foregroundColor(highlight ? .blue : .primary) + } + } + + private func pdfButton(type: PDFType, title: String) -> some View { + Button(action: { + generatePDF(type: type) + }) { + Text(title) + .frame(maxWidth: .infinity) + .padding() + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(8) + } + } + + private func generatePDF(type: PDFType) { + let content = receiptContent + let renderer = ImageRenderer(content: content) + + let width: CGFloat + let height: CGFloat = 2000 + + switch type { + case .a4: width = 595.2 // A4 width in pt + case .receipt80mm: width = 226.77 // 80mm in pt + } + + renderer.scale = UIScreen.main.scale + + if let image = renderer.uiImage { + let pdfDoc = PDFDocument() + let pdfPage = PDFPage(image: image) + pdfDoc.insert(pdfPage!, at: 0) + + let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("receipt.pdf") + if pdfDoc.write(to: tempURL) { + let av = UIActivityViewController(activityItems: [tempURL], applicationActivities: nil) + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootVC = windowScene.windows.first?.rootViewController { + rootVC.present(av, animated: true) + } +// let av = UIActivityViewController(activityItems: [tempURL], applicationActivities: nil) +// UIApplication.shared.windows.first?.rootViewController?.present(av, animated: true) + } + } + } +} + +struct ReceiptView_Previews: PreviewProvider { + static var previews: some View { + ReceiptView() + } +} diff --git a/StockMate/StockMate/app/feature/orders/viewmodel/OrderDetailViewModel.swift b/StockMate/StockMate/app/feature/orders/viewmodel/OrderDetailViewModel.swift new file mode 100644 index 0000000..2903e8c --- /dev/null +++ b/StockMate/StockMate/app/feature/orders/viewmodel/OrderDetailViewModel.swift @@ -0,0 +1,31 @@ +// +// OrderDetailViewModel.swift +// StockMate +// +// Created by Admin on 10/22/25. +// + +import Foundation + +@MainActor +final class OrderDetailViewModel: ObservableObject { + @Published var order: OrderResponseItem? + @Published var isLoading = false + @Published var errorMessage: String? + + private let repository = OrderRepositoryImpl() + + func fetchOrderDetail(orderId: Int) async { + isLoading = true + defer { isLoading = false } + + let result = await repository.fetchOrderDetail(orderId: orderId) + + switch result { + case .success(let detail): + self.order = detail + case .failure(let error): + self.errorMessage = error.localizedDescription + } + } +} diff --git a/StockMate/StockMate/app/feature/orders/viewmodel/OrderViewModel.swift b/StockMate/StockMate/app/feature/orders/viewmodel/OrderViewModel.swift new file mode 100644 index 0000000..dccd03f --- /dev/null +++ b/StockMate/StockMate/app/feature/orders/viewmodel/OrderViewModel.swift @@ -0,0 +1,83 @@ +// +// OrderViewModel.swift +// StockMate +// +// Created by Admin on 10/21/25. +// + +import Foundation + +@MainActor +final class OrderViewModel: ObservableObject { + @Published var orders: [OrderResponseItem] = [] + @Published var isLoading = false + @Published var errorMessage: String? + + @Published var isOrderSuccess: Bool = false + @Published var isOrderCanceled: Bool = false + + @Published var createdOrderId: Int? + + private let repository: OrderRepositoryProtocol + + init(repository: OrderRepositoryProtocol = OrderRepositoryImpl()) { + self.repository = repository + } + + func loadOrders( + status: String? = nil, + startDate: String? = nil, + endDate: String? = nil, + page: Int = 0, + size: Int = 20 + ) async { + isLoading = true + defer { isLoading = false } + + let result = await repository.fetchMyOrders( + status: status, + startDate: startDate, + endDate: endDate, + page: page, + size: size + ) + + switch result { + case .success(let pageData): + orders = pageData.content + case .failure(let error): + errorMessage = error.message + } + } + + // 주문 생성 + func createOrder(request: OrderRequest) async { + let result = await repository.createOrder(request: request) + + switch result { + case .success(let response): + self.createdOrderId = response.orderId + self.isOrderSuccess = true + + case .failure(let error): + print("❌ 주문 실패:", error.message) + self.errorMessage = error.message + } + } + + + func cancelOrder(orderId: Int) async { + isLoading = true + let result = await repository.cancelOrder(orderId: orderId) + + switch result { + case .success: + await loadOrders() // ✅ 취소 후 즉시 UI 새로고침 + case .failure(let error): + errorMessage = error.message + } + isLoading = false + } + + +} diff --git a/StockMate/StockMate/app/feature/user/ui/ProfileView.swift b/StockMate/StockMate/app/feature/user/ui/ProfileView.swift new file mode 100644 index 0000000..92fdf6a --- /dev/null +++ b/StockMate/StockMate/app/feature/user/ui/ProfileView.swift @@ -0,0 +1,152 @@ +// +// ProfileView().swift +// StockMate +// +// Created by Admin on 10/21/25. +// + +import SwiftUI + +struct ProfileView: View { + @StateObject private var userViewModel = UserViewModel() + + var body: some View { +// NavigationStack { + VStack(alignment: .leading, spacing: 24) { + + // MARK: - Profile Header + HStack(spacing: 16) { + ProfileCircleView(name: userViewModel.userInfo?.owner ?? "사용자", size: 50) + + VStack(alignment: .leading, spacing: 4) { + HStack { + Image("location") + .foregroundColor(.gray) + Text(userViewModel.userInfo?.storeName ?? "가게명 없음") + .foregroundColor(.gray) + .font(.subheadline) + } + Text(userViewModel.userInfo?.owner ?? "이름 없음") + .font(.title3.bold()) + .foregroundColor(Color(hex: "#2B3A1A")) + } + + Spacer() + } + .padding(.horizontal) + .padding(.top, 32) + + // MARK: - General Section + VStack(alignment: .leading, spacing: 12) { + Text("General") + .font(.system(size: 16, weight: .semibold)) + .padding(.leading) + + VStack(spacing: 10) { + SettingRow(icon: "person.crop.circle", title: "Edit Profile") + SettingRow(icon: "lock.circle", title: "Change Password") + SettingRow(icon: "bell", title: "Notifications") + SettingRow(icon: "location.circle", title: "배송 현황") + + SettingNavigationRow(icon: "bag", title: "주문 내역", destination: OrderListView()) + } + .padding(3) + .background(Color.Light) + .cornerRadius(12) + .padding(.horizontal) + } + + // MARK: - Preferences Section + VStack(alignment: .leading, spacing: 12) { + Text("Preferences") + .font(.system(size: 16, weight: .semibold)) + .padding(.leading) + + VStack(spacing: 10) { + SettingRow(icon: "shield", title: "Legal and Policies") + SettingRow(icon: "questionmark.circle", title: "Help & Support") + SettingRow(icon: "arrow.right.circle", title: "Logout", iconColor: .red, textColor: .red) + } + .padding(3) + .background(Color.Light) + .cornerRadius(12) + .padding(.horizontal) + } + + Spacer() + } + .background(Color.Light) + .navigationBarTitleDisplayMode(.inline) + .onAppear { + Task { await userViewModel.loadUserInfo() } + } +// } + } + } + + // MARK: - SettingRow (동일) +struct SettingRow: View { + var icon: String + var title: String + var iconColor: Color = .black + var textColor: Color = .primary + + var body: some View { + HStack { + Image(systemName: icon) + .font(.system(size: 18)) + .foregroundColor(iconColor) + .frame(width: 24) + + Text(title) + .font(.system(size: 16)) + .foregroundColor(textColor) + + Spacer() + Image(systemName: "chevron.right") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.gray) + } + .padding() + .background(RoundedRectangle(cornerRadius: 10).fill(Color.Light)) + .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color(.systemGray4), lineWidth: 1)) + .shadow(color: .black.opacity(0.05), radius: 1, x: 0, y: 1) + } +} + +struct SettingNavigationRow: View { + var icon: String + var title: String + var iconColor: Color = .black + var textColor: Color = .primary + var destination: Destination + + var body: some View { + NavigationLink(destination: destination) { + HStack { + Image(systemName: icon) + .font(.system(size: 18)) + .foregroundColor(iconColor) + .frame(width: 24) + + Text(title) + .font(.system(size: 16)) + .foregroundColor(textColor) + + Spacer() + Image(systemName: "chevron.right") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.gray) + } + .padding() + .background(RoundedRectangle(cornerRadius: 10).fill(Color.Light)) + .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color(.systemGray4), lineWidth: 1)) + .shadow(color: .black.opacity(0.05), radius: 1, x: 0, y: 1) + } + } +} + + + #Preview { + ProfileView() + } diff --git a/StockMate/StockMate/app/navigation/MainTabView.swift b/StockMate/StockMate/app/navigation/MainTabView.swift index 0b42bed..b5bf61c 100644 --- a/StockMate/StockMate/app/navigation/MainTabView.swift +++ b/StockMate/StockMate/app/navigation/MainTabView.swift @@ -8,6 +8,8 @@ import SwiftUI struct MainTabView: View { + @StateObject var cartVM = CartViewModel() + @State private var selectedTab = 0 @State private var tabTappedTrigger = false @@ -17,11 +19,14 @@ struct MainTabView: View { ZStack { switch selectedTab { case 0: NavigationStack{ HomeView() } - case 1: NavigationStack{ OrderView() } + case 1: NavigationStack{ OrderView(cartViewModel: cartVM) } //, inventoryViewModel: inventoryVM) } +// case 1: NavigationStack{ OrderView() } case 2: NavigationStack { InventoryView(selectedTab: $selectedTab, tabTappedTrigger: $tabTappedTrigger) } - case 3: NavigationStack{ ContentView() } - default: NavigationStack{ HomeView() } +// case 3: NavigationStack{ ContentView() } +// case 3: NavigationStack{ ReceiptView() } + case 3: NavigationStack{ ProfileView() } + default: NavigationStack{ ContentView() } } } .frame(maxWidth: .infinity, maxHeight: .infinity) diff --git a/StockMate/StockMate/resources/Assets.xcassets/chair.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/chair.imageset/Contents.json new file mode 100644 index 0000000..ad1d744 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/chair.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "chair.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/chair.imageset/chair.svg b/StockMate/StockMate/resources/Assets.xcassets/chair.imageset/chair.svg new file mode 100644 index 0000000..ea15a1f --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/chair.imageset/chair.svg @@ -0,0 +1,3 @@ + + + diff --git a/StockMate/StockMate/resources/Assets.xcassets/cog.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/cog.imageset/Contents.json new file mode 100644 index 0000000..7124035 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/cog.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "cog.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/cog.imageset/cog.svg b/StockMate/StockMate/resources/Assets.xcassets/cog.imageset/cog.svg new file mode 100644 index 0000000..bb97be6 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/cog.imageset/cog.svg @@ -0,0 +1,4 @@ + + + + diff --git a/StockMate/StockMate/resources/Assets.xcassets/lightbulb.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/lightbulb.imageset/Contents.json new file mode 100644 index 0000000..beab311 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/lightbulb.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "lightbulb.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/lightbulb.imageset/lightbulb.svg b/StockMate/StockMate/resources/Assets.xcassets/lightbulb.imageset/lightbulb.svg new file mode 100644 index 0000000..e6ad3ff --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/lightbulb.imageset/lightbulb.svg @@ -0,0 +1,3 @@ + + + diff --git a/StockMate/StockMate/resources/Assets.xcassets/package.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/package.imageset/Contents.json new file mode 100644 index 0000000..e7f4c71 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/package.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "package.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/package.imageset/package.svg b/StockMate/StockMate/resources/Assets.xcassets/package.imageset/package.svg new file mode 100644 index 0000000..c2f544d --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/package.imageset/package.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/StockMate/StockMate/resources/Assets.xcassets/spanner.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/spanner.imageset/Contents.json new file mode 100644 index 0000000..36cb254 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/spanner.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "spanner.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/spanner.imageset/spanner.svg b/StockMate/StockMate/resources/Assets.xcassets/spanner.imageset/spanner.svg new file mode 100644 index 0000000..8203013 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/spanner.imageset/spanner.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/StockMate/StockMate/resources/Color.swift b/StockMate/StockMate/resources/Color.swift index f98469f..f927e60 100644 --- a/StockMate/StockMate/resources/Color.swift +++ b/StockMate/StockMate/resources/Color.swift @@ -28,12 +28,12 @@ extension Color { // Etc static let Gray = Color(hex: "#ABABAB") + static let GrayStroke = Color(hex: "#DBDBDB") static let White = Color(hex: "#FFFFFF") static let Dark = Color(hex: "#04150C") static let Light = Color(hex: "#F7F7F7") - // Text static let textBlack = Color(hex: "#152C07") static let textGray1 = Color(hex: "#5D5C5D") @@ -77,6 +77,25 @@ extension Color { static let StatusRedBg = Color(hex: "#FDE0DD") static let StatusPurpleBg = Color(hex: "#E9E6FF") + + + //DFF6FC + // Home Status + static let Hstatus1 = Color(hex: "#DFF6FC") + static let Hstatus2 = Color(hex: "#DBDFF3") + static let Hstatus3 = Color(hex: "#EB5032") + static let Hstatus4 = Color(hex: "#8DDB55") + static let Hstatus5 = Color(hex: "#8892A2") + + // Home Status Background + static let Hstatus1Bg = Color(hex: "#DFF6FC") + static let Hstatus2Bg = Color(hex: "#DBDFF3") + static let Hstatus3Bg = Color(hex: "#FCE3DE") + static let Hstatus4Bg = Color(hex: "#EDF9E4") + static let Hstatus5Bg = Color(hex: "#ECEEF0") + + + // 투명도 포함 예시 static let boxBgWhite = Color(hex: "#40FFFFFF") // 투명도 포함 }