Files
superport_v2/doc/API_CLIENT_SPEC.md
JiWoong Sul 47cc62a33d feat(inventory): 재고 현황 요약/상세 플로우를 릴리스
- lib/features/inventory/summary 계층과 warehouse select 위젯을 추가해 목록/상세, 자동 새로고침, 필터, 상세 시트를 구현

- PermissionBootstrapper, scope 파서, 라우트 가드로 inventory.view 기반 권한 부여와 메뉴 노출을 통합(lib/core, lib/main.dart 등)

- Inventory Summary API/QA/Audit 문서와 PR 템플릿, CHANGELOG를 신규 스펙과 검증 커맨드로 업데이트

- DTO 직렬화 의존성을 추가하고 Golden·Widget·단위 테스트를 작성했으며 flutter analyze / flutter test --coverage를 통과
2025-11-09 01:13:10 +09:00

9.1 KiB

ApiClient 설계서 (Dio 기반, Superport 스타일)

Implementation Snapshot (2025-10-31)

  • ApiClient/ApiErrorMapper/AuthInterceptor가 구현되어 모든 원격 저장소가 공통 경로를 사용한다 (lib/core/network/api_client.dart, lib/core/network/api_error.dart, lib/core/network/interceptors/auth_interceptor.dart).
  • DI/환경 설정은 Environment.initialize() 이후 lib/injection_container.dart에서 ApiClient와 TokenStorage, 인터셉터를 등록한다.
  • 단위 테스트가 경로·쿼리·에러 매핑·토큰 재발급 동작을 검증한다 (test/core/network/api_client_test.dart, test/core/network/auth_interceptor_test.dart).
  • 문서/코드가 doc/stock_approval_system_api_v4.md 계약과 동기화됐으며, 엔드포인트별 Remote Repository 테스트가 include·필터 직렬화를 검증한다.

1) 목표

  • 단일 진입점 ApiClient(Dio 래퍼)로 모든 네트워크 호출 일원화
  • 환경 변수 기반 BaseURL/타임아웃/로그 레벨 설정
  • 인증 토큰 주입, 401 자동 처리(토큰 갱신 → 재시도), 에러 매핑 일관화
  • 목록/단건 표준 응답 구조에 맞춘 헬퍼 제공 (구현 완료)

2) 의존성

  • dio: ^5.x, get_it: ^7.x, flutter_secure_storage, intl, pretty_dio_logger(dev) — pubspec.yaml에 반영됨.
  • 테스트: mocktail, flutter_test (already configured).

3) 환경 변수

  • API_BASE_URL, API_CONNECT_TIMEOUT_MS, API_RECEIVE_TIMEOUT_MS, LOG_LEVEL
  • 로드 순서: await Environment.initialize()injection_container.dart에서 ApiClient 생성
  • .env.* 파일에 샘플 값이 포함되어 있으며, FeatureFlags.initialize() 이전에 호출된다.

4) 인증 스택

  • 로그인/토큰 재발급 플로우는 Superport 백엔드와 동일 (POST /auth/login, POST /auth/refresh).
  • AuthInterceptor가 저장소에서 토큰을 읽어 Authorization 헤더를 주입하고, 401 발생 시 리프레시 콜백을 호출해 한 번만 재시도한다 (lib/core/network/interceptors/auth_interceptor.dart:45).
  • TokenStorage는 플랫폼별 저장소(web/local) 구현을 제공한다 (lib/core/network/services/token_storage.dart).

5) 에러 매핑 정책

  • ApiErrorMapperDioExceptionFailure로 변환해 코드/메시지를 표준화한다 (lib/core/network/api_error.dart).
  • HTTP 상태별 매핑: 400(검증), 401(세션 만료), 403(권한), 404(리소스 없음), 409(충돌), 422(업무 규칙), 500+(서버 오류).
  • 테스트 test/core/network/api_client_test.dart:62가 매핑 결과를 검증한다.

6) 쿼리 헬퍼와 규약

  • ApiClient.buildQuery가 페이지네이션(page,page_size), 정렬(sort,order), 검색(q), 증분(updated_since), include, 맞춤 필터를 직렬화한다 (lib/core/network/api_client.dart:25).
  • buildPath가 세그먼트를 안전하게 결합한다 (lib/core/network/api_client.dart:14).
  • 모든 Remote Repository는 해당 헬퍼를 사용하도록 테스트로 강제된다 (예: test/features/approvals/data/approval_repository_remote_test.dart:42).

7) ApiClient 인터페이스

class ApiClient {
  Future<Response<T>> get<T>(String path, {Map<String, dynamic>? query, Options? options, CancelToken? cancelToken});
  Future<Response<T>> post<T>(String path, {dynamic data, Map<String, dynamic>? query, Options? options, CancelToken? cancelToken});
  Future<Response<T>> patch<T>(String path, {dynamic data, Map<String, dynamic>? query, Options? options, CancelToken? cancelToken});
  Future<Response<T>> delete<T>(String path, {dynamic data, Map<String, dynamic>? query, Options? options, CancelToken? cancelToken});
  static Map<String, dynamic> buildQuery({...});
  static String buildPath(Object base, [Iterable<Object?> segments = const []]);
}

8) 인터셉터 구성

  • AuthInterceptor: 토큰 주입 + 401 재시도, 동시 갱신 방지 큐 적용.
  • LoggingInterceptor: 개발 모드에서만 pretty 출력 (lib/core/network/interceptors/logging_interceptor.dart).
  • RetryInterceptor는 필요 시 idempotent 요청 재시도를 담당한다 (lib/core/network/interceptors/retry_interceptor.dart).

