Skip to content

[FEAT] 출고목록, 장바구니, 출고처리 로직, 장바구니 담기 로직 구현#14

Merged
Sangyoon98 merged 5 commits intodevfrom
SPM-130
Oct 20, 2025
Merged

[FEAT] 출고목록, 장바구니, 출고처리 로직, 장바구니 담기 로직 구현#14
Sangyoon98 merged 5 commits intodevfrom
SPM-130

Conversation

@Sangyoon98
Copy link
Copy Markdown
Member

@Sangyoon98 Sangyoon98 commented Oct 20, 2025

📝 Summary

출고목록, 장바구니, 출고처리 로직, 장바구니 담기 로직 구현

🙏 Question & PR point

📬 Reference

Summary by CodeRabbit

  • 새로운 기능

    • 출고(Outbound) 목록 화면 추가 — 조회, 수량 수정, 삭제, 전체 비우기, 출고 처리
    • 장바구니(Cart) 목록 화면 추가 — 조회, 수량 변경, 삭제, 전체 비우기
    • 부품 상세 바텀시트 추가 — 출고·장바구니로 바로 추가 가능
  • 개선 사항

    • 앱 탭명·아이콘을 Delivery → Outbound로 변경
    • 부품 목록에 바텀시트 흐름 도입 및 상호작용 개선
    • 공통 버튼에 커스텀 아이콘 지원 추가

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Oct 20, 2025

Walkthrough

앱 탭에서 Delivery를 Outbound로 대체하고 Cart 및 Outbound 기능을 추가했으며, 네트워크 레이어를 completion 콜백에서 async/await로 마이그레이션했습니다. Part 목록에 바텀시트 흐름을 연결하고 DI에 관련 뷰모델 팩토리를 추가했습니다.

Changes

