API 명세 - 100-hours-a-week/KTB4-5th-wiki GitHub Wiki

재고 인식 결과 콜백 API 명세

AI가 사진 한 장의 분석을 종료하면 BE에 결과 또는 실패 정보를 전달한다.

1. 요청 경로

POST /api/v1/internal/image-analyses/callback

요청 헤더

헤더 타입 필수 설명
Content-Type String O application/json
X-Internal-API-Key String O AI→BE 서비스 인증 키

2. 요청 본문

공통 필드

필드 타입 필수 null 허용 설명
analysisId String O X AI가 발급한 분석 ID
status String O X COMPLETED 또는 FAILED
completedAt String O X 시간대를 포함한 ISO 8601 분석 종료 시각
result Object O O COMPLETED이면 결과 객체, FAILED이면 null
error Object O O COMPLETED이면 null, FAILED이면 오류 객체

status의 의미

두 상태 모두 작업 처리가 종료됐음을 의미하며, 정상 결과 생성 여부를 구분한다.

상태 의미 예시
COMPLETED 분석 절차가 정상적으로 종료되어 결과를 생성함 상품 인식, 일부 정보 누락, 인식 항목 없음
FAILED 오류로 분석을 정상적으로 수행하지 못해 결과를 생성하지 못함 모델 장애, 이미지 접근 실패, 지원하지 않는 이미지 형식
  • 상품을 인식하지 못했더라도 분석 절차가 정상적으로 끝났으면 COMPLETED로 전달한다.
  • 인식 항목이 없으면 result.items를 빈 배열로 전달한다.
  • 소비기한 등 일부 값이 누락돼도 COMPLETED로 전달할 수 있다.
  • FAILED는 시스템 장애뿐 아니라 입력 이미지 처리 오류도 포함한다.
  • 항목별 확인 필요 여부는 items[].displayStatus로 전달한다.

성공 결과: result

필드 타입 필수 설명
documentType String O RECEIPT, PRODUCT 또는 OTHER
items Array O 인식 항목 목록

PRODUCT 결과도 콜백으로 수신·보관한다. v2에서 PRODUCT 사용자 기능을 제공한다는 의미는 아니다.

인식 항목: result.items[]

필드 타입 필수 null 허용 설명
itemId String O X 해당 분석 안에서 항목을 구분하는 ID
displayStatus String O X RECOGNIZED, UNRECOGNIZED, NEEDS_REVIEW, AI_ESTIMATED
name String O O 상품명. 원본 인식명과 정규화명 모두 없으면 null
category String O O 카테고리 목록에 정의한 값. 판단하지 못하면 null
storageType String O O REFRIGERATED 또는 FROZEN. 판단하지 못하면 null
measureType String O O COUNT 또는 WEIGHT. 판단하지 못하면 null
quantity Integer O O COUNT에서 인식한 개수는 1~100. 수량 미인식 또는 COUNT가 아니면 null
weight Object O X 중량·용량 정보
expiration Object O X 소비기한 정보
reviewReasons Array O X 해당 상품에서 사용자가 확인할 사유 객체 목록. 없으면 빈 배열

상품명은 원본 인식 상품명을 우선 사용한다. 원본 인식 상품명이 비어 있으면 정규화한 상품명을 사용한다. 둘 다 비어 있으면 name은 null로 전달한다.

중량·용량: weight

필드 타입 필수 null 허용 설명
value Number O O WEIGHT에서 인식한 중량·용량은 양수. 값 미인식 또는 WEIGHT가 아니면 null
unit String O O WEIGHT이면 G 또는 ML이며, 단위 미인식이면 null. COUNT이면 NONE. 측정 방식이 불명확하면 null

소비기한: expiration

