API 명세 - 100-hours-a-week/KTB4-5th-wiki GitHub Wiki
AI가 사진 한 장의 분석을 종료하면 BE에 결과 또는 실패 정보를 전달한다.
POST /api/v1/internal/image-analyses/callback
| 헤더 | 타입 | 필수 | 설명 |
|---|---|---|---|
| Content-Type | String | O | application/json |
| X-Internal-API-Key | String | O | AI→BE 서비스 인증 키 |
| 필드 | 타입 | 필수 | 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이면 오류 객체 |
두 상태 모두 작업 처리가 종료됐음을 의미하며, 정상 결과 생성 여부를 구분한다.
| 상태 | 의미 | 예시 |
|---|---|---|
| COMPLETED | 분석 절차가 정상적으로 종료되어 결과를 생성함 | 상품 인식, 일부 정보 누락, 인식 항목 없음 |
| FAILED | 오류로 분석을 정상적으로 수행하지 못해 결과를 생성하지 못함 | 모델 장애, 이미지 접근 실패, 지원하지 않는 이미지 형식 |
- 상품을 인식하지 못했더라도 분석 절차가 정상적으로 끝났으면 COMPLETED로 전달한다.
- 인식 항목이 없으면 result.items를 빈 배열로 전달한다.
- 소비기한 등 일부 값이 누락돼도 COMPLETED로 전달할 수 있다.
- FAILED는 시스템 장애뿐 아니라 입력 이미지 처리 오류도 포함한다.
- 항목별 확인 필요 여부는 items[].displayStatus로 전달한다.
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| documentType | String | O | RECEIPT, PRODUCT 또는 OTHER |
| items | Array | O | 인식 항목 목록 |
PRODUCT 결과도 콜백으로 수신·보관한다. v2에서 PRODUCT 사용자 기능을 제공한다는 의미는 아니다.
| 필드 | 타입 | 필수 | 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로 전달한다.
| 필드 | 타입 | 필수 | null 허용 | 설명 |
|---|---|---|---|---|
| value | Number | O | O | WEIGHT에서 인식한 중량·용량은 양수. 값 미인식 또는 WEIGHT가 아니면 null |
| unit | String | O | O | WEIGHT이면 G 또는 ML이며, 단위 미인식이면 null. COUNT이면 NONE. 측정 방식이 불명확하면 null |
| 필드 | 타입 | 필수 | 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은 근거가 없는 경우다.
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 |
유사 상품명으로 카테고리를 추정함 |
| 필드 | 타입 | 필수 | 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로 전달한다.
{
"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
}
}분석 결과 또는 분석 실패 정보를 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
}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과 별도의 재요청 정책으로 판단한다.
category에 값이 있으면 다음 11개 String 값 중 하나를 사용한다. 카테고리를 판단하지 못하면 null로 전달한다.
| 카테고리 값 | 분류 |
|---|---|
| VEGETABLE | 채소 |
| FRUIT | 과일 |
| MEAT | 육류 |
| SEAFOOD | 수산물 |
| DAIRY | 유제품 |
| TOFU_BEAN | 두부·콩류 |
| GRAINS_NOODLE | 곡류·면류 |
| PROCESSED_FOOD | 가공식품 |
| SEASONING | 조미료 |
| BEVERAGE | 음료 |
| OTHER | 기타 |
AI 결과 조회 및 콜백의 category를 위 목록과 통일 가능하면 통일해주시고 불가능하시다면 AI에서 사용하는 카테고리값 목록 남겨주시면 백엔드에서 처리하겠습니다.
- GRAIN_NOODLE → GRAINS_NOODLE
- PROCESSED → PROCESSED_FOOD
- ETC → OTHER
category는 String으로 전달해 주세요.
{
"category": "DAIRY"
}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.
아래 정책은 후속 결과 보관기간 PR 적용 기준이다.
| 시점 | 작업·결과의 자동 만료 | 접수·조회 응답의 expiresAt |
|---|---|---|
| 접수·대기·처리 중 | 없음 | null |
| 일부 이미지만 종료 | 없음 | null |
| 모든 이미지의 종료 결과를 BE가 저장 | 그 시점부터 기본 1시간 | 종료 확정 시각 + 결과 보관기간 |
| 중복 콜백·조회 | 기존 만료 시각 유지 | 변경 없음 |
- 실패한 이미지도 종료에 포함한다. 전부 실패한 묶음에도 같은 결과 보관기간을 적용한다.
- 보관 시작 시각은 BE가 마지막 종료 결과를 저장한 시각이다. 작업 생성 시각이나 AI가 보낸
completedAt으로 계산하지 않는다. - 먼저 도착한 결과도 묶음 전체가 종료될 때까지 보존한다.
- 결과 보관 만료 시각은 서울 시간으로 전달한다. 업로드 발급 내역의 유효기간은 별도이며 유지한다.
- 콜백이 끝내 오지 않은 작업은 자동 삭제하지 않는다. 상태 확인과 명시적인 종료 처리는 콜백 누락 보완 구현 범위다.
근거: 결과 보관기간에 대한 BE 정책 합의, 확인일 2026-10-14.