Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Walkthrough주소 검색(카카오 우편번호) 통합, 대시보드·거래·입출고 히스토리 및 결제 데이터 계층 추가, 부품 관리(PartStore)·QR 스캐너 제어 및 출고 워크플로우 도입, 주문/배송 상태 재구성 및 여러 UI 컴포넌트·자산 추가/개선이 포함된 집합적 변경입니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant User as 사용자
participant OutgoingView as OutgoingScanView
participant Scanner as QRScannerViewController
participant API as PartApi
participant Store as PartStore
participant Sheet as UsedPartListSheetView
User->>Scanner: QR 스캔
Scanner->>API: fetchPartDetail(partIds)
API-->>Scanner: PartDetailResponse
Scanner->>Store: addPart(PartDetail)
Store-->>OutgoingView: parts 업데이트
OutgoingView->>Sheet: show sheet (UsedPartList)
User->>Sheet: 수량 조절 / 사용 처리
Sheet->>API: releaseParts(partId, quantity)
API-->>Sheet: 성공 응답
Sheet->>Store: clear()
sequenceDiagram
autonumber
participant User as 사용자
participant Home as HomeView
participant VM as DashboardViewModel
participant API as PaymentApi
participant Charts as ChartViews
User->>Home: 홈 진입
Home->>VM: fetchMonthlySpending()
VM->>API: GET /api/v1/payment/monthly-spending
API-->>VM: [MonthlySpending]
VM-->>Home: monthlySpendings 업데이트
Home->>Charts: BarChartView(데이터)
Home->>VM: fetchCategorySpending()
VM->>API: GET /api/v1/order/category-spend
API-->>VM: [CategorySpending]
VM-->>Home: categorySpendings 업데이트
Charts-->>User: 차트 렌더링
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 주의 깊게 검토할 부분:
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
@coderabbitai review |
|
@Yoo-Hyuna: I'll review the changes in this PR for you now. ✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 19
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
StockMate/StockMate/app/feature/orders/ui/ReceiptView.swift (1)
232-244: UTC 파싱 타임존 변경으로 시간 표시가 틀어집니다
createdAt이 UTC 기준으로 내려오는 상황에서inputFormatter.timeZone을TimeZone.current로 바꾸면, 파싱 자체가 현지 시각으로 해석되어 +/− 시차만큼 잘못된 시간이 노출됩니다. 기존처럼 UTC로 파싱한 뒤 출력 포맷에서만 현지 시각으로 변환하도록 되돌려 주세요.- inputFormatter.timeZone = TimeZone.current // ✅ 실제 한국 시간 기준 + inputFormatter.timeZone = TimeZone(abbreviation: "UTC")
🧹 Nitpick comments (30)
StockMate/StockMate/resources/Assets.xcassets/notification 1.imageset/Contents.json (1)
1-12: 자산 카탈로그 파일명에서 공백 제거를 권장합니다.현재 디렉토리명이
notification 1.imageset으로 공백을 포함하고 있습니다. 일반적으로 리소스 파일명에서 공백을 피하는 것이 관례입니다. 코드에서 이 자산을 참조할 때 혼동을 줄이기 위해notification.imageset으로 변경하는 것을 권장합니다.또한
notification.svg파일이 실제로 같은 디렉토리에 존재하는지 확인하여 런타임 오류를 방지해주세요.StockMate/StockMate/app/core/components/CustomTextField.swift (1)
48-60: 접근성 속성 추가를 권장합니다읽기 전용 모드에서 스크린 리더 사용자를 위한 접근성 정보가 부족합니다.
.accessibilityValue(text)와.accessibilityTraits(.staticText)추가를 고려해주세요.다음 diff를 적용하여 접근성을 개선할 수 있습니다:
if isReadOnly { // ✅ 가로 스크롤 가능한 읽기 전용 텍스트 ScrollView(.horizontal, showsIndicators: false) { Text(text.isEmpty ? placeholder : text) .font(.system(size: 15)) .foregroundColor(text.isEmpty ? .gray : .black) .padding(.horizontal, 14) .padding(.vertical, 12) .lineLimit(1) + .accessibilityValue(text.isEmpty ? placeholder : text) + .accessibilityTraits(.staticText) } .frame(height: 46) .contentShape(Rectangle())StockMate/StockMate/app/core/components/CustomButtonStyle.swift (2)
35-47: outlined 버튼에 press 피드백이 부족합니다.filled 버튼은 눌렀을 때 opacity가 변경되어 시각적 피드백을 제공하지만, outlined 버튼은 press 상태에 대한 시각적 변화가 없습니다. 사용자 경험 개선을 위해 press 피드백을 추가하는 것을 권장합니다.
예시:
.overlay( RoundedRectangle(cornerRadius: cornerRadius) - .stroke(color, lineWidth: 1.5) + .stroke(color.opacity(configuration.isPressed ? 0.6 : 1), lineWidth: 1.5) ) - .foregroundColor(color) + .foregroundColor(color.opacity(configuration.isPressed ? 0.6 : 1))
32-32: 주석 처리된 코드를 정리하세요.
scaleEffect가 주석 처리되어 있습니다. 필요하지 않다면 삭제하고, 나중에 사용할 계획이라면 TODO 코멘트를 추가하는 것이 좋습니다.Also applies to: 46-46
StockMate/StockMate/app/core/components/CartCard.swift (2)
74-135: 수량 컨트롤 로직의 중복을 제거하세요.
quantity == 1과quantity > 1분기의 UI 구성이 거의 동일합니다. 유일한 차이점은 감소 버튼(trash vs minus)과 수량 표시뿐입니다. 이러한 중복은 유지보수를 어렵게 만들고 DRY 원칙에 위배됩니다.공통 스타일링 로직을 별도의 뷰 빌더 함수나 컴포넌트로 추출하는 것을 권장합니다.
예시:
@ViewBuilder private func quantityControlView(showTrash: Bool, quantity: Int) -> some View { HStack(spacing: 10) { Button(action: showTrash ? onRemoveFromCart : onDecrease) { Image(systemName: showTrash ? "trash" : "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) }
98-103: clipShape와 cornerRadius 사용을 검토하세요.
cornerRadius(10)을 적용한 후 동일한 반경으로.clipShape(RoundedRectangle(cornerRadius: 10))을 다시 적용하고 있습니다. SwiftUI에서cornerRadius는 내부적으로 clipping을 수행하므로, 명시적인clipShape가 중복일 수 있습니다.다만, 테두리 오버레이와 함께 사용할 때 정확한 clipping을 보장하기 위해 의도적으로 사용한 것이라면 문제없습니다.
Also applies to: 129-134
StockMate/StockMate/app/core/components/OrderRequestCardView.swift (1)
55-62: CartCard.swift와 shadow 스타일이 일관되지 않습니다.이 파일에서는
shadow(color: Color.black.opacity(0.25), ...)를 사용하지만,CartCard.swift(line 71)에서는opacity(0.2)를 사용합니다. 동일한 UI 요소는 일관된 스타일을 유지해야 합니다.두 파일 간의 shadow opacity를 통일하거나, 공통 스타일 상수로 정의하는 것을 권장합니다.
StockMate/StockMate/app/feature/user/ui/UserProfileView.swift (2)
22-127: 코드 중복을 줄이는 리팩토링을 고려하세요.각 필드의 구조가 반복적입니다. 재사용 가능한 컴포넌트로 추출하면 유지보수성이 향상됩니다.
예시:
struct ProfileFieldView: View { let title: String let value: String var body: some View { VStack(spacing: 9) { Text(title) .font(.system(size: 14, weight: .medium)) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 10) .foregroundColor(Color.black) HStack { Text(value) .font(.system(size: 16, weight: .regular)) .foregroundColor(.black) Spacer() } .padding() .cornerRadius(12) .overlay( RoundedRectangle(cornerRadius: 12) .stroke(Color.GrayMordern300, lineWidth: 1) ) .padding(.horizontal, 10) .padding(.bottom, 10) } } }
138-140: 에러 처리 추가를 권장합니다.API 호출 실패 시 사용자에게 피드백을 제공하는 것이 좋습니다. 로딩 상태나 에러 메시지 표시를 고려하세요.
StockMate/StockMate/app/feature/auth/ui/RegisterView.swift (2)
258-288: 사업자등록번호 포맷팅 로직이 잘 구현되었습니다.숫자만 필터링하고 하이픈을 자동으로 삽입하며 10자리로 제한하는 로직이 올바르게 작동합니다. 다만, 로직을 약간 단순화할 수 있습니다.
더 간결한 구현 예시:
private func formatBizNoInput(_ input: String) { let digitsOnly = input.filter { $0.isNumber } let trimmed = String(digitsOnly.prefix(10)) var formatted = "" for (index, char) in trimmed.enumerated() { if index == 3 || index == 5 { formatted.append("-") } formatted.append(char) } if formatted != bizNo { bizNo = formatted } }
174-180: 키보드 처리 개선을 권장합니다.고정된 300 높이의
Spacer는 모든 기기에서 최적이 아닐 수 있습니다. iOS 14+에서는 더 나은 키보드 회피 방법을 사용할 수 있습니다.다음 접근을 고려하세요:
.ignoresSafeArea(.keyboard) // 또는 적절한 키보드 회피 로직 사용또는
@FocusState와 함께 동적 패딩을 사용하는 방법도 있습니다.StockMate/StockMate/app/core/KakaoPostcode/KakaoZipCodeVC.swift (2)
36-37: 하드코딩된 URL을 구성 가능하게 만드는 것을 고려하세요.URL이 하드코딩되어 있어 나중에 변경하기 어렵습니다. 설정 가능한 프로퍼티로 만들거나 앱 설정에서 관리하는 것이 좋습니다.
class KakaoZipCodeVC: UIViewController { var postcodeURL: String = "https://yoo-hyuna.github.io/Kakao-Postcode/" // ... private func setupWebView() { // ... guard let webView = webView, let url = URL(string: postcodeURL) else { return } // ... } }
72-80: 웹 로딩 실패 처리를 추가하는 것을 권장합니다.네트워크 문제나 URL 오류로 웹 페이지 로딩이 실패할 경우 사용자에게 피드백을 제공하지 않습니다.
다음 델리게이트 메서드를 추가하세요:
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { indicator.stopAnimating() // 사용자에게 에러 알림 표시 let alert = UIAlertController(title: "오류", message: "주소 검색 페이지를 불러올 수 없습니다.", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "확인", style: .default) { [weak self] _ in self?.dismiss(animated: true) }) present(alert, animated: true) } func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { indicator.stopAnimating() // 동일한 에러 처리 }StockMate/StockMate/app/core/components/AlertModal.swift (1)
80-80: 중복된import SwiftUI를 정리해주세요
동일 모듈을 두 번 import 하면 SwiftLint 경고가 발생합니다. 필요 없는 두 번째import SwiftUI를 제거해 주세요.-import SwiftUI - #Preview {StockMate/StockMate/app/navigation/MainTabView.swift (1)
25-30: 코드 포맷팅 개선이 적용되었습니다.InventoryView 초기화가 여러 줄로 변경되어 가독성이 향상되었습니다. 기능적 변경 사항은 없습니다.
30번 라인의 주석 처리된 이전 코드는 제거하는 것을 권장합니다:
-// NavigationStack { InventoryView(selectedTab: $selectedTab, tabTappedTrigger: $tabTappedTrigger) }StockMate/StockMate/app/feature/parts/ui/QRScannerView.swift (1)
20-34: 세션 제어 로직이 올바르게 구현되었습니다.isActive 상태에 따라 startSession/stopSession을 호출하여 스캐너를 제어하는 로직이 정확합니다.
20-27번 라인의 주석 처리된 이전 구현들은 제거하는 것을 권장합니다:
-// 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) {StockMate/StockMate/app/feature/parts/viewmodel/PartViewModel.swift (1)
59-66: 로깅 개선 고려현재
StockMate/StockMate/app/feature/dashboard/domain/HistoryRepositoryProtocol.swift (1)
8-9: 불필요한 import 제거 고려프로토콜이
AppResult와ApiResponse만 사용하고 Alamofire 타입을 직접 노출하지 않으므로,import Alamofire는 불필요할 수 있습니다. Foundation import만으로 충분한지 확인하세요.import Foundation -import AlamofireStockMate/StockMate/app/feature/payment/viewmodel/DashboardViewModel.swift (3)
12-12: 의존성 주입 개선 권장
PaymentRepositoryImpl을 직접 인스턴스화하는 대신, 생성자를 통한 의존성 주입을 사용하는 것이 테스트 용이성과 유연성을 높입니다.- private let repo: PaymentRepositoryProtocol = PaymentRepositoryImpl() + private let repo: PaymentRepositoryProtocol + + init(repo: PaymentRepositoryProtocol = PaymentRepositoryImpl()) { + self.repo = repo + }
29-29: 로깅 개선 고려
Also applies to: 43-43
54-67: 월 라벨 파싱 로직 단순화 고려현재 문자열 조작이 복잡합니다. DateFormatter나 더 명확한 파싱 로직을 사용하여 가독성을 높이는 것을 고려하세요.
예시:
var monthLabels: [String] { monthlySpendings.map { spending in let components = spending.month.split(separator: "-") if components.count >= 2, let month = Int(components[1]) { return "\(month)월" } return spending.month } }StockMate/StockMate/app/feature/dashboard/ui/ReleaseDetailView.swift (1)
123-128: 불필요한 nil 초기화를 제거해 주세요.
SwiftLint가redundant_optional_initialization경고를 내고 있어요. 잡음 없이 빌드하려면 불필요한= nil을 제거하는 편이 좋겠습니다.- var date: Date? = nil + var date: Date?StockMate/StockMate/app/feature/orders/ui/OrderInfoView.swift (1)
48-56: 주석 처리된 코드를 제거하세요.더 이상 사용하지 않는 코드는 버전 관리 시스템에 기록되므로, 코드베이스를 깔끔하게 유지하기 위해 제거하는 것이 좋습니다.
-// private var destinationView: some View { -// Group { -// if let id = orderViewModel.createdOrderId { -// OrderDetailView(orderId: id, orderViewModel: orderViewModel) -// } else { -// EmptyView() -// } -// } -// }StockMate/StockMate/app/feature/parts/data/PartStore.swift (2)
28-34: 불필요한objectWillChange.send()호출을 제거하세요.
@Published프로퍼티는 자동으로 변경을 감지하므로, Line 32의objectWillChange.send()는 중복입니다. 배열 요소를 직접 수정하면 SwiftUI가 자동으로 뷰를 업데이트합니다.func increaseQuantity(for part: PartDetail) { if let index = parts.firstIndex(where: { $0.id == part.id }) { parts[index].quantity += 1 - objectWillChange.send() // 수동 갱신 트리거 } }
36-42: 불필요한objectWillChange.send()호출을 제거하세요.Line 40의
objectWillChange.send()도 마찬가지로 중복입니다. 또한, 최소 수량을 1로 제한하는 로직이 잘 구현되어 있습니다.func decreaseQuantity(for part: PartDetail) { if let index = parts.firstIndex(where: { $0.id == part.id }), parts[index].quantity > 1 { parts[index].quantity -= 1 - objectWillChange.send() } }StockMate/StockMate/app/feature/dashboard/ui/InOutHistoryView.swift (2)
28-32: 날짜 파싱 로직을 개선하세요.Line 30의 날짜 파싱 로직(
split(separator: "T").first)은 ISO 8601 형식을 가정하지만, 형식이 잘못된 경우 예상치 못한 결과를 초래할 수 있습니다. 더 견고한DateFormatter를 사용하는 것을 권장합니다.-let groupedHistories = Dictionary(grouping: viewModel.histories) { history in - history.createdAt.split(separator: "T").first.map(String.init) ?? "" -} +let groupedHistories = Dictionary(grouping: viewModel.histories) { history in + let isoFormatter = ISO8601DateFormatter() + isoFormatter.formatOptions = [.withFullDate] + if let date = isoFormatter.date(from: history.createdAt) { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + return dateFormatter.string(from: date) + } + return "" +} .sorted { $0.key > $1.key }
111-129: 중복된 네비게이션 UI를 리팩토링하세요.Lines 111-129에서 chevron 아이콘과 NavigationLink가 중복되어 있습니다. 조건에 따라 destination만 변경하는 단일 NavigationLink로 리팩토링하는 것이 좋습니다.
-if history.status == "RECEIVED", let orderId = history.orderId { - NavigationLink( - destination: OrderDetailView(orderId: orderId, orderViewModel: OrderViewModel()) - ) { - - Image(systemName: "chevron.right") - .font(.system(size: 14, weight: .medium)) - .foregroundColor(.gray) - - } - .buttonStyle(.plain) -} else { - NavigationLink(destination: ReleaseDetailView(history: history)) { - Image(systemName: "chevron.right") - .font(.system(size: 14, weight: .medium)) - .foregroundColor(.gray) - } - .buttonStyle(.plain) -} +NavigationLink(destination: destinationView(for: history)) { + Image(systemName: "chevron.right") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.gray) +} +.buttonStyle(.plain) + +// Helper 메서드 추가 +@ViewBuilder +private func destinationView(for history: HistoryItem) -> some View { + if history.status == "RECEIVED", let orderId = history.orderId { + OrderDetailView(orderId: orderId, orderViewModel: OrderViewModel()) + } else { + ReleaseDetailView(history: history) + } +}StockMate/StockMate/app/feature/parts/data/PartApi.swift (2)
53-53: 주석 처리된 중복 코드를 제거하세요.Line 53의 주석은 Line 54와 동일하므로 불필요합니다. 코드베이스를 깔끔하게 유지하기 위해 제거하세요.
let body: [String: Any] = [ -// "items": items.map { ["partId": $0.partId, "quantity": $0.quantity] } "items": items.map { ["partId": $0.partId, "quantity": $0.quantity] } - ]
65-76: 빈 배열 입력에 대한 검증을 추가하는 것을 고려하세요.
fetchPartDetail메서드가 빈partIds배열로 호출될 경우, 불필요한 네트워크 요청이 발생합니다. 호출 전에 배열이 비어있지 않은지 확인하는 것이 좋습니다.StockMate/StockMate/app/feature/auth/ui/HomeView.swift (1)
18-18: 불필요한 nil 초기화를 제거하세요.Optional 변수는 기본적으로
nil로 초기화되므로, 명시적인= nil할당은 중복입니다.Based on learnings from static analysis tools.
-@State private var selectedMonth: String? = nil // ✅ 추가 +@State private var selectedMonth: String?
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (14)
StockMate/.DS_Storeis excluded by!**/.DS_StoreStockMate/StockMate/resources/Assets.xcassets/SuccessIllust.imageset/SuccessIllust.svgis excluded by!**/*.svgStockMate/StockMate/resources/Assets.xcassets/add_shopping_cart.imageset/add_shopping_cart@1x.pngis excluded by!**/*.pngStockMate/StockMate/resources/Assets.xcassets/add_shopping_cart.imageset/add_shopping_cart@2x.pngis excluded by!**/*.pngStockMate/StockMate/resources/Assets.xcassets/add_shopping_cart.imageset/add_shopping_cart@3x.pngis excluded by!**/*.pngStockMate/StockMate/resources/Assets.xcassets/bag.imageset/bag.svgis excluded by!**/*.svgStockMate/StockMate/resources/Assets.xcassets/exchange.imageset/exchange.svgis excluded by!**/*.svgStockMate/StockMate/resources/Assets.xcassets/flag.imageset/flag.svgis excluded by!**/*.svgStockMate/StockMate/resources/Assets.xcassets/lock.imageset/lock.svgis excluded by!**/*.svgStockMate/StockMate/resources/Assets.xcassets/logout.imageset/logout.svgis excluded by!**/*.svgStockMate/StockMate/resources/Assets.xcassets/notification 1.imageset/notification.svgis excluded by!**/*.svgStockMate/StockMate/resources/Assets.xcassets/receipt.imageset/receipt.svgis excluded by!**/*.svgStockMate/StockMate/resources/Assets.xcassets/shoppingcart.imageset/shoppingcart.svgis excluded by!**/*.svgStockMate/StockMate/resources/Assets.xcassets/user.imageset/user.svgis excluded by!**/*.svg
📒 Files selected for processing (59)
StockMate/StockMate/ContentView.swift(1 hunks)StockMate/StockMate/app/core/KakaoPostcode/KakaoZipCodeVC.swift(1 hunks)StockMate/StockMate/app/core/KakaoPostcode/KakaoZipCodeView.swift(1 hunks)StockMate/StockMate/app/core/KakaoPostcode/ViewController.swift(1 hunks)StockMate/StockMate/app/core/components/AlertModal.swift(1 hunks)StockMate/StockMate/app/core/components/BarChartView.swift(1 hunks)StockMate/StockMate/app/core/components/CartCard.swift(1 hunks)StockMate/StockMate/app/core/components/CustomButtonStyle.swift(1 hunks)StockMate/StockMate/app/core/components/CustomTextField.swift(2 hunks)StockMate/StockMate/app/core/components/DonutChartView.swift(1 hunks)StockMate/StockMate/app/core/components/OrderRequestCardView.swift(2 hunks)StockMate/StockMate/app/feature/auth/ui/HomeView.swift(7 hunks)StockMate/StockMate/app/feature/auth/ui/LoginView.swift(1 hunks)StockMate/StockMate/app/feature/auth/ui/RegisterView.swift(4 hunks)StockMate/StockMate/app/feature/cart/ui/DeliveryStatusView.swift(1 hunks)StockMate/StockMate/app/feature/dashboard/data/HistoryApi.swift(1 hunks)StockMate/StockMate/app/feature/dashboard/data/HistoryRepositoryImpl.swift(1 hunks)StockMate/StockMate/app/feature/dashboard/domain/HistoryRepositoryProtocol.swift(1 hunks)StockMate/StockMate/app/feature/dashboard/ui/InOutHistoryView.swift(1 hunks)StockMate/StockMate/app/feature/dashboard/ui/ReleaseDetailView.swift(1 hunks)StockMate/StockMate/app/feature/dashboard/ui/TransactionTypeListView.swift(1 hunks)StockMate/StockMate/app/feature/dashboard/viewmodel/HistoryViewModel.swift(1 hunks)StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift(1 hunks)StockMate/StockMate/app/feature/inventory/ui/OutgoingScanView.swift(2 hunks)StockMate/StockMate/app/feature/inventory/ui/UsedPartListSheetView.swift(1 hunks)StockMate/StockMate/app/feature/orders/data/OrderApi.swift(2 hunks)StockMate/StockMate/app/feature/orders/ui/OrderDetailView.swift(4 hunks)StockMate/StockMate/app/feature/orders/ui/OrderInfoView.swift(2 hunks)StockMate/StockMate/app/feature/orders/ui/OrderListView.swift(0 hunks)StockMate/StockMate/app/feature/orders/ui/OrderView.swift(2 hunks)StockMate/StockMate/app/feature/orders/ui/ReceiptView.swift(2 hunks)StockMate/StockMate/app/feature/parts/data/PartApi.swift(2 hunks)StockMate/StockMate/app/feature/parts/data/PartRepositoryImpl.swift(1 hunks)StockMate/StockMate/app/feature/parts/data/PartStore.swift(1 hunks)StockMate/StockMate/app/feature/parts/domain/PartRepositoryProtocol.swift(1 hunks)StockMate/StockMate/app/feature/parts/ui/QRScannerView.swift(1 hunks)StockMate/StockMate/app/feature/parts/ui/QRScannerViewController.swift(1 hunks)StockMate/StockMate/app/feature/parts/viewmodel/PartViewModel.swift(2 hunks)StockMate/StockMate/app/feature/payment/data/PaymentApi.swift(2 hunks)StockMate/StockMate/app/feature/payment/data/PaymentRepositoryImpl.swift(1 hunks)StockMate/StockMate/app/feature/payment/domain/PaymentRepositoryProtocol.swift(1 hunks)StockMate/StockMate/app/feature/payment/viewmodel/DashboardViewModel.swift(1 hunks)StockMate/StockMate/app/feature/user/ui/ProfileCircleView.swift(1 hunks)StockMate/StockMate/app/feature/user/ui/ProfileView.swift(5 hunks)StockMate/StockMate/app/feature/user/ui/UserProfileView.swift(1 hunks)StockMate/StockMate/app/navigation/AppNavHost.swift(2 hunks)StockMate/StockMate/app/navigation/MainTabView.swift(1 hunks)StockMate/StockMate/resources/Assets.xcassets/SuccessIllust.imageset/Contents.json(1 hunks)StockMate/StockMate/resources/Assets.xcassets/add_shopping_cart.imageset/Contents.json(1 hunks)StockMate/StockMate/resources/Assets.xcassets/bag.imageset/Contents.json(1 hunks)StockMate/StockMate/resources/Assets.xcassets/exchange.imageset/Contents.json(1 hunks)StockMate/StockMate/resources/Assets.xcassets/flag.imageset/Contents.json(1 hunks)StockMate/StockMate/resources/Assets.xcassets/lock.imageset/Contents.json(1 hunks)StockMate/StockMate/resources/Assets.xcassets/logout.imageset/Contents.json(1 hunks)StockMate/StockMate/resources/Assets.xcassets/notification 1.imageset/Contents.json(1 hunks)StockMate/StockMate/resources/Assets.xcassets/receipt.imageset/Contents.json(1 hunks)StockMate/StockMate/resources/Assets.xcassets/shoppingcart.imageset/Contents.json(1 hunks)StockMate/StockMate/resources/Assets.xcassets/user.imageset/Contents.json(1 hunks)StockMate/StockMate/resources/Color.swift(2 hunks)
💤 Files with no reviewable changes (1)
- StockMate/StockMate/app/feature/orders/ui/OrderListView.swift
🧰 Additional context used
🪛 SwiftLint (0.57.0)
StockMate/StockMate/app/feature/dashboard/ui/ReleaseDetailView.swift
[Warning] 123-123: Initializing an optional variable with nil is redundant
(redundant_optional_initialization)
StockMate/StockMate/app/feature/auth/ui/HomeView.swift
[Warning] 18-18: Initializing an optional variable with nil is redundant
(redundant_optional_initialization)
StockMate/StockMate/app/core/components/AlertModal.swift
[Warning] 80-80: Imports should be unique
(duplicate_imports)
[Warning] 12-12: Initializing an optional variable with nil is redundant
(redundant_optional_initialization)
[Warning] 14-14: Initializing an optional variable with nil is redundant
(redundant_optional_initialization)
[Warning] 19-19: Initializing an optional variable with nil is redundant
(redundant_optional_initialization)
[Warning] 20-20: Initializing an optional variable with nil is redundant
(redundant_optional_initialization)
StockMate/StockMate/app/feature/inventory/ui/OutgoingScanView.swift
[Warning] 25-25: Initializing an optional variable with nil is redundant
(redundant_optional_initialization)
🔇 Additional comments (51)
StockMate/StockMate/resources/Assets.xcassets/logout.imageset/Contents.json (1)
1-12: 자산 카탈로그 구조가 올바르게 구성되었습니다.Xcode 자산 카탈로그 형식이 올바르게 작성되었으며, JSON 구조가 유효하고 필수 필드들이 모두 포함되어 있습니다. SVG 이미지를 위한 "universal" idiom 설정도 적절합니다.
로컬 환경에서 다음을 확인하세요:
- logout.svg 파일이 동일 디렉토리에 실제로 존재하는지 확인
- 프로젝트 빌드 시 자산이 올바르게 로드되는지 확인
StockMate/StockMate/resources/Assets.xcassets/SuccessIllust.imageset/Contents.json (1)
1-12: 에셋 카탈로그 정의가 올바릅니다.Xcode 에셋 카탈로그 메타데이터가 올바른 형식으로 정의되어 있습니다. SVG 형식을 사용하여 확장성이 우수하고, "universal" idiom을 통해 모든 기기에서 동일하게 적용됩니다.
StockMate/StockMate/resources/Assets.xcassets/flag.imageset/Contents.json (1)
1-12: 검증 완료 - 자산 파일 및 사용처 모두 정상flag.svg 파일이 올바른 위치(
StockMate/StockMate/resources/Assets.xcassets/flag.imageset/flag.svg)에 존재하며, DeliveryStatusView.swift에서 배송 상태 표시(배송시작 단계)에 실제로 사용되고 있습니다. Contents.json 구조도 표준 iOS 자산 카탈로그 형식을 따릅니다.StockMate/StockMate/resources/Assets.xcassets/exchange.imageset/Contents.json (1)
1-12: 자산 파일과 사용 현황 확인 완료 - 문제 없음검증 결과:
- ✅
exchange.svg파일이exchange.imageset디렉터리에 정상 존재- ✅ 자산이
TransactionTypeListView.swift(59행)에서Image("exchange")로 올바르게 사용 중- ✅ Contents.json 구조가 Xcode 자산 카탈로그 표준에 부합
파일 구조와 참조가 모두 올바르게 구성되어 있습니다.
StockMate/StockMate/resources/Assets.xcassets/user.imageset/Contents.json (1)
1-12: 수정 없음 - 검증 완료파일 존재 여부와 사용 위치를 확인했습니다:
- ✅
user.svg파일 존재 확인- ✅
Contents.json형식 정확 (XCode 표준 준수)- ✅
ProfileView.swift라인 44에서icon: "user"로 실제 사용 중에셋 카탈로그 설정이 올바르게 구성되어 있습니다.
StockMate/StockMate/resources/Assets.xcassets/bag.imageset/Contents.json (1)
8-15: 현재 구성이 올바릅니다. 검증 결과에 따라 이 review comment는 부정확합니다.스크립트 실행 결과, bag.imageset 폴더에는 bag.svg 파일만 존재하며 bag@2x.svg나 bag@3x.svg는 없습니다. 이는 실제로 정상적인 구성입니다.
SVG는 벡터 형식이므로 단일 파일로 모든 해상도에 대응 가능하며, iOS asset catalog에서는 1x scale 파일만 명시하면 Xcode가 자동으로 2x, 3x scale에 적용합니다. 각 scale별 filename을 따로 지정할 필요가 없으며, 현재의 Contents.json 구성이 모범 사례입니다.
Likely an incorrect or invalid review comment.
StockMate/StockMate/resources/Assets.xcassets/lock.imageset/Contents.json (1)
1-12: 자산 추가가 올바르게 구현되었습니다.lock.svg 파일이 정상적으로 존재하며, Contents.json이 Xcode 자산 카탈로그의 표준 형식을 따르고 있습니다. 해당 자산은 ProfileView.swift(라인 46)에서 SettingRow 컴포넌트의 "lock" 아이콘으로 실제 사용되고 있습니다.
StockMate/StockMate/app/core/components/CustomTextField.swift (3)
16-16: 변경사항 확인 완료기본값이
false로 설정되어 기존 코드와의 호환성이 유지되며, 구현이 깔끔합니다.
59-59:.contentShape(Rectangle())의 목적 확인 필요제스처 핸들러가 없는 상태에서
.contentShape(Rectangle())가 추가되어 있습니다. 향후 탭 제스처를 추가할 계획이 있으신가요? 그렇지 않다면 이 modifier는 제거하는 것이 좋습니다.
61-68: 기존 TextField 로직 유지 확인기존 TextField의 동작이 올바르게 보존되었으며, 기본 동작에 영향을 주지 않습니다.
StockMate/StockMate/app/feature/orders/data/OrderApi.swift (1)
119-124: 구조체 정의가 적절합니다.주문 요약 정보를 담기 위한 간단한 데이터 구조로, 필드 타입과 구성이 적절합니다.
StockMate/StockMate/app/core/components/CustomButtonStyle.swift (1)
11-21: 잘 구조화된 버튼 스타일 정의입니다!enum과 프로퍼티 구조가 명확하고, 기본값 설정도 적절합니다.
cornerRadius = 9999는 완전히 둥근 캡슐 형태 버튼을 만드는 일반적인 패턴입니다.StockMate/StockMate/resources/Assets.xcassets/shoppingcart.imageset/Contents.json (1)
1-12: 애셋 카탈로그 구조가 올바릅니다.표준 Xcode 애셋 카탈로그 형식을 따르고 있으며, SVG 이미지를 universal idiom으로 정의한 구조가 적절합니다.
StockMate/StockMate/resources/Assets.xcassets/add_shopping_cart.imageset/Contents.json (1)
1-23: 멀티 스케일 애셋 구성이 올바릅니다.1x, 2x, 3x 해상도의 PNG 이미지를 모두 정의하여 다양한 iOS 디바이스를 적절히 지원합니다.
StockMate/StockMate/app/core/components/OrderRequestCardView.swift (1)
139-190: Preview 구현이 훌륭합니다.여러 수량 상태(0, 1, 3)를 보여주는 Preview를 추가하여 SwiftUI Canvas에서 다양한 시나리오를 쉽게 확인할 수 있도록 했습니다. 이는 개발 및 디버깅 생산성을 높이는 좋은 사례입니다.
StockMate/StockMate/resources/Assets.xcassets/receipt.imageset/Contents.json (1)
1-12: LGTM!에셋 카탈로그 항목이 올바르게 추가되었습니다. SVG 이미지 형식을 사용하여 해상도 독립적인 아이콘을 제공하는 것은 좋은 선택입니다.
StockMate/StockMate/app/feature/user/ui/UserProfileView.swift (1)
10-21: LGTM!
@StateObject를 사용하여UserViewModel을 올바르게 관리하고 있으며,ProfileCircleView에 대한 nil 처리도 적절합니다.StockMate/StockMate/ContentView.swift (1)
11-29: LGTM!주소 검색 기능이 깔끔하게 구현되었습니다.
sheet프레젠테이션과 바인딩을 적절히 사용하고 있으며,KakaoZipCodeView와의 통합도 올바릅니다.StockMate/StockMate/app/core/KakaoPostcode/KakaoZipCodeView.swift (1)
12-24: LGTM!
UIViewControllerRepresentable프로토콜을 올바르게 구현했습니다.onAddressSelected콜백을 통한 양방향 바인딩이 적절하게 설정되어 있으며, SwiftUI와 UIKit 간의 브릿지가 깔끔합니다.StockMate/StockMate/app/feature/auth/ui/RegisterView.swift (3)
89-120: LGTM!주소 검색 기능이 훌륭하게 통합되었습니다. 읽기 전용 필드와 검색 버튼의 조합은 명확한 UX를 제공하며,
KakaoZipCodeView와의 바인딩도 올바릅니다.
127-130: LGTM!사업자등록번호 입력 시 자동 포맷팅을 위한
onChange핸들러가 적절히 설정되었습니다.
252-252: LGTM!서버로 전송하기 전에 숫자만 추출하는 것은 올바른 접근입니다. 서버에서 하이픈을 처리할 필요가 없어집니다.
StockMate/StockMate/app/core/KakaoPostcode/KakaoZipCodeVC.swift (3)
12-24: LGTM!뷰 컨트롤러의 구조가 깔끔하고 콜백 기반 아키텍처가 적절합니다.
42-59: LGTM!레이아웃 설정이 올바르게 구현되었습니다. Auto Layout 제약 조건이 적절합니다.
62-69: LGTM!JavaScript 메시지 처리가 올바르게 구현되었습니다. 주소 선택 후 자동으로 dismiss하는 것은 좋은 UX입니다.
StockMate/StockMate/app/feature/payment/domain/PaymentRepositoryProtocol.swift (1)
14-20: 프로토콜 확장이 잘 구현되었습니다.두 개의 새로운 메서드가 프로토콜에 추가되었으며, async/await 패턴과 AppResult 래퍼를 일관되게 사용하고 있습니다. 메서드 시그니처가 명확하고 기존 패턴을 잘 따르고 있습니다.
StockMate/StockMate/app/navigation/AppNavHost.swift (2)
12-12: PartStore 상태 관리 추가가 적절합니다.@StateObject로 PartStore를 생성하여 소유권을 명확히 하고 있으며, 이는 SwiftUI의 권장 패턴입니다.
27-28: 환경 객체 주입이 올바르게 구현되었습니다.MainTabView에 authViewModel과 partStore를 모두 environmentObject로 전달하여 탭 뷰 전체에서 공유 상태에 접근할 수 있도록 했습니다.
StockMate/StockMate/app/feature/inventory/ui/InventoryView.swift (1)
127-127: 입출고 히스토리 내비게이션이 올바르게 구현되었습니다."입출고 히스토리" 메뉴 항목이 InOutHistoryView로 이동하도록 변경되었으며, 이는 PR의 목표와 일치합니다.
StockMate/StockMate/app/feature/parts/ui/QRScannerView.swift (1)
12-12: 카메라 활성화 상태 제어 기능이 잘 추가되었습니다.isActive 파라미터를 통해 외부에서 스캐너의 활성화 상태를 제어할 수 있으며, 기본값 true로 하위 호환성을 유지하고 있습니다.
StockMate/StockMate/app/feature/parts/data/PartRepositoryImpl.swift (1)
17-20: 부품 상세 정보 조회 메서드가 올바르게 구현되었습니다.fetchPartDetail 메서드가 기존 패턴을 잘 따르고 있으며, safeApi를 사용하여 에러 처리를 일관되게 수행하고 있습니다.
StockMate/StockMate/app/feature/orders/ui/OrderView.swift (2)
34-37: NavigationLink 포맷팅이 개선되었습니다.OrderRequestSearchView 초기화가 여러 줄로 변경되어 가독성이 향상되었습니다.
52-52: 디자인 시스템 컬러 사용으로 개선되었습니다.검색창의 테두리 색상을 Color.GrayMordern400로 변경하여 디자인 시스템의 명명된 색상을 사용하도록 개선했습니다. 이는 유지보수성과 일관성을 향상시킵니다.
StockMate/StockMate/app/feature/user/ui/ProfileCircleView.swift (1)
21-30: 동적 크기 조절 로직이 잘 구현되었습니다.GeometryReader를 사용하여 프로필 원 내부의 텍스트 크기를 동적으로 조절하도록 개선했습니다. 최소 변의 35%를 폰트 크기로 사용하는 것은 시각적으로 균형잡힌 비율입니다. 다양한 크기에서 더 나은 반응형 디자인을 제공합니다.
StockMate/StockMate/app/feature/parts/domain/PartRepositoryProtocol.swift (1)
12-12: LGTM!새로운 프로토콜 메서드가 적절하게 정의되었습니다. 비동기 패턴과 반환 타입이 기존 코드베이스의 규칙을 잘 따르고 있습니다.
StockMate/StockMate/app/feature/dashboard/domain/HistoryRepositoryProtocol.swift (1)
11-16: LGTM!프로토콜 메서드들이 일관된 비동기 패턴을 따르고 있으며, 페이지네이션 지원을 위한
page와size매개변수가 적절히 정의되었습니다.StockMate/StockMate/app/feature/dashboard/data/HistoryRepositoryImpl.swift (1)
11-22: LGTM!리포지토리 구현이 일관된 패턴을 따르고 있으며,
safeApi헬퍼를 통한 에러 처리가 적절합니다. 두 메서드 모두 프로토콜을 올바르게 구현하고 있습니다.StockMate/StockMate/app/feature/payment/viewmodel/DashboardViewModel.swift (1)
48-51: LGTM!
spendingRatios계산 로직이 올바르며, 0으로 나누는 경우를 적절히 처리하고 있습니다.StockMate/StockMate/resources/Color.swift (1)
36-37: LGTM!새로운 색상 상수 추가와 기존 상수 업데이트가 적절합니다. 명명 규칙도 일관성 있게 유지되고 있습니다.
Also applies to: 88-89
StockMate/StockMate/app/feature/payment/data/PaymentApi.swift (2)
20-24: ID 고유성 확인 필요
month와categoryName을Identifiable의id로 사용하고 있습니다. 이 값들이 항상 고유함을 보장할 수 있는지 확인하세요. 중복 값이 있을 경우 SwiftUI 리스트 렌더링에 문제가 발생할 수 있습니다.서버 API 응답에서 이 필드들의 고유성이 보장되는지 확인하세요. 보장되지 않는다면, 서버에서 제공하는 고유 ID를 사용하거나 UUID를 생성하는 것을 고려하세요.
Also applies to: 26-30
54-65: LGTM!새로운 API 엔드포인트들이 기존 패턴과 일관성 있게 구현되었습니다. URL 구성과 HTTP 메서드 사용이 적절합니다.
StockMate/StockMate/app/feature/user/ui/ProfileView.swift (2)
12-13: LGTM!로그아웃 모달 구현이 적절합니다.
@EnvironmentObject를 통한 전역 인증 상태 접근과 ZStack 기반 모달 오버레이 패턴이 SwiftUI 모범 사례를 잘 따르고 있습니다.Also applies to: 74-95
111-111: 프로젝트에 모든 아이콘 에셋이 존재합니다.검증 결과, 다음 아이콘 에셋들이 모두 프로젝트에 존재합니다:
- ✓ user.imageset
- ✓ lock.imageset
- ✓ notification.imageset
- ✓ receipt.imageset
- ✓ logout.imageset
- ✓ location.imageset
- ✓ bag.imageset
SF Symbols에서 커스텀 에셋 기반 아이콘으로의 변경이 안전하게 진행되었습니다.
StockMate/StockMate/app/feature/orders/ui/OrderInfoView.swift (1)
116-152: 모달 구현이 잘 되어 있습니다.주문 성공 후 모달을 통한 사용자 경험이 명확하게 구현되어 있고, 주문 상세 또는 홈으로의 네비게이션 옵션을 제공하는 것이 좋습니다.
StockMate/StockMate/app/feature/parts/data/PartStore.swift (1)
14-22: 부품 추가 로직이 올바르게 구현되었습니다.중복된 부품을 감지하고 수량만 증가시키는 로직이 잘 구현되어 있습니다.
StockMate/StockMate/app/feature/dashboard/ui/InOutHistoryView.swift (2)
111-114: OrderViewModel 인스턴스 생성 방식을 검증하세요.Line 113에서 매번 새로운
OrderViewModel()인스턴스를 생성하는 것이 의도된 동작인지 확인이 필요합니다. 이미 로드된 주문 데이터가 있다면 기존 ViewModel을 재사용하는 것이 더 효율적일 수 있습니다.
142-164: 상태 매핑 헬퍼 함수들이 잘 구조화되어 있습니다.입고/출고 상태에 따른 텍스트, 색상, 배경색 매핑이 명확하게 구현되어 있습니다.
StockMate/StockMate/app/feature/auth/ui/HomeView.swift (3)
74-102: 데이터 기반 차트 렌더링이 잘 구현되었습니다.로딩, 에러, 빈 상태를 모두 처리하는 것이 훌륭하며, 사용자 경험을 향상시킵니다.
112-134: 월간 지출 차트 구현이 견고합니다.고정된 높이 컨테이너를 사용하여 레이아웃 시프트를 방지하고, 모든 상태(로딩/비어있음/데이터 있음)를 적절히 처리합니다.
169-193: 재고 부족 섹션의 플레이스홀더 처리가 우수합니다.데이터가 없을 때도 UI 공간을 유지하고
.redacted모디파이어를 사용한 로딩 효과가 좋습니다.StockMate/StockMate/app/feature/parts/data/PartApi.swift (1)
12-16: API 필드 변경(partCode → partId)이 완전히 적용되었습니다.모든 호출처를 검증한 결과, ReleaseItemRequest의 필드 변경이 일관되게 적용되었음을 확인했습니다. OutgoingScanView를 포함한 모든 사용 지점에서 새로운
partId필드를 올바르게 사용하고 있으며, 기존partCode필드에 대한 참조는 존재하지 않습니다.
| let reversedValues = Array(values.reversed()) | ||
| let reversedLabels = Array(labels.reversed()) | ||
| let reversedAmounts = Array(amounts.reversed()) | ||
|
|
||
| // ✅ "07" → "7월" 형식 변환 | ||
| let displayLabels = reversedLabels.map { label in | ||
| if let monthInt = Int(label) { | ||
| return "\(monthInt)월" | ||
| } else { | ||
| return label | ||
| } | ||
| } | ||
|
|
||
| // ✅ 기본 선택: 최신월 | ||
| let defaultMonth = displayLabels.last ?? "" | ||
| let activeMonth = selectedMonth ?? defaultMonth | ||
|
|
||
| VStack(alignment: .leading, spacing: 14) { | ||
| // ✅ 막대 그래프 | ||
| GeometryReader { geometry in | ||
| let chartHeight = geometry.size.height * 0.85 // 상하 여백 고려 | ||
| let totalWidth = geometry.size.width | ||
| let barCount = CGFloat(reversedValues.count) | ||
| let barWidth: CGFloat = 28 | ||
| let spacing = max((totalWidth - (barWidth * barCount)) / (barCount + 1), 6) | ||
|
|
||
| HStack(alignment: .bottom, spacing: spacing) { | ||
| ForEach(reversedValues.indices, id: \.self) { i in | ||
| VStack { | ||
| RoundedRectangle(cornerRadius: 8) | ||
| .fill(activeMonth == displayLabels[i] ? Color.Primary : Color.LightBlue04) | ||
| // ✅ 막대 높이를 geometry 기준으로 조정 | ||
| .frame(width: barWidth, height: chartHeight * reversedValues[i]) | ||
| .onTapGesture { | ||
| selectedMonth = (selectedMonth == displayLabels[i]) ? nil : displayLabels[i] | ||
| } | ||
|
|
||
| Text(displayLabels[i]) | ||
| .font(.caption2) | ||
| .foregroundColor(.black) | ||
| .padding(.top, 4) | ||
| } | ||
| } | ||
| } | ||
| .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) | ||
| } | ||
| .frame(height: 140) // ← 전체 그래프 영역 높이 확장 | ||
| .padding(.vertical, 8) | ||
|
|
||
| Divider() | ||
|
|
||
| // ✅ 하단 "n월 지출금액 ooo원" 표시 | ||
| if let index = displayLabels.firstIndex(of: activeMonth) { | ||
| HStack { | ||
| Text("\(displayLabels[index]) 지출 현황") | ||
| .font(.system(size: 17, weight: .medium)) | ||
| Spacer() | ||
| Text("\(reversedAmounts[index].formatted())원") | ||
| .font(.system(size: 18, weight: .bold)) | ||
| .foregroundColor(Color.Primary) |
There was a problem hiding this comment.
월 순서가 뒤집혀 기본 선택이 어긋납니다.
주석의 예시처럼 이미 오름차순(["06","07","08","09","10"])으로 들어오는 데이터를 다시 reversed() 하면 최신월이 왼쪽으로 가고 기본 선택도 가장 오래된 달로 고정됩니다. 실제 화면에서 월 표시와 하단 금액이 뒤섞여 해석이 어렵습니다. 역순 정렬을 제거하고 원본 순서를 그대로 사용해 주세요.
- // ✅ 최신월이 오른쪽에 오도록 역순 정렬
- let reversedValues = Array(values.reversed())
- let reversedLabels = Array(labels.reversed())
- let reversedAmounts = Array(amounts.reversed())
-
- // ✅ "07" → "7월" 형식 변환
- let displayLabels = reversedLabels.map { label in
+ // ✅ "07" → "7월" 형식 변환
+ let displayLabels = labels.map { label in
if let monthInt = Int(label) {
return "\(monthInt)월"
} else {
return label
}
}
@@
- let totalWidth = geometry.size.width
- let barCount = CGFloat(reversedValues.count)
+ let totalWidth = geometry.size.width
+ let barCount = CGFloat(values.count)
@@
- ForEach(reversedValues.indices, id: \.self) { i in
+ ForEach(values.indices, id: \.self) { i in
@@
- .fill(activeMonth == displayLabels[i] ? Color.Primary : Color.LightBlue04)
- // ✅ 막대 높이를 geometry 기준으로 조정
- .frame(width: barWidth, height: chartHeight * reversedValues[i])
+ .fill(activeMonth == displayLabels[i] ? Color.Primary : Color.LightBlue04)
+ // ✅ 막대 높이를 geometry 기준으로 조정
+ .frame(width: barWidth, height: chartHeight * values[i])
@@
- Text("\(reversedAmounts[index].formatted())원")
+ Text("\(amounts[index].formatted())원")🤖 Prompt for AI Agents
In StockMate/StockMate/app/core/components/BarChartView.swift around lines 18 to
77, the code reverses values, labels and amounts which flips chronological order
and makes the default selected month point to the oldest month; remove the
Array(...reversed()) calls and use the incoming values/labels/amounts in their
original order, update displayLabels mapping to operate on labels (no reversal),
compute defaultMonth/activeMonth from the last element (latest month) of the
original labels-derived displayLabels, and ensure the ForEach and the index
lookups use the same non-reversed arrays so the bar colors, taps and the bottom
amount text align with the visible month order.
| } else if quantity == 1 { | ||
| HStack(spacing: 10) { | ||
| Button(action: onRemoveFromCart) { | ||
| Image(systemName: "trash") | ||
| .font(.system(size: 14, weight: .bold)) | ||
| .foregroundColor(.red) | ||
| .font(.system(size: 14, weight: .regular)) | ||
| .frame(width: 13,height: 13) | ||
| .foregroundColor(.black) | ||
| } | ||
|
|
||
| Text("1") | ||
| .font(.system(size: 15, weight: .semibold)) | ||
| .frame(width: 24) | ||
| .font(.system(size: 15, weight: .medium)) | ||
| .frame(width: 20) | ||
|
|
||
| Button(action: onIncrease) { | ||
| Image(systemName: "plus") | ||
| .font(.system(size: 14, weight: .bold)) | ||
| .foregroundColor(.Primary) | ||
| .font(.system(size: 14, weight: .regular)) | ||
| .frame(width: 13,height: 13) | ||
| .foregroundColor(.black) | ||
| } | ||
| } | ||
| .padding(.vertical, 6) | ||
| .padding(.horizontal, 10) | ||
| .background(Color.white) | ||
| .cornerRadius(10) | ||
| .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) | ||
| .overlay( // ✅ 테두리 추가 | ||
| RoundedRectangle(cornerRadius: 10) | ||
| .stroke( Color.LightBlue03, lineWidth: 2) | ||
| ) | ||
| .clipShape(RoundedRectangle(cornerRadius: 10)) | ||
| .shadow(color: Color.black.opacity(0.25), radius: 4, x: 0, y: 4) | ||
|
|
||
|
|
||
| } else { | ||
| HStack(spacing: 10) { | ||
| Button(action: onDecrease) { | ||
| Image(systemName: "minus") | ||
| .font(.system(size: 14, weight: .bold)) | ||
| .foregroundColor(.gray) | ||
| .font(.system(size: 14, weight: .regular)) | ||
| .frame(width: 13,height: 13) | ||
| .foregroundColor(.black) | ||
| } | ||
|
|
||
| Text("\(quantity)") | ||
| .font(.system(size: 15, weight: .semibold)) | ||
| .frame(width: 24) | ||
| .font(.system(size: 15, weight: .medium)) | ||
| .frame(width: 20) | ||
|
|
||
| Button(action: onIncrease) { | ||
| Image(systemName: "plus") | ||
| .font(.system(size: 14, weight: .bold)) | ||
| .foregroundColor(.Primary) | ||
| .font(.system(size: 14, weight: .regular)) | ||
| .frame(width: 13,height: 13) | ||
| .foregroundColor(.black) | ||
| } | ||
| } | ||
| .padding(.vertical, 6) | ||
| .padding(.horizontal, 10) | ||
| .background(Color.white) | ||
| .cornerRadius(10) | ||
| .shadow(color: .black.opacity(0.05), radius: 4, x: 0, y: 4) | ||
| .overlay( // ✅ 테두리 추가 | ||
| RoundedRectangle(cornerRadius: 10) | ||
| .stroke( Color.LightBlue03, lineWidth: 2) | ||
| ) | ||
| .clipShape(RoundedRectangle(cornerRadius: 10)) | ||
| .shadow(color: Color.black.opacity(0.25), radius: 4, x: 0, y: 4) | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
CartCard.swift와 중복된 수량 컨트롤 로직을 공통 컴포넌트로 추출하세요.
이 파일의 수량 컨트롤 UI 로직이 CartCard.swift의 코드와 거의 동일하게 중복되어 있습니다. 두 파일 모두 동일한 스타일링, 레이아웃, 인터랙션 패턴을 구현하고 있습니다.
이러한 중복은 다음과 같은 문제를 야기합니다:
- 스타일 변경 시 여러 곳을 수정해야 함
- 버그 수정이 한 곳에만 적용될 위험
- 코드베이스 유지보수 비용 증가
공통 QuantityControl 컴포넌트를 별도 파일로 추출하여 재사용하는 것을 강력히 권장합니다.
예시:
// QuantityControl.swift (새 파일)
struct QuantityControl: View {
let quantity: Int
let onIncrease: () -> Void
let onDecrease: () -> Void
let onRemove: () -> Void
var body: some View {
HStack(spacing: 10) {
Button(action: quantity == 1 ? onRemove : onDecrease) {
Image(systemName: quantity == 1 ? "trash" : "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)
}
}🤖 Prompt for AI Agents
In StockMate/StockMate/app/core/components/OrderRequestCardView.swift around
lines 66-128 the quantity control UI is duplicated (same logic/styling as in
CartCard.swift); extract a reusable QuantityControl SwiftUI View in a new file
(e.g., QuantityControl.swift) that accepts quantity:Int, onIncrease:()->Void,
onDecrease:()->Void and onRemove:()->Void, reproduces the exact
layout/styling/overlay/shadow, uses a trash button when quantity == 1 (calling
onRemove) and a minus button otherwise (calling onDecrease), and then replace
the duplicated HStack blocks in this file (and update CartCard.swift to use the
new component) so both screens reuse the single component.
| func startSession() { | ||
| if !captureSession.isRunning { | ||
| captureSession.startRunning() | ||
| } | ||
| } | ||
|
|
||
| func stopSession() { | ||
| if captureSession.isRunning { | ||
| captureSession.stopRunning() | ||
| } | ||
| } |
There was a problem hiding this comment.
captureSession nil 처리 누락으로 즉시 크래시가 발생합니다
카메라 권한 거부 등으로 setupCamera()가 실패하면 captureSession이 nil인 채로 남는데, startSession()/stopSession()에서 강제로 언래핑하면서 바로 크래시가 납니다. 또한 startRunning()/stopRunning()은 메인 스레드를 블로킹하므로 background 큐에서 실행해야 합니다. 아래처럼 nil 가드와 비동기 실행을 추가해 주세요.
- func startSession() {
- if !captureSession.isRunning {
- captureSession.startRunning()
- }
- }
-
- func stopSession() {
- if captureSession.isRunning {
- captureSession.stopRunning()
- }
- }
+ func startSession() {
+ guard let session = captureSession else { return }
+ if !session.isRunning {
+ DispatchQueue.global(qos: .userInitiated).async {
+ session.startRunning()
+ }
+ }
+ }
+
+ func stopSession() {
+ guard let session = captureSession else { return }
+ if session.isRunning {
+ DispatchQueue.global(qos: .userInitiated).async {
+ session.stopRunning()
+ }
+ }
+ }🤖 Prompt for AI Agents
In StockMate/StockMate/app/feature/parts/ui/QRScannerViewController.swift around
lines 79 to 89, startSession() and stopSession() force-unwrap and call
startRunning()/stopRunning() directly which crashes when captureSession is nil
(e.g. setupCamera() failed) and blocks the main thread; add a nil guard to
safely return if captureSession is nil and invoke
captureSession.startRunning()/stopRunning() asynchronously on a background queue
(e.g., DispatchQueue.global(qos: .userInitiated).async) to avoid blocking the
main thread.
|
|
||
| @Published var partDetails: [PartDetailResponse] = [] | ||
|
|
||
| @Published var quantities: [Int: Int] = [:] // partId별 수량 관리 |
There was a problem hiding this comment.
🧩 Analysis chain
사용되지 않는 상태 변수
quantities 딕셔너리가 선언되었지만 fetchPartDetail 메서드에서 업데이트되지 않습니다. 이 상태가 필요하다면 메서드 내에서 초기화하거나, 불필요하다면 제거하는 것을 고려하세요.
다음 스크립트로 quantities의 사용처를 확인하세요:
🏁 Script executed:
#!/bin/bash
# quantities 딕셔너리의 사용처 확인
rg -n -C3 'quantities\[' --type=swiftLength of output: 0
더 포괄적인 검증이 필요합니다. 다음 스크립트로 quantities 변수의 모든 사용처를 확인하겠습니다:
🏁 Script executed:
#!/bin/bash
# quantities의 모든 사용처 검색
echo "=== All 'quantities' occurrences ==="
rg -n 'quantities' --type=swift
echo -e "\n=== PartViewModel.swift file content ==="
fd 'PartViewModel.swift' --type=swift -x cat {}Length of output: 388
파일 내용을 직접 확인하겠습니다:
🏁 Script executed:
#!/bin/bash
cat StockMate/StockMate/app/feature/parts/viewmodel/PartViewModel.swiftLength of output: 1963
사용되지 않는 상태 변수 제거 필요
quantities 딕셔너리가 선언되었지만 코드 전체에서 사용되지 않습니다. fetchPartDetail 메서드에서도 업데이트되지 않으며, 다른 곳에서도 참조되지 않으므로 이 변수를 제거하거나 실제 구현을 완료하세요.
🤖 Prompt for AI Agents
In StockMate/StockMate/app/feature/parts/viewmodel/PartViewModel.swift around
line 20, the @Published var quantities: [Int: Int] = [:] is declared but never
used; either remove this unused state or implement its intended behavior. If you
choose removal: delete the property and any (future) references and run the
project/tests to ensure no compile errors. If you choose to implement: update
quantities where part details are fetched/updated (e.g., inside fetchPartDetail
or the part update handlers) to set quantities[part.id] = fetchedQuantity and
ensure observers are notified, then remove any stale/commented code and run the
build to verify correctness.
| func fetchPartDetail(partId: Int) async { | ||
| isLoading = true | ||
| defer { isLoading = false } | ||
|
|
||
| let result = await repo.fetchPartDetail(partIds: [partId]) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
매개변수 명명 불일치
메서드 시그니처는 partId: Int (단수)를 받지만, 프로토콜의 fetchPartDetail은 partIds: [Int] (복수 배열)을 기대합니다. 매개변수 이름을 partIds로 변경하여 일관성을 유지하거나, 단일 ID만 필요한 경우 별도의 오버로드를 고려하세요.
- func fetchPartDetail(partId: Int) async {
+ func fetchPartDetail(partIds: [Int]) async {
isLoading = true
defer { isLoading = false }
- let result = await repo.fetchPartDetail(partIds: [partId])
+ let result = await repo.fetchPartDetail(partIds: partIds)🤖 Prompt for AI Agents
In StockMate/StockMate/app/feature/parts/viewmodel/PartViewModel.swift around
lines 50–54, the method accepts a single parameter named partId but calls
repo.fetchPartDetail which expects partIds: [Int]; rename the method parameter
to partIds: [Int] and update the call sites to pass an array (or wrap the
existing single id into an array) so the signature matches the protocol, and
ensure any protocol conformance and external usages are updated accordingly.
@coderabbitai review |
|
@Yoo-Hyuna: 리뷰를 시작하겠습니다. 변경사항을 검토하여 피드백을 제공하겠습니다. ✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
StockMate/StockMate/app/feature/auth/ui/LoginView.swift (1)
103-112: 인증 우회 코드를 제거해야 합니다.이 버튼은 사용자가 로그인 자격 증명 없이
authState를 직접.authenticated로 설정하여 인증을 완전히 우회할 수 있게 합니다. 이는 심각한 보안 취약점입니다:
- 누구나 자격 증명 없이 인증된 상태로 접근 가능
- 모든 인증 및 권한 부여 메커니즘을 우회
- 프로덕션 환경에 배포되어서는 안 되는 코드
또한, 이 변경 사항은 PR의 목적(히스토리 기능 구현)과 무관합니다.
다음 diff를 적용하여 인증 우회 코드를 제거하세요:
- // 승인 아이디 받기 전 - // 홈화면으로 이동 - HStack { - Button(action: { - authViewModel.authState = .authenticated - }) { - Text("홈화면으로 이동") - .font(.system(size: 13, weight: .bold)) - .foregroundColor(Color.Secondary) - } - } - .padding(.top, 5) - Spacer()만약 디버깅 목적으로 이 기능이 필요하다면, 다음과 같이 디버그 빌드에서만 활성화되도록 조건부로 컴파일해야 합니다:
#if DEBUG // 승인 아이디 받기 전 // 홈화면으로 이동 HStack { Button(action: { authViewModel.authState = .authenticated }) { Text("홈화면으로 이동 (DEBUG)") .font(.system(size: 13, weight: .bold)) .foregroundColor(Color.red) } } .padding(.top, 5) #endif
♻️ Duplicate comments (3)
StockMate/StockMate/app/feature/orders/ui/OrderDetailView.swift (1)
368-369: REJECTED 상태 레이블을 “주문 반려”로 맞춰 주세요
주문 상태가REJECTED인데 사용자에게 “결제 실패”로 노출되면 UX가 혼선됩니다. 코드 주석도 “주문 반려”를 가리키니 텍스트를 일치시켜 주세요.- case "REJECTED": return "결제 실패" // 주문 반려 + case "REJECTED": return "주문 반려" // 주문 반려StockMate/StockMate/app/core/components/OrderRequestCardView.swift (1)
69-131: CartCard.swift와 중복된 수량 컨트롤 로직을 공통 컴포넌트로 추출하는 것을 권장합니다.이전 리뷰에서 지적된 것처럼, 이 파일의 수량 컨트롤 UI 로직이
CartCard.swift와 중복되어 있으며, 이 파일 내에서도 lines 69-99와 lines 101-131이 거의 동일합니다.재사용 가능한
QuantityControl컴포넌트를 별도로 추출하면 유지보수성이 향상됩니다.StockMate/StockMate/app/feature/parts/viewmodel/PartViewModel.swift (1)
20-20: 사용되지 않는 상태 프로퍼티 정리 필요
Line 20의quantities는 선언 이후 어디에서도 읽히거나 갱신되지 않아 유지보수 비용만 늘립니다. 이미PartStore가 수량을 책임지고 있으므로 이 상태를 제거하거나 실제 로직에 연결해 주세요. 이전 리뷰에서도 동일한 문제가 지적되었습니다.
🧹 Nitpick comments (5)
StockMate/StockMate/app/feature/user/ui/UserProfileView.swift (1)
138-140: 로딩 및 에러 상태 처리를 고려하세요.현재 구현은 데이터를 비동기로 로드하지만, 로딩 중이거나 에러가 발생했을 때 사용자에게 피드백을 제공하지 않습니다. 사용자 경험 개선을 위해 로딩 인디케이터나 에러 메시지를 추가하는 것을 고려해보세요.
예시:
var body: some View { Group { if userViewModel.isLoading { ProgressView() } else if let error = userViewModel.error { VStack { Text("오류가 발생했습니다") Text(error.localizedDescription) .font(.caption) } } else { // 기존 UI 코드 } } .navigationTitle("프로필 확인") .background(Color.Light) .navigationBarTitleDisplayMode(.inline) .onAppear { Task { await userViewModel.loadUserInfo() } } }참고: 이를 위해서는
UserViewModel에isLoading과error프로퍼티를 추가해야 합니다.StockMate/StockMate/app/feature/user/ui/ProfileView.swift (1)
12-13: EnvironmentObject 주입 경로 점검 부탁드립니다
ProfileView가 이제@EnvironmentObject var authViewModel을 요구하므로, 상위 컨테이너에서.environmentObject(authViewModel)를 빠뜨리면 런타임에서 “No ObservableObject of type AuthViewModel found”로 바로 크래시합니다. 실제 네비게이션/탭 루트에서 확실히 주입되고 있는지 한 번 더 확인해 주세요. 또한 동일한 이유로 현재#Preview도 즉시 실패하니, 프리뷰 전용 모형 인스턴스를.environmentObject(...)로 주입하도록 정리해 주시면 좋겠습니다.Also applies to: 163-165
StockMate/StockMate/app/feature/dashboard/viewmodel/HistoryViewModel.swift (1)
71-95: 예치금 거래내역 로드 로직이 올바르게 구현되었지만, 코드 중복을 리팩토링할 수 있습니다.이 메서드는
fetchInOutHistory와 거의 동일한 구조를 가지고 있습니다. 제네릭을 활용한 공통 메서드로 리팩토링하면 유지보수성이 향상될 수 있습니다.제네릭 메서드로 리팩토링하는 예시:
private func fetchPaginatedData<T>( page: Int, size: Int, isLoading: inout Bool, currentPage: inout Int, totalPages: inout Int, items: inout [T], fetchOperation: (Int, Int) async -> Result<PageResponse<T>, Error> ) async { guard !isLoading else { return } isLoading = true defer { isLoading = false } let result = await fetchOperation(page, size) switch result { case .success(let response): if let data = response.data { if page == 0 { items = data.content } else { items.append(contentsOf: data.content) } currentPage = data.currentPage totalPages = data.totalPages errorMessage = nil } else { errorMessage = "데이터가 없습니다." } case .failure(let error): errorMessage = error.localizedDescription print("❌ 데이터 조회 실패:", error) } }StockMate/StockMate/app/core/components/OrderRequestCardView.swift (2)
92-98: 중복된 clipShape 제거를 고려하세요.Line 92의
.cornerRadius(10)과 Line 97의.clipShape(RoundedRectangle(cornerRadius: 10))가 중복 적용되고 있습니다.clipShape가 이미 뷰를 클리핑하므로cornerRadius는 불필요합니다.다음과 같이 수정할 수 있습니다:
.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)
124-130: 중복된 clipShape 제거를 고려하세요.수량 1 상태와 동일하게, Line 124의
.cornerRadius(10)과 Line 129의.clipShape(RoundedRectangle(cornerRadius: 10))가 중복 적용되고 있습니다.다음과 같이 수정할 수 있습니다:
.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)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
StockMate/StockMate/app/core/components/BarChartView.swift(1 hunks)StockMate/StockMate/app/core/components/CartCard.swift(1 hunks)StockMate/StockMate/app/core/components/CustomButtonStyle.swift(1 hunks)StockMate/StockMate/app/core/components/InventoryListSection.swift(0 hunks)StockMate/StockMate/app/core/components/OrderRequestCardView.swift(4 hunks)StockMate/StockMate/app/feature/auth/ui/LoginView.swift(1 hunks)StockMate/StockMate/app/feature/dashboard/viewmodel/HistoryViewModel.swift(1 hunks)StockMate/StockMate/app/feature/inventory/ui/OutgoingScanView.swift(2 hunks)StockMate/StockMate/app/feature/inventory/ui/UsedPartListSheetView.swift(1 hunks)StockMate/StockMate/app/feature/orders/data/OrderApi.swift(0 hunks)StockMate/StockMate/app/feature/orders/ui/OrderDetailView.swift(4 hunks)StockMate/StockMate/app/feature/orders/ui/OrderInfoView.swift(2 hunks)StockMate/StockMate/app/feature/parts/data/PartStore.swift(1 hunks)StockMate/StockMate/app/feature/parts/ui/QRScannerViewController.swift(1 hunks)StockMate/StockMate/app/feature/parts/viewmodel/PartViewModel.swift(2 hunks)StockMate/StockMate/app/feature/user/ui/ProfileView.swift(5 hunks)StockMate/StockMate/app/feature/user/ui/UserProfileView.swift(1 hunks)
💤 Files with no reviewable changes (2)
- StockMate/StockMate/app/core/components/InventoryListSection.swift
- StockMate/StockMate/app/feature/orders/data/OrderApi.swift
🚧 Files skipped from review as they are similar to previous changes (4)
- StockMate/StockMate/app/core/components/CartCard.swift
- StockMate/StockMate/app/feature/parts/ui/QRScannerViewController.swift
- StockMate/StockMate/app/feature/inventory/ui/UsedPartListSheetView.swift
- StockMate/StockMate/app/core/components/BarChartView.swift
🧰 Additional context used
🪛 SwiftLint (0.57.0)
StockMate/StockMate/app/feature/dashboard/viewmodel/HistoryViewModel.swift
[Warning] 16-16: Initializing an optional variable with nil is redundant
(redundant_optional_initialization)
StockMate/StockMate/app/feature/inventory/ui/OutgoingScanView.swift
[Warning] 25-25: Initializing an optional variable with nil is redundant
(redundant_optional_initialization)
🔇 Additional comments (10)
StockMate/StockMate/app/feature/auth/ui/LoginView.swift (1)
121-126: 이전 리뷰에서 지적된 유효성 검사 로직이 올바르게 복원되었습니다!개발자께서 이전 리뷰의 권고사항을 반영하여 이메일 형식 검증과 비밀번호 길이 검증을 정상적으로 복원하셨습니다. 이제 유효성 검사가 제대로 작동하여 사용자가 올바른 입력 형식을 사용할 때만 로그인이 진행됩니다.
StockMate/StockMate/app/feature/user/ui/UserProfileView.swift (2)
10-12: 뷰 모델 초기화가 적절합니다.
@StateObject를 사용하여 뷰 모델을 초기화하는 것은 SwiftUI의 올바른 패턴입니다. 뷰가 뷰 모델의 생명주기를 관리하게 됩니다.
144-146: Preview 제공이 좋습니다.SwiftUI Preview를 제공하여 개발 중 실시간으로 UI를 확인할 수 있어 개발 경험이 향상됩니다.
StockMate/StockMate/app/feature/dashboard/viewmodel/HistoryViewModel.swift (4)
33-57: LGTM! 입출고 히스토리 로드 로직이 올바르게 구현되었습니다.동시 로드 방지(
isLoading가드), 페이지네이션 처리(첫 페이지 교체 vs 추가), 에러 핸들링이 모두 적절하게 구현되어 있습니다.
60-68: 이전 리뷰의 크래시 이슈가 올바르게 수정되었습니다!
max(histories.count - 5, 0)를 사용하여 배열의 요소가 5개 미만일 때 발생하던 범위 초과 크래시가 안전하게 방지되었습니다. 무한 스크롤 로직이 정확하게 구현되었습니다.
98-109: LGTM! 예치금 거래내역 무한 스크롤이 안전하게 구현되었습니다.동시 로드 방지, 마지막 페이지 확인, 안전한 threshold 계산 등 모든 가드 조건이 적절하게 구현되어 있습니다. 입출고 히스토리와 동일한 안전한 패턴을 사용하고 있습니다.
85-85: API 응답 모델의 프로퍼티 이름 불일치가 확인되었습니다.검증 결과, 두 개의 API 응답 모델이 실제로 다른 프로퍼티 이름을 사용하고 있습니다:
HistoryPageData:currentPage프로퍼티 사용PaymentTransactionPageData:page프로퍼티 사용현재 코드에서 각 구조체의 프로퍼티를 올바르게 사용하고 있으나, 백엔드 API 응답 모델이 같은 개념(페이지 번호)에 대해 서로 다른 이름을 사용하고 있습니다. 백엔드와 협의하여 일관된 프로퍼티 이름(예:
currentPage또는page로 통일)으로 개선하면 향후 유지보수성과 일관성이 향상될 것을 권장합니다.StockMate/StockMate/app/core/components/CustomButtonStyle.swift (1)
11-50: 이전 리뷰 이슈가 모두 해결되었습니다! 👍이전 리뷰에서 지적된 modifier 순서 문제가 올바르게 수정되었습니다:
- filled 케이스:
clipShape(RoundedRectangle)을 사용하여 배경이 올바르게 클리핑됩니다.- outlined 케이스:
background에RoundedRectangle.fill을 사용하여 둥근 배경이 정확히 적용됩니다.코드 구조가 깔끔하고 재사용성이 좋습니다.
StockMate/StockMate/app/core/components/OrderRequestCardView.swift (2)
142-193: 유용한 Preview 추가! 👍세 가지 수량 상태(0, 1, 3)를 모두 보여주는 Preview가 추가되어 UI 개발 및 테스트에 도움이 됩니다.
58-66: 에셋 검증 완료 - 우려사항 없음"add_shopping_cart" 에셋이 프로젝트에 올바르게 추가되어 있으며, iOS 다중 해상도(@1x, @2x, @3x)가 모두 구성되어 있습니다. 코드에서 Image("add_shopping_cart")로 참조하는 것이 유효하며, 런타임 오류는 발생하지 않을 것입니다.
| @Published var histories: [HistoryItem] = [] | ||
| @Published var isLoading = false | ||
| @Published var errorMessage: String? = nil // ✅ 추가 | ||
| @Published var currentPage = 0 | ||
| @Published var totalPages = 1 | ||
|
|
||
| // MARK: - 예치금 거래내역 관련 | ||
| @Published var transactions: [PaymentTransactionItem] = [] | ||
| @Published var transactionPage = 0 | ||
| @Published var transactionTotalPages = 1 | ||
| @Published var isTransactionLoading = false |
There was a problem hiding this comment.
공유된 errorMessage로 인한 UX 혼란 가능성이 있습니다.
입출고 히스토리와 예치금 거래내역이 동일한 errorMessage 프로퍼티를 공유하고 있어, 한쪽에서 발생한 에러가 다른 화면에 표시될 수 있습니다. 각 히스토리 타입별로 별도의 에러 프로퍼티를 사용하는 것을 고려해보세요.
다음 diff를 적용하여 에러 메시지를 분리할 수 있습니다:
// MARK: - 입출고 히스토리 관련
@Published var histories: [HistoryItem] = []
@Published var isLoading = false
- @Published var errorMessage: String?
+ @Published var historyErrorMessage: String?
@Published var currentPage = 0
@Published var totalPages = 1
// MARK: - 예치금 거래내역 관련
@Published var transactions: [PaymentTransactionItem] = []
@Published var transactionPage = 0
@Published var transactionTotalPages = 1
@Published var isTransactionLoading = false
+ @Published var transactionErrorMessage: String?📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Published var histories: [HistoryItem] = [] | |
| @Published var isLoading = false | |
| @Published var errorMessage: String? = nil // ✅ 추가 | |
| @Published var currentPage = 0 | |
| @Published var totalPages = 1 | |
| // MARK: - 예치금 거래내역 관련 | |
| @Published var transactions: [PaymentTransactionItem] = [] | |
| @Published var transactionPage = 0 | |
| @Published var transactionTotalPages = 1 | |
| @Published var isTransactionLoading = false | |
| @Published var histories: [HistoryItem] = [] | |
| @Published var isLoading = false | |
| @Published var historyErrorMessage: String? | |
| @Published var currentPage = 0 | |
| @Published var totalPages = 1 | |
| // MARK: - 예치금 거래내역 관련 | |
| @Published var transactions: [PaymentTransactionItem] = [] | |
| @Published var transactionPage = 0 | |
| @Published var transactionTotalPages = 1 | |
| @Published var isTransactionLoading = false | |
| @Published var transactionErrorMessage: String? |
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 16-16: Initializing an optional variable with nil is redundant
(redundant_optional_initialization)
🤖 Prompt for AI Agents
In StockMate/StockMate/app/feature/dashboard/viewmodel/HistoryViewModel.swift
around lines 14–24, the single shared @Published errorMessage is causing
cross-contamination between history and payment transaction UIs; create two
distinct error properties (e.g., @Published var historyErrorMessage: String? and
@Published var transactionErrorMessage: String?) and replace all assignments and
reads of the old errorMessage within this file so history-related logic sets
historyErrorMessage and transaction-related logic sets transactionErrorMessage;
also update any view bindings or callers that consume errorMessage to use the
new specific properties to ensure errors only appear on the relevant screens.
| // MARK: - 입출고 히스토리 관련 | ||
| @Published var histories: [HistoryItem] = [] | ||
| @Published var isLoading = false | ||
| @Published var errorMessage: String? = nil // ✅ 추가 |
There was a problem hiding this comment.
중복된 nil 초기화를 제거하세요.
Optional 타입 프로퍼티는 명시적으로 = nil을 지정하지 않아도 자동으로 nil로 초기화됩니다.
다음 diff를 적용하여 중복 초기화를 제거하세요:
- @Published var errorMessage: String? = nil // ✅ 추가
+ @Published var errorMessage: String?📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Published var errorMessage: String? = nil // ✅ 추가 | |
| @Published var errorMessage: String? |
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 16-16: Initializing an optional variable with nil is redundant
(redundant_optional_initialization)
🤖 Prompt for AI Agents
In StockMate/StockMate/app/feature/dashboard/viewmodel/HistoryViewModel.swift
around line 16, the optional Published property is redundantly initialized to
nil; remove the explicit "= nil" so the declaration becomes just the optional
property (e.g. @Published var errorMessage: String?) to rely on Swift's
automatic nil initialization.
| func addPart(_ part: PartDetail) { | ||
| if let index = parts.firstIndex(where: { $0.id == part.id }) { | ||
| parts[index].quantity += 1 // 이미 존재하면 수량만 +1 | ||
| } else { | ||
| var newPart = part | ||
| newPart.quantity = 1 | ||
| parts.append(newPart) | ||
| } |
There was a problem hiding this comment.
기존 부품 수량 증가 시 UI 갱신이 누락됩니다
Line 16에서 동일한 ID를 찾았을 때 parts[index].quantity += 1만 수행하면 @Published가 변경 신호를 내보내지 않아 뷰가 새 수량을 반영하지 않습니다. 수량만 늘리는 재스캔 시 리스트가 갱신되지 않는 문제가 발생합니다. 변경 전에 objectWillChange.send() 등을 호출해 주세요.
func addPart(_ part: PartDetail) {
if let index = parts.firstIndex(where: { $0.id == part.id }) {
+ objectWillChange.send()
parts[index].quantity += 1 // 이미 존재하면 수량만 +1
} else {
var newPart = part
newPart.quantity = 1
parts.append(newPart)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func addPart(_ part: PartDetail) { | |
| if let index = parts.firstIndex(where: { $0.id == part.id }) { | |
| parts[index].quantity += 1 // 이미 존재하면 수량만 +1 | |
| } else { | |
| var newPart = part | |
| newPart.quantity = 1 | |
| parts.append(newPart) | |
| } | |
| func addPart(_ part: PartDetail) { | |
| if let index = parts.firstIndex(where: { $0.id == part.id }) { | |
| objectWillChange.send() | |
| parts[index].quantity += 1 // 이미 존재하면 수량만 +1 | |
| } else { | |
| var newPart = part | |
| newPart.quantity = 1 | |
| parts.append(newPart) | |
| } |
🤖 Prompt for AI Agents
In StockMate/StockMate/app/feature/parts/data/PartStore.swift around lines 14 to
21, the increment branch only mutates a property on an element of the @Published
parts array which does not emit change notifications to views; before updating
parts[index].quantity, call objectWillChange.send() to notify subscribers (or
alternatively replace the element with a new value and assign it back to
parts[index]) so the UI refreshes after the quantity change.
| let result = await repo.fetchPartDetail(partIds: [partIds]) | ||
| switch result { | ||
| case .success(let apiResp): | ||
| if apiResp.success, let data = apiResp.data { | ||
| partDetails = data | ||
| print("✅ 부품 상세 조회 성공:", data) | ||
| } else { | ||
| message = apiResp.message | ||
| print("⚠️ 서버 응답 실패:", apiResp.message) | ||
| } | ||
| case .failure(let err): | ||
| message = err.message | ||
| print("❌ 네트워크 오류:", err) | ||
| } |
There was a problem hiding this comment.
실패 후 이전 부품 상세가 남아 잘못된 부품이 추가됩니다
Line 63 이후 실패 분기에서 partDetails를 비우지 않으면 이전 성공 결과가 그대로 남아 OutgoingScanView의 Line 163 guard를 통과시키고, 잘못된 부품이 다시 추가됩니다. 요청 시작 전에 상태를 초기화하고 실패/비성공 응답에서도 비워 주세요.
- let result = await repo.fetchPartDetail(partIds: [partIds])
+ partDetails = []
+ let result = await repo.fetchPartDetail(partIds: [partIds])
switch result {
case .success(let apiResp):
if apiResp.success, let data = apiResp.data {
partDetails = data
print("✅ 부품 상세 조회 성공:", data)
} else {
message = apiResp.message
print("⚠️ 서버 응답 실패:", apiResp.message)
+ partDetails = []
}
case .failure(let err):
message = err.message
print("❌ 네트워크 오류:", err)
+ partDetails = []
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let result = await repo.fetchPartDetail(partIds: [partIds]) | |
| switch result { | |
| case .success(let apiResp): | |
| if apiResp.success, let data = apiResp.data { | |
| partDetails = data | |
| print("✅ 부품 상세 조회 성공:", data) | |
| } else { | |
| message = apiResp.message | |
| print("⚠️ 서버 응답 실패:", apiResp.message) | |
| } | |
| case .failure(let err): | |
| message = err.message | |
| print("❌ 네트워크 오류:", err) | |
| } | |
| partDetails = [] | |
| let result = await repo.fetchPartDetail(partIds: [partIds]) | |
| switch result { | |
| case .success(let apiResp): | |
| if apiResp.success, let data = apiResp.data { | |
| partDetails = data | |
| print("✅ 부품 상세 조회 성공:", data) | |
| } else { | |
| message = apiResp.message | |
| print("⚠️ 서버 응답 실패:", apiResp.message) | |
| partDetails = [] | |
| } | |
| case .failure(let err): | |
| message = err.message | |
| print("❌ 네트워크 오류:", err) | |
| partDetails = [] | |
| } |
🤖 Prompt for AI Agents
In StockMate/StockMate/app/feature/parts/viewmodel/PartViewModel.swift around
lines 54 to 67, the previous successful partDetails state remains when a
subsequent fetch fails, letting OutgoingScanView's guard incorrectly pass;
before calling repo.fetchPartDetail clear/reset partDetails (e.g., set to nil or
an empty value) and ensure you also clear/reset partDetails in both the
non-success branch (apiResp.success == false) and the .failure branch so no
stale data persists after a failed request.
| VStack(spacing: 9) { | ||
| Text("대표자") | ||
| .font(.system(size: 14, weight: .medium)) | ||
| .frame(maxWidth: .infinity, alignment: .leading) | ||
| .padding(.horizontal, 10) | ||
| .foregroundColor(Color.black) | ||
|
|
||
| HStack { | ||
| Text(userViewModel.userInfo?.owner ?? "이름 없음") | ||
| .font(.system(size: 16, weight: .regular)) | ||
| .foregroundColor(.black) | ||
| Spacer() | ||
| } | ||
| .padding() | ||
| .cornerRadius(12) | ||
| .overlay( | ||
| RoundedRectangle(cornerRadius: 12) | ||
| .stroke(Color.GrayMordern300, lineWidth: 1) | ||
| ) | ||
| .padding(.horizontal, 10) | ||
| .padding(.bottom, 10) | ||
|
|
||
| Text("이메일") | ||
| .font(.system(size: 14, weight: .medium)) | ||
| .frame(maxWidth: .infinity, alignment: .leading) | ||
| .padding(.horizontal, 10) | ||
| .foregroundColor(Color.black) | ||
|
|
||
| HStack { | ||
| Text(userViewModel.userInfo?.email ?? "이메일 없음") | ||
| .font(.system(size: 16, weight: .regular)) | ||
| .foregroundColor(.black) | ||
| Spacer() | ||
| } | ||
| .padding() | ||
| .cornerRadius(12) | ||
| .overlay( | ||
| RoundedRectangle(cornerRadius: 12) | ||
| .stroke(Color.GrayMordern300, lineWidth: 1) | ||
| ) | ||
| .padding(.horizontal, 10) | ||
| .padding(.bottom, 10) | ||
|
|
||
| Text("지점") | ||
| .font(.system(size: 14, weight: .medium)) | ||
| .frame(maxWidth: .infinity, alignment: .leading) | ||
| .padding(.horizontal, 10) | ||
| .foregroundColor(Color.black) | ||
|
|
||
| HStack { | ||
| Text(userViewModel.userInfo?.storeName ?? "지점명 없음") | ||
| .font(.system(size: 16, weight: .regular)) | ||
| .foregroundColor(.black) | ||
| Spacer() | ||
| } | ||
| .padding() | ||
| .cornerRadius(12) | ||
| .overlay( | ||
| RoundedRectangle(cornerRadius: 12) | ||
| .stroke(Color.GrayMordern300, lineWidth: 1) | ||
| ) | ||
| .padding(.horizontal, 10) | ||
| .padding(.bottom, 10) | ||
|
|
||
| Text("주소") | ||
| .font(.system(size: 14, weight: .medium)) | ||
| .frame(maxWidth: .infinity, alignment: .leading) | ||
| .padding(.horizontal, 10) | ||
| .foregroundColor(Color.black) | ||
|
|
||
| HStack { | ||
| Text(userViewModel.userInfo?.address ?? "주소 없음") | ||
| .font(.system(size: 16, weight: .regular)) | ||
| .foregroundColor(.black) | ||
| Spacer() | ||
| } | ||
| .padding() | ||
| .cornerRadius(12) | ||
| .overlay( | ||
| RoundedRectangle(cornerRadius: 12) | ||
| .stroke(Color.GrayMordern300, lineWidth: 1) | ||
| ) | ||
| .padding(.horizontal, 10) | ||
| .padding(.bottom, 10) | ||
|
|
||
| Text("사업자등록번호") | ||
| .font(.system(size: 14, weight: .medium)) | ||
| .frame(maxWidth: .infinity, alignment: .leading) | ||
| .padding(.horizontal, 10) | ||
| .foregroundColor(Color.black) | ||
|
|
||
| HStack { | ||
| Text(userViewModel.userInfo?.businessNumber ?? "사업자등록번호 없음") | ||
| .font(.system(size: 16, weight: .regular)) | ||
| .foregroundColor(.black) | ||
| Spacer() | ||
| } | ||
| .padding() | ||
| .cornerRadius(12) | ||
| .overlay( | ||
| RoundedRectangle(cornerRadius: 12) | ||
| .stroke(Color.GrayMordern300, lineWidth: 1) | ||
| ) | ||
| .padding(.horizontal, 10) | ||
| .padding(.bottom, 10) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
반복되는 UI 패턴을 재사용 가능한 컴포넌트로 추출하세요.
5개의 필드(대표자, 이메일, 지점, 주소, 사업자등록번호)가 동일한 레이아웃 패턴을 반복하고 있습니다. 이는 유지보수성을 저하시키고 코드 중복을 야기합니다.
다음과 같이 재사용 가능한 컴포넌트로 추출하는 것을 권장합니다:
struct ProfileFieldView: View {
let label: String
let value: String
var body: some View {
VStack(spacing: 9) {
Text(label)
.font(.system(size: 14, weight: .medium))
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 10)
.foregroundColor(Color.black)
HStack {
Text(value)
.font(.system(size: 16, weight: .regular))
.foregroundColor(.black)
Spacer()
}
.padding()
.cornerRadius(12)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.GrayMordern300, lineWidth: 1)
)
.padding(.horizontal, 10)
.padding(.bottom, 10)
}
}
}그런 다음 기존 코드를 다음과 같이 간소화할 수 있습니다:
VStack(spacing: 9) {
ProfileFieldView(label: "대표자", value: userViewModel.userInfo?.owner ?? "이름 없음")
ProfileFieldView(label: "이메일", value: userViewModel.userInfo?.email ?? "이메일 없음")
ProfileFieldView(label: "지점", value: userViewModel.userInfo?.storeName ?? "지점명 없음")
ProfileFieldView(label: "주소", value: userViewModel.userInfo?.address ?? "주소 없음")
ProfileFieldView(label: "사업자등록번호", value: userViewModel.userInfo?.businessNumber ?? "사업자등록번호 없음")
Spacer()
}
.padding(3)
.cornerRadius(12)
.padding(.horizontal)🤖 Prompt for AI Agents
In StockMate/StockMate/app/feature/user/ui/UserProfileView.swift around lines 22
to 126, multiple repeated blocks render labeled read-only profile fields;
extract this repeated pattern into a reusable SwiftUI View (e.g.,
ProfileFieldView with label and value parameters) and replace each repeated
VStack/HStack block with calls to that component using the existing
userViewModel optional fallbacks, ensuring to preserve fonts, paddings,
cornerRadius, overlay stroke, and spacings; update the parent VStack to
instantiate ProfileFieldView for "대표자", "이메일", "지점", "주소", and "사업자등록번호" with
the same default strings and keep the surrounding paddings/spacers as before.
📣 Related Issue
📝 Summary
📬 Reference
https://kasroid.github.io/posts/ios/20200916-webkit-search-address-with-kakao-with-uikit/#wrap-up
Summary by CodeRabbit
새로운 기능
개선 사항
문제 해결