필드 타입 필수 null 허용 설명
date String O O YYYY-MM-DD. 확인되지 않으면 null
dateType String O X USE_BY, UNKNOWN, ESTIMATED_CONSUMPTION_WINDOW
estimatedRange Object O O 예상 기간. 없으면 null
estimatedRange.from String 조건부 X 예상 기간이 있을 때 필수. YYYY-MM-DD
estimatedRange.to String 조건부 X 예상 기간이 있을 때 필수. YYYY-MM-DD
  • 예상 기간의 시작일은 종료일보다 늦을 수 없다.
  • 예상 기간은 확정 소비기한과 구분한다.
  • dateType은 AI가 반환하는 세 값만 수신한다. USE_BY는 이미지에서 확인한 날짜, ESTIMATED_CONSUMPTION_WINDOW는 예상 기간, UNKNOWN은 근거가 없는 경우다.

확인 사유: reviewReasons[]

name은 어떤 상품인지 나타내고, field는 그 상품의 어느 값을 확인할지 나타낸다.

필드 타입 필수 설명
field String O ITEM, NAME, CATEGORY, STORAGE_TYPE, MEASURE_TYPE, QUANTITY, WEIGHT, EXPIRATION, IMAGE
code String O 아래 목록에 정의한 확인 사유 코드
messages Array O 사용자에게 보여줄 문구. null·공백만 있는 문자열을 허용하지 않으며 최소 1개 필요
  • reviewReasons와 각 사유의 field, code, messages는 null을 허용하지 않는다.
  • 확인할 사유가 없으면 reviewReasons: []로 전달한다. 사유가 있으면 messages를 빈 배열로 보내지 않는다.
  • 안내 문구 필드 이름은 messages이며, 단일 문자열인 message를 사용하지 않는다.
  • 하나의 항목에 여러 사유를 담을 수 있고, 하나의 사유에 여러 메시지를 담을 수 있다.
code field 의미
ITEM_REVIEW_REQUIRED ITEM 항목 전체 확인 필요
IMAGE_QUALITY_REVIEW_REQUIRED IMAGE 이미지 품질 문제로 결과 확인 필요
EXPIRATION_NOT_FOUND EXPIRATION 확정 가능한 소비기한을 찾지 못함
EXPIRATION_ESTIMATED EXPIRATION 보관 조건과 참고 규칙으로 소비 가능 기간을 추정함
EXPIRATION_BASIS_FALLBACK EXPIRATION 구매일·제조일이 없어 분석일을 기준으로 사용함
QUANTITY_DEFAULTED QUANTITY 수량을 읽지 못해 1개를 기본 후보로 제안함
CATEGORY_FUZZY_MATCHED CATEGORY 유사 상품명으로 카테고리를 추정함

분석 실패: error

필드 타입 필수 null 허용 설명
code String O X AI 분석 오류 코드
message String O X 분석 실패 설명
retryable Boolean O X 분석을 다시 요청할 수 있는지
retryAfterMs Integer X O 분석 재요청까지 권장 대기 시간. 밀리초

error.retryable은 실패한 분석을 다시 실행할 수 있는지에 대한 정보다. 이미 생성된 콜백을 다시 보내는 것과 구분한다.

인식값 누락 처리

  • 인식하지 못한 상품명, 카테고리, 보관 방법, 측정 방식, 수량, 중량·단위, 소비기한은 null로 전달한다.
  • 인식하지 못한 값을 수량 1 등의 기본값으로 대체하지 않는다.
  • itemId, displayStatus 등 항목 식별·상태 정보는 유지한다.
  • weight와 expiration 객체는 유지하고, 확인하지 못한 내부 값에 null을 사용한다.
  • 소비기한을 확인하지 못하면 expiration.date는 null, expiration.dateType은 UNKNOWN으로 전달한다. 예상 기간이 있으면 dateType과 estimatedRange로 별도 전달한다.
  • 측정 방식별 값은 다음 기준을 따른다.
