diff --git a/StockMate/.DS_Store b/StockMate/.DS_Store index 96d21b8..7939a51 100644 Binary files a/StockMate/.DS_Store and b/StockMate/.DS_Store differ diff --git a/StockMate/StockMate.xcodeproj/project.pbxproj b/StockMate/StockMate.xcodeproj/project.pbxproj index 788b909..97cbaa9 100644 --- a/StockMate/StockMate.xcodeproj/project.pbxproj +++ b/StockMate/StockMate.xcodeproj/project.pbxproj @@ -271,6 +271,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 35TSG7VB2B; @@ -290,6 +291,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.hyuna.StockMate; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -301,6 +303,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 35TSG7VB2B; @@ -320,6 +323,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.hyuna.StockMate; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/StockMate/StockMate/app/StockMateApp.swift b/StockMate/StockMate/app/StockMateApp.swift index d9d9848..da169f7 100644 --- a/StockMate/StockMate/app/StockMateApp.swift +++ b/StockMate/StockMate/app/StockMateApp.swift @@ -10,11 +10,25 @@ import SwiftUI @main struct StockMateApp: App { @StateObject private var authViewModel = AuthViewModel() + @State private var isLoading = true // 인트로 상태 var body: some Scene { WindowGroup { - AppNavHost() - .environmentObject(authViewModel) + + Group { + if isLoading { + IntroView() // 로고만 보여주는 화면 + } else { + AppNavHost() + .environmentObject(authViewModel) + } + } + .onAppear { + Task { + try await Task.sleep(nanoseconds: 1_500_000_000) // 1.5초 + isLoading = false + } + } } } } diff --git a/StockMate/StockMate/app/core/KakaoPostcode/KakaoZipCodeVC.swift b/StockMate/StockMate/app/core/KakaoPostcode/KakaoZipCodeVC.swift index a4fecb1..04e3245 100644 --- a/StockMate/StockMate/app/core/KakaoPostcode/KakaoZipCodeVC.swift +++ b/StockMate/StockMate/app/core/KakaoPostcode/KakaoZipCodeVC.swift @@ -14,7 +14,7 @@ class KakaoZipCodeVC: UIViewController { // MARK: - Properties var webView: WKWebView? let indicator = UIActivityIndicatorView(style: .medium) - var onAddressSelected: ((String) -> Void)? // ✅ SwiftUI로 결과 전달용 콜백 + var onAddressSelected: ((String) -> Void)? // SwiftUI로 결과 전달용 콜백 override func viewDidLoad() { super.viewDidLoad() @@ -64,7 +64,7 @@ extension KakaoZipCodeVC: WKScriptMessageHandler { didReceive message: WKScriptMessage) { guard let data = message.body as? [String: Any] else { return } let address = data["roadAddress"] as? String ?? "" - onAddressSelected?(address) // ✅ SwiftUI로 전달 + onAddressSelected?(address) // SwiftUI로 전달 dismiss(animated: true) } } diff --git a/StockMate/StockMate/app/core/KakaoPostcode/KakaoZipCodeView.swift b/StockMate/StockMate/app/core/KakaoPostcode/KakaoZipCodeView.swift index fa69a87..2efd44f 100644 --- a/StockMate/StockMate/app/core/KakaoPostcode/KakaoZipCodeView.swift +++ b/StockMate/StockMate/app/core/KakaoPostcode/KakaoZipCodeView.swift @@ -9,16 +9,23 @@ import SwiftUI import WebKit +// SwiftUI에서 UIKit 기반의 Kakao 우편번호 검색 화면을 사용하기 위한 래퍼 뷰 +// UIViewControllerRepresentable을 통해 KakaoZipCodeVC를 SwiftUI에 통합 struct KakaoZipCodeView: UIViewControllerRepresentable { + // 선택된 주소 값을 SwiftUI와 바인딩 @Binding var address: String + // KakaoZipCodeVC 생성 및 초기 설정 func makeUIViewController(context: Context) -> KakaoZipCodeVC { let vc = KakaoZipCodeVC() + + // 주소가 선택되었을 때 SwiftUI 바인딩 변수로 전달 vc.onAddressSelected = { selectedAddress in address = selectedAddress } return vc } - + + // UIViewController 상태 갱신 (현재는 별도 갱신 로직 없음) func updateUIViewController(_ uiViewController: KakaoZipCodeVC, context: Context) {} } diff --git a/StockMate/StockMate/app/core/KakaoPostcode/ViewController.swift b/StockMate/StockMate/app/core/KakaoPostcode/ViewController.swift index 2c793d0..b6d74f8 100644 --- a/StockMate/StockMate/app/core/KakaoPostcode/ViewController.swift +++ b/StockMate/StockMate/app/core/KakaoPostcode/ViewController.swift @@ -5,9 +5,10 @@ // Created by Admin on 11/5/25. // - import UIKit +// 기본 테스트용 ViewController +// 버튼을 눌러 Kakao 우편번호 검색 화면(KakaoZipCodeVC)을 표시함 class ViewController: UIViewController { // MARK: - UI Components diff --git a/StockMate/StockMate/app/core/common/Validators.swift b/StockMate/StockMate/app/core/common/Validators.swift index 1b4b4d3..61c264c 100644 --- a/StockMate/StockMate/app/core/common/Validators.swift +++ b/StockMate/StockMate/app/core/common/Validators.swift @@ -15,10 +15,10 @@ func isValidEmail(_ email: String) -> Bool { func isValidPassword(_ pw: String) -> Bool { guard pw.count >= 8 else { return false } return pw.range(of: "[A-Za-z]", options: .regularExpression) != nil && - pw.range(of: "[0-9]", options: .regularExpression) != nil + pw.range(of: "[0-9]", options: .regularExpression) != nil } func isValidBizNo(_ no: String) -> Bool { - let pattern = #"^\d{3}-\d{2}-\d{5}$"# - return no.range(of: pattern, options: .regularExpression) != nil + let regex = "^\\d{3}-\\d{2}-\\d{5}$" + return NSPredicate(format: "SELF MATCHES %@", regex).evaluate(with: no) } diff --git a/StockMate/StockMate/app/core/components/AlertModal.swift b/StockMate/StockMate/app/core/components/AlertModal.swift index 72d1e1b..88db275 100644 --- a/StockMate/StockMate/app/core/components/AlertModal.swift +++ b/StockMate/StockMate/app/core/components/AlertModal.swift @@ -5,22 +5,28 @@ // Created by Admin on 11/4/25. // - import SwiftUI + +// 공용 알림 모달 뷰 +// 아이콘, 제목, 메시지, 버튼 구성에 따라 다양한 형태로 표시 가능 struct AlertModal: View { - var icon: Image? = nil // ✅ 아이콘 없을 수도 있음 + 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 + // 버튼 배치 방향 설정 (세로 / 가로) + var buttonLayout: ButtonLayout = .vertical + // 버튼 레이아웃 타입 정의 enum ButtonLayout { case vertical case horizontal @@ -28,6 +34,7 @@ struct AlertModal: View { var body: some View { VStack(spacing: 15) { + // 아이콘이 있을 경우 표시 if let icon = icon { icon .resizable() @@ -35,12 +42,13 @@ struct AlertModal: View { .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)) @@ -49,23 +57,34 @@ struct AlertModal: View { .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))) + .buttonStyle( + CustomButtonStyle(type: .outlined(.Primary)) + ) } Button(primaryButtonTitle, action: primaryAction) - .buttonStyle(CustomButtonStyle(type: .filled(Color.Primary))) + .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))) + .buttonStyle( + CustomButtonStyle(type: .outlined(.Primary)) + ) } Button(primaryButtonTitle, action: primaryAction) - .buttonStyle(CustomButtonStyle(type: .filled(Color.Primary))) + .buttonStyle( + CustomButtonStyle(type: .filled(Color.Primary)) + ) } } } @@ -82,7 +101,7 @@ import SwiftUI #Preview { ScrollView{ VStack(spacing: 40) { - // ✅ 1. 체크 아이콘 + 버튼 1개 + // 체크 아이콘 + 버튼 1개 AlertModal( icon: Image("SuccessIllust"), title: "등록 완료!", @@ -98,9 +117,7 @@ import SwiftUI primaryAction: {} ) - - - // ✅ 2. 아이콘 없이 버튼 2개 (가로) + // 아이콘 없이 버튼 2개 (가로 배치) AlertModal( title: "주문 취소", message: "주문을 취소하시겠습니까?", @@ -120,8 +137,7 @@ import SwiftUI buttonLayout: .horizontal ) - - // ✅ 3. 주문완료 + // 주문 완료 알림 (세로 버튼 배치) AlertModal( icon: Image("SuccessIllust"), title: "주문완료!", @@ -136,5 +152,4 @@ import SwiftUI .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 index 5fa4e35..852180d 100644 --- a/StockMate/StockMate/app/core/components/BarChartView.swift +++ b/StockMate/StockMate/app/core/components/BarChartView.swift @@ -14,12 +14,12 @@ struct BarChartView: View { @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월" 형식 변환 + // "07" → "7월" 형식 변환 let displayLabels = reversedLabels.map { label in if let monthInt = Int(label) { return "\(monthInt)월" @@ -28,60 +28,62 @@ struct BarChartView: View { } } - // ✅ 기본 선택: 최신월 + // 기본 선택: 최신월 let defaultMonth = displayLabels.last ?? "" let activeMonth = selectedMonth ?? defaultMonth - VStack(alignment: .leading, spacing: 14) { - // ✅ 막대 그래프 + VStack(alignment: .leading, spacing: 12) { + // 막대 그래프 GeometryReader { geometry in - let chartHeight = geometry.size.height * 0.85 // 상하 여백 고려 + let chartHeight = geometry.size.height * 0.88 // 상하 여백 고려 let totalWidth = geometry.size.width let barCount = CGFloat(reversedValues.count) - let barWidth: CGFloat = 28 + let barWidth: CGFloat = 35 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) + RoundedRectangle(cornerRadius: 10) .fill(activeMonth == displayLabels[i] ? Color.Primary : Color.LightBlue04) - // ✅ 막대 높이를 geometry 기준으로 조정 - .frame(width: barWidth, height: chartHeight * reversedValues[i]) + .frame(width: barWidth, height: max(chartHeight * reversedValues[i], 8)) // 최소 높이 보장 .onTapGesture { selectedMonth = (selectedMonth == displayLabels[i]) ? nil : displayLabels[i] } Text(displayLabels[i]) - .font(.caption2) - .foregroundColor(.black) - .padding(.top, 4) + .font(.system(size: 13, weight: activeMonth == displayLabels[i] ? .semibold : .light)) // 선택된 막대는 글씨 bold + .padding(.top, 3) } } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) } - .frame(height: 140) // ← 전체 그래프 영역 높이 확장 - .padding(.vertical, 8) + .frame(height: 163) // 전체 그래프 영역 높이 확장 + .padding(.bottom, 7) - Divider() + Rectangle() + .fill(Color.textGray2) + .frame(height: 0.8) // Divider보다 살짝 두껍게 + .padding(.horizontal, 4) - // ✅ 하단 "n월 지출금액 ooo원" 표시 + + // 하단 "n월 지출금액 ooo원" 표시 if let index = displayLabels.firstIndex(of: activeMonth) { HStack { Text("\(displayLabels[index]) 지출 현황") - .font(.system(size: 17, weight: .medium)) + .font(.system(size: 15, weight: .regular)) Spacer() Text("\(reversedAmounts[index].formatted())원") - .font(.system(size: 18, weight: .bold)) + .font(.system(size: 17, weight: .bold)) .foregroundColor(Color.Primary) } - .padding(.top, 6) .padding(.horizontal,4) + .padding(.vertical, 2) } } .frame(maxWidth: .infinity) - // ✅ 초기 로드 시 최신월 자동 선택 + // 초기 로드 시 최신월 자동 선택 .onAppear { if selectedMonth == nil { selectedMonth = defaultMonth diff --git a/StockMate/StockMate/app/core/components/BottomToast.swift b/StockMate/StockMate/app/core/components/BottomToast.swift new file mode 100644 index 0000000..dd9f586 --- /dev/null +++ b/StockMate/StockMate/app/core/components/BottomToast.swift @@ -0,0 +1,57 @@ +// +// BottomToast.swift +// StockMate +// +// Created by Admin on 11/10/25. +// + +import SwiftUI + +// 화면 하단에 토스트 알림 뷰 +struct BottomToast: View { + let message: String + @Binding var isVisible: Bool + var iconName: String = "toastlogo" + var iconColor: Color = .white + var backgroundColor: Color = Color(hex: "4CAF50") + var duration: Double = 2.2 + + var body: some View { + VStack { + Spacer() + + // 토스트가 표시될 때만 렌더링 + if isVisible { + HStack(spacing: 10) { + Image(iconName) + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + + Text(message) + .font(.system(size: 14, weight: .light)) + .foregroundColor(.black) + .multilineTextAlignment(.center) + .lineLimit(2) + } + .padding(.horizontal, 22) + .padding(.vertical, 12) + .background( + RoundedRectangle(cornerRadius: 9999) + .fill(backgroundColor) + ) + .shadow(color: .black.opacity(0.1), radius: 5, x: 0, y: 4) + .padding(.bottom, 60) + .padding(.horizontal, 50) + .onAppear { // 토스트가 나타날 때 타이머 시작 + DispatchQueue.main.asyncAfter(deadline: .now() + duration) { + withAnimation(.easeInOut(duration: 0.25)) { + isVisible = false + } + } + } + } + } + .animation(.easeInOut(duration: 0.25), value: isVisible) + } +} diff --git a/StockMate/StockMate/app/core/components/CartCard.swift b/StockMate/StockMate/app/core/components/CartCard.swift index 6211f93..b8157e9 100644 --- a/StockMate/StockMate/app/core/components/CartCard.swift +++ b/StockMate/StockMate/app/core/components/CartCard.swift @@ -12,8 +12,6 @@ struct CartCard: View { let quantity: Int let onIncrease: () -> Void let onDecrease: () -> Void - let onAddToCart: (() -> Void)? - let onRemoveFromCart: () -> Void var body: some View { VStack(alignment: .leading, spacing: 6) { @@ -51,82 +49,11 @@ struct CartCard: View { Spacer() - // 🪄 수량에 따른 3단계 분기 - if quantity == 0 { - if let onAddToCart = onAddToCart { - Button(action: onAddToCart) { - Image("add_shopping_cart") - .resizable() - .scaledToFit() - .frame(width: 18, height: 18) - .padding(10) - .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: .regular)) - .frame(width: 13,height: 13) - .foregroundColor(.black) - } - - Text("1") - .font(.system(size: 15, weight: .medium)) - .frame(width: 20) - - Button(action: onIncrease) { - Image(systemName: "plus") - .font(.system(size: 14, weight: .regular)) - .frame(width: 13,height: 13) - .foregroundColor(.black) - } - } - .padding(.vertical, 6) - .padding(.horizontal, 10) - .background(Color.white) - .cornerRadius(10) - .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: .regular)) - .frame(width: 13,height: 13) - .foregroundColor(.black) - } - - Text("\(quantity)") - .font(.system(size: 15, weight: .medium)) - .frame(width: 20) - - Button(action: onIncrease) { - Image(systemName: "plus") - .font(.system(size: 14, weight: .regular)) - .frame(width: 13,height: 13) - .foregroundColor(.black) - } - } - .padding(.vertical, 6) - .padding(.horizontal, 10) - .background(Color.white) - .cornerRadius(10) - .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) - } + QuantityControlView( + quantity: quantity, + onIncrease: onIncrease, + onDecrease: onDecrease + ) } } .padding() diff --git a/StockMate/StockMate/app/core/components/CartInfoCard.swift b/StockMate/StockMate/app/core/components/CartInfoCard.swift index d38e0f1..6c3c0c6 100644 --- a/StockMate/StockMate/app/core/components/CartInfoCard.swift +++ b/StockMate/StockMate/app/core/components/CartInfoCard.swift @@ -59,7 +59,6 @@ struct CartInfoCard: View { .padding() .background(Color.white) .cornerRadius(14) -// .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) } } diff --git a/StockMate/StockMate/app/core/components/CategoryButton.swift b/StockMate/StockMate/app/core/components/CategoryButton.swift new file mode 100644 index 0000000..1e0b599 --- /dev/null +++ b/StockMate/StockMate/app/core/components/CategoryButton.swift @@ -0,0 +1,32 @@ +// +// CategoryButton.swift +// StockMate +// +// Created by Admin on 11/8/25. +// + +import SwiftUI + +struct CategoryButton: View { + let title: String + let isSelected: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + Text(title) + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(isSelected ? .Primary : .black) + .padding(.vertical, 8) + .padding(.horizontal, 12) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(isSelected ? Color(hex: "#E1E7F7") : Color.Light) + ) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(isSelected ? Color.Primary : Color.GrayStroke, lineWidth: 1) + ) + } + } +} diff --git a/StockMate/StockMate/app/core/components/CustomSecureField.swift b/StockMate/StockMate/app/core/components/CustomSecureField.swift index 71da03e..c85f895 100644 --- a/StockMate/StockMate/app/core/components/CustomSecureField.swift +++ b/StockMate/StockMate/app/core/components/CustomSecureField.swift @@ -12,9 +12,21 @@ struct CustomSecureField: View { var placeholder: String @Binding var text: String var errorMessage: String? = nil + @FocusState private var isFocused: Bool @State private var showPassword = false + // 테두리 색상 계산 로직 (CustomTextField와 동일) + private var borderColor: Color { + if let error = errorMessage, !error.isEmpty { + return .red + } else if isFocused { + return .Primary + } else { + return .LightBlue04 + } + } + var body: some View { VStack(alignment: .leading, spacing: 6) { // 필드 제목 @@ -25,11 +37,7 @@ struct CustomSecureField: View { // 텍스트 입력 박스 ZStack { RoundedRectangle(cornerRadius: 8) - .strokeBorder( - isFocused ? Color.Primary : - (errorMessage == nil ? Color.LightBlue04 : .red), - lineWidth: 1 - ) + .strokeBorder(borderColor, lineWidth: 1) .background(RoundedRectangle(cornerRadius: 8).fill(Color.white)) // 포커스일 때만 그림자 표시 .shadow( diff --git a/StockMate/StockMate/app/core/components/CustomTextField.swift b/StockMate/StockMate/app/core/components/CustomTextField.swift index 074d2c2..a2fa910 100644 --- a/StockMate/StockMate/app/core/components/CustomTextField.swift +++ b/StockMate/StockMate/app/core/components/CustomTextField.swift @@ -13,10 +13,22 @@ struct CustomTextField: View { @Binding var text: String var isEmail: Bool = false var errorMessage: String? = nil - var isReadOnly: Bool = false // ✅ 추가 + var isReadOnly: Bool = false @FocusState private var isFocused: Bool + // 테두리 색상 계산 로직 + private var borderColor: Color { + if let error = errorMessage, !error.isEmpty { + return .red + } else if isFocused { + return .Primary + } else { + return .LightBlue04 + } + } + + var body: some View { VStack(alignment: .leading, spacing: 6) { Text(title) @@ -25,12 +37,7 @@ struct CustomTextField: View { ZStack { RoundedRectangle(cornerRadius: 8) - .strokeBorder( - isFocused - ? Color.Primary - : (errorMessage == nil ? Color.LightBlue04 : .red), - lineWidth: 1 - ) + .strokeBorder(borderColor, lineWidth: 1) .background( RoundedRectangle(cornerRadius: 8) .fill(Color.white) @@ -46,7 +53,7 @@ struct CustomTextField: View { ) if isReadOnly { - // ✅ 가로 스크롤 가능한 읽기 전용 텍스트 + // 가로 스크롤 가능한 읽기 전용 텍스트 ScrollView(.horizontal, showsIndicators: false) { Text(text.isEmpty ? placeholder : text) .font(.system(size: 15)) @@ -74,11 +81,6 @@ struct CustomTextField: View { .foregroundColor(.red) .frame(height: 14) // 고정 높이 확보 .opacity(errorMessage == nil ? 0 : 1) // 없을 땐 투명 -// if let errorMessage = errorMessage { -// Text(errorMessage) -// .font(.caption) -// .foregroundColor(.red) -// } } } } diff --git a/StockMate/StockMate/app/core/components/DonutChartView.swift b/StockMate/StockMate/app/core/components/DonutChartView.swift index 2dd37c6..5163845 100644 --- a/StockMate/StockMate/app/core/components/DonutChartView.swift +++ b/StockMate/StockMate/app/core/components/DonutChartView.swift @@ -15,7 +15,7 @@ struct DonutChartView: View { Double(data.map { $0.totalAmount }.reduce(0, +)) } - // ✅ 각 항목별 비율 계산 + // 각 항목별 비율 계산 var percentages: [Double] { data.map { total == 0 ? 0 : (Double($0.totalAmount) / total * 100) } } @@ -39,7 +39,7 @@ struct DonutChartView: View { var body: some View { HStack(alignment: .center, spacing: 24) { - // ✅ 도넛 차트 + // 도넛 차트 if total == 0 { Text("데이터 없음") .foregroundColor(.gray) @@ -61,9 +61,8 @@ struct DonutChartView: View { endPoint: .bottomTrailing ) ) -// .foregroundStyle(colors[index % colors.count]) .cornerRadius(8.0) - // ✅ 도넛 안쪽에 비율 표시 + // 도넛 안쪽에 비율 표시 .annotation(position: .overlay) { let percentage = percentages[index] Text("\(percentage, specifier: "%.1f")%") @@ -77,7 +76,7 @@ struct DonutChartView: View { .chartLegend(.hidden) // 기본 범례 숨김 } - // ✅ 오른쪽 커스텀 범례 + // 오른쪽 커스텀 범례 VStack(alignment: .leading, spacing: 18) { ForEach(Array(data.enumerated()), id: \.offset) { index, item in let percentage = percentages[index] diff --git a/StockMate/StockMate/app/core/components/InventoryCardView.swift b/StockMate/StockMate/app/core/components/InventoryCardView.swift index 7113417..017f669 100644 --- a/StockMate/StockMate/app/core/components/InventoryCardView.swift +++ b/StockMate/StockMate/app/core/components/InventoryCardView.swift @@ -55,7 +55,7 @@ struct InventoryCardView: View { VStack(alignment: .center, spacing: 6) { if item.isLack { Text("수량 부족") - .font(.system(size: 13, weight: .regular)) + .font(.system(size: 13, weight: .semibold)) .padding(.horizontal, 10) .padding(.vertical, 5) .background(Color.DangerBg) @@ -63,7 +63,7 @@ struct InventoryCardView: View { .cornerRadius(12) } else { Text("수량 여유") - .font(.system(size: 13, weight: .regular)) + .font(.system(size: 13, weight: .semibold)) .padding(.horizontal, 10) .padding(.vertical, 5) .background(Color.StatusGreenBg) diff --git a/StockMate/StockMate/app/core/components/OrderRequestCardView.swift b/StockMate/StockMate/app/core/components/OrderRequestCardView.swift index f2fa6ab..b4c31f8 100644 --- a/StockMate/StockMate/app/core/components/OrderRequestCardView.swift +++ b/StockMate/StockMate/app/core/components/OrderRequestCardView.swift @@ -13,7 +13,6 @@ struct OrderRequestCardView: View { let onIncrease: () -> Void let onDecrease: () -> Void let onAddToCart: () -> Void - let onRemoveFromCart: () -> Void var body: some View { VStack(alignment: .leading, spacing: 6) { @@ -52,7 +51,6 @@ struct OrderRequestCardView: View { Spacer() // 수량 컨트롤러 - // 🪄 수량에 따른 3단계 분기 if quantity == 0 { Button(action: onAddToCart) { Image("add_shopping_cart") @@ -63,72 +61,13 @@ struct OrderRequestCardView: View { .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: .regular)) - .frame(width: 13,height: 13) - .foregroundColor(.black) - } - - Text("1") - .font(.system(size: 15, weight: .medium)) - .frame(width: 20) - - Button(action: onIncrease) { - Image(systemName: "plus") - .font(.system(size: 14, weight: .regular)) - .frame(width: 13,height: 13) - .foregroundColor(.black) - } } - .padding(.vertical, 6) - .padding(.horizontal, 10) - .background(Color.white) - .cornerRadius(10) - .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: .regular)) - .frame(width: 13,height: 13) - .foregroundColor(.black) - } - - Text("\(quantity)") - .font(.system(size: 15, weight: .medium)) - .frame(width: 20) - - Button(action: onIncrease) { - Image(systemName: "plus") - .font(.system(size: 14, weight: .regular)) - .frame(width: 13,height: 13) - .foregroundColor(.black) - } - } - .padding(.vertical, 6) - .padding(.horizontal, 10) - .background(Color.white) - .cornerRadius(10) - .overlay( // ✅ 테두리 추가 - RoundedRectangle(cornerRadius: 10) - .stroke( Color.LightBlue03, lineWidth: 2) + QuantityControlView( + quantity: quantity, + onIncrease: onIncrease, + onDecrease: onDecrease ) - .clipShape(RoundedRectangle(cornerRadius: 10)) - .shadow(color: Color.black.opacity(0.25), radius: 4, x: 0, y: 4) - } } } @@ -164,8 +103,7 @@ struct OrderRequestCardView: View { quantity: 0, onIncrease: {}, onDecrease: {}, - onAddToCart: {}, - onRemoveFromCart: {} + onAddToCart: {} ) // 수량 1 (카트에 하나 있음) @@ -174,8 +112,7 @@ struct OrderRequestCardView: View { quantity: 1, onIncrease: {}, onDecrease: {}, - onAddToCart: {}, - onRemoveFromCart: {} + onAddToCart: {} ) // 수량 3 (여러 개 담긴 상태) @@ -184,8 +121,7 @@ struct OrderRequestCardView: View { quantity: 3, onIncrease: {}, onDecrease: {}, - onAddToCart: {}, - onRemoveFromCart: {} + onAddToCart: {} ) } .padding() diff --git a/StockMate/StockMate/app/core/components/PrimaryButton.swift b/StockMate/StockMate/app/core/components/PrimaryButton.swift deleted file mode 100644 index b8f165f..0000000 --- a/StockMate/StockMate/app/core/components/PrimaryButton.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// PrimaryButton.swift -// StockMate -// -// Created by Admin on 10/10/25. -// - -import SwiftUI - -struct PrimaryButton: View { - var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) - } -} - -#Preview { - PrimaryButton() -} diff --git a/StockMate/StockMate/app/core/components/QuantityControlView.swift b/StockMate/StockMate/app/core/components/QuantityControlView.swift new file mode 100644 index 0000000..7f455ab --- /dev/null +++ b/StockMate/StockMate/app/core/components/QuantityControlView.swift @@ -0,0 +1,60 @@ +// +// QuantityControlView.swift +// StockMate +// +// Created by Admin on 11/8/25. +// + +import SwiftUI + +struct QuantityControlView: View { + let quantity: Int + let onIncrease: () -> Void + let onDecrease: () -> Void + + var body: some View { + HStack(spacing: 10) { + // 감소 버튼 + Button(action: onDecrease) { + Image(systemName: "minus") + .font(.system(size: 14, weight: .regular)) + .frame(width: 13, height: 13) + .foregroundColor(.black) + } + + // 수량 표시 + Text("\(quantity)") + .font(.system(size: 15, weight: .medium)) + .frame(width: 20) + .animation(.easeInOut(duration: 0.2), value: quantity) + + // 증가 버튼 + Button(action: onIncrease) { + Image(systemName: "plus") + .font(.system(size: 14, weight: .regular)) + .frame(width: 13, height: 13) + .foregroundColor(.black) + } + } + .padding(.vertical, 6) + .padding(.horizontal, 10) + .background(Color.white) + .cornerRadius(10) + .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) + } +} + +#Preview { + QuantityControlView( + quantity: 2, + onIncrease: { print("➕ 수량 증가") }, + onDecrease: { print("➖ 수량 감소") } + ) + .padding() + .background(Color.Light) +} diff --git a/StockMate/StockMate/app/core/components/RoundedCorner.swift b/StockMate/StockMate/app/core/components/RoundedCorner.swift index b93b448..43057fe 100644 --- a/StockMate/StockMate/app/core/components/RoundedCorner.swift +++ b/StockMate/StockMate/app/core/components/RoundedCorner.swift @@ -7,10 +7,12 @@ import SwiftUI +// 특정 모서리만 둥글게 처리할 수 있는 커스텀 Shape 구조체 struct RoundedCorner: Shape { - var radius: CGFloat = .infinity - var corners: UIRectCorner = .allCorners - + var radius: CGFloat = .infinity // 둥근 모서리의 반경 (기본값은 무한대) + var corners: UIRectCorner = .allCorners // 둥글게 적용할 모서리 (기본값: 전체 모서리) + + // 지정된 모서리에만 둥근 경로를 적용하여 Path 생성 func path(in rect: CGRect) -> Path { let path = UIBezierPath( roundedRect: rect, diff --git a/StockMate/StockMate/app/core/components/ToastModifier.swift b/StockMate/StockMate/app/core/components/ToastModifier.swift new file mode 100644 index 0000000..4fc4ab1 --- /dev/null +++ b/StockMate/StockMate/app/core/components/ToastModifier.swift @@ -0,0 +1,97 @@ +// +// ToastModifier.swift +// StockMate +// +// Created by Admin on 11/9/25. +// + +import SwiftUI + +struct ToastView: View { + let message: String + let iconName: String? // 아이콘 이름 (예: "checkmark.circle.fill" or "xmark.circle.fill") + let iconColor: Color? // 아이콘 색상 (예: .green, .red 등) + + var body: some View { + ZStack { + // 메시지 중앙 정렬 + Text(message) + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.white) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding(.horizontal, 40) // 아이콘 영역 고려해서 여백 확보 + + // 아이콘 왼쪽 고정 + if let iconName, let iconColor { + HStack { + Image(systemName: iconName) + .foregroundColor(iconColor) + .font(.system(size: 18)) + Spacer() + } + .padding(.leading, 16) + } + } + .padding(.vertical, 14) + .frame(maxWidth: .infinity) + .background(Color.black.opacity(0.75)) + .cornerRadius(9999) + .padding(.horizontal, 24) + } +} + +// ViewModifier를 사용해 기존 View 위에 토스트를 표시 +struct ToastModifier: ViewModifier { + @Binding var isPresented: Bool + let message: String + let iconName: String? + let iconColor: Color? + let duration: Double + + func body(content: Content) -> some View { + ZStack { + content + + if isPresented { + VStack { + Spacer() + ToastView(message: message, iconName: iconName, iconColor: iconColor) + .transition(.opacity.combined(with: .scale)) + .padding(.bottom, 60) + } + .animation(.easeInOut(duration: 0.35), value: isPresented) + } + } + .onChange(of: isPresented) { shown in + if shown { + DispatchQueue.main.asyncAfter(deadline: .now() + duration) { + withAnimation(.easeOut(duration: 0.4)) { + isPresented = false + } + } + } + } + } +} + +// View 확장을 통해 modifier를 간편하게 사용할 수 있도록 함 +extension View { + func toast( + isPresented: Binding, + message: String, + iconName: String? = nil, + iconColor: Color? = nil, + duration: Double = 2.0 + ) -> some View { + self.modifier( + ToastModifier( + isPresented: isPresented, + message: message, + iconName: iconName, + iconColor: iconColor, + duration: duration + ) + ) + } +} diff --git a/StockMate/StockMate/app/core/components/TopToast.swift b/StockMate/StockMate/app/core/components/TopToast.swift index 1e61918..abf4a60 100644 --- a/StockMate/StockMate/app/core/components/TopToast.swift +++ b/StockMate/StockMate/app/core/components/TopToast.swift @@ -10,36 +10,51 @@ import SwiftUI struct TopToast: View { let message: String @Binding var isVisible: Bool + var iconName: String = "exclamationmark.triangle.fill" // 기본 아이콘 + var iconColor: Color = .Danger var duration: Double = 2.2 var body: some View { - if isVisible { - VStack { - HStack(spacing: 8) { - Image(systemName: "exclamationmark.triangle.fill") - .foregroundColor(.white) + VStack(spacing: 0) { + if isVisible { + ZStack { + // 메시지 중앙 정렬 Text(message) - .foregroundColor(.white) .font(.system(size: 14, weight: .medium)) + .foregroundColor(Color(hex: "AB3029")) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding(.horizontal, 40) // 아이콘 여백 확보 + + // 왼쪽 아이콘 + HStack { + Image(systemName: iconName) + .foregroundColor(iconColor) + .font(.system(size: 18)) + Spacer() + } + .padding(.leading, 16) } - .padding() - .background(Color.red.opacity(0.9)) + .padding(.vertical, 14) + .frame(maxWidth: .infinity) + .background(Color.DangerBg.opacity(0.85)) .cornerRadius(12) - .shadow(radius: 5) - .padding(.horizontal, 16) - .padding(.top, 8) - - Spacer() - } - .transition(.move(edge: .top).combined(with: .opacity)) - .animation(.easeInOut(duration: 0.25), value: isVisible) - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + duration) { - withAnimation { - isVisible = false + .padding(.horizontal, 24) + .padding(.top, 14) + .transition(.move(edge: .top).combined(with: .opacity)) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + duration) { + withAnimation { + isVisible = false + } } } } + + Spacer() } + .frame(maxWidth: .infinity) + .animation(.easeInOut(duration: 0.3), value: isVisible) + .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) } } diff --git a/StockMate/StockMate/app/core/components/UIApplication+Extension.swift b/StockMate/StockMate/app/core/components/UIApplication+Extension.swift new file mode 100644 index 0000000..f239309 --- /dev/null +++ b/StockMate/StockMate/app/core/components/UIApplication+Extension.swift @@ -0,0 +1,14 @@ +// +// UIApplication+Extension.swift +// StockMate +// +// Created by Admin on 11/9/25. +// + +import SwiftUI + +extension UIApplication { + func hideKeyboard() { + sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } +} diff --git a/StockMate/StockMate/app/core/network/ApiClient.swift b/StockMate/StockMate/app/core/network/ApiClient.swift index 94307e0..d28bd7a 100644 --- a/StockMate/StockMate/app/core/network/ApiClient.swift +++ b/StockMate/StockMate/app/core/network/ApiClient.swift @@ -9,12 +9,18 @@ import Foundation import Alamofire struct ApiClient { - static let baseURL = "https://api.stockmate.site/" - static let shared: Session = { + static let baseURL = "https://api.stockmate.site/" // 기본 API 서버 주소 + static let shared: Session = { // Alamofire의 Session을 싱글톤 형태로 공유 + + // 요청 시 토큰 등을 자동으로 처리하기 위한 인터셉터 설정 let interceptor = AuthInterceptor() + + // 네트워크 요청 관련 기본 설정 구성 let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 20 config.timeoutIntervalForResource = 20 + + // 커스텀 설정과 인터셉터를 적용한 세션 생성 return Session(configuration: config, interceptor: interceptor, eventMonitors: []) }() } diff --git a/StockMate/StockMate/app/core/network/AuthInterceptor.swift b/StockMate/StockMate/app/core/network/AuthInterceptor.swift index 4a94468..76709f9 100644 --- a/StockMate/StockMate/app/core/network/AuthInterceptor.swift +++ b/StockMate/StockMate/app/core/network/AuthInterceptor.swift @@ -8,6 +8,7 @@ import Foundation import Alamofire +// API 요청 시 Access Token을 자동으로 헤더에 추가하는 인터셉터 final class AuthInterceptor: RequestInterceptor, @unchecked Sendable { private let tokenStore = TokenStore.shared @@ -19,5 +20,5 @@ final class AuthInterceptor: RequestInterceptor, @unchecked Sendable { completion(.success(req)) } - // 필요 시 retry(_:for:dueTo:completion:) 구현해서 401 -> refresh token 흐름 처리 가능 + // TODO: 401 Unauthorized 응답 시 Refresh Token을 사용해 토큰 재발급 로직 추가 } diff --git a/StockMate/StockMate/app/feature/auth/data/AuthApi.swift b/StockMate/StockMate/app/feature/auth/data/AuthApi.swift index d8e4840..2528705 100644 --- a/StockMate/StockMate/app/feature/auth/data/AuthApi.swift +++ b/StockMate/StockMate/app/feature/auth/data/AuthApi.swift @@ -8,6 +8,8 @@ import Foundation import Alamofire +// === Request === +// 회원가입 요청 바디 struct RegisterRequest: Encodable { let email: String let password: String @@ -16,22 +18,29 @@ struct RegisterRequest: Encodable { let storeName: String let businessNumber: String } +// 로그인 요청 바디 struct LoginRequest: Encodable { let email: String let password: String } + +// === Response === +// 회원가입 응답 struct RegisterResponse: Decodable { let status: Int let success: Bool let message: String } +// 로그인 응답 데이터 struct LoginData: Decodable { let accessToken: String let refreshToken: String let role: String } + +// 로그인 응답 전체 구조 struct LoginResponse: Decodable { let status: Int let success: Bool @@ -39,12 +48,16 @@ struct LoginResponse: Decodable { let data: LoginData? } +// === API === +// 인증 관련 API 모음 enum AuthApi { + // POST - 회원가입 요청 static func register(_ req: RegisterRequest) -> DataRequest { let url = ApiClient.baseURL + "api/v1/auth/register" return ApiClient.shared.request(url, method: .post, parameters: req, encoder: JSONParameterEncoder.default) } + // POST - 로그인 요청 static func login(_ req: LoginRequest) -> DataRequest { let url = ApiClient.baseURL + "api/v1/auth/login" return ApiClient.shared.request(url, method: .post, parameters: req, encoder: JSONParameterEncoder.default) diff --git a/StockMate/StockMate/app/feature/auth/data/AuthRepositoryImpl.swift b/StockMate/StockMate/app/feature/auth/data/AuthRepositoryImpl.swift index cd07cd2..1e83f57 100644 --- a/StockMate/StockMate/app/feature/auth/data/AuthRepositoryImpl.swift +++ b/StockMate/StockMate/app/feature/auth/data/AuthRepositoryImpl.swift @@ -9,11 +9,13 @@ import Foundation import Alamofire final class AuthRepositoryImpl: AuthRepositoryProtocol { + // 회원가입 요청 func register(_ req: RegisterRequest) async -> AppResult> { let dataReq = AuthApi.register(req) return await safeApi(dataReq, decodeTo: ApiResponse.self) } + // 로그인 요청 func login(_ req: LoginRequest) async -> AppResult> { let dataReq = AuthApi.login(req) return await safeApi(dataReq, decodeTo: ApiResponse.self) diff --git a/StockMate/StockMate/app/feature/auth/data/TokenStore.swift b/StockMate/StockMate/app/feature/auth/data/TokenStore.swift index 6a7e353..45d7e42 100644 --- a/StockMate/StockMate/app/feature/auth/data/TokenStore.swift +++ b/StockMate/StockMate/app/feature/auth/data/TokenStore.swift @@ -14,27 +14,31 @@ final class TokenStore: @unchecked Sendable { private let defaults = UserDefaults.standard + // 토큰 및 역할 저장 func save(access: String, refresh: String, role: String) { defaults.set(role, forKey: "role") - // access/refresh는 Keychain에 저장하는 걸 권장 KeychainHelper.standard.save(access, service: "com.stockmate", account: "accessToken") KeychainHelper.standard.save(refresh, service: "com.stockmate", account: "refreshToken") } + // 저장된 토큰 및 역할 제거 func clear() { defaults.removeObject(forKey: "role") KeychainHelper.standard.delete(service: "com.stockmate", account: "accessToken") KeychainHelper.standard.delete(service: "com.stockmate", account: "refreshToken") } + // 액세스 토큰 조회 func getAccessToken() -> String? { return KeychainHelper.standard.read(service: "com.stockmate", account: "accessToken") } + // 리프레시 토큰 조회 func getRefreshToken() -> String? { return KeychainHelper.standard.read(service: "com.stockmate", account: "refreshToken") } + // 사용자 역할(Role) 조회 func getRole() -> String? { return defaults.string(forKey: "role") } diff --git a/StockMate/StockMate/app/feature/auth/domain/AuthRepositoryProtocol.swift b/StockMate/StockMate/app/feature/auth/domain/AuthRepositoryProtocol.swift index 6691965..eac76f4 100644 --- a/StockMate/StockMate/app/feature/auth/domain/AuthRepositoryProtocol.swift +++ b/StockMate/StockMate/app/feature/auth/domain/AuthRepositoryProtocol.swift @@ -9,6 +9,9 @@ import Foundation import Alamofire protocol AuthRepositoryProtocol { + // 회원가입 요청 func register(_ req: RegisterRequest) async -> AppResult> + + // 로그인 요청 func login(_ req: LoginRequest) async -> AppResult> } diff --git a/StockMate/StockMate/app/feature/auth/ui/HomeView.swift b/StockMate/StockMate/app/feature/auth/ui/HomeView.swift index 825748f..3d6de8d 100644 --- a/StockMate/StockMate/app/feature/auth/ui/HomeView.swift +++ b/StockMate/StockMate/app/feature/auth/ui/HomeView.swift @@ -12,11 +12,10 @@ 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() - @StateObject private var notificationViewModel = NotificationViewModel() // 🔴 추가 + @StateObject private var notificationViewModel = NotificationViewModel() - @State private var selectedMonth: String? = nil // ✅ 추가 + @State private var selectedMonth: String? = nil var body: some View { @@ -41,7 +40,7 @@ struct HomeView: View { Spacer() - NavigationLink(destination: NotificationListView()) { // 🔴 전달 + NavigationLink(destination: NotificationListView()) { ZStack(alignment: .topTrailing) { Image("notification") .resizable() @@ -56,7 +55,7 @@ struct HomeView: View { .padding(5) .background(Color.red) .clipShape(Circle()) - .offset(x: 5, y: -5) + .offset(x: 2, y: -9) } } } @@ -66,7 +65,7 @@ struct HomeView: View { } .padding(.horizontal) - // 🔍 검색창 + // 검색창 NavigationLink(destination: InventorySearchView()) { HStack { Image(systemName: "magnifyingglass") @@ -94,7 +93,7 @@ struct HomeView: View { Text("지난달 카테고리 별 지출") .font(.system(size: 15, weight: .semibold)) .padding(4) - .frame(maxWidth: .infinity, alignment: .leading) // ✅ 항상 왼쪽 정렬 + .frame(maxWidth: .infinity, alignment: .leading) HStack { if dashboardViewModel.isLoading { @@ -126,31 +125,22 @@ struct HomeView: View { Text("월간 지출 현황") .font(.system(size: 15, weight: .semibold)) .padding(4) - .frame(maxWidth: .infinity, alignment: .leading) // ✅ 항상 왼쪽 정렬 + .frame(maxWidth: .infinity, alignment: .leading) - ZStack { // ✅ 크기 고정용 컨테이너 - RoundedRectangle(cornerRadius: 16) - .fill(Color.white) - .frame(height: 220) // ✅ 일정 높이 고정 if dashboardViewModel.isLoading { ProgressView("데이터 불러오는 중...") - .frame(height: 220) + .frame(height: 163) } else if dashboardViewModel.monthlySpendings.isEmpty { Text("최근 지출 내역이 없습니다.") .foregroundColor(.gray) - .frame(height: 220) + .frame(height: 163) } 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) @@ -163,31 +153,31 @@ struct HomeView: View { .task { // 카테고리 데이터 로드 await inventoryViewModel.loadLackCountByCategory() - await dashboardViewModel.fetchMonthlySpending() // ✅ 추가 - await dashboardViewModel.fetchCategorySpending() // ✅ 추가 - await notificationViewModel.fetchUnreadCount() // 🔴 추가 + await dashboardViewModel.fetchMonthlySpending() + await dashboardViewModel.fetchCategorySpending() + await notificationViewModel.fetchUnreadCount() } .onAppear { Task { await userViewModel.loadUserInfo() } } // 화면 디자인 시 잠시 주석처리 - // ✅ 세션 만료 시 자동으로 로그인 뷰로 이동 -// .onChange(of: userViewModel.shouldGoToLogin) { shouldGo in -// if shouldGo { -// print("세션 만료됨 → 로그인 화면으로 이동") -// authViewModel.logout() -// } -// } + // 세션 만료 시 자동으로 로그인 뷰로 이동 + .onChange(of: userViewModel.shouldGoToLogin) { shouldGo in + if shouldGo { + print("세션 만료됨 → 로그인 화면으로 이동") + authViewModel.logout() + } + } } private var lackStockSection: some View { VStack(alignment: .leading, spacing: 8) { Text("재고 부족 조회") .font(.system(size: 15, weight: .semibold)) - .frame(maxWidth: .infinity, alignment: .leading) // ✅ 항상 왼쪽 정렬 유지 + .frame(maxWidth: .infinity, alignment: .leading) HStack(spacing: 13) { if inventoryViewModel.lackCounts.isEmpty { - // ✅ 데이터가 없을 때도 공간 확보 + // 데이터가 없을 때도 공간 확보 ForEach(0..<5) { _ in StatusItem( title: "-", @@ -220,8 +210,6 @@ struct HomeView: View { .cornerRadius(16) .padding(.horizontal) } - - } @@ -278,19 +266,3 @@ struct StatusItem: View { }.frame(maxWidth: .infinity, minHeight: 70) } } - - -#Preview { - let dashboardVM = DashboardViewModel() - dashboardVM.categorySpendings = [ - CategorySpending(categoryName: "전기/램프", totalAmount: 450000), - CategorySpending(categoryName: "엔진/미션", totalAmount: 300000), - CategorySpending(categoryName: "하체/바디", totalAmount: 150000), - CategorySpending(categoryName: "내장/외장", totalAmount: 100000), - CategorySpending(categoryName: "기타소모품", totalAmount: 50000) - ] - - return HomeView() - .environmentObject(AuthViewModel()) - .environmentObject(dashboardVM) // ✅ 이제 진짜 연결됨! -} diff --git a/StockMate/StockMate/app/feature/auth/ui/LoginView.swift b/StockMate/StockMate/app/feature/auth/ui/LoginView.swift index 4f1da94..a83eb77 100644 --- a/StockMate/StockMate/app/feature/auth/ui/LoginView.swift +++ b/StockMate/StockMate/app/feature/auth/ui/LoginView.swift @@ -18,111 +18,151 @@ struct LoginView: View { @State private var emailError: String? = nil @State private var pwError: String? = nil + // 토스트 관련 상태 추가 + @State private var showTopToast = false + @State private var topToastMessage = "" + var onLogin: (String, String) -> Void = { _, _ in } var onClickRegister: () -> Void = {} var body: some View { - VStack { - Spacer().frame(height: 140) - - // MARK: - Logo - Image("stockmate_logo") - .resizable() - .scaledToFit() - .frame(width: 216, height: 44) + ZStack { - Spacer().frame(height: 67) - // MARK: - Title - Text("로그인") - .font(.system(size: 28, weight: .bold)) - .frame(maxWidth: .infinity, alignment: .leading) + VStack { + Spacer().frame(height: 140) + + // MARK: - Logo + Image("stockmate_logo") + .resizable() + .scaledToFit() + .frame(width: 216, height: 44) + + Spacer().frame(height: 67) + + // MARK: - Title + Text("로그인") + .font(.system(size: 28, weight: .bold)) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 24) + .foregroundColor(Color.DarkBlue01) + + Spacer().frame(height: 17) + + // MARK: - Text Fields + CustomTextField( + title: "이메일", + placeholder: "stockmate@gmail.com", + text: $authViewModel.email, + isEmail: true, + errorMessage: emailError + ) + .keyboardType(.emailAddress) .padding(.horizontal, 24) - .foregroundColor(Color.DarkBlue01) - - Spacer().frame(height: 17) - - // MARK: - Text Fields - CustomTextField( - title: "이메일", - placeholder: "stockmate@gmail.com", - text: $authViewModel.email, - isEmail: true, - errorMessage: emailError - ) - .keyboardType(.emailAddress) - .padding(.horizontal, 24) - - Spacer().frame(height: 18) - - // 비밀번호 - CustomSecureField( - title: "비밀번호", - placeholder: "비밀번호를 입력하세요", - text: $authViewModel.password, - errorMessage: pwError - ) - .padding(.horizontal, 24) - - Spacer().frame(height: 56) - - // MARK: - Login Button - Button(action: { - if isValidForm() { - Task { - await authViewModel.login() - } + .onChange(of: authViewModel.email) { newValue in + // 입력 중 실시간 validation + emailError = isValidEmail(newValue) ? nil : "이메일 형식을 확인해주세요" } - }) { - Text("로그인") - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .frame(height: 42) - .background(Color.Primary) - .cornerRadius(4) - } - .padding(.horizontal, 24) - - Spacer().frame(height: 24) - - // 회원가입 링크 - HStack { - Text("계정이 없으신가요?") - .foregroundColor(Color.Secondary) - .font(.system(size: 13)) - Button(action: { onClickRegister() }) { - Text("회원가입") - .font(.system(size: 13, weight: .bold)) - .foregroundColor(Color.Secondary) + + Spacer().frame(height: 18) + + // 비밀번호 + CustomSecureField( + title: "비밀번호", + placeholder: "비밀번호를 입력하세요", + text: $authViewModel.password, + errorMessage: pwError + ) + .padding(.horizontal, 24) + .onChange(of: authViewModel.password) { newValue in + pwError = newValue.count >= 8 ? nil : "8자 이상 비밀번호를 입력해주세요" } - } - - // 승인 아이디 받기 전 - // 홈화면으로 이동 - HStack { + + Spacer().frame(height: 26) + + // MARK: - Login Button + // 3) 버튼 — 시각적 상태 반영 + disabled 처리 Button(action: { - authViewModel.authState = .authenticated + // 버튼이 눌렸을 때는 한번 더 확정적으로 에러 상태를 설정 + // (뷰 업데이트 중이 아니므로 상태 변경해도 안전) + validateAndSetErrors() + guard isFormValid else { return } + + Task { + let success = await authViewModel.login() + if !success { + showToast("아이디 또는 비밀번호가 잘못되었습니다.") + } + // await authViewModel.login() + } }) { - Text("홈화면으로 이동") - .font(.system(size: 13, weight: .bold)) - .foregroundColor(Color.Secondary) + Text("로그인") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 42) + .background( + RoundedRectangle(cornerRadius: 4) + .fill(isFormValid ? Color.Primary : Color.gray.opacity(0.45)) + ) + .cornerRadius(4) + } + .padding(.horizontal, 24) + .disabled(!isFormValid) + + + Spacer().frame(height: 24) + + // 회원가입 링크 + HStack { + Text("계정이 없으신가요?") + .foregroundColor(Color.gray) + .font(.system(size: 13)) + Button(action: { onClickRegister() }) { + Text("회원가입") + .font(.system(size: 13, weight: .bold)) + .foregroundColor(Color.Primary) + } } + + Spacer() + } - .padding(.top, 5) + .background(Color.Light) + .onTapGesture { + UIApplication.shared.hideKeyboard() + } + .ignoresSafeArea() + + TopToast(message: topToastMessage, + isVisible: $showTopToast, + iconName: "exclamationmark.circle", + iconColor: .black) + .zIndex(1) // 다른 뷰 위로 + - Spacer() } - .background(Color.Light) - .ignoresSafeArea() } // MARK: - 유효성 검사 함수 - private func isValidForm() -> Bool { + // 1) 뷰 내부(바디 바깥) — 부작용 없는 computed property + private var isFormValid: Bool { + return isValidEmail(authViewModel.email) && authViewModel.password.count >= 8 + } + + // 4) body 바깥에 유틸 함수 추가 — 뷰 업데이트 중 호출하지 말고 이벤트에서만 호출 + private func validateAndSetErrors() { + // 이 함수는 호출되는 시점이 사용자 액션(버튼)일 때만 사용 emailError = isValidEmail(authViewModel.email) ? nil : "이메일 형식을 확인해주세요" pwError = authViewModel.password.count >= 8 ? nil : "8자 이상 비밀번호를 입력해주세요" - return emailError == nil && pwError == nil -// return true } + + // 상단 토스트 표시 함수 + private func showToast(_ message: String) { + topToastMessage = message + withAnimation { + showTopToast = true + } + } } diff --git a/StockMate/StockMate/app/feature/auth/ui/RegisterView.swift b/StockMate/StockMate/app/feature/auth/ui/RegisterView.swift index 2e29490..bd9a1b4 100644 --- a/StockMate/StockMate/app/feature/auth/ui/RegisterView.swift +++ b/StockMate/StockMate/app/feature/auth/ui/RegisterView.swift @@ -9,7 +9,7 @@ import SwiftUI struct RegisterView: View { @EnvironmentObject private var viewModel: AuthViewModel - + // MARK: - 사용자 입력값 @State private var email = "" @State private var password = "" @State private var confirmPassword = "" @@ -18,7 +18,7 @@ struct RegisterView: View { @State private var address = "" @State private var bizNo = "" - // 에러 메시지 상태 + // MARK: - 에러 메시지 상태값 @State private var emailError: String? = nil @State private var pwError: String? = nil @State private var confirmPasswordError: String? = nil @@ -27,239 +27,230 @@ struct RegisterView: View { @State private var addressError: String? = nil @State private var bizNoError: String? = nil + // MARK: - UI 상태 관리 @State private var isLoading = false @State private var showToast = false - @State private var showAddressSearch = false - + @State private var showSuccessToast = false var body: some View { - ScrollView { - VStack(spacing: 16) { - Spacer().frame(height: 70) - - // MARK: - Logo - Image("stockmate_logo") - .resizable() - .scaledToFit() - .frame(width: 216, height: 44) - - Spacer().frame(height: 4) - - // MARK: - Title - Text("회원가입") - .font(.system(size: 28, weight: .bold)) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 24) - .foregroundColor(Color.DarkBlue01) - - // MARK: - Text Fields - VStack { - CustomTextField( - title: "이메일", - placeholder: "stockmate@gmail.com", - text: $email, - errorMessage: emailError - ) - .keyboardType(.emailAddress) - CustomSecureField( - title: "비밀번호", - placeholder: "비밀번호를 입력하세요", - text: $password, - errorMessage: pwError - ) - CustomSecureField( - title: "비밀번호 확인", - placeholder: "비밀번호를 다시 입력하세요", - text: $confirmPassword, - errorMessage: confirmPasswordError - ) - CustomTextField( - title: "대표자 이름", - placeholder: "홍길동", - text: $owner, - errorMessage: ownerError - ) - CustomTextField( - title: "지점 이름", - placeholder: "서울 1호점", - text: $storeName, - errorMessage: storeNameError - ) - // ✅ 주소 입력 필드 + 버튼 추가 부분 - VStack(alignment: .leading, spacing: 4) { - HStack { + ZStack { + // MARK: - 메인 스크롤 영역 + ScrollView { + VStack(spacing: 16) { + Spacer().frame(height: 70) + + // MARK: - 로고 + Image("stockmate_logo") + .resizable() + .scaledToFit() + .frame(width: 216, height: 44) + + Spacer().frame(height: 4) + + // MARK: - 화면 제목 + Text("회원가입") + .font(.system(size: 28, weight: .bold)) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 24) + .foregroundColor(Color.DarkBlue01) + + // MARK: - 입력 폼 + VStack { + // 이메일 + CustomTextField( + title: "이메일", + placeholder: "stockmate@gmail.com", + text: $email, + errorMessage: emailError + ) + .keyboardType(.emailAddress) + .onChange(of: email) { newValue in + emailError = isValidEmail(newValue) ? nil : "이메일 형식을 확인해주세요" + } + + // 비밀번호 + CustomSecureField( + title: "비밀번호", + placeholder: "비밀번호를 입력하세요", + text: $password, + errorMessage: pwError + ) + .onChange(of: password) { newValue in + pwError = isValidPassword(newValue) ? nil : "8자 이상, 영문+숫자 조합입니다." + // confirm도 재검증 + confirmPasswordError = (confirmPassword.isEmpty || confirmPassword == newValue) ? nil : "비밀번호가 일치하지 않습니다" + } + + // 비밀번호 확인 + CustomSecureField( + title: "비밀번호 확인", + placeholder: "비밀번호를 다시 입력하세요", + text: $confirmPassword, + errorMessage: confirmPasswordError + ) + .onChange(of: confirmPassword) { newValue in + confirmPasswordError = (password == newValue) ? nil : "비밀번호가 일치하지 않습니다" + } + // 대표자 이름 + CustomTextField( + title: "대표자 이름", + placeholder: "홍길동", + text: $owner, + errorMessage: nil + ) + // 지점 이름 + CustomTextField( + title: "지점 이름", + placeholder: "강남점", + text: $storeName, + errorMessage: nil + ) + // 주소 입력 필드 + VStack(alignment: .leading, spacing: 4) { CustomTextField( title: "주소", - placeholder: "서울특별시 강남구 ...", + placeholder: "도로명 주소를 검색하세요", text: $address, errorMessage: addressError, - isReadOnly: true // ✅ 추가 + 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(.numberPad) - .onChange(of: bizNo) { newValue in - formatBizNoInput(newValue) - } - } - .padding(.horizontal, 24) - - - // MARK: - Register Button - if isLoading { - ProgressView("회원가입 중...") - .progressViewStyle(CircularProgressViewStyle()) - } else { - Button(action: { - handleRegister() - }) { - Text("회원가입") - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .frame(height: 48) - .background(Color(hex: "#1D4ED8")) - .cornerRadius(8) + + CustomTextField( + title: "사업자등록번호", + placeholder: "000-00-00000", + text: $bizNo, + errorMessage: bizNoError + ) + .keyboardType(.numberPad) + .onChange(of: bizNo) { newValue in + formatBizNoInput(newValue) + bizNoError = isValidBizNo(newValue) ? nil : "형식: 000-00-00000" + } } .padding(.horizontal, 24) - } - - TopToast(message: viewModel.message, isVisible: $showToast) - - Spacer().frame(height: 5) - // MARK: - Login Link - HStack(spacing: 4) { - Text("이미 계정이 있으신가요?") - .foregroundColor(Color.Secondary) - .font(.system(size: 13)) - Button(action: { - viewModel.goToLogin() - print("로그인으로 이동") - }) { - Text("로그인") - .fontWeight(.semibold) - .font(.system(size: 13, weight: .bold)) - .foregroundColor(Color.Secondary) + + + // MARK: - 회원가입 버튼 + if isLoading { + ProgressView("회원가입 중...") + .progressViewStyle(CircularProgressViewStyle()) + } else { + Button(action: { + validateAndSetErrors() + guard isFormValid else { return } + Task { + isLoading = true + let success = await viewModel.register( + email: email, + password: password, + owner: owner, + address: address, + storeName: storeName, + bizNo: bizNo.filter { $0.isNumber } + ) + isLoading = false + } + }) { + Text("회원가입") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 48) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(isFormValid ? Color.Primary : Color.gray.opacity(0.45)) + ) + .cornerRadius(8) + } + .disabled(!isFormValid) + .padding(.horizontal, 24) } + + // 상단 토스트 (오류/알림용) + TopToast(message: viewModel.message, isVisible: $showToast) + + Spacer().frame(height: 5) + + // MARK: - 로그인 페이지로 이동 링크 + HStack(spacing: 4) { + Text("이미 계정이 있으신가요?") + .foregroundColor(Color.gray) + .font(.system(size: 13)) + Button(action: { + viewModel.goToLogin() + print("로그인으로 이동") + }) { + Text("로그인") + .fontWeight(.semibold) + .font(.system(size: 13, weight: .bold)) + .foregroundColor(Color.Primary) + } + } + .padding(.bottom, 40) + + // 키보드 가림 방지용 여백 + Spacer().frame(height: 300) } - .padding(.bottom, 40) - - // ✅ 키보드 가림 방지용 여백 - Spacer().frame(height: 300) } + .background(Color.Light) + .ignoresSafeArea() + .onTapGesture { + UIApplication.shared.hideKeyboard() + } + .scrollDismissesKeyboard(.interactively) // 손가락으로 스크롤하면 키보드 자동 내려감 + .onChange(of: viewModel.message) { newMsg in + guard !newMsg.isEmpty else { return } + showToast = true + } + + // 회원가입 성공 토스트 + BottomToast( + message: "회원가입 성공", + isVisible: $showSuccessToast, + iconName: "toastlogo", + backgroundColor: Color(hex: "EEEDF5") // 초록색 + ) + .zIndex(1) // 다른 뷰 위로 } - .background(Color.Light) - .ignoresSafeArea() - .scrollDismissesKeyboard(.interactively) // ✅ 손가락으로 스크롤하면 키보드 자동 내려감 - + } - - // MARK: - 유효성 검사 함수 - private func isValidForm() -> Bool { + + // MARK: - 전체 폼 유효성 검사 + private var isFormValid: Bool { + return isValidEmail(email) + && isValidPassword(password) + && password == confirmPassword + && !owner.trimmingCharacters(in: .whitespaces).isEmpty + && !storeName.trimmingCharacters(in: .whitespaces).isEmpty + && !address.trimmingCharacters(in: .whitespaces).isEmpty + && isValidBizNo(bizNo) + } + + // MARK: - 입력값 검증 및 에러 설정 + private func validateAndSetErrors() { emailError = isValidEmail(email) ? nil : "이메일 형식을 확인해주세요" - pwError = isValidPassword(password) ? nil : "영문과 숫자를 포함한 8자 이상 비밀번호를 입력해주세요" - confirmPasswordError = (password == confirmPassword) ? nil : "비밀번호가 일치하지 않습니다." - bizNoError = isValidBizNo(bizNo) ? nil : "사업자등록번호 형식이 올바르지 않습니다. (예: 123-45-67890)" - - return emailError == nil && pwError == nil - && confirmPasswordError == nil && bizNoError == nil - } - - // MARK: - Register Handler - private func handleRegister() { - // 초기화 - emailError = nil - pwError = nil - confirmPasswordError = nil - ownerError = nil - storeNameError = nil - addressError = nil - bizNoError = nil - - var hasEmptyField = false - - // 필수 필드 체크 - if email.isEmpty { - emailError = "이메일을 입력해주세요." - hasEmptyField = true - } - if password.isEmpty { - pwError = "비밀번호를 입력해주세요." - hasEmptyField = true - } - if confirmPassword.isEmpty { - confirmPasswordError = "비밀번호를 다시 입력해주세요." - hasEmptyField = true - } - if owner.isEmpty { - ownerError = "대표자 이름을 입력해주세요." - showToast = true - } - if storeName.isEmpty { - storeNameError = "지점 이름을 입력해주세요." - showToast = true - } - if address.isEmpty { - addressError = "주소를 입력해주세요." - showToast = true - } - if bizNo.isEmpty { - bizNoError = "사업자등록번호를 입력해주세요." - hasEmptyField = true - } - - // 빈 칸이 하나라도 있으면 종료 - guard !hasEmptyField else { return } - // 유효성 검사 함수 실행 - guard isValidForm() else { return } - - // 통과 → 회원가입 진행 - Task { - isLoading = true - await viewModel.register( - email: email, - password: password, - owner: owner, - address: address, - storeName: storeName, - bizNo: bizNo.filter { $0.isNumber } // ← 여기서 숫자만 추출해서 전송 - ) - isLoading = false - } + pwError = isValidPassword(password) ? nil : "8자 이상, 영문+숫자 조합입니다." + confirmPasswordError = (password == confirmPassword) ? nil : "비밀번호가 일치하지 않습니다" + addressError = address.isEmpty ? "주소를 입력해주세요." : nil + bizNoError = isValidBizNo(bizNo) ? nil : "형식: 000-00-00000" } + // MARK: - 사업자등록번호 자동 포맷팅 private func formatBizNoInput(_ input: String) { - // 1️⃣ 숫자만 남기기 + // 숫자만 남기기 let digitsOnly = input.filter { $0.isNumber } - // 2️⃣ 하이픈 자동 삽입 + // 하이픈 자동 삽입 var formatted = "" let length = digitsOnly.count @@ -276,12 +267,12 @@ struct RegisterView: View { formatted = "\(first)-\(middle)-\(last)" } - // 3️⃣ 10자리 이상은 자르기 + // 10자리 이상은 자르기 if digitsOnly.count > 10 { formatted = String(formatted.prefix(12)) // 하이픈 포함 } - // 4️⃣ 상태 업데이트 + // 상태 업데이트 if formatted != bizNo { bizNo = formatted } diff --git a/StockMate/StockMate/app/feature/auth/viewmodel/AuthViewModel.swift b/StockMate/StockMate/app/feature/auth/viewmodel/AuthViewModel.swift index 4ff557b..72eea38 100644 --- a/StockMate/StockMate/app/feature/auth/viewmodel/AuthViewModel.swift +++ b/StockMate/StockMate/app/feature/auth/viewmodel/AuthViewModel.swift @@ -7,12 +7,14 @@ import SwiftUI +// 인증 상태를 나타내는 enum enum AuthState { case unauthenticated case registering case authenticated } +// 인증 관련 로직(ViewModel) @MainActor final class AuthViewModel: ObservableObject { @Published var email = "" @@ -20,14 +22,16 @@ final class AuthViewModel: ObservableObject { @Published var message: String = "" @Published var authState: AuthState = .unauthenticated + // MARK: - Dependencies private let repo: AuthRepositoryProtocol + // MARK: - Init init(repo: AuthRepositoryProtocol = AuthRepositoryImpl()) { self.repo = repo } // MARK: - 로그인 - func login() async { + func login() async -> Bool{ print("로그인 시도 - email: \(email), password: \(password)") let req = LoginRequest(email: email, password: password) let result = await repo.login(req) @@ -38,7 +42,7 @@ final class AuthViewModel: ObservableObject { guard let data = apiResp.data else { print("데이터 없음: \(apiResp.message)") message = apiResp.message - return + return false } TokenStore.shared.save( access: data.accessToken, @@ -48,18 +52,22 @@ final class AuthViewModel: ObservableObject { message = "로그인 성공" authState = .authenticated print("authState 변경됨 → authenticated") + return true case .failure(let err): print("로그인 실패: \(err.message)") message = err.message + return false } } + // MARK: - 로그아웃 func logout() { TokenStore.shared.clear() authState = .unauthenticated } + // MARK: - 화면 상태 전환 func goToLogin() { authState = .unauthenticated } @@ -76,20 +84,8 @@ final class AuthViewModel: ObservableObject { address: String, storeName: String, bizNo: String - ) async { - - print( - """ - [회원가입 시도] - email: \(email) - password: \(password) - owner: \(owner) - address: \(address) - storeName: \(storeName) - businessNumber: \(bizNo) - """ - ) - + ) async -> Bool { + let req = RegisterRequest( email: email, password: password, @@ -105,8 +101,10 @@ final class AuthViewModel: ObservableObject { case .success(let apiResp): message = apiResp.message authState = .unauthenticated + return true case .failure(let err): message = err.message + return false } } diff --git a/StockMate/StockMate/app/feature/cart/data/CartApi.swift b/StockMate/StockMate/app/feature/cart/data/CartApi.swift index 9a717b4..23281f4 100644 --- a/StockMate/StockMate/app/feature/cart/data/CartApi.swift +++ b/StockMate/StockMate/app/feature/cart/data/CartApi.swift @@ -34,7 +34,6 @@ struct CartItem: Decodable, Identifiable { let stock: Int? let image: String? -// var id: Int { cartItemId } var id: Int { partId } } diff --git a/StockMate/StockMate/app/feature/cart/data/CartRepositoryImpl.swift b/StockMate/StockMate/app/feature/cart/data/CartRepositoryImpl.swift index 479f1d2..e72b3b3 100644 --- a/StockMate/StockMate/app/feature/cart/data/CartRepositoryImpl.swift +++ b/StockMate/StockMate/app/feature/cart/data/CartRepositoryImpl.swift @@ -9,13 +9,16 @@ import Foundation import Alamofire +// 장바구니 관련 API 통신을 수행하는 Repository 구현체 final class CartRepositoryImpl: CartRepositoryProtocol { + // MARK: - 장바구니 조회 func fetchCart() async -> AppResult> { let req = CartApi.fetchCart() return await safeApi(req, decodeTo: ApiResponse.self) } + // MARK: - 장바구니 추가 func addToCart( request: CartUpdateRequest ) async -> AppResult> { @@ -23,6 +26,7 @@ final class CartRepositoryImpl: CartRepositoryProtocol { return await safeApi(req, decodeTo: ApiResponse.self) } + // MARK: - 장바구니 수정 func updateCart( request: CartUpdateRequest ) async -> AppResult> { @@ -30,9 +34,9 @@ final class CartRepositoryImpl: CartRepositoryProtocol { return await safeApi(req, decodeTo: ApiResponse.self) } + // MARK: - 장바구니 비우기 func clearCart() async -> AppResult> { let req = CartApi.clearCart() return await safeApi(req, decodeTo: ApiResponse.self) } - } diff --git a/StockMate/StockMate/app/feature/cart/ui/DeliveryStatusView.swift b/StockMate/StockMate/app/feature/cart/ui/DeliveryStatusView.swift index 6a9f189..3f21bfd 100644 --- a/StockMate/StockMate/app/feature/cart/ui/DeliveryStatusView.swift +++ b/StockMate/StockMate/app/feature/cart/ui/DeliveryStatusView.swift @@ -26,7 +26,7 @@ struct DeliveryStatusView: View { var body: some View { GeometryReader { geo in - HStack(alignment: .center, spacing: 0) { + HStack(alignment: .center, spacing: 4) { ForEach(0.. DataRequest { let url = ApiClient.baseURL + "api/v1/information/order-history/my?page=\(page)&size=\(size)" return ApiClient.shared.request(url, method: .get) } - // ✅ 예치금 거래내역 조회 + // 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 index 1c8dfe4..ad9e84a 100644 --- a/StockMate/StockMate/app/feature/dashboard/data/HistoryRepositoryImpl.swift +++ b/StockMate/StockMate/app/feature/dashboard/data/HistoryRepositoryImpl.swift @@ -8,13 +8,16 @@ import Foundation import Alamofire +// === Repository Implementation === +// 입출고 및 예치금 거래내역 데이터 요청을 처리하는 구현체 final class HistoryRepositoryImpl: HistoryRepositoryProtocol { + // GET - 입출고 히스토리 조회 func getInOutHistory(page: Int, size: Int) async -> AppResult> { let request = HistoryApi.getInOutHistory(page: page, size: size) return await safeApi(request, decodeTo: ApiResponse.self) } - // ✅ 예치금 거래내역 조회 + // GET - 예치금 거래내역 조회 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 index fddd5cc..5c534fb 100644 --- a/StockMate/StockMate/app/feature/dashboard/domain/HistoryRepositoryProtocol.swift +++ b/StockMate/StockMate/app/feature/dashboard/domain/HistoryRepositoryProtocol.swift @@ -8,9 +8,13 @@ import Foundation import Alamofire +// === Repository Protocol === +// 입출고 및 예치금 거래내역 관련 데이터 요청 정의 protocol HistoryRepositoryProtocol { + + // GET - 입출고 히스토리 조회 func getInOutHistory(page: Int, size: Int) async -> AppResult> - // ✅ 예치금 거래내역 조회 + // GET - 예치금 거래내역 조회 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 index 12998e9..2f6dc2a 100644 --- a/StockMate/StockMate/app/feature/dashboard/ui/InOutHistoryView.swift +++ b/StockMate/StockMate/app/feature/dashboard/ui/InOutHistoryView.swift @@ -8,6 +8,7 @@ import SwiftUI struct InOutHistoryView: View { + @Environment(\.dismiss) private var dismiss @StateObject private var viewModel = HistoryViewModel() var body: some View { @@ -25,7 +26,7 @@ struct InOutHistoryView: View { .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) ?? "" } @@ -35,13 +36,13 @@ struct InOutHistoryView: View { 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) + .padding(.top, 8) - // ✅ 해당 날짜의 히스토리 카드들 + // 해당 날짜의 히스토리 카드들 ForEach(histories) { history in InOutHistoryCard(history: history) } @@ -54,7 +55,21 @@ struct InOutHistoryView: View { } } .background(Color.Light) - .navigationTitle("입출고 내역") + .navigationTitle("입출고 히스토리") + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.left") + .font(.system(size: 15, weight: .medium)) + } + .foregroundColor(.black) + } + } + } .task { await viewModel.fetchInOutHistory() } @@ -81,7 +96,7 @@ struct InOutHistoryCard: View { } placeholder: { Color.gray.opacity(0.2) } - .frame(width: 64, height: 64) + .frame(width: 60, height: 60) .cornerRadius(10) VStack(alignment: .leading, spacing: 4) { @@ -89,7 +104,7 @@ struct InOutHistoryCard: View { .font(.system(size: 15)) .lineLimit(1) if history.items.count > 1 { - Text("외 \(history.items.count - 1)개 품목") + Text("외 \(history.items.count - 1)개") .font(.caption) .foregroundColor(.gray) } @@ -106,7 +121,7 @@ struct InOutHistoryCard: View { .padding(.vertical, 4) .background(statusBgColor(history.status)) .foregroundColor(statusTextColor(history.status)) - .cornerRadius(8) + .cornerRadius(12) if history.status == "RECEIVED", let orderId = history.orderId { NavigationLink( @@ -128,9 +143,9 @@ struct InOutHistoryCard: View { .buttonStyle(.plain) } } - .padding() + .padding(9) .background(Color.white) - .cornerRadius(12) + .cornerRadius(16) .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 2) .padding(.horizontal) } @@ -141,8 +156,8 @@ struct InOutHistoryCard: View { func statusText(_ status: String) -> String { switch status { - case "RECEIVED": return "입고 완료" - case "RELEASED": return "출고 완료" + case "RECEIVED": return "입고" + case "RELEASED": return "출고" default: return status } } diff --git a/StockMate/StockMate/app/feature/dashboard/ui/ReleaseDetailView.swift b/StockMate/StockMate/app/feature/dashboard/ui/ReleaseDetailView.swift index d1cd2ed..edd2ede 100644 --- a/StockMate/StockMate/app/feature/dashboard/ui/ReleaseDetailView.swift +++ b/StockMate/StockMate/app/feature/dashboard/ui/ReleaseDetailView.swift @@ -8,18 +8,19 @@ import SwiftUI struct ReleaseDetailView: View { + @Environment(\.dismiss) private var dismiss 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) @@ -35,6 +36,20 @@ struct ReleaseDetailView: View { .background(Color.Light) .navigationTitle("출고 상세") .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.left") + .font(.system(size: 15, weight: .medium)) + } + .foregroundColor(.black) + } + } + } } } @@ -118,7 +133,7 @@ func formattedDate3(_ timestamp: String) -> String { let trimmed = timestamp.trimmingCharacters(in: .whitespacesAndNewlines) let parser = DateFormatter() parser.locale = Locale(identifier: "en_US_POSIX") - parser.timeZone = TimeZone(identifier: "Asia/Seoul") // ✅ 서버 시간 기준으로 맞춤 + parser.timeZone = TimeZone(identifier: "Asia/Seoul") // 서버 시간 기준으로 맞춤 var date: Date? = nil for format in inputFormats { diff --git a/StockMate/StockMate/app/feature/dashboard/ui/TransactionTypeListView.swift b/StockMate/StockMate/app/feature/dashboard/ui/TransactionTypeListView.swift index 622585a..dac5ebd 100644 --- a/StockMate/StockMate/app/feature/dashboard/ui/TransactionTypeListView.swift +++ b/StockMate/StockMate/app/feature/dashboard/ui/TransactionTypeListView.swift @@ -8,6 +8,7 @@ import SwiftUI struct TransactionTypeListView: View { + @Environment(\.dismiss) private var dismiss @StateObject private var viewModel = HistoryViewModel() var body: some View { @@ -29,6 +30,20 @@ struct TransactionTypeListView: View { .background(Color.Light.ignoresSafeArea()) .navigationTitle("예치금 히스토리") .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.left") + .font(.system(size: 15, weight: .medium)) + } + .foregroundColor(.black) + } + } + } .overlay { if viewModel.isTransactionLoading && viewModel.transactions.isEmpty { ProgressView("불러오는 중...") @@ -52,8 +67,13 @@ struct TransactionCard: View { HStack(alignment: .center, spacing: 12) { // PAY일 때만 부품 이미지 표시 - if item.transactionType == "PAY" { - // 대표 이미지 + if item.transactionType == "CHARGE" { + Image("exchange") + .foregroundColor(Color.Primary) + .frame(width: 64, height: 64) + .cornerRadius(10) + }else { + // 부품 대표 이미지 AsyncImage(url: URL(string: item.orderItems?.first?.image ?? "")) { image in image.resizable().scaledToFit() } placeholder: { @@ -61,13 +81,7 @@ struct TransactionCard: View { } .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) { @@ -87,7 +101,24 @@ struct TransactionCard: View { Text("예치금 충전") .font(.system(size: 14, weight: .bold)) .foregroundColor(.black) + } else { + HStack { + Text("주문 취소:") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(.black) + if 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) + } + } } + // 날짜 Text( item.transactionTime != nil ? formattedDate( @@ -97,19 +128,19 @@ struct TransactionCard: View { .font(.system(size: 13, weight: .regular)) .foregroundColor(.textGray1) - // ✅ 금액 표시 (PAY/CHARGE 구분) + // 금액 표시 (PAY/CHARGE 구분) let isPay = item.transactionType == "PAY" let sign = isPay ? "-" : "+" let color: Color = isPay ? .Danger : .Primary - Text("\(sign) \(formatPrice(item.totalAmount))") + Text("\(sign) \(formatPrice(item.totalAmount))원") .font(.system(size: 13, weight: .bold)) .foregroundColor(color) } .frame(height: 60, alignment: .top) Spacer() - // ✅ PAY일 때만 꺾새 표시 + // PAY일 때만 상세 페이지 연결 if item.transactionType == "PAY" { NavigationLink(destination: ReceiptView(orderId: item.orderId ?? 1)) { Image(systemName: "chevron.right") @@ -122,7 +153,7 @@ struct TransactionCard: View { } .padding() .background(Color.white) - .cornerRadius(10) + .cornerRadius(13) .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 index 0eb95d7..82fa036 100644 --- a/StockMate/StockMate/app/feature/dashboard/viewmodel/HistoryViewModel.swift +++ b/StockMate/StockMate/app/feature/dashboard/viewmodel/HistoryViewModel.swift @@ -8,34 +8,40 @@ import Foundation import Alamofire + +// === ViewModel === +// 입출고 및 예치금 거래내역 화면에서 사용할 데이터 상태 관리 @MainActor final class HistoryViewModel: ObservableObject { - // MARK: - 입출고 히스토리 관련 + + // MARK: - 입출고 히스토리 관련 상태 @Published var histories: [HistoryItem] = [] @Published var isLoading = false @Published var errorMessage: String? @Published var currentPage = 0 @Published var totalPages = 1 - // MARK: - 예치금 거래내역 관련 + // MARK: - 예치금 거래내역 관련 상태 @Published var transactions: [PaymentTransactionItem] = [] @Published var transactionPage = 0 @Published var transactionTotalPages = 1 @Published var isTransactionLoading = false + // Repository 의존성 주입 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) + let result = await repository.getInOutHistory(page: page, size: size) switch result { case .success(let response): if let data = response.data { @@ -56,7 +62,7 @@ final class HistoryViewModel: ObservableObject { } } - /// ✅ 다음 페이지 로드 (무한 스크롤 등) + // 무한 스크롤 시 다음 페이지 로드 func loadMoreIfNeeded(currentItem item: HistoryItem?) async { guard let item = item else { return } let threshold = max(histories.count - 5, 0) @@ -67,7 +73,8 @@ final class HistoryViewModel: ObservableObject { } } - // MARK: - ✅ 예치금 거래내역 불러오기 + // === 예치금 거래내역 === + // 예치금 거래내역 조회 func fetchPaymentTransactions(page: Int = 0, size: Int = 20) async { guard !isTransactionLoading else { return } isTransactionLoading = true @@ -94,13 +101,13 @@ final class HistoryViewModel: ObservableObject { } } - // MARK: - ✅ 무한 스크롤 (예치금 내역) + // 무한 스크롤 시 다음 페이지 로드 (예치금 내역) func loadMoreTransactionsIfNeeded(currentItem item: PaymentTransactionItem?) async { guard let item = item else { return } guard !isTransactionLoading else { return } // 중복 로드 방지 guard transactionPage + 1 < transactionTotalPages else { return } // 마지막 페이지 방지 - // ✅ 안전한 threshold 계산 + // 안전한 threshold 계산 let thresholdIndex = max(transactions.count - 5, 0) if let currentIndex = transactions.firstIndex(where: { $0.transactionId == item.transactionId }), currentIndex >= thresholdIndex { diff --git a/StockMate/StockMate/app/feature/inventory/data/InventoryApi.swift b/StockMate/StockMate/app/feature/inventory/data/InventoryApi.swift index a4ef6e9..058b8f1 100644 --- a/StockMate/StockMate/app/feature/inventory/data/InventoryApi.swift +++ b/StockMate/StockMate/app/feature/inventory/data/InventoryApi.swift @@ -5,10 +5,11 @@ // Created by Admin on 10/15/25. // - import Foundation import Alamofire + +// MARK: - 재고 조회 응답 구조 struct InventoryResponse: Decodable { let status: Int let success: Bool @@ -16,6 +17,7 @@ struct InventoryResponse: Decodable { let data: InventoryPageData? } +// MARK: - 페이징 데이터 struct InventoryPageData: Decodable { let content: [InventoryItem] let page: Int @@ -24,6 +26,7 @@ struct InventoryPageData: Decodable { let totalPages: Int } +// MARK: - 개별 재고 아이템 struct InventoryItem: Decodable, Identifiable { let id: Int let name: String @@ -41,6 +44,7 @@ struct InventoryItem: Decodable, Identifiable { let isLack: Bool } +// MARK: - 카테고리별 부족 재고 개수 struct LackCountItem: Decodable, Identifiable { var id: String { categoryName } // SwiftUI ForEach에서 식별자 사용 let categoryName: String @@ -48,6 +52,7 @@ struct LackCountItem: Decodable, Identifiable { } +// MARK: 재고 목록 조회 enum InventoryApi { static func getInventoryList( page: Int, @@ -71,7 +76,7 @@ enum InventoryApi { return ApiClient.shared.request(url, method: .get) } - // ✅ 부족 재고 조회 API 추가 + // MARK: 부족 재고 목록 조회 static func getUnderLimitList(categoryName: String? = nil, page: Int = 0, size: Int = 10) -> DataRequest { var url = ApiClient.baseURL + "api/v1/store/under-limit?page=\(page)&size=\(size)" if let categoryName = categoryName, !categoryName.isEmpty { @@ -80,14 +85,14 @@ enum InventoryApi { return ApiClient.shared.request(url, method: .get) } - // ✅ 부품 이름으로 검색 + // MARK: 부품명 검색 static func findByName(name: String, page: Int, size: Int) -> DataRequest { let encodedName = name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" let url = ApiClient.baseURL + "api/v1/store/find-name?name=\(encodedName)&page=\(page)&size=\(size)" return ApiClient.shared.request(url, method: .get) } - // ✅ 카테고리별 부족 재고 개수 조회 + // MARK: 카테고리별 부족 재고 개수 조회 static func getLackCountByCategory() -> DataRequest { let url = ApiClient.baseURL + "api/v1/store/lack-count" return ApiClient.shared.request(url, method: .get) diff --git a/StockMate/StockMate/app/feature/inventory/data/InventoryRepositoryImpl.swift b/StockMate/StockMate/app/feature/inventory/data/InventoryRepositoryImpl.swift index 93a8885..8bc5dac 100644 --- a/StockMate/StockMate/app/feature/inventory/data/InventoryRepositoryImpl.swift +++ b/StockMate/StockMate/app/feature/inventory/data/InventoryRepositoryImpl.swift @@ -8,7 +8,10 @@ import Foundation import Alamofire +// MARK: - 재고 관련 Repository 구현체 final class InventoryRepositoryImpl: InventoryRepositoryProtocol { + + // MARK: 재고 리스트 조회 func getInventoryList( page: Int, size: Int, @@ -25,7 +28,8 @@ final class InventoryRepositoryImpl: InventoryRepositoryProtocol { ) return await safeApi(dataReq, decodeTo: ApiResponse.self) } - // 부족 재고 리스트 호출 + + // MARK: 부족 재고 목록 조회 func getUnderLimitList( categoryName: String?, page: Int, @@ -35,7 +39,7 @@ final class InventoryRepositoryImpl: InventoryRepositoryProtocol { return await safeApi(dataReq, decodeTo: ApiResponse.self) } - // ✅ 이름 검색 + // MARK: 부품명 검색 func findByName( name: String, page: Int, @@ -45,10 +49,9 @@ final class InventoryRepositoryImpl: InventoryRepositoryProtocol { return await safeApi(dataReq, decodeTo: ApiResponse.self) } - // 카테고리별 부족 재고 개수 조회 + // MARK: 카테고리별 부족 재고 개수 조회 func getLackCountByCategory() async -> AppResult> { let dataReq = InventoryApi.getLackCountByCategory() return await safeApi(dataReq, decodeTo: ApiResponse<[LackCountItem]>.self) } - } diff --git a/StockMate/StockMate/app/feature/inventory/domain/InventoryRepositoryProtocol.swift b/StockMate/StockMate/app/feature/inventory/domain/InventoryRepositoryProtocol.swift index fe2a090..19873d5 100644 --- a/StockMate/StockMate/app/feature/inventory/domain/InventoryRepositoryProtocol.swift +++ b/StockMate/StockMate/app/feature/inventory/domain/InventoryRepositoryProtocol.swift @@ -8,7 +8,9 @@ import Foundation import Alamofire +// MARK: - 재고 관련 Repository 프로토콜 protocol InventoryRepositoryProtocol { + // MARK: 재고 리스트 조회 func getInventoryList( page: Int, size: Int, @@ -17,19 +19,21 @@ protocol InventoryRepositoryProtocol { models: [String] ) async -> AppResult> + // MARK: 부족 재고 목록 조회 func getUnderLimitList( categoryName: String?, page: Int, size: Int ) async -> AppResult> - // 이름 검색 + // MARK: 부품명 검색 func findByName( name: String, page: Int, size: Int ) async -> AppResult> + // MARK: 카테고리별 부족 재고 개수 조회 func getLackCountByCategory() async -> AppResult> } diff --git a/StockMate/StockMate/app/feature/inventory/ui/IncomingScanView.swift b/StockMate/StockMate/app/feature/inventory/ui/IncomingScanView.swift index 18e375a..7f23226 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/IncomingScanView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/IncomingScanView.swift @@ -13,15 +13,15 @@ struct IncomingScanView: View { @State private var showAlert = false @State private var alertMessage = "" - @StateObject private var orderViewModel = OrderViewModel() // ✅ 뷰모델 추가 + @StateObject private var orderViewModel = OrderViewModel() // 뷰모델 추가 var body: some View { ZStack { - // ✅ 1. 카메라 화면 (QR 스캐너) + // 1. 카메라 화면 (QR 스캐너) QRScannerView(scannedCode: $scannedCode) .ignoresSafeArea() - // ✅ 2. 스캔 영역 가이드 박스 + // 2. 스캔 영역 가이드 박스 VStack { Text("입고 부품의 QR을 스캔해주세요") .font(.headline) @@ -43,25 +43,9 @@ struct IncomingScanView: View { .padding(.bottom, 180) Spacer() - - // ✅ 직접 등록 버튼 - Button(action: { - dismiss() - }) { - Text("직접 등록 하기") - .fontWeight(.semibold) - .foregroundColor(.black) - .frame(maxWidth: .infinity) - .padding() - .background(Color.white) - .cornerRadius(10) - .shadow(color: .gray.opacity(0.3), radius: 2, x: 0, y: 2) - } - .padding(.horizontal, 40) - .padding(.bottom, 40) } - // ✅ 로딩 표시 + // 로딩 표시 if orderViewModel.isLoading { Color.black.opacity(0.3).ignoresSafeArea() ProgressView("입고 처리 중...") @@ -85,6 +69,20 @@ struct IncomingScanView: View { } .navigationTitle("입고 부품 등록") .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.left") + .font(.system(size: 15, weight: .medium)) + } + .foregroundColor(.black) + } + } + } } private func handleScannedCode(_ code: String) async { diff --git a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift index 000cc5c..1fd2b69 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/InventorySearchView.swift @@ -9,6 +9,7 @@ import SwiftUI struct InventorySearchView: View { + @Environment(\.dismiss) private var dismiss @StateObject private var inventoryViewModel = InventoryViewModel() @State private var searchText = "" @@ -36,7 +37,7 @@ struct InventorySearchView: View { var body: some View { NavigationStack { VStack(spacing: 0) { - // 🔍 검색창 + // 검색창 HStack { Image(systemName: "magnifyingglass") .foregroundColor(.gray) @@ -47,7 +48,6 @@ struct InventorySearchView: View { let term = searchText.trimmingCharacters(in: .whitespacesAndNewlines) guard !term.isEmpty else { return } Task { -// await inventoryViewModel.searchByName(name: searchText, reset: true) await inventoryViewModel.searchByName(name: term, reset: true) } } @@ -97,17 +97,21 @@ struct InventorySearchView: View { onTap: { inventoryViewModel.toggleModel($0) } ) - // 🔄 초기화 버튼 + // 초기화 버튼 Button(action: { inventoryViewModel.resetFilters(with: searchText) }) { - HStack(spacing: 4) { - Image(systemName: "arrow.counterclockwise") - Text("초기화") - } - .font(.system(size: 13, weight: .medium)) - .foregroundColor(.blue) - .padding(.trailing, 8) + Image(systemName: "arrow.clockwise") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor( + inventoryViewModel.selectedCategories.isEmpty && + inventoryViewModel.selectedTrims.isEmpty && + inventoryViewModel.selectedModels.isEmpty + ? .black + : .Primary + ) + .padding(.trailing, 8) + .rotationEffect(.degrees(35)) } .frame(maxWidth: .infinity, alignment: .trailing) @@ -115,7 +119,7 @@ struct InventorySearchView: View { .padding(.horizontal) .padding(.bottom, 16) - // 📋 재고 리스트 + // 재고 리스트 ScrollView { LazyVStack(spacing: 10) { @@ -152,6 +156,23 @@ struct InventorySearchView: View { } .background(Color.Light) .navigationTitle("재고 조회") + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.left") + .font(.system(size: 15, weight: .medium)) + } + .foregroundColor(.black) + } + } + } + .onTapGesture { + UIApplication.shared.hideKeyboard() + } .task { await inventoryViewModel.loadInventoryList(reset: true) } @@ -169,8 +190,6 @@ struct FilterMenu: View { var displayTitle: String { if selectedItems.isEmpty { return title - } else if selectedItems.count == 1 { - return selectedItems.first ?? title } else { return "\(title) (\(selectedItems.count))" } @@ -178,15 +197,6 @@ struct FilterMenu: View { var isActive: Bool { !selectedItems.isEmpty } - var truncatedTitle: String { - // 글자 6자까지만 표시, 이후 "..." 처리 - if displayTitle.count > 8 { - let prefix = displayTitle.prefix(8) - return "\(prefix)…" - } - return displayTitle - } - var body: some View { Menu { ForEach(items, id: \.self) { item in @@ -198,7 +208,7 @@ struct FilterMenu: View { if selectedItems.contains(item) { Spacer() Image(systemName: "checkmark") - .foregroundColor(.blue) + .foregroundColor(.Primary) } } } @@ -207,19 +217,21 @@ struct FilterMenu: View { HStack(spacing: 6) { Image(systemName: "chevron.down") .font(.system(size: 11, weight: .semibold)) - .foregroundColor(isActive ? .blue : .gray) + .foregroundColor(isActive ? .Primary : .gray) - Text(truncatedTitle) + Text(displayTitle) .font(.system(size: 13)) - .foregroundColor(isActive ? .blue : .black) + .foregroundColor(isActive ? .Primary : .black) .lineLimit(1) - .truncationMode(.tail) // 안전하게 "..." 처리 - .multilineTextAlignment(.center) + .truncationMode(.tail) } .padding(.horizontal, 12) - .padding(.vertical, 10) // 높이 늘림 - .background( - isActive ? Color.blue.opacity(0.2) : Color(.systemGray6) + .padding(.vertical, 9) + .fixedSize(horizontal: true, vertical: false) + .background(isActive ? Color(hex: "DBEAFE") : Color.Light) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.gray.opacity(0.3), lineWidth: 1) ) .cornerRadius(8) } diff --git a/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift b/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift index 70063c4..c6a598c 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift @@ -26,8 +26,6 @@ struct InventoryView: View { GeometryReader { geo in Color.clear .onChange(of: geo.frame(in: .global).minY) { newValue in - // 👇 스크롤 시 값이 변함 - //print("📏 Scroll offsetY:", newValue) // 테스트용, 화면 안정화 후 제거 withAnimation(.easeInOut(duration: 0.25)) { showScrollToTopButton = newValue < -150 } @@ -47,7 +45,7 @@ struct InventoryView: View { GridMenuView() Text("얼마 남지 않았어요!") - .font(.system(size: 22, weight: .semibold)) + .font(.system(size: 20, weight: .semibold)) .foregroundColor(.black) .padding(.horizontal, 25) .padding(.top) @@ -91,15 +89,15 @@ struct InventoryView: View { Circle() .fill(Color.Primary) // 배경색 .frame(width: 50, height: 50) - Image(systemName: "arrow.up") + Image(systemName: "chevron.up") .font( - .system(size: 24, weight: .bold) + .system(size: 14, weight: .semibold) ) .foregroundColor(.white) // 화살표 색 } } .padding(.trailing, 20) - .padding(.bottom, 20) + .padding(.bottom, 15) } } .transition(.opacity) @@ -145,34 +143,36 @@ struct GridMenuView: View { Spacer() ZStack { // 타원 배경 - Rectangle() - .fill(item.1 ? Color.white.opacity(0.2) : Color.Primary) - .frame(width: 35, height: 26) - .cornerRadius(80) + RoundedRectangle(cornerRadius: 12) + .fill(item.1 ? Color.white.opacity(0.28) : Color.Primary.opacity(0.15)) + .frame(width: 32, height: 32) // 아이콘 Image(item.2) .renderingMode(.template) .resizable() .scaledToFit() - .frame(width: 14, height: 14) - .foregroundColor(.white) + .frame(width: 19, height: 19) + .foregroundColor(item.1 ? .white : .Primary) } } + .padding(.top, 34) + Text(item.0) .font(.system(size: 15, weight: .semibold)) .foregroundColor(item.1 ? .white : Color.Primary) + .padding(.leading, 5) + .padding(.top, 5) + .padding(.bottom, 40) } - .padding(.horizontal, 20) - .padding(.vertical, 20) + .padding(12) .frame(height: 99) .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: 24) .fill(item.1 ? Color.Primary : Color.white) - // 카드 그림자 (Figma 스펙: y=4, blur=4, opacity=25%, black) - .shadow(color: .black.opacity(0.35), radius: 2, x: 0, y: 4) + .shadow(color: .black.opacity(0.35), radius: 2, x: 0, y: 4) // 카드 그림자 (Figma 스펙: y=4, blur=4, opacity=25%, black) ) } .buttonStyle(.plain) diff --git a/StockMate/StockMate/app/feature/inventory/ui/LackListView.swift b/StockMate/StockMate/app/feature/inventory/ui/LackListView.swift index b9ca69a..def5f29 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/LackListView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/LackListView.swift @@ -8,6 +8,7 @@ import SwiftUI struct LackListView: View { + @Environment(\.dismiss) private var dismiss @StateObject private var inventoryViewModel = InventoryViewModel() @State private var isFirstAppear = true @@ -17,23 +18,42 @@ struct LackListView: View { var body: some View { VStack(spacing: 0) { - // 상단 카테고리 탭 - HStack(spacing: 8) { - ForEach(categories, id: \.self) { category in - CategoryButton( - title: category, - isSelected: selectedCategory == category - ) { - Task { - selectedCategory = category - inventoryViewModel.selectedCategories = [category] - await inventoryViewModel.loadUnderLimitList(reset: true) + // 상단 카테고리 탭 (가로 스크롤 가능) + ScrollViewReader { proxy in + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(categories, id: \.self) { category in + CategoryButton( + title: category, + isSelected: selectedCategory == category + ) { + Task { + selectedCategory = category + inventoryViewModel.selectedCategories = [category] + await inventoryViewModel.loadUnderLimitList(reset: true) + + // 버튼 클릭 시 해당 카테고리로 스크롤 이동 + withAnimation { + proxy.scrollTo(category, anchor: .center) + } + } + } + .id(category) // ScrollViewReader용 id + } + } + .padding(.horizontal) + .padding(.vertical, 10) + } + .onAppear { + // 진입 시 선택된 카테고리 위치로 자동 스크롤 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + withAnimation { + proxy.scrollTo(selectedCategory, anchor: .center) + } } } } - } - .padding(.horizontal) - .padding(.vertical, 10) + // 리스트 ScrollView { @@ -63,6 +83,20 @@ struct LackListView: View { .background(Color.Light) .navigationTitle("부족 재고") .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.left") + .font(.system(size: 15, weight: .medium)) + } + .foregroundColor(.black) + } + } + } .task { if isFirstAppear { isFirstAppear = false @@ -73,26 +107,3 @@ struct LackListView: View { } } -struct CategoryButton: View { - let title: String - let isSelected: Bool - let action: () -> Void - - var body: some View { - Button(action: action) { - Text(title) - .font(.system(size: 11, weight: .regular)) - .foregroundColor(isSelected ? .Primary : .black) - .padding(.vertical, 8) - .padding(.horizontal, 12) - .background( - RoundedRectangle(cornerRadius: 16) - .fill(Color.Light) - ) - .overlay( - RoundedRectangle(cornerRadius: 16) - .stroke(isSelected ? Color.Primary : Color.GrayStroke, lineWidth: 1) - ) - } - } -} diff --git a/StockMate/StockMate/app/feature/inventory/ui/OutgoingScanView.swift b/StockMate/StockMate/app/feature/inventory/ui/OutgoingScanView.swift index af8bb03..38a894e 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/OutgoingScanView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/OutgoingScanView.swift @@ -16,7 +16,7 @@ struct OutgoingScanView: View { // 전역 부품 저장소 @EnvironmentObject var partStore: PartStore - @StateObject private var partViewModel = PartViewModel() // ✅ ViewModel 추가 + @StateObject private var partViewModel = PartViewModel() @State private var showBottomSheet = false @@ -27,14 +27,12 @@ struct OutgoingScanView: View { var body: some View { ZStack { - // ✅ 카메라 미리보기 (QR 스캐너) -// QRScannerView(scannedCode: $scannedCode) -// .ignoresSafeArea() + // 카메라 미리보기 (QR 스캐너) QRScannerView(scannedCode: $scannedCode, isActive: !showBottomSheet) .ignoresSafeArea() - // ✅ 스캔 가이드 및 UI 오버레이 + // 스캔 가이드 및 UI 오버레이 VStack { Text("사용할 부품의 QR을 스캔해주세요") .font(.headline) @@ -44,38 +42,23 @@ struct OutgoingScanView: View { Spacer() - // 📷 스캔 박스 + // 스캔 박스 ZStack { RoundedRectangle(cornerRadius: 12) .fill(Color.clear) .frame(width: 250, height: 250) RoundedRectangle(cornerRadius: 8) - .stroke(Color.green, lineWidth: 3) + .stroke(Color.Primary, lineWidth: 3) .frame(width: 220, height: 220) } .padding(.bottom, 180) Spacer() - // 📦 직접 입력 버튼 - Button(action: { - dismiss() - }) { - Text("직접 입력 하기") - .fontWeight(.semibold) - .foregroundColor(.black) - .frame(maxWidth: .infinity) - .padding() - .background(Color.white) - .cornerRadius(10) - .shadow(color: .gray.opacity(0.3), radius: 2, x: 0, y: 2) - } - .padding(.horizontal, 40) - .padding(.bottom, 40) } - // ✅ 로딩 인디케이터 + // 로딩 인디케이터 if partViewModel.isLoading { Color.black.opacity(0.3).ignoresSafeArea() ProgressView("부품 조회 중...") //ProgressView("부품 사용 처리 중...") @@ -97,12 +80,12 @@ struct OutgoingScanView: View { UsedPartListSheetView( onUseParts: { Task { - // ✅ payload 생성 + // payload 생성 let payload = partStore.parts.map { ReleaseItemRequest(partId: $0.id, quantity: $0.quantity) } - // ✅ API 호출 + // API 호출 let result = await partViewModel.releaseParts(items: payload) await MainActor.run { @@ -111,7 +94,7 @@ struct OutgoingScanView: View { alertMessage = message showAlert = true - // ✅ 성공 시: 전역 부품 초기화 + 바텀시트 닫기 + 화면 복귀 + // 성공 시: 전역 부품 초기화 + 바텀시트 닫기 + 화면 복귀 partStore.clear() showBottomSheet = false DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { @@ -126,20 +109,34 @@ struct OutgoingScanView: View { } }, onRescan: { - // ✅ 다시 스캔 버튼 눌렀을 때 + // 다시 스캔 버튼 눌렀을 때 showBottomSheet = false resetScanState() // QR 다시 활성화 } ) .presentationDetents([.fraction(0.80)]) // 시트 높이 80% - .presentationCornerRadius(28) // ✅ 모서리 곡률 + .presentationCornerRadius(28) .environmentObject(partStore) } .navigationTitle("부품 사용 처리") .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.left") + .font(.system(size: 15, weight: .medium)) + } + .foregroundColor(.black) + } + } + } } - // ✅ 스캔된 코드로 부품 상세 조회만 수행 (출고 X) + // 스캔된 코드로 부품 상세 조회만 수행 (출고 X) private func handleScannedCode(_ code: String) async { await MainActor.run { partViewModel.isLoading = true } @@ -153,10 +150,10 @@ struct OutgoingScanView: View { return } - // ✅ 부품 상세 조회 API 호출 + // 부품 상세 조회 API 호출 await partViewModel.fetchPartDetail(partIds: partId) - // ✅ 결과 출력 + // 결과 출력 await MainActor.run { partViewModel.isLoading = false @@ -166,7 +163,7 @@ struct OutgoingScanView: View { return } - // ✅ 응답을 PartDetail로 변환 후 저장 + // 응답을 PartDetail로 변환 후 저장 let newPart = PartDetail( id: response.id, price: response.price, @@ -180,17 +177,17 @@ struct OutgoingScanView: View { partStore.addPart(newPart) - // ✅ 자동으로 바텀시트 열기 + // 자동으로 바텀시트 열기 showBottomSheet = true - // ✅ 스캔 상태 초기화 (다시 스캔 가능하도록) + // 스캔 상태 초기화 (다시 스캔 가능하도록) resetScanState() - print("✅ \(newPart.korName) 부품이 전역 Store에 추가됨") + print("\(newPart.korName) 부품이 전역 Store에 추가됨") } } - // ✅ 상태 초기화 (QR 다시 활성화) + // 상태 초기화 (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 index f644a0a..8e5b5cc 100644 --- a/StockMate/StockMate/app/feature/inventory/ui/UsedPartListSheetView.swift +++ b/StockMate/StockMate/app/feature/inventory/ui/UsedPartListSheetView.swift @@ -5,24 +5,16 @@ // 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 onUseParts: (() -> Void)? // ‘사용 처리’ 버튼 액션 콜백 + var onRescan: (() -> Void)? // 다시 스캔 콜백 추가 var body: some View { VStack (alignment: .center){ - // ✅ 상단 헤더 + // 상단 헤더 ZStack { Text("사용할 부품") .font(.system(size: 18, weight: .bold)) @@ -40,7 +32,7 @@ struct UsedPartListSheetView: View { .background(Color.white) - // ✅ 내용 영역 + // 내용 영역 ScrollView { if partStore.parts.isEmpty { // 부품 목록 전체 삭제의 경우 @@ -56,7 +48,7 @@ struct UsedPartListSheetView: View { .frame(maxWidth: .infinity, minHeight: 200) } else { LazyVStack { - ForEach($partStore.parts) { $part in // ✅ 바인딩으로 변경 ($ 붙임) + ForEach($partStore.parts) { $part in // 바인딩으로 변경 ($ 붙임) VStack(alignment: .leading, spacing: 6) { Text(part.categoryName) .font(.system(size: 12, weight: .semibold)) @@ -95,7 +87,7 @@ struct UsedPartListSheetView: View { Spacer() - // ✅ 수량 조절 버튼 (디자인 개선) + // 수량 조절 버튼 (디자인 개선) HStack(spacing: 10) { Button { partStore.decreaseQuantityOrRemove(for: part) @@ -121,7 +113,7 @@ struct UsedPartListSheetView: View { .padding(.horizontal, 8) .background(Color.white) .cornerRadius(10) - .overlay( // ✅ 테두리 추가 + .overlay( RoundedRectangle(cornerRadius: 10) .stroke( Color.LightBlue03, lineWidth: 1) ) @@ -146,7 +138,7 @@ struct UsedPartListSheetView: View { .frame(maxHeight: .infinity) HStack{ - // 🔹 다시 스캔 버튼 + // 다시 스캔 버튼 Button { onRescan?() } label: { @@ -164,7 +156,7 @@ struct UsedPartListSheetView: View { } .padding(.bottom, 16) - // ✅ 사용 처리 버튼 + // 사용 처리 버튼 Button { onUseParts?() } label: { @@ -182,12 +174,11 @@ struct UsedPartListSheetView: View { .padding(.bottom, 16) } .padding(.horizontal) - } } } -// MARK: - 프리뷰 (안전 버전) +// MARK: - 프리뷰 @MainActor struct UsedPartListSheetView_Previews: PreviewProvider { static var previewStore: PartStore = { @@ -220,8 +211,8 @@ struct UsedPartListSheetView_Previews: PreviewProvider { static var previews: some View { NavigationStack { UsedPartListSheetView( - onUseParts: { print("✅ 사용 처리 버튼 눌림") }, - onRescan: { print("🔄 다시 스캔 버튼 눌림") } + onUseParts: { print("사용 처리 버튼 눌림") }, + onRescan: { print("다시 스캔 버튼 눌림") } ) .environmentObject(previewStore) } diff --git a/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift b/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift index a9e1329..1b466b5 100644 --- a/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift +++ b/StockMate/StockMate/app/feature/inventory/viewmodel/InventoryViewModel.swift @@ -97,7 +97,6 @@ final class InventoryViewModel: ObservableObject { // MARK: - 부족 재고 로드 func loadUnderLimitList(reset: Bool = false, size: Int = 10) async { -// guard !isLoading, underLimitHasMore else { return } guard !isLoading, (underLimitHasMore || reset) else { return } isLoading = true @@ -195,15 +194,12 @@ final class InventoryViewModel: ObservableObject { } else { selectedCategories.append(name) } - // ✅ 검색 중일 때는 로컬 필터링만 다시 계산 + // 검색 중일 때는 로컬 필터링만 다시 계산 if isSearching { objectWillChange.send() } else { Task { await resetAndLoad() } } - -// isSearching = false -// Task { await resetAndLoad() } } func toggleTrim(_ trim: String) { @@ -217,8 +213,6 @@ final class InventoryViewModel: ObservableObject { } else { Task { await resetAndLoad() } } -// isSearching = false -// Task { await resetAndLoad() } } func toggleModel(_ model: String) { @@ -232,8 +226,6 @@ final class InventoryViewModel: ObservableObject { } else { Task { await resetAndLoad() } } -// isSearching = false -// Task { await resetAndLoad() } } // MARK: - 필터 초기화 @@ -253,9 +245,7 @@ final class InventoryViewModel: ObservableObject { // 3. 검색어가 있는 경우 → 해당 검색어로 전체 결과 다시 검색 else { isSearching = true - //searchPage = 0 searchResults.removeAll() -// Task { await searchByName(name: searchText, reset: true) } Task { await searchByName( name: searchText.trimmingCharacters(in: .whitespacesAndNewlines), @@ -265,7 +255,7 @@ final class InventoryViewModel: ObservableObject { } } - // MARK: - ✅ 무한 스크롤 로드 + // MARK: - 무한 스크롤 로드 func loadMore(searchText: String) async { if isSearching { guard !searchText.trimmingCharacters(in: .whitespaces).isEmpty else { return } diff --git a/StockMate/StockMate/app/feature/notification/data/NotificationApi.swift b/StockMate/StockMate/app/feature/notification/data/NotificationApi.swift index b9c139a..a2513c7 100644 --- a/StockMate/StockMate/app/feature/notification/data/NotificationApi.swift +++ b/StockMate/StockMate/app/feature/notification/data/NotificationApi.swift @@ -8,6 +8,7 @@ import Foundation import Alamofire +// MARK: - 알림 데이터 구조 struct NotificationItem: Decodable, Identifiable { let id: Int let orderId: Int @@ -17,33 +18,34 @@ struct NotificationItem: Decodable, Identifiable { let read: Bool } +// MARK: - 알림 관련 API enum NotificationApi { - // 1️⃣ 알림 전체 조회 + // MARK: 알림 전체 조회 static func getAllNotifications() -> DataRequest { let url = ApiClient.baseURL + "api/v1/order/store/notifications/" return ApiClient.shared.request(url, method: .get) } - // 2️⃣ 읽지 않은 알림 개수 조회 + // MARK: 읽지 않은 알림 개수 조회 static func getUnreadCount() -> DataRequest { let url = ApiClient.baseURL + "api/v1/order/store/notifications/unread/count" return ApiClient.shared.request(url, method: .get) } - // 3️⃣ 읽지 않은 알림 조회 + // MARK: 읽지 않은 알림 목록 조회 static func getUnreadNotifications() -> DataRequest { let url = ApiClient.baseURL + "api/v1/order/store/notifications/unread" return ApiClient.shared.request(url, method: .get) } - // 4️⃣ 전체 알림 읽음 처리 + // MARK: 전체 알림 읽음 처리 static func markAllAsRead() -> DataRequest { let url = ApiClient.baseURL + "api/v1/order/store/notifications/read-all" return ApiClient.shared.request(url, method: .patch) } - // 5️⃣ 개별 알림 읽음 처리 + // MARK: 개별 알림 읽음 처리 static func markAsRead(notificationId: Int) -> DataRequest { let url = ApiClient.baseURL + "api/v1/order/store/notifications/read?notificationId=\(notificationId)" return ApiClient.shared.request(url, method: .patch) diff --git a/StockMate/StockMate/app/feature/notification/data/NotificationRepositoryImpl.swift b/StockMate/StockMate/app/feature/notification/data/NotificationRepositoryImpl.swift index b5ec459..47d6593 100644 --- a/StockMate/StockMate/app/feature/notification/data/NotificationRepositoryImpl.swift +++ b/StockMate/StockMate/app/feature/notification/data/NotificationRepositoryImpl.swift @@ -8,28 +8,32 @@ import SwiftUI import Alamofire +// 알림 관련 API 호출을 담당하는 Repository 구현체 final class NotificationRepositoryImpl: NotificationRepositoryProtocol { - + // 전체 알림 목록 조회 func getAllNotifications() async -> AppResult> { let request = NotificationApi.getAllNotifications() return await safeApi(request, decodeTo: ApiResponse<[NotificationItem]>.self) } - + // 읽지 않은 알림 개수 조회 func getUnreadCount() async -> AppResult> { let request = NotificationApi.getUnreadCount() return await safeApi(request, decodeTo: ApiResponse.self) } + // 읽지 않은 알림 목록 조회 func getUnreadNotifications() async -> AppResult> { let request = NotificationApi.getUnreadNotifications() return await safeApi(request, decodeTo: ApiResponse<[NotificationItem]>.self) } + // 모든 알림을 읽음 처리 func markAllAsRead() async -> AppResult> { let request = NotificationApi.markAllAsRead() return await safeApi(request, decodeTo: ApiResponse.self) } + // 특정 알림을 읽음 처리 func markAsRead(notificationId: Int) async -> AppResult> { let request = NotificationApi.markAsRead(notificationId: notificationId) return await safeApi(request, decodeTo: ApiResponse.self) diff --git a/StockMate/StockMate/app/feature/notification/domain/NotificationRepositoryProtocol.swift b/StockMate/StockMate/app/feature/notification/domain/NotificationRepositoryProtocol.swift index 207f460..07fe713 100644 --- a/StockMate/StockMate/app/feature/notification/domain/NotificationRepositoryProtocol.swift +++ b/StockMate/StockMate/app/feature/notification/domain/NotificationRepositoryProtocol.swift @@ -10,18 +10,18 @@ import Alamofire protocol NotificationRepositoryProtocol { - // 1️⃣ 전체 알림 조회 + // 전체 알림 조회 func getAllNotifications() async -> AppResult> - // 2️⃣ 읽지 않은 알림 개수 조회 + // 읽지 않은 알림 개수 조회 func getUnreadCount() async -> AppResult> - // 3️⃣ 읽지 않은 알림 조회 + // 읽지 않은 알림 조회 func getUnreadNotifications() async -> AppResult> - // 4️⃣ 전체 알림 읽음 처리 + // 전체 알림 읽음 처리 func markAllAsRead() async -> AppResult> - // 5️⃣ 개별 알림 읽음 처리 + // 개별 알림 읽음 처리 func markAsRead(notificationId: Int) async -> AppResult> } diff --git a/StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift b/StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift index c04134c..7df3f6a 100644 --- a/StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift +++ b/StockMate/StockMate/app/feature/notification/ui/NotificationListView.swift @@ -7,6 +7,7 @@ import SwiftUI struct NotificationListView: View { + @Environment(\.dismiss) private var dismiss @StateObject private var notificationViewModel = NotificationViewModel() @State private var selectedOrderId: Int? = nil @@ -29,7 +30,19 @@ struct NotificationListView: View { .background(Color.Light) .navigationTitle("알림") .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.left") + .font(.system(size: 15, weight: .medium)) + } + .foregroundColor(.black) + } + } ToolbarItem(placement: .navigationBarTrailing) { Button("전체 읽음") { Task { await notificationViewModel.markAllAsRead() } @@ -73,7 +86,7 @@ struct NotificationCardView: View { } Spacer() - // 🔴 안 읽은 알림 표시 점 + // 안 읽은 알림 표시 삘간 점 if !item.read { Circle() .fill(Color.red) diff --git a/StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift b/StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift index a2fb0de..8c5d479 100644 --- a/StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift +++ b/StockMate/StockMate/app/feature/notification/viewmodel/NotificationViewModel.swift @@ -7,12 +7,14 @@ import Foundation +// 알림 데이터를 관리하고, API 요청을 통해 상태를 갱신하는 ViewModel @MainActor final class NotificationViewModel: ObservableObject { @Published var notifications: [NotificationItem] = [] - @Published var unreadCount: Int = 0 // 🔴 추가 + @Published var unreadCount: Int = 0 @Published var isLoading = false + // 알림 데이터 요청용 Repository private let repository: NotificationRepositoryProtocol = NotificationRepositoryImpl() // 전체 알림 조회 @@ -29,7 +31,7 @@ final class NotificationViewModel: ObservableObject { } } - // 🔴 읽지 않은 개수 조회 + // 읽지 않은 개수 조회 func fetchUnreadCount() async { let result = await repository.getUnreadCount() switch result { @@ -55,7 +57,7 @@ final class NotificationViewModel: ObservableObject { read: true ) } - unreadCount = max(0, unreadCount - 1) // 🔴 카운트 즉시 반영 + unreadCount = max(0, unreadCount - 1) // 카운트 즉시 반영 case .failure(let error): print("❌ 알림 읽음 처리 실패:", error.localizedDescription) } @@ -76,7 +78,7 @@ final class NotificationViewModel: ObservableObject { read: true ) } - unreadCount = 0 // 🔴 전체 읽음 시 0으로 초기화 + unreadCount = 0 // 전체 읽음 시 0으로 초기화 case .failure(let error): print("❌ 전체 읽음 실패:", error.localizedDescription) } diff --git a/StockMate/StockMate/app/feature/orders/data/OrderApi.swift b/StockMate/StockMate/app/feature/orders/data/OrderApi.swift index bfa026e..13159e1 100644 --- a/StockMate/StockMate/app/feature/orders/data/OrderApi.swift +++ b/StockMate/StockMate/app/feature/orders/data/OrderApi.swift @@ -10,6 +10,7 @@ import Alamofire // MARK: - Response Models +// 주문 목록 응답 모델 struct OrderListResponse: Decodable { let status: Int let success: Bool @@ -17,6 +18,7 @@ struct OrderListResponse: Decodable { let data: OrderPageData? } +// 주문 상세 응답 모델 struct OrderDetailResponse: Decodable { let status: Int let success: Bool @@ -24,7 +26,7 @@ struct OrderDetailResponse: Decodable { let data: OrderResponseItem? } - +// 주문 페이지 데이터 struct OrderPageData: Decodable { let totalElements: Int let totalPages: Int @@ -34,6 +36,7 @@ struct OrderPageData: Decodable { let last: Bool } +// 주문 항목 정보 struct OrderResponseItem: Decodable, Identifiable { var id: Int { orderId } @@ -55,6 +58,7 @@ struct OrderResponseItem: Decodable, Identifiable { let updatedAt: String } +// 주문자 정보 struct OrderUserInfo: Decodable { let id: Int let memberId: Int @@ -69,12 +73,14 @@ struct OrderUserInfo: Decodable { let longitude: Double } +// 주문된 부품 항목 struct OrderItem: Decodable { let partId: Int let amount: Int let partDetail: OrderPartDetail } +// 주문 부품 상세 정보 struct OrderPartDetail: Decodable { let id: Int let name: String @@ -90,7 +96,9 @@ struct OrderPartDetail: Decodable { } -// MARK: - 주문 생성 Request +// MARK: - Request Models + +// 주문 생성 요청 모델 struct OrderRequest: Encodable { let orderItems: [OrderItems] let requestedShippingDate: String @@ -98,28 +106,29 @@ struct OrderRequest: Encodable { let etc: String } +// 개별 주문 품목 요청 모델 struct OrderItems: Encodable { let partId: Int let amount: Int } +// 주문 생성 응답 데이터 struct OrderCreateResponseData: Decodable { let orderId: Int let orderNumber: String let totalPrice: Int - let paymentType: String // ✅ 서버 필드와 맞춤 + let paymentType: String } -// ✅ 입고 처리 요청 API +// 입고 처리 요청 모델 struct ReceiveOrderRequest: Encodable { let orderNumber: String } -// MARK: - API Call - +// MARK: - API enum OrderApi { - // ✅ 내 주문 리스트 조회 API + // 내 주문 리스트 조회 API static func getMyOrderList( status: String? = nil, startDate: String? = nil, @@ -143,14 +152,14 @@ enum OrderApi { } - // ✅ 주문 상세 조회 API + // 주문 상세 조회 API static func getOrderDetail(orderId: Int) -> DataRequest { let url = ApiClient.baseURL + "api/v1/order/detail?orderId=\(orderId)" return ApiClient.shared.request(url, method: .get) } - // ✅ 주문 생성 API + // 주문 생성 API static func createOrder(_ requestBody: OrderRequest) -> DataRequest { let url = ApiClient.baseURL + "api/v1/order" return ApiClient.shared.request( @@ -161,15 +170,15 @@ enum OrderApi { ) } - // ✅ 주문 취소 API + // 주문 취소 API static func cancelOrder(orderId: Int) -> DataRequest { let url = ApiClient.baseURL + "api/v1/order/\(orderId)/cancel" - print("🚀 CancelOrder URL:", url) + print("CancelOrder URL:", url) return ApiClient.shared.request(url, method: .put) - .validate() // ✅ 서버 상태코드 확인 + .validate() } - // ✅ 입고 처리 API + // 입고 처리 API static func receiveOrder(_ requestBody: ReceiveOrderRequest) -> DataRequest { let url = ApiClient.baseURL + "api/v1/order/receive" return ApiClient.shared.request( diff --git a/StockMate/StockMate/app/feature/orders/data/OrderRepositoryImpl.swift b/StockMate/StockMate/app/feature/orders/data/OrderRepositoryImpl.swift index c670736..6de6a5b 100644 --- a/StockMate/StockMate/app/feature/orders/data/OrderRepositoryImpl.swift +++ b/StockMate/StockMate/app/feature/orders/data/OrderRepositoryImpl.swift @@ -9,6 +9,8 @@ import Foundation import Alamofire final class OrderRepositoryImpl: OrderRepositoryProtocol { + + // 내 주문 목록 조회 func fetchMyOrders( status: String?, startDate: String?, @@ -24,16 +26,15 @@ final class OrderRepositoryImpl: OrderRepositoryProtocol { size: size ) - // safeApi 으로 전체 ApiResponse 를 디코딩 + // 서버 응답을 OrderListResponse 형태로 디코딩 let result = await safeApi(request, decodeTo: OrderListResponse.self) switch result { case .success(let response): - // response.data 는 OrderPageData? 이므로 안전하게 꺼내서 반환 - if let pageData = response.data { + if let pageData = response.data { // response.data 는 OrderPageData? 이므로 안전하게 꺼내서 반환 return .success(pageData) } else { - // 서버가 data를 비워서 보냈다면 메시지로 실패 처리 + // data가 없을 경우 서버 메시지로 실패 처리 return .failure(AppError(code: response.status, message: response.message, underlying: nil)) } @@ -42,23 +43,24 @@ final class OrderRepositoryImpl: OrderRepositoryProtocol { } } - + // 주문 상세 조회 func fetchOrderDetail(orderId: Int) async -> AppResult { let request = OrderApi.getOrderDetail(orderId: orderId) - let result = await safeApi(request, decodeTo: OrderDetailResponse.self) // ✅ 올바른 타입으로 변경 + let result = await safeApi(request, decodeTo: OrderDetailResponse.self) switch result { case .success(let response): if let data = response.data { return .success(data) } else { - return .failure(.init(code: -1, message: "주문 상세 데이터를 불러오지 못했습니다.", underlying: nil)) // ✅ 누락된 인자 채움 + return .failure(.init(code: -1, message: "주문 상세 데이터를 불러오지 못했습니다.", underlying: nil)) } case .failure(let error): return .failure(error) } } + // 주문 생성 func createOrder(request: OrderRequest) async -> AppResult { let request = OrderApi.createOrder(request) @@ -76,30 +78,31 @@ final class OrderRepositoryImpl: OrderRepositoryProtocol { } } - + // 주문 취소 func cancelOrder(orderId: Int) async -> AppResult { let request = OrderApi.cancelOrder(orderId: orderId) let result = await safeApi(request, decodeTo: ApiResponse.self) switch result { case .success(let response): - print("✅ 취소 성공:", response) + print(" 취소 성공:", response) return .success(response.data ?? "success") case .failure(let error): return .failure(error) } } + // 입고 처리 func receiveOrder(orderNumber: String) async -> AppResult { let request = OrderApi.receiveOrder(.init(orderNumber: orderNumber)) let result = await safeApi(request, decodeTo: ApiResponse.self) switch result { case .success(let response): - print("✅ 입고 처리 성공:", response) + print("입고 처리 성공:", response) return .success(response.data ?? "입고 처리가 완료되었습니다.") case .failure(let error): - print("❌ 입고 처리 실패:", error.message) + print("입고 처리 실패:", error.message) return .failure(error) } } diff --git a/StockMate/StockMate/app/feature/orders/domain/OrderRepositoryProtocol.swift b/StockMate/StockMate/app/feature/orders/domain/OrderRepositoryProtocol.swift index 81be290..f2837be 100644 --- a/StockMate/StockMate/app/feature/orders/domain/OrderRepositoryProtocol.swift +++ b/StockMate/StockMate/app/feature/orders/domain/OrderRepositoryProtocol.swift @@ -16,7 +16,7 @@ protocol OrderRepositoryProtocol { size: Int ) async -> AppResult - /// 주문 상세 조회 + // 주문 상세 조회 func fetchOrderDetail( orderId: Int ) async -> AppResult diff --git a/StockMate/StockMate/app/feature/orders/ui/OrderCartView.swift b/StockMate/StockMate/app/feature/orders/ui/OrderCartView.swift index b39a506..ba66504 100644 --- a/StockMate/StockMate/app/feature/orders/ui/OrderCartView.swift +++ b/StockMate/StockMate/app/feature/orders/ui/OrderCartView.swift @@ -8,52 +8,81 @@ import SwiftUI struct OrderCartView: View { + @Environment(\.dismiss) private var dismiss @ObservedObject var cartViewModel: CartViewModel var body: some View { VStack(spacing: 0) { - ScrollView { - LazyVStack(spacing: 16) { - ForEach(cartViewModel.items) { cartItem in - - CartCard( - item: cartItem, - quantity: cartItem.amount, - onIncrease: { - Task { await cartViewModel.increaseQuantity(for: cartItem.partId) } - }, - onDecrease: { - Task { await cartViewModel.decreaseQuantity(for: cartItem.partId) } - }, - onAddToCart: nil, - onRemoveFromCart: { - Task { - await cartViewModel.decreaseQuantity(for: cartItem.partId) + if cartViewModel.items.isEmpty { + // 장바구니 비어있을 때 + VStack(spacing: 8) { + Text("장바구니가 비어있어요.") + .font(.system(size: 15, weight: .regular)) + .foregroundColor(.black) + Text("부품을 담아보세요.") + .font(.system(size: 13)) + .foregroundColor(.gray) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.Light) + } else { + ScrollView { + LazyVStack(spacing: 16) { + ForEach(cartViewModel.items) { cartItem in + + CartCard( + item: cartItem, + quantity: cartItem.amount, + onIncrease: { + Task { await cartViewModel.increaseQuantity(for: cartItem.partId) } + }, + onDecrease: { + Task { await cartViewModel.decreaseQuantity(for: cartItem.partId) } } - } - ) - .padding(.horizontal) + ) + .padding(.horizontal) + } } + .padding(.vertical) } - .padding(.vertical) + .background(Color.Light) } - .background(Color.Light) - NavigationLink(destination: OrderInfoView(cartViewModel: cartViewModel)) { Text("\(cartViewModel.cart?.totalPrice ?? 0)원 결제하기") .font(.system(size: 16, weight: .bold)) .foregroundColor(.white) .frame(maxWidth: .infinity) - .frame(height: 60) - .background(Color.Primary) + .frame(height: 50) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(cartViewModel.items.isEmpty ? Color.gray.opacity(0.3) : Color.Primary) + ) + .padding(.horizontal, 16) + .padding(.bottom, 30) } + .disabled(cartViewModel.items.isEmpty) + } .background(Color.Light) .navigationTitle("장바구니 확인") .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.left") + .font(.system(size: 15, weight: .medium)) + } + .foregroundColor(.black) + } + } + } .task { await cartViewModel.fetchCart() } diff --git a/StockMate/StockMate/app/feature/orders/ui/OrderDetailView.swift b/StockMate/StockMate/app/feature/orders/ui/OrderDetailView.swift index 8ff3d78..bdc8067 100644 --- a/StockMate/StockMate/app/feature/orders/ui/OrderDetailView.swift +++ b/StockMate/StockMate/app/feature/orders/ui/OrderDetailView.swift @@ -9,16 +9,22 @@ import SwiftUI struct OrderDetailView: View { let orderId: Int + @Environment(\.dismiss) private var dismiss @ObservedObject var orderViewModel: OrderViewModel @StateObject private var viewModel = OrderDetailViewModel() - + + // 입고처리 버튼 + 모달 + 리프레시 + @State private var showSuccessModal = false // 모달 표시용 상태 + @State private var refreshTrigger = UUID() // 화면 리프레시 트리거 + + var body: some View { ScrollView { if viewModel.isLoading { ProgressView() .frame(maxWidth: .infinity, maxHeight: .infinity) } else if let order = viewModel.order { - VStack(spacing: 16) { + VStack(spacing: 10) { VStack { DeliveryStatusView(currentStep: deliveryStep(for: order.orderStatus)) .frame(maxWidth: .infinity, alignment: .center) @@ -26,7 +32,7 @@ struct OrderDetailView: View { .frame(maxWidth: .infinity, alignment: .center) - // ✅ 주문 정보 + // 주문 정보 VStack(alignment: .leading, spacing: 6) { Text(formatDate(order.createdAt)) .font(.system(size: 15, weight: .semibold)) @@ -50,7 +56,7 @@ struct OrderDetailView: View { } } - .frame(maxWidth: .infinity, alignment: .leading) // ✅ 이거 추가 + .frame(maxWidth: .infinity, alignment: .leading) .padding(.all, 20) .background(Color.white) .cornerRadius(16) @@ -66,14 +72,14 @@ struct OrderDetailView: View { .font(.system(size: 14)) } - .frame(maxWidth: .infinity, alignment: .leading) // ✅ 여기도 추가 + .frame(maxWidth: .infinity, alignment: .leading) .padding(.all, 20) .background(Color.white) .cornerRadius(16) .shadow(color: .black.opacity(0.05), radius: 3, y: 2) } - // ✅ 배송 정보 + // 배송 정보 VStack(alignment: .leading, spacing: 6) { Text("배송정보") .font(.system(size: 15, weight: .semibold)) @@ -81,43 +87,43 @@ struct OrderDetailView: View { infoRow("주문자명", order.userInfo?.owner ?? "-") infoRow("주소", order.userInfo?.address ?? "-") - // ✅ 운송장정보 안전 처리 + // 운송장정보 안전 처리 let trackingText: String = { if let carrier = order.carrier, let trackingNo = order.trackingNumber, !carrier.isEmpty, !trackingNo.isEmpty { - return "\(carrier): \(trackingNo)" + return "(\(carrier): \(trackingNo))" } return "-" }() infoRow("운송장번호", trackingText) } - .frame(maxWidth: .infinity, alignment: .leading) // ✅ 여기도 추가 + .frame(maxWidth: .infinity, alignment: .leading) .padding(.all, 20) .background(Color.white) .cornerRadius(16) .shadow(color: .black.opacity(0.05), radius: 3, y: 2) - // 요청사항 따로 빼기 - VStack(alignment: .leading, spacing: 6) { - Text("요청사항") - .font(.system(size: 15, weight: .semibold)) - .padding(.bottom, 4) + // 요청사항이 있는 경우에만 표시 + if let etc = order.etc, !etc.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + VStack(alignment: .leading, spacing: 6) { + Text("요청사항") + .font(.system(size: 15, weight: .semibold)) + .padding(.bottom, 4) - Text(order.etc ?? "") - .font(.system(size: 14)) - + Text(etc) + .font(.system(size: 14)) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.all, 20) + .background(Color.white) + .cornerRadius(16) + .shadow(color: .black.opacity(0.05), radius: 3, y: 2) } - .frame(maxWidth: .infinity, alignment: .leading) // ✅ 여기도 추가 - .padding(.all, 20) - .background(Color.white) - .cornerRadius(16) - .shadow(color: .black.opacity(0.05), radius: 3, y: 2) - - // ✅ 주문 상품 + // 주문 상품 OrderSectionCard { VStack(alignment: .leading, spacing: 6) { Text("주문 상품 \(order.orderItems.count)개") @@ -148,16 +154,16 @@ struct OrderDetailView: View { .font(.system(size: 14, weight: .bold)) } } - .frame(maxWidth: .infinity, alignment: .leading) // ✅ 왼쪽 정렬 강제 + .frame(maxWidth: .infinity, alignment: .leading) } } .padding(.top, 8) // 위 여백만 살짝 } - .frame(maxWidth: .infinity, alignment: .leading) // ✅ 섹션 전체도 왼쪽으로 정렬 + .frame(maxWidth: .infinity, alignment: .leading) } - // ✅ 결제 정보 + // 결제 정보 OrderSectionCard { VStack(alignment: .leading, spacing: 10) { Text("결제 정보") @@ -183,76 +189,94 @@ struct OrderDetailView: View { } } - // ✅ 하단 버튼 - HStack(spacing: 12) { - // 왼쪽: 영수증 확인 - NavigationLink(destination: ReceiptView(orderId: order.id)) { - Text("영수증 확인") - .font(.system(size: 15, weight: .semibold)) - .foregroundColor(Color.Primary) - .frame(maxWidth: .infinity) - .frame(height: 48) - .background(Color.white) - .overlay( - RoundedRectangle(cornerRadius: 10) - .stroke(Color.Primary, lineWidth: 1.5) - ) - .cornerRadius(10) - } - - - // 오른쪽 버튼: 주문 상태에 따라 변경 - if order.orderStatus == "ORDER_COMPLETED" || - order.orderStatus == "PAY_COMPLETED" { - // "주문취소" → 주문완료/결제완료/승인대기 - Button(action: { - Task { - await orderViewModel.cancelOrder(orderId: orderId) - } - }) { - Text("주문 취소") - .font(.system(size: 15, weight: .semibold)) - .frame(maxWidth: .infinity) - .frame(height: 48) - .background(Color(hex: "#1D4ED8")) - .foregroundColor(.white) - .cornerRadius(10) - } - - } else if order.orderStatus == "SHIPPING" { - // "입고 하기" → 입고대기/배송완료 - Button(action: { - // TODO: 입고 처리 버튼 - }) { - Text("입고 처리") - .font(.system(size: 15, weight: .semibold)) - .frame(maxWidth: .infinity) - .frame(height: 48) - .background(Color(hex: "#1D4ED8")) - .foregroundColor(.white) - .cornerRadius(10) - } - - } else { - // 👉 나머지 상태 → "재주문하기" 버튼 - Button(action: { - // TODO: 평가 액션 처리 - }) { - Text("재주문하기") + // 오른쪽 버튼: 주문 상태에 따라 변경 + if order.orderStatus == "ORDER_COMPLETED" || + order.orderStatus == "PAY_COMPLETED" || + order.orderStatus == "SHIPPING"{ + + HStack(spacing: 12) { + // 왼쪽: 영수증 확인 + NavigationLink(destination: ReceiptView(orderId: order.id)) { + Text("영수증 확인") .font(.system(size: 15, weight: .semibold)) + .foregroundColor(Color.Primary) .frame(maxWidth: .infinity) .frame(height: 48) - .background(Color(hex: "#1D4ED8")) - .foregroundColor(.white) + .background(Color.white) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.Primary, lineWidth: 1.5) + ) .cornerRadius(10) } + + // 오른쪽 버튼: 주문 상태에 따라 변경 + if order.orderStatus == "ORDER_COMPLETED" || + order.orderStatus == "PAY_COMPLETED" { + // "주문취소" → 주문완료/결제완료/승인대기 + Button(action: { + Task { + await orderViewModel.cancelOrder(orderId: orderId) + } + }) { + Text("주문 취소") + .font(.system(size: 15, weight: .semibold)) + .frame(maxWidth: .infinity) + .frame(height: 48) + .background(Color(hex: "#1D4ED8")) + .foregroundColor(.white) + .cornerRadius(10) + } + + } else { + // "입고 하기" → 배송중 + Button(action: { + Task { + let result = await orderViewModel.receiveOrder(orderNumber: order.orderNumber) + switch result { + case .success(let message): + showSuccessModal = true // 입고 처리 성공 시 모달 표시 + print("입고 처리 성공: \(message)") + case .failure(let error): + print("입고 처리 실패: \(error.message)") + // 실패 토스트 띄우기 등 + } + } + }) { + Text("입고 처리") + .font(.system(size: 15, weight: .semibold)) + .frame(maxWidth: .infinity) + .frame(height: 48) + .background(Color(hex: "#1D4ED8")) + .foregroundColor(.white) + .cornerRadius(10) + } + + + } } + .padding(.top, 5) + } else{ + // 나머지 상태: 영수증 확인 버튼만 하나 + NavigationLink(destination: ReceiptView(orderId: order.id)) { + Text("영수증 확인") + .font(.system(size: 15, weight: .semibold)) + .foregroundColor(Color.Primary) + .frame(maxWidth: .infinity) + .frame(height: 48) + .background(Color.white) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.Primary, lineWidth: 1.5) + ) + .cornerRadius(10) + } + .padding(.top, 5) } - .padding(.top, 5) } - .padding(.horizontal, 20) // ✅ 전체 섹션 동일 여백 + .padding(.horizontal, 20) // 전체 섹션 동일 여백 .padding(.vertical, 16) } else if let error = viewModel.errorMessage { @@ -261,12 +285,53 @@ struct OrderDetailView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } } + .overlay( + Group { + if showSuccessModal { + Color.black.opacity(0.4) + .ignoresSafeArea() + .overlay( + AlertModal( + icon: Image("SuccessIllust"), + title: "입고 처리 완료!", + message: "입고 부품 등록이 완료되었습니다.", + primaryButtonTitle: "확인", + primaryAction: { + showSuccessModal = false + // 화면 리프레시 트리거 + refreshTrigger = UUID() + Task { + await viewModel.fetchOrderDetail(orderId: orderId) + } + } + ) + ) + } + } + ) + .animation(.easeInOut, value: showSuccessModal) .background(Color.Light) .navigationTitle("주문 내역 상세") .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.left") + .font(.system(size: 15, weight: .medium)) + } + .foregroundColor(.black) + } + } + } .task { await viewModel.fetchOrderDetail(orderId: orderId) } + .id(refreshTrigger) + } // MARK: - Helper @@ -282,7 +347,7 @@ struct OrderDetailView: View { } -// ✅ 카드 레이아웃 통일용 +// 카드 레이아웃 통일용 struct OrderSectionCard: View { let content: Content init(@ViewBuilder content: () -> Content) { diff --git a/StockMate/StockMate/app/feature/orders/ui/OrderInfoView.swift b/StockMate/StockMate/app/feature/orders/ui/OrderInfoView.swift index 3310f9a..3fa994e 100644 --- a/StockMate/StockMate/app/feature/orders/ui/OrderInfoView.swift +++ b/StockMate/StockMate/app/feature/orders/ui/OrderInfoView.swift @@ -14,20 +14,27 @@ enum PaymentType: String { enum ShippingDateOption { case today case tomorrow - case specific(Date) + case specific(Date?) } struct OrderInfoView: View { + @Environment(\.dismiss) private var dismiss @ObservedObject var cartViewModel: CartViewModel @StateObject var orderViewModel = OrderViewModel() @StateObject private var depositViewModel = DepositViewModel() + @StateObject private var userViewModel = UserViewModel() @State private var paymentType: PaymentType = .deposit @State private var shippingDateOption: ShippingDateOption = .today - @State private var specificDate = Date() + @State private var specificDate: Date? = nil @State private var requestMessage: String = "" - // ✅ 모달 관련 상태 + // 토스트 메세지 관련 + @State private var showDepositToast = false // 예치금 부족 + @State private var showChargeToast = false // 충전 완료 + + + // 모달 관련 상태 @State private var showOrderSuccessModal = false @State private var navigateToOrderDetail = false @State private var navigateToHome = false @@ -55,9 +62,8 @@ struct OrderInfoView: View { case .tomorrow: let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date()) ?? Date() return formatter.string(from: tomorrow) -// return formatter.string(from: Calendar.current.date(byAdding: .day, value: 1, to: Date())!) case .specific(let date): - return formatter.string(from: date) + return formatter.string(from: date ?? Date()) // nil이면 오늘 날짜로 fallback } } @@ -72,17 +78,49 @@ struct OrderInfoView: View { ScrollView { contentView } + .onTapGesture { + UIApplication.shared.hideKeyboard() // 화면 아무데나 탭하면 키보드 내려감 + } .padding(.horizontal) .padding(.top) bottomOrderButton } + .toast( + isPresented: $showChargeToast, + message: "충전이 완료되었습니다.", + iconName: "checkmark", + iconColor: .green + ) + // 예치금 부족 토스트 + .toast( + isPresented: $showDepositToast, + message: "예치금이 부족합니다. (부족: \(formatPrice((cartViewModel.cart?.totalPrice ?? 0) - depositViewModel.balance))원)", + iconName: "info.circle", + iconColor: .LightBlue04 + ) .background(Color.Light) .navigationTitle("주문/결제") .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) .task { await cartViewModel.fetchCart() await depositViewModel.fetchDepositAmount() + await userViewModel.loadUserInfo() + + } + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.left") + .font(.system(size: 15, weight: .medium)) + } + .foregroundColor(.black) + } + } } .edgesIgnoringSafeArea(.bottom) .onChange(of: orderViewModel.isOrderSuccess) { success in @@ -96,11 +134,16 @@ struct OrderInfoView: View { } } } - // ✅ 충전 bottom sheet 연결 - .sheet(isPresented: $depositViewModel.showChargeSheet) { - DepositChargeView(viewModel: depositViewModel) - .presentationDetents([.fraction(0.80)]) // 시트 높이 80% - } + .sheet(isPresented: $depositViewModel.showChargeSheet) { + DepositChargeView(viewModel: depositViewModel) { + // 충전 성공 시 토스트 표시 + withAnimation { + showChargeToast = true + } + } + .presentationDetents([.fraction(0.58)]) // 시트 높이 80% + .presentationCornerRadius(20) + } // 모달 오버레이 (body 안) .overlay { if showOrderSuccessModal { @@ -175,6 +218,8 @@ extension OrderInfoView { paymentSection shippingDateSection totalPriceSection + + Spacer().frame(height: 10) } } @@ -183,41 +228,47 @@ extension OrderInfoView { Text("배송 정보") .font(.headline) - VStack(alignment: .leading, spacing: 8) { - Text("홍길동").font(.system(size: 15, weight: .medium)) - Text("서울특별시 강남구 테헤란로114길") - .font(.system(size: 14)) - .foregroundColor(.textGray1) - Text("010-1111-2222") + VStack(alignment: .leading, spacing: 4) { + Text(userViewModel.userInfo?.owner ?? "이름 없음") + .font(.system(size: 15, weight: .medium)) + .padding(.bottom, 4) + Text(userViewModel.userInfo?.address ?? "주소 없음") .font(.system(size: 14)) + .padding(.bottom, 4) .foregroundColor(.textGray1) Text("요청사항") .font(.system(size: 14, weight: .medium)) .padding(.top, 5) - - ZStack(alignment: .topLeading) { - if requestMessage.isEmpty { - Text("요청사항을 입력하세요") - .foregroundColor(.gray) - .font(.system(size: 14)) - .padding(.top, 12) - .padding(.leading, 10) + + TextEditor(text: $requestMessage) + .font(.system(size: 14)) + .padding(10) + .onChange(of: requestMessage) { newValue in + if newValue.count > 50 { // 50자 제한 + requestMessage = String(newValue.prefix(50)) + } } - - TextEditor(text: $requestMessage) - .font(.system(size: 14)) - .padding(.top, 4) - .padding(.leading, 6) - .scrollContentBackground(.hidden) - .background(Color.clear) + .scrollContentBackground(.hidden) + .background(Color.white) + .frame(height: 93) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(requestMessage.isEmpty ? Color(.systemGray4) : Color.Primary, lineWidth: 1) + ) + + + HStack { + Spacer() + Text("\(requestMessage.count)/50") + .font(.system(size: 12)) + .foregroundColor(.gray) + .padding(.trailing, 2) } - .frame(height: 70) - .background(Color.white) - .overlay(RoundedRectangle(cornerRadius: 10) - .stroke(Color(.systemGray4), lineWidth: 1)) + .padding(.bottom, -8) } .padding() + .padding(.bottom,7) .background(Color.white) .cornerRadius(16) } @@ -226,7 +277,7 @@ extension OrderInfoView { private var orderListSection: some View { VStack(alignment: .leading) { - Text("주문 목록") + Text("주문 목록 (\(cartViewModel.items.count))") .font(.headline) .padding(.leading, 5) @@ -248,7 +299,7 @@ extension OrderInfoView { Image("deposit_background") // ← 에셋에 넣은 이미지 이름 .resizable() .scaledToFill() - .frame(height: 185) + .frame(height: 190) .clipped() .cornerRadius(16.39) @@ -284,8 +335,8 @@ extension OrderInfoView { } label: { Text("충전") .foregroundColor(Color.Primary) - .font(.system(size: 14, weight: .bold)) - .padding(.vertical, 6) + .font(.system(size: 14, weight: .semibold)) + .padding(.vertical, 8) .padding(.horizontal, 14) .background(Color.white) .cornerRadius(20) @@ -333,17 +384,18 @@ extension OrderInfoView { if case .specific(_) = shippingDateOption { return true } return false }()) { - shippingDateOption = .specific(specificDate) + shippingDateOption = .specific(nil) } .padding(.trailing, 5) - if case .specific(_) = shippingDateOption { - CustomDatePickerField(date: Binding { - specificDate - } set: { newValue in - specificDate = newValue - shippingDateOption = .specific(newValue) - }) + if case .specific(let selectedDate) = shippingDateOption { + CustomDatePickerField(date: Binding( + get: { selectedDate ?? Date() }, + set: { newValue in + specificDate = newValue + shippingDateOption = .specific(newValue) + } + ), isDateSelected: selectedDate != nil) } } .frame(height: 35) @@ -372,22 +424,38 @@ extension OrderInfoView { private var bottomOrderButton: some View { VStack { Button { - Task { - let orderRequest = OrderRequest( - orderItems: makeOrderItems(), - requestedShippingDate: formattedShippingDate(), - paymentType: paymentType.rawValue, - etc: requestMessage - ) - await orderViewModel.createOrder(request: orderRequest) + let totalPrice = cartViewModel.cart?.totalPrice ?? 0 + let deposit = depositViewModel.balance + + if totalPrice > deposit { + // 예치금 부족 + withAnimation { + showDepositToast = true + } + } else { + Task { + let orderRequest = OrderRequest( + orderItems: makeOrderItems(), + requestedShippingDate: formattedShippingDate(), + paymentType: paymentType.rawValue, + etc: requestMessage + ) + await orderViewModel.createOrder(request: orderRequest) + } } + } label: { - Text("결제하기") + Text("\(cartViewModel.cart?.totalPrice ?? 0)원 결제하기") .font(.system(size: 16, weight: .bold)) .foregroundColor(.white) .frame(maxWidth: .infinity) - .frame(height: 70) - .background(Color.Primary) + .frame(height: 50) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(cartViewModel.items.isEmpty ? Color.gray.opacity(0.3) : Color.Primary) + ) + .padding(.horizontal, 16) + .padding(.bottom, 30) } } } @@ -408,31 +476,32 @@ struct RadioButtonRow: View { } } - struct CustomDatePickerField: View { @Binding var date: Date + var isDateSelected: Bool @State private var showPicker: Bool = false - - // 날짜 포맷 변환용 Formatter + private var dateFormatter: DateFormatter { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" return formatter } - + var body: some View { Button { showPicker.toggle() } label: { HStack { - Text(dateFormatter.string(from: date)) + Text(isDateSelected ? dateFormatter.string(from: date) : "선택하세요") .font(.system(size: 14)) - .foregroundColor(.black) - + .foregroundColor(isDateSelected ? .black : .gray) + Spacer() - - Image(systemName: "calendar") - .foregroundColor(.gray) + + Image("cal") + .resizable() + .frame(width: 15, height: 15) + .scaledToFit() } .padding(10) .frame(width: 140, height: 30) @@ -451,7 +520,7 @@ struct CustomDatePickerField: View { ) .datePickerStyle(.graphical) .padding() - + Button("완료") { showPicker = false } @@ -463,4 +532,3 @@ struct CustomDatePickerField: View { } } } - diff --git a/StockMate/StockMate/app/feature/orders/ui/OrderListView.swift b/StockMate/StockMate/app/feature/orders/ui/OrderListView.swift index 7cd04b1..160593b 100644 --- a/StockMate/StockMate/app/feature/orders/ui/OrderListView.swift +++ b/StockMate/StockMate/app/feature/orders/ui/OrderListView.swift @@ -8,10 +8,10 @@ import SwiftUI struct OrderListView: View { + @Environment(\.dismiss) private var dismiss @StateObject private var orderViewModel = OrderViewModel() var body: some View { -// NavigationStack { VStack(alignment: .leading, spacing: 0) { if orderViewModel.isLoading { @@ -54,10 +54,23 @@ struct OrderListView: View { } .background(Color.Light) .navigationTitle("주문 내역") + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.left") + .font(.system(size: 15, weight: .medium)) + } + .foregroundColor(.black) + } + } + } .task { await orderViewModel.loadOrders() } -// } } func formatDate(_ dateString: String) -> String { @@ -103,9 +116,10 @@ struct OrderListCardView: View { VStack(alignment: .leading, spacing: 4) { if let first = order.orderItems.first { Text(first.partDetail.korName) - .font(.system(size: 16, weight: .semibold)) + .font(.system(size: 13, weight: .semibold)) .foregroundColor(.black) - .lineLimit(1) + .lineLimit(nil) + .fixedSize(horizontal: false, vertical: true) if order.orderItems.count > 1 { Text("외 \(order.orderItems.count - 1)개") @@ -128,7 +142,7 @@ struct OrderListCardView: View { } // 주문취소 버튼 (필요 시) - if order.orderStatus == "ORDER_COMPLETED" { + if order.orderStatus == "PAY_COMPLETED" { Button(action: { // 주문취소 처리 Task { diff --git a/StockMate/StockMate/app/feature/orders/ui/OrderRequestSearchView.swift b/StockMate/StockMate/app/feature/orders/ui/OrderRequestSearchView.swift index d75fea6..fd44969 100644 --- a/StockMate/StockMate/app/feature/orders/ui/OrderRequestSearchView.swift +++ b/StockMate/StockMate/app/feature/orders/ui/OrderRequestSearchView.swift @@ -8,8 +8,9 @@ import SwiftUI struct OrderRequestSearchView: View { - @ObservedObject var cartViewModel: CartViewModel + @Environment(\.dismiss) private var dismiss + @ObservedObject var cartViewModel: CartViewModel @StateObject var inventoryViewModel = InventoryViewModel() @State private var searchText = "" @@ -37,7 +38,7 @@ struct OrderRequestSearchView: View { var body: some View { ZStack { VStack(spacing: 0) { - // 🔍 검색창 + // 검색창 HStack { Image(systemName: "magnifyingglass") .foregroundColor(.gray) @@ -100,17 +101,21 @@ struct OrderRequestSearchView: View { onTap: { inventoryViewModel.toggleModel($0) } ) - // 🔄 초기화 버튼 + // 초기화 버튼 Button(action: { inventoryViewModel.resetFilters(with: searchText) }) { - HStack(spacing: 4) { - Image(systemName: "arrow.counterclockwise") - Text("초기화") - } - .font(.system(size: 13, weight: .medium)) - .foregroundColor(.blue) - .padding(.trailing, 8) + Image(systemName: "arrow.clockwise") + .font(.system(size: 16, weight: .semibold)) + .foregroundColor( + inventoryViewModel.selectedCategories.isEmpty && + inventoryViewModel.selectedTrims.isEmpty && + inventoryViewModel.selectedModels.isEmpty + ? .black + : .Primary + ) + .padding(.trailing, 8) + .rotationEffect(.degrees(35)) } .frame(maxWidth: .infinity, alignment: .trailing) @@ -118,7 +123,7 @@ struct OrderRequestSearchView: View { .padding(.horizontal) .padding(.bottom, 16) - // 📋 재고 리스트 + // 재고 리스트 ScrollView { LazyVStack(spacing: 10) { @@ -134,8 +139,7 @@ struct OrderRequestSearchView: View { quantity: qty, onIncrease: { Task { await cartViewModel.increaseQuantity(for: item.id) } }, onDecrease: { Task { await cartViewModel.decreaseQuantity(for: item.id) }}, - onAddToCart: { Task { await cartViewModel.addToCart(partId: item.id, amount: 1) }}, - onRemoveFromCart: { Task { await cartViewModel.decreaseQuantity(for: item.id) }} + onAddToCart: { Task { await cartViewModel.addToCart(partId: item.id, amount: 1) }} ) .padding(.horizontal) .onAppear { @@ -166,6 +170,20 @@ struct OrderRequestSearchView: View { } .background(Color.Light) .navigationTitle("직접 발주") + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.left") + .font(.system(size: 15, weight: .medium)) + } + .foregroundColor(.black) + } + } + } .task { await inventoryViewModel.loadInventoryList(reset: true) await cartViewModel.fetchCart() @@ -178,5 +196,8 @@ struct OrderRequestSearchView: View { .ignoresSafeArea(edges: .bottom) } + .onTapGesture { + UIApplication.shared.hideKeyboard() + } } } diff --git a/StockMate/StockMate/app/feature/orders/ui/OrderResultView.swift b/StockMate/StockMate/app/feature/orders/ui/OrderResultView.swift index 39fc6bf..0b89be5 100644 --- a/StockMate/StockMate/app/feature/orders/ui/OrderResultView.swift +++ b/StockMate/StockMate/app/feature/orders/ui/OrderResultView.swift @@ -15,14 +15,14 @@ struct OrderResultView: View { Spacer() - // ✅ 결제 완료 아이콘 + // 결제 완료 아이콘 Image(systemName: "checkmark.circle.fill") .resizable() .frame(width: 80, height: 80) .foregroundColor(.blue) .padding(.bottom, 8) - // ✅ 완료 문구 + // 완료 문구 Text("결제가 완료되었습니다") .font(.title3) .bold() @@ -32,7 +32,7 @@ struct OrderResultView: View { Spacer() - // ✅ 하단 버튼 + // 하단 버튼 VStack(spacing: 12) { Button { dismiss() // 이전 화면으로 돌아가기 diff --git a/StockMate/StockMate/app/feature/orders/ui/OrderView.swift b/StockMate/StockMate/app/feature/orders/ui/OrderView.swift index 3fa5db5..8334011 100644 --- a/StockMate/StockMate/app/feature/orders/ui/OrderView.swift +++ b/StockMate/StockMate/app/feature/orders/ui/OrderView.swift @@ -15,7 +15,7 @@ struct OrderView: View { ZStack{ ScrollView { // 타이틀 - Text("재고 관리") + Text("발주 요청") .font(.title2) .bold() .padding(.top, 13) @@ -29,12 +29,9 @@ struct OrderView: View { .padding(.leading, 25) .frame(maxWidth: .infinity, alignment: .leading) - // 🔍 검색창 + // 검색창 NavigationLink(destination: - OrderRequestSearchView( - cartViewModel: cartViewModel - //inventoryViewModel: inventoryViewModel - ) + OrderRequestSearchView(cartViewModel: cartViewModel) ) { HStack { Image(systemName: "magnifyingglass") @@ -71,26 +68,9 @@ struct OrderView: View { OrderRequestCardView( item: item, quantity: qty, - onIncrease: { - Task { - await cartViewModel.increaseQuantity(for: item.id) - } - }, - onDecrease: { - Task { - await cartViewModel.decreaseQuantity(for: item.id) - } - }, - onAddToCart: { - Task { - await cartViewModel.addToCart(partId: item.id, amount: 1) - } - }, - onRemoveFromCart: { - Task { - await cartViewModel.decreaseQuantity(for: item.id) - } - } + onIncrease: { Task { await cartViewModel.increaseQuantity(for: item.id) } }, + onDecrease: { Task { await cartViewModel.decreaseQuantity(for: item.id) } }, + onAddToCart: { Task { await cartViewModel.addToCart(partId: item.id, amount: 1) } } ) .onAppear { if item.id == inventoryViewModel.underLimitItems.last?.id { diff --git a/StockMate/StockMate/app/feature/orders/ui/ReceiptView.swift b/StockMate/StockMate/app/feature/orders/ui/ReceiptView.swift index 91b100a..e0b953b 100644 --- a/StockMate/StockMate/app/feature/orders/ui/ReceiptView.swift +++ b/StockMate/StockMate/app/feature/orders/ui/ReceiptView.swift @@ -15,12 +15,13 @@ enum PDFType { } struct ReceiptView: View { + @Environment(\.dismiss) private var dismiss let orderId: Int @StateObject private var detailViewModel = OrderDetailViewModel() @State var sellerName = "홍길동" - @State var businessNumber = "215-87-12345" // 형식만 맞춘 랜덤번호 + @State var businessNumber = "215-87-12345" @State var phone = "02-567-8901" @State var address = "서울특별시 금천구 가산동 459-9" @@ -33,29 +34,46 @@ struct ReceiptView: View { } else if let order = detailViewModel.order { VStack(alignment: .leading) { receiptContent(order: order) - - Button { - generatePDF(type: .receipt80mm, order: order) - } label: { - Text("영수증 PDF 저장") - .frame(maxWidth: .infinity) - .padding() - .background(Color.blue) - .foregroundColor(.white) - .cornerRadius(8) - } - .padding(.horizontal) - .padding(.bottom) - } .background(Color.white) .cornerRadius(12) - .padding() + .padding(.horizontal) + .padding(.top) + .padding(.bottom,4) + + Button { + generatePDF(type: .receipt80mm, order: order) + } label: { + Text("PDF 저장") + .font(.system(size: 13, weight: .semibold)) + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .padding(.horizontal, 8) + .background(Color.Primary) + .foregroundColor(.white) + .cornerRadius(8) + } + .padding(.horizontal) + .padding(.bottom) } } .background(Color.Light) .navigationTitle("영수증") .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.left") + .font(.system(size: 15, weight: .medium)) + } + .foregroundColor(.black) + } + } + } .task { await detailViewModel.fetchOrderDetail(orderId: orderId) } @@ -123,24 +141,8 @@ struct ReceiptView: View { Divider() - Text(""" - • 비현금성으로 지급되는 예치금 사용 금액의 경우 현금 영수증 발행 대상에서 - 제외될 수 있습니다. - • 발생 정보는 구매확정 또는 거래 완료 이후 전달되기 때문에 국세청 사이트에서 - 즉시 확인되지 않을 수 있습니다. - • 이 영수증은 조세특례제한법 제 126조 3항에 의거 연말정산 시 소득공제혜택 - 부여 목적으로 발행됩니다. (국세청 회원가입 필수) - • 현금 영수증은 구매확정 또는 거래 완료 후 48시간 내로 국세청에서 확인 작업 - 후 최종 확정됩니다. - • 국세청 확인: 홈택스 홈페이지(https://www.hometax.go.kr/) 또는 국세청 - 상담센터(현금영수증 문의 ☎️126-1-1) - """) - .font(.system(size: 10.5)) - .foregroundColor(.textGray1) - .multilineTextAlignment(.leading) - .frame(maxWidth: .infinity, alignment: .leading) - .lineSpacing(4) - .padding(.leading, 2) // 문장 들여쓰기 추가 + NoticeTextView() + .padding(.top, 4) } .padding() @@ -161,75 +163,48 @@ struct ReceiptView: View { Spacer() Text(value) .fontWeight(highlight ? .bold : .regular) - .foregroundColor(highlight ? .blue : .primary) + .foregroundColor(highlight ? .Primary : .primary) } } private func generatePDF(type: PDFType, order: OrderResponseItem) { - let view = receiptContent(order: order) // ✅ 실제 View 생성 - let renderer = ImageRenderer(content: view) - - let width: CGFloat - switch type { - case .a4: width = 595.2 // A4 width in pt - case .receipt80mm: width = 226.77 // 80mm in pt - } + // 아이폰 화면 비율로 렌더링 (디바이스 폭 고정) + let screenWidth = UIScreen.main.bounds.width + let view = receiptContent(order: order) + .frame(width: screenWidth) // 폭 고정 (문장 길이에 따라 늘어나지 않음) + .background(Color.white) + let renderer = ImageRenderer(content: view) renderer.scale = UIScreen.main.scale - - // ✅ cgImage 기반 안전 처리 - if let cgImage = renderer.cgImage { - let uiImage = UIImage(cgImage: cgImage) - let pdfDoc = PDFDocument() - if let pdfPage = PDFPage(image: uiImage) { - pdfDoc.insert(pdfPage, at: 0) - } - // ✅ 주문번호 기반 파일명 - let fileName = "receipt_\(order.orderNumber).pdf" - let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName) + if let cgImage = renderer.cgImage { + let uiImage = UIImage(cgImage: cgImage) + let pdfDoc = PDFDocument() + if let pdfPage = PDFPage(image: uiImage) { + pdfDoc.insert(pdfPage, at: 0) + } - if pdfDoc.write(to: tempURL) { - let av = UIActivityViewController(activityItems: [tempURL], applicationActivities: nil) + // 파일명: 주문번호 기반 + let fileName = "receipt_\(order.orderNumber).pdf" + let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName) - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootVC = windowScene.windows.first?.rootViewController { - av.popoverPresentationController?.sourceView = rootVC.view - rootVC.present(av, animated: true) - } - } - } - + if pdfDoc.write(to: tempURL) { + let av = UIActivityViewController(activityItems: [tempURL], applicationActivities: nil) + + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootVC = windowScene.windows.first?.rootViewController { + av.popoverPresentationController?.sourceView = rootVC.view + rootVC.present(av, animated: true) + } + } + } } } -//struct ReceiptView_Previews: PreviewProvider { -// static var previews: some View { -// ReceiptView() -// } -//} - -//func formattedDate(_ timestamp: String) -> String { -// let inputFormatter = DateFormatter() -// inputFormatter.locale = Locale(identifier: "ko_KR") -// inputFormatter.timeZone = TimeZone(abbreviation: "UTC") -// inputFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS" -// -// guard let date = inputFormatter.date(from: timestamp) else { -// return timestamp -// } -// -// let outputFormatter = DateFormatter() -// outputFormatter.locale = Locale(identifier: "ko_KR") -// outputFormatter.timeZone = TimeZone.current -// outputFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss" -// -// return outputFormatter.string(from: date) -//} func formattedDate(_ timestamp: String) -> String { let inputFormatter = DateFormatter() inputFormatter.locale = Locale(identifier: "ko_KR") - inputFormatter.timeZone = TimeZone.current // ✅ 실제 한국 시간 기준 + inputFormatter.timeZone = TimeZone.current inputFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS" guard let date = inputFormatter.date(from: timestamp) else { @@ -258,7 +233,39 @@ func formattedApprovalNumber(_ timestamp: String) -> String { let outputFormatter = DateFormatter() outputFormatter.locale = Locale(identifier: "ko_KR") outputFormatter.timeZone = TimeZone.current - outputFormatter.dateFormat = "yyyyMMddHHmm" // ✅ 승인번호 포맷 + outputFormatter.dateFormat = "yyyyMMddHHmm" return outputFormatter.string(from: date) } + +struct NoticeTextView: View { + let notices = [ + "본 영수증은 거래완료 후 국세청 반영까지 시간이 소요될 수 있습니다.", + "현금영수증/지출증빙 여부는 국세청 홈페이지 또는 상담센터(126)에서 확인하세요.", + "비현금성으로 지급되는 포인트로 결제한 금액은 현금영수증 발행 대상에서 제외될 수 있습니다.", + "발행 방법이 자진 발급인 경우 국세청 사이트에서 자진발급분을 사용자 등록 후 소득공제 등 혜택을 받으실 수 있습니다.", + "발행 정보는 구매확정 또는 거래 완료 후 전달되며, 국세청 사이트에 즉시 반영되지 않을 수 있습니다.", + "이 영수증은 조세특례제한법 제126조 3항에 의거, 연말정산 시 소득공제 혜택 부여 목적 등으로 발행됩니다. (국세청 회원가입 필요)", + "현금 영수증은 구매 확정 또는 거래 완료 후 48시간 내에 국세청에서 확인 작업 후 최종 확정됩니다.", + "국세청 확인: 홈택스 홈페이지(https://www.hometax.go.kr/) 또는 국세청 상담센터(현금영수증 문의 ☎️126-1-1)." + ] + + var body: some View { + VStack(alignment: .leading, spacing: 5) { + ForEach(notices, id: \.self) { text in + HStack(alignment: .top, spacing: 3) { + Text("•") + .font(.system(size: 11)) + .foregroundColor(.gray) + Text(text) + .font(.system(size: 11.5)) + .foregroundColor(.textGray1) + .lineSpacing(4) + .fixedSize(horizontal: false, vertical: true) + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 2) + } +} diff --git a/StockMate/StockMate/app/feature/orders/viewmodel/OrderViewModel.swift b/StockMate/StockMate/app/feature/orders/viewmodel/OrderViewModel.swift index 447274b..ddb87c9 100644 --- a/StockMate/StockMate/app/feature/orders/viewmodel/OrderViewModel.swift +++ b/StockMate/StockMate/app/feature/orders/viewmodel/OrderViewModel.swift @@ -23,7 +23,7 @@ final class OrderViewModel: ObservableObject { init(repository: OrderRepositoryProtocol = OrderRepositoryImpl()) { self.repository = repository } - + // MARK: - 주문 목록 조회 func loadOrders( status: String? = nil, startDate: String? = nil, @@ -50,7 +50,7 @@ final class OrderViewModel: ObservableObject { } } - // 주문 생성 + // MARK: - 주문 생성 func createOrder(request: OrderRequest) async { let result = await repository.createOrder(request: request) @@ -60,25 +60,27 @@ final class OrderViewModel: ObservableObject { self.isOrderSuccess = true case .failure(let error): - print("❌ 주문 실패:", error.message) + print("주문 실패:", error.message) self.errorMessage = error.message } } + // MARK: - 주문 취소 func cancelOrder(orderId: Int) async { isLoading = true let result = await repository.cancelOrder(orderId: orderId) switch result { case .success: - await loadOrders() // ✅ 취소 후 즉시 UI 새로고침 + await loadOrders() // 취소 후 즉시 UI 새로고침 case .failure(let error): errorMessage = error.message } isLoading = false } + // MARK: - 입고 처리 (주문 수령) func receiveOrder(orderNumber: String) async -> AppResult { isLoading = true defer { isLoading = false } @@ -86,7 +88,7 @@ final class OrderViewModel: ObservableObject { let result = await repository.receiveOrder(orderNumber: orderNumber) switch result { case .success(let message): - print("✅ 입고 처리 성공:", message) + print("입고 처리 성공:", message) await loadOrders() return .success(message) case .failure(let error): @@ -94,19 +96,4 @@ final class OrderViewModel: ObservableObject { return .failure(error) } } - - -// func receiveOrder(orderNumber: String) async { -// isLoading = true -// defer { isLoading = false } -// -// let result = await repository.receiveOrder(orderNumber: orderNumber) -// switch result { -// case .success(let message): -// print("✅ 입고 처리 성공:", message) -// await loadOrders() // 리스트 갱신 -// case .failure(let error): -// errorMessage = error.message -// } -// } } diff --git a/StockMate/StockMate/app/feature/parts/data/PartApi.swift b/StockMate/StockMate/app/feature/parts/data/PartApi.swift index e1d4050..9df2b88 100644 --- a/StockMate/StockMate/app/feature/parts/data/PartApi.swift +++ b/StockMate/StockMate/app/feature/parts/data/PartApi.swift @@ -9,12 +9,13 @@ import Foundation import Alamofire -// ✅ 요청 모델 (partCode → partId 로 변경) +// 출고(Release) 요청 시 사용되는 부품 항목 데이터 모델 struct ReleaseItemRequest: Encodable { let partId: Int let quantity: Int } +// 부품 상세 정보 응답 struct PartDetailResponse: Decodable, Identifiable { let id: Int let name: String @@ -45,12 +46,11 @@ struct PartDetail: Identifiable { } -// ✅ API 정의 +// API enum PartApi { static func releaseParts(items: [ReleaseItemRequest]) -> DataRequest { let url = ApiClient.baseURL + "api/v1/store/release" let body: [String: Any] = [ -// "items": items.map { ["partId": $0.partId, "quantity": $0.quantity] } "items": items.map { ["partId": $0.partId, "quantity": $0.quantity] } ] @@ -62,11 +62,11 @@ enum PartApi { ) } - // ✅ 부품 상세 조회 API + // 부품 상세 조회 API static func fetchPartDetail(partIds: [Int]) -> DataRequest { let url = ApiClient.baseURL + "api/v1/parts/detail" - // ✅ 요청 본문은 단순 배열 형태이므로 parameters 사용 X, 직접 body에 encode + // 요청 본문은 단순 배열 형태이므로 parameters 사용 X, 직접 body에 encode return ApiClient.shared.request( url, method: .post, diff --git a/StockMate/StockMate/app/feature/parts/data/PartRepositoryImpl.swift b/StockMate/StockMate/app/feature/parts/data/PartRepositoryImpl.swift index cc577f5..87da8ce 100644 --- a/StockMate/StockMate/app/feature/parts/data/PartRepositoryImpl.swift +++ b/StockMate/StockMate/app/feature/parts/data/PartRepositoryImpl.swift @@ -9,11 +9,13 @@ import Foundation import Alamofire final class PartRepositoryImpl: PartRepositoryProtocol { + // 부품 출고 요청 func releaseParts(items: [ReleaseItemRequest]) async -> AppResult> { 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 index 48436dc..11f7fb2 100644 --- a/StockMate/StockMate/app/feature/parts/data/PartStore.swift +++ b/StockMate/StockMate/app/feature/parts/data/PartStore.swift @@ -7,10 +7,12 @@ 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 @@ -21,11 +23,12 @@ final class PartStore: ObservableObject { } } + // 선택된 부품 전체 초기화 func clear() { parts.removeAll() } - // ✅ 수량 변경용 메서드 추가 + // 수량 변경용 메서드 추가 func increaseQuantity(for part: PartDetail) { if let index = parts.firstIndex(where: { $0.id == part.id }) { parts[index].quantity += 1 @@ -33,6 +36,7 @@ final class PartStore: ObservableObject { } } + // 부품 수량 감소 (최소 1개까지) func decreaseQuantity(for part: PartDetail) { if let index = parts.firstIndex(where: { $0.id == part.id }), parts[index].quantity > 1 { @@ -41,6 +45,7 @@ final class PartStore: ObservableObject { } } + // 수량 감소 후 1개 이하일 경우 목록에서 제거 func decreaseQuantityOrRemove(for part: PartDetail) { if let index = parts.firstIndex(where: { $0.id == part.id }) { if parts[index].quantity > 1 { diff --git a/StockMate/StockMate/app/feature/parts/domain/PartRepositoryProtocol.swift b/StockMate/StockMate/app/feature/parts/domain/PartRepositoryProtocol.swift index 14f73c1..d75befa 100644 --- a/StockMate/StockMate/app/feature/parts/domain/PartRepositoryProtocol.swift +++ b/StockMate/StockMate/app/feature/parts/domain/PartRepositoryProtocol.swift @@ -8,6 +8,9 @@ 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 c4a109a..4773356 100644 --- a/StockMate/StockMate/app/feature/parts/ui/QRScannerView.swift +++ b/StockMate/StockMate/app/feature/parts/ui/QRScannerView.swift @@ -9,27 +9,19 @@ import SwiftUI struct QRScannerView: UIViewControllerRepresentable { @Binding var scannedCode: String? - var isActive: Bool = true // ✅ 추가: 카메라 활성화 상태 + var isActive: Bool = true func makeUIViewController(context: Context) -> QRScannerViewController { let controller = QRScannerViewController() controller.delegate = context.coordinator return controller } - -// 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() // ✅ 바텀시트 닫혔을 때 다시 스캔 시작 + uiViewController.startSession() // 바텀시트 닫혔을 때 다시 스캔 시작 } else { - uiViewController.stopSession() // ✅ 바텀시트 열렸을 때 스캔 일시정지 + uiViewController.stopSession() // 바텀시트 열렸을 때 스캔 일시정지 } } diff --git a/StockMate/StockMate/app/feature/parts/ui/QRScannerViewController.swift b/StockMate/StockMate/app/feature/parts/ui/QRScannerViewController.swift index 626dab2..0ec635f 100644 --- a/StockMate/StockMate/app/feature/parts/ui/QRScannerViewController.swift +++ b/StockMate/StockMate/app/feature/parts/ui/QRScannerViewController.swift @@ -53,13 +53,9 @@ final class QRScannerViewController: UIViewController, AVCaptureMetadataOutputOb startScanning() - // ⚠️ 백그라운드에서 실행 -// DispatchQueue.global(qos: .userInitiated).async { -// self.captureSession.startRunning() -// } } - // ✅ 스캔 재시작/중단 함수 추가 + // 스캔 재시작/중단 함수 추가 func startScanning() { guard captureSession != nil else { return } if !captureSession.isRunning { @@ -89,7 +85,7 @@ final class QRScannerViewController: UIViewController, AVCaptureMetadataOutputOb } - // ✅ QR 감지 시 호출 + // 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 394dc62..d4ce883 100644 --- a/StockMate/StockMate/app/feature/parts/viewmodel/PartViewModel.swift +++ b/StockMate/StockMate/app/feature/parts/viewmodel/PartViewModel.swift @@ -56,14 +56,14 @@ final class PartViewModel: ObservableObject { case .success(let apiResp): if apiResp.success, let data = apiResp.data { partDetails = data - print("✅ 부품 상세 조회 성공:", data) + print("부품 상세 조회 성공:", data) } else { message = apiResp.message - print("⚠️ 서버 응답 실패:", apiResp.message) + print("서버 응답 실패:", apiResp.message) } case .failure(let err): message = err.message - print("❌ 네트워크 오류:", err) + print("네트워크 오류:", err) } } diff --git a/StockMate/StockMate/app/feature/payment/data/PaymentApi.swift b/StockMate/StockMate/app/feature/payment/data/PaymentApi.swift index c2db3ef..fcca9b3 100644 --- a/StockMate/StockMate/app/feature/payment/data/PaymentApi.swift +++ b/StockMate/StockMate/app/feature/payment/data/PaymentApi.swift @@ -16,7 +16,7 @@ struct PaymentAmount: Decodable { let userId: Int } -// ✅ 월별 소비 내역 구조체 +// 월별 소비 내역 구조체 struct MonthlySpending: Decodable, Identifiable { var id: String { month } // 리스트에서 사용하기 편하게 let month: String @@ -51,14 +51,14 @@ enum PaymentApi { ) } - // ✅ 최근 5개월 소비 내역 조회 + // 최근 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 6024082..eee8a37 100644 --- a/StockMate/StockMate/app/feature/payment/data/PaymentRepositoryImpl.swift +++ b/StockMate/StockMate/app/feature/payment/data/PaymentRepositoryImpl.swift @@ -10,8 +10,10 @@ import Foundation import Alamofire +// 결제 및 예치금 관련 Repository 구현체 final class PaymentRepositoryImpl: PaymentRepositoryProtocol { + // 예치금 잔액 조회 func fetchDepositAmount() async -> AppResult { let request = PaymentApi.getPaymentAmount() let result = await safeApi(request, decodeTo: ApiResponse.self) @@ -33,6 +35,7 @@ final class PaymentRepositoryImpl: PaymentRepositoryProtocol { } } + // 예치금 충전 func chargeDeposit(amount: Int) async -> AppResult { let request = PaymentApi.chargeDeposit(amount: amount) let result = await safeApi(request, decodeTo: ApiResponse.self) @@ -46,7 +49,7 @@ final class PaymentRepositoryImpl: PaymentRepositoryProtocol { } } - // ✅ 최근 5개월 소비 내역 조회 + // 최근 5개월 소비 내역 조회 func fetchMonthlySpending() async -> AppResult<[MonthlySpending]> { let request = PaymentApi.getMonthlySpending() let result = await safeApi(request, decodeTo: ApiResponse<[MonthlySpending]>.self) @@ -62,7 +65,7 @@ final class PaymentRepositoryImpl: PaymentRepositoryProtocol { } } - // ✅ 지난달 카테고리별 지출 + // 지난달 카테고리별 지출 func fetchCategorySpending() async -> AppResult<[CategorySpending]> { let request = PaymentApi.getCategorySpending() let result = await safeApi(request, decodeTo: ApiResponse<[CategorySpending]>.self) diff --git a/StockMate/StockMate/app/feature/payment/domain/PaymentRepositoryProtocol.swift b/StockMate/StockMate/app/feature/payment/domain/PaymentRepositoryProtocol.swift index fe22926..bc5ffbc 100644 --- a/StockMate/StockMate/app/feature/payment/domain/PaymentRepositoryProtocol.swift +++ b/StockMate/StockMate/app/feature/payment/domain/PaymentRepositoryProtocol.swift @@ -12,7 +12,7 @@ protocol PaymentRepositoryProtocol { func fetchDepositAmount() async -> AppResult func chargeDeposit(amount: Int) async -> AppResult - // ✅ 최근 5개월 소비 내역 조회 + // 최근 5개월 소비 내역 조회 func fetchMonthlySpending() async -> AppResult<[MonthlySpending]> // 지난달 카테고리별 지출 diff --git a/StockMate/StockMate/app/feature/payment/ui/DepositChargeView.swift b/StockMate/StockMate/app/feature/payment/ui/DepositChargeView.swift index 538412f..5070451 100644 --- a/StockMate/StockMate/app/feature/payment/ui/DepositChargeView.swift +++ b/StockMate/StockMate/app/feature/payment/ui/DepositChargeView.swift @@ -6,6 +6,9 @@ struct DepositChargeView: View { @State private var amountText: String = "" @State private var isCharging: Bool = false + var onChargeSuccess: (() -> Void)? + + let keypad: [[String]] = [ ["1","2","3"], ["4","5","6"], @@ -37,44 +40,38 @@ struct DepositChargeView: View { // Title Text("예치금 충전") - .font(.system(size: 20, weight: .semibold)) - .padding(.top, 35) + .font(.system(size: 18, weight: .semibold)) + .padding(.top, 42) // 금액 Text(formattedAmount) - .font(.system(size: 38, weight: .bold)) - .padding(.top, 35) - - // 아래 작은 라벨 -// Text("\(formattedNumberString)원") -// .font(.system(size: 15)) -// .foregroundColor(.textGray1) -// .padding(.horizontal, 10) -// .padding(.vertical, 4) -// .background(Color.white.opacity(0.9)) -// .cornerRadius(12) - - Spacer().frame(height: 80) + .font(.system(size: 32, weight: .bold)) // 키패드 - LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 0), count: 3), - spacing: 1) { + LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 11), count: 3), + spacing: 16) { ForEach(keypad.flatMap { $0 }, id: \.self) { key in Button { buttonAction(key) } label: { - Text(key) - .font(.system(size: 20)) - .frame(width: 50, height: 45) - .foregroundColor(Color.black) -// .background(Color.red.opacity(0.5)) - .padding(.horizontal, 4) - .background(Color.white) + if key == "⌫" { + Image("backspace") + .resizable() + .scaledToFit() + .frame(width: 32, height: 32) + .frame(width: 60, height: 45) + .background(Color.white) + } else { + Text(key) + .font(.system(size: 19)) + .foregroundColor(Color.black) + .frame(width: 60, height: 45) + .background(Color.white) + } } } } .padding(.horizontal,16) - .padding(.top, 28) // 충전 버튼 @@ -88,6 +85,9 @@ struct DepositChargeView: View { if success { viewModel.showChargeSheet = false dismiss() + onChargeSuccess?() + } else { + } } } label: { @@ -98,20 +98,25 @@ struct DepositChargeView: View { .frame(maxWidth: .infinity) } else { Text("충전") - .font(.system(size: 18, weight: .bold)) + .font(.system(size: 15, weight: .semibold)) .foregroundColor(.white) - .frame(height: 59) + .frame(height: 49) .frame(maxWidth: .infinity) } } - .background(Color.Primary) - .cornerRadius(28) - .padding(.top, 28) + .background( + (Int(amountText) ?? 0) > 0 && !isCharging + ? Color.Primary + : Color.gray.opacity(0.3) + ) + .cornerRadius(18) + .padding(.bottom, 25) .padding(.horizontal, 10) - .disabled(isCharging) + .disabled(isCharging || (Int(amountText) ?? 0) == 0) } .padding(.horizontal, 20) -// .padding(.top, 20) .background(Color.white) + + } } diff --git a/StockMate/StockMate/app/feature/payment/viewmodel/DashboardViewModel.swift b/StockMate/StockMate/app/feature/payment/viewmodel/DashboardViewModel.swift index 8ef7136..07b2b94 100644 --- a/StockMate/StockMate/app/feature/payment/viewmodel/DashboardViewModel.swift +++ b/StockMate/StockMate/app/feature/payment/viewmodel/DashboardViewModel.swift @@ -16,7 +16,7 @@ final class DashboardViewModel: ObservableObject { @Published var isLoading = false - /// ✅ 최근 5개월 소비 내역 조회 + // 최근 5개월 소비 내역 조회 func fetchMonthlySpending() async { isLoading = true let result = await repo.fetchMonthlySpending() @@ -30,27 +30,27 @@ final class DashboardViewModel: ObservableObject { } } - // ✅ 지난달 카테고리별 지출 금액 조회 - func fetchCategorySpending() async { - isLoading = true - let result = await repo.fetchCategorySpending() - isLoading = false + // 지난달 카테고리별 지출 금액 조회 + 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) - } - } + 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월") + // 월 라벨 (예: "10월", "11월") var monthLabels: [String] { monthlySpendings.map { month in // "2025-10" → "10월" diff --git a/StockMate/StockMate/app/feature/payment/viewmodel/DepositViewModel.swift b/StockMate/StockMate/app/feature/payment/viewmodel/DepositViewModel.swift index 27d6009..3eaad6e 100644 --- a/StockMate/StockMate/app/feature/payment/viewmodel/DepositViewModel.swift +++ b/StockMate/StockMate/app/feature/payment/viewmodel/DepositViewModel.swift @@ -15,7 +15,10 @@ final class DepositViewModel: ObservableObject { @Published var isLoading: Bool = false @Published var showChargeSheet: Bool = false - /// ✅ 예치금 조회 + @Published var depositAmount: Int = 0 + @Published var isChargeSuccess: Bool = false + + // 예치금 조회 func fetchDepositAmount() async { isLoading = true @@ -31,12 +34,13 @@ final class DepositViewModel: ObservableObject { } } - /// ✅ 예치금 충전 + // 예치금 충전 func chargeDeposit(amount: Int) async -> Bool { let result = await repository.chargeDeposit(amount: amount) switch result { case .success(_): await fetchDepositAmount() + isChargeSuccess = true return true case .failure(let error): print("❌ 충전 실패:", error.message) diff --git a/StockMate/StockMate/app/feature/user/data/UserApi.swift b/StockMate/StockMate/app/feature/user/data/UserApi.swift index 368f6ff..5a6970a 100644 --- a/StockMate/StockMate/app/feature/user/data/UserApi.swift +++ b/StockMate/StockMate/app/feature/user/data/UserApi.swift @@ -8,6 +8,7 @@ import Foundation import Alamofire +// MARK: - 사용자 정보 응답 모델 struct UserInfoResponse: Decodable { let status: Int let success: Bool @@ -15,6 +16,7 @@ struct UserInfoResponse: Decodable { let data: UserInfo? } +// MARK: - 사용자 정보 모델 struct UserInfo: Decodable { let createdAt: String let updatedAt: String @@ -31,6 +33,7 @@ struct UserInfo: Decodable { let verified: String } +// MARK: - 사용자 API 요청 정의 enum UserApi { static func getUserInfo() -> DataRequest { let url = ApiClient.baseURL + "api/v1/user/my" diff --git a/StockMate/StockMate/app/feature/user/data/UserRepositoryImpl.swift b/StockMate/StockMate/app/feature/user/data/UserRepositoryImpl.swift index 7bd1289..69caa17 100644 --- a/StockMate/StockMate/app/feature/user/data/UserRepositoryImpl.swift +++ b/StockMate/StockMate/app/feature/user/data/UserRepositoryImpl.swift @@ -8,7 +8,9 @@ import Foundation import Alamofire +// MARK: - 사용자 관련 Repository 구현체 final class UserRepositoryImpl: UserRepositoryProtocol { + // 사용자 정보 조회 func getUserInfo() async -> AppResult> { let dataReq = UserApi.getUserInfo() return await safeApi(dataReq, decodeTo: ApiResponse.self) diff --git a/StockMate/StockMate/app/feature/user/domain/UserRepositoryProtocol.swift b/StockMate/StockMate/app/feature/user/domain/UserRepositoryProtocol.swift index f5b031b..460f943 100644 --- a/StockMate/StockMate/app/feature/user/domain/UserRepositoryProtocol.swift +++ b/StockMate/StockMate/app/feature/user/domain/UserRepositoryProtocol.swift @@ -8,5 +8,6 @@ import Foundation protocol UserRepositoryProtocol { + // 사용자 정보 조회 func getUserInfo() async -> AppResult> } diff --git a/StockMate/StockMate/app/feature/user/ui/ProfileCircleView.swift b/StockMate/StockMate/app/feature/user/ui/ProfileCircleView.swift index 2e1a97d..3f26da6 100644 --- a/StockMate/StockMate/app/feature/user/ui/ProfileCircleView.swift +++ b/StockMate/StockMate/app/feature/user/ui/ProfileCircleView.swift @@ -21,7 +21,7 @@ struct ProfileCircleView: View { GeometryReader { geometry in let minSide = min(geometry.size.width, geometry.size.height) Text(initials) - .font(.system(size: minSide * 0.35, weight: .regular)) // ✅ 내부 크기 비례 + .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")) diff --git a/StockMate/StockMate/app/feature/user/ui/ProfileView.swift b/StockMate/StockMate/app/feature/user/ui/ProfileView.swift index 00d3191..fc1edc6 100644 --- a/StockMate/StockMate/app/feature/user/ui/ProfileView.swift +++ b/StockMate/StockMate/app/feature/user/ui/ProfileView.swift @@ -9,8 +9,8 @@ import SwiftUI struct ProfileView: View { @StateObject private var userViewModel = UserViewModel() - @EnvironmentObject var authViewModel: AuthViewModel // 🔹 전역 Auth 상태 참조 - @State private var showLogoutModal = false // 🔹 로그아웃 모달 상태 + @EnvironmentObject var authViewModel: AuthViewModel // 전역 Auth 상태 참조 + @State private var showLogoutModal = false // 로그아웃 모달 상태 var body: some View { ZStack{ @@ -21,16 +21,13 @@ struct ProfileView: View { ProfileCircleView(name: userViewModel.userInfo?.owner ?? "사용자", size: 50) VStack(alignment: .leading, spacing: 4) { - HStack { - Image("location") - .foregroundColor(.gray) - Text(userViewModel.userInfo?.storeName ?? "가게명 없음") - .foregroundColor(.gray) - .font(.subheadline) - } Text(userViewModel.userInfo?.owner ?? "이름 없음") .font(.title3.bold()) .foregroundColor(Color(hex: "#2B3A1A")) + + Text(userViewModel.userInfo?.email ?? "이메일 없음") + .foregroundColor(.gray) + .font(.subheadline) } Spacer() @@ -43,12 +40,9 @@ struct ProfileView: View { VStack(spacing: 10) { SettingNavigationRow(icon: "user", title: "프로필 확인", destination: UserProfileView()) SettingNavigationRow(icon: "notification", title: "알림", destination: NotificationListView()) -// SettingRow(icon: "notification", title: "알림") SettingNavigationRow(icon: "receipt", title: "예치금 히스토리", destination: TransactionTypeListView()) -// SettingNavigationRow(icon: "receipt", title: "예치금 히스토리", destination: PaymentTransactionView()) SettingNavigationRow(icon: "bag", title: "주문 내역", destination: OrderListView()) - // SettingRow(icon: "logout", title: "로그아웃") - // 🔹 로그아웃 버튼 + // 로그아웃 버튼 Button { showLogoutModal = true } label: { @@ -69,7 +63,7 @@ struct ProfileView: View { Task { await userViewModel.loadUserInfo() } } - // 🔹 AlertModal (ZStack 위에 오버레이로 표시) + // AlertModal (ZStack 위에 오버레이로 표시) if showLogoutModal { Color.black.opacity(0.3) .ignoresSafeArea() diff --git a/StockMate/StockMate/app/feature/user/ui/UserProfileView.swift b/StockMate/StockMate/app/feature/user/ui/UserProfileView.swift index 9094fbe..0ce5291 100644 --- a/StockMate/StockMate/app/feature/user/ui/UserProfileView.swift +++ b/StockMate/StockMate/app/feature/user/ui/UserProfileView.swift @@ -8,6 +8,7 @@ import SwiftUI struct UserProfileView: View { + @Environment(\.dismiss) private var dismiss @StateObject private var userViewModel = UserViewModel() var body: some View { @@ -36,6 +37,20 @@ struct UserProfileView: View { .navigationTitle("프로필 확인") .background(Color.Light) .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + dismiss() + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.left") + .font(.system(size: 15, weight: .medium)) + } + .foregroundColor(.black) + } + } + } .onAppear { Task { await userViewModel.loadUserInfo() } } diff --git a/StockMate/StockMate/app/feature/user/viewmodel/UserViewModel.swift b/StockMate/StockMate/app/feature/user/viewmodel/UserViewModel.swift index e47d22e..c683087 100644 --- a/StockMate/StockMate/app/feature/user/viewmodel/UserViewModel.swift +++ b/StockMate/StockMate/app/feature/user/viewmodel/UserViewModel.swift @@ -33,8 +33,8 @@ final class UserViewModel: ObservableObject { case .failure(let err): message = err.message print("유저 정보 불러오기 실패:", err.message) - // ✅ 세션 만료나 인증 문제면 로그인화면으로 유도 - if err.code == 401 || err.code == 403 { + + if err.code == 401 || err.code == 403 { // 세션 만료나 인증 문제면 로그인 화면으로 유도 shouldGoToLogin = true } diff --git a/StockMate/StockMate/app/navigation/AppNavHost.swift b/StockMate/StockMate/app/navigation/AppNavHost.swift index ffb9779..f95ea21 100644 --- a/StockMate/StockMate/app/navigation/AppNavHost.swift +++ b/StockMate/StockMate/app/navigation/AppNavHost.swift @@ -7,22 +7,25 @@ import SwiftUI +// 앱의 전반적인 네비게이션 흐름을 관리하는 뷰 struct AppNavHost: View { - @EnvironmentObject var authViewModel: AuthViewModel - @StateObject private var partStore = PartStore() + @EnvironmentObject var authViewModel: AuthViewModel // 인증 상태 관리 뷰모델 + @StateObject private var partStore = PartStore() // 부품 관련 상태 저장소 var body: some View { NavigationStack { switch authViewModel.authState { - case .unauthenticated: + // 로그인되지 않은 경우 → 로그인 화면 표시 LoginView(onClickRegister: { authViewModel.goToRegister() }) case .registering: + // 회원가입 중인 경우 → 회원가입 화면 표시 RegisterView() case .authenticated: + // 로그인 완료된 경우 → 메인 탭 화면 표시 MainTabView() .environmentObject(authViewModel) .environmentObject(partStore) diff --git a/StockMate/StockMate/app/navigation/IntroView.swift b/StockMate/StockMate/app/navigation/IntroView.swift new file mode 100644 index 0000000..742d03a --- /dev/null +++ b/StockMate/StockMate/app/navigation/IntroView.swift @@ -0,0 +1,21 @@ +// +// IntroView.swift +// StockMate +// +// Created by Admin on 11/10/25. +// + +import SwiftUI + +struct IntroView: View { + var body: some View { + ZStack { + Color.white.ignoresSafeArea() // 배경 흰색 + + Image("intrologo") // Assets에 있는 이미지 이름 + .resizable() + .scaledToFit() + .frame(width: 180, height: 180) // 필요시 크기 조정 + } + } +} diff --git a/StockMate/StockMate/app/navigation/MainTabView.swift b/StockMate/StockMate/app/navigation/MainTabView.swift index 3645577..ec1ad2a 100644 --- a/StockMate/StockMate/app/navigation/MainTabView.swift +++ b/StockMate/StockMate/app/navigation/MainTabView.swift @@ -19,18 +19,12 @@ struct MainTabView: View { ZStack { switch selectedTab { case 0: HomeView() - case 1: NavigationStack{ OrderView(cartViewModel: cartVM) } //, inventoryViewModel: inventoryVM) } -// case 1: NavigationStack{ OrderView() } + case 1: NavigationStack{ OrderView(cartViewModel: cartVM) } case 2: InventoryView( selectedTab: $selectedTab, tabTappedTrigger: $tabTappedTrigger ) - -// NavigationStack { InventoryView(selectedTab: $selectedTab, tabTappedTrigger: $tabTappedTrigger) } -// case 3: NavigationStack{ ContentView() } -// case 3: NavigationStack{ ReceiptView() } -// case 3: NavigationStack{ NotificationListView() } case 3: ProfileView() default: NavigationStack{ ContentView() } } @@ -44,9 +38,9 @@ struct MainTabView: View { tabButton(index: 2, icon: "tabInventory", text: "재고관리") tabButton(index: 3, icon: "tabProfile", text: "사용자") } - .padding(.vertical, 24) // 탭 높이 조절 + .padding(.vertical, 24) .padding(.horizontal, 20) - .background(Color.White) // 탭바 배경색 + .background(Color.White) } .edgesIgnoringSafeArea(.bottom) } @@ -56,16 +50,13 @@ struct MainTabView: View { let isSelected = selectedTab == index return Button { if selectedTab == index { - // ✅ 같은 탭 다시 누르면 트리거 토글 + // 같은 탭 다시 누르면 트리거 토글 tabTappedTrigger.toggle() } else { withAnimation(.easeInOut) { selectedTab = index } } -// withAnimation(.easeInOut) { // 탭 전환 애니메이션. 없애도 됨 -// selectedTab = index -// } } label: { VStack(spacing: 6) { Image(icon) diff --git a/StockMate/StockMate/resources/Assets.xcassets/AppIcon.appiconset/AppIcon_1024.png b/StockMate/StockMate/resources/Assets.xcassets/AppIcon.appiconset/AppIcon_1024.png new file mode 100644 index 0000000..de54277 Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/AppIcon.appiconset/AppIcon_1024.png differ diff --git a/StockMate/StockMate/resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/AppIcon.appiconset/Contents.json index 2305880..840dd3d 100644 --- a/StockMate/StockMate/resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/StockMate/StockMate/resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "AppIcon_1024.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/StockMate/StockMate/resources/Assets.xcassets/backspace.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/backspace.imageset/Contents.json new file mode 100644 index 0000000..b693e6f --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/backspace.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "backspace@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "backspace@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "backspace@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/backspace.imageset/backspace@1x.png b/StockMate/StockMate/resources/Assets.xcassets/backspace.imageset/backspace@1x.png new file mode 100644 index 0000000..1aff5a9 Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/backspace.imageset/backspace@1x.png differ diff --git a/StockMate/StockMate/resources/Assets.xcassets/backspace.imageset/backspace@2x.png b/StockMate/StockMate/resources/Assets.xcassets/backspace.imageset/backspace@2x.png new file mode 100644 index 0000000..84f1648 Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/backspace.imageset/backspace@2x.png differ diff --git a/StockMate/StockMate/resources/Assets.xcassets/backspace.imageset/backspace@3x.png b/StockMate/StockMate/resources/Assets.xcassets/backspace.imageset/backspace@3x.png new file mode 100644 index 0000000..f7b505b Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/backspace.imageset/backspace@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 index 66aacdc..0ce9f32 100644 --- a/StockMate/StockMate/resources/Assets.xcassets/bag.imageset/Contents.json +++ b/StockMate/StockMate/resources/Assets.xcassets/bag.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "filename" : "bag.svg", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { diff --git a/StockMate/StockMate/resources/Assets.xcassets/profile_sample.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/cal.imageset/Contents.json similarity index 73% rename from StockMate/StockMate/resources/Assets.xcassets/profile_sample.imageset/Contents.json rename to StockMate/StockMate/resources/Assets.xcassets/cal.imageset/Contents.json index 54602a7..77d19ae 100644 --- a/StockMate/StockMate/resources/Assets.xcassets/profile_sample.imageset/Contents.json +++ b/StockMate/StockMate/resources/Assets.xcassets/cal.imageset/Contents.json @@ -1,15 +1,17 @@ { "images" : [ { - "filename" : "profile_sample.png", + "filename" : "cal@1x.png", "idiom" : "universal", "scale" : "1x" }, { + "filename" : "cal@2x.png", "idiom" : "universal", "scale" : "2x" }, { + "filename" : "cal@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/StockMate/StockMate/resources/Assets.xcassets/cal.imageset/cal@1x.png b/StockMate/StockMate/resources/Assets.xcassets/cal.imageset/cal@1x.png new file mode 100644 index 0000000..e1c01f1 Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/cal.imageset/cal@1x.png differ diff --git a/StockMate/StockMate/resources/Assets.xcassets/cal.imageset/cal@2x.png b/StockMate/StockMate/resources/Assets.xcassets/cal.imageset/cal@2x.png new file mode 100644 index 0000000..0ea001a Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/cal.imageset/cal@2x.png differ diff --git a/StockMate/StockMate/resources/Assets.xcassets/cal.imageset/cal@3x.png b/StockMate/StockMate/resources/Assets.xcassets/cal.imageset/cal@3x.png new file mode 100644 index 0000000..4f98e83 Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/cal.imageset/cal@3x.png differ diff --git a/StockMate/StockMate/resources/Assets.xcassets/dline.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/dline.imageset/Contents.json new file mode 100644 index 0000000..30d1dee --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/dline.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "dline.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/dline.imageset/dline.svg b/StockMate/StockMate/resources/Assets.xcassets/dline.imageset/dline.svg new file mode 100644 index 0000000..b75984c --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/dline.imageset/dline.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/StockMate/StockMate/resources/Assets.xcassets/intrologo.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/intrologo.imageset/Contents.json new file mode 100644 index 0000000..8cd6366 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/intrologo.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "intrologo@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "intrologo@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "intrologo@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/intrologo.imageset/intrologo@1x.png b/StockMate/StockMate/resources/Assets.xcassets/intrologo.imageset/intrologo@1x.png new file mode 100644 index 0000000..de433f1 Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/intrologo.imageset/intrologo@1x.png differ diff --git a/StockMate/StockMate/resources/Assets.xcassets/intrologo.imageset/intrologo@2x.png b/StockMate/StockMate/resources/Assets.xcassets/intrologo.imageset/intrologo@2x.png new file mode 100644 index 0000000..0810427 Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/intrologo.imageset/intrologo@2x.png differ diff --git a/StockMate/StockMate/resources/Assets.xcassets/intrologo.imageset/intrologo@3x.png b/StockMate/StockMate/resources/Assets.xcassets/intrologo.imageset/intrologo@3x.png new file mode 100644 index 0000000..e3e80d0 Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/intrologo.imageset/intrologo@3x.png differ diff --git a/StockMate/StockMate/resources/Assets.xcassets/profile_sample.imageset/profile_sample.png b/StockMate/StockMate/resources/Assets.xcassets/profile_sample.imageset/profile_sample.png deleted file mode 100644 index f5266f9..0000000 Binary files a/StockMate/StockMate/resources/Assets.xcassets/profile_sample.imageset/profile_sample.png and /dev/null differ diff --git a/StockMate/StockMate/resources/Assets.xcassets/stockmate_logo.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/stockmate_logo.imageset/Contents.json index cbb69f9..10b8a40 100644 --- a/StockMate/StockMate/resources/Assets.xcassets/stockmate_logo.imageset/Contents.json +++ b/StockMate/StockMate/resources/Assets.xcassets/stockmate_logo.imageset/Contents.json @@ -1,15 +1,17 @@ { "images" : [ { - "filename" : "Group 148272 (2).png", + "filename" : "stockmate_logo@1x.png", "idiom" : "universal", "scale" : "1x" }, { + "filename" : "stockmate_logo@2x.png", "idiom" : "universal", "scale" : "2x" }, { + "filename" : "stockmate_logo@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/StockMate/StockMate/resources/Assets.xcassets/stockmate_logo.imageset/stockmate_logo@1x.png b/StockMate/StockMate/resources/Assets.xcassets/stockmate_logo.imageset/stockmate_logo@1x.png new file mode 100644 index 0000000..0b4cb42 Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/stockmate_logo.imageset/stockmate_logo@1x.png differ diff --git a/StockMate/StockMate/resources/Assets.xcassets/stockmate_logo.imageset/stockmate_logo@2x.png b/StockMate/StockMate/resources/Assets.xcassets/stockmate_logo.imageset/stockmate_logo@2x.png new file mode 100644 index 0000000..f7ea6fe Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/stockmate_logo.imageset/stockmate_logo@2x.png differ diff --git a/StockMate/StockMate/resources/Assets.xcassets/stockmate_logo.imageset/Group 148272 (2).png b/StockMate/StockMate/resources/Assets.xcassets/stockmate_logo.imageset/stockmate_logo@3x.png similarity index 100% rename from StockMate/StockMate/resources/Assets.xcassets/stockmate_logo.imageset/Group 148272 (2).png rename to StockMate/StockMate/resources/Assets.xcassets/stockmate_logo.imageset/stockmate_logo@3x.png diff --git a/StockMate/StockMate/resources/Assets.xcassets/toastlogo.imageset/Contents.json b/StockMate/StockMate/resources/Assets.xcassets/toastlogo.imageset/Contents.json new file mode 100644 index 0000000..a3a0113 --- /dev/null +++ b/StockMate/StockMate/resources/Assets.xcassets/toastlogo.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "toastlogo@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "toastlogo@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "toastlogo@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StockMate/StockMate/resources/Assets.xcassets/toastlogo.imageset/toastlogo@1x.png b/StockMate/StockMate/resources/Assets.xcassets/toastlogo.imageset/toastlogo@1x.png new file mode 100644 index 0000000..9ffd3ba Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/toastlogo.imageset/toastlogo@1x.png differ diff --git a/StockMate/StockMate/resources/Assets.xcassets/toastlogo.imageset/toastlogo@2x.png b/StockMate/StockMate/resources/Assets.xcassets/toastlogo.imageset/toastlogo@2x.png new file mode 100644 index 0000000..1193e9d Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/toastlogo.imageset/toastlogo@2x.png differ diff --git a/StockMate/StockMate/resources/Assets.xcassets/toastlogo.imageset/toastlogo@3x.png b/StockMate/StockMate/resources/Assets.xcassets/toastlogo.imageset/toastlogo@3x.png new file mode 100644 index 0000000..3d0fa5d Binary files /dev/null and b/StockMate/StockMate/resources/Assets.xcassets/toastlogo.imageset/toastlogo@3x.png differ