diff --git a/StockMate/.DS_Store b/StockMate/.DS_Store index 8b131bc..9b2b289 100644 Binary files a/StockMate/.DS_Store and b/StockMate/.DS_Store differ diff --git a/StockMate/StockMate/app/feature/cart/ui/DeliveryStatusView.swift b/StockMate/StockMate/app/feature/cart/ui/DeliveryStatusView.swift new file mode 100644 index 0000000..2825687 --- /dev/null +++ b/StockMate/StockMate/app/feature/cart/ui/DeliveryStatusView.swift @@ -0,0 +1,106 @@ +// +// DeliveryStatusView.swift +// StockMate +// +// Created by Admin on 10/30/25. +// + +import SwiftUI + +struct DeliveryStep { + let title: String + let iconName: String // Asset 이름 +} + + +struct DeliveryStatusView: View { + let steps: [DeliveryStep] = [ + DeliveryStep(title: "결제완료", iconName: "check"), + DeliveryStep(title: "승인대기중", iconName: "hourglass"), + DeliveryStep(title: "상품준비중", iconName: "uploadprogress"), + DeliveryStep(title: "배송중", iconName: "rocket"), + DeliveryStep(title: "배송완료", iconName: "pindrop") + ] + + let currentStep: Int + + var body: some View { + GeometryReader { geo in + HStack(alignment: .center, spacing: 0) { + ForEach(0.. 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) - } - } + } // ✅ 카드 레이아웃 통일용 @@ -278,3 +293,87 @@ func formatDateOrDash(_ isoDate: String?) -> String { guard comps.count == 3 else { return "-" } return "\(comps[0])년 \(comps[1])월 \(comps[2])일" } + +func deliveryStep(for status: String) -> Int { + //6 -> 전체 회색 + //4 -> 전체 파란색 + switch status { + case "ORDER_COMPLETED": return 0 // 주문 완료 + case "PAY_COMPLETED": return 0 // 결제 완료 + case "PENDING_APPROVAL": return 1 // 승인 대기 + case "FAILED": return 6 // 결제 실패 + case "PENDING_SHIPPING": return 2 // 출고 대기 + case "SHIPPING": return 3 // 배송중 + case "PENDING_RECEIVING": return 4 // 입고 대기 + case "REJECTED": return 6 // 승인 반려 + case "DELIVERED": return 4 // 배송 완료 + case "RECEIVED": return 4 // 입고 완료 + case "REFUNDED": return 6 // 환불 완료 + case "REFUND_REJECTED": return 6 // 환불 반려 + case "CANCELLED": return 6 // 주문 취소 + default: return 6 + } +} + +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 "PAY_COMPLETED": return "결제 완료" // 결제 완료 + case "PENDING_APPROVAL": return "승인 대기" // 승인대기 + case "FAILED": return "결제 실패" // 결제 실패 + case "PENDING_SHIPPING": return "출고 대기" // 출고 대기 + case "SHIPPING": return "배송중" // 배송중 + case "PENDING_RECEIVING": return "배송 완료" // 입고대기 + case "REJECTED": return "승인 반려" // 이론상 출고 반려 + case "DELIVERED": return "배송 완료" // 배송 완료 + case "RECEIVED": return "입고 완료" // 입고 완료 + case "REFUNDED": return "환불 완료" // 환불 완료 + case "REFUND_REJECTED": return "환불 반려" // 환불 반려 + case "CANCELLED": return "주문 취소" // 주문 취소 + default: return "알 수 없음" + } +} + +func statusColor(_ status: String) -> Color { + switch status { + case "ORDER_COMPLETED": return .StatusGreen + case "PAY_COMPLETED": return .StatusGreen + case "PENDING_APPROVAL": return .Warning + case "FAILED": return .Danger + case "PENDING_SHIPPING": return .InvUse + case "SHIPPING": return .Transfer + case "PENDING_RECEIVING": return .Secondary + case "REJECTED": return .Danger + case "DELIVERED": return .Secondary + case "RECEIVED": return .StatusPurple + case "REFUNDED": return .Gray + case "REFUND_REJECTED": return .Gray + case "CANCELLED": return .Gray + default: return .gray.opacity(0.6) + } +} + +func statusBdColor(_ status: String) -> Color { + switch status { + case "ORDER_COMPLETED": return .StatusGreenBg + case "PAY_COMPLETED": return .StatusGreenBg + case "PENDING_APPROVAL": return .WarningBg + case "FAILED": return .DangerBg + case "PENDING_SHIPPING": return .InvUseBg + case "SHIPPING": return .TransferBg + case "PENDING_RECEIVING": return .LightBlue04 + case "REJECTED": return .DangerBg + case "DELIVERED": return .LightBlue04 + case "RECEIVED": return .StatusPurpleBg + case "REFUNDED": return Color(hex: "#EEEEEF") + case "REFUND_REJECTED": return Color(hex: "#EEEEEF") + case "CANCELLED": return Color(hex: "#EEEEEF") + default: return .gray.opacity(0.6) + } +} diff --git a/StockMate/StockMate/app/feature/orders/ui/OrderInfoView.swift b/StockMate/StockMate/app/feature/orders/ui/OrderInfoView.swift index b167671..f32ac74 100644 --- a/StockMate/StockMate/app/feature/orders/ui/OrderInfoView.swift +++ b/StockMate/StockMate/app/feature/orders/ui/OrderInfoView.swift @@ -20,6 +20,7 @@ enum ShippingDateOption { struct OrderInfoView: View { @ObservedObject var cartViewModel: CartViewModel @StateObject var orderViewModel = OrderViewModel() + @StateObject private var depositViewModel = DepositViewModel() @State private var paymentType: PaymentType = .deposit @State private var shippingDateOption: ShippingDateOption = .today @@ -74,7 +75,10 @@ struct OrderInfoView: View { .background(Color.Light) .navigationTitle("주문/결제") .navigationBarTitleDisplayMode(.inline) - .task { await cartViewModel.fetchCart() } + .task { + await cartViewModel.fetchCart() + await depositViewModel.fetchDepositAmount() + } .edgesIgnoringSafeArea(.bottom) .onChange(of: orderViewModel.isOrderSuccess) { success in if success { @@ -84,7 +88,16 @@ struct OrderInfoView: View { } } } + // ✅ 충전 bottom sheet 연결 + .sheet(isPresented: $depositViewModel.showChargeSheet) { + DepositChargeView(viewModel: depositViewModel) + .presentationDetents([.fraction(0.80)]) // 시트 높이 85% +// .presentationDragIndicator(.visible) + } + } + + } // MARK: - UI 구성 View @@ -164,35 +177,67 @@ extension OrderInfoView { .cornerRadius(16) } } - private var paymentSection: some View { - VStack(alignment: .leading) { - Text("결제 수단") - .font(.headline) - .padding(.leading, 5) + ZStack { + // 배경 이미지 적용 + Image("deposit_background") // ← 에셋에 넣은 이미지 이름 + .resizable() + .scaledToFill() + .frame(height: 185) + .clipped() + .cornerRadius(16.39) - VStack(alignment: .leading, spacing: 5) { - HStack{ - RadioButtonRow(title: "예치금 (잔액 ₩1,200,000)", selected: paymentType == .deposit) { - paymentType = .deposit + VStack(alignment: .leading, spacing: 12) { + VStack (alignment: .leading, spacing: 13){ + HStack { + Text("사용 가능 예치금") + .font(.system(size: 17, weight: .bold)) + .padding(.leading, 5) + .padding(.top, 25) + .foregroundColor(Color.white) } - Spacer() - } - .frame(height: 35) - HStack{ - RadioButtonRow(title: "직접 결제", selected: paymentType == .card) { - paymentType = .card + + HStack { + // 예치금 금액 표시 + if depositViewModel.isLoading { + ProgressView() + .tint(.white) + } else { + Text("₩\(formatPrice(depositViewModel.balance))") + .font(.system(size: 26, weight: .bold)) + .foregroundColor(Color.white) + } + + // Text("₩\(cartViewModel.depositBalance?.formatted() ?? "0")") + // .font(.system(size: 22, weight: .bold)) } + } + + Spacer() + + HStack { Spacer() + Button { + depositViewModel.showChargeSheet = true // <-- $ 없이 할당 + } label: { + Text("충전") + .foregroundColor(Color.Primary) + .font(.system(size: 14, weight: .bold)) + .padding(.vertical, 6) + .padding(.horizontal, 14) + .background(Color.white) + .cornerRadius(20) + } + .padding(.trailing, 5) } - .frame(height: 35) + .padding(.bottom, 25) } - .padding() - .frame(maxWidth: .infinity) - .background(Color.white) - .cornerRadius(16) + .frame(height: 185) + .padding(20) } + .frame(maxWidth: .infinity) } + private var shippingDateSection: some View { VStack(alignment: .leading) { diff --git a/StockMate/StockMate/app/feature/orders/ui/OrderListView.swift b/StockMate/StockMate/app/feature/orders/ui/OrderListView.swift index 0dc04a5..a6e9aa5 100644 --- a/StockMate/StockMate/app/feature/orders/ui/OrderListView.swift +++ b/StockMate/StockMate/app/feature/orders/ui/OrderListView.swift @@ -155,43 +155,4 @@ struct OrderListCardView: View { .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/ReceiptView.swift b/StockMate/StockMate/app/feature/orders/ui/ReceiptView.swift index 59aec01..9e690f1 100644 --- a/StockMate/StockMate/app/feature/orders/ui/ReceiptView.swift +++ b/StockMate/StockMate/app/feature/orders/ui/ReceiptView.swift @@ -7,70 +7,106 @@ import SwiftUI import PDFKit +import UIKit enum PDFType { case a4 case receipt80mm } -// TODO: API 연결 후 주문 상세 페이지와 연결 + struct ReceiptView: View { + let orderId: Int + @StateObject private var detailViewModel = OrderDetailViewModel() - @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 } + @State var sellerName = "홍길동" + @State var businessNumber = "215-87-12345" // 형식만 맞춘 랜덤번호 + @State var phone = "02-567-8901" + @State var address = "서울특별시 금천구 가산동 459-9" + var body: some View { ScrollView { - VStack(alignment: .leading) { - receiptContent - - HStack { - pdfButton(type: .a4, title: "A4 PDF") - pdfButton(type: .receipt80mm, title: "영수증 PDF") + if detailViewModel.isLoading { + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if let order = detailViewModel.order { + VStack(alignment: .leading) { + receiptContent(order: order) + + Button { + generatePDF(type: .receipt80mm, order: order) + } label: { + Text("영수증 PDF 저장") + .frame(maxWidth: .infinity) + .padding() + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(8) + } + .padding(.horizontal) + .padding(.bottom) + } - .padding(.horizontal) - .padding(.bottom) + .background(Color.white) + .cornerRadius(12) + .padding() } - .background(Color.white) - .cornerRadius(12) - .padding() } .background(Color.Light) .navigationTitle("영수증") .navigationBarTitleDisplayMode(.inline) + .task { + await detailViewModel.fetchOrderDetail(orderId: orderId) + } } - - private var receiptContent: some View { - VStack(alignment: .leading, spacing: 16) { + + private func receiptContent(order: OrderResponseItem) -> some View { + let total = order.totalPrice + let vat = Int(Double(total) * 10 / 110) // 부가세액 + let supplyPrice = total - vat // 공급가액 + + return VStack(alignment: .leading, spacing: 16) { section("결제 정보") { Divider() - row("거래종류", paymentType) - row("승인번호", approvalNumber) - row("거래일시", date) + if order.paymentType == "DEPOSIT" { + row("거래종류", "예치금") + } else { + row("결제수단", "신용카드") + } + row("승인번호", formattedApprovalNumber(order.createdAt)) + row("거래일시", formattedDate(order.createdAt)) + } .padding(4) .padding(.top,5) section("구매정보") { Divider() - row("주문번호", orderNumber) - row("상품명", "\(itemName), \(quantity)개") - row("공급가액", "\(price)원") - row("부가세액", "\(vat)원") - row("합계금액", "\(total)원", highlight: true) + VStack(spacing: 8){ + row("주문번호", order.orderNumber) + VStack{ + // 상품명 라벨과 첫 번째 상품 같은 라인 + if let first = order.orderItems.first { + HStack { + Text("상품명") + Spacer() + Text("\(first.partDetail.korName) \(first.amount)개") + } + } + // 나머지는 label 없이 아래에 + ForEach(order.orderItems.dropFirst(), id: \.partId) { item in + HStack { + Spacer() // label 영역만큼 들여쓰기 효과 + Text("\(item.partDetail.korName) \(item.amount)개") + } + } + } + row("공급가액", "\(formatPrice(supplyPrice))원") + row("부가세액", "\(formatPrice(vat))원") + row("합계금액", "\(formatPrice(total))원", highlight: true) + } } .padding(4) .padding(.top) @@ -104,7 +140,7 @@ struct ReceiptView: View { .multilineTextAlignment(.leading) .frame(maxWidth: .infinity, alignment: .leading) .lineSpacing(4) - .padding(.leading, 2) // ✅ 총알 뒤 문장 들여쓰기 추가 + .padding(.leading, 2) // 문장 들여쓰기 추가 } .padding() @@ -129,54 +165,82 @@ struct ReceiptView: View { } } - 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) + private func generatePDF(type: PDFType, order: OrderResponseItem) { + let view = receiptContent(order: order) // ✅ 실제 View 생성 + let renderer = ImageRenderer(content: view) 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 + + // ✅ cgImage 기반 안전 처리 + if let cgImage = renderer.cgImage { + let uiImage = UIImage(cgImage: cgImage) + let pdfDoc = PDFDocument() + if let pdfPage = PDFPage(image: uiImage) { + pdfDoc.insert(pdfPage, at: 0) + } + + // ✅ 주문번호 기반 파일명 + let fileName = "receipt_\(order.orderNumber).pdf" + let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName) + + 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 { + av.popoverPresentationController?.sourceView = rootVC.view + rootVC.present(av, animated: true) + } + } + } + + } +} - 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() +// } +//} + +func formattedDate(_ timestamp: String) -> String { + let inputFormatter = DateFormatter() + inputFormatter.locale = Locale(identifier: "ko_KR") + inputFormatter.timeZone = TimeZone(abbreviation: "UTC") + inputFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS" + + guard let date = inputFormatter.date(from: timestamp) else { + return timestamp } + + let outputFormatter = DateFormatter() + outputFormatter.locale = Locale(identifier: "ko_KR") + outputFormatter.timeZone = TimeZone.current + outputFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss" + + return outputFormatter.string(from: date) } -struct ReceiptView_Previews: PreviewProvider { - static var previews: some View { - ReceiptView() +func formattedApprovalNumber(_ timestamp: String) -> String { + let inputFormatter = DateFormatter() + inputFormatter.locale = Locale(identifier: "ko_KR") + inputFormatter.timeZone = TimeZone(abbreviation: "UTC") + inputFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS" + + guard let date = inputFormatter.date(from: timestamp) else { + return timestamp } + + let outputFormatter = DateFormatter() + outputFormatter.locale = Locale(identifier: "ko_KR") + outputFormatter.timeZone = TimeZone.current + outputFormatter.dateFormat = "yyyyMMddHHmm" // ✅ 승인번호 포맷 + + return outputFormatter.string(from: date) } diff --git a/StockMate/StockMate/app/feature/payment/data/PaymentApi.swift b/StockMate/StockMate/app/feature/payment/data/PaymentApi.swift new file mode 100644 index 0000000..53346c3 --- /dev/null +++ b/StockMate/StockMate/app/feature/payment/data/PaymentApi.swift @@ -0,0 +1,40 @@ +// +// PaymentApi.swift +// StockMate +// +// Created by Admin on 10/29/25. +// + + +import Foundation +import Alamofire + + +struct PaymentAmount: Decodable { + let id: Int + let balance: Int + let userId: Int +} + + +enum PaymentApi { + // 예치금 조회 + static func getPaymentAmount() -> DataRequest { + let url = ApiClient.baseURL + "api/v1/payment/amount" + return ApiClient.shared.request(url, method: .get) + } + + // 예치금 충전 + static func chargeDeposit(amount: Int) -> DataRequest { + let url = ApiClient.baseURL + "api/v1/payment/charge" + let params: [String: Any] = [ + "amount": amount + ] + return ApiClient.shared.request( + url, + method: .post, + parameters: params, + encoding: URLEncoding.queryString + ) + } +} diff --git a/StockMate/StockMate/app/feature/payment/data/PaymentRepositoryImpl.swift b/StockMate/StockMate/app/feature/payment/data/PaymentRepositoryImpl.swift new file mode 100644 index 0000000..625b887 --- /dev/null +++ b/StockMate/StockMate/app/feature/payment/data/PaymentRepositoryImpl.swift @@ -0,0 +1,48 @@ +// +// PaymentRepositoryImpl.swift +// StockMate +// +// Created by Admin on 10/29/25. +// + + +import Foundation +import Alamofire + + +final class PaymentRepositoryImpl: PaymentRepositoryProtocol { + + func fetchDepositAmount() async -> AppResult { + let request = PaymentApi.getPaymentAmount() + 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 chargeDeposit(amount: Int) async -> AppResult { + let request = PaymentApi.chargeDeposit(amount: amount) + let result = await safeApi(request, decodeTo: ApiResponse.self) + + switch result { + case .success(let response): + return .success(response.data ?? response.message) + + case .failure(let error): + return .failure(error) + } + } +} diff --git a/StockMate/StockMate/app/feature/payment/domain/PaymentRepositoryProtocol.swift b/StockMate/StockMate/app/feature/payment/domain/PaymentRepositoryProtocol.swift new file mode 100644 index 0000000..150ab26 --- /dev/null +++ b/StockMate/StockMate/app/feature/payment/domain/PaymentRepositoryProtocol.swift @@ -0,0 +1,14 @@ +// +// PaymentRepositoryProtocol.swift +// StockMate +// +// Created by Admin on 10/29/25. +// + +import Foundation +import Alamofire + +protocol PaymentRepositoryProtocol { + func fetchDepositAmount() async -> AppResult + func chargeDeposit(amount: Int) async -> AppResult +} diff --git a/StockMate/StockMate/app/feature/payment/ui/DepositChargeView.swift b/StockMate/StockMate/app/feature/payment/ui/DepositChargeView.swift new file mode 100644 index 0000000..538412f --- /dev/null +++ b/StockMate/StockMate/app/feature/payment/ui/DepositChargeView.swift @@ -0,0 +1,117 @@ +import SwiftUI + +struct DepositChargeView: View { + @Environment(\.dismiss) private var dismiss + @ObservedObject var viewModel: DepositViewModel + @State private var amountText: String = "" + @State private var isCharging: Bool = false + + let keypad: [[String]] = [ + ["1","2","3"], + ["4","5","6"], + ["7","8","9"], + ["00","0","⌫"] + ] + + private var formattedNumberString: String { + if let value = Int(amountText) { + return value.formatted(.number) + } + return "0" + } + + private var formattedAmount: String { + return formattedNumberString + "원" + } + + func buttonAction(_ val: String) { + if val == "⌫" { + if !amountText.isEmpty { amountText.removeLast() } + } else { + amountText.append(val) + } + } + + var body: some View { + VStack(spacing: 20) { + + // Title + Text("예치금 충전") + .font(.system(size: 20, weight: .semibold)) + .padding(.top, 35) + + // 금액 + Text(formattedAmount) + .font(.system(size: 38, weight: .bold)) + .padding(.top, 35) + + // 아래 작은 라벨 +// Text("\(formattedNumberString)원") +// .font(.system(size: 15)) +// .foregroundColor(.textGray1) +// .padding(.horizontal, 10) +// .padding(.vertical, 4) +// .background(Color.white.opacity(0.9)) +// .cornerRadius(12) + + Spacer().frame(height: 80) + + // 키패드 + LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 0), count: 3), + spacing: 1) { + ForEach(keypad.flatMap { $0 }, id: \.self) { key in + Button { + buttonAction(key) + } label: { + Text(key) + .font(.system(size: 20)) + .frame(width: 50, height: 45) + .foregroundColor(Color.black) +// .background(Color.red.opacity(0.5)) + .padding(.horizontal, 4) + .background(Color.white) + } + } + } + .padding(.horizontal,16) + .padding(.top, 28) + + + // 충전 버튼 + Button { + guard !isCharging else { return } + Task { + guard let amount = Int(amountText), amount > 0 else { return } + isCharging = true + let success = await viewModel.chargeDeposit(amount: amount) + isCharging = false + if success { + viewModel.showChargeSheet = false + dismiss() + } + } + } label: { + if isCharging { + ProgressView() + .tint(.white) + .frame(height: 56) + .frame(maxWidth: .infinity) + } else { + Text("충전") + .font(.system(size: 18, weight: .bold)) + .foregroundColor(.white) + .frame(height: 59) + .frame(maxWidth: .infinity) + } + } + .background(Color.Primary) + .cornerRadius(28) + .padding(.top, 28) + .padding(.horizontal, 10) + .disabled(isCharging) + } + .padding(.horizontal, 20) +// .padding(.top, 20) + .background(Color.white) + } +} diff --git a/StockMate/StockMate/app/feature/payment/viewmodel/DepositViewModel.swift b/StockMate/StockMate/app/feature/payment/viewmodel/DepositViewModel.swift new file mode 100644 index 0000000..27d6009 --- /dev/null +++ b/StockMate/StockMate/app/feature/payment/viewmodel/DepositViewModel.swift @@ -0,0 +1,46 @@ +// +// PaymentViewModel.swift +// StockMate +// +// Created by Admin on 10/29/25. +// + +import Foundation + +@MainActor +final class DepositViewModel: ObservableObject { + private let repository: PaymentRepositoryProtocol = PaymentRepositoryImpl() + + @Published var balance: Int = 0 + @Published var isLoading: Bool = false + @Published var showChargeSheet: Bool = false + + /// ✅ 예치금 조회 + func fetchDepositAmount() async { + isLoading = true + + let result = await repository.fetchDepositAmount() + + isLoading = false + + switch result { + case .success(let data): + self.balance = data.balance + case .failure(let error): + print("❌ 예치금 조회 실패:", error.message) + } + } + + /// ✅ 예치금 충전 + func chargeDeposit(amount: Int) async -> Bool { + let result = await repository.chargeDeposit(amount: amount) + switch result { + case .success(_): + await fetchDepositAmount() + return true + case .failure(let error): + print("❌ 충전 실패:", error.message) + return false + } + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/chair.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/chair.imageset/Contents.json index ad1d744..052b5da 100644 --- a/StockMate/StockMate/resources/Assets.xcassets/chair.imageset/Contents.json +++ b/StockMate/StockMate/resources/Assets.xcassets/chair.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "filename" : "chair.svg", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { diff --git a/StockMate/StockMate/resources/Assets.xcassets/check.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/check.imageset/Contents.json new file mode 100644 index 0000000..b09981a --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/check.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "check.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/check.imageset/check.svg b/StockMate/StockMate/resources/Assets.xcassets/check.imageset/check.svg new file mode 100644 index 0000000..8bdc85f --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/check.imageset/check.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/StockMate/StockMate/resources/Assets.xcassets/cog.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/cog.imageset/Contents.json index 7124035..bd8698d 100644 --- a/StockMate/StockMate/resources/Assets.xcassets/cog.imageset/Contents.json +++ b/StockMate/StockMate/resources/Assets.xcassets/cog.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "filename" : "cog.svg", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { diff --git a/StockMate/StockMate/resources/Assets.xcassets/deposit_background.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/deposit_background.imageset/Contents.json new file mode 100644 index 0000000..7c4374c --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/deposit_background.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "deposit_background@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "deposit_background@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "deposit_background@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/deposit_background.imageset/deposit_background@1x.png b/StockMate/StockMate/resources/Assets.xcassets/deposit_background.imageset/deposit_background@1x.png new file mode 100644 index 0000000..b988925 Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/deposit_background.imageset/deposit_background@1x.png differ diff --git a/StockMate/StockMate/resources/Assets.xcassets/deposit_background.imageset/deposit_background@2x.png b/StockMate/StockMate/resources/Assets.xcassets/deposit_background.imageset/deposit_background@2x.png new file mode 100644 index 0000000..e94ecc4 Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/deposit_background.imageset/deposit_background@2x.png differ diff --git a/StockMate/StockMate/resources/Assets.xcassets/deposit_background.imageset/deposit_background@3x.png b/StockMate/StockMate/resources/Assets.xcassets/deposit_background.imageset/deposit_background@3x.png new file mode 100644 index 0000000..84218d3 Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/deposit_background.imageset/deposit_background@3x.png differ diff --git a/StockMate/StockMate/resources/Assets.xcassets/dottedline.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/dottedline.imageset/Contents.json new file mode 100644 index 0000000..3551fde --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/dottedline.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "dottedline.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/dottedline.imageset/dottedline.svg b/StockMate/StockMate/resources/Assets.xcassets/dottedline.imageset/dottedline.svg new file mode 100644 index 0000000..5f4b4b2 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/dottedline.imageset/dottedline.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/StockMate/StockMate/resources/Assets.xcassets/hourglass.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/hourglass.imageset/Contents.json new file mode 100644 index 0000000..900b128 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/hourglass.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "hourglass.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/hourglass.imageset/hourglass.svg b/StockMate/StockMate/resources/Assets.xcassets/hourglass.imageset/hourglass.svg new file mode 100644 index 0000000..f229793 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/hourglass.imageset/hourglass.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/StockMate/StockMate/resources/Assets.xcassets/lightbulb.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/lightbulb.imageset/Contents.json index beab311..e82017e 100644 --- a/StockMate/StockMate/resources/Assets.xcassets/lightbulb.imageset/Contents.json +++ b/StockMate/StockMate/resources/Assets.xcassets/lightbulb.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "filename" : "lightbulb.svg", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { diff --git a/StockMate/StockMate/resources/Assets.xcassets/location.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/location.imageset/Contents.json index 306ca5c..be50dad 100644 --- a/StockMate/StockMate/resources/Assets.xcassets/location.imageset/Contents.json +++ b/StockMate/StockMate/resources/Assets.xcassets/location.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "filename" : "location.svg", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { diff --git a/StockMate/StockMate/resources/Assets.xcassets/notification.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/notification.imageset/Contents.json index acfc443..5dabea1 100644 --- a/StockMate/StockMate/resources/Assets.xcassets/notification.imageset/Contents.json +++ b/StockMate/StockMate/resources/Assets.xcassets/notification.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "filename" : "notification.svg", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { diff --git a/StockMate/StockMate/resources/Assets.xcassets/package.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/package.imageset/Contents.json index e7f4c71..f289ed5 100644 --- a/StockMate/StockMate/resources/Assets.xcassets/package.imageset/Contents.json +++ b/StockMate/StockMate/resources/Assets.xcassets/package.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "filename" : "package.svg", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { diff --git a/StockMate/StockMate/resources/Assets.xcassets/pindrop.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/pindrop.imageset/Contents.json new file mode 100644 index 0000000..01ce80a --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/pindrop.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "pindrop.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/pindrop.imageset/pindrop.svg b/StockMate/StockMate/resources/Assets.xcassets/pindrop.imageset/pindrop.svg new file mode 100644 index 0000000..f9cfacb --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/pindrop.imageset/pindrop.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/StockMate/StockMate/resources/Assets.xcassets/rocket.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/rocket.imageset/Contents.json new file mode 100644 index 0000000..a61a5d8 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/rocket.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "rocket.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/rocket.imageset/rocket.svg b/StockMate/StockMate/resources/Assets.xcassets/rocket.imageset/rocket.svg new file mode 100644 index 0000000..b63f40e --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/rocket.imageset/rocket.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/StockMate/StockMate/resources/Assets.xcassets/spanner.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/spanner.imageset/Contents.json index 36cb254..7fcd63e 100644 --- a/StockMate/StockMate/resources/Assets.xcassets/spanner.imageset/Contents.json +++ b/StockMate/StockMate/resources/Assets.xcassets/spanner.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "filename" : "spanner.svg", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { diff --git a/StockMate/StockMate/resources/Assets.xcassets/tabHome.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/tabHome.imageset/Contents.json index e847277..a1b0717 100644 --- a/StockMate/StockMate/resources/Assets.xcassets/tabHome.imageset/Contents.json +++ b/StockMate/StockMate/resources/Assets.xcassets/tabHome.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "filename" : "tabHome.svg", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { diff --git a/StockMate/StockMate/resources/Assets.xcassets/tabInventory.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/tabInventory.imageset/Contents.json index 3421138..53286b8 100644 --- a/StockMate/StockMate/resources/Assets.xcassets/tabInventory.imageset/Contents.json +++ b/StockMate/StockMate/resources/Assets.xcassets/tabInventory.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "filename" : "tabInventory.svg", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { diff --git a/StockMate/StockMate/resources/Assets.xcassets/tabPackage.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/tabPackage.imageset/Contents.json index c6df33b..d90fcd3 100644 --- a/StockMate/StockMate/resources/Assets.xcassets/tabPackage.imageset/Contents.json +++ b/StockMate/StockMate/resources/Assets.xcassets/tabPackage.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "filename" : "tabPackage.svg", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { diff --git a/StockMate/StockMate/resources/Assets.xcassets/tabProfile.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/tabProfile.imageset/Contents.json index 9bea2a3..25e0b53 100644 --- a/StockMate/StockMate/resources/Assets.xcassets/tabProfile.imageset/Contents.json +++ b/StockMate/StockMate/resources/Assets.xcassets/tabProfile.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "filename" : "tabProfile.svg", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { diff --git a/StockMate/StockMate/resources/Assets.xcassets/uploadprogress.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/uploadprogress.imageset/Contents.json new file mode 100644 index 0000000..a1bff87 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/uploadprogress.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "uploadprogress.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/uploadprogress.imageset/uploadprogress.svg b/StockMate/StockMate/resources/Assets.xcassets/uploadprogress.imageset/uploadprogress.svg new file mode 100644 index 0000000..3e65143 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/uploadprogress.imageset/uploadprogress.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/StockMate/StockMate/resources/Color.swift b/StockMate/StockMate/resources/Color.swift index f927e60..94172ee 100644 --- a/StockMate/StockMate/resources/Color.swift +++ b/StockMate/StockMate/resources/Color.swift @@ -95,6 +95,8 @@ extension Color { static let Hstatus5Bg = Color(hex: "#ECEEF0") + static let Grayline = Color(hex: "#ECECED") + static let Grayline2 = Color(hex: "#DDDDDD") // 투명도 포함 예시 static let boxBgWhite = Color(hex: "#40FFFFFF") // 투명도 포함