measureType quantity weight.value weight.unit
COUNT 인식한 개수 또는 미인식 시 null null NONE
WEIGHT null 인식한 중량·용량 또는 미인식 시 null G, ML 또는 미인식 시 null
null null null null
  • 해당 측정 방식에서 사용하지 않는 값의 null은 인식 실패로 판단하지 않는다.
  • 상품 후보의 인식값이 모두 없으면 displayStatus는 UNRECOGNIZED로 전달한다.
  • 등록에 필요한 인식값이 일부 누락되면 displayStatus는 NEEDS_REVIEW로 전달한다. 해당 측정 방식에서 사용하지 않는 값이나 선택 정보의 null은 누락 판단에서 제외한다.
  • 상품 후보 자체를 찾지 못한 경우에는 items에 항목을 만들지 않는다. 인식 항목이 하나도 없으면 items는 빈 배열로 전달한다.
  • 이러한 인식값 누락은 분석 실행 오류와 구분하며, 분석 절차가 정상적으로 종료됐다면 status는 COMPLETED로 전달한다.

3. 요청 예시

분석 완료

{
  "analysisId": "ai-analysis-1",
  "status": "COMPLETED",
  "completedAt": "2026-10-08T01:00:00Z",
  "result": {
    "documentType": "RECEIPT",
    "items": [
      {
        "itemId": "item_001",
        "displayStatus": "NEEDS_REVIEW",
        "name": "서울우유",
        "category": "DAIRY",
        "storageType": "REFRIGERATED",
        "measureType": "WEIGHT",
        "quantity": null,
        "weight": {
          "value": 1000,
          "unit": "ML"
        },
        "expiration": {
          "date": null,
          "dateType": "UNKNOWN",
          "estimatedRange": null
        },
        "reviewReasons": [
          {
            "field": "EXPIRATION",
            "code": "EXPIRATION_NOT_FOUND",
            "messages": ["영수증에서 확정 가능한 소비기한을 찾지 못했습니다."]
          }
        ]
      }
    ]
  },
  "error": null
}

일부 인식값이 누락된 경우

상품명·카테고리·보관 방법은 인식했지만, 수량과 소비기한을 인식하지 못한 예시다.

{
  "analysisId": "ai-analysis-4",
  "status": "COMPLETED",
  "completedAt": "2026-10-08T01:00:00Z",
  "result": {
    "documentType": "RECEIPT",
    "items": [
      {
        "itemId": "item_001",
        "displayStatus": "NEEDS_REVIEW",
        "name": "두부",
        "category": "TOFU_BEAN",
        "storageType": "REFRIGERATED",
        "measureType": "COUNT",
        "quantity": null,
        "weight": {
          "value": null,
          "unit": "NONE"
        },
        "expiration": {
          "date": null,
          "dateType": "UNKNOWN",
          "estimatedRange": null
        },
        "reviewReasons": [
          {
            "field": "QUANTITY",
            "code": "QUANTITY_DEFAULTED",
            "messages": ["수량을 확인해 주세요."]
          },
          {
            "field": "EXPIRATION",
            "code": "EXPIRATION_NOT_FOUND",
            "messages": ["영수증에서 확정 가능한 소비기한을 찾지 못했습니다."]
          }
        ]
      }
    ]
  },
  "error": null
}
  • 분석 절차는 정상적으로 종료됐으므로 status는 COMPLETED다.
  • 사용자가 보완해야 할 값이 있으므로 displayStatus는 NEEDS_REVIEW다.
  • quantity가 null인 것은 수량을 인식하지 못했다는 의미다.
  • expiration.date가 null인 것은 소비기한을 확인하지 못했다는 의미다.
  • weight.value가 null인 것은 COUNT 방식에서 중량을 사용하지 않는다는 의미다.
  • 사용자는 누락된 값을 입력한 뒤 재고를 등록한다.

인식 항목 없이 분석 완료

{
  "analysisId": "ai-analysis-2",
  "status": "COMPLETED",
  "completedAt": "2026-10-08T01:00:00Z",
  "result": {
    "documentType": "RECEIPT",
    "items": []
  },
  "error": null
}

