[FEAT] 출고목록, 장바구니, 출고처리 로직, 장바구니 담기 로직 구현#14
Conversation
Walkthrough앱 탭에서 Delivery를 Outbound로 대체하고 Cart 및 Outbound 기능을 추가했으며, 네트워크 레이어를 completion 콜백에서 async/await로 마이그레이션했습니다. Part 목록에 바텀시트 흐름을 연결하고 DI에 관련 뷰모델 팩토리를 추가했습니다. Changes
Sequence Diagram(s)%%{init: {"themeVariables": {"actorBorder":"#2b6cb0","actorBackground":"#e6f2ff"}}}%%
sequenceDiagram
participant User as 사용자
participant Content as ContentView
participant OutboundView as OutboundListView
participant OutboundVM as OutboundListViewModel
participant GetOutbound as GetOutboundUseCase
participant Repo as OutboundRepository
participant API as OutboundAPI
User->>Content: 앱 시작/탭 선택(Outbound)
Content->>OutboundView: Outbound 탭 표시
OutboundView->>OutboundVM: onAppear → loadOutboundList
OutboundVM->>GetOutbound: execute()
GetOutbound->>Repo: getOutboundList()
Repo->>API: getOutboundList()
API-->>Repo: [OutboundDto]
Repo-->>GetOutbound: OutboundList
GetOutbound-->>OutboundVM: OutboundList
OutboundVM->>OutboundView: uiState 업데이트 (화면 갱신)
%%{init: {"themeVariables": {"actorBorder":"#2f855a","actorBackground":"#e6ffef"}}}%%
sequenceDiagram
participant PartList as PartListView
participant PartVM as PartListViewModel
participant Sheet as PartDetailBottomSheetView
participant DetailVM as PartDetailViewModel
participant AddOutbound as AddOutboundUseCase
participant AddCart as AddCartUseCase
PartList->>PartVM: 아이템 클릭 -> onEvent(.showBottomSheet)
PartVM-->>PartList: selectedPart 설정 -> 시트 표시
PartList->>Sheet: sheet 생성(dependencies로 DetailVM 주입)
Sheet->>DetailVM: onEvent(.initialize)
User->>Sheet: 출고/카트 추가 버튼 탭
Sheet->>DetailVM: onEvent(.addToOutbound / .addToCart)
DetailVM->>AddOutbound: execute(partId, quantity)
DetailVM->>AddCart: execute(partId, quantity)
AddOutbound-->>DetailVM: 성공/실패
AddCart-->>DetailVM: 성공/실패
DetailVM->>Sheet: 성공/에러 플래그 업데이트 → 토스트/알림
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 평가 근거: 네트워크 레이어의 async/await 브리지, DI와 ContentView 시그니처 변경, Outbound/Cart/Part 전체 계층에 걸친 많은 신규 파일 및 ViewModel의 optimistic 업데이트/롤백 로직이 있어 집중 검토 필요. Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
🧹 Nitpick comments (40)
SampoomManagement/Features/Outbound/Domain/Models/OutboundList.swift (1)
10-19: 중복 프로퍼티를 계산 프로퍼티로 변경 고려
totalCount와isEmpty가 저장 프로퍼티로 정의되어 있지만, 이들은items.count와items.isEmpty를 단순히 복사하고 있습니다. 계산 프로퍼티로 변경하면 메모리를 절약하고 일관성을 보장할 수 있습니다.다음과 같이 리팩토링할 수 있습니다:
struct OutboundList: Equatable { let items: [Outbound] - let totalCount: Int - let isEmpty: Bool + + var totalCount: Int { + items.count + } + + var isEmpty: Bool { + items.isEmpty + } init(items: [Outbound]) { self.items = items - self.totalCount = items.count - self.isEmpty = items.isEmpty }SampoomManagement/Features/Outbound/Domain/UseCase/DeleteAllOutboundUseCase.swift (1)
17-19: 불필요한 return 키워드 제거 권장Void를 반환하는 함수에서
return키워드가 불필요합니다.다음과 같이 수정할 수 있습니다:
func execute() async throws { - return try await repository.deleteAllOutbound() + try await repository.deleteAllOutbound() }SampoomManagement/Features/Outbound/Domain/UseCase/ProcessOutboundUseCase.swift (1)
17-19: Void 반환 함수에서 불필요한return제거
repository.processOutbound()가 Void를 반환하므로return키워드가 불필요합니다.다음 diff를 적용하세요:
func execute() async throws { - return try await repository.processOutbound() + try await repository.processOutbound() }SampoomManagement/Features/Outbound/Domain/UseCase/DeleteOutboundUseCase.swift (1)
17-19: Void 반환 함수에서 불필요한return제거
repository.deleteOutbound(outboundId:)가 Void를 반환하므로return키워드가 불필요합니다.다음 diff를 적용하세요:
func execute(outboundId: Int) async throws { - return try await repository.deleteOutbound(outboundId: outboundId) + try await repository.deleteOutbound(outboundId: outboundId) }SampoomManagement/Features/Auth/Data/Remote/API/AuthAPI.swift (2)
19-32: DTO에서 parameters로의 수동 매핑을 제거하는 것을 권장합니다.현재 LoginRequestDTO를 생성한 후 그 속성들을 수동으로 파라미터 딕셔너리에 다시 매핑하고 있습니다. 이는 코드 중복을 발생시키며, DTO 속성이 변경될 경우 매핑 코드도 함께 수정해야 하는 유지보수 부담이 있습니다.
NetworkManager가 Encodable 타입을 직접 받을 수 있도록 수정하거나, DTO를 딕셔너리로 인코딩하는 헬퍼 메서드를 사용하는 것을 고려해보세요.
예시 개선안:
// Option 1: DTO를 딕셔너리로 인코딩하는 헬퍼 사용 func login(email: String, password: String) async throws -> APIResponse<LoginResponseDTO> { let requestDTO = LoginRequestDTO(email: email, password: password) return try await networkManager.request( endpoint: "auth/login", method: .post, parameters: requestDTO.asDictionary(), // 헬퍼 메서드 사용 responseType: LoginResponseDTO.self ) } // Option 2: NetworkManager가 Encodable을 직접 받도록 수정 func login(email: String, password: String) async throws -> APIResponse<LoginResponseDTO> { let requestDTO = LoginRequestDTO(email: email, password: password) return try await networkManager.request( endpoint: "auth/login", method: .post, body: requestDTO, // Encodable 직접 전달 responseType: LoginResponseDTO.self ) }
36-67: DTO에서 parameters로의 수동 매핑을 제거하는 것을 권장합니다.login 메서드와 동일한 문제가 있습니다. SignupRequestDTO를 생성한 후 6개의 속성을 수동으로 딕셔너리에 매핑하고 있어 유지보수성이 떨어집니다.
위의 login 메서드에 제안한 것과 동일한 리팩토링 패턴을 적용하세요.
SampoomManagement/Features/Cart/Domain/Repository/CartRepository.swift (1)
12-15:-> Void반환 타입은 생략 가능합니다SwiftLint가 지적한 것처럼
-> Void반환 타입은 Swift에서 선택 사항입니다. 명시하지 않아도 기본적으로 Void를 반환하므로, 코드 일관성을 위해 생략하는 것을 권장합니다.다음과 같이 수정할 수 있습니다:
- func addCart(partId: Int, quantity: Int) async throws -> Void + func addCart(partId: Int, quantity: Int) async throws - func deleteCart(cartItemId: Int) async throws -> Void + func deleteCart(cartItemId: Int) async throws - func deleteAllCart() async throws -> Void + func deleteAllCart() async throws - func updateCartQuantity(cartItemId: Int, quantity: Int) async throws -> Void + func updateCartQuantity(cartItemId: Int, quantity: Int) async throwsSampoomManagement/Features/Outbound/Domain/UseCase/UpdateOutboundQuantityUseCase.swift (1)
17-19: 불필요한return키워드를 제거하세요함수가 Void를 반환할 때는 명시적인
return키워드가 필요하지 않습니다. UpdateCartQuantityUseCase와의 일관성을 위해 제거하는 것이 좋습니다.다음과 같이 수정할 수 있습니다:
func execute(outboundId: Int, quantity: Int) async throws { - return try await repository.updateOutboundQuantity(outboundId: outboundId, quantity: quantity) + try await repository.updateOutboundQuantity(outboundId: outboundId, quantity: quantity) }SampoomManagement/Features/Outbound/Domain/UseCase/AddOutboundUseCase.swift (1)
17-19: 불필요한 return 키워드Void를 반환하는 async throws 함수에서
return키워드는 불필요합니다. 코드는 정상 작동하지만 가독성을 위해 제거하는 것이 좋습니다.다음과 같이 수정할 수 있습니다:
func execute(partId: Int, quantity: Int) async throws { - return try await repository.addOutbound(partId: partId, quantity: quantity) + try await repository.addOutbound(partId: partId, quantity: quantity) }SampoomManagement/Features/Cart/Domain/Models/CartList.swift (1)
10-19: computed property 사용 고려
totalCount와isEmpty는items로부터 계산 가능한 값입니다. 현재 구조도 문제없지만, computed property로 구현하면 데이터 중복을 방지하고 항상 일관된 값을 보장할 수 있습니다.다음과 같이 수정할 수 있습니다:
struct CartList: Equatable { let items: [Cart] - let totalCount: Int - let isEmpty: Bool + var totalCount: Int { items.count } + var isEmpty: Bool { items.isEmpty } init(items: [Cart]) { self.items = items - self.totalCount = items.count - self.isEmpty = items.isEmpty }SampoomManagement/Features/Outbound/Domain/Repository/OutboundRepository.swift (1)
10-17: Repo 프로토콜에 Sendable 채택 + 파라미터 계약 명시 제안멀티스레드/비동기 경계를 안전하게 넘기려면 구현체가 Sendable을 따르도록 강제하는 편이 좋습니다. 또한 quantity(>0) 등 파라미터 제약을 주석으로 명시해 주세요. 모듈 간 사용 시에는 접근제어(public/internal)도 확인 바랍니다.
적용 예:
-protocol OutboundRepository { +protocol OutboundRepository: Sendable {SampoomManagement/Features/Cart/Data/Repository/CartRepositoryImpl.swift (1)
10-16: 클래스를 final로 마크해 의도와 성능을 개선하세요상속이 필요 없다면 final class로 선언하는 것이 관례적이며 성능에도 이점이 있습니다.
-class CartRepositoryImpl: CartRepository { +final class CartRepositoryImpl: CartRepository {추가로, addCart의 quantity(>0) 사전검증이 상위 계층에서 보장되지 않는다면 여기서도 방어코드를 고려해 주세요.
SampoomManagement/Features/Outbound/UI/OutboundListUiState.swift (1)
10-20: 불린 플래그 난립 → 상태 모델로 단순화 제안outboundLoading/isUpdating/isDeleting/isOrderSuccess와 에러 문자열이 분산되어 있어 상태 조합이 늘어납니다. 다음과 같이 enum 기반 상태로 단순화하면 처리·표현 모두 수월해집니다.
예시:
enum LoadState: Equatable { case idle, loading, success, failure(String) } struct OutboundListUiState: Equatable { let listState: LoadState let updateState: LoadState let deleteState: LoadState let selected: Outbound? }SwiftUI 비교 최적화를 위해 Equatable 채택도 고려해 주세요.
Also applies to: 43-65
SampoomManagement/Features/Part/UI/PartDetailViewModel.swift (2)
13-18: ViewModel을 final로 선언상속 의도가 없다면 final class로 명시해 주세요.
-@MainActor -class PartDetailViewModel: ObservableObject { +@MainActor +final class PartDetailViewModel: ObservableObject {
51-60: nil 체크 간결화: guard let 사용part 존재 여부만 확인하므로 guard let으로 단순화하세요.
- case .addToOutbound(let partId, let quantity): - let part = uiState.part - if part != nil { - addToOutbound(partId: partId, quantity: quantity) - } + case .addToOutbound(let partId, let quantity): + guard uiState.part != nil else { break } + addToOutbound(partId: partId, quantity: quantity) ... - case .addToCart(let partId, let quantity): - let part = uiState.part - if part != nil { - addToCart(partId: partId, quantity: quantity) - } + case .addToCart(let partId, let quantity): + guard uiState.part != nil else { break } + addToCart(partId: partId, quantity: quantity)SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift (2)
11-16: 클래스를 final로 명시API 레이어는 상속 가능성이 낮으므로 final class 권장.
-class CartAPI { +final class CartAPI {
42-52: 불필요한-> Void제거 (SwiftLint: redundant_void_return)함수 반환 타입의
-> Void는 생략 가능합니다.- func deleteCart(cartItemId: Int) async throws -> Void { + func deleteCart(cartItemId: Int) async throws { ... - func deleteAllCart() async throws -> Void { + func deleteAllCart() async throws {로그는 운영 빌드에서 비활성화되도록 Logger/OSLog로 전환을 고려해 주세요.
Also applies to: 67-77
SampoomManagement/Core/Network/NetworkManager.swift (1)
18-23: 제네릭 제약 축소 및 인코딩 전략 개선(옵션)응답은 디코딩만 하므로
T: Decodable로 축소하세요. 또한 GET/DELETE에는 URLEncoding 사용을 고려해 주세요.- func request<T: Codable>( + func request<T: Decodable>( endpoint: String, method: HTTPMethod = .get, parameters: Parameters? = nil, responseType: T.Type ) async throws -> APIResponse<T> {인코딩은 정책에 따라 별도 분기:
let encoding: ParameterEncoding = (method == .get || method == .delete) ? URLEncoding.default : JSONEncoding.default dataRequest = AF.request(url, method: method, parameters: parameters, encoding: encoding)SampoomManagement/Features/Cart/UI/CartListView.swift (3)
114-131: onChange 클로저의 미사용 매개변수 정리 (SwiftLint 경고 해소).oldValue를 사용하지 않으니 언더스코어로 치환해 경고를 없애주세요.
- .onChange(of: viewModel.uiState.isOrderSuccess) { oldValue, newValue in + .onChange(of: viewModel.uiState.isOrderSuccess) { _, newValue in if newValue { Toast.text("주문 성공!").show() viewModel.clearSuccess() } } - .onChange(of: viewModel.uiState.updateError) { oldValue, newValue in + .onChange(of: viewModel.uiState.updateError) { _, newValue in if let error = newValue { Toast.text("수량 업데이트 에러: \(error)").show() viewModel.onEvent(.clearUpdateError) } } - .onChange(of: viewModel.uiState.deleteError) { oldValue, newValue in + .onChange(of: viewModel.uiState.deleteError) { _, newValue in if let error = newValue { Toast.text("삭제 에러: \(error)").show() viewModel.onEvent(.clearDeleteError) } }
48-63: ForEach에서 index 대신 안정된 ID 사용.index 기반 ForEach는 식별성/애니메이션/갱신 시 비효율을 유발할 수 있습니다. 모델의 고유 ID를 사용해 주세요.
- ForEach(viewModel.uiState.cartList.indices, id: \.self) { categoryIndex in - let category = viewModel.uiState.cartList[categoryIndex] - ForEach(category.groups.indices, id: \.self) { groupIndex in - let group = category.groups[groupIndex] + ForEach(viewModel.uiState.cartList, id: \.categoryId) { category in + ForEach(category.groups, id: \.groupId) { group in CartSection( categoryName: category.categoryName, groupName: group.groupName, parts: group.parts, isUpdating: viewModel.uiState.isUpdating, isDeleting: viewModel.uiState.isDeleting, onEvent: { event in viewModel.onEvent(event) } ) } }
182-187: 전역 isUpdating/isDeleting로 전체 아이템이 잠김.단일 항목 업데이트/삭제 중에도 전체 리스트가 비활성화됩니다. per‑item 진행 상태(예: updatingItemIds/deletingItemIds Set)를 도입해 해당 행만 잠그는 방식을 권장합니다. UX와 동시성 모두에 이점이 있습니다.
Also applies to: 198-217
SampoomManagement/Features/Part/UI/PartDetailBottomSheetView.swift (2)
105-109: 수량 입력 정규화(0, 공백, 선행 0 등) 보완.현재는 유효 정수만 반영하고, 나머지는 그대로 남습니다. 사용자가 0을 입력하면 1로 되돌리는 등 양방향 동기화를 권장합니다.
- private func handleQuantityTextChange(_ newText: String) { - if let q = Int(newText), q > 0 { - viewModel.onEvent(.setQuantity(q)) - } - } + private func handleQuantityTextChange(_ newText: String) { + let trimmed = newText.trimmingCharacters(in: .whitespacesAndNewlines) + if let q = Int(trimmed), q > 0 { + if String(q) != quantityText { quantityText = String(q) } + viewModel.onEvent(.setQuantity(q)) + } else if trimmed.isEmpty || trimmed == "0" { + quantityText = "1" + viewModel.onEvent(.setQuantity(1)) + } + }
62-97: NavigationView → NavigationStack로 통일.앱 전반이 NavigationStack을 사용한다면 여기서도 동일하게 맞추는 것이 좋습니다. iOS 16+에서의 push/pop API 일관성이 좋아집니다.
- private var mainContent: some View { - NavigationView { + private var mainContent: some View { + NavigationStack { VStack(alignment: .leading, spacing: 16) { … } .padding(24) .background(Color.background) .navigationBarTitleDisplayMode(.inline) } }SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift (2)
25-27: 로그 민감도/용량 관리.전체 APIResponse를 print로 덤프하면 대용량/민감 데이터 노출 위험이 있습니다. 디버그 빌드에서만 요약 필드만 로깅하도록 조정 권장.
30-30: '-> Void' 표기는 불필요합니다.Swift 스타일상 생략 권장(경고 해소).
Also applies to: 43-43, 55-55, 67-67, 80-80
SampoomManagement/Features/Cart/UI/CartListViewModel.swift (3)
10-11: 미사용 import 정리.Combine을 사용하지 않습니다. 제거해 빌드/린트 잡음을 줄이세요.
-import Combine
22-38: 미사용 프로퍼티/메서드 제거.errorLabel과 bindLabel은 사용되지 않습니다. 삭제해 주세요.
- private var errorLabel: String = "" - - func bindLabel(error: String) { - errorLabel = error - }
83-90: 주문 처리 TODO: UX 일관성 점검.Cart 화면에서 주문 확인 UI가 존재하나 실제 처리/성공 플래그가 없습니다. 구현 계획과 릴리스 범위를 명확히 하거나, 버튼을 임시 비활성화/가리기 처리해 혼란을 줄이세요.
SampoomManagement/Features/Outbound/UI/OutboundListView.swift (3)
46-63: onChange 클로저의 미사용 매개변수 정리.oldValue를 쓰지 않으니 언더스코어로 치환하세요. (SwiftLint 예방)
- .onChange(of: viewModel.uiState.isOrderSuccess) { oldValue, newValue in + .onChange(of: viewModel.uiState.isOrderSuccess) { _, newValue in if newValue { Toast.text("출고 주문 성공").show() viewModel.clearSuccess() } } - .onChange(of: viewModel.uiState.updateError) { oldValue, newValue in + .onChange(of: viewModel.uiState.updateError) { _, newValue in if let error = newValue { Toast.text("수량 업데이트 에러: \(error)").show() viewModel.onEvent(.clearUpdateError) } } - .onChange(of: viewModel.uiState.deleteError) { oldValue, newValue in + .onChange(of: viewModel.uiState.deleteError) { _, newValue in if let error = newValue { Toast.text("삭제 에러: \(error)").show() viewModel.onEvent(.clearDeleteError) } }
41-45: bindLabel 호출 제거 제안.bindLabel로 설정한 문자열이 실제 사용되지 않습니다(뷰모델에서도 활용 없음). 호출/메서드 삭제로 단순화 권장.
216-236: 수량 변경 중 전체 행 비활성화.Cart와 동일하게 전역 isUpdating/isDeleting로 모든 행이 잠깁니다. 항목별 잠금으로 UX 개선을 권장합니다.
SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift (3)
22-40: 미사용 프로퍼티/메서드 정리 제안: errorLabel, bindLabel.현재 errorLabel과 bindLabel(error:)은 참조가 없습니다. 제거하거나 실제 바인딩 로직에 통합하세요.
- private var errorLabel: String = "" - - func bindLabel(error: String) { - errorLabel = error - }
102-124: 수량 유효성 검증/클램프 필요.음수/0 수량 업데이트 요청을 사전에 걸러주세요(서버 계약에 따라 최소 1 혹은 0 불가 등). 낙관적 갱신 전 클램프/검증이 안전합니다.
예시:
guard quantity > 0 else { uiState = uiState.copy(updateError: "수량은 1 이상이어야 합니다") return }서버 스펙이 0 허용인지 확인 부탁드립니다.
Also applies to: 126-153
80-81: print 로그를 운영 로그로 교체하거나 DEBUG 가드 처리.Console print는 운영에 남기기 어렵습니다. os.Logger 또는 앱 공용 로거를 사용하고, 필요 시
#if DEBUG로 감싸세요.- print("OutboundListViewModel - loadOutboundList: \(uiState)") + logger.info("OutboundListViewModel.loadOutboundList state=\(String(describing: uiState))")Also applies to: 98-99, 113-114, 166-167, 209-210, 217-218
SampoomManagement/App/ContentView.swift (2)
55-69: ViewModel을 body에서 새로 만들지 말고 StateObject로 보존하세요.body 재계산 시 새 인스턴스가 만들어질 수 있습니다. 탭 전환 시 상태 손실/불필요 재호출이 생깁니다. init에서 생성해 @StateObject로 유지하세요.
struct ContentView: View { let dependencies: AppDependencies @StateObject private var partViewModel: PartViewModel + @StateObject private var outboundViewModel: OutboundListViewModel + @StateObject private var cartViewModel: CartListViewModel @@ init(dependencies: AppDependencies) { self.dependencies = dependencies _partViewModel = StateObject(wrappedValue: dependencies.makePartViewModel()) + _outboundViewModel = StateObject(wrappedValue: dependencies.makeOutboundListViewModel()) + _cartViewModel = StateObject(wrappedValue: dependencies.makeCartListViewModel()) } @@ - Tab(value: .outbound) { + Tab(value: .outbound) { NavigationStack { - OutboundListView(viewModel: dependencies.makeOutboundListViewModel()) + OutboundListView(viewModel: outboundViewModel) } } label: { @@ - Tab(value: .cart) { + Tab(value: .cart) { NavigationStack { - CartListView(viewModel: dependencies.makeCartListViewModel()) + CartListView(viewModel: cartViewModel) } } label: {Also applies to: 71-85, 14-23
125-131: PartListViewModel 생성도 팩토리로 일관화.DI 일관성을 위해 AppDependencies.makePartListViewModel(groupId:)를 사용하세요.
- PartListView( - viewModel: PartListViewModel( - getPartUseCase: dependencies.getPartUseCase, - groupId: groupId - ), - dependencies: dependencies - ) + PartListView( + viewModel: dependencies.makePartListViewModel(groupId: groupId), + dependencies: dependencies + )SampoomManagement/Features/Part/UI/PartListView.swift (3)
12-14: Bottom Sheet 상태 단일화(.sheet(item:) 권장).showBottomSheet(Boolean)과 selectedPart가 분리되어 불일치 위험이 있습니다. selectedPart를 단일 소스로 사용해
.sheet(item:)로 전환하면 안전합니다. Part는 Identifiable이 아니면 래퍼를 쓰세요.예시 개념(발췌):
// 1) @State showBottomSheet 제거 // 2) onClick: viewModel.onEvent(.showBottomSheet(part)) // 3) sheet(item:) 사용 .sheet(item: Binding( get: { viewModel.uiState.selectedPart }, set: { part in if part == nil { viewModel.onEvent(.dismissBottomSheet) } } )) { selectedPart in let detailVM = dependencies.makePartDetailViewModel() PartDetailBottomSheetView(viewModel: detailVM) .onAppear { detailVM.onEvent(.initialize(selectedPart)) } .presentationDetents([.fraction(0.3)]) .presentationDragIndicator(.automatic) .presentationBackground(.clear) }주의: PartListUiState.copy가 더블 Optional을 지원해야
.dismissBottomSheet로 nil 클리어가 동작합니다(별도 코멘트 참고).Also applies to: 55-63, 72-87
72-87: 상세 ViewModel 재생성을 최소화하세요.시트가 열릴 때마다 VM을 새로 만들면 상태가 초기화됩니다. 필요 시 작은 컨테이너 View에서 @StateObject로 보존하거나, DI 팩토리에 “scoped” 생성 전략을 도입하세요.
96-124: 카드 버튼 접근성 보강.음성 사용자에게 맥락을 제공하세요.
- Button(action: onClick) { + Button(action: onClick) { ... - } + } + .accessibilityLabel("\(part.name), 수량 \(part.quantity)") + .accessibilityAddTraits(.isButton) + .buttonStyle(.plain)SampoomManagement/Core/DI/AppDependencies.swift (1)
12-12: DI 컨테이너를 final로 선언.상속이 필요 없다면 final로 고정해 의도를 명확히 하고 성능을 미세 개선하세요.
-class AppDependencies { +final class AppDependencies {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
SampoomManagement/Resources/Assets.xcassets/outbound.imageset/outbound.svgis excluded by!**/*.svg
📒 Files selected for processing (57)
SampoomManagement/App/ContentView.swift(3 hunks)SampoomManagement/Core/DI/AppDependencies.swift(3 hunks)SampoomManagement/Core/Network/APIResponse.swift(1 hunks)SampoomManagement/Core/Network/NetworkError.swift(2 hunks)SampoomManagement/Core/Network/NetworkManager.swift(1 hunks)SampoomManagement/Core/Resources/StringResources.swift(1 hunks)SampoomManagement/Core/UI/Components/CommonButton.swift(4 hunks)SampoomManagement/Features/Auth/Data/Remote/API/AuthAPI.swift(2 hunks)SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift(1 hunks)SampoomManagement/Features/Cart/Data/Mappers/CartMappers.swift(1 hunks)SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift(1 hunks)SampoomManagement/Features/Cart/Data/Remote/DTO/AddCartRequestDto.swift(1 hunks)SampoomManagement/Features/Cart/Data/Remote/DTO/CartDto.swift(1 hunks)SampoomManagement/Features/Cart/Data/Remote/DTO/UpdateCartRequestDto.swift(1 hunks)SampoomManagement/Features/Cart/Data/Repository/CartRepositoryImpl.swift(1 hunks)SampoomManagement/Features/Cart/Domain/Models/Cart.swift(1 hunks)SampoomManagement/Features/Cart/Domain/Models/CartList.swift(1 hunks)SampoomManagement/Features/Cart/Domain/Repository/CartRepository.swift(1 hunks)SampoomManagement/Features/Cart/Domain/UseCase/AddCartUseCase.swift(1 hunks)SampoomManagement/Features/Cart/Domain/UseCase/DeleteAllCartUseCase.swift(1 hunks)SampoomManagement/Features/Cart/Domain/UseCase/DeleteCartUseCase.swift(1 hunks)SampoomManagement/Features/Cart/Domain/UseCase/GetCartUseCase.swift(1 hunks)SampoomManagement/Features/Cart/Domain/UseCase/UpdateCartQuantityUseCase.swift(1 hunks)SampoomManagement/Features/Cart/UI/CartListUiEvent.swift(1 hunks)SampoomManagement/Features/Cart/UI/CartListUiState.swift(1 hunks)SampoomManagement/Features/Cart/UI/CartListView.swift(1 hunks)SampoomManagement/Features/Cart/UI/CartListViewModel.swift(1 hunks)SampoomManagement/Features/Outbound/Data/Mappers/OutboundMappers.swift(1 hunks)SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift(1 hunks)SampoomManagement/Features/Outbound/Data/Remote/DTO/AddOutboundRequestDto.swift(1 hunks)SampoomManagement/Features/Outbound/Data/Remote/DTO/OutboundDto.swift(1 hunks)SampoomManagement/Features/Outbound/Data/Remote/DTO/UpdateOutboundRequestDto.swift(1 hunks)SampoomManagement/Features/Outbound/Data/Repository/OutboundRepositoryImpl.swift(1 hunks)SampoomManagement/Features/Outbound/Domain/Models/Outbound.swift(1 hunks)SampoomManagement/Features/Outbound/Domain/Models/OutboundList.swift(1 hunks)SampoomManagement/Features/Outbound/Domain/Repository/OutboundRepository.swift(1 hunks)SampoomManagement/Features/Outbound/Domain/UseCase/AddOutboundUseCase.swift(1 hunks)SampoomManagement/Features/Outbound/Domain/UseCase/DeleteAllOutboundUseCase.swift(1 hunks)SampoomManagement/Features/Outbound/Domain/UseCase/DeleteOutboundUseCase.swift(1 hunks)SampoomManagement/Features/Outbound/Domain/UseCase/GetOutboundUseCase.swift(1 hunks)SampoomManagement/Features/Outbound/Domain/UseCase/ProcessOutboundUseCase.swift(1 hunks)SampoomManagement/Features/Outbound/Domain/UseCase/UpdateOutboundQuantityUseCase.swift(1 hunks)SampoomManagement/Features/Outbound/UI/OutboundListUiEvent.swift(1 hunks)SampoomManagement/Features/Outbound/UI/OutboundListUiState.swift(1 hunks)SampoomManagement/Features/Outbound/UI/OutboundListView.swift(1 hunks)SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift(1 hunks)SampoomManagement/Features/Part/Data/Remote/API/PartAPI.swift(1 hunks)SampoomManagement/Features/Part/UI/PartDetailBottomSheetView.swift(1 hunks)SampoomManagement/Features/Part/UI/PartDetailUiEvent.swift(1 hunks)SampoomManagement/Features/Part/UI/PartDetailUiState.swift(1 hunks)SampoomManagement/Features/Part/UI/PartDetailViewModel.swift(1 hunks)SampoomManagement/Features/Part/UI/PartListUiEvent.swift(1 hunks)SampoomManagement/Features/Part/UI/PartListUiState.swift(1 hunks)SampoomManagement/Features/Part/UI/PartListView.swift(3 hunks)SampoomManagement/Features/Part/UI/PartListViewModel.swift(2 hunks)SampoomManagement/Features/Part/UI/PartView.swift(1 hunks)SampoomManagement/Resources/Assets.xcassets/outbound.imageset/Contents.json(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (40)
SampoomManagement/Features/Part/Data/Remote/API/PartAPI.swift (2)
SampoomManagement/Core/Network/NetworkManager.swift (1)
request(18-51)SampoomManagement/Features/Part/Data/Mappers/PartMappers.swift (3)
toModel(11-17)toModel(21-28)toModel(32-39)
SampoomManagement/Features/Outbound/Domain/Models/OutboundList.swift (1)
SampoomManagement/Features/Cart/Domain/Models/CartList.swift (1)
empty(21-23)
SampoomManagement/Features/Outbound/Domain/UseCase/DeleteAllOutboundUseCase.swift (4)
SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift (1)
deleteAllOutbound(80-89)SampoomManagement/Features/Outbound/Data/Repository/OutboundRepositoryImpl.swift (1)
deleteAllOutbound(36-38)SampoomManagement/Features/Outbound/UI/OutboundListView.swift (1)
deleteAllOutbound(266-266)SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift (1)
deleteAllOutbound(198-220)
SampoomManagement/Features/Cart/Domain/Repository/CartRepository.swift (3)
SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift (4)
getCartList(19-27)addCart(30-40)deleteCart(43-52)deleteAllCart(68-77)SampoomManagement/Features/Cart/Data/Repository/CartRepositoryImpl.swift (5)
getCartList(17-21)addCart(23-26)deleteCart(28-30)deleteAllCart(32-34)updateCartQuantity(36-39)SampoomManagement/Features/Cart/UI/CartListViewModel.swift (2)
deleteCart(145-167)deleteAllCart(188-210)
SampoomManagement/Features/Part/UI/PartDetailUiState.swift (3)
SampoomManagement/Features/Cart/UI/CartListUiState.swift (1)
copy(43-65)SampoomManagement/Features/Outbound/UI/OutboundListUiState.swift (1)
copy(43-65)SampoomManagement/Features/Part/UI/PartListUiState.swift (1)
copy(28-40)
SampoomManagement/Features/Part/UI/PartListViewModel.swift (1)
SampoomManagement/Features/Part/UI/PartListUiState.swift (1)
copy(28-40)
SampoomManagement/Features/Outbound/Domain/Repository/OutboundRepository.swift (3)
SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift (5)
getOutboundList(19-27)processOutbound(43-52)addOutbound(30-40)deleteOutbound(55-64)deleteAllOutbound(80-89)SampoomManagement/Features/Outbound/Data/Repository/OutboundRepositoryImpl.swift (6)
getOutboundList(17-21)processOutbound(23-25)addOutbound(27-30)deleteOutbound(32-34)deleteAllOutbound(36-38)updateOutboundQuantity(40-43)SampoomManagement/Features/Outbound/UI/OutboundListView.swift (6)
getOutboundList(259-261)processOutbound(263-263)addOutbound(264-264)deleteOutbound(265-265)deleteAllOutbound(266-266)updateOutboundQuantity(267-267)
SampoomManagement/Features/Part/UI/PartDetailUiEvent.swift (2)
SampoomManagement/Features/Part/UI/PartDetailBottomSheetView.swift (4)
increaseQuantity(11-11)decreaseQuantity(10-10)addToOutbound(12-16)addToCart(17-21)SampoomManagement/Features/Part/UI/PartDetailViewModel.swift (2)
addToOutbound(72-89)addToCart(91-108)
SampoomManagement/Features/Cart/UI/CartListViewModel.swift (8)
SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift (7)
bindLabel(38-40)onEvent(42-61)updateQuantity(102-124)updateLocalQuantity(126-153)removeFromLocalList(179-196)removeAllFromLocalList(222-224)clearSuccess(226-228)SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift (2)
deleteCart(43-52)deleteAllCart(68-77)SampoomManagement/Features/Cart/Data/Repository/CartRepositoryImpl.swift (2)
deleteCart(28-30)deleteAllCart(32-34)SampoomManagement/Features/Cart/UI/CartListUiState.swift (1)
copy(43-65)SampoomManagement/Features/Cart/Domain/UseCase/DeleteAllCartUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Domain/UseCase/DeleteCartUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Domain/UseCase/GetCartUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Domain/UseCase/UpdateCartQuantityUseCase.swift (1)
execute(17-19)
SampoomManagement/Features/Outbound/Domain/UseCase/AddOutboundUseCase.swift (3)
SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift (1)
addOutbound(30-40)SampoomManagement/Features/Outbound/Data/Repository/OutboundRepositoryImpl.swift (1)
addOutbound(27-30)SampoomManagement/Features/Outbound/UI/OutboundListView.swift (1)
addOutbound(264-264)
SampoomManagement/Features/Outbound/Domain/UseCase/UpdateOutboundQuantityUseCase.swift (2)
SampoomManagement/Features/Outbound/Data/Repository/OutboundRepositoryImpl.swift (1)
updateOutboundQuantity(40-43)SampoomManagement/Features/Outbound/UI/OutboundListView.swift (1)
updateOutboundQuantity(267-267)
SampoomManagement/Features/Part/UI/PartDetailBottomSheetView.swift (1)
SampoomManagement/Features/Part/UI/PartDetailViewModel.swift (4)
onEvent(30-70)addToOutbound(72-89)addToCart(91-108)clearSuccess(110-112)
SampoomManagement/Features/Cart/Domain/UseCase/GetCartUseCase.swift (3)
SampoomManagement/Features/Outbound/Domain/UseCase/GetOutboundUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift (1)
getCartList(19-27)SampoomManagement/Features/Cart/Data/Repository/CartRepositoryImpl.swift (1)
getCartList(17-21)
SampoomManagement/Features/Outbound/Domain/UseCase/ProcessOutboundUseCase.swift (4)
SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift (1)
processOutbound(43-52)SampoomManagement/Features/Outbound/Data/Repository/OutboundRepositoryImpl.swift (1)
processOutbound(23-25)SampoomManagement/Features/Outbound/UI/OutboundListView.swift (1)
processOutbound(263-263)SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift (1)
processOutbound(84-100)
SampoomManagement/Features/Outbound/UI/OutboundListUiEvent.swift (1)
SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift (5)
loadOutboundList(63-82)processOutbound(84-100)updateQuantity(102-124)deleteOutbound(155-177)deleteAllOutbound(198-220)
SampoomManagement/Features/Outbound/Domain/UseCase/GetOutboundUseCase.swift (3)
SampoomManagement/Features/Cart/Domain/UseCase/GetCartUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift (1)
getOutboundList(19-27)SampoomManagement/Features/Outbound/Data/Repository/OutboundRepositoryImpl.swift (1)
getOutboundList(17-21)
SampoomManagement/Features/Cart/UI/CartListUiState.swift (3)
SampoomManagement/Features/Outbound/UI/OutboundListUiState.swift (1)
copy(43-65)SampoomManagement/Features/Part/UI/PartDetailUiState.swift (1)
copy(34-50)SampoomManagement/Features/Part/UI/PartListUiState.swift (1)
copy(28-40)
SampoomManagement/Features/Outbound/Data/Repository/OutboundRepositoryImpl.swift (2)
SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift (6)
getOutboundList(19-27)processOutbound(43-52)addOutbound(30-40)deleteOutbound(55-64)deleteAllOutbound(80-89)updateOutbound(67-77)SampoomManagement/Features/Outbound/Data/Mappers/OutboundMappers.swift (3)
toModel(11-17)toModel(21-27)toModel(31-39)
SampoomManagement/Features/Cart/Domain/UseCase/DeleteAllCartUseCase.swift (3)
SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift (1)
deleteAllCart(68-77)SampoomManagement/Features/Cart/Data/Repository/CartRepositoryImpl.swift (1)
deleteAllCart(32-34)SampoomManagement/Features/Cart/UI/CartListViewModel.swift (1)
deleteAllCart(188-210)
SampoomManagement/Features/Cart/Domain/Models/CartList.swift (1)
SampoomManagement/Features/Outbound/Domain/Models/OutboundList.swift (1)
empty(21-23)
SampoomManagement/Features/Cart/Domain/UseCase/UpdateCartQuantityUseCase.swift (5)
SampoomManagement/Features/Cart/Domain/UseCase/AddCartUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Domain/UseCase/DeleteAllCartUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Domain/UseCase/DeleteCartUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Domain/UseCase/GetCartUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Data/Repository/CartRepositoryImpl.swift (1)
updateCartQuantity(36-39)
SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift (2)
SampoomManagement/Features/Outbound/Data/Repository/OutboundRepositoryImpl.swift (5)
getOutboundList(17-21)addOutbound(27-30)processOutbound(23-25)deleteOutbound(32-34)deleteAllOutbound(36-38)SampoomManagement/Core/Network/NetworkManager.swift (1)
request(18-51)
SampoomManagement/Features/Outbound/Domain/UseCase/DeleteOutboundUseCase.swift (4)
SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift (1)
deleteOutbound(55-64)SampoomManagement/Features/Outbound/Data/Repository/OutboundRepositoryImpl.swift (1)
deleteOutbound(32-34)SampoomManagement/Features/Outbound/UI/OutboundListView.swift (1)
deleteOutbound(265-265)SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift (1)
deleteOutbound(155-177)
SampoomManagement/Features/Cart/UI/CartListUiEvent.swift (1)
SampoomManagement/Features/Cart/UI/CartListViewModel.swift (5)
loadCartList(61-81)processOrder(84-90)updateQuantity(92-114)deleteCart(145-167)deleteAllCart(188-210)
SampoomManagement/Features/Cart/Data/Mappers/CartMappers.swift (1)
SampoomManagement/Features/Outbound/Data/Mappers/OutboundMappers.swift (3)
toModel(11-17)toModel(21-27)toModel(31-39)
SampoomManagement/Features/Part/UI/PartListUiState.swift (4)
SampoomManagement/Features/Cart/UI/CartListUiState.swift (1)
copy(43-65)SampoomManagement/Features/Outbound/UI/OutboundListUiState.swift (1)
copy(43-65)SampoomManagement/Features/Part/UI/PartDetailUiState.swift (1)
copy(34-50)SampoomManagement/Features/Part/UI/PartUIState.swift (1)
copy(43-61)
SampoomManagement/Features/Part/UI/PartDetailViewModel.swift (4)
SampoomManagement/Features/Part/UI/PartDetailUiState.swift (1)
copy(34-50)SampoomManagement/Features/Part/UI/PartDetailBottomSheetView.swift (4)
increaseQuantity(11-11)decreaseQuantity(10-10)addToOutbound(12-16)addToCart(17-21)SampoomManagement/Features/Cart/Domain/UseCase/AddCartUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Outbound/Domain/UseCase/AddOutboundUseCase.swift (1)
execute(17-19)
SampoomManagement/Features/Auth/Data/Remote/API/AuthAPI.swift (1)
SampoomManagement/Core/Network/NetworkManager.swift (1)
request(18-51)
SampoomManagement/Features/Outbound/Data/Mappers/OutboundMappers.swift (1)
SampoomManagement/Features/Cart/Data/Mappers/CartMappers.swift (3)
toModel(11-13)toModel(17-19)toModel(23-25)
SampoomManagement/Features/Part/UI/PartView.swift (1)
SampoomManagement/Features/Part/UI/PartViewModel.swift (1)
onEvent(28-39)
SampoomManagement/Features/Cart/Domain/UseCase/AddCartUseCase.swift (5)
SampoomManagement/Features/Cart/Domain/UseCase/DeleteCartUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Domain/UseCase/GetCartUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Domain/UseCase/UpdateCartQuantityUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift (1)
addCart(30-40)SampoomManagement/Features/Cart/Data/Repository/CartRepositoryImpl.swift (1)
addCart(23-26)
SampoomManagement/Features/Cart/Domain/UseCase/DeleteCartUseCase.swift (7)
SampoomManagement/Features/Cart/Domain/UseCase/AddCartUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Domain/UseCase/DeleteAllCartUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Domain/UseCase/GetCartUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Domain/UseCase/UpdateCartQuantityUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift (1)
deleteCart(43-52)SampoomManagement/Features/Cart/Data/Repository/CartRepositoryImpl.swift (1)
deleteCart(28-30)SampoomManagement/Features/Cart/UI/CartListViewModel.swift (1)
deleteCart(145-167)
SampoomManagement/Features/Cart/Data/Repository/CartRepositoryImpl.swift (3)
SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift (5)
getCartList(19-27)addCart(30-40)deleteCart(43-52)deleteAllCart(68-77)updateCart(55-65)SampoomManagement/Features/Cart/Data/Mappers/CartMappers.swift (3)
toModel(11-13)toModel(17-19)toModel(23-25)SampoomManagement/Core/Network/NetworkManager.swift (1)
request(18-51)
SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift (7)
SampoomManagement/Features/Cart/UI/CartListViewModel.swift (7)
bindLabel(36-38)onEvent(40-59)updateQuantity(92-114)updateLocalQuantity(116-143)removeFromLocalList(169-186)removeAllFromLocalList(212-214)clearSuccess(216-218)SampoomManagement/Features/Outbound/UI/OutboundListUiState.swift (1)
copy(43-65)SampoomManagement/Features/Outbound/Domain/UseCase/DeleteAllOutboundUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Outbound/Domain/UseCase/DeleteOutboundUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Outbound/Domain/UseCase/GetOutboundUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Outbound/Domain/UseCase/ProcessOutboundUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Outbound/Domain/UseCase/UpdateOutboundQuantityUseCase.swift (1)
execute(17-19)
SampoomManagement/Features/Cart/UI/CartListView.swift (3)
SampoomManagement/Features/Cart/UI/CartListViewModel.swift (7)
onEvent(40-59)deleteAllCart(188-210)processOrder(84-90)loadCartList(61-81)clearSuccess(216-218)deleteCart(145-167)updateQuantity(92-114)SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift (2)
deleteAllCart(68-77)deleteCart(43-52)SampoomManagement/Features/Cart/Data/Repository/CartRepositoryImpl.swift (2)
deleteAllCart(32-34)deleteCart(28-30)
SampoomManagement/Features/Outbound/UI/OutboundListView.swift (2)
SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift (8)
clearSuccess(226-228)bindLabel(38-40)onEvent(42-61)loadOutboundList(63-82)deleteAllOutbound(198-220)processOutbound(84-100)deleteOutbound(155-177)updateQuantity(102-124)SampoomManagement/Features/Outbound/Domain/Models/OutboundList.swift (1)
empty(21-23)
SampoomManagement/Features/Outbound/UI/OutboundListUiState.swift (3)
SampoomManagement/Features/Cart/UI/CartListUiState.swift (1)
copy(43-65)SampoomManagement/Features/Part/UI/PartDetailUiState.swift (1)
copy(34-50)SampoomManagement/Features/Part/UI/PartListUiState.swift (1)
copy(28-40)
SampoomManagement/App/ContentView.swift (1)
SampoomManagement/Core/DI/AppDependencies.swift (2)
makeOutboundListViewModel(117-125)makeCartListViewModel(127-134)
SampoomManagement/Features/Part/UI/PartListView.swift (3)
SampoomManagement/Features/Part/UI/PartDetailViewModel.swift (1)
onEvent(30-70)SampoomManagement/Features/Part/UI/PartListViewModel.swift (1)
onEvent(30-41)SampoomManagement/Core/DI/AppDependencies.swift (1)
makePartDetailViewModel(113-115)
SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift (3)
SampoomManagement/Features/Cart/Data/Repository/CartRepositoryImpl.swift (4)
getCartList(17-21)addCart(23-26)deleteCart(28-30)deleteAllCart(32-34)SampoomManagement/Core/Network/NetworkManager.swift (1)
request(18-51)SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift (1)
toDictionary(94-97)
🪛 SwiftLint (0.57.0)
SampoomManagement/Features/Cart/Domain/Repository/CartRepository.swift
[Warning] 12-12: Returning Void in a function declaration is redundant
(redundant_void_return)
[Warning] 13-13: Returning Void in a function declaration is redundant
(redundant_void_return)
[Warning] 14-14: Returning Void in a function declaration is redundant
(redundant_void_return)
[Warning] 15-15: Returning Void in a function declaration is redundant
(redundant_void_return)
SampoomManagement/Features/Cart/UI/CartListViewModel.swift
[Warning] 20-20: TODOs should be resolved (ProcessOrderUseCase 구현 후 주입)
(todo)
[Warning] 83-83: TODOs should be resolved (주문 생성 UseCase 구현 후 수정)
(todo)
[Warning] 88-88: TODOs should be resolved (ProcessOrderUseCase 구현 후 사용)
(todo)
SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift
[Warning] 30-30: Returning Void in a function declaration is redundant
(redundant_void_return)
[Warning] 43-43: Returning Void in a function declaration is redundant
(redundant_void_return)
[Warning] 55-55: Returning Void in a function declaration is redundant
(redundant_void_return)
[Warning] 67-67: Returning Void in a function declaration is redundant
(redundant_void_return)
[Warning] 80-80: Returning Void in a function declaration is redundant
(redundant_void_return)
SampoomManagement/Features/Cart/UI/CartListView.swift
[Warning] 114-114: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
[Warning] 120-120: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
[Warning] 126-126: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift
[Warning] 30-30: Returning Void in a function declaration is redundant
(redundant_void_return)
[Warning] 43-43: Returning Void in a function declaration is redundant
(redundant_void_return)
[Warning] 55-55: Returning Void in a function declaration is redundant
(redundant_void_return)
[Warning] 68-68: Returning Void in a function declaration is redundant
(redundant_void_return)
🔇 Additional comments (38)
SampoomManagement/Core/UI/Components/CommonButton.swift (1)
45-45: 커스텀 아이콘 속성 추가가 잘 구현되었습니다.새로운
customIcon속성과 초기화 로직이 올바르게 추가되었으며, 기본값nil로 인해 기존 코드와의 하위 호환성이 유지됩니다.Also applies to: 58-58, 70-70
SampoomManagement/Core/Resources/StringResources.swift (1)
20-20: 변경 사항 승인delivery에서 outbound로의 명명 변경이 PR의 목적과 일치하며, 전체 코드베이스에서 일관되게 적용되었습니다.
SampoomManagement/Core/Network/NetworkError.swift (1)
37-48: 새로운 에러 케이스 추가 승인
invalidResponse케이스 추가가 적절하며, 기존 에러 처리 패턴과 일관성을 유지하고 있습니다. AuthRepositoryImpl에서 nil 응답 처리에 사용되는 것이 확인됩니다.SampoomManagement/Features/Outbound/Domain/Models/OutboundList.swift (1)
21-23: empty() 헬퍼 메서드 승인CartList와 동일한 패턴을 따르는 편리한 팩토리 메서드입니다.
SampoomManagement/Features/Part/UI/PartListUiEvent.swift (1)
13-14: 바텀 시트 이벤트 추가 승인Part 상세 바텀 시트 처리를 위한 이벤트가 적절하게 추가되었습니다. 기존 이벤트 패턴과 일관성을 유지합니다.
SampoomManagement/Features/Outbound/Domain/Models/Outbound.swift (1)
10-28: 도메인 모델 구조 승인출고 도메인을 위한 깔끔한 계층 구조입니다.
Outbound→OutboundGroup→OutboundPart의 중첩 구조가 명확하며, Equatable 준수가 적절합니다.SampoomManagement/Resources/Assets.xcassets/outbound.imageset/Contents.json (1)
4-4: 애셋 파일명 변경 승인delivery에서 outbound로의 애셋 파일명 변경이 전체 명명 변경과 일치합니다.
SampoomManagement/Features/Cart/Data/Remote/DTO/UpdateCartRequestDto.swift (1)
10-12: 간단한 DTO 구조 승인장바구니 수량 업데이트를 위한 명확하고 간단한 DTO입니다. Codable 준수가 적절합니다.
SampoomManagement/Features/Outbound/Domain/UseCase/DeleteAllOutboundUseCase.swift (1)
10-16: Use Case 구조 승인의존성 주입과 클린 아키텍처 패턴을 올바르게 따르고 있습니다.
SampoomManagement/Features/Cart/Domain/UseCase/DeleteAllCartUseCase.swift (1)
10-20: 구현이 깔끔합니다!표준 Use Case 패턴을 올바르게 따르고 있으며, Repository로 작업을 위임하는 구조가 적절합니다.
SampoomManagement/Core/Network/APIResponse.swift (1)
14-18: API 응답 처리 개선이 좋습니다!
data를 옵셔널로 변경하고EmptyResponse타입을 추가한 것은 빈 응답을 처리하는 안전하고 명시적인 방법입니다. 이를 통해 데이터가 없는 엔드포인트(예: 삭제, 업데이트)를 더 명확하게 표현할 수 있습니다.SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (1)
41-59: 응답 데이터 검증 로직이 훌륭합니다!
guard let구문을 통해 응답 데이터의 존재를 명시적으로 검증하고, 데이터가 없을 경우AuthError.invalidResponse를 던지는 방식이 적절합니다. 이는 방어적 프로그래밍의 좋은 예시입니다.SampoomManagement/Features/Part/UI/PartListViewModel.swift (2)
30-41: 바텀 시트 이벤트 처리가 올바르게 구현되었습니다!
showBottomSheet와dismissBottomSheet이벤트 핸들러가selectedPart상태를 적절하게 업데이트하고 있습니다.
72-72: 프로덕션 환경에서 디버그 로그 노출 검토 필요
#if DEBUG조건부 컴파일이 제거되어 이제 프로덕션 빌드에서도 로그가 출력됩니다. 프로덕션 환경에서 이 로그가 필요한지, 아니면 디버그 빌드로만 제한해야 하는지 검토가 필요합니다.SampoomManagement/Features/Cart/Data/Remote/DTO/AddCartRequestDto.swift (1)
10-13: 간단하고 명확한 DTO 구조입니다!장바구니 추가 요청에 필요한 데이터를 명확하게 정의하고 있습니다.
Codable준수로 직렬화/역직렬화가 자동으로 처리됩니다.SampoomManagement/Features/Part/UI/PartView.swift (1)
60-81: UI 레이아웃 개선이 좋습니다!에러 상태와 빈 상태를
HStack과Spacer()로 감싸 중앙 정렬한 것은 더 나은 사용자 경험을 제공합니다. 구현이 깔끔하고 일관성 있습니다.SampoomManagement/Features/Cart/Domain/UseCase/DeleteCartUseCase.swift (1)
10-20: 잘 구현되었습니다!Use case 패턴이 올바르게 적용되었으며, repository로의 단순 위임 구조가 적절합니다. 다른 use case들(AddCartUseCase, GetCartUseCase 등)과 일관된 패턴을 따르고 있습니다.
SampoomManagement/Features/Outbound/Data/Remote/DTO/AddOutboundRequestDto.swift (1)
10-13: 깔끔한 DTO 구조입니다!요청 데이터 구조가 명확하고 Codable을 통한 직렬화가 적절히 구현되어 있습니다.
SampoomManagement/Features/Part/Data/Remote/API/PartAPI.swift (1)
17-42: async/await 마이그레이션이 깔끔하게 수행되었습니다!세 개의 메서드 모두 일관된 패턴으로 잘 구현되었습니다:
- 명확한 네트워크 요청 처리
- nil 데이터에 대한 적절한 빈 배열 기본값 처리
- DTO에서 도메인 모델로의 깔끔한 매핑
withCheckedThrowingContinuation에서 직접적인 await 호출로의 전환이 코드를 더 읽기 쉽고 유지보수하기 쉽게 만들었습니다.
SampoomManagement/Features/Cart/Domain/UseCase/GetCartUseCase.swift (1)
10-20: Use case 구현이 올바릅니다!Repository로의 단순 위임과 에러 전파가 적절하며, 다른 use case들과 일관된 패턴을 따릅니다.
SampoomManagement/Features/Cart/Data/Remote/DTO/CartDto.swift (1)
10-28: 계층적 DTO 구조가 명확하고 잘 정의되어 있습니다!카트 데이터의 3단계 계층 구조(Category → Group → Part)가 명확하게 표현되어 있으며, 각 DTO의 속성들이 적절합니다. Codable 준수를 통한 JSON 직렬화/역직렬화가 올바르게 구현되었습니다.
SampoomManagement/Features/Outbound/Data/Remote/DTO/UpdateOutboundRequestDto.swift (1)
10-12: 간결하고 명확한 DTO입니다!수량 업데이트를 위한 단순하고 적절한 구조입니다. AddOutboundRequestDto와 일관된 패턴을 따릅니다.
SampoomManagement/Features/Part/UI/PartDetailUiEvent.swift (1)
10-19: LGTM!UI 이벤트 정의가 명확하고 포괄적입니다. Part 상세 화면의 모든 주요 동작(수량 조정, 출고/장바구니 추가, 에러 처리 등)을 잘 표현하고 있습니다.
SampoomManagement/Features/Cart/Domain/UseCase/AddCartUseCase.swift (1)
10-20: LGTM!장바구니 추가 Use Case가 올바르게 구현되었습니다. 다른 Cart Use Case들과 일관된 패턴을 따르고 있으며, 리포지토리에 적절하게 위임하고 있습니다.
SampoomManagement/Features/Outbound/Data/Remote/DTO/OutboundDto.swift (1)
10-28: LGTM!출고 데이터를 위한 DTO 구조가 잘 설계되었습니다. 계층적 구조(OutboundDto → OutboundGroupDto → OutboundPartDto)가 명확하고 Codable 프로토콜을 통해 직렬화/역직렬화가 가능합니다.
SampoomManagement/Features/Outbound/Domain/UseCase/GetOutboundUseCase.swift (1)
10-20: LGTM!출고 목록 조회 Use Case가 올바르게 구현되었습니다. GetCartUseCase와 일관된 패턴을 따르고 있으며, 리포지토리로부터 데이터를 가져오는 역할을 명확하게 수행합니다.
SampoomManagement/Features/Cart/UI/CartListUiEvent.swift (1)
10-19: LGTM!장바구니 목록 UI 이벤트가 포괄적으로 정의되어 있습니다. 로딩, 재시도, 주문 처리, 수량 변경, 삭제 작업, 에러 처리 등 필요한 모든 동작을 커버하고 있습니다.
SampoomManagement/Features/Cart/Domain/UseCase/UpdateCartQuantityUseCase.swift (1)
10-20: LGTM!장바구니 수량 업데이트 Use Case가 올바르게 구현되었습니다. 다른 Cart Use Case들과 일관된 구조를 유지하고 있으며, 리포지토리에 적절하게 위임합니다.
SampoomManagement/Features/Outbound/Data/Repository/OutboundRepositoryImpl.swift (1)
10-44: 잘 구현된 Repository 패턴Repository 구현이 깔끔하고 명확합니다. DTO에서 도메인 모델로의 변환이 적절하게 처리되었으며, 에러 전파도 올바르게 구현되었습니다.
SampoomManagement/Features/Outbound/UI/OutboundListUiEvent.swift (1)
10-19: 포괄적인 UI 이벤트 정의UI 이벤트가 잘 정의되어 있으며, 모든 필요한 사용자 상호작용을 커버하고 있습니다. 에러 처리를 위한 clear 이벤트도 적절하게 포함되어 있습니다.
SampoomManagement/Features/Cart/Data/Mappers/CartMappers.swift (1)
10-26: 깔끔한 매퍼 구현DTO에서 도메인 모델로의 변환이 명확하고 일관성 있게 구현되었습니다. 중첩된 구조의 매핑도 적절하게 처리되었습니다.
SampoomManagement/Features/Outbound/Data/Mappers/OutboundMappers.swift (1)
10-40: 일관성 있는 매퍼 구현Outbound 매퍼가 Cart 매퍼와 동일한 패턴으로 잘 구현되었습니다. DTO 계층 구조를 도메인 모델로 명확하게 변환하고 있습니다.
SampoomManagement/Features/Cart/Domain/Models/Cart.swift (1)
10-28: 잘 설계된 도메인 모델Cart 도메인 모델의 계층 구조가 명확하고 적절하게 정의되었습니다. Equatable 준수도 올바르게 구현되었습니다.
SampoomManagement/Features/Cart/UI/CartListUiState.swift (1)
10-66: 완성도 높은 UI 상태 관리UI 상태가 포괄적으로 정의되어 있으며,
copy()메서드를 통한 불변 상태 업데이트 패턴이 적절하게 구현되었습니다. 로딩, 에러, 선택 상태 등 모든 필요한 상태를 커버하고 있습니다.SampoomManagement/Features/Part/UI/PartDetailViewModel.swift (1)
72-89: 리뷰 의견이 잘못되었습니다. @mainactor 클래스 선언을 간과했습니다.PartDetailViewModel은 클래스 레벨에서 이미
@MainActor로 선언되어 있습니다(12줄). Swift 동시성에서 @mainactor 클래스의 모든 메서드와 속성은 자동으로 메인 스레드에서 실행됩니다. 따라서 메서드 내부의Task { }블록도 MainActor 컨텍스트를 상속받아 이미 메인 스레드에서 실행됩니다.제안된
await MainActor.run { ... }래핑은 불필요한 중복이며, 현재 코드는 스레드 안전성이 이미 보장되어 있습니다.Likely an incorrect or invalid review comment.
SampoomManagement/Features/Outbound/UI/OutboundListView.swift (1)
26-27: 내비게이션 컨테이너 존재 여부 확인.이 뷰 자체에는 NavigationStack이 없고 navigationTitle만 설정됩니다. 상위에서 NavigationStack으로 감싸는지 확인 부탁드립니다. 아니라면 여기에서 감싸 주세요.
SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift (1)
92-98: 리뷰 의견이 타당하지 않습니다.코드베이스 전체에서
Encodable확장과toDictionary()메서드를 검색한 결과, 다음과 같이 정확히 1개의 정의만 존재합니다:
extension Encodable정의: SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift:93 (1개만)func toDictionary()정의: SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift:94 (1개만)중복 정의가 없으므로 ODR 위반이나 유지보수 비용 증가의 위험이 없습니다. 현재 구현은 문제없습니다.
Likely an incorrect or invalid review comment.
SampoomManagement/Features/Cart/UI/CartListView.swift (1)
70-79: 부품 주문 프로세스의 placeholder 구현으로 토스트 알림 미작동CartListViewModel의
processOrder()메서드(라인 84-89)가 placeholder 상태이므로isOrderSuccess가 true로 설정되지 않습니다. 결과적으로 CartListView의 onChange 감시자(라인 114-119)가 트리거되지 않아 토스트 "주문 성공!"이 표시되지 않습니다.구현 계획을 명확히 하거나, 최소한 다음 중 하나를 적용해주세요:
- 임시로
uiState.copy(isOrderSuccess: true)설정하여 UX 검증- TODO 배지 추가 또는 주문 버튼 비활성화 처리
CHOOSLA
left a comment
There was a problem hiding this comment.
백엔드 api 통신을 느슨한 결합을 위해 데이터 계층을 분리하셨군요
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (7)
SampoomManagement/Features/Cart/UI/CartListView.swift (1)
64-65: 하단 버튼을 위한 여백 처리 방식 개선을 고려하세요.
LazyVStack내부에 고정 높이Spacer를 사용하는 것은 비표준적이며 레이아웃 문제를 일으킬 수 있습니다.ScrollView의contentInsets또는safeAreaInsetmodifier를 사용하는 것이 더 적절합니다.다음과 같이 개선할 수 있습니다:
ScrollView { LazyVStack(spacing: 16) { ForEach(...) { ... } - Spacer() - .frame(height: 100) } .padding(.horizontal, 16) } +.safeAreaInset(edge: .bottom) { + Color.clear.frame(height: 80) +}SampoomManagement/Features/Outbound/UI/OutboundListView.swift (6)
41-45: 디버그성 코드 제거 또는 로컬라이즈bindLabel(error: "오류가 발생했습니다")는 현재 화면 어디에도 사용되지 않는 상수 문자열을 설정합니다. 불필요한 사이드 이펙트로 보이며 제거 또는 StringResources 기반 로컬라이즈 권장.
.onAppear { viewModel.clearSuccess() - viewModel.bindLabel(error: "오류가 발생했습니다") viewModel.onEvent(.loadOutboundList) }
64-79: Alert 두 개 체이닝 대신 하나로 통합 — 상태 기반 전환.alert를 두 번 체이닝하면 우선순위/표시 충돌 가능성이 있습니다. 단일 enum 상태로 통합해 관리하세요.
상태 추가(상단 @State 근처):
@State private var showEmptyOutboundDialog = false @State private var showConfirmDialog = false +@State private var activeAlert: AlertKind? + +private enum AlertKind: Identifiable { + case emptyAll, process + var id: Int { self.hashValue } +}버튼 트리거 변경과 단일 alert 적용:
-Button(StringResources.Outbound.emptyAll) { - showEmptyOutboundDialog = true -} +Button(StringResources.Outbound.emptyAll) { activeAlert = .emptyAll } -CommonButton(StringResources.Outbound.processOrder, backgroundColor: .red, textColor: .white) { - showConfirmDialog = true -} +CommonButton(StringResources.Outbound.processOrder, backgroundColor: .red, textColor: .white) { + activeAlert = .process +} -.alert(StringResources.Outbound.confirmEmptyTitle, isPresented: $showEmptyOutboundDialog) { ... } -... -.alert(StringResources.Outbound.confirmProcessTitle, isPresented: $showConfirmDialog) { ... } +.alert(item: $activeAlert) { kind in + switch kind { + case .emptyAll: + return Alert( + title: Text(StringResources.Outbound.confirmEmptyTitle), + message: Text(StringResources.Outbound.confirmEmptyMessage), + primaryButton: .default(Text(StringResources.Common.ok)) { viewModel.onEvent(.deleteAllOutbound) }, + secondaryButton: .cancel(Text(StringResources.Common.cancel)) + ) + case .process: + return Alert( + title: Text(StringResources.Outbound.confirmProcessTitle), + message: Text(StringResources.Outbound.confirmProcessMessage), + primaryButton: .default(Text(StringResources.Common.ok)) { viewModel.onEvent(.processOutbound) }, + secondaryButton: .cancel(Text(StringResources.Common.cancel)) + ) + } +}
109-110: 하드코딩된 문자열을 리소스로 이동"출고 항목이 없습니다"를 StringResources로 이전해 일관된 로컬라이제이션을 유지하세요.
- EmptyView( - icon: "tray", - title: "출고 항목이 없습니다" - ) + EmptyView( + icon: "tray", + title: StringResources.Outbound.emptyList // 키가 없다면 추가 필요 + )
141-146: 출고처리 버튼 동작 중 비활성화수량 업데이트/삭제(네트워크 요청) 중에는 주문 버튼을 잠시 비활성화해 중복 요청을 방지하세요.
CommonButton(StringResources.Outbound.processOrder, backgroundColor: .red, textColor: .white) { showConfirmDialog = true } .padding(.horizontal, 16) .padding(.bottom, 16) +.disabled(viewModel.uiState.isUpdating || viewModel.uiState.isDeleting)
132-147: 하단 고정 CTA는 safeAreaInset 사용 권장Spacer(height: 100) + ZStack 오버레이 대신 safeAreaInset(edge: .bottom)을 사용하면 안전 영역/홈 인디케이터/키보드와 자연스럽게 동작합니다.
예시:
ScrollView { ... } .safeAreaInset(edge: .bottom) { CommonButton(StringResources.Outbound.processOrder, backgroundColor: .red, textColor: .white) { showConfirmDialog = true } .padding(.horizontal, 16) .padding(.vertical, 12) .background(.ultraThinMaterial) }기존 Spacer(100)와 ZStack 하단 VStack은 제거 가능합니다.
216-236: 글로벌 isUpdating/isDeleting로 전체 리스트가 잠기는 UX단일 아이템 수정을 전체 리스트 비활성화로 처리하면 연속 작업이 막힙니다. 아이템 단위 in-flight 상태(예: Set updatingIds, deletingIds)를 ViewModel에서 관리하고, 각 아이템만 비활성화하도록 개선을 권장합니다.
간단한 방향:
- ViewModel: uiState에 updatingIds/deletingIds(Set) 추가, 요청별로 insert/remove.
- View: OutboundPartItem에 isUpdating: updatingIds.contains(part.outboundId), isDeleting: deletingIds.contains(part.outboundId) 전달.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
SampoomManagement/Core/Resources/StringResources.swift(2 hunks)SampoomManagement/Features/Cart/UI/CartListView.swift(1 hunks)SampoomManagement/Features/Outbound/UI/OutboundListView.swift(1 hunks)SampoomManagement/Features/Part/UI/PartDetailBottomSheetView.swift(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- SampoomManagement/Core/Resources/StringResources.swift
- SampoomManagement/Features/Part/UI/PartDetailBottomSheetView.swift
🧰 Additional context used
🧬 Code graph analysis (2)
SampoomManagement/Features/Outbound/UI/OutboundListView.swift (2)
SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift (8)
clearSuccess(226-228)bindLabel(38-40)onEvent(42-61)loadOutboundList(63-82)deleteAllOutbound(198-220)processOutbound(84-100)deleteOutbound(155-177)updateQuantity(102-124)SampoomManagement/Features/Outbound/Domain/Models/OutboundList.swift (1)
empty(21-23)
SampoomManagement/Features/Cart/UI/CartListView.swift (3)
SampoomManagement/Features/Cart/UI/CartListViewModel.swift (7)
onEvent(40-59)processOrder(84-90)deleteAllCart(188-210)loadCartList(61-81)clearSuccess(216-218)deleteCart(145-167)updateQuantity(92-114)SampoomManagement/Features/Cart/Data/Repository/CartRepositoryImpl.swift (2)
deleteAllCart(32-34)deleteCart(28-30)SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift (2)
deleteAllCart(68-77)deleteCart(43-52)
🪛 SwiftLint (0.57.0)
SampoomManagement/Features/Cart/UI/CartListView.swift
[Warning] 114-114: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
[Warning] 120-120: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
[Warning] 126-126: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
🔇 Additional comments (4)
SampoomManagement/Features/Cart/UI/CartListView.swift (3)
1-10: LGTM!파일 헤더와 임포트 구문이 적절합니다.
135-159: LGTM!
CartSection구현이 적절하며, 안정적인cartItemId를 식별자로 사용하고 있습니다.
161-225: LGTM!
CartPartItem구현이 올바르며, 수량 조절 버튼의 비활성화 로직이 적절합니다. optimistic update 중 사용자 상호작용을 방지하는 것이 좋습니다.SampoomManagement/Features/Outbound/UI/OutboundListView.swift (1)
46-63: 배포 타겟 확인 결과: 코드가 올바릅니다프로젝트의 iOS 배포 타겟이 18.6 및 26.0으로 설정되어 있어 iOS 17 이상입니다. 따라서 현재 사용 중인
onChange(of:perform:) { oldValue, newValue in }두 인수 클로저 시그니처는 배포 타겟에 완벽하게 호환되며 올바른 형태입니다. 변경이 필요하지 않습니다.
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (3)
SampoomManagement/Features/Cart/UI/CartListView.swift (3)
51-66: ForEach에서 indices 사용 금지: 안정 ID로 전환
장바구니가 변하는 동안 인덱스는 쉽게 무효화되어 크래시/오표시를 유발합니다. 카테고리/그룹의 고유 ID를 사용하세요.-ForEach(viewModel.uiState.cartList.indices, id: \.self) { categoryIndex in - let category = viewModel.uiState.cartList[categoryIndex] - ForEach(category.groups.indices, id: \.self) { groupIndex in - let group = category.groups[groupIndex] +ForEach(viewModel.uiState.cartList, id: \.categoryId) { category in + ForEach(category.groups, id: \.groupId) { group in CartSection( categoryName: category.categoryName, groupName: group.groupName, parts: group.parts,ID 필드가 없다면 모델에
id추가 후 해당 값을 사용하세요.
117-134: onChange의 미사용 매개변수_처리
첫 번째 인자(이전 값)를 사용하지 않으므로_로 치환해 경고를 제거하세요. iOS 17의 2-파라미터 시그니처 유지 OK.Based on learnings
-.onChange(of: viewModel.uiState.isOrderSuccess) { oldValue, newValue in +.onChange(of: viewModel.uiState.isOrderSuccess) { _, newValue in @@ -.onChange(of: viewModel.uiState.updateError) { oldValue, newValue in +.onChange(of: viewModel.uiState.updateError) { _, newValue in @@ -.onChange(of: viewModel.uiState.deleteError) { oldValue, newValue in +.onChange(of: viewModel.uiState.deleteError) { _, newValue in
229-229: 불필요한 닫는 중괄호 제거 — 컴파일 오류
파일 끝의 여분}를 삭제하세요.-}
🧹 Nitpick comments (14)
SampoomManagement/Features/Part/UI/PartDetailViewModel.swift (3)
45-54: 불필요한 nil 체크를 제거하세요.Lines 47과 52의
if part != nil체크는 불필요합니다.PartDetailBottomSheetView에서 이미guard let id = viewModel.uiState.part?.id else { return }로 검증하고 있으며, partId가 파라미터로 전달된 시점에는 이미 유효성이 보장됩니다.다음과 같이 단순화할 수 있습니다:
case .addToOutbound(let partId, let quantity): - let part = uiState.part - if part != nil { - addToOutbound(partId: partId, quantity: quantity) - } + addToOutbound(partId: partId, quantity: quantity) case .addToCart(let partId, let quantity): - let part = uiState.part - if part != nil { - addToCart(partId: partId, quantity: quantity) - } + addToCart(partId: partId, quantity: quantity)
66-89: MainActor.run 래핑이 불필요합니다.클래스 전체가
@MainActor로 어노테이션되어 있기 때문에, Task 내부의await MainActor.run { }래핑은 불필요합니다. 또한 print 문(lines 78, 86)은 적절한 로깅 시스템으로 대체하거나 제거하는 것이 좋습니다.다음과 같이 리팩토링할 수 있습니다:
private func addToOutbound(partId: Int, quantity: Int) { Task { - await MainActor.run { - uiState = uiState.copy(isUpdating: true, updateError: nil) - } + uiState = uiState.copy(isUpdating: true, updateError: nil) do { try await addOutboundUseCase.execute(partId: partId, quantity: quantity) - await MainActor.run { - uiState = uiState.copy(isUpdating: false, isOutboundSuccess: true) - } - print("PartDetailViewModel - addToOutbound success: \(uiState)") + uiState = uiState.copy(isUpdating: false, isOutboundSuccess: true) } catch { - await MainActor.run { - uiState = uiState.copy( - isUpdating: false, - updateError: error.localizedDescription - ) - } - print("PartDetailViewModel - addToOutbound error: \(error)") + uiState = uiState.copy( + isUpdating: false, + updateError: error.localizedDescription + ) } } }
91-114: MainActor.run 래핑이 불필요합니다.
addToOutbound와 동일한 문제가 있습니다. 클래스가 이미@MainActor이므로await MainActor.run { }래핑을 제거하고, print 문(lines 103, 111)은 적절한 로깅으로 대체하거나 제거하세요.다음과 같이 리팩토링할 수 있습니다:
private func addToCart(partId: Int, quantity: Int) { Task { - await MainActor.run { - uiState = uiState.copy(isUpdating: true, updateError: nil) - } + uiState = uiState.copy(isUpdating: true, updateError: nil) do { try await addCartUseCase.execute(partId: partId, quantity: quantity) - await MainActor.run { - uiState = uiState.copy(isUpdating: false, isCartSuccess: true) - } - print("PartDetailViewModel - addToCart success: \(uiState)") + uiState = uiState.copy(isUpdating: false, isCartSuccess: true) } catch { - await MainActor.run { - uiState = uiState.copy( - isUpdating: false, - updateError: error.localizedDescription - ) - } - print("PartDetailViewModel - addToCart error: \(error)") + uiState = uiState.copy( + isUpdating: false, + updateError: error.localizedDescription + ) } } }SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift (3)
22-22: 사용되지 않는 프로퍼티를 제거하세요.
errorLabel프로퍼티와bindLabel메서드가 선언되었지만 클래스 내 어디에서도 실제로 사용되지 않습니다.다음 diff를 적용하여 미사용 코드를 제거하세요:
- private var errorLabel: String = "" - init( getOutboundUseCase: GetOutboundUseCase, processOutboundUseCase: ProcessOutboundUseCase, @@ ... } - func bindLabel(error: String) { - errorLabel = error - } - func onEvent(_ event: OutboundListUiEvent) {Also applies to: 38-40
63-88: 이전 리뷰의 MainActor 이슈가 해결되었습니다.모든 UI 상태 업데이트가
await MainActor.run블록으로 올바르게 래핑되어 있습니다. 교차 액터 접근 문제가 수정되었습니다.다만, 86번 라인의
86-86: 디버그 print 문을 로깅 시스템으로 교체하는 것을 고려하세요.여러 위치에
로깅 레벨(debug, info, error)을 구분하고, 프로덕션에서는 불필요한 로그를 비활성화할 수 있습니다.
Also applies to: 110-110, 129-129, 139-139, 188-188, 198-198, 237-237, 247-247
SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift (2)
21-21: 엔드포인트의 에이전시 ID 하드코딩 제거 권장
"agency/1/..."하드코딩은 테스트/운영 전환 시 리스크입니다. DI 또는 메서드 파라미터로agencyId를 주입하세요.예시 diff:
-class CartAPI { +class CartAPI { private let networkManager: NetworkManager + private let agencyId: Int - init(networkManager: NetworkManager) { + init(networkManager: NetworkManager, agencyId: Int) { self.networkManager = networkManager + self.agencyId = agencyId } @@ - endpoint: "agency/1/cart", + endpoint: "agency/\(agencyId)/cart", @@ - endpoint: "agency/1/cart", + endpoint: "agency/\(agencyId)/cart", @@ - endpoint: "agency/1/cart/\(cartItemId)", + endpoint: "agency/\(agencyId)/cart/\(cartItemId)", @@ - endpoint: "agency/1/cart/\(cartItemId)", + endpoint: "agency/\(agencyId)/cart/\(cartItemId)", @@ - endpoint: "agency/1/cart/clear", + endpoint: "agency/\(agencyId)/cart/clear",Also applies to: 33-33, 46-46, 59-59, 72-72
25-25: 개발용 로그는 DEBUG 가드로 제한하세요
#if DEBUG로 감싸거나 통합 로거로 레벨 분리하세요.- print("CartAPI - getCartList response: \(response)") + #if DEBUG + print("CartAPI - getCartList response: \(response)") + #endif @@ - if !response.success { + if !response.success { throw NetworkError.serverError(response.status) }Also applies to: 38-40, 50-52, 64-66, 76-78
SampoomManagement/Features/Cart/UI/CartListViewModel.swift (2)
61-87: MainActor 업데이트 보일러플레이트 축소
Task { await MainActor.run { ... } }반복 대신Task { @MainActor in ... }블록을 부분 적용하면 가독성이 좋아집니다. 기능 변화는 없습니다.예시:
-Task { - await MainActor.run { - uiState = uiState.copy(cartLoading: true, cartError: nil) - } +Task { @MainActor in + uiState = uiState.copy(cartLoading: true, cartError: nil) // ... }Also applies to: 103-126, 162-185, 211-234
85-85: 디버그 로그는 DEBUG로 한정PII/상태 노출을 방지하기 위해
#if DEBUG로 감싸거나 로거로 대체하세요.- print("CartListViewModel - loadCartList: \(uiState)") + #if DEBUG + print("CartListViewModel - loadCartList: \(uiState)") + #endifAlso applies to: 113-114, 123-124, 172-173, 182-183, 221-222, 231-232
SampoomManagement/Features/Cart/UI/CartListView.swift (2)
67-71: 하단 버튼 겹침 방지: 매직 넘버 Spacer(100) 대신 safeAreaInset 사용
스크롤 콘텐츠 높이를 상수로 띄우는 방식은 기기/폰트/접근성에 취약합니다.-Spacer() - .frame(height: 100) +// 콘텐츠 자체를 인셋해 하단 버튼과 겹치지 않도록 처리 +.safeAreaInset(edge: .bottom) { + Color.clear.frame(height: 0) +} @@ -VStack { - Spacer() - CommonButton( ... ) - .padding(.horizontal, 16) - .padding(.bottom, 16) -} +safeAreaInset(edge: .bottom) { + CommonButton(StringResources.Cart.processOrder, + backgroundColor: .accentColor, + textColor: .white) { + showConfirmDialog = true + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background(.ultraThinMaterial) // 선택 +}Also applies to: 74-81
43-43: 표준EmptyView와 이름 충돌 가능성
프로젝트에 커스텀EmptyView(title:)가 있다면 표준 타입과 이름이 겹칩니다. 혼동/임포트 이슈 방지를 위해CartEmptyView등으로 개명 고려.SampoomManagement/Features/Cart/UI/CartListUiState.swift (2)
47-47:copy의selectedCart는 nil로 리셋이 불가
현재 파라미터가Cart?여서nil을 전달해도 원본이 유지됩니다. 다른 상태들과 일관되게Cart??로 바꿔 명시적 nil 설정을 지원하세요.- selectedCart: Cart? = nil, + selectedCart: Cart?? = nil, @@ - selectedCart: selectedCart ?? self.selectedCart, + selectedCart: selectedCart ?? self.selectedCart,(호출부에서
selectedCart: .some(nil)로 해제 가능)
10-19: 불필요하면Equatable채택 고려 (리렌더 최적화)
상태 비교가 잦다면CartListUiState: Equatable채택으로 변경 최소화가 용이합니다(전제: 하위 모델들도 Equatable).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
SampoomManagement/Core/Network/NetworkError.swift(4 hunks)SampoomManagement/Core/Network/NetworkManager.swift(1 hunks)SampoomManagement/Core/UI/Components/CommonButton.swift(4 hunks)SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift(1 hunks)SampoomManagement/Features/Cart/UI/CartListUiState.swift(1 hunks)SampoomManagement/Features/Cart/UI/CartListView.swift(1 hunks)SampoomManagement/Features/Cart/UI/CartListViewModel.swift(1 hunks)SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift(1 hunks)SampoomManagement/Features/Outbound/UI/OutboundListUiState.swift(1 hunks)SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift(1 hunks)SampoomManagement/Features/Part/UI/PartDetailBottomSheetView.swift(1 hunks)SampoomManagement/Features/Part/UI/PartDetailUiState.swift(1 hunks)SampoomManagement/Features/Part/UI/PartDetailViewModel.swift(1 hunks)SampoomManagement/Features/Part/UI/PartListUiState.swift(1 hunks)SampoomManagement/Features/Part/UI/PartListViewModel.swift(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
- SampoomManagement/Core/Network/NetworkError.swift
- SampoomManagement/Features/Part/UI/PartDetailBottomSheetView.swift
- SampoomManagement/Core/UI/Components/CommonButton.swift
- SampoomManagement/Core/Network/NetworkManager.swift
- SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift
- SampoomManagement/Features/Outbound/UI/OutboundListUiState.swift
- SampoomManagement/Features/Part/UI/PartListUiState.swift
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-11T12:58:56.474Z
Learnt from: Sangyoon98
PR: 33-Auto/Sampoom-Management-iOS#7
File: SampoomManagement/Core/UI/Components/CommonTextField.swift:117-119
Timestamp: 2025-10-11T12:58:56.474Z
Learning: In Swift/SwiftUI iOS 17.0+, the onChange(of:perform:) modifier expects a two-parameter closure (oldValue, newValue) or zero-parameter closure, not a single-parameter closure. The older single-parameter version was deprecated in iOS 17.0.
Applied to files:
SampoomManagement/Features/Cart/UI/CartListView.swift
🧬 Code graph analysis (8)
SampoomManagement/Features/Cart/UI/CartListViewModel.swift (8)
SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift (7)
bindLabel(38-40)onEvent(42-61)updateQuantity(114-142)updateLocalQuantity(144-171)removeFromLocalList(203-220)removeAllFromLocalList(252-254)clearSuccess(256-258)SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift (2)
deleteCart(44-53)deleteAllCart(70-79)SampoomManagement/Features/Cart/Data/Repository/CartRepositoryImpl.swift (2)
deleteCart(28-30)deleteAllCart(32-34)SampoomManagement/Features/Cart/UI/CartListUiState.swift (1)
copy(43-65)SampoomManagement/Features/Cart/Domain/UseCase/DeleteAllCartUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Domain/UseCase/UpdateCartQuantityUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Domain/UseCase/GetCartUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Domain/UseCase/DeleteCartUseCase.swift (1)
execute(17-19)
SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift (3)
SampoomManagement/Features/Cart/Data/Repository/CartRepositoryImpl.swift (4)
getCartList(17-21)addCart(23-26)deleteCart(28-30)deleteAllCart(32-34)SampoomManagement/Core/Network/NetworkManager.swift (1)
request(18-57)SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift (1)
toDictionary(96-99)
SampoomManagement/Features/Part/UI/PartDetailViewModel.swift (4)
SampoomManagement/Features/Part/UI/PartDetailUiState.swift (1)
copy(34-50)SampoomManagement/Features/Part/UI/PartDetailBottomSheetView.swift (4)
increaseQuantity(11-11)decreaseQuantity(10-10)addToOutbound(12-16)addToCart(17-21)SampoomManagement/Features/Cart/Domain/UseCase/AddCartUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Outbound/Domain/UseCase/AddOutboundUseCase.swift (1)
execute(17-19)
SampoomManagement/Features/Cart/UI/CartListView.swift (1)
SampoomManagement/Features/Cart/UI/CartListViewModel.swift (7)
onEvent(40-59)processOrder(90-96)deleteAllCart(206-234)loadCartList(61-87)clearSuccess(240-242)deleteCart(157-185)updateQuantity(98-126)
SampoomManagement/Features/Part/UI/PartDetailUiState.swift (3)
SampoomManagement/Features/Cart/UI/CartListUiState.swift (1)
copy(43-65)SampoomManagement/Features/Outbound/UI/OutboundListUiState.swift (1)
copy(43-65)SampoomManagement/Features/Part/UI/PartListUiState.swift (1)
copy(28-40)
SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift (9)
SampoomManagement/Features/Cart/UI/CartListViewModel.swift (7)
bindLabel(36-38)onEvent(40-59)updateQuantity(98-126)updateLocalQuantity(128-155)removeFromLocalList(187-204)removeAllFromLocalList(236-238)clearSuccess(240-242)SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift (3)
processOutbound(44-53)deleteOutbound(56-65)deleteAllOutbound(82-91)SampoomManagement/Features/Outbound/Data/Repository/OutboundRepositoryImpl.swift (3)
processOutbound(23-25)deleteOutbound(32-34)deleteAllOutbound(36-38)SampoomManagement/Features/Outbound/UI/OutboundListUiState.swift (1)
copy(43-65)SampoomManagement/Features/Outbound/Domain/UseCase/DeleteAllOutboundUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Outbound/Domain/UseCase/DeleteOutboundUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Outbound/Domain/UseCase/GetOutboundUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Outbound/Domain/UseCase/ProcessOutboundUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Outbound/Domain/UseCase/UpdateOutboundQuantityUseCase.swift (1)
execute(17-19)
SampoomManagement/Features/Part/UI/PartListViewModel.swift (2)
SampoomManagement/Features/Part/UI/PartListUiState.swift (1)
copy(28-40)SampoomManagement/Features/Part/UI/PartUIState.swift (1)
copy(43-61)
SampoomManagement/Features/Cart/UI/CartListUiState.swift (3)
SampoomManagement/Features/Outbound/UI/OutboundListUiState.swift (1)
copy(43-65)SampoomManagement/Features/Part/UI/PartDetailUiState.swift (1)
copy(34-50)SampoomManagement/Features/Part/UI/PartListUiState.swift (1)
copy(28-40)
🔇 Additional comments (11)
SampoomManagement/Features/Part/UI/PartListViewModel.swift (3)
32-40: 바텀시트 이벤트 처리 로직이 올바르게 구현되었습니다.
selectedPart를 통한 바텀시트 표시/해제 로직이 정확합니다..some(nil)패턴은copy메서드의 이중 옵셔널 파라미터에서 "필드를 nil로 설정"하는 올바른 방식입니다.
50-50: 이중 옵셔널 패턴 사용이 적절합니다.
.some(nil)패턴은copy메서드에서 "필드를 업데이트하지 않음"과 "필드를 nil로 설정"을 구분하기 위한 올바른 Swift 패턴입니다.Also applies to: 59-59
43-74: 동시성 처리가 올바르게 구현되었습니다.
@MainActor어노테이션과 함께await MainActor.run을 적절히 사용하여 UI 업데이트의 스레드 안전성을 보장하고 있습니다. Task 취소 처리도 정확합니다.SampoomManagement/Features/Part/UI/PartDetailUiState.swift (1)
10-51: 이전 리뷰 피드백이 반영되었습니다!Double-optional 패턴(
Part??,String??)이 copy 메서드에 올바르게 적용되어 있습니다. 이제 "값 변경 없음"과 "명시적으로 nil 설정"을 구분할 수 있으며, 다른 UI state 구조체들(CartListUiState, OutboundListUiState)과도 일관성을 유지하고 있습니다.SampoomManagement/Features/Part/UI/PartDetailViewModel.swift (1)
12-22: 의존성 주입 패턴이 잘 구현되었습니다.@mainactor 어노테이션과 ObservableObject 프로토콜 준수가 적절하며, use case 의존성이 명확하게 주입되고 있습니다.
SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift (3)
56-60: 이전 리뷰의 에러 초기화 이슈가 해결되었습니다.
.some(nil)패턴을 올바르게 사용하여 에러 상태를 명시적으로 초기화하고 있습니다. 이전 리뷰에서 지적된 문제가 수정되었습니다.
90-112: 성공 후 리스트 갱신 타이밍을 검토하세요.101번 라인에서
loadOutboundList()를await없이 호출하고 있습니다.loadOutboundList()가 내부적으로 Task를 생성하므로 기술적으로는 동작하지만,isOrderSuccess = true상태가 리스트 새로고침이 완료되기 전에 설정됩니다.사용자가 성공 상태를 보는 시점에 리스트가 아직 업데이트되지 않았을 수 있습니다.
다음과 같이 수정을 고려해보세요:
do { try await processOutboundUseCase.execute() - await MainActor.run { - uiState = uiState.copy(outboundLoading: false, isOrderSuccess: true) - } - loadOutboundList() // 성공 후 리스트 새로고침 + loadOutboundList() // 성공 후 리스트 새로고침 + await MainActor.run { + uiState = uiState.copy(outboundLoading: false, isOrderSuccess: true) + } } catch {또는 리스트 새로고침 완료를 기다려야 한다면:
do { try await processOutboundUseCase.execute() await MainActor.run { uiState = uiState.copy(outboundLoading: false, isOrderSuccess: true) } - loadOutboundList() // 성공 후 리스트 새로고침 + await loadOutboundListAsync() // 별도의 awaitable 메서드 추가 필요 } catch {
256-258: LGTM!성공 상태를 초기화하는 간단하고 올바른 구현입니다.
SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift (2)
30-41: nil 직렬화 방지 가드 추가 확인 — 잘 처리됨
toDictionary()결과를guard로 언래핑해 빈 바디 전송을 막은 점 좋습니다.Also applies to: 56-67
31-31:NetworkError.invalidParameters케이스가 존재합니다—검토 요청 해결됨코드 검증 결과,
NetworkError.invalidParameters는SampoomManagement/Core/Network/NetworkError.swift16번 줄에 정의되어 있으며, 30번 줄에 로컬라이즈된 에러 설명("잘못된 매개변수입니다")도 올바르게 구현되어 있습니다. CartAPI.swift 31번 줄과 57번 줄의 사용이 모두 정상입니다.Likely an incorrect or invalid review comment.
SampoomManagement/Features/Cart/UI/CartListViewModel.swift (1)
98-126: 수량 업데이트 낙관적 적용 — 패턴 적절로컬 선적용 후 실패 시 롤백(load) 전략이 일관적입니다.
실패 케이스에서 서버 응답 지연이 길면 잦은 재로딩이 UX에 영향을 줄 수 있습니다. 필요 시, 실패 항목만 부분 롤백하는 전략도 고려해 주세요.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (9)
SampoomManagement/Core/UI/Components/CommonButton.swift (1)
82-105: 코드 중복을 줄이는 것을 고려해보세요.leading과 trailing 위치의 커스텀 아이콘 렌더링 로직이 동일하게 중복되어 있습니다. 헬퍼 메서드나
@ViewBuilder를 사용하여 공통 로직을 추출하면 유지보수가 더 쉬워집니다.예시 리팩토링:
@ViewBuilder private func iconView(for position: IconPosition) -> some View { if let customIcon = customIcon, iconPosition == position { Image(customIcon) .renderingMode(.template) .resizable() .scaledToFit() .frame(width: size.height * 0.5, height: size.height * 0.5) } else if let icon = icon, iconPosition == position { Image(systemName: icon) .font(size.font) } }그런 다음 HStack에서:
HStack(spacing: 8) { iconView(for: .leading) Text(title) .font(.gmarketBody) iconView(for: .trailing) }SampoomManagement/Features/Cart/UI/CartListViewModel.swift (6)
10-10: 사용하지 않는 import 제거
Combine을 import했지만 파일 내에서 사용되지 않습니다.@Published는 Foundation/SwiftUI에서 제공되므로 이 import는 불필요합니다.-import Combine
55-81: 불필요한MainActor.run호출 제거클래스 전체가
@MainActor로 선언되어 있으므로await MainActor.run호출이 불필요합니다. 모든 메서드는 이미 메인 스레드에서 실행됩니다. 또한 79번 라인의private func loadCartList() { Task { - await MainActor.run { - uiState = uiState.copy(cartLoading: true, cartError: nil) - } + uiState = uiState.copy(cartLoading: true, cartError: nil) do { let cartList = try await getCartUseCase.execute() - await MainActor.run { - uiState = uiState.copy( - cartList: cartList.items, - cartLoading: false, - cartError: nil - ) - } + uiState = uiState.copy( + cartList: cartList.items, + cartLoading: false, + cartError: nil + ) } catch { - await MainActor.run { - uiState = uiState.copy( - cartLoading: false, - cartError: error.localizedDescription - ) - } + uiState = uiState.copy( + cartLoading: false, + cartError: error.localizedDescription + ) } - print("CartListViewModel - loadCartList: \(uiState)") + // TODO: Use proper logging mechanism } }
92-149: 불필요한MainActor.run제거 및 print 문 교체
updateQuantity와 관련 메서드에서도 동일하게await MainActor.run이 불필요합니다. 또한 107, 117번 라인의 print 문은 적절한 로깅으로 교체하는 것이 좋습니다.private func updateQuantity(cartItemId: Int, quantity: Int) { updateLocalQuantity(cartItemId: cartItemId, quantity: quantity) Task { - await MainActor.run { - uiState = uiState.copy(isUpdating: true, updateError: nil) - } + uiState = uiState.copy(isUpdating: true, updateError: nil) do { try await updateCartQuantityUseCase.execute(cartItemId: cartItemId, quantity: quantity) - await MainActor.run { - uiState = uiState.copy(isUpdating: false) - } - print("CartListViewModel - updateQuantity success: \(uiState)") + uiState = uiState.copy(isUpdating: false) + // TODO: Use proper logging } catch { - await MainActor.run { - uiState = uiState.copy( - isUpdating: false, - updateError: error.localizedDescription - ) - } + uiState = uiState.copy( + isUpdating: false, + updateError: error.localizedDescription + ) loadCartList() - print("CartListViewModel - updateQuantity error: \(error)") + // TODO: Use proper logging } } }
151-179: 불필요한MainActor.run제거 및 print 문 교체
deleteCart메서드에서도await MainActor.run이 불필요하며, 166, 176번 라인의 print 문은 적절한 로깅으로 교체해야 합니다.private func deleteCart(cartItemId: Int) { removeFromLocalList(cartItemId: cartItemId) Task { - await MainActor.run { - uiState = uiState.copy(isDeleting: true, deleteError: nil) - } + uiState = uiState.copy(isDeleting: true, deleteError: nil) do { try await deleteCartUseCase.execute(cartItemId: cartItemId) - await MainActor.run { - uiState = uiState.copy(isDeleting: false) - } - print("CartListViewModel - deleteCart success: \(uiState)") + uiState = uiState.copy(isDeleting: false) + // TODO: Use proper logging } catch { - await MainActor.run { - uiState = uiState.copy( - isDeleting: false, - deleteError: error.localizedDescription - ) - } + uiState = uiState.copy( + isDeleting: false, + deleteError: error.localizedDescription + ) loadCartList() - print("CartListViewModel - deleteCart error: \(error)") + // TODO: Use proper logging } } }
200-228: 불필요한MainActor.run제거 및 print 문 교체
deleteAllCart메서드에서도 동일한 리팩토링이 필요합니다.private func deleteAllCart() { removeAllFromLocalList() Task { - await MainActor.run { - uiState = uiState.copy(isDeleting: true, deleteError: nil) - } + uiState = uiState.copy(isDeleting: true, deleteError: nil) do { try await deleteAllCartUseCase.execute() - await MainActor.run { - uiState = uiState.copy(isDeleting: false) - } - print("CartListViewModel - deleteAllCart success: \(uiState)") + uiState = uiState.copy(isDeleting: false) + // TODO: Use proper logging } catch { - await MainActor.run { - uiState = uiState.copy( - isDeleting: false, - deleteError: error.localizedDescription - ) - } + uiState = uiState.copy( + isDeleting: false, + deleteError: error.localizedDescription + ) loadCartList() - print("CartListViewModel - deleteAllCart error: \(error)") + // TODO: Use proper logging } } }
12-237: OutboundListViewModel과의 코드 중복 고려이 ViewModel은
OutboundListViewModel과 구조가 거의 동일합니다 (optimistic update 패턴, 에러 처리, 상태 관리 등). 향후 유지보수성을 위해 공통 로직을 generic base class나 protocol extension으로 추상화하는 것을 고려해볼 수 있습니다. 다만 현재 구현은 각 도메인의 요구사항을 명확하게 표현하고 있어 당장 리팩토링이 필수적이지는 않습니다.향후 리팩토링이 필요하다고 판단되면, generic list management 패턴 구현을 도와드릴 수 있습니다.
SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift (2)
80-80: 디버깅 로그를 위한 로깅 프레임워크 사용을 고려하세요.현재
OSLog,CocoaLumberjack등)를 사용하는 것이 좋습니다.예시:
+import os.log + +private let logger = Logger(subsystem: "com.sampoom.management", category: "OutboundList") + private func loadOutboundList() { Task { // ... - print("OutboundListViewModel - loadOutboundList: \(uiState)") + logger.debug("loadOutboundList completed: \(String(describing: uiState))") } }Also applies to: 104-104, 123-123, 133-133, 182-182, 192-192, 231-231, 241-241
108-136: 낙관적 업데이트 패턴에 대한 문서화를 고려하세요.
updateQuantity,deleteOutbound,deleteAllOutbound메서드들이 일관된 낙관적 업데이트 패턴(로컬 상태 먼저 변경 → 서버 동기화 → 실패 시 롤백)을 구현하고 있습니다. 이 패턴을 이해하기 쉽도록 메서드나 클래스 레벨에 문서 주석을 추가하면 유지보수성이 향상됩니다.예시:
/// 낙관적 UI 업데이트를 수행합니다. /// 1. 로컬 상태를 즉시 업데이트하여 빠른 UI 반응 제공 /// 2. 백그라운드에서 서버와 동기화 /// 3. 서버 요청 실패 시 에러 표시 후 서버 상태로 롤백 private func updateQuantity(outboundId: Int, quantity: Int) { // ... }Also applies to: 167-195, 216-244
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
SampoomManagement/Core/Resources/StringResources.swift(2 hunks)SampoomManagement/Core/UI/Components/AppHeader.swift(0 hunks)SampoomManagement/Core/UI/Components/CommonButton.swift(4 hunks)SampoomManagement/Core/UI/Components/CommonTextField.swift(0 hunks)SampoomManagement/Core/UI/Components/ErrorView.swift(0 hunks)SampoomManagement/Core/UI/Components/LoadingView.swift(0 hunks)SampoomManagement/Features/Cart/UI/CartListView.swift(1 hunks)SampoomManagement/Features/Cart/UI/CartListViewModel.swift(1 hunks)SampoomManagement/Features/Outbound/UI/OutboundListView.swift(1 hunks)SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift(1 hunks)
💤 Files with no reviewable changes (4)
- SampoomManagement/Core/UI/Components/CommonTextField.swift
- SampoomManagement/Core/UI/Components/ErrorView.swift
- SampoomManagement/Core/UI/Components/LoadingView.swift
- SampoomManagement/Core/UI/Components/AppHeader.swift
🚧 Files skipped from review as they are similar to previous changes (3)
- SampoomManagement/Features/Outbound/UI/OutboundListView.swift
- SampoomManagement/Features/Cart/UI/CartListView.swift
- SampoomManagement/Core/Resources/StringResources.swift
🧰 Additional context used
🧬 Code graph analysis (2)
SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift (8)
SampoomManagement/Features/Cart/UI/CartListViewModel.swift (6)
onEvent(34-53)updateQuantity(92-120)updateLocalQuantity(122-149)removeFromLocalList(181-198)removeAllFromLocalList(230-232)clearSuccess(234-236)SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift (3)
processOutbound(44-53)deleteOutbound(56-65)deleteAllOutbound(82-91)SampoomManagement/Features/Outbound/UI/OutboundListUiState.swift (1)
copy(43-65)SampoomManagement/Features/Outbound/Domain/UseCase/DeleteAllOutboundUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Outbound/Domain/UseCase/DeleteOutboundUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Outbound/Domain/UseCase/GetOutboundUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Outbound/Domain/UseCase/UpdateOutboundQuantityUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Outbound/Domain/UseCase/ProcessOutboundUseCase.swift (1)
execute(17-19)
SampoomManagement/Features/Cart/UI/CartListViewModel.swift (8)
SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift (6)
onEvent(36-55)updateQuantity(108-136)updateLocalQuantity(138-165)removeFromLocalList(197-214)removeAllFromLocalList(246-248)clearSuccess(250-252)SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift (2)
deleteCart(44-53)deleteAllCart(70-79)SampoomManagement/Features/Cart/Data/Repository/CartRepositoryImpl.swift (2)
deleteCart(28-30)deleteAllCart(32-34)SampoomManagement/Features/Cart/UI/CartListUiState.swift (1)
copy(43-65)SampoomManagement/Features/Cart/Domain/UseCase/DeleteCartUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Domain/UseCase/GetCartUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Domain/UseCase/DeleteAllCartUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Cart/Domain/UseCase/UpdateCartQuantityUseCase.swift (1)
execute(17-19)
🔇 Additional comments (3)
SampoomManagement/Core/UI/Components/CommonButton.swift (1)
82-91: 이전에 지적된 문제가 수정되었습니다. 하지만 아이콘 크기 일관성을 확인해야 합니다.커스텀 이미지에
.font()대신.resizable(),.scaledToFit(),.frame()을 사용하도록 올바르게 수정되었습니다.다만 커스텀 아이콘과 시스템 아이콘의 크기 계산 방식이 다릅니다:
- 커스텀 아이콘:
size.height * 0.5→ 16/22/26 pt (small/medium/large)- 시스템 아이콘:
.font(size.font)→ 14/16/18 pt실제 화면에서 두 아이콘 타입의 시각적 크기가 일관되게 보이는지 확인해주세요.
SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift (2)
50-54: 에러 초기화 로직이 올바르게 수정되었습니다.이전 리뷰에서 지적된 double optional 이슈가
.some(nil)패턴을 사용하여 정확하게 해결되었습니다. 이제clearUpdateError와clearDeleteError이벤트가 의도한 대로 에러 상태를 명시적으로 지웁니다.
57-244: 비동기 처리 및 롤백 로직이 올바르게 개선되었습니다.이전 리뷰에서 지적된 모든 동시성 이슈가 해결되었습니다:
MainActor 보장: 모든
uiState변경이await MainActor.run { ... }내에서 실행되어 교차-액터 접근 문제가 해결되었습니다.롤백 순서 개선:
updateQuantity(132번 라인),deleteOutbound(191번 라인),deleteAllOutbound(240번 라인)에서 에러 상태를 먼저 설정한 후loadOutboundList()를 호출하는 패턴으로 변경되어, 사용자에게 에러 메시지가 즉시 표시되고 롤백은 백그라운드에서 처리됩니다.이 패턴은
CartListViewModel과도 일관성 있게 적용되었습니다.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (2)
SampoomManagement/Features/Part/UI/PartDetailBottomSheetView.swift (2)
12-21: id 안전 처리 확인 (0 전송 방지).nil 가드로 잘 막았습니다. 이전 코멘트가 적절히 반영되었습니다.
48-59: 알럿 즉시 닫고 액션 실행으로 중복 탭 방지 OK.확인 탭 시 먼저 알럿을 닫아 재입력 차단하는 흐름이 적절합니다. 이전 코멘트 해결 확인했습니다.
🧹 Nitpick comments (6)
SampoomManagement/Features/Part/UI/PartDetailBottomSheetView.swift (3)
77-79: 재고 초과 입력 방지: + 버튼 비활성 조건에 재고 상한 포함 권장.현재는 isUpdating만 체크합니다. 사용자가 보유 수량을 초과해 증가하지 않도록 클라이언트에서도 가드해 주세요. 서버 검증은 유지하되, UX 차원에서 버튼을 비활성화하는 것이 좋습니다.
재고 상한 반영 예시:
- isIncreaseDisabled: viewModel.uiState.isUpdating + isIncreaseDisabled: viewModel.uiState.isUpdating + || (viewModel.uiState.part.map { viewModel.uiState.quantity >= $0.quantity } ?? false)추가로, 상한 도달 시 토스트나 미세한 진동 피드백을 주면 더 좋습니다.
100-104: 수량 입력값 정제 및 UI/상태 동기화 개선.숫자가 아닌 문자를 입력하거나 "0", 빈 문자열일 때 모델은 갱신되지 않아 필드와 상태가 어긋날 수 있습니다. 숫자만 허용하고 공백/0은 최소 1로 보정하는 정제가 필요합니다.
- private func handleQuantityTextChange(_ newText: String) { - if let q = Int(newText), q > 0 { - viewModel.onEvent(.setQuantity(q)) - } - } + private func handleQuantityTextChange(_ newText: String) { + // 숫자만 허용 + let digits = newText.filter(\.isNumber) + if digits != newText { + quantityText = digits + } + if digits.isEmpty { + quantityText = "1" + viewModel.onEvent(.setQuantity(1)) + return + } + if let q = Int(digits), q > 0 { + viewModel.onEvent(.setQuantity(q)) + } + }대안: TextField(value:format:) 또는 Formatter 활용해 숫자만 받도록 구성.
62-92: BottomSheet 내부 NavigationView 사용 재검토.시트 내부에 별도 NavigationView를 두지 않아도 되면 제거를, 내비가 필요하면 iOS 16+에서 NavigationStack으로 전환을 고려해 주세요.
타겟 iOS 버전을 알려주시면 적합한 권장안을 확정하겠습니다.
SampoomManagement/Features/Part/UI/PartListView.swift (3)
12-21: 시트 표시 상태 단일 소스로 관리(.sheet 바인딩 단순화).showBottomSheet(로컬)와 selectedPart(ViewModel) 이중 상태는 드리프트 위험이 있습니다. selectedPart 파생 바인딩으로 시트를 제어하면 더 안정적입니다.
- @State private var showBottomSheet = false + // 로컬 상태 제거: selectedPart 파생 바인딩으로 대체 @@ - PartListItemCard( + PartListItemCard( part: part, onClick: { viewModel.onEvent(.showBottomSheet(part)) - showBottomSheet = true } ) @@ - .sheet(isPresented: $showBottomSheet) { - if let selectedPart = viewModel.uiState.selectedPart { - let detailViewModel = dependencies.makePartDetailViewModel() - PartDetailBottomSheetView(viewModel: detailViewModel) - .onAppear { - detailViewModel.onEvent(.initialize(selectedPart)) - } - .onDisappear { - showBottomSheet = false - viewModel.onEvent(.dismissBottomSheet) - } - .presentationDetents([.fraction(0.3)]) - .presentationDragIndicator(.visible) - .presentationBackground(.clear) - } - } + .sheet(isPresented: Binding( + get: { viewModel.uiState.selectedPart != nil }, + set: { if !$0 { viewModel.onEvent(.dismissBottomSheet) } } + )) { + if let selectedPart = viewModel.uiState.selectedPart { + let detailViewModel = dependencies.makePartDetailViewModel() + PartDetailBottomSheetView(viewModel: detailViewModel) + .onAppear { detailViewModel.onEvent(.initialize(selectedPart)) } + .onDisappear { viewModel.onEvent(.dismissBottomSheet) } + .presentationDetents([.fraction(0.3)]) + .presentationDragIndicator(.visible) + .presentationBackground(.clear) + } + }추가 개선(선택):
- Part가 Identifiable이면 .sheet(item:) 사용으로 더 간결화 가능.
- 시트 내부 ViewModel 재생성 방지를 위해 컨테이너 뷰에서 @StateObject로 보존 권장(아래 코멘트 참고).
Also applies to: 56-62, 72-87
74-76: 시트 콘텐츠에서 ViewModel 재생성 가능성 — @StateObject로 아이덴티티 고정 권장..body 재평가 시 매번 makePartDetailViewModel() 호출로 새 인스턴스가 만들어질 수 있습니다. 컨테이너에서 @StateObject로 생성/보존하세요.
컨테이너 예시(파일 외부 지원 코드):
struct PartDetailSheetContainer: View { @StateObject private var vm: PartDetailViewModel let part: Part init(dependencies: AppDependencies, part: Part) { _vm = StateObject(wrappedValue: dependencies.makePartDetailViewModel()) self.part = part } var body: some View { PartDetailBottomSheetView(viewModel: vm) .onAppear { vm.onEvent(.initialize(part)) } } }사용:
- let detailViewModel = dependencies.makePartDetailViewModel() - PartDetailBottomSheetView(viewModel: detailViewModel) - .onAppear { detailViewModel.onEvent(.initialize(selectedPart)) } + PartDetailSheetContainer(dependencies: dependencies, part: selectedPart)
83-85: Detent 유연성 확보 제안.단일 .fraction(0.3)은 컨텐츠가 잘릴 수 있습니다. medium 추가를 고려해 주세요.
- .presentationDetents([.fraction(0.3)]) + .presentationDetents([.fraction(0.35), .medium])
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
SampoomManagement/Features/Part/UI/PartDetailBottomSheetView.swift(1 hunks)SampoomManagement/Features/Part/UI/PartListView.swift(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
SampoomManagement/Features/Part/UI/PartDetailBottomSheetView.swift (1)
SampoomManagement/Features/Part/UI/PartDetailViewModel.swift (4)
onEvent(24-64)addToOutbound(66-89)addToCart(91-114)clearSuccess(116-118)
SampoomManagement/Features/Part/UI/PartListView.swift (3)
SampoomManagement/Features/Part/UI/PartListViewModel.swift (1)
onEvent(30-41)SampoomManagement/Features/Part/UI/PartDetailViewModel.swift (1)
onEvent(24-64)SampoomManagement/Core/DI/AppDependencies.swift (1)
makePartDetailViewModel(113-115)
🔇 Additional comments (3)
SampoomManagement/Features/Part/UI/PartDetailBottomSheetView.swift (1)
106-121: 성공/에러 핸들링 흐름 적절.성공 시 토스트 → 다이얼로그 정리 → 성공 플래그 초기화, 에러 시 토스트 후 에러 클리어 흐름이 명확합니다.
SampoomManagement/Features/Part/UI/PartListView.swift (2)
27-33: 로딩 UI 중앙 정렬 처리 적절.Spacer로 ProgressView 중앙 배치 + 배경색 지정 흐름 깔끔합니다.
96-124: 카드 인터랙션 단순화 및 접근성 개선 LGTM.Button 래핑 + PlainButtonStyle로 하이라이트 제어, 텍스트/수량/체브론 레이아웃이 명확합니다.
📝 Summary
출고목록, 장바구니, 출고처리 로직, 장바구니 담기 로직 구현
🙏 Question & PR point
📬 Reference
Summary by CodeRabbit
새로운 기능
개선 사항