diff --git a/StockMate/.DS_Store b/StockMate/.DS_Store index 9b2b289..96d21b8 100644 Binary files a/StockMate/.DS_Store and b/StockMate/.DS_Store differ diff --git a/StockMate/StockMate/ContentView.swift b/StockMate/StockMate/ContentView.swift index 4d36f82..5787aab 100644 --- a/StockMate/StockMate/ContentView.swift +++ b/StockMate/StockMate/ContentView.swift @@ -7,67 +7,30 @@ import SwiftUI -//struct ContentView: View { -// @State private var showingScanner = false -// @State private var scannedCode: String? = nil -// -// var body: some View { -// NavigationView { -// VStack(spacing: 20) { -// if let code = scannedCode { -// Text("스캔 결과:") -// .font(.headline) -// Text(code) -// .font(.body) -// .multilineTextAlignment(.center) -// .padding() -// .background(Color(.systemGray6)) -// .cornerRadius(8) -// } else { -// Text("아직 스캔된 코드가 없습니다.") -// .foregroundColor(.secondary) -// } -// -// Button("QR 스캔 시작") { -// // 카메라 권한 체크는 시스템이 자동으로 권한 알림을 띄우므로 -// // 필요하면 권한 상태 확인 로직 추가 가능 -// showingScanner = true -// } -// .buttonStyle(.borderedProminent) -// .padding(.top) -// -// Spacer() -// } -// .padding() -// .navigationTitle("QR 스캐너 예제") -// .sheet(isPresented: $showingScanner) { -// QRScannerView(isPresented: $showingScanner, scannedCode: $scannedCode) -// .edgesIgnoringSafeArea(.all) -// } -// } -// } -//} struct ContentView: View { + @State private var address: String = "주소를 선택하세요" + @State private var showWebView = false + var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("임시 화면") - } - .padding() - HStack(spacing: 20) { - Image(systemName: "gearshape") - .font(.system(size: 40)) - .foregroundColor(.blue) + VStack(spacing: 20) { + Text(address) + .font(.title3) + .multilineTextAlignment(.center) + .padding() - Image(systemName: "lightbulb") - .font(.system(size: 40)) - .foregroundColor(.cyan) + Button("주소 검색") { + showWebView.toggle() + } + .font(.headline) + .buttonStyle(.borderedProminent) + } + .sheet(isPresented: $showWebView) { + KakaoZipCodeView(address: $address) } } } + #Preview { ContentView() } diff --git a/StockMate/StockMate/app/core/KakaoPostcode/KakaoZipCodeVC.swift b/StockMate/StockMate/app/core/KakaoPostcode/KakaoZipCodeVC.swift new file mode 100644 index 0000000..a4fecb1 --- /dev/null +++ b/StockMate/StockMate/app/core/KakaoPostcode/KakaoZipCodeVC.swift @@ -0,0 +1,80 @@ +// +// KakaoZipCodeVC.swift +// StockMate +// +// Created by Admin on 11/5/25. +// + + +import UIKit +import WebKit + +class KakaoZipCodeVC: UIViewController { + + // MARK: - Properties + var webView: WKWebView? + let indicator = UIActivityIndicatorView(style: .medium) + var onAddressSelected: ((String) -> Void)? // ✅ SwiftUI로 결과 전달용 콜백 + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + setupWebView() + setupLayout() + } + + private func setupWebView() { + let contentController = WKUserContentController() + contentController.add(self, name: "callBackHandler") + + let config = WKWebViewConfiguration() + config.userContentController = contentController + + webView = WKWebView(frame: .zero, configuration: config) + webView?.navigationDelegate = self + + guard let webView = webView, + let url = URL(string: "https://yoo-hyuna.github.io/Kakao-Postcode/") else { return } + + webView.load(URLRequest(url: url)) + } + + private func setupLayout() { + guard let webView = webView else { return } + view.addSubview(webView) + webView.translatesAutoresizingMaskIntoConstraints = false + + webView.addSubview(indicator) + indicator.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + webView.topAnchor.constraint(equalTo: view.topAnchor), + webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + webView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + webView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + indicator.centerXAnchor.constraint(equalTo: webView.centerXAnchor), + indicator.centerYAnchor.constraint(equalTo: webView.centerYAnchor) + ]) + } +} + +extension KakaoZipCodeVC: WKScriptMessageHandler { + func userContentController(_ userContentController: WKUserContentController, + didReceive message: WKScriptMessage) { + guard let data = message.body as? [String: Any] else { return } + let address = data["roadAddress"] as? String ?? "" + onAddressSelected?(address) // ✅ SwiftUI로 전달 + dismiss(animated: true) + } +} + +extension KakaoZipCodeVC: WKNavigationDelegate { + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + indicator.startAnimating() + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + indicator.stopAnimating() + } +} diff --git a/StockMate/StockMate/app/core/KakaoPostcode/KakaoZipCodeView.swift b/StockMate/StockMate/app/core/KakaoPostcode/KakaoZipCodeView.swift new file mode 100644 index 0000000..fa69a87 --- /dev/null +++ b/StockMate/StockMate/app/core/KakaoPostcode/KakaoZipCodeView.swift @@ -0,0 +1,24 @@ +// +// KakaoZipCodeView.swift +// StockMate +// +// Created by Admin on 11/5/25. +// + + +import SwiftUI +import WebKit + +struct KakaoZipCodeView: UIViewControllerRepresentable { + @Binding var address: String + + func makeUIViewController(context: Context) -> KakaoZipCodeVC { + let vc = KakaoZipCodeVC() + vc.onAddressSelected = { selectedAddress in + address = selectedAddress + } + return vc + } + + func updateUIViewController(_ uiViewController: KakaoZipCodeVC, context: Context) {} +} diff --git a/StockMate/StockMate/app/core/KakaoPostcode/ViewController.swift b/StockMate/StockMate/app/core/KakaoPostcode/ViewController.swift new file mode 100644 index 0000000..2c793d0 --- /dev/null +++ b/StockMate/StockMate/app/core/KakaoPostcode/ViewController.swift @@ -0,0 +1,53 @@ +// +// ViewController.swift +// StockMate +// +// Created by Admin on 11/5/25. +// + + +import UIKit + +class ViewController: UIViewController { + + // MARK: - UI Components + let button = UIButton(type: .system) + let label = UILabel() + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + configureUI() + } + + private func configureUI() { + [label, button].forEach { + view.addSubview($0) + $0.translatesAutoresizingMaskIntoConstraints = false + } + + label.text = "주소를 선택하세요" + label.font = UIFont.systemFont(ofSize: 18) + label.textAlignment = .center + + button.setTitle("주소 검색", for: .normal) + button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18) + button.addTarget(self, action: #selector(handleButton(_:)), for: .touchUpInside) + + NSLayoutConstraint.activate([ + label.centerXAnchor.constraint(equalTo: view.centerXAnchor), + label.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -50), + label.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8), + + button.centerXAnchor.constraint(equalTo: view.centerXAnchor), + button.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 30) + ]) + } + + @objc + private func handleButton(_ sender: UIButton) { + let vc = KakaoZipCodeVC() + vc.modalPresentationStyle = .fullScreen + present(vc, animated: true) + } +} diff --git a/StockMate/StockMate/app/core/components/AlertModal.swift b/StockMate/StockMate/app/core/components/AlertModal.swift new file mode 100644 index 0000000..72d1e1b --- /dev/null +++ b/StockMate/StockMate/app/core/components/AlertModal.swift @@ -0,0 +1,140 @@ +// +// AlertModal.swift +// StockMate +// +// Created by Admin on 11/4/25. +// + + +import SwiftUI + +struct AlertModal: View { + var icon: Image? = nil // ✅ 아이콘 없을 수도 있음 + var title: String + var message: String? = nil + + var primaryButtonTitle: String + var primaryAction: () -> Void + + var secondaryButtonTitle: String? = nil + var secondaryAction: (() -> Void)? = nil + + var buttonLayout: ButtonLayout = .vertical // ✅ horizontal / vertical + + enum ButtonLayout { + case vertical + case horizontal + } + + var body: some View { + VStack(spacing: 15) { + if let icon = icon { + icon + .resizable() + .scaledToFit() + .frame(width: 120, height: 120) + .padding(.top, 6) + } + + Text(title) + .font(.system(size: 18, weight: .bold)) + .multilineTextAlignment(.center) + .padding(.top, 10) + + if let message = message { + Text(message) + .font(.system(size: 14, weight: .regular)) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.bottom, 6) + } + + if buttonLayout == .vertical { + VStack(spacing: 10) { + if let secondary = secondaryButtonTitle, let secondaryAction = secondaryAction { + Button(secondary, action: secondaryAction) + .buttonStyle(CustomButtonStyle(type: .outlined(.Primary))) + } + Button(primaryButtonTitle, action: primaryAction) + .buttonStyle(CustomButtonStyle(type: .filled(Color.Primary))) + } + } else { + HStack(spacing: 10) { + if let secondary = secondaryButtonTitle, let secondaryAction = secondaryAction { + Button(secondary, action: secondaryAction) + .buttonStyle(CustomButtonStyle(type: .outlined(.Primary))) + } + Button(primaryButtonTitle, action: primaryAction) + .buttonStyle(CustomButtonStyle(type: .filled(Color.Primary))) + } + } + } + .padding(20) + .frame(maxWidth: 300) + .background(Color.white) + .cornerRadius(32) + .shadow(radius: 8) + } +} + +import SwiftUI + +#Preview { + ScrollView{ + VStack(spacing: 40) { + // ✅ 1. 체크 아이콘 + 버튼 1개 + AlertModal( + icon: Image("SuccessIllust"), + title: "등록 완료!", + message: "입고 부품 등록이 완료되었습니다.", + primaryButtonTitle: "확인", + primaryAction: {} + ) + AlertModal( + icon: Image("SuccessIllust"), + title: "출고 완료!", + message: "사용 처리가 완료되었습니다.", + primaryButtonTitle: "확인", + primaryAction: {} + ) + + + + // ✅ 2. 아이콘 없이 버튼 2개 (가로) + AlertModal( + title: "주문 취소", + message: "주문을 취소하시겠습니까?", + primaryButtonTitle: "예", + primaryAction: {}, + secondaryButtonTitle: "아니오", + secondaryAction: {}, + buttonLayout: .horizontal + ) + AlertModal( + title: "로그아웃", + message: "로그아웃 하시겠습니까?", + primaryButtonTitle: "로그아웃", + primaryAction: {}, + secondaryButtonTitle: "취소", + secondaryAction: {}, + buttonLayout: .horizontal + ) + + + // ✅ 3. 주문완료 + AlertModal( + icon: Image("SuccessIllust"), + title: "주문완료!", + message: "해당 부품 주문이 완료되었습니다.", + primaryButtonTitle: "주문상세", + primaryAction: {}, + secondaryButtonTitle: "홈으로", + secondaryAction: {}, + buttonLayout: .vertical + ) + } + .padding() + .background(Color.gray.opacity(0.1)) + } + +} diff --git a/StockMate/StockMate/app/core/components/BarChartView.swift b/StockMate/StockMate/app/core/components/BarChartView.swift new file mode 100644 index 0000000..5fa4e35 --- /dev/null +++ b/StockMate/StockMate/app/core/components/BarChartView.swift @@ -0,0 +1,91 @@ +// +// BarChartView.swift +// StockMate +// +// Created by Admin on 11/2/25. +// + +import SwiftUI + +struct BarChartView: View { + let values: [CGFloat] // 각 월별 비율값 (0~1) + let labels: [String] // 예: ["10", "09", "08", "07", "06"] + let amounts: [Int] // 예: [230000, 250000, 310000, 280000, 400000] + @Binding var selectedMonth: String? + + var body: some View { + // ✅ 최신월이 오른쪽에 오도록 역순 정렬 + let reversedValues = Array(values.reversed()) + let reversedLabels = Array(labels.reversed()) + let reversedAmounts = Array(amounts.reversed()) + + // ✅ "07" → "7월" 형식 변환 + let displayLabels = reversedLabels.map { label in + if let monthInt = Int(label) { + return "\(monthInt)월" + } else { + return label + } + } + + // ✅ 기본 선택: 최신월 + let defaultMonth = displayLabels.last ?? "" + let activeMonth = selectedMonth ?? defaultMonth + + VStack(alignment: .leading, spacing: 14) { + // ✅ 막대 그래프 + GeometryReader { geometry in + let chartHeight = geometry.size.height * 0.85 // 상하 여백 고려 + let totalWidth = geometry.size.width + let barCount = CGFloat(reversedValues.count) + let barWidth: CGFloat = 28 + let spacing = max((totalWidth - (barWidth * barCount)) / (barCount + 1), 6) + + HStack(alignment: .bottom, spacing: spacing) { + ForEach(reversedValues.indices, id: \.self) { i in + VStack { + RoundedRectangle(cornerRadius: 8) + .fill(activeMonth == displayLabels[i] ? Color.Primary : Color.LightBlue04) + // ✅ 막대 높이를 geometry 기준으로 조정 + .frame(width: barWidth, height: chartHeight * reversedValues[i]) + .onTapGesture { + selectedMonth = (selectedMonth == displayLabels[i]) ? nil : displayLabels[i] + } + + Text(displayLabels[i]) + .font(.caption2) + .foregroundColor(.black) + .padding(.top, 4) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) + } + .frame(height: 140) // ← 전체 그래프 영역 높이 확장 + .padding(.vertical, 8) + + Divider() + + // ✅ 하단 "n월 지출금액 ooo원" 표시 + if let index = displayLabels.firstIndex(of: activeMonth) { + HStack { + Text("\(displayLabels[index]) 지출 현황") + .font(.system(size: 17, weight: .medium)) + Spacer() + Text("\(reversedAmounts[index].formatted())원") + .font(.system(size: 18, weight: .bold)) + .foregroundColor(Color.Primary) + } + .padding(.top, 6) + .padding(.horizontal,4) + } + } + .frame(maxWidth: .infinity) + // ✅ 초기 로드 시 최신월 자동 선택 + .onAppear { + if selectedMonth == nil { + selectedMonth = defaultMonth + } + } + } +} diff --git a/StockMate/StockMate/app/core/components/CartCard.swift b/StockMate/StockMate/app/core/components/CartCard.swift index 1d959d0..6211f93 100644 --- a/StockMate/StockMate/app/core/components/CartCard.swift +++ b/StockMate/StockMate/app/core/components/CartCard.swift @@ -47,13 +47,6 @@ struct CartCard: View { .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() @@ -62,61 +55,77 @@ struct CartCard: View { if quantity == 0 { if let onAddToCart = onAddToCart { Button(action: onAddToCart) { - Image(systemName: "cart.badge.plus") - .font(.system(size: 18)) - .foregroundColor(.Primary) + Image("add_shopping_cart") + .resizable() + .scaledToFit() + .frame(width: 18, height: 18) .padding(10) - .background(Color.Primary.opacity(0.1)) + .background(Color.white) .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 4) } } } else if quantity == 1 { HStack(spacing: 10) { Button(action: onRemoveFromCart) { Image(systemName: "trash") - .font(.system(size: 14, weight: .bold)) - .foregroundColor(.red) + .font(.system(size: 14, weight: .regular)) + .frame(width: 13,height: 13) + .foregroundColor(.black) } Text("1") - .font(.system(size: 15, weight: .semibold)) - .frame(width: 24) + .font(.system(size: 15, weight: .medium)) + .frame(width: 20) Button(action: onIncrease) { Image(systemName: "plus") - .font(.system(size: 14, weight: .bold)) - .foregroundColor(.Primary) + .font(.system(size: 14, weight: .regular)) + .frame(width: 13,height: 13) + .foregroundColor(.black) } } .padding(.vertical, 6) .padding(.horizontal, 10) .background(Color.white) .cornerRadius(10) - .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) + .overlay( // ✅ 테두리 추가 + RoundedRectangle(cornerRadius: 10) + .stroke( Color.LightBlue03, lineWidth: 2) + ) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .shadow(color: Color.black.opacity(0.25), radius: 4, x: 0, y: 4) } else { HStack(spacing: 10) { Button(action: onDecrease) { Image(systemName: "minus") - .font(.system(size: 14, weight: .bold)) - .foregroundColor(.gray) + .font(.system(size: 14, weight: .regular)) + .frame(width: 13,height: 13) + .foregroundColor(.black) } Text("\(quantity)") - .font(.system(size: 15, weight: .semibold)) - .frame(width: 24) + .font(.system(size: 15, weight: .medium)) + .frame(width: 20) Button(action: onIncrease) { Image(systemName: "plus") - .font(.system(size: 14, weight: .bold)) - .foregroundColor(.Primary) + .font(.system(size: 14, weight: .regular)) + .frame(width: 13,height: 13) + .foregroundColor(.black) } } .padding(.vertical, 6) .padding(.horizontal, 10) .background(Color.white) .cornerRadius(10) - .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) + .overlay( // ✅ 테두리 추가 + RoundedRectangle(cornerRadius: 10) + .stroke( Color.LightBlue03, lineWidth: 2) + ) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .shadow(color: Color.black.opacity(0.25), radius: 4, x: 0, y: 4) } } } diff --git a/StockMate/StockMate/app/core/components/CustomButtonStyle.swift b/StockMate/StockMate/app/core/components/CustomButtonStyle.swift new file mode 100644 index 0000000..c25f344 --- /dev/null +++ b/StockMate/StockMate/app/core/components/CustomButtonStyle.swift @@ -0,0 +1,50 @@ +// +// CustomButtonStyle.swift +// StockMate +// +// Created by Admin on 11/4/25. +// + + +import SwiftUI + +struct CustomButtonStyle: ButtonStyle { + enum StyleType { + case filled(Color) + case outlined(Color) + } + + var type: StyleType + var height: CGFloat = 52 + var cornerRadius: CGFloat = 9999 + var fontSize: CGFloat = 16 + var fontWeight: Font.Weight = .semibold + + func makeBody(configuration: Configuration) -> some View { + switch type { + case .filled(let color): + configuration.label + .frame(maxWidth: .infinity, minHeight: height) + .background(color.opacity(configuration.isPressed ? 0.8 : 1)) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .foregroundColor(.white) + .font(.system(size: fontSize, weight: fontWeight)) + .animation(.easeOut(duration: 0.15), value: configuration.isPressed) + + case .outlined(let color): + configuration.label + .frame(maxWidth: .infinity, minHeight: height) + .overlay( + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(color, lineWidth: 1) + ) + .foregroundColor(color) + .font(.system(size: fontSize, weight: fontWeight)) + .background( + RoundedRectangle(cornerRadius: cornerRadius) + .fill(Color.white) + ) + .animation(.easeOut(duration: 0.15), value: configuration.isPressed) + } + } +} diff --git a/StockMate/StockMate/app/core/components/CustomTextField.swift b/StockMate/StockMate/app/core/components/CustomTextField.swift index 62eedc3..074d2c2 100644 --- a/StockMate/StockMate/app/core/components/CustomTextField.swift +++ b/StockMate/StockMate/app/core/components/CustomTextField.swift @@ -13,6 +13,7 @@ struct CustomTextField: View { @Binding var text: String var isEmail: Bool = false var errorMessage: String? = nil + var isReadOnly: Bool = false // ✅ 추가 @FocusState private var isFocused: Bool @@ -44,13 +45,27 @@ struct CustomTextField: View { y: 2 ) + if isReadOnly { + // ✅ 가로 스크롤 가능한 읽기 전용 텍스트 + ScrollView(.horizontal, showsIndicators: false) { + Text(text.isEmpty ? placeholder : text) + .font(.system(size: 15)) + .foregroundColor(text.isEmpty ? .gray : .black) + .padding(.horizontal, 14) + .padding(.vertical, 12) + .lineLimit(1) + } + .frame(height: 46) + .contentShape(Rectangle()) + + } else { TextField(placeholder, text: $text) .focused($isFocused) .padding(.horizontal, 14) .padding(.vertical, 12) .textInputAutocapitalization(.never) .autocorrectionDisabled() - + } } .frame(height: 46) // 높이 일정하게 고정 diff --git a/StockMate/StockMate/app/core/components/DonutChartView.swift b/StockMate/StockMate/app/core/components/DonutChartView.swift new file mode 100644 index 0000000..2dd37c6 --- /dev/null +++ b/StockMate/StockMate/app/core/components/DonutChartView.swift @@ -0,0 +1,109 @@ +// +// DonutChartView.swift +// StockMate +// +// Created by Admin on 11/3/25. +// + +import SwiftUI +import Charts + +struct DonutChartView: View { + let data: [CategorySpending] + + var total: Double { + Double(data.map { $0.totalAmount }.reduce(0, +)) + } + + // ✅ 각 항목별 비율 계산 + var percentages: [Double] { + data.map { total == 0 ? 0 : (Double($0.totalAmount) / total * 100) } + } + + var colors: [Color] = [ + Color.Hstatus1, + Color.Hstatus2, + Color.Hstatus3, + Color.Hstatus4, + Color.Hstatus5 + ] + + var gradients: [AngularGradient] = [ + AngularGradient(gradient: Gradient(colors: [.pink, .orange]), center: .center), + AngularGradient(gradient: Gradient(colors: [.blue, .teal]), center: .center), + AngularGradient(gradient: Gradient(colors: [.green, .mint]), center: .center), + AngularGradient(gradient: Gradient(colors: [.purple, .indigo]), center: .center), + AngularGradient(gradient: Gradient(colors: [.gray, .black]), center: .center) + ] + + + var body: some View { + HStack(alignment: .center, spacing: 24) { + // ✅ 도넛 차트 + if total == 0 { + Text("데이터 없음") + .foregroundColor(.gray) + } else { + Chart { + ForEach(Array(data.enumerated()), id: \.offset) { index, item in + SectorMark( + angle: .value("지출", item.totalAmount), + innerRadius: .ratio(0.49), + angularInset: 1.9 + ) + .foregroundStyle( + LinearGradient( + gradient: Gradient(colors: [ + colors[index % colors.count], + colors[index % colors.count].opacity(0.5) + ]), + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) +// .foregroundStyle(colors[index % colors.count]) + .cornerRadius(8.0) + // ✅ 도넛 안쪽에 비율 표시 + .annotation(position: .overlay) { + let percentage = percentages[index] + Text("\(percentage, specifier: "%.1f")%") + .font(.system(size: 10, weight: .light)) + .foregroundColor(.black) + .offset(y: -2) + } + } + } + .frame(height: 150) + .chartLegend(.hidden) // 기본 범례 숨김 + } + + // ✅ 오른쪽 커스텀 범례 + VStack(alignment: .leading, spacing: 18) { + ForEach(Array(data.enumerated()), id: \.offset) { index, item in + let percentage = percentages[index] + HStack(spacing: 7) { + Circle() + .fill(colors[index % colors.count]) + .frame(width: 10, height: 10) + Text(item.categoryName) + .font(.system(size: 12, weight: .light)) + .frame(width: 70, alignment: .leading) + Text("\(percentage, specifier: "%.1f")%") + .font(.system(size: 12)) + .foregroundColor(.black) + } + } + } + } + } +} + +#Preview { + DonutChartView(data: [ + CategorySpending(categoryName: "전기/램프", totalAmount: 450000), + CategorySpending(categoryName: "엔진/미션", totalAmount: 300000), + CategorySpending(categoryName: "하체/바디", totalAmount: 150000), + CategorySpending(categoryName: "내장/외장", totalAmount: 100000), + CategorySpending(categoryName: "기타소모품", totalAmount: 50000) + ]) +} diff --git a/StockMate/StockMate/app/core/components/InventoryListSection.swift b/StockMate/StockMate/app/core/components/InventoryListSection.swift deleted file mode 100644 index c8d6c19..0000000 --- a/StockMate/StockMate/app/core/components/InventoryListSection.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// InventoryListSection.swift -// StockMate -// -// Created by Admin on 10/16/25. -// - -import SwiftUI - -struct InventoryListSection: View { - var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) - } -} - -#Preview { - InventoryListSection() -} diff --git a/StockMate/StockMate/app/core/components/OrderRequestCardView.swift b/StockMate/StockMate/app/core/components/OrderRequestCardView.swift index aa29bef..f2fa6ab 100644 --- a/StockMate/StockMate/app/core/components/OrderRequestCardView.swift +++ b/StockMate/StockMate/app/core/components/OrderRequestCardView.swift @@ -24,6 +24,7 @@ struct OrderRequestCardView: View { 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: { @@ -32,6 +33,7 @@ struct OrderRequestCardView: View { .frame(width: 64, height: 64) .cornerRadius(10) + // 이름 및 정보 VStack(alignment: .leading, spacing: 6) { Text(item.korName) .font(.system(size: 14, weight: .bold)) @@ -49,64 +51,84 @@ struct OrderRequestCardView: View { Spacer() + // 수량 컨트롤러 // 🪄 수량에 따른 3단계 분기 if quantity == 0 { Button(action: onAddToCart) { - Image(systemName: "cart.badge.plus") - .font(.system(size: 18)) - .foregroundColor(.Primary) + Image("add_shopping_cart") + .resizable() + .scaledToFit() + .frame(width: 18, height: 18) .padding(10) - .background(Color.Primary.opacity(0.1)) + .background(Color.white) .clipShape(Circle()) + .shadow(color: Color.black.opacity(0.25), radius: 4, x: 0, y: 4) + } } else if quantity == 1 { HStack(spacing: 10) { Button(action: onRemoveFromCart) { Image(systemName: "trash") - .font(.system(size: 14, weight: .bold)) - .foregroundColor(.red) + .font(.system(size: 14, weight: .regular)) + .frame(width: 13,height: 13) + .foregroundColor(.black) } Text("1") - .font(.system(size: 15, weight: .semibold)) - .frame(width: 24) + .font(.system(size: 15, weight: .medium)) + .frame(width: 20) Button(action: onIncrease) { Image(systemName: "plus") - .font(.system(size: 14, weight: .bold)) - .foregroundColor(.Primary) + .font(.system(size: 14, weight: .regular)) + .frame(width: 13,height: 13) + .foregroundColor(.black) } } .padding(.vertical, 6) .padding(.horizontal, 10) .background(Color.white) .cornerRadius(10) - .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) + .overlay( // ✅ 테두리 추가 + RoundedRectangle(cornerRadius: 10) + .stroke( Color.LightBlue03, lineWidth: 2) + ) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .shadow(color: Color.black.opacity(0.25), radius: 4, x: 0, y: 4) + } else { HStack(spacing: 10) { Button(action: onDecrease) { Image(systemName: "minus") - .font(.system(size: 14, weight: .bold)) - .foregroundColor(.gray) + .font(.system(size: 14, weight: .regular)) + .frame(width: 13,height: 13) + .foregroundColor(.black) } Text("\(quantity)") - .font(.system(size: 15, weight: .semibold)) - .frame(width: 24) + .font(.system(size: 15, weight: .medium)) + .frame(width: 20) Button(action: onIncrease) { Image(systemName: "plus") - .font(.system(size: 14, weight: .bold)) - .foregroundColor(.Primary) + .font(.system(size: 14, weight: .regular)) + .frame(width: 13,height: 13) + .foregroundColor(.black) } } .padding(.vertical, 6) .padding(.horizontal, 10) .background(Color.white) .cornerRadius(10) - .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) + .overlay( // ✅ 테두리 추가 + RoundedRectangle(cornerRadius: 10) + .stroke( Color.LightBlue03, lineWidth: 2) + ) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .shadow(color: Color.black.opacity(0.25), radius: 4, x: 0, y: 4) + } } } @@ -116,3 +138,56 @@ struct OrderRequestCardView: View { .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) } } + +#Preview { + let sampleItem = InventoryItem( + id: 1, + name: "Engine Oil Filter", + price: 18000, + image: "https://picsum.photos/200", + trim: "1.6 Turbo", + model: "SM-230", + category: 3, + korName: "엔진 오일 필터", + engName: "Engine Oil Filter", + categoryName: "엔진 부품", + stock: 42, + amount: 3, + limitAmount: 5, + isLack: true + ) + + VStack(spacing: 20) { + // 수량 0 (아직 카트에 안 담김) + OrderRequestCardView( + item: sampleItem, + quantity: 0, + onIncrease: {}, + onDecrease: {}, + onAddToCart: {}, + onRemoveFromCart: {} + ) + + // 수량 1 (카트에 하나 있음) + OrderRequestCardView( + item: sampleItem, + quantity: 1, + onIncrease: {}, + onDecrease: {}, + onAddToCart: {}, + onRemoveFromCart: {} + ) + + // 수량 3 (여러 개 담긴 상태) + OrderRequestCardView( + item: sampleItem, + quantity: 3, + onIncrease: {}, + onDecrease: {}, + onAddToCart: {}, + onRemoveFromCart: {} + ) + } + .padding() + .background(Color(uiColor: .systemGray6)) +} diff --git a/StockMate/StockMate/app/core/components/ProfileFieldView.swift b/StockMate/StockMate/app/core/components/ProfileFieldView.swift new file mode 100644 index 0000000..3f680d5 --- /dev/null +++ b/StockMate/StockMate/app/core/components/ProfileFieldView.swift @@ -0,0 +1,38 @@ +// +// ProfileFieldView.swift +// StockMate +// +// Created by Admin on 11/7/25. +// + +import SwiftUI + +struct ProfileFieldView: View { + let label: String + let value: String + + var body: some View { + VStack(spacing: 9) { + Text(label) + .font(.system(size: 14, weight: .medium)) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 10) + .foregroundColor(Color.black) + + HStack { + Text(value) + .font(.system(size: 16, weight: .regular)) + .foregroundColor(.black) + Spacer() + } + .padding() + .cornerRadius(12) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.GrayMordern300, lineWidth: 1) + ) + .padding(.horizontal, 10) + .padding(.bottom, 10) + } + } +} diff --git a/StockMate/StockMate/app/feature/auth/ui/HomeView.swift b/StockMate/StockMate/app/feature/auth/ui/HomeView.swift index 1094e52..fa9e0b8 100644 --- a/StockMate/StockMate/app/feature/auth/ui/HomeView.swift +++ b/StockMate/StockMate/app/feature/auth/ui/HomeView.swift @@ -12,7 +12,12 @@ struct HomeView: View { @EnvironmentObject var authViewModel: AuthViewModel @StateObject private var userViewModel = UserViewModel() @StateObject private var inventoryViewModel = InventoryViewModel() +// @EnvironmentObject var dashboardViewModel: DashboardViewModel //preview 용 + @StateObject private var dashboardViewModel = DashboardViewModel() + @State private var selectedMonth: String? = nil // ✅ 추가 + + var body: some View { ScrollView { VStack(alignment: .leading, spacing: 15) { @@ -57,7 +62,7 @@ struct HomeView: View { .cornerRadius(9999) .overlay( RoundedRectangle(cornerRadius: 9999) - .stroke(Color.gray.opacity(0.4), lineWidth: 1) + .stroke(Color.GrayMordern400, lineWidth: 1) ) .padding(.horizontal) } @@ -65,20 +70,28 @@ struct HomeView: View { lackStockSection - // 도넛 차트 섹션 VStack(alignment: .leading, spacing: 18) { Text("지난달 카테고리 별 지출") - .font(.headline) + .font(.system(size: 15, weight: .semibold)) .padding(4) + .frame(maxWidth: .infinity, alignment: .leading) // ✅ 항상 왼쪽 정렬 - HStack{ - DonutChartView() - .frame(height: 130) - .padding() + HStack { + if dashboardViewModel.isLoading { + ProgressView("불러오는 중...") + .frame(height: 155) + } else if dashboardViewModel.categorySpendings.isEmpty { + Text("지난달 지출 내역이 없습니다.") + .foregroundColor(.gray) + .frame(maxWidth: .infinity) + .frame(height: 155, alignment: .center) + } else { + DonutChartView(data: dashboardViewModel.categorySpendings) + .frame(height: 155) .background(Color.white) .cornerRadius(16) - .shadow(color: .gray.opacity(0.1), radius: 4) + } Spacer() } @@ -87,32 +100,52 @@ struct HomeView: View { .background(Color.white) .cornerRadius(16) .padding(.horizontal) - - + // 막대그래프 섹션 VStack(alignment: .leading, spacing: 8) { - Text("지출 현황") - .font(.headline) + Text("월간 지출 현황") + .font(.system(size: 15, weight: .semibold)) .padding(4) + .frame(maxWidth: .infinity, alignment: .leading) // ✅ 항상 왼쪽 정렬 - BarChartView() - .frame(height: 150) -// .padding() - .background(Color.white) - .shadow(color: .gray.opacity(0.1), radius: 4) + ZStack { // ✅ 크기 고정용 컨테이너 + RoundedRectangle(cornerRadius: 16) + .fill(Color.white) + .frame(height: 220) // ✅ 일정 높이 고정 + if dashboardViewModel.isLoading { + ProgressView("데이터 불러오는 중...") + .frame(height: 220) + } else if dashboardViewModel.monthlySpendings.isEmpty { + Text("최근 지출 내역이 없습니다.") + .foregroundColor(.gray) + .frame(height: 220) + } else { + BarChartView( + values: dashboardViewModel.spendingRatios, + labels: dashboardViewModel.monthLabels, + amounts: dashboardViewModel.monthlySpendings.map { $0.totalAmount }, selectedMonth: $selectedMonth + ) + .padding() + // .frame(height: 220) + // .background(Color.white) + // .cornerRadius(16) + } + } } .padding() .background(Color.white) .cornerRadius(16) .padding(.horizontal) } - .padding(.vertical) + .padding(.vertical,5) } .background(Color.Light) .task { // 카테고리 데이터 로드 await inventoryViewModel.loadLackCountByCategory() + await dashboardViewModel.fetchMonthlySpending() // ✅ 추가 + await dashboardViewModel.fetchCategorySpending() // ✅ 추가 } .onAppear { Task { await userViewModel.loadUserInfo() } @@ -130,19 +163,33 @@ struct HomeView: View { private var lackStockSection: some View { VStack(alignment: .leading, spacing: 8) { Text("재고 부족 조회") - .font(.headline) + .font(.system(size: 15, weight: .semibold)) + .frame(maxWidth: .infinity, alignment: .leading) // ✅ 항상 왼쪽 정렬 유지 HStack(spacing: 13) { - ForEach(inventoryViewModel.lackCounts, id: \.id) { item in - NavigationLink { - LackListView(selectedCategory: item.categoryName) - } label: { + if inventoryViewModel.lackCounts.isEmpty { + // ✅ 데이터가 없을 때도 공간 확보 + ForEach(0..<5) { _ in StatusItem( - title: item.categoryName, - count: item.count, - color: colorForCategory(item.categoryName), - icon: iconForCategory(item.categoryName) + title: "-", + count: 0, + color: .gray.opacity(0.1), + icon: "questionmark" ) } + .redacted(reason: .placeholder) // 로딩 중 효과 (선택사항) + } else { + 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) @@ -177,7 +224,7 @@ private func iconForCategory(_ name: String) -> String { case "하체/바디": return "spanner" case "내장/외장": return "chair" case "기타소모품": return "package" - default: return "questionmark" + default: return "uploadprogress" } } @@ -199,7 +246,7 @@ struct StatusItem: View { .clipShape(RoundedRectangle(cornerRadius: 100)) Text(title) - .font(.system(size: 13, weight: .semibold)) + .font(.system(size: 12, weight: .semibold)) .foregroundColor(.black) .padding(.top, 5) .lineLimit(1) @@ -212,46 +259,23 @@ struct StatusItem: View { } } -// MARK: - 도넛 차트 (더미) -struct DonutChartView: View { - var body: some View { - ZStack { - Circle() - .trim(from: 0, to: 0.521) - .stroke(Color.black, lineWidth: 40) - Circle() - .trim(from: 0.521, to: 0.749) - .stroke(Color(hex: "#7DBBFF"), lineWidth: 40) - Circle() - .trim(from: 0.749, to: 0.888) - .stroke(Color(hex: "#71DD8C"), lineWidth: 40) - Circle() - .trim(from: 0.888, to: 1) - .stroke(Color(hex: "#A0BCE8"), lineWidth: 40) - } - .rotationEffect(.degrees(-89.9)) - .padding() - } -} -// MARK: - 막대그래프 (더미) -struct BarChartView: View { - let values: [CGFloat] = [0.89, 0.5, 0.9, 0.3, 0.7] - let colors: [Color] = [ - .LightBlue04, .Primary, .LightBlue04, .LightBlue04, .LightBlue04 - ] - var body: some View { - HStack(alignment: .bottom, spacing: 33) { - ForEach(0..= 8 ? nil : "8자 이상 비밀번호를 입력해주세요" return emailError == nil && pwError == nil - return true +// return true } } diff --git a/StockMate/StockMate/app/feature/auth/ui/RegisterView.swift b/StockMate/StockMate/app/feature/auth/ui/RegisterView.swift index 7656f2c..2e29490 100644 --- a/StockMate/StockMate/app/feature/auth/ui/RegisterView.swift +++ b/StockMate/StockMate/app/feature/auth/ui/RegisterView.swift @@ -30,6 +30,9 @@ struct RegisterView: View { @State private var isLoading = false @State private var showToast = false + @State private var showAddressSearch = false + + var body: some View { ScrollView { VStack(spacing: 16) { @@ -83,19 +86,48 @@ struct RegisterView: View { text: $storeName, errorMessage: storeNameError ) - CustomTextField( - title: "주소", - placeholder: "서울특별시 강남구 ...", - text: $address, - errorMessage: addressError - ) + // ✅ 주소 입력 필드 + 버튼 추가 부분 + VStack(alignment: .leading, spacing: 4) { + HStack { + CustomTextField( + title: "주소", + placeholder: "서울특별시 강남구 ...", + text: $address, + errorMessage: addressError, + isReadOnly: true // ✅ 추가 + ) + .disabled(true) // 사용자가 직접 입력 못하게 + .onTapGesture { + // 탭해도 검색창 열 수 있게 (선택사항) + showAddressSearch.toggle() + } + + Button(action: { + showAddressSearch.toggle() + }) { + Text("주소 검색") + .font(.system(size: 14, weight: .semibold)) + .frame(height: 43) + .padding(.horizontal, 12) + .background(Color.Primary) + .foregroundColor(.white) + .cornerRadius(8) + } + .sheet(isPresented: $showAddressSearch) { + KakaoZipCodeView(address: $address) + } + } + } CustomTextField( title: "사업자등록번호", placeholder: "123-45-67890", text: $bizNo, errorMessage: bizNoError ) - .keyboardType(.numbersAndPunctuation) + .keyboardType(.numberPad) + .onChange(of: bizNo) { newValue in + formatBizNoInput(newValue) + } } .padding(.horizontal, 24) @@ -138,10 +170,15 @@ struct RegisterView: View { } } .padding(.bottom, 40) + + // ✅ 키보드 가림 방지용 여백 + Spacer().frame(height: 300) } } .background(Color.Light) .ignoresSafeArea() + .scrollDismissesKeyboard(.interactively) // ✅ 손가락으로 스크롤하면 키보드 자동 내려감 + } // MARK: - 유효성 검사 함수 @@ -212,9 +249,42 @@ struct RegisterView: View { owner: owner, address: address, storeName: storeName, - bizNo: bizNo + bizNo: bizNo.filter { $0.isNumber } // ← 여기서 숫자만 추출해서 전송 ) isLoading = false } } + + private func formatBizNoInput(_ input: String) { + // 1️⃣ 숫자만 남기기 + let digitsOnly = input.filter { $0.isNumber } + + // 2️⃣ 하이픈 자동 삽입 + var formatted = "" + let length = digitsOnly.count + + if length <= 3 { + formatted = digitsOnly + } else if length <= 5 { + formatted = "\(digitsOnly.prefix(3))-\(digitsOnly.suffix(from: digitsOnly.index(digitsOnly.startIndex, offsetBy: 3)))" + } else { + let first = digitsOnly.prefix(3) + let middleStart = digitsOnly.index(digitsOnly.startIndex, offsetBy: 3) + let middleEnd = digitsOnly.index(middleStart, offsetBy: 2, limitedBy: digitsOnly.endIndex) ?? digitsOnly.endIndex + let middle = digitsOnly[middleStart.. 10 { + formatted = String(formatted.prefix(12)) // 하이픈 포함 + } + + // 4️⃣ 상태 업데이트 + if formatted != bizNo { + bizNo = formatted + } + } } + diff --git a/StockMate/StockMate/app/feature/cart/ui/DeliveryStatusView.swift b/StockMate/StockMate/app/feature/cart/ui/DeliveryStatusView.swift index 2825687..6a9f189 100644 --- a/StockMate/StockMate/app/feature/cart/ui/DeliveryStatusView.swift +++ b/StockMate/StockMate/app/feature/cart/ui/DeliveryStatusView.swift @@ -16,8 +16,8 @@ struct DeliveryStep { struct DeliveryStatusView: View { let steps: [DeliveryStep] = [ DeliveryStep(title: "결제완료", iconName: "check"), - DeliveryStep(title: "승인대기중", iconName: "hourglass"), DeliveryStep(title: "상품준비중", iconName: "uploadprogress"), + DeliveryStep(title: "배송시작", iconName: "flag"), DeliveryStep(title: "배송중", iconName: "rocket"), DeliveryStep(title: "배송완료", iconName: "pindrop") ] diff --git a/StockMate/StockMate/app/feature/dashboard/data/HistoryApi.swift b/StockMate/StockMate/app/feature/dashboard/data/HistoryApi.swift new file mode 100644 index 0000000..97f41d9 --- /dev/null +++ b/StockMate/StockMate/app/feature/dashboard/data/HistoryApi.swift @@ -0,0 +1,114 @@ +// +// HistoryApi.swift +// StockMate +// +// Created by Admin on 11/1/25. +// + +import Foundation +import Alamofire + +// MARK: - 입출고 히스토리 데이터 구조 +struct HistoryPageData: Decodable { + let totalElements: Int + let totalPages: Int + let currentPage: Int + let pageSize: Int + let content: [HistoryItem] + let last: Bool +} + +struct HistoryItem: Decodable, Identifiable { + let id: Int + let memberId: Int + let orderId: Int? + let orderNumber: String? + let message: String + let status: String + let type: String + let createdAt: String + let updatedAt: String + let userInfo: HistoryUserInfo? + let items: [HistoryPart] +} + +struct HistoryUserInfo: 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 HistoryPart: Decodable, Identifiable { + 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 + let code: String + let location: String + let cost: Int + let historyQuantity: Int +} + + +// MARK: - 예치금 거래내역 데이터 구조 +struct PaymentTransactionPageData: Decodable { + let content: [PaymentTransactionItem] + let page: Int + let size: Int + let totalElements: Int + let totalPages: Int + let hasNext: Bool + let hasPrevious: Bool + let last: Bool + let first: Bool +} + +struct PaymentTransactionItem: Decodable { + let transactionId: Int + let transactionType: String // "CHARGE" or "PAY" + let transactionTime: String? + let totalAmount: Int + let orderId: Int? + let orderItems: [OrderItemHistory]? + let balance: Int +} + + +struct OrderItemHistory: Decodable { + let id: Int + let name: String + let image: String + let korName: String + let categoryName: String +} + + +// MARK: - API +enum HistoryApi { + // ✅ 가맹점별 입출고 히스토리 조회 + static func getInOutHistory(page: Int = 0, size: Int = 20) -> DataRequest { + let url = ApiClient.baseURL + "api/v1/information/order-history/my?page=\(page)&size=\(size)" + return ApiClient.shared.request(url, method: .get) + } + + // ✅ 예치금 거래내역 조회 + static func getPaymentTransaction(page: Int = 0, size: Int = 20) -> DataRequest { + let url = ApiClient.baseURL + "api/v1/payment/transaction?page=\(page)&size=\(size)" + return ApiClient.shared.request(url, method: .get) + } +} diff --git a/StockMate/StockMate/app/feature/dashboard/data/HistoryRepositoryImpl.swift b/StockMate/StockMate/app/feature/dashboard/data/HistoryRepositoryImpl.swift new file mode 100644 index 0000000..1c8dfe4 --- /dev/null +++ b/StockMate/StockMate/app/feature/dashboard/data/HistoryRepositoryImpl.swift @@ -0,0 +1,22 @@ +// +// HistoryRepositoryImpl.swift +// StockMate +// +// Created by Admin on 11/1/25. +// + +import Foundation +import Alamofire + +final class HistoryRepositoryImpl: HistoryRepositoryProtocol { + func getInOutHistory(page: Int, size: Int) async -> AppResult> { + let request = HistoryApi.getInOutHistory(page: page, size: size) + return await safeApi(request, decodeTo: ApiResponse.self) + } + + // ✅ 예치금 거래내역 조회 + func getPaymentTransaction(page: Int, size: Int) async -> AppResult> { + let request = HistoryApi.getPaymentTransaction(page: page, size: size) + return await safeApi(request, decodeTo: ApiResponse.self) + } +} diff --git a/StockMate/StockMate/app/feature/dashboard/domain/HistoryRepositoryProtocol.swift b/StockMate/StockMate/app/feature/dashboard/domain/HistoryRepositoryProtocol.swift new file mode 100644 index 0000000..fddd5cc --- /dev/null +++ b/StockMate/StockMate/app/feature/dashboard/domain/HistoryRepositoryProtocol.swift @@ -0,0 +1,16 @@ +// +// HistoryRepositoryProtocol.swift +// StockMate +// +// Created by Admin on 11/1/25. +// + +import Foundation +import Alamofire + +protocol HistoryRepositoryProtocol { + func getInOutHistory(page: Int, size: Int) async -> AppResult> + + // ✅ 예치금 거래내역 조회 + func getPaymentTransaction(page: Int, size: Int) async -> AppResult> +} diff --git a/StockMate/StockMate/app/feature/dashboard/ui/InOutHistoryView.swift b/StockMate/StockMate/app/feature/dashboard/ui/InOutHistoryView.swift new file mode 100644 index 0000000..12998e9 --- /dev/null +++ b/StockMate/StockMate/app/feature/dashboard/ui/InOutHistoryView.swift @@ -0,0 +1,169 @@ +// +// InOutHistoryView.swift +// StockMate +// +// Created by Admin on 11/1/25. +// + +import SwiftUI + +struct InOutHistoryView: View { + @StateObject private var viewModel = HistoryViewModel() + + var body: some View { + VStack { + if viewModel.isLoading { + ProgressView("불러오는 중...") + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if let error = viewModel.errorMessage { + Text(error) + .foregroundColor(.red) + .multilineTextAlignment(.center) + .padding() + } else if viewModel.histories.isEmpty { + Text("입출고 내역이 없습니다.") + .foregroundColor(.gray) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + // ✅ 날짜별로 그룹화 (최신순) + let groupedHistories = Dictionary(grouping: viewModel.histories) { history in + history.createdAt.split(separator: "T").first.map(String.init) ?? "" + } + .sorted { $0.key > $1.key } // 최신순 정렬 + + ScrollView { + LazyVStack(alignment: .leading, spacing: 20) { + ForEach(groupedHistories, id: \.key) { date, histories in + VStack(alignment: .leading, spacing: 12) { + // ✅ 날짜 헤더 + Text(formatDate(String(date))) + .font(.headline) + .padding(.leading, 25) + .padding(.top) + + // ✅ 해당 날짜의 히스토리 카드들 + ForEach(histories) { history in + InOutHistoryCard(history: history) + } + } + } + } + .padding(.bottom) + } + .padding(.top) + } + } + .background(Color.Light) + .navigationTitle("입출고 내역") + .task { + await viewModel.fetchInOutHistory() + } + } + + 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 InOutHistoryCard: View { + let history: HistoryItem + + var body: some View { + HStack(spacing: 10) { + if let firstItem = history.items.first { + HStack(alignment: .center, spacing: 12) { + AsyncImage(url: URL(string: firstItem.image)) { image in + image.resizable().scaledToFill() + } placeholder: { + Color.gray.opacity(0.2) + } + .frame(width: 64, height: 64) + .cornerRadius(10) + + VStack(alignment: .leading, spacing: 4) { + Text(firstItem.korName) + .font(.system(size: 15)) + .lineLimit(1) + if history.items.count > 1 { + Text("외 \(history.items.count - 1)개 품목") + .font(.caption) + .foregroundColor(.gray) + } + } + } + } + + Spacer() + + // 입출고 상태 + Text(statusText(history.status)) + .font(.caption) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(statusBgColor(history.status)) + .foregroundColor(statusTextColor(history.status)) + .cornerRadius(8) + + if history.status == "RECEIVED", let orderId = history.orderId { + NavigationLink( + destination: OrderDetailView(orderId: orderId, orderViewModel: OrderViewModel()) + ) { + + Image(systemName: "chevron.right") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.gray) + + } + .buttonStyle(.plain) + } else { + NavigationLink(destination: ReleaseDetailView(history: history)) { + Image(systemName: "chevron.right") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.gray) + } + .buttonStyle(.plain) + } + } + .padding() + .background(Color.white) + .cornerRadius(12) + .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 2) + .padding(.horizontal) + } + + func formatDate(_ iso: String) -> String { + String(iso.prefix(10)).replacingOccurrences(of: "-", with: ".") + } + + func statusText(_ status: String) -> String { + switch status { + case "RECEIVED": return "입고 완료" + case "RELEASED": return "출고 완료" + default: return status + } + } + + func statusTextColor(_ status: String) -> Color { + switch status { + case "RELEASED": return .StatusGreen + case "RECEIVED": return .StatusPurple + default: return .gray + } + } + + func statusBgColor(_ status: String) -> Color { + switch status { + case "RELEASED": return .StatusGreenBg + case "RECEIVED": return .StatusPurpleBg + default: return Color.gray.opacity(0.15) + } + } +} + +#Preview { + InOutHistoryView() +} diff --git a/StockMate/StockMate/app/feature/dashboard/ui/ReleaseDetailView.swift b/StockMate/StockMate/app/feature/dashboard/ui/ReleaseDetailView.swift new file mode 100644 index 0000000..d1cd2ed --- /dev/null +++ b/StockMate/StockMate/app/feature/dashboard/ui/ReleaseDetailView.swift @@ -0,0 +1,141 @@ +// +// ReleaseDetailView.swift +// StockMate +// +// Created by Admin on 11/2/25. +// + +import SwiftUI + +struct ReleaseDetailView: View { + let history: HistoryItem + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 20) { + // ✅ 부품 리스트 + VStack(alignment: .leading, spacing: 7) { + Text("출고 품목 (\(history.items.count)개)") + .font(.system(size: 17, weight: .semibold)) + .padding(.leading) + + // ✅ 처리일자 포맷팅 + Text("처리일자: \(formattedDate3(history.createdAt))") + .font(.system(size: 15)) + .foregroundColor(Color.textGray1) + .padding(.leading) + + ForEach(history.items) { part in + ReleasePartCard(part: part) + } + } + } + .padding(.vertical) + } + .background(Color.Light) + .navigationTitle("출고 상세") + .navigationBarTitleDisplayMode(.inline) + } +} + +struct ReleasePartCard: View { + let part: HistoryPart + + var body: some View { + VStack(alignment: .leading, spacing: 5){ + // 상단 카테고리 + Text(part.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: part.image)) { image in + image.resizable().scaledToFill() + } placeholder: { + Color.gray.opacity(0.2) + } + .frame(width: 64, height: 64) + .cornerRadius(10) + + VStack(alignment: .leading, spacing: 4) { + Text(part.korName) + .font(.system(size: 14, weight: .bold)) + .lineLimit(1) + + Text("\(part.model) / \(part.trim) / \(part.price)원 / \(part.historyQuantity)개") + .font(.system(size: 12)) + .foregroundColor(.gray) + + Text("\(part.price * part.historyQuantity)원") + .font(.system(size: 12)) + .foregroundColor(.black) + } + + Spacer() + + + } + } + .padding() + .background(Color.white) + .cornerRadius(12) + .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 2) + .padding(.horizontal) + } +} + +#Preview { + ReleaseDetailView(history: HistoryItem( + id: 1, + memberId: 1, + orderId: 2, + orderNumber: "ORD-1234", + message: "출고 완료", + status: "RELEASED", + type: "RELEASE", + createdAt: "2025-11-01T12:30:00", + updatedAt: "2025-11-01T12:40:00", + userInfo: nil, + items: [ + HistoryPart(id: 1, name: "partA", price: 1000, image: "", trim: "basic", model: "A1", category: 1, korName: "부품A", engName: "PartA", categoryName: "카테고리A", amount: 5, code: "P001", location: "A-01", cost: 500, historyQuantity: 3) + ] + )) +} + + +func formattedDate3(_ timestamp: String) -> String { + let inputFormats = [ + "yyyy-MM-dd'T'HH:mm:ss.SSSSSS", + "yyyy-MM-dd'T'HH:mm:ss.SSSSSSS", + "yyyy-MM-dd'T'HH:mm:ss.SSS", + "yyyy-MM-dd'T'HH:mm:ss" + ] + + let trimmed = timestamp.trimmingCharacters(in: .whitespacesAndNewlines) + let parser = DateFormatter() + parser.locale = Locale(identifier: "en_US_POSIX") + parser.timeZone = TimeZone(identifier: "Asia/Seoul") // ✅ 서버 시간 기준으로 맞춤 + + var date: Date? = nil + for format in inputFormats { + parser.dateFormat = format + if let parsed = parser.date(from: trimmed) { + date = parsed + break + } + } + + guard let finalDate = date else { return timestamp } + + // 출력도 한국시간으로 + let output = DateFormatter() + output.locale = Locale(identifier: "ko_KR") + output.timeZone = TimeZone(identifier: "Asia/Seoul") + output.dateFormat = "yyyy.MM.dd HH:mm:ss" + + return output.string(from: finalDate) +} diff --git a/StockMate/StockMate/app/feature/dashboard/ui/TransactionTypeListView.swift b/StockMate/StockMate/app/feature/dashboard/ui/TransactionTypeListView.swift new file mode 100644 index 0000000..622585a --- /dev/null +++ b/StockMate/StockMate/app/feature/dashboard/ui/TransactionTypeListView.swift @@ -0,0 +1,129 @@ +// +// TransactionTypeListView.swift +// StockMate +// +// Created by Admin on 11/6/25. +// + +import SwiftUI + +struct TransactionTypeListView: View { + @StateObject private var viewModel = HistoryViewModel() + + var body: some View { + NavigationStack { + ScrollView { + LazyVStack(spacing: 12) { + ForEach(viewModel.transactions, id: \.transactionId) { item in + TransactionCard(item: item) + .onAppear { + Task { + await viewModel.loadMoreTransactionsIfNeeded(currentItem: item) + } + } + } + } + .padding(.horizontal) + .padding(.top, 10) + } + .background(Color.Light.ignoresSafeArea()) + .navigationTitle("예치금 히스토리") + .navigationBarTitleDisplayMode(.inline) + .overlay { + if viewModel.isTransactionLoading && viewModel.transactions.isEmpty { + ProgressView("불러오는 중...") + } + } + .task { + if viewModel.transactions.isEmpty { + await viewModel.fetchPaymentTransactions() + } + } + } + .background(Color.Light) + } +} + +struct TransactionCard: View { + let item: PaymentTransactionItem + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack(alignment: .center, spacing: 12) { + + // PAY일 때만 부품 이미지 표시 + if item.transactionType == "PAY" { + // 대표 이미지 + AsyncImage(url: URL(string: item.orderItems?.first?.image ?? "")) { image in + image.resizable().scaledToFit() + } placeholder: { + Color.gray.opacity(0.2) + } + .frame(width: 64, height: 64) + .cornerRadius(10) + } else { + Image("exchange") + .foregroundColor(Color.Primary) + .frame(width: 64, height: 64) + .cornerRadius(10) + } + + + + VStack(alignment: .leading, spacing: 5) { + if item.transactionType == "PAY", + let orderItems = item.orderItems, + !orderItems.isEmpty { + let firstName = orderItems.first?.korName ?? "-" + let extraCount = orderItems.count - 1 + let displayText = extraCount > 0 + ? "\(firstName) 외 \(extraCount)개" + : firstName + Text(displayText) + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.black) + + } else if item.transactionType == "CHARGE" { + Text("예치금 충전") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.black) + } + // 날짜 + Text( + item.transactionTime != nil ? formattedDate( + item.transactionTime! + ) : "-" + ) + .font(.system(size: 13, weight: .regular)) + .foregroundColor(.textGray1) + + // ✅ 금액 표시 (PAY/CHARGE 구분) + let isPay = item.transactionType == "PAY" + let sign = isPay ? "-" : "+" + let color: Color = isPay ? .Danger : .Primary + + Text("\(sign) \(formatPrice(item.totalAmount))") + .font(.system(size: 13, weight: .bold)) + .foregroundColor(color) + + } + .frame(height: 60, alignment: .top) + Spacer() + // ✅ PAY일 때만 꺾새 표시 + if item.transactionType == "PAY" { + NavigationLink(destination: ReceiptView(orderId: item.orderId ?? 1)) { + Image(systemName: "chevron.right") + .font(.system(size: 15, weight: .medium)) + .foregroundColor(.gray) + } + .buttonStyle(.plain) + } + } + } + .padding() + .background(Color.white) + .cornerRadius(10) + .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) + } +} + diff --git a/StockMate/StockMate/app/feature/dashboard/viewmodel/HistoryViewModel.swift b/StockMate/StockMate/app/feature/dashboard/viewmodel/HistoryViewModel.swift new file mode 100644 index 0000000..0eb95d7 --- /dev/null +++ b/StockMate/StockMate/app/feature/dashboard/viewmodel/HistoryViewModel.swift @@ -0,0 +1,110 @@ +// +// HistoryViewModel.swift +// StockMate +// +// Created by Admin on 11/1/25. +// + +import Foundation +import Alamofire + +@MainActor +final class HistoryViewModel: ObservableObject { + // MARK: - 입출고 히스토리 관련 + @Published var histories: [HistoryItem] = [] + @Published var isLoading = false + @Published var errorMessage: String? + @Published var currentPage = 0 + @Published var totalPages = 1 + + // MARK: - 예치금 거래내역 관련 + @Published var transactions: [PaymentTransactionItem] = [] + @Published var transactionPage = 0 + @Published var transactionTotalPages = 1 + @Published var isTransactionLoading = false + + private let repository: HistoryRepositoryProtocol + + init(repository: HistoryRepositoryProtocol = HistoryRepositoryImpl()) { + self.repository = repository + } + + /// ✅ 입출고 히스토리 불러오기 + func fetchInOutHistory(page: Int = 0, size: Int = 20) async { + guard !isLoading else { return } + isLoading = true + defer { isLoading = false } + + let result = await repository.getInOutHistory(page: page, size: size) // ✅ 오타도 수정됨 (getInout → getInOut) + switch result { + case .success(let response): + if let data = response.data { + if page == 0 { + histories = data.content + } else { + histories.append(contentsOf: data.content) + } + currentPage = data.currentPage + totalPages = data.totalPages + errorMessage = nil + } else { + errorMessage = "데이터가 없습니다." + } + case .failure(let error): + errorMessage = error.localizedDescription + print("❌ 입출고 히스토리 조회 실패:", error) + } + } + + /// ✅ 다음 페이지 로드 (무한 스크롤 등) + func loadMoreIfNeeded(currentItem item: HistoryItem?) async { + guard let item = item else { return } + let threshold = max(histories.count - 5, 0) + if let currentIndex = histories.firstIndex(where: { $0.id == item.id }), + currentIndex >= threshold, + currentPage + 1 < totalPages { + await fetchInOutHistory(page: currentPage + 1) + } + } + + // MARK: - ✅ 예치금 거래내역 불러오기 + func fetchPaymentTransactions(page: Int = 0, size: Int = 20) async { + guard !isTransactionLoading else { return } + isTransactionLoading = true + defer { isTransactionLoading = false } + + let result = await repository.getPaymentTransaction(page: page, size: size) + switch result { + case .success(let response): + if let data = response.data { + if page == 0 { + transactions = data.content + } else { + transactions.append(contentsOf: data.content) + } + transactionPage = data.page + transactionTotalPages = data.totalPages + errorMessage = nil + } else { + errorMessage = "데이터가 없습니다." + } + case .failure(let error): + errorMessage = error.localizedDescription + print("❌ 예치금 거래내역 조회 실패:", error) + } + } + + // MARK: - ✅ 무한 스크롤 (예치금 내역) + func loadMoreTransactionsIfNeeded(currentItem item: PaymentTransactionItem?) async { + guard let item = item else { return } + guard !isTransactionLoading else { return } // 중복 로드 방지 + guard transactionPage + 1 < transactionTotalPages else { return } // 마지막 페이지 방지 + + // ✅ 안전한 threshold 계산 + let thresholdIndex = max(transactions.count - 5, 0) + if let currentIndex = transactions.firstIndex(where: { $0.transactionId == item.transactionId }), + currentIndex >= thresholdIndex { + await fetchPaymentTransactions(page: transactionPage + 1) + } + } +} diff --git a/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift b/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift index 9641e2e..70063c4 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift @@ -124,9 +124,9 @@ struct InventoryView: View { struct GridMenuView: View { let menuItems = [ ("재고 조회", true, "InvStock", AnyView(InventorySearchView())), - ("입출고 히스토리", false, "InvTrans", AnyView(IncomingScanView())), + ("입출고 히스토리", false, "InvTrans", AnyView(InOutHistoryView())), ("입고 처리", false, "InvIncoming", AnyView(IncomingScanView())), - ("사용 처리", true, "InvUse", AnyView(OutgoingScanView())), + ("사용 처리", true, "InvUse", AnyView(OutgoingScanView().environmentObject(PartStore()))), ] var body: some View { diff --git a/StockMate/StockMate/app/feature/inventory/ui/OutgoingScanView.swift b/StockMate/StockMate/app/feature/inventory/ui/OutgoingScanView.swift index 506c0fa..af8bb03 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/OutgoingScanView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/OutgoingScanView.swift @@ -13,15 +13,27 @@ struct OutgoingScanView: View { @State private var scannedCode: String? = nil @State private var showAlert = false @State private var alertMessage = "" - + + // 전역 부품 저장소 + @EnvironmentObject var partStore: PartStore @StateObject private var partViewModel = PartViewModel() // ✅ ViewModel 추가 + + @State private var showBottomSheet = false + + @State private var scannerRestartTrigger = false + + @State private var partDetail: PartDetail? = nil + var body: some View { ZStack { // ✅ 카메라 미리보기 (QR 스캐너) - QRScannerView(scannedCode: $scannedCode) +// QRScannerView(scannedCode: $scannedCode) +// .ignoresSafeArea() + QRScannerView(scannedCode: $scannedCode, isActive: !showBottomSheet) .ignoresSafeArea() + // ✅ 스캔 가이드 및 UI 오버레이 VStack { Text("사용할 부품의 QR을 스캔해주세요") @@ -29,23 +41,23 @@ struct OutgoingScanView: View { .padding(.top, 60) .foregroundColor(.white) .shadow(radius: 2) - + Spacer() - + // 📷 스캔 박스 ZStack { RoundedRectangle(cornerRadius: 12) .fill(Color.clear) .frame(width: 250, height: 250) - + RoundedRectangle(cornerRadius: 8) .stroke(Color.green, lineWidth: 3) .frame(width: 220, height: 220) } .padding(.bottom, 180) - + Spacer() - + // 📦 직접 입력 버튼 Button(action: { dismiss() @@ -62,59 +74,125 @@ struct OutgoingScanView: View { .padding(.horizontal, 40) .padding(.bottom, 40) } - + // ✅ 로딩 인디케이터 if partViewModel.isLoading { Color.black.opacity(0.3).ignoresSafeArea() - ProgressView("부품 사용 처리 중...") + ProgressView("부품 조회 중...") //ProgressView("부품 사용 처리 중...") .padding() .background(.ultraThinMaterial) .cornerRadius(10) } } - // ✅ 알림창 - .alert("부품 사용 결과", isPresented: $showAlert) { - Button("확인") { - dismiss() - } + .alert("알림", isPresented: $showAlert) { + Button("확인") {} } message: { Text(alertMessage) } - // ✅ QR 스캔 이벤트 발생 시 .onChange(of: scannedCode) { newValue in guard let code = newValue, !code.isEmpty else { return } - Task { - await handleScannedCode(code) - } + Task { await handleScannedCode(code) } } - .navigationTitle("부품 사용 처리") - .navigationBarTitleDisplayMode(.inline) + .sheet(isPresented: $showBottomSheet) { + UsedPartListSheetView( + onUseParts: { + Task { + // ✅ payload 생성 + let payload = partStore.parts.map { + ReleaseItemRequest(partId: $0.id, quantity: $0.quantity) + } + + // ✅ API 호출 + let result = await partViewModel.releaseParts(items: payload) + + await MainActor.run { + switch result { + case .success(let message): + alertMessage = message + showAlert = true + + // ✅ 성공 시: 전역 부품 초기화 + 바텀시트 닫기 + 화면 복귀 + partStore.clear() + showBottomSheet = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + dismiss() + } + + case .failure(let error): + alertMessage = error.message + showAlert = true + } + } + } + }, + onRescan: { + // ✅ 다시 스캔 버튼 눌렀을 때 + showBottomSheet = false + resetScanState() // QR 다시 활성화 + } + ) + .presentationDetents([.fraction(0.80)]) // 시트 높이 80% + .presentationCornerRadius(28) // ✅ 모서리 곡률 + .environmentObject(partStore) + } + .navigationTitle("부품 사용 처리") + .navigationBarTitleDisplayMode(.inline) } - - // ✅ 스캔된 코드로 출고 API 호출 + + // ✅ 스캔된 코드로 부품 상세 조회만 수행 (출고 X) private func handleScannedCode(_ code: String) async { - await MainActor.run { - partViewModel.isLoading = true + await MainActor.run { partViewModel.isLoading = true } + + // 문자열 → Int 변환 (QR 코드가 숫자 아닐 경우 예외 처리) + guard let partId = Int(code) else { + await MainActor.run { + partViewModel.isLoading = false + alertMessage = "잘못된 QR 코드입니다. (숫자형 ID가 아닙니다)" + showAlert = true + } + return } - let request = [ReleaseItemRequest(partCode: code, quantity: 1)] // 기본 1개로 설정 - let result = await partViewModel.releaseParts(items: request) + // ✅ 부품 상세 조회 API 호출 + await partViewModel.fetchPartDetail(partIds: partId) + // ✅ 결과 출력 await MainActor.run { partViewModel.isLoading = false - switch result { - case .success(let message): - alertMessage = message - case .failure(let error): - alertMessage = error.message - } - showAlert = true + + guard let response = partViewModel.partDetails.first else { + alertMessage = "부품 정보를 불러오지 못했습니다." + showAlert = true + return + } + + // ✅ 응답을 PartDetail로 변환 후 저장 + let newPart = PartDetail( + id: response.id, + price: response.price, + image: response.image, + trim: response.trim, + model: response.model, + korName: response.korName, + categoryName: response.categoryName, + quantity: 1 + ) + + partStore.addPart(newPart) + + // ✅ 자동으로 바텀시트 열기 + showBottomSheet = true + + // ✅ 스캔 상태 초기화 (다시 스캔 가능하도록) + resetScanState() + + print("✅ \(newPart.korName) 부품이 전역 Store에 추가됨") } } -} - -#Preview { - NavigationStack { - OutgoingScanView() + + // ✅ 상태 초기화 (QR 다시 활성화) + private func resetScanState() { + scannedCode = nil + scannerRestartTrigger.toggle() } } diff --git a/StockMate/StockMate/app/feature/inventory/ui/UsedPartListSheetView.swift b/StockMate/StockMate/app/feature/inventory/ui/UsedPartListSheetView.swift new file mode 100644 index 0000000..f644a0a --- /dev/null +++ b/StockMate/StockMate/app/feature/inventory/ui/UsedPartListSheetView.swift @@ -0,0 +1,231 @@ +// +// UsedPartListSheetView.swift +// StockMate +// +// Created by Admin on 11/4/25. +// + + +// +// UsedPartListSheetView.swift +// StockMate +// +// Created by Admin on 11/4/25. +// + +import SwiftUI + +struct UsedPartListSheetView: View { + @EnvironmentObject var partStore: PartStore + var onUseParts: (() -> Void)? // ‘사용 처리’ 버튼 액션 콜백 + var onRescan: (() -> Void)? // ✅ 다시 스캔 콜백 추가 + + var body: some View { + VStack (alignment: .center){ + // ✅ 상단 헤더 + ZStack { + Text("사용할 부품") + .font(.system(size: 18, weight: .bold)) + .foregroundColor(.black) + + HStack { + Spacer() + Button("전체 삭제") {partStore.clear()} + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.red) + .padding(.trailing, 20) + } + } + .padding(.vertical, 26) + .background(Color.white) + + + // ✅ 내용 영역 + ScrollView { + if partStore.parts.isEmpty { + // 부품 목록 전체 삭제의 경우 + VStack(spacing: 12) { + Image(systemName: "cube.box") + .resizable() + .scaledToFit() + .frame(width: 60, height: 60) + .foregroundColor(.gray.opacity(0.6)) + Text("추가된 부품이 없습니다.") + .foregroundColor(.gray) + } + .frame(maxWidth: .infinity, minHeight: 200) + } else { + LazyVStack { + ForEach($partStore.parts) { $part in // ✅ 바인딩으로 변경 ($ 붙임) + VStack(alignment: .leading, spacing: 6) { + Text(part.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: part.image) + ) { image in + image.resizable().scaledToFill() + } placeholder: { + Color.gray.opacity(0.2) + } + .frame(width: 64, height: 64) + .clipShape( + RoundedRectangle(cornerRadius: 10) + ) + + VStack(alignment: .leading,spacing: 6) { + Text(part.korName) + .font( .system(size: 13, weight: .bold)) + .foregroundColor(.black) + .lineLimit(2) + Text("\((part.trim)) / \((part.model))") + .font(.system(size: 12)) + .foregroundColor(.black) + .lineLimit(1) + Text("\(part.price)원") + .font(.system(size: 15, weight: .semibold)) + .foregroundColor(.black) + } + + Spacer() + + // ✅ 수량 조절 버튼 (디자인 개선) + HStack(spacing: 10) { + Button { + partStore.decreaseQuantityOrRemove(for: part) + } label: { + Image(systemName: "minus") + .font(.system(size: 14, weight: .regular)) + .frame(width: 13,height: 13) + .foregroundColor(.black) + } + Text("\(part.quantity)") + .font(.system(size: 15, weight: .medium)) + .frame(width: 20) + Button { + partStore.increaseQuantity(for: part) + } label: { + Image(systemName: "plus") + .font(.system(size: 14, weight: .regular)) + .frame(width: 13,height: 13) + .foregroundColor(.black) + } + } + .padding(.vertical, 6) + .padding(.horizontal, 8) + .background(Color.white) + .cornerRadius(10) + .overlay( // ✅ 테두리 추가 + RoundedRectangle(cornerRadius: 10) + .stroke( Color.LightBlue03, lineWidth: 1) + ) + .shadow(color: .black.opacity(0.15), radius: 4, x: 0, y: 4 ) + } + } + .padding() + .background(Color.white) + .cornerRadius(14) + .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) + + } + .onDelete { indexSet in + partStore.parts.remove(atOffsets: indexSet) + } + } + .listStyle(.plain) + + } + } + .padding(7) + .frame(maxHeight: .infinity) + + HStack{ + // 🔹 다시 스캔 버튼 + Button { + onRescan?() + } label: { + Text("부품 추가") + .font(.headline) + .foregroundColor(.Primary) + .frame(maxWidth: .infinity) + .padding() + .background(Color.white) + .cornerRadius(9999) + .background( + RoundedRectangle(cornerRadius: 9999) + .stroke(Color.Primary, lineWidth: 2) + ) + } + .padding(.bottom, 16) + + // ✅ 사용 처리 버튼 + Button { + onUseParts?() + } label: { + Text("사용 처리") + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background( + partStore.parts.isEmpty ? Color.gray : Color.Primary + ) + .cornerRadius(9999) + } + .disabled(partStore.parts.isEmpty) + .padding(.bottom, 16) + } + .padding(.horizontal) + + } + } +} + +// MARK: - 프리뷰 (안전 버전) +@MainActor +struct UsedPartListSheetView_Previews: PreviewProvider { + static var previewStore: PartStore = { + let store = PartStore() + store.parts = [ + PartDetail( + id: 1, + price: 10000, + image: "https://via.placeholder.com/150", + trim: "준준형/소형", + model: "아반떼MD", + korName: "액츄에이터 - 템퍼러처 도어", + categoryName: "전기/램프", + quantity: 2 + ), + PartDetail( + id: 2, + price: 25000, + image: "https://via.placeholder.com/150", + trim: "준준형/소형", + model: "아반떼 MD", + korName: "스위치 어셈블리 - 도어", + categoryName: "전기/램프", + quantity: 1 + ) + ] + return store + }() + + static var previews: some View { + NavigationStack { + UsedPartListSheetView( + onUseParts: { print("✅ 사용 처리 버튼 눌림") }, + onRescan: { print("🔄 다시 스캔 버튼 눌림") } + ) + .environmentObject(previewStore) + } + } +} + + diff --git a/StockMate/StockMate/app/feature/orders/data/OrderApi.swift b/StockMate/StockMate/app/feature/orders/data/OrderApi.swift index ef3d651..bfa026e 100644 --- a/StockMate/StockMate/app/feature/orders/data/OrderApi.swift +++ b/StockMate/StockMate/app/feature/orders/data/OrderApi.swift @@ -103,14 +103,6 @@ struct OrderItems: Encodable { let amount: Int } -// Response -//struct OrderCreateResponseData: Decodable { -// let orderId: Int -// let orderNumber: String -// let totalPrice: Int -// let orderStatus: String -//} - struct OrderCreateResponseData: Decodable { let orderId: Int let orderNumber: String @@ -124,7 +116,6 @@ struct ReceiveOrderRequest: Encodable { } - // MARK: - API Call enum OrderApi { diff --git a/StockMate/StockMate/app/feature/orders/ui/OrderDetailView.swift b/StockMate/StockMate/app/feature/orders/ui/OrderDetailView.swift index 0e4c5de..8ff3d78 100644 --- a/StockMate/StockMate/app/feature/orders/ui/OrderDetailView.swift +++ b/StockMate/StockMate/app/feature/orders/ui/OrderDetailView.swift @@ -203,8 +203,7 @@ struct OrderDetailView: View { // 오른쪽 버튼: 주문 상태에 따라 변경 if order.orderStatus == "ORDER_COMPLETED" || - order.orderStatus == "PAY_COMPLETED" || - order.orderStatus == "PENDING_APPROVAL" { + order.orderStatus == "PAY_COMPLETED" { // "주문취소" → 주문완료/결제완료/승인대기 Button(action: { Task { @@ -220,8 +219,7 @@ struct OrderDetailView: View { .cornerRadius(10) } - } else if order.orderStatus == "PENDING_RECEIVING" || - order.orderStatus == "DELIVERED" { + } else if order.orderStatus == "SHIPPING" { // "입고 하기" → 입고대기/배송완료 Button(action: { // TODO: 입고 처리 버튼 @@ -319,22 +317,30 @@ func formatDateOrDash(_ isoDate: String?) -> String { } func deliveryStep(for status: String) -> Int { - //6 -> 전체 회색 - //4 -> 전체 파란색 + // 6 -> 전체 회색 + // 4 -> 전체 파란색 switch status { case "ORDER_COMPLETED": return 0 // 주문 완료 - case "PAY_COMPLETED": return 0 // 결제 완료 - case "PENDING_APPROVAL": return 1 // 승인 대기 + + // 결제 후 결과에 따라 결제 실패 or 완료 case "FAILED": return 6 // 결제 실패 + case "PAY_COMPLETED": return 0 // 결제 완료 + + // 결제 완료 상태에서 지점이 주문 취소 + case "CANCELLED": return 6 // 주문 취소 + + // 본사에서 "결제 완료"에 대해서 주문을 반려 or 승인 + case "REJECTED": return 6 // 주문 반려 + case "APPROVAL_ORDER": return 1 // 주문 승인 + + // 창고관리자가 "주문 승인"에 대해서 송장(인보이스)를 뽑으면 출고 대기 case "PENDING_SHIPPING": return 2 // 출고 대기 + + // 창고관리자가 QR을 스캔하여 출고처리 하면 배송중 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 // 주문 취소 + + // 지점에서 QR을 스캔하여 입고 완료 처리 + case "RECEIVED": return 4 // 입고 완료 default: return 6 } } @@ -345,21 +351,31 @@ func formatDate(_ isoDate: String) -> String { return "\(comps[0])년 \(comps[1])월 \(comps[2])일" } +// 0: 초록, 1: 빨강, 2: 주황, 3: 노랑, 4: 파랑, 5: 보라 + func statusText(_ status: String) -> String { switch status { case "ORDER_COMPLETED": return "주문 완료" // 주문 완료 - case "PAY_COMPLETED": return "결제 완료" // 결제 완료 - case "PENDING_APPROVAL": return "승인 대기" // 승인대기 + + // 결제 후 결과에 따라 결제 실패 or 완료 case "FAILED": return "결제 실패" // 결제 실패 + case "PAY_COMPLETED": return "결제 완료" // 결제 완료 + + // 결제 완료 상태에서 지점이 주문 취소 + case "CANCELLED": return "주문 취소" // 주문 취소 + + // 본사에서 "결제 완료"에 대해서 주문을 반려 or 승인 + case "REJECTED": return "결제 실패" // 주문 반려 + case "APPROVAL_ORDER": return "주문 승인" // 주문 승인 + + // 창고관리자가 "주문 승인"에 대해서 송장(인보이스)를 뽑으면 출고 대기 case "PENDING_SHIPPING": return "출고 대기" // 출고 대기 + + // 창고관리자가 QR을 스캔하여 출고처리 하면 배송중 case "SHIPPING": return "배송중" // 배송중 - case "PENDING_RECEIVING": return "배송 완료" // 입고대기 - case "REJECTED": return "승인 반려" // 이론상 출고 반려 - case "DELIVERED": return "배송 완료" // 배송 완료 + + // 지점에서 QR을 스캔하여 입고 완료 처리 case "RECEIVED": return "입고 완료" // 입고 완료 - case "REFUNDED": return "환불 완료" // 환불 완료 - case "REFUND_REJECTED": return "환불 반려" // 환불 반려 - case "CANCELLED": return "주문 취소" // 주문 취소 default: return "알 수 없음" } } @@ -367,18 +383,19 @@ func statusText(_ status: String) -> String { 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 "PAY_COMPLETED": return .StatusGreen + + case "CANCELLED": return .Danger + case "REJECTED": return .Danger - case "DELIVERED": return .Secondary + case "APPROVAL_ORDER": return .Warning + + case "PENDING_SHIPPING": return .InvUse + case "SHIPPING": 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) } } @@ -386,18 +403,19 @@ func statusColor(_ status: String) -> Color { 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 "PAY_COMPLETED": return .StatusGreenBg + + case "CANCELLED": return .DangerBg + case "REJECTED": return .DangerBg - case "DELIVERED": return .LightBlue04 + case "APPROVAL_ORDER": return .WarningBg + + case "PENDING_SHIPPING": return .InvUseBg + case "SHIPPING": 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 f32ac74..3310f9a 100644 --- a/StockMate/StockMate/app/feature/orders/ui/OrderInfoView.swift +++ b/StockMate/StockMate/app/feature/orders/ui/OrderInfoView.swift @@ -27,16 +27,21 @@ struct OrderInfoView: View { @State private var specificDate = Date() @State private var requestMessage: String = "" - @State private var navigateToSuccessPage = false + // ✅ 모달 관련 상태 + @State private var showOrderSuccessModal = false + @State private var navigateToOrderDetail = false + @State private var navigateToHome = false private var destinationView: some View { - Group { - if let id = orderViewModel.createdOrderId { - OrderDetailView(orderId: id, orderViewModel: orderViewModel) - } else { - EmptyView() - } - } + Group { + if navigateToOrderDetail, let id = orderViewModel.createdOrderId { + OrderDetailView(orderId: id, orderViewModel: orderViewModel) + } else if navigateToHome { + HomeView() + } else { + EmptyView() + } + } } func formattedShippingDate() -> String { @@ -83,18 +88,78 @@ struct OrderInfoView: View { .onChange(of: orderViewModel.isOrderSuccess) { success in if success { Task { + // 1) 서버에 반영된 장바구니를 먼저 비운다 (await) await cartViewModel.clearCart() - navigateToSuccessPage = true + // 2) cart가 비워진 후에 모달을 띄운다 + // (모달을 띄우기 전에 createdOrderId는 orderViewModel에 이미 세팅되어 있어야 함) + showOrderSuccessModal = true } } } // ✅ 충전 bottom sheet 연결 .sheet(isPresented: $depositViewModel.showChargeSheet) { DepositChargeView(viewModel: depositViewModel) - .presentationDetents([.fraction(0.80)]) // 시트 높이 85% -// .presentationDragIndicator(.visible) + .presentationDetents([.fraction(0.80)]) // 시트 높이 80% } + // 모달 오버레이 (body 안) + .overlay { + if showOrderSuccessModal { + ZStack { + Color.black.opacity(0.4).ignoresSafeArea() + .onTapGesture { + // 배경 탭으로도 모달 닫을 수 있게 하려면 uncomment + // showOrderSuccessModal = false + } + + AlertModal( + icon: Image("SuccessIllust"), + title: "주문완료!", + message: "해당 부품 주문이 완료되었습니다.", + primaryButtonTitle: "주문상세", + primaryAction: { + // 1) 모달 닫기 + showOrderSuccessModal = false + + // 2) 네비게이션 트리거 -> OrderDetail 로 이동 + // orderViewModel.createdOrderId 가 있어야 함 + navigateToOrderDetail = true + }, + secondaryButtonTitle: "홈으로", + secondaryAction: { + // 모달 닫고 홈으로 + showOrderSuccessModal = false + navigateToHome = true + }, + buttonLayout: .vertical + ) + .transition(.scale) + .padding(.horizontal, 20) + } + .animation(.easeInOut, value: showOrderSuccessModal) + } + } + + // 네비게이션 실행을 위한 숨은 링크 (body 밖 어디든) + .background( + Group { + // OrderDetail 우선 (OrderDetail은 createdOrderId 를 필요로 함) + NavigationLink(destination: + Group { + if let id = orderViewModel.createdOrderId { + OrderDetailView(orderId: id, orderViewModel: orderViewModel) + } else { + EmptyView() + } + }, + isActive: $navigateToOrderDetail) { + EmptyView() + } + NavigationLink(destination: HomeView(), isActive: $navigateToHome) { + EmptyView() + } + } + ) } @@ -207,9 +272,6 @@ extension OrderInfoView { .font(.system(size: 26, weight: .bold)) .foregroundColor(Color.white) } - - // Text("₩\(cartViewModel.depositBalance?.formatted() ?? "0")") - // .font(.system(size: 22, weight: .bold)) } } @@ -238,7 +300,6 @@ extension OrderInfoView { .frame(maxWidth: .infinity) } - private var shippingDateSection: some View { VStack(alignment: .leading) { Text("배송 요청일") @@ -328,10 +389,6 @@ extension OrderInfoView { .frame(height: 70) .background(Color.Primary) } - - NavigationLink(destination: destinationView, isActive: $navigateToSuccessPage) { - EmptyView() - } } } } @@ -346,7 +403,6 @@ struct RadioButtonRow: View { Image(systemName: selected ? "circle.inset.filled" : "circle") .foregroundColor(selected ? .Primary : .gray) Text(title) - // Spacer() } .onTapGesture { action() } } diff --git a/StockMate/StockMate/app/feature/orders/ui/OrderListView.swift b/StockMate/StockMate/app/feature/orders/ui/OrderListView.swift index a6e9aa5..7cd04b1 100644 --- a/StockMate/StockMate/app/feature/orders/ui/OrderListView.swift +++ b/StockMate/StockMate/app/feature/orders/ui/OrderListView.swift @@ -27,9 +27,6 @@ struct OrderListView: View { .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) ?? "" } diff --git a/StockMate/StockMate/app/feature/orders/ui/OrderView.swift b/StockMate/StockMate/app/feature/orders/ui/OrderView.swift index c238a30..3fa5db5 100644 --- a/StockMate/StockMate/app/feature/orders/ui/OrderView.swift +++ b/StockMate/StockMate/app/feature/orders/ui/OrderView.swift @@ -31,10 +31,10 @@ struct OrderView: View { // 🔍 검색창 NavigationLink(destination: - OrderRequestSearchView( - cartViewModel: cartViewModel - //inventoryViewModel: inventoryViewModel - ) + OrderRequestSearchView( + cartViewModel: cartViewModel + //inventoryViewModel: inventoryViewModel + ) ) { HStack { Image(systemName: "magnifyingglass") @@ -49,7 +49,7 @@ struct OrderView: View { .cornerRadius(9999) .overlay( RoundedRectangle(cornerRadius: 9999) - .stroke(Color.gray.opacity(0.4), lineWidth: 1) + .stroke(Color.GrayMordern400, lineWidth: 1) ) .padding(.horizontal) } @@ -126,7 +126,3 @@ struct OrderView: View { } } -// -//#Preview { -// OrderView() -//} diff --git a/StockMate/StockMate/app/feature/orders/ui/ReceiptView.swift b/StockMate/StockMate/app/feature/orders/ui/ReceiptView.swift index 9e690f1..91b100a 100644 --- a/StockMate/StockMate/app/feature/orders/ui/ReceiptView.swift +++ b/StockMate/StockMate/app/feature/orders/ui/ReceiptView.swift @@ -209,10 +209,27 @@ struct ReceiptView: View { // } //} +//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) +//} func formattedDate(_ timestamp: String) -> String { let inputFormatter = DateFormatter() inputFormatter.locale = Locale(identifier: "ko_KR") - inputFormatter.timeZone = TimeZone(abbreviation: "UTC") + inputFormatter.timeZone = TimeZone.current // ✅ 실제 한국 시간 기준 inputFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS" guard let date = inputFormatter.date(from: timestamp) else { @@ -227,6 +244,7 @@ func formattedDate(_ timestamp: String) -> String { return outputFormatter.string(from: date) } + func formattedApprovalNumber(_ timestamp: String) -> String { let inputFormatter = DateFormatter() inputFormatter.locale = Locale(identifier: "ko_KR") diff --git a/StockMate/StockMate/app/feature/parts/data/PartApi.swift b/StockMate/StockMate/app/feature/parts/data/PartApi.swift index 9c5070d..e1d4050 100644 --- a/StockMate/StockMate/app/feature/parts/data/PartApi.swift +++ b/StockMate/StockMate/app/feature/parts/data/PartApi.swift @@ -9,18 +9,50 @@ import Foundation import Alamofire -// ✅ 요청 모델 +// ✅ 요청 모델 (partCode → partId 로 변경) struct ReleaseItemRequest: Encodable { - let partCode: String + let partId: Int let quantity: Int } +struct PartDetailResponse: Decodable, Identifiable { + 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 + let code: String + let location: String + let cost: Int +} + +// 사용처리 임시 값 +struct PartDetail: Identifiable { + let id: Int + let price: Int + let image: String + let trim: String + let model: String + let korName: String + let categoryName: String + var quantity: Int = 1 +} + + // ✅ API 정의 enum PartApi { static func releaseParts(items: [ReleaseItemRequest]) -> DataRequest { let url = ApiClient.baseURL + "api/v1/store/release" let body: [String: Any] = [ - "items": items.map { ["partCode": $0.partCode, "quantity": $0.quantity] } +// "items": items.map { ["partId": $0.partId, "quantity": $0.quantity] } + "items": items.map { ["partId": $0.partId, "quantity": $0.quantity] } + ] return ApiClient.shared.request( url, @@ -29,4 +61,17 @@ enum PartApi { encoding: JSONEncoding.default ) } + + // ✅ 부품 상세 조회 API + static func fetchPartDetail(partIds: [Int]) -> DataRequest { + let url = ApiClient.baseURL + "api/v1/parts/detail" + + // ✅ 요청 본문은 단순 배열 형태이므로 parameters 사용 X, 직접 body에 encode + return ApiClient.shared.request( + url, + method: .post, + parameters: partIds, + encoder: JSONParameterEncoder.default + ) + } } diff --git a/StockMate/StockMate/app/feature/parts/data/PartRepositoryImpl.swift b/StockMate/StockMate/app/feature/parts/data/PartRepositoryImpl.swift index 7ac1c08..cc577f5 100644 --- a/StockMate/StockMate/app/feature/parts/data/PartRepositoryImpl.swift +++ b/StockMate/StockMate/app/feature/parts/data/PartRepositoryImpl.swift @@ -13,4 +13,10 @@ final class PartRepositoryImpl: PartRepositoryProtocol { let dataReq = PartApi.releaseParts(items: items) return await safeApi(dataReq, decodeTo: ApiResponse.self) } + + func fetchPartDetail(partIds: [Int]) async -> AppResult> { + let dataReq = PartApi.fetchPartDetail(partIds: partIds) + return await safeApi(dataReq, decodeTo: ApiResponse<[PartDetailResponse]>.self) + } + } diff --git a/StockMate/StockMate/app/feature/parts/data/PartStore.swift b/StockMate/StockMate/app/feature/parts/data/PartStore.swift new file mode 100644 index 0000000..48436dc --- /dev/null +++ b/StockMate/StockMate/app/feature/parts/data/PartStore.swift @@ -0,0 +1,54 @@ +// +// PartStore.swift +// StockMate +// +// Created by Admin on 11/3/25. +// + +import Foundation + +@MainActor +final class PartStore: ObservableObject { + @Published var parts: [PartDetail] = [] + + func addPart(_ part: PartDetail) { + if let index = parts.firstIndex(where: { $0.id == part.id }) { + parts[index].quantity += 1 // 이미 존재하면 수량만 +1 + } else { + var newPart = part + newPart.quantity = 1 + parts.append(newPart) + } + } + + func clear() { + parts.removeAll() + } + + // ✅ 수량 변경용 메서드 추가 + func increaseQuantity(for part: PartDetail) { + if let index = parts.firstIndex(where: { $0.id == part.id }) { + parts[index].quantity += 1 + objectWillChange.send() // 수동 갱신 트리거 + } + } + + func decreaseQuantity(for part: PartDetail) { + if let index = parts.firstIndex(where: { $0.id == part.id }), + parts[index].quantity > 1 { + parts[index].quantity -= 1 + objectWillChange.send() + } + } + + func decreaseQuantityOrRemove(for part: PartDetail) { + if let index = parts.firstIndex(where: { $0.id == part.id }) { + if parts[index].quantity > 1 { + parts[index].quantity -= 1 + } else { + parts.remove(at: index) + } + objectWillChange.send() + } + } +} diff --git a/StockMate/StockMate/app/feature/parts/domain/PartRepositoryProtocol.swift b/StockMate/StockMate/app/feature/parts/domain/PartRepositoryProtocol.swift index f3a939e..14f73c1 100644 --- a/StockMate/StockMate/app/feature/parts/domain/PartRepositoryProtocol.swift +++ b/StockMate/StockMate/app/feature/parts/domain/PartRepositoryProtocol.swift @@ -9,4 +9,5 @@ import Foundation protocol PartRepositoryProtocol { func releaseParts(items: [ReleaseItemRequest]) async -> AppResult> + func fetchPartDetail(partIds: [Int]) async -> AppResult> } diff --git a/StockMate/StockMate/app/feature/parts/ui/QRScannerView.swift b/StockMate/StockMate/app/feature/parts/ui/QRScannerView.swift index 761468f..c4a109a 100644 --- a/StockMate/StockMate/app/feature/parts/ui/QRScannerView.swift +++ b/StockMate/StockMate/app/feature/parts/ui/QRScannerView.swift @@ -9,6 +9,7 @@ import SwiftUI struct QRScannerView: UIViewControllerRepresentable { @Binding var scannedCode: String? + var isActive: Bool = true // ✅ 추가: 카메라 활성화 상태 func makeUIViewController(context: Context) -> QRScannerViewController { let controller = QRScannerViewController() @@ -16,8 +17,22 @@ struct QRScannerView: UIViewControllerRepresentable { return controller } - func updateUIViewController(_ uiViewController: QRScannerViewController, context: Context) {} - +// func updateUIViewController(_ uiViewController: QRScannerViewController, context: Context) {} +// func updateUIViewController(_ uiViewController: QRScannerViewController, context: Context) { +// // ✅ scannedCode가 nil이면 다시 스캔 시작 +// if scannedCode == nil { +// uiViewController.startScanning() +// } +// } + + func updateUIViewController(_ uiViewController: QRScannerViewController, context: Context) { + if isActive { + uiViewController.startSession() // ✅ 바텀시트 닫혔을 때 다시 스캔 시작 + } else { + uiViewController.stopSession() // ✅ 바텀시트 열렸을 때 스캔 일시정지 + } + } + func makeCoordinator() -> Coordinator { Coordinator(self) } diff --git a/StockMate/StockMate/app/feature/parts/ui/QRScannerViewController.swift b/StockMate/StockMate/app/feature/parts/ui/QRScannerViewController.swift index f338a55..626dab2 100644 --- a/StockMate/StockMate/app/feature/parts/ui/QRScannerViewController.swift +++ b/StockMate/StockMate/app/feature/parts/ui/QRScannerViewController.swift @@ -51,13 +51,45 @@ final class QRScannerViewController: UIViewController, AVCaptureMetadataOutputOb previewLayer.videoGravity = .resizeAspectFill view.layer.addSublayer(previewLayer) + + startScanning() // ⚠️ 백그라운드에서 실행 - DispatchQueue.global(qos: .userInitiated).async { - self.captureSession.startRunning() +// DispatchQueue.global(qos: .userInitiated).async { +// self.captureSession.startRunning() +// } + } + + // ✅ 스캔 재시작/중단 함수 추가 + func startScanning() { + guard captureSession != nil else { return } + if !captureSession.isRunning { + DispatchQueue.global(qos: .userInitiated).async { + self.captureSession.startRunning() + } } } + func stopScanning() { + guard captureSession != nil else { return } + if captureSession.isRunning { + captureSession.stopRunning() + } + } + + func startSession() { + if !captureSession.isRunning { + captureSession.startRunning() + } + } + func stopSession() { + if captureSession.isRunning { + captureSession.stopRunning() + } + } + + + // ✅ QR 감지 시 호출 func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { if let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject, let stringValue = metadataObject.stringValue { diff --git a/StockMate/StockMate/app/feature/parts/viewmodel/PartViewModel.swift b/StockMate/StockMate/app/feature/parts/viewmodel/PartViewModel.swift index fc3fecd..394dc62 100644 --- a/StockMate/StockMate/app/feature/parts/viewmodel/PartViewModel.swift +++ b/StockMate/StockMate/app/feature/parts/viewmodel/PartViewModel.swift @@ -14,12 +14,17 @@ final class PartViewModel: ObservableObject { @Published var message: String = "" @Published var shouldGoToLogin = false + + @Published var partDetails: [PartDetailResponse] = [] + + @Published var quantities: [Int: Int] = [:] // partId별 수량 관리 + private let repo: PartRepositoryProtocol init(repo: PartRepositoryProtocol = PartRepositoryImpl()) { self.repo = repo } - + func releaseParts(items: [ReleaseItemRequest]) async -> AppResult { isLoading = true defer { isLoading = false } @@ -41,4 +46,26 @@ final class PartViewModel: ObservableObject { return .failure(err) } } -} \ No newline at end of file + + func fetchPartDetail(partIds: Int) async { + isLoading = true + defer { isLoading = false } + + let result = await repo.fetchPartDetail(partIds: [partIds]) + switch result { + case .success(let apiResp): + if apiResp.success, let data = apiResp.data { + partDetails = data + print("✅ 부품 상세 조회 성공:", data) + } else { + message = apiResp.message + print("⚠️ 서버 응답 실패:", apiResp.message) + } + case .failure(let err): + message = err.message + print("❌ 네트워크 오류:", err) + } + } + + +} diff --git a/StockMate/StockMate/app/feature/payment/data/PaymentApi.swift b/StockMate/StockMate/app/feature/payment/data/PaymentApi.swift index 53346c3..c2db3ef 100644 --- a/StockMate/StockMate/app/feature/payment/data/PaymentApi.swift +++ b/StockMate/StockMate/app/feature/payment/data/PaymentApi.swift @@ -16,6 +16,19 @@ struct PaymentAmount: Decodable { let userId: Int } +// ✅ 월별 소비 내역 구조체 +struct MonthlySpending: Decodable, Identifiable { + var id: String { month } // 리스트에서 사용하기 편하게 + let month: String + let totalAmount: Int +} + +struct CategorySpending: Decodable, Identifiable { + var id: String { categoryName } + let categoryName: String + let totalAmount: Int +} + enum PaymentApi { // 예치금 조회 @@ -37,4 +50,17 @@ enum PaymentApi { encoding: URLEncoding.queryString ) } + + // ✅ 최근 5개월 소비 내역 조회 + static func getMonthlySpending() -> DataRequest { + let url = ApiClient.baseURL + "api/v1/payment/monthly-spending" + return ApiClient.shared.request(url, method: .get) + } + + + // ✅ 지난달 카테고리별 지출금액 조회 + static func getCategorySpending() -> DataRequest { + let url = ApiClient.baseURL + "api/v1/order/category-spend" + return ApiClient.shared.request(url, method: .get) + } } diff --git a/StockMate/StockMate/app/feature/payment/data/PaymentRepositoryImpl.swift b/StockMate/StockMate/app/feature/payment/data/PaymentRepositoryImpl.swift index 625b887..6024082 100644 --- a/StockMate/StockMate/app/feature/payment/data/PaymentRepositoryImpl.swift +++ b/StockMate/StockMate/app/feature/payment/data/PaymentRepositoryImpl.swift @@ -45,4 +45,37 @@ final class PaymentRepositoryImpl: PaymentRepositoryProtocol { return .failure(error) } } + + // ✅ 최근 5개월 소비 내역 조회 + func fetchMonthlySpending() async -> AppResult<[MonthlySpending]> { + let request = PaymentApi.getMonthlySpending() + let result = await safeApi(request, decodeTo: ApiResponse<[MonthlySpending]>.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 fetchCategorySpending() async -> AppResult<[CategorySpending]> { + let request = PaymentApi.getCategorySpending() + let result = await safeApi(request, decodeTo: ApiResponse<[CategorySpending]>.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) + } + } + } diff --git a/StockMate/StockMate/app/feature/payment/domain/PaymentRepositoryProtocol.swift b/StockMate/StockMate/app/feature/payment/domain/PaymentRepositoryProtocol.swift index 150ab26..fe22926 100644 --- a/StockMate/StockMate/app/feature/payment/domain/PaymentRepositoryProtocol.swift +++ b/StockMate/StockMate/app/feature/payment/domain/PaymentRepositoryProtocol.swift @@ -11,4 +11,11 @@ import Alamofire protocol PaymentRepositoryProtocol { func fetchDepositAmount() async -> AppResult func chargeDeposit(amount: Int) async -> AppResult + + // ✅ 최근 5개월 소비 내역 조회 + func fetchMonthlySpending() async -> AppResult<[MonthlySpending]> + + // 지난달 카테고리별 지출 + func fetchCategorySpending() async -> AppResult<[CategorySpending]> + } diff --git a/StockMate/StockMate/app/feature/payment/viewmodel/DashboardViewModel.swift b/StockMate/StockMate/app/feature/payment/viewmodel/DashboardViewModel.swift new file mode 100644 index 0000000..8ef7136 --- /dev/null +++ b/StockMate/StockMate/app/feature/payment/viewmodel/DashboardViewModel.swift @@ -0,0 +1,69 @@ +// +// DashboardViewModel.swift +// StockMate +// +// Created by Admin on 11/2/25. +// + +import Foundation + +@MainActor +final class DashboardViewModel: ObservableObject { + private let repo: PaymentRepositoryProtocol = PaymentRepositoryImpl() + + @Published var monthlySpendings: [MonthlySpending] = [] + @Published var categorySpendings: [CategorySpending] = [] + + @Published var isLoading = false + + /// ✅ 최근 5개월 소비 내역 조회 + func fetchMonthlySpending() async { + isLoading = true + let result = await repo.fetchMonthlySpending() + isLoading = false + + switch result { + case .success(let data): + monthlySpendings = data + case .failure(let err): + print("❌ 월별 소비 내역 조회 실패:", err.message) + } + } + + // ✅ 지난달 카테고리별 지출 금액 조회 + func fetchCategorySpending() async { + isLoading = true + let result = await repo.fetchCategorySpending() + isLoading = false + + switch result { + case .success(let data): + categorySpendings = data + case .failure(let err): + print("❌ 카테고리별 지출 금액 조회 실패:", err.message) + } + } + + /// ✅ 막대그래프 비율 계산 + var spendingRatios: [CGFloat] { + guard let max = monthlySpendings.map({ $0.totalAmount }).max(), max > 0 else { return [] } + return monthlySpendings.map { CGFloat($0.totalAmount) / CGFloat(max) } + } + + /// ✅ 월 라벨 (예: "10월", "11월") + var monthLabels: [String] { + monthlySpendings.map { month in + // "2025-10" → "10월" + if month.month.count >= 7 { + let suffix = String(month.month.suffix(2)) + if let monthInt = Int(suffix) { + return "\(monthInt)월" + } else { + return suffix + "월" + } + } else { + return month.month + } + } + } +} diff --git a/StockMate/StockMate/app/feature/user/ui/ProfileCircleView.swift b/StockMate/StockMate/app/feature/user/ui/ProfileCircleView.swift index 16fbe17..2e1a97d 100644 --- a/StockMate/StockMate/app/feature/user/ui/ProfileCircleView.swift +++ b/StockMate/StockMate/app/feature/user/ui/ProfileCircleView.swift @@ -18,12 +18,16 @@ struct ProfileCircleView: View { } var body: some View { - Text(initials) - .font(.headline) - .foregroundColor(Color(hex: "#374EAF")) - .frame(width: size, height: size) - .background(Color(hex: "#DCE0F1")) // 고정 색상 - .clipShape(Circle()) + GeometryReader { geometry in + let minSide = min(geometry.size.width, geometry.size.height) + Text(initials) + .font(.system(size: minSide * 0.35, weight: .regular)) // ✅ 내부 크기 비례 + .foregroundColor(Color(hex: "#374EAF")) + .frame(width: geometry.size.width, height: geometry.size.height) + .background(Color(hex: "#DCE0F1")) + .clipShape(Circle()) + } + .frame(width: size, height: size) } } diff --git a/StockMate/StockMate/app/feature/user/ui/ProfileView.swift b/StockMate/StockMate/app/feature/user/ui/ProfileView.swift index 92fdf6a..43d8ff3 100644 --- a/StockMate/StockMate/app/feature/user/ui/ProfileView.swift +++ b/StockMate/StockMate/app/feature/user/ui/ProfileView.swift @@ -9,9 +9,11 @@ import SwiftUI struct ProfileView: View { @StateObject private var userViewModel = UserViewModel() + @EnvironmentObject var authViewModel: AuthViewModel // 🔹 전역 Auth 상태 참조 + @State private var showLogoutModal = false // 🔹 로그아웃 모달 상태 var body: some View { -// NavigationStack { + ZStack{ VStack(alignment: .leading, spacing: 24) { // MARK: - Profile Header @@ -38,34 +40,19 @@ struct ProfileView: View { // 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: "user", title: "프로필 확인", destination: UserProfileView()) + SettingRow(icon: "notification", title: "알림") + SettingNavigationRow(icon: "receipt", title: "예치금 히스토리", destination: TransactionTypeListView()) +// SettingNavigationRow(icon: "receipt", title: "예치금 히스토리", destination: PaymentTransactionView()) 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) + // SettingRow(icon: "logout", title: "로그아웃") + // 🔹 로그아웃 버튼 + Button { + showLogoutModal = true + } label: { + SettingRow(icon: "logout", title: "로그아웃") + } } .padding(3) .background(Color.Light) @@ -80,7 +67,33 @@ struct ProfileView: View { .onAppear { Task { await userViewModel.loadUserInfo() } } -// } + + // 🔹 AlertModal (ZStack 위에 오버레이로 표시) + if showLogoutModal { + Color.black.opacity(0.3) + .ignoresSafeArea() + .transition(.opacity) + + AlertModal( + title: "로그아웃", + message: "정말 로그아웃 하시겠습니까?", + primaryButtonTitle: "로그아웃", + primaryAction: { + authViewModel.logout() + showLogoutModal = false + }, + secondaryButtonTitle: "취소", + secondaryAction: { + showLogoutModal = false + }, + buttonLayout: .horizontal + ) + .transition(.scale) + .zIndex(1) + } + + } + .animation(.easeInOut, value: showLogoutModal) } } @@ -93,7 +106,7 @@ struct SettingRow: View { var body: some View { HStack { - Image(systemName: icon) + Image(icon) .font(.system(size: 18)) .foregroundColor(iconColor) .frame(width: 24) @@ -124,7 +137,7 @@ struct SettingNavigationRow: View { var body: some View { NavigationLink(destination: destination) { HStack { - Image(systemName: icon) + Image(icon) .font(.system(size: 18)) .foregroundColor(iconColor) .frame(width: 24) diff --git a/StockMate/StockMate/app/feature/user/ui/UserProfileView.swift b/StockMate/StockMate/app/feature/user/ui/UserProfileView.swift new file mode 100644 index 0000000..9094fbe --- /dev/null +++ b/StockMate/StockMate/app/feature/user/ui/UserProfileView.swift @@ -0,0 +1,47 @@ +// +// UserProfileView.swift +// StockMate +// +// Created by Admin on 11/5/25. +// + +import SwiftUI + +struct UserProfileView: View { + @StateObject private var userViewModel = UserViewModel() + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + HStack(spacing: 16) { + Spacer() + ProfileCircleView(name: userViewModel.userInfo?.owner ?? "사용자", size: 103) + Spacer() + } + .padding(.vertical, 32) + + VStack(spacing: 9) { + ProfileFieldView(label: "대표자", value: userViewModel.userInfo?.owner ?? "이름 없음") + ProfileFieldView(label: "이메일", value: userViewModel.userInfo?.email ?? "이메일 없음") + ProfileFieldView(label: "지점", value: userViewModel.userInfo?.storeName ?? "지점명 없음") + ProfileFieldView(label: "주소", value: userViewModel.userInfo?.address ?? "주소 없음") + ProfileFieldView(label: "사업자등록번호", value: userViewModel.userInfo?.businessNumber ?? "사업자등록번호 없음") + + Spacer() + + } + .padding(3) + .cornerRadius(12) + .padding(.horizontal) + } + .navigationTitle("프로필 확인") + .background(Color.Light) + .navigationBarTitleDisplayMode(.inline) + .onAppear { + Task { await userViewModel.loadUserInfo() } + } + } +} + +#Preview { + UserProfileView() +} diff --git a/StockMate/StockMate/app/navigation/AppNavHost.swift b/StockMate/StockMate/app/navigation/AppNavHost.swift index f2c5bca..ffb9779 100644 --- a/StockMate/StockMate/app/navigation/AppNavHost.swift +++ b/StockMate/StockMate/app/navigation/AppNavHost.swift @@ -9,6 +9,8 @@ import SwiftUI struct AppNavHost: View { @EnvironmentObject var authViewModel: AuthViewModel + @StateObject private var partStore = PartStore() + var body: some View { NavigationStack { @@ -22,6 +24,8 @@ struct AppNavHost: View { RegisterView() case .authenticated: MainTabView() + .environmentObject(authViewModel) + .environmentObject(partStore) } } } diff --git a/StockMate/StockMate/app/navigation/MainTabView.swift b/StockMate/StockMate/app/navigation/MainTabView.swift index b5bf61c..acc21ef 100644 --- a/StockMate/StockMate/app/navigation/MainTabView.swift +++ b/StockMate/StockMate/app/navigation/MainTabView.swift @@ -22,7 +22,12 @@ struct MainTabView: View { case 1: NavigationStack{ OrderView(cartViewModel: cartVM) } //, inventoryViewModel: inventoryVM) } // case 1: NavigationStack{ OrderView() } case 2: - NavigationStack { InventoryView(selectedTab: $selectedTab, tabTappedTrigger: $tabTappedTrigger) } + InventoryView( + selectedTab: $selectedTab, + tabTappedTrigger: $tabTappedTrigger + ) + +// NavigationStack { InventoryView(selectedTab: $selectedTab, tabTappedTrigger: $tabTappedTrigger) } // case 3: NavigationStack{ ContentView() } // case 3: NavigationStack{ ReceiptView() } case 3: NavigationStack{ ProfileView() } diff --git a/StockMate/StockMate/resources/Assets.xcassets/SuccessIllust.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/SuccessIllust.imageset/Contents.json new file mode 100644 index 0000000..4890788 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/SuccessIllust.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "SuccessIllust.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/SuccessIllust.imageset/SuccessIllust.svg b/StockMate/StockMate/resources/Assets.xcassets/SuccessIllust.imageset/SuccessIllust.svg new file mode 100644 index 0000000..60b221d --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/SuccessIllust.imageset/SuccessIllust.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/StockMate/StockMate/resources/Assets.xcassets/add_shopping_cart.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/add_shopping_cart.imageset/Contents.json new file mode 100644 index 0000000..566c0a1 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/add_shopping_cart.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "add_shopping_cart@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "add_shopping_cart@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "add_shopping_cart@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/add_shopping_cart.imageset/add_shopping_cart@1x.png b/StockMate/StockMate/resources/Assets.xcassets/add_shopping_cart.imageset/add_shopping_cart@1x.png new file mode 100644 index 0000000..a9d1e53 Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/add_shopping_cart.imageset/add_shopping_cart@1x.png differ diff --git a/StockMate/StockMate/resources/Assets.xcassets/add_shopping_cart.imageset/add_shopping_cart@2x.png b/StockMate/StockMate/resources/Assets.xcassets/add_shopping_cart.imageset/add_shopping_cart@2x.png new file mode 100644 index 0000000..c10a2d1 Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/add_shopping_cart.imageset/add_shopping_cart@2x.png differ diff --git a/StockMate/StockMate/resources/Assets.xcassets/add_shopping_cart.imageset/add_shopping_cart@3x.png b/StockMate/StockMate/resources/Assets.xcassets/add_shopping_cart.imageset/add_shopping_cart@3x.png new file mode 100644 index 0000000..083f568 Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/add_shopping_cart.imageset/add_shopping_cart@3x.png differ diff --git a/StockMate/StockMate/resources/Assets.xcassets/bag.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/bag.imageset/Contents.json new file mode 100644 index 0000000..66aacdc --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/bag.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "bag.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/bag.imageset/bag.svg b/StockMate/StockMate/resources/Assets.xcassets/bag.imageset/bag.svg new file mode 100644 index 0000000..efc5703 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/bag.imageset/bag.svg @@ -0,0 +1,3 @@ + + + diff --git a/StockMate/StockMate/resources/Assets.xcassets/exchange.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/exchange.imageset/Contents.json new file mode 100644 index 0000000..b7cfc19 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/exchange.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "exchange.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/exchange.imageset/exchange.svg b/StockMate/StockMate/resources/Assets.xcassets/exchange.imageset/exchange.svg new file mode 100644 index 0000000..84b5196 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/exchange.imageset/exchange.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/StockMate/StockMate/resources/Assets.xcassets/flag.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/flag.imageset/Contents.json new file mode 100644 index 0000000..02f005f --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/flag.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "flag.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/flag.imageset/flag.svg b/StockMate/StockMate/resources/Assets.xcassets/flag.imageset/flag.svg new file mode 100644 index 0000000..3c142bb --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/flag.imageset/flag.svg @@ -0,0 +1,3 @@ + + + diff --git a/StockMate/StockMate/resources/Assets.xcassets/lock.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/lock.imageset/Contents.json new file mode 100644 index 0000000..72cfcb5 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/lock.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "lock.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/lock.imageset/lock.svg b/StockMate/StockMate/resources/Assets.xcassets/lock.imageset/lock.svg new file mode 100644 index 0000000..fe83ff3 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/lock.imageset/lock.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/StockMate/StockMate/resources/Assets.xcassets/logout.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/logout.imageset/Contents.json new file mode 100644 index 0000000..bc4d524 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/logout.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "logout.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/logout.imageset/logout.svg b/StockMate/StockMate/resources/Assets.xcassets/logout.imageset/logout.svg new file mode 100644 index 0000000..039492d --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/logout.imageset/logout.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/StockMate/StockMate/resources/Assets.xcassets/notification 1.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/notification 1.imageset/Contents.json new file mode 100644 index 0000000..5dabea1 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/notification 1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "notification.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/notification 1.imageset/notification.svg b/StockMate/StockMate/resources/Assets.xcassets/notification 1.imageset/notification.svg new file mode 100644 index 0000000..6ba310d --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/notification 1.imageset/notification.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/StockMate/StockMate/resources/Assets.xcassets/receipt.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/receipt.imageset/Contents.json new file mode 100644 index 0000000..59421d0 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/receipt.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "receipt.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/receipt.imageset/receipt.svg b/StockMate/StockMate/resources/Assets.xcassets/receipt.imageset/receipt.svg new file mode 100644 index 0000000..6c77a8c --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/receipt.imageset/receipt.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/StockMate/StockMate/resources/Assets.xcassets/shoppingcart.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/shoppingcart.imageset/Contents.json new file mode 100644 index 0000000..b5a8e22 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/shoppingcart.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "shoppingcart.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/shoppingcart.imageset/shoppingcart.svg b/StockMate/StockMate/resources/Assets.xcassets/shoppingcart.imageset/shoppingcart.svg new file mode 100644 index 0000000..bc001c9 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/shoppingcart.imageset/shoppingcart.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/StockMate/StockMate/resources/Assets.xcassets/user.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/user.imageset/Contents.json new file mode 100644 index 0000000..8ac482c --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/user.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "user.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/user.imageset/user.svg b/StockMate/StockMate/resources/Assets.xcassets/user.imageset/user.svg new file mode 100644 index 0000000..3f51a37 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/user.imageset/user.svg @@ -0,0 +1,4 @@ + + + + diff --git a/StockMate/StockMate/resources/Color.swift b/StockMate/StockMate/resources/Color.swift index 94172ee..a5baa30 100644 --- a/StockMate/StockMate/resources/Color.swift +++ b/StockMate/StockMate/resources/Color.swift @@ -33,6 +33,10 @@ extension Color { static let Dark = Color(hex: "#04150C") static let Light = Color(hex: "#F7F7F7") + static let GrayMordern300 = Color(hex: "#CDD5DF") + static let GrayMordern400 = Color(hex: "#9AA4B2") + + // Text static let textBlack = Color(hex: "#152C07") @@ -81,8 +85,8 @@ extension Color { //DFF6FC // Home Status - static let Hstatus1 = Color(hex: "#DFF6FC") - static let Hstatus2 = Color(hex: "#DBDFF3") + static let Hstatus1 = Color(hex: "#08C2EB") + static let Hstatus2 = Color(hex: "#1F40AE") static let Hstatus3 = Color(hex: "#EB5032") static let Hstatus4 = Color(hex: "#8DDB55") static let Hstatus5 = Color(hex: "#8892A2")