오류로 분석 실패

{
  "analysisId": "ai-analysis-3",
  "status": "FAILED",
  "completedAt": "2026-10-08T01:00:00Z",
  "result": null,
  "error": {
    "code": "MODEL_UNAVAILABLE",
    "message": "이미지 분석을 완료하지 못했습니다.",
    "retryable": true,
    "retryAfterMs": 30000
  }
}

4. 콜백 수신 응답

정상 수신: 200 OK

분석 결과 또는 분석 실패 정보를 BE가 정상적으로 수신·보관하면 200을 반환한다.

분석 status가 FAILED인 콜백도 정상적으로 수신하면 200을 반환한다.

필드 타입 필수 설명
code String O 콜백 수신 성공 코드
message String O 수신 성공 메시지
data Object O 수신한 작업 정보
data.analysisId String O 수신한 AI 분석 ID
{
  "code": "IMAGE-200-003",
  "message": "분석 결과를 수신했습니다.",
  "data": {
    "analysisId": "ai-analysis-1"
  }
}

동일한 결과가 재전송된 경우에도 중복 반영하지 않고 정상 수신 응답을 반환한다.

수신 실패

필드 타입 필수 설명
code String O BE 오류 코드
message String O 오류 설명
retryable Boolean X 콜백 재전송이 가능한 오류이면 true
HTTP 상태 코드 설명
400 GLOBAL-400-001 요청 형식·필드 조합 오류
401 IMAGE-401-001 서비스 인증 실패
503 IMAGE-503-003 일시적인 수신·보관 불가

요청 형식 오류:

{
  "code": "GLOBAL-400-001",
  "message": "요청 형식이 올바르지 않습니다."
}

일시적인 수신 실패:

{
  "code": "IMAGE-503-003",
  "message": "분석 결과를 일시적으로 수신할 수 없습니다.",
  "retryable": true
}

5. 콜백 전달 재시도

AI가 분석을 끝냈더라도 BE가 결과를 받지 못할 수 있으므로, 콜백 요청에 대한 응답으로 전달 성공 여부를 판단한다.

BE 응답 또는 통신 결과 AI 처리
200 BE가 결과를 수신했으므로 해당 콜백의 전달을 종료한다.
400 요청 형식 수정이 필요하므로 같은 본문의 자동 재전송을 중단한다.
401 인증 설정 확인이 필요하므로 자동 재전송을 중단한다.
409 결과 충돌 확인이 필요하므로 자동 재전송을 중단한다.
429 일시적인 요청 제한이므로 같은 콜백을 재전송한다.
500, 502, 503, 504 일시적인 장애일 수 있으므로 같은 콜백을 재전송한다.
연결 실패 또는 타임아웃 BE 수신 여부를 확인할 수 없으므로 같은 콜백을 재전송한다.

재전송은 분석을 다시 실행하는 것이 아니다. 이미 생성한 analysisId, status, completedAt, result, error를 그대로 다시 보낸다.

예를 들어 AI가 COMPLETED 결과를 보냈는데 BE가 503을 반환하면, 분석을 다시 수행하지 않고 같은 완료 결과를 다시 보낸다.

반대로 AI가 FAILED 정보를 보냈더라도 BE가 200을 반환하면 실패 정보의 전달은 완료된 것이다. 분석을 다시 실행할지는 error.retryable과 별도의 재요청 정책으로 판단한다.

6. 카테고리 전체 목록

category에 값이 있으면 다음 11개 String 값 중 하나를 사용한다. 카테고리를 판단하지 못하면 null로 전달한다.

카테고리 값 분류
VEGETABLE 채소
FRUIT 과일
MEAT 육류
SEAFOOD 수산물
DAIRY 유제품
TOFU_BEAN 두부·콩류
GRAINS_NOODLE 곡류·면류
PROCESSED_FOOD 가공식품
SEASONING 조미료
BEVERAGE 음료
OTHER 기타