Cohort / File(s) 변경 요약
앱 진입점 / 리소스
SampoomManagement/App/ContentView.swift, SampoomManagement/Core/Resources/StringResources.swift, SampoomManagement/Resources/Assets.xcassets/outbound.imageset/Contents.json
탭 enum deliveryoutbound 변경, 탭 라벨/아이콘 리소스 갱신, OutboundListView/CartListView로 자리 변경
DI 및 팩토리
SampoomManagement/Core/DI/AppDependencies.swift
Outbound/Cart API·Repository·UseCase 추가 및 초기화, PartDetail/OutboundList/CartList 뷰모델 팩토리 메서드 추가
네트워크 공통
SampoomManagement/Core/Network/NetworkManager.swift, SampoomManagement/Core/Network/APIResponse.swift, SampoomManagement/Core/Network/NetworkError.swift
NetworkManager.request를 async/throws로 리팩터링(continuation 브리지), APIResponse.data 옵셔널화, EmptyResponse 추가, 에러 케이스·로그 추가
공통 UI 컴포넌트
SampoomManagement/Core/UI/Components/CommonButton.swift
customIcon: String? 추가 및 커스텀 아이콘 우선 렌더링 지원
Auth 계층
SampoomManagement/Features/Auth/.../AuthAPI.swift, .../AuthRepositoryImpl.swift
API 호출을 await 기반으로 단순화, Repository에서 response.data nil 체크 후 AuthError.invalidResponse 처리
Outbound 전층 (DTO→API→Repo→UseCase→VM→UI)
SampoomManagement/Features/Outbound/...
(DTOs, OutboundAPI.swift, OutboundMappers.swift, OutboundRepositoryImpl.swift, Domain/Models/*, Domain/Repository/OutboundRepository.swift, UseCase들, OutboundListUiState.swift, OutboundListUiEvent.swift, OutboundListViewModel.swift, OutboundListView.swift)
Outbound 관련 DTO/모델/매퍼/API/리포지토리/유스케이스/뷰모델/뷰 추가 및 UI(확인 대화상자, 토스트, optimistic 업데이트) 구현
Cart 전층 (DTO→API→Repo→UseCase→VM→UI)
SampoomManagement/Features/Cart/...
(DTOs, CartAPI.swift, CartMappers.swift, CartRepositoryImpl.swift, Domain/Models/*, Domain/Repository/CartRepository.swift, UseCase들, CartListUiState.swift, CartListUiEvent.swift, CartListViewModel.swift, CartListView.swift)
Cart 관련 DTO/모델/매퍼/API/레포지토리/유스케이스/뷰모델/뷰 추가 및 UI(확인·토스트·optimistic 업데이트) 구현
Part 화면 통합 및 바텀시트
SampoomManagement/Features/Part/...
(PartAPI.swift, PartListView.swift, PartListViewModel.swift, PartDetailBottomSheetView.swift, PartDetailViewModel.swift, UI 이벤트/상태 파일들)
PartAPI의 async/await 변환, PartList에 selectedPart/바텀시트 흐름 추가, PartDetail 바텀시트 뷰·뷰모델·이벤트/상태 추가, PartListView에 dependencies 주입
DTO/모델/매퍼/유스케이스/뷰모델/뷰 추가(여러 파일)
여러 Features/{Outbound,Cart,Part}/.../*.swift
여러 도메인에 걸친 신규 타입·파일 대량 추가(데이터 모델, 매퍼, API, 레포지토리, 유스케이스, UI 상태/이벤트/뷰모델/뷰)
프리뷰 제거(비기능)
SampoomManagement/Core/UI/Components/{AppHeader,CommonTextField,ErrorView,LoadingView}.swift
SwiftUI Preview 블록 제거 (런타임 API 변경 없음)

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 업데이트 (화면 갱신)
Loading
%%{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: 성공/에러 플래그 업데이트 → 토스트/알림
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

평가 근거: 네트워크 레이어의 async/await 브리지, DI와 ContentView 시그니처 변경, Outbound/Cart/Part 전체 계층에 걸친 많은 신규 파일 및 ViewModel의 optimistic 업데이트/롤백 로직이 있어 집중 검토 필요.

Possibly related PRs

Suggested reviewers

  • Lee-Jong-Jin
  • taemin3
  • yangjiseonn
  • CHOOSLA

"🐰 탭이 바뀌자 길이 열렸네,
바구니와 출고가 함께 뛰고,
비동기 바람에 데이터 춤추며,
바텀시트에 당근 하나 톡,
코드 밭에서 깡충 웃음 짓네. 🥕"

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed PR 제목 "[FEAT] 출고목록, 장바구니, 출고처리 로직, 장바구니 담기 로직 구현"은 변경 사항의 핵심 목표를 정확하게 반영합니다. 제목에서 언급된 네 가지 주요 기능(출고목록, 장바구니, 출고처리 로직, 장바구니 담기 로직)은 모두 raw summary에서 확인되는 변경 사항과 일치합니다. Outbound와 Cart 관련 완전한 데이터 레이어(API, Repository, UseCase), UI 레이어(ViewModel, View), 그리고 Part Detail에서의 통합 기능이 실제로 구현되어 있습니다. 제목은 구체적이고 명확하며 이전 "delivery" 탭을 "outbound" 탭으로 변경하는 주요 UI 변경도 포함된 범위를 잘 대표합니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch SPM-130

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (40)
SampoomManagement/Features/Outbound/Domain/Models/OutboundList.swift (1)

10-19: 중복 프로퍼티를 계산 프로퍼티로 변경 고려

totalCountisEmpty가 저장 프로퍼티로 정의되어 있지만, 이들은 items.countitems.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 throws
SampoomManagement/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 사용 고려

totalCountisEmptyitems로부터 계산 가능한 값입니다. 현재 구조도 문제없지만, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 220f2f1 and 498f732.

⛔ Files ignored due to path filters (1)
  • SampoomManagement/Resources/Assets.xcassets/outbound.imageset/outbound.svg is 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: 도메인 모델 구조 승인

출고 도메인을 위한 깔끔한 계층 구조입니다. OutboundOutboundGroupOutboundPart의 중첩 구조가 명확하며, 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: 바텀 시트 이벤트 처리가 올바르게 구현되었습니다!

showBottomSheetdismissBottomSheet 이벤트 핸들러가 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 레이아웃 개선이 좋습니다!

에러 상태와 빈 상태를 HStackSpacer()로 감싸 중앙 정렬한 것은 더 나은 사용자 경험을 제공합니다. 구현이 깔끔하고 일관성 있습니다.

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 배지 추가 또는 주문 버튼 비활성화 처리

Comment thread SampoomManagement/Core/Network/NetworkManager.swift Outdated
Comment thread SampoomManagement/Core/UI/Components/CommonButton.swift
Comment thread SampoomManagement/Core/UI/Components/CommonButton.swift
Comment thread SampoomManagement/Features/Cart/Data/Remote/API/CartAPI.swift
Comment thread SampoomManagement/Features/Outbound/Data/Remote/API/OutboundAPI.swift Outdated
Comment thread SampoomManagement/Features/Part/UI/PartDetailBottomSheetView.swift
Comment thread SampoomManagement/Features/Part/UI/PartDetailBottomSheetView.swift Outdated
Comment thread SampoomManagement/Features/Part/UI/PartDetailUiState.swift
Comment thread SampoomManagement/Features/Part/UI/PartDetailViewModel.swift Outdated
Comment thread SampoomManagement/Features/Part/UI/PartListUiState.swift Outdated
Copy link
Copy Markdown
Member

@CHOOSLA CHOOSLA left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

백엔드 api 통신을 느슨한 결합을 위해 데이터 계층을 분리하셨군요

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (7)
SampoomManagement/Features/Cart/UI/CartListView.swift (1)

64-65: 하단 버튼을 위한 여백 처리 방식 개선을 고려하세요.

LazyVStack 내부에 고정 높이 Spacer를 사용하는 것은 비표준적이며 레이아웃 문제를 일으킬 수 있습니다. ScrollViewcontentInsets 또는 safeAreaInset modifier를 사용하는 것이 더 적절합니다.

다음과 같이 개선할 수 있습니다:

 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

📥 Commits

Reviewing files that changed from the base of the PR and between 498f732 and b71c31c.

📒 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 } 두 인수 클로저 시그니처는 배포 타겟에 완벽하게 호환되며 올바른 형태입니다. 변경이 필요하지 않습니다.

Comment thread SampoomManagement/Features/Cart/UI/CartListView.swift Outdated
Comment thread SampoomManagement/Features/Cart/UI/CartListView.swift Outdated
Comment thread SampoomManagement/Features/Cart/UI/CartListView.swift
Comment thread SampoomManagement/Features/Outbound/UI/OutboundListView.swift
Comment thread SampoomManagement/Features/Outbound/UI/OutboundListView.swift Outdated
Copy link
Copy Markdown

@vivivim vivivim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

승인 담기

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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번 라인의 print 문은 프로덕션 환경에서 적절한 로깅 시스템으로 교체하는 것을 권장합니다.


86-86: 디버그 print 문을 로깅 시스템으로 교체하는 것을 고려하세요.

여러 위치에 print 문이 사용되고 있습니다. 개발 중에는 유용하지만, 프로덕션 환경에서는 적절한 로깅 프레임워크(예: OSLog, SwiftLog)로 교체하는 것을 권장합니다.

로깅 레벨(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 가드로 제한하세요

print로 응답/상태를 그대로 출력하면 PII 노출 위험이 있습니다. #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/상태 노출을 방지하기 위해 print들을 #if DEBUG로 감싸거나 로거로 대체하세요.

- print("CartListViewModel - loadCartList: \(uiState)")
+ #if DEBUG
+ print("CartListViewModel - loadCartList: \(uiState)")
+ #endif

Also 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: copyselectedCart는 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

📥 Commits

Reviewing files that changed from the base of the PR and between b71c31c and 3217936.

📒 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.invalidParametersSampoomManagement/Core/Network/NetworkError.swift 16번 줄에 정의되어 있으며, 30번 줄에 로컬라이즈된 에러 설명("잘못된 매개변수입니다")도 올바르게 구현되어 있습니다. CartAPI.swift 31번 줄과 57번 줄의 사용이 모두 정상입니다.

Likely an incorrect or invalid review comment.

SampoomManagement/Features/Cart/UI/CartListViewModel.swift (1)

98-126: 수량 업데이트 낙관적 적용 — 패턴 적절

로컬 선적용 후 실패 시 롤백(load) 전략이 일관적입니다.

실패 케이스에서 서버 응답 지연이 길면 잦은 재로딩이 UX에 영향을 줄 수 있습니다. 필요 시, 실패 항목만 부분 롤백하는 전략도 고려해 주세요.

Comment thread SampoomManagement/Features/Cart/UI/CartListViewModel.swift Outdated
Comment thread SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift
Comment thread SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift
Comment thread SampoomManagement/Features/Outbound/UI/OutboundListViewModel.swift
Comment thread SampoomManagement/Features/Part/UI/PartListViewModel.swift
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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번 라인의 print 문은 적절한 로깅 메커니즘으로 교체하는 것이 좋습니다.

 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: 디버깅 로그를 위한 로깅 프레임워크 사용을 고려하세요.

현재 print 문으로 상태 변경을 추적하고 있습니다. 프로덕션 환경에서는 로그 레벨 제어, 필터링, 구조화된 로깅 등의 기능을 제공하는 로깅 프레임워크(예: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3217936 and cba2bc4.

📒 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) 패턴을 사용하여 정확하게 해결되었습니다. 이제 clearUpdateErrorclearDeleteError 이벤트가 의도한 대로 에러 상태를 명시적으로 지웁니다.


57-244: 비동기 처리 및 롤백 로직이 올바르게 개선되었습니다.

이전 리뷰에서 지적된 모든 동시성 이슈가 해결되었습니다:

  1. MainActor 보장: 모든 uiState 변경이 await MainActor.run { ... } 내에서 실행되어 교차-액터 접근 문제가 해결되었습니다.

  2. 롤백 순서 개선: updateQuantity (132번 라인), deleteOutbound (191번 라인), deleteAllOutbound (240번 라인)에서 에러 상태를 먼저 설정한 후 loadOutboundList()를 호출하는 패턴으로 변경되어, 사용자에게 에러 메시지가 즉시 표시되고 롤백은 백그라운드에서 처리됩니다.

이 패턴은 CartListViewModel과도 일관성 있게 적용되었습니다.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between cba2bc4 and ec819c3.

📒 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로 하이라이트 제어, 텍스트/수량/체브론 레이아웃이 명확합니다.

Copy link
Copy Markdown

@taemin3 taemin3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다!

@Sangyoon98 Sangyoon98 merged commit 10099d4 into dev Oct 20, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants