응답 규약

모든 LinkOStar endpoint 는 일관된 envelope로 응답합니다. 통합 코드 한 곳에서 unwrap 유틸을 둬 두면 모든 호출이 같은 함수를 통과합니다.

1. Single resource — { data }

HTTP 200 OK
{
  "data": {
    "hubUuid": "...",
    "tenantUuid": "...",
    "createdAt": "2026-06-01T12:00:00Z",
    ...
  }
}

존재하지 않는 ID에 대한 GET은 404 + error envelope (아래).

2. List + Pagination — { data, pagination }

HTTP 200 OK
{
  "data": [
    { "hubUuid": "...", ... },
    { "hubUuid": "...", ... }
  ],
  "pagination": {
    "page": 1,
    "size": 20,
    "totalElements": 137,
    "totalPages": 7
  }
}

Query 파라미터는 page (1-based), size. 응답의 totalElements 가 BFF의 카운트 집계에 가장 자주 쓰입니다.

3. 빈 body (204 No Content)

Telemetry 수집 (POST /v1/hubs/{uuid}/telemetry), claim 코드 revoke, 일부 mutation 성공이 여기 해당. body 없음. 클라이언트는 .json()호출 전에 status 또는 content-length 확인 필수.

4. Error envelope (RFC 7807 problem+json)

Backend Spring Boot 가 ProblemDetail 형식으로 통일.

HTTP 4xx / 5xx
Content-Type: application/problem+json

{
  "type":      "https://api.linkostar.com/errors/validation-failed",
  "title":     "Validation Failed",
  "status":    400,
  "detail":    "claimCode is required",
  "instance":  "/v1/hubs/claim",
  "traceId":   "abc123...",
  "timestamp": "2026-06-04T12:34:56Z"
}

주요 status:

HTTP의미대응
400입력 형식/유효성 오류응답 detail 확인 후 입력 수정
401자격증명 없음/만료토큰 갱신 후 재시도
403자격은 유효한데 권한 없음역할/tenant scope 확인
404리소스 없음ID 확인
409충돌 (예: 이미 존재)응답 detail 확인 후 분기
423Locked (예: blocked hub)운영팀 문의
429Rate limitbackoff + retry
500서버 오류 (transient 가능)traceId 확인 후 재시도; 반복되면 운영팀

5. TypeScript unwrap 유틸 예시

interface ApiEnvelope<T> {
  data?: T;
  pagination?: { page: number; size: number; totalElements: number; totalPages: number };
}

async function apiList<T>(path: string): Promise<{ items: T[]; total: number }> {
  const res = await fetch(`${BASE}${path}`, { headers: { 'X-API-Key': KEY } });
  if (!res.ok) throw new ApiError(await res.json());
  const env = await res.json() as ApiEnvelope<T[]>;
  return {
    items: env.data ?? [],
    total: env.pagination?.totalElements ?? (env.data?.length ?? 0),
  };
}

6. Java unwrap 유틸 예시

record Envelope<T>(T data, Pagination pagination) {}
record Pagination(int page, int size, long totalElements, int totalPages) {}

public <T> long countViaList(String path, ParameterizedTypeReference<Envelope<List<T>>> tref) {
    Envelope<List<T>> env = restClient
        .get().uri(path + "?page=1&size=1")
        .retrieve()
        .body(tref);
    return env.pagination() != null ? env.pagination().totalElements() : 0L;
}

7. 호환성 약속

  • 이 envelope shape은 breaking change 대상이 아닙니다 — 기존 키는 유지하고 새 키만 추가됩니다.
  • data 안의 entity 키는 endpoint별로 진화. 추가는 minor, 제거/이름변경은 major (Changelog 공지).
  • 알 수 없는 key를 만나면 무시 하세요. throw 하지 마세요.