AI 카테고리 수정 요청

AI 결과 조회 및 콜백의 category를 위 목록과 통일 가능하면 통일해주시고 불가능하시다면 AI에서 사용하는 카테고리값 목록 남겨주시면 백엔드에서 처리하겠습니다.

  • GRAIN_NOODLE → GRAINS_NOODLE
  • PROCESSED → PROCESSED_FOOD
  • ETC → OTHER

category는 String으로 전달해 주세요.

{
  "category": "DAIRY"
}

7. 프론트 분석 결과 조회의 확인 사유

GET /api/v1/image-analyses/{analysisId}

프론트 응답의 data.results[].items[].reviewReasons는 콜백의 result.items[].reviewReasons와 같은 {field, code, messages} 객체 배열을 사용한다. 기존 문자열 배열(string[])을 객체 배열로 변경한다. 위의 필수 필드·enum·메시지 제약을 동일하게 적용하며, 사유와 메시지의 순서를 보존한다. 사유가 있는 배열을 빈 배열로 대체하거나 여러 메시지를 하나의 문자열로 합치지 않는다.

항목 응답 예시

{
  "itemId": "item_001",
  "displayStatus": "NEEDS_REVIEW",
  "name": "서울우유",
  "category": "DAIRY",
  "storageType": "REFRIGERATED",
  "measureType": "COUNT",
  "quantity": 2,
  "weight": {"value": null, "unit": "NONE"},
  "expiration": null,
  "reviewReasons": [
    {
      "field": "EXPIRATION",
      "code": "EXPIRATION_ESTIMATED",
      "messages": ["표시된 소비기한이 없어 보관 조건을 기준으로 예상했습니다."]
    }
  ]
}
  • itemId는 해당 분석 안의 항목을 식별한다. field는 그 항목에서 확인할 입력값을 구분한다.
  • 사유별 동작은 code로 구분하고, 사용자 안내에는 messages를 사용한다.
  • 사유가 없는 항목도 reviewReasons를 생략하거나 null로 보내지 않고 []를 반환한다.
  • 이번 변경은 항목의 검토 사유 계약이다. 최종 재고 등록에는 사용자가 확정한 날짜를 전달한다.

이 조회 응답 변경은 BE reviewReasons 계약 반영 PR이 적용된 뒤 제공한다. 콜백 수신·Redis 저장·저장 결과 조회 연결은 후속 BE 구현에서 같은 형식을 사용한다.

근거: AI 1단계 API 명세의 콜백 및 확인 사유 계약, 확인일 2026-10-13.

8. 분석 작업과 결과 보관기간

아래 정책은 후속 결과 보관기간 PR 적용 기준이다.

시점 작업·결과의 자동 만료 접수·조회 응답의 expiresAt
접수·대기·처리 중 없음 null
일부 이미지만 종료 없음 null
모든 이미지의 종료 결과를 BE가 저장 그 시점부터 기본 1시간 종료 확정 시각 + 결과 보관기간
중복 콜백·조회 기존 만료 시각 유지 변경 없음
  • 실패한 이미지도 종료에 포함한다. 전부 실패한 묶음에도 같은 결과 보관기간을 적용한다.
  • 보관 시작 시각은 BE가 마지막 종료 결과를 저장한 시각이다. 작업 생성 시각이나 AI가 보낸 completedAt으로 계산하지 않는다.
  • 먼저 도착한 결과도 묶음 전체가 종료될 때까지 보존한다.
  • 결과 보관 만료 시각은 서울 시간으로 전달한다. 업로드 발급 내역의 유효기간은 별도이며 유지한다.
  • 콜백이 끝내 오지 않은 작업은 자동 삭제하지 않는다. 상태 확인과 명시적인 종료 처리는 콜백 누락 보완 구현 범위다.

근거: 결과 보관기간에 대한 BE 정책 합의, 확인일 2026-10-14.

⚠️ **GitHub.com Fallback** ⚠️