9) 표준 응답 파서

  • 목록: { items, page, page_size, total }PaginatedResult<T> (lib/core/common/models/paginated_result.dart).
  • 단건: { data: {...} }ApiClient.unwrapAsMap/unwrap 헬퍼가 추출한다 (lib/core/network/api_client.dart:91).

10) 사용 예시

final response = await _api.get<Map<String, dynamic>>(
  ApiRoutes.approvals,
  query: ApiClient.buildQuery(page: page, pageSize: pageSize, include: ['requested_by']),
);
return ApprovalDto.parsePaginated(response.data ?? const {});

11) 보안/스토리지

  • 웹: localStorage, 모바일: secure storage — TokenStorage가 추상화.
  • 민감정보 로깅 금지, 개발 모드에서만 pretty_dio_logger 활성화.
  • 쿠키 기반 인증 시 dio.options.extra['withCredentials']=true를 사용하도록 확장 가능.

12) 테스트 전략

  • 단위 테스트: test/core/network/api_client_test.dart, test/core/network/auth_interceptor_test.dart에서 경로·쿼리·에러·토큰 재시도를 검증.
  • 기능 테스트: 각 Remote Repository 테스트가 파라미터 직렬화를 검증하고, 통합 테스트(integration_test/approvals_flow_test.dart)가 실제 플로우를 검증한다.

13) 구현 체크리스트

  • dio 및 보조 패키지 의존성을 추가했다.
  • ApiClient/AuthInterceptor/ApiErrorMapper를 구현하고 테스트했다.
  • Environment.initialize() 이후 DI에서 ApiClient와 인터셉터를 등록한다 (lib/injection_container.dart:60).
  • 모든 Remote Repository가 ApiClient를 사용하도록 마이그레이션했다.
  • 에러/토큰/재시도 정책을 위젯 및 도메인 테스트에 연결했다.
  • 문서와 코드가 동기화되었으며, 변경 시 tool/sync_stock_docs.sh --check를 사용한다.

14) Inventory Summary API (신규)

14.1 목록 GET /api/v1/inventory/summary

  • 권한: scope:inventory.view + menu_code=inventory can_read=true
  • Query 파라미터
    • page, page_size (기본 1/50, 최대 200)
    • q, product_name, vendor_name
    • warehouse_id, include_empty, updated_since
    • sort: last_event_at|product_name|vendor_name|total_quantity
    • order: asc|desc
  • 응답 스키마
{
  "items": [
    {
      "product": {
        "id": 101,
        "product_code": "INV-DEMO-001",
        "product_name": "QA 데모 장비",
        "vendor": { "id": 10, "vendor_name": "Inventory Demo Vendor" }
      },
      "total_quantity": 145,
      "warehouse_balances": [
        {
          "warehouse": {
            "id": 1,
            "warehouse_code": "INV-QA-A",
            "warehouse_name": "QA 1센터"
          },
          "quantity": 115
        }
      ],
      "recent_event": {
        "event_id": 15001,
        "event_kind": "issue",
        "event_label": "출고",
        "delta_quantity": -20,
        "counterparty": { "type": "customer", "name": "Inventory QA 고객" },
        "warehouse": { "id": 1, "warehouse_code": "INV-QA-A", "warehouse_name": "QA 1센터" },
        "transaction": { "id": 9100, "transaction_no": "INV-ISS-001" },
        "occurred_at": "2025-10-24T02:58:00Z"
      },
      "updated_at": "2025-10-24T03:12:00Z"
    }
  ],
  "page": 1,
  "page_size": 50,
  "total": 1
}
  • 오류: 403 INVENTORY_SCOPE_REQUIRED, 409 INVENTORY_SNAPSHOT_NOT_READY

14.2 단건 GET /api/v1/inventory/summary/{product_id}

  • Query: event_limit(1~100, 기본 20), warehouse_id
  • 응답
{
  "data": {
    "product": { "id": 101, "product_code": "INV-DEMO-001", "product_name": "QA 데모 장비",
      "vendor": { "id": 10, "vendor_name": "Inventory Demo Vendor" }
    },
    "total_quantity": 145,
    "warehouse_balances": [
      {
        "warehouse": { "id": 1, "warehouse_code": "INV-QA-A", "warehouse_name": "QA 1센터" },
        "quantity": 115
      }
    ],
    "recent_events": [
      {
        "event_id": 15001,
        "event_kind": "issue",
        "event_label": "출고",
        "delta_quantity": -20,
        "counterparty": { "type": "customer", "name": "Inventory QA 고객" },
        "warehouse": { "id": 1, "warehouse_code": "INV-QA-A", "warehouse_name": "QA 1센터" },
        "transaction": { "id": 9100, "transaction_no": "INV-ISS-001" },
        "line": { "id": 12001, "line_no": 1, "quantity": 20 },
        "occurred_at": "2025-10-24T02:58:00Z"
      }
    ],
    "updated_at": "2025-10-24T03:12:00Z",
    "last_refreshed_at": "2025-10-24T03:10:00Z"
  }
}
  • 오류: 403 INVENTORY_SCOPE_REQUIRED, 409 INVENTORY_SNAPSHOT_NOT_READY, 404 NOT_FOUND

14.3 프런트 TODO

  • DTO/JSON 직렬화: InventorySummaryResponse, InventoryDetailResponsebuild_runner 재생성
  • 상태관리: InventorySummaryController, InventoryDetailController (Pagination, 필터, event_limit)
  • UI: 리스트(테이블) + 상세 시트, warehouse_balances 시각화, recent_event 배지
  • 테스트: 위젯/Golden/통합 + flutter analyze, flutter test --coverage