Files
superport_v2/doc/API_CLIENT_SPEC.md

6.0 KiB

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

본 문서는 Superport 레포 스타일과 동일한 인증/네트워킹 패턴을 본 프로젝트에 적용하기 위한 ApiClient 설계를 정의한다. 실제 구현은 이후 단계에서 진행한다(문서 선정리).

1) 목표

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

2) 의존성(추가 예정)

  • dio: ^5.x (HTTP 클라이언트)
  • get_it: ^7.x (DI) — 이미 사용 중
  • flutter_secure_storage(or web localStorage 대체): 토큰 저장(플랫폼별 분기)
  • intl: ^0.20.x (기존)
  • 개발 전용: pretty_dio_logger(선택)

3) 환경 변수

  • API_BASE_URL: 예) https://api.example.com/api/v1
  • API_CONNECT_TIMEOUT_MS: 예) 15000
  • API_RECEIVE_TIMEOUT_MS: 예) 30000
  • LOG_LEVEL: debug|info|warn|error

로드 순서: await Environment.initialize() → DI에서 ApiClient 생성 시 사용

4) 인증 방식(슈퍼포트와 동일)

  • 로그인: POST /auth/login{ data: { token: string, user?: {...} } }
  • 요청 헤더: Authorization: Bearer <token>
  • 토큰 저장: 보안 저장소(모바일)/localStorage(웹) 또는 httpOnly 쿠키(백엔드 정책에 따름)
  • 토큰 갱신(선택): POST /auth/refresh{ data: { token: string } }
  • 401 처리: AuthInterceptor가 401 수신 시 자동 갱신 → 원요청 재시도(1회). 갱신 실패 시 로그아웃/세션 초기화 및 로그인 화면 이동

5) 에러 매핑 정책

  • 400 BAD_REQUEST: 검증 오류 → 필드 에러로 매핑
  • 404 NOT_FOUND: 리소스 없음
  • 409 CONFLICT: 유니크 충돌/상태 충돌
  • 422 UNPROCESSABLE_ENTITY: 비즈니스 규칙 위반(예: 출고 고객 미선택, blocking 전이)
  • 500+: 서버 오류 → 공통 메시지 + 로그 수집
  • 표준 포맷: { error: { code, message, details? } } 수용. 비표준 응답은 DioException 메시지로 대체

6) 쿼리 규약/헬퍼

  • 페이지네이션: page, page_size
  • 정렬: sort, order=asc|desc
  • 검색: q
  • 증분: updated_since
  • include 확장: include=lines,customers,approval
  • 헬퍼: buildQuery({page, pageSize, q, sort, order, include, filters})

7) ApiClient 스켈레톤(인터페이스)

/// 네트워크 공통 클라이언트 (Dio 래퍼)
class ApiClient {
  // 내부 Dio 인스턴스(외부 사용 금지, 필요한 경우 read-only 게터 제공)
  final Dio _dio;

  ApiClient({required Dio dio}) : _dio = dio;

  Dio get dio => _dio; // 과도한 사용은 지양하고, 가능하면 아래 헬퍼 사용

  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,
  });
}

구현 시 기본 옵션

  • BaseOptions: baseUrl, connectTimeout, receiveTimeout
  • 공통 헤더: Accept: application/json, Authorization: Bearer <token?>
  • Interceptors:
    • AuthInterceptor(요청 전 토큰 주입, 401에서 갱신/재시도)
    • LoggingInterceptor(개발 모드에서만)

8) Interceptor 설계

  • AuthInterceptor
    • 요청: 저장된 토큰이 있으면 Authorization 헤더 추가
    • 응답: 401이면 1) 갱신 중 동시성 잠금 2) 갱신 성공 시 대기 중 요청 재시도 3) 실패 시 토큰 삭제/로그아웃
  • Retry 정책: 재시도는 1회, idempotent GET/HEAD 위주. POST/PATCH는 401 갱신 후 재시도 1회만 허용

9) 표준 응답 파서

  • 목록: { items: [...], page, page_size, total }
  • 단건: { data: {...} }
  • 제네릭 파서 유틸 제공: parseList<T>(res, fromJson), parseItem<T>(res, fromJson)

10) 샘플 사용 (Repository)

class VendorRepositoryImpl implements VendorRepository {
  final ApiClient api;
  VendorRepositoryImpl(this.api);

  @override
  Future<Paged<Vendor>> list({int page = 1, int pageSize = 20, String? q}) async {
    final res = await api.get('/vendors', query: { 'page': page, 'page_size': pageSize, if (q != null) 'q': q });
    return parseList<Vendor>(res.data, Vendor.fromJson);
  }

  @override
  Future<Vendor> create(VendorCreate body) async {
    final res = await api.post('/vendors', data: body.toJson());
    return parseItem<Vendor>(res.data, Vendor.fromJson);
  }
}

11) 보안/스토리지

  • 토큰 저장: 플랫폼별로 적합한 저장소 사용(웹은 localStorage, 모바일은 secure storage)
  • 민감정보 로깅 금지(토큰/쿠키 마스킹)
  • CORS/쿠키 기반 인증 사용 시, Dio 요청에 withCredentials=true 설정 필요(백엔드 정책에 따름)

12) 테스트 전략

  • 위젯/도메인 테스트: 네트워크 의존 제거(리포지토리를 테스트 더블로 대체)
  • 통합 테스트: 실제 스테이징 API를 사용하여 로그인→호출→401→갱신→재시도 플로우 검증

13) 구현 순서 요약(체크)

  • pubspec에 dio(필수), pretty_dio_logger(개발) 추가
  • ApiClient/AuthInterceptor 스켈레톤 작성
  • Environment.initialize()get_it DI에서 ApiClient 생성/주입
  • 리포지토리 구현에서 ApiClient 사용으로 통일(직접 Dio 인스턴스화 금지)
  • 에러/토큰/재시도 정책 위젯 레벨 연결(토스트/로그아웃)

참고

  • Superport 레포: .envAPI_BASE_URL, test_api_integration.sh/auth/login + Bearer 사용
  • 본 프로젝트: AGENTS.md의 “Do not use mock data” 및 DI/레이어 경계 정책 준수