딜리버리랩
경력근거: 주문·재고·배송 등 핵심 도메인 로직 설계 및 구현
※ 실제 산출물이 아닌 AI 예측 시안입니다.
딜리버리랩은 식자재 통합 중개 플랫폼 오더히어로를 굴리면서 주문·재고·배송을 한 도메인으로 묶어 OMS·WMS·TMS 를 직접 만듭니다. 이 세 기록이 한 회사 안에 다 있어야 새벽에 들어온 물건이 주문과 같은지 품목 단위로 맞춰 볼 수 있고, 유통사 취급 품목의 대체 묶음과 허용 결품률까지 플랫폼이 쥐고 있어야 바꿔 담기가 허용된 것인지 가려집니다. 공고가 AI 에이전트에게 맡길 일과 사람이 판단할 일을 가르라고 적은 대목도 이 제품의 구성과 맞물립니다. 판정은 규칙이 하고 모델은 요약 문장만 쓰기 때문입니다. 다만 이 포트폴리오는 세 기록을 맞춰 판정을 만드는 백엔드 설계와 그 결과를 화면으로 내는 일을 보여줍니다. 식자재 유통 실무나 신선식품 품질 판정의 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
공고는 오더히어로 웹과 앱, 백오피스 백엔드를 맡기면서 주문·재고·배송 도메인 로직 설계와 구현을 첫 업무로 적습니다. 세 기록이 서로 어긋나는 자리를 다루는 일이 그대로 직무입니다.
물류 플랫폼 세 벌을 개발하고 고도화하며 시스템 간 데이터 흐름을 설계·연동한다고 적혀 있습니다. 주문 쪽 기록과 창고 쪽 기록, 배송 쪽 기록이 한 화면에서 맞물리는지가 일의 무게중심입니다.
스펙과 컨텍스트, 검증의 흐름으로 에이전트를 오케스트레이션하고 그럴듯하지만 틀린 결과를 낼 지점을 판단해 개입하라고 적습니다. 판정은 규칙이 하고 모델은 문장만 맡는 구성이 이 요구와 그대로 맞물립니다.
주문 줄과 창고 피킹 기록, 기사 입고 기록, 유통사 취급 품목 기준표를 올리면 품목마다 주문한 수량·상품·시각이 실제 입고와 같은지 맞춰 보고 다섯 갈래로 나눠 보여줍니다. 어느 갈래인지는 수량 계산과 시각 계산, 대체 묶음 대조가 정하고, 클로드는 그 결과를 사장님이 유통사에 그대로 보낼 한 줄로 풀어 씁니다.
오더히어로로 여러 유통사에서 매일 새벽 식자재를 받는 식당 사장님 — 배송 기사가 두고 간 물건을 혼자 검수해야 하는데, 유통사마다 명세서 모양이 다르고 영업 준비 중에 상자를 다 열어 볼 시간이 없다
새벽 검수가 미뤄지는 까닭은 물건이 많아서가 아니라, 주문 줄과 창고 기록과 기사 기록이 서로 다른 화면에 흩어져 한 품목을 세 번 찾아야 하기 때문입니다. 세 기록을 품목 단위로 맞춰 보고 다섯 갈래로 나눠 두면, 사장님은 상자를 다 열지 않고 걸린 품목만 열어 봅니다.
사장님이 새벽에 여는 화면 하나가 돕니다. 주문 줄과 피킹 기록, 입고 기록, 유통사 기준표를 올리면 품목마다 다섯 갈래 중 하나가 붙고 그 근거가 된 값이 같이 열립니다. 네 기록은 파일로 올리는 것으로 시작하고, 오더히어로 사내 API 로 받아 오는 일은 3주차로 미룹니다.
사장님이 상자를 열어 확인한 결과를 주문 줄 40개마다 정답으로 붙여 판정 일치를 자동으로 세고, 어느 갈래에서 갈라지는지 유형을 리포트로 냅니다. 같은 화면에서 유통사별로 결품과 지연이 몇 건씩 쌓였는지도 봅니다. 2주차부터 AWS 로 배포해 사장님이 휴대폰 브라우저로 들어와 씁니다.
입고 기록을 Kafka 로 받아 기사가 찍는 즉시 판정이 다시 돌고, 결품과 대체로 걸린 품목은 유통사에 보낼 반품·환불 요청 초안으로 뽑힙니다. 초안은 사장님이 고쳐서 쓰는 문안까지만 만들고 발송은 하지 않습니다.
사장님이 네 기록을 올리면 품목마다 판정이 붙고, 걸린 품목은 어느 값 때문에 걸렸는지 그 자리에서 읽습니다.
도착 수량이 비거나 도착 시각이 비거나 창고 기록이 아예 없는 줄을 섞은 입력 30종을 올려도 목록이 사라지지 않고, 판정을 멈춘 줄만 근거가 빈 채로 남습니다.
허용 결품률과 정확히 같은 결품, 유예 시간과 정확히 같은 지연, 같은 대체 묶음으로 바꿔 담은 품목을 넣어도 넘김과 못 넘김이 규칙대로 갈라집니다.
사장님이 상자를 열어 확인한 결과를 주문 줄 40개(시드 13행에 증강 27행)마다 정답으로 붙여 일치를 셉니다. 단, 정답과 데이터를 모두 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
위 수치는 더미데이터 자가채점이며 실제 판정 적중률은 딜리버리랩의 실제 주문 줄과 유통사 피킹·입고 기록이 있어야 측정됩니다. 이 포트폴리오는 그 검증을 바로 돌릴 수 있는 상태로 만듭니다.
판정 일치 수는 제가 만든 주문·피킹·입고 더미를 제가 채점한 값이라 실제 입고 점검의 성능 증명이 아닙니다. 면접에서는 딜리버리랩의 실제 새벽 입고 기록이 들어왔을 때 무엇부터 재야 하는지로 이야기를 잇습니다.
어느 품목이 걸릴지 제가 정해 놓고 채점했으니 잘 맞는 것은 당연합니다. 그래서 도착 시각이 빈 줄, 입고 줄이 통째로 없는 품목, 재고는 남았는데 피킹 줄만 빠진 품목, 같은 대체 묶음으로 바꿔 담아 문제가 아닌 품목을 더미에 넣어 규칙이 어디서 멈추고 어디서 판정을 내는지 카드로 드러나게 했습니다.
수량이 모자라다는 판정이 곧 유통사 잘못을 뜻하지는 않습니다. 신선식품은 다듬는 과정에서 무게가 줄고 낱개 단위도 흔들리므로 허용 결품률을 잣대로 두고 마지막은 사장님이 정합니다.
이 포트폴리오는 여러 기록을 키로 맞춰 판정을 만드는 백엔드 설계를 보여줍니다. 식자재 유통 실무와 신선식품 품질 판정의 전문성은 주장하지 않습니다.
반품과 환불의 금액 처리는 유통사 계약과 결제 대행 심사가 있어야 다룰 수 있어 범위 밖입니다. 여기서는 요청 문안 초안을 만드는 데까지만 갑니다.
실제 도입에서는 유통사마다 피킹 기록의 시각 기준이 다르고 도착 시각을 기사가 나중에 몰아서 찍기도 합니다. 시각을 어디까지 믿을지 유통사별로 정하는 일은 도입 단계에서 따로 잡습니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
식당이 오더히어로에서 유통사별로 넣은 새벽 주문 줄입니다. 대조의 기준이 되는 쪽이며 한 주문에 여러 유통사 품목이 섞여 들어옵니다.
| order_id | store_id | vendor_id | item_code | item_name | order_qty | promised_at |
|---|---|---|---|---|---|---|
| ORD-20260312-001 | ST-0142 | V-1001 | ITM-1101 | 대파 1단 | 10 | 2026-03-12T05:30:00 |
| ORD-20260312-001 | ST-0142 | V-1001 | ITM-1102 | 청양고추 1kg | 3 | 2026-03-12T05:30:00 |
| ORD-20260312-001 | ST-0142 | V-1001 | ITM-1103 | 양파 15kg망 | 2 | 2026-03-12T05:30:00 |
| ORD-20260312-001 | ST-0142 | V-1001 | ITM-1104 | 애호박 | 12 | 2026-03-12T05:30:00 |
| ORD-20260312-001 | ST-0142 | V-1001 | ITM-1105 | 깻잎 100장 | 5 | 2026-03-12T05:30:00 |
아래 JSON Schema 를 그대로 지키는 주문 줄을 27행 더 만듭니다. {"type":"object","properties":{"order_id":{"type":"string"},"store_id":{"type":"string"},"vendor_id":{"type":"string"},"item_code":{"type":"string"},"item_name":{"type":"string"},"order_qty":{"type":"integer"},"promised_at":{"type":"string"}},"required":["order_id","store_id","vendor_id","item_code","order_qty","promised_at"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: 한 order_id 에 품목 3~8줄이 붙게 하고, order_qty 는 1~40 사이에서 낱개·박스·kg 단위가 섞이게 합니다. promised_at 은 새벽 03:00~07:00 사이로 두고 유통사마다 약속 시각이 다르게 합니다. 형식을 깨뜨리는 시도 3건(promised_at 을 날짜만 적기·order_qty 를 문자열로 적기·item_code 를 소문자로 적기)은 데이터가 아니라 파서 시험용으로 따로 만듭니다.{"type":"object","properties":{"order_id":{"type":"string"},"store_id":{"type":"string"},"vendor_id":{"type":"string"},"item_code":{"type":"string"},"item_name":{"type":"string"},"order_qty":{"type":"integer"},"promised_at":{"type":"string"}},"required":["order_id","store_id","vendor_id","item_code","order_qty","promised_at"]}유통사 창고에서 그 주문 줄을 실제로 집어 담은 기록입니다. 집지 못한 품목은 줄 자체가 남지 않고, 다른 상품으로 바꿔 담으면 substituted_item_code 가 채워집니다. 바꿔 담은 품목이 주문 품목과 같은 대체 묶음인 줄과 다른 묶음인 줄이 둘 다 들어 있고, 재고가 남아 있는데도 피킹 줄 자체가 없는 품목도 그대로 둡니다.
| order_id | item_code | picked_qty | substituted_item_code | picked_at |
|---|---|---|---|---|
| ORD-20260312-001 | ITM-1101 | 6 | — | 2026-03-12T04:10:00 |
| ORD-20260312-001 | ITM-1103 | 2 | — | 2026-03-12T04:12:00 |
| ORD-20260312-001 | ITM-1104 | 12 | — | 2026-03-12T04:15:00 |
| ORD-20260312-001 | ITM-1105 | 5 | — | 2026-03-12T04:18:00 |
| ORD-20260312-002 | ITM-2201 | 6 | ITM-2205 | 2026-03-12T04:40:00 |
아래 JSON Schema 를 그대로 지키는 피킹 줄을 30행 더 만듭니다. {"type":"object","properties":{"order_id":{"type":"string"},"item_code":{"type":"string"},"picked_qty":{"type":"integer"},"substituted_item_code":{"type":["string","null"]},"picked_at":{"type":"string"}},"required":["order_id","item_code","picked_qty","picked_at"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: 주문 줄 가운데 5~10%는 피킹 줄을 아예 만들지 않아 창고에서 빠진 품목이 생기게 하고, 10% 정도는 substituted_item_code 를 채워 다른 상품으로 바꿔 담은 줄을 만듭니다. picked_at 은 promised_at 보다 1~3시간 이른 새벽으로 두되, 15% 정도는 promised_at 이후로 늦게 찍히게 합니다. 형식을 깨뜨리는 시도 2건(picked_qty 를 음수로 적기·picked_at 을 빈 문자열로 적기)은 파서 시험용으로 따로 만듭니다.{"type":"object","properties":{"order_id":{"type":"string"},"item_code":{"type":"string"},"picked_qty":{"type":"integer"},"substituted_item_code":{"type":["string","null"]},"picked_at":{"type":"string"}},"required":["order_id","item_code","picked_qty","picked_at"]}배송 기사가 식당 문 앞에 두고 간 뒤 남긴 입고 기록입니다. 새벽에 급히 찍어 수량이나 도착 시각이 비는 줄이 실제로 섞입니다.
| order_id | item_code | arrived_qty | arrived_at |
|---|---|---|---|
| ORD-20260312-001 | ITM-1101 | 6 | 2026-03-12T05:20:00 |
| ORD-20260312-001 | ITM-1103 | 2 | 2026-03-12T05:20:00 |
| ORD-20260312-001 | ITM-1104 | 12 | 2026-03-12T05:20:00 |
| ORD-20260312-001 | ITM-1105 | 5 | — |
| ORD-20260312-002 | ITM-2201 | 6 | 2026-03-12T05:55:00 |
아래 JSON Schema 를 그대로 지키는 입고 줄을 30행 더 만듭니다. {"type":"object","properties":{"order_id":{"type":"string"},"item_code":{"type":"string"},"arrived_qty":{"type":["integer","null"]},"arrived_at":{"type":["string","null"]}},"required":["order_id","item_code"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: arrived_qty 를 null 로 둔 결측 줄 2~3행, arrived_at 을 null 로 둔 결측 줄 2~3행을 섞습니다. arrived_qty 가 0 인 줄과 주문보다 많이 온 줄도 각각 1~2행 넣습니다. arrived_at 은 대부분 promised_at 앞뒤 30분 안이지만 10% 정도는 두 시간 넘게 늦게 찍히게 합니다. 형식을 깨뜨리는 시도 2건(arrived_qty 에 '6박스' 처럼 단위를 붙이기·arrived_at 을 12시간제로 적기)은 파서 시험용으로 따로 만듭니다.{"type":"object","properties":{"order_id":{"type":"string"},"item_code":{"type":"string"},"arrived_qty":{"type":["integer","null"]},"arrived_at":{"type":["string","null"]}},"required":["order_id","item_code"]}유통사가 오더히어로에 등록해 둔 취급 품목 기준표입니다. 대체 가능한 묶음과 허용 결품률, 배송 유예 시간이 여기에 있어 판정의 잣대가 됩니다.
| vendor_id | item_code | substitute_group | stock_qty | allowed_shortage_rate | lead_time_hours |
|---|---|---|---|---|---|
| V-1001 | ITM-1101 | SG-대파 | 40 | 0.05 | 1.5 |
| V-1001 | ITM-1102 | SG-고추 | 0 | 0.05 | 1.5 |
| V-1001 | ITM-1103 | SG-양파 | 25 | 0.05 | 1.5 |
| V-1001 | ITM-1104 | SG-애호박 | 60 | 0.1 | 1.5 |
| V-1001 | ITM-1105 | SG-깻잎 | 30 | 0.1 | 1.5 |
아래 JSON Schema 를 그대로 지키는 취급 품목 줄을 25행 더 만듭니다. {"type":"object","properties":{"vendor_id":{"type":"string"},"item_code":{"type":"string"},"substitute_group":{"type":"string"},"stock_qty":{"type":"integer"},"allowed_shortage_rate":{"type":"number"},"lead_time_hours":{"type":"number"}},"required":["vendor_id","item_code","substitute_group","stock_qty","allowed_shortage_rate","lead_time_hours"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: stock_qty 가 0 인 품절 줄을 3~4행 두고, allowed_shortage_rate 는 0.0~0.15 사이에서 신선식품일수록 높게 잡습니다. lead_time_hours 는 유통사 성격에 따라 0.5~4.0 사이로 두고 같은 유통사 안에서는 같은 값을 씁니다. substitute_group 은 같은 묶음에 두 품목 이상이 들어가도록 짜서, 바꿔 담기가 허용된 경우와 아닌 경우가 둘 다 나오게 합니다.{"type":"object","properties":{"vendor_id":{"type":"string"},"item_code":{"type":"string"},"substitute_group":{"type":"string"},"stock_qty":{"type":"integer"},"allowed_shortage_rate":{"type":"number"},"lead_time_hours":{"type":"number"}},"required":["vendor_id","item_code","substitute_group","stock_qty","allowed_shortage_rate","lead_time_hours"]}| Kotlin | 판정 엔진 본체의 언어입니다. 수량 계산과 시각 계산, 대체 묶음 대조가 여기 모여 있습니다. |
| Spring Boot | 네 기록을 받아 판정을 돌리고 결과를 내주는 API 서버입니다. 규칙은 이 한 곳에만 둡니다. |
| PostgreSQL | 주문 줄과 피킹·입고 기록, 유통사 기준표를 담습니다. 기준표가 바뀐 날짜를 남겨 지난 새벽 판정을 그대로 되짚습니다. |
| 쿼리 최적화 | 품목 단위 대조는 주문 줄 수만큼 조인이 늘어나므로 order_id 와 item_code 복합 인덱스를 잡고 실행 계획을 읽어 가며 줄입니다. |
| React | 사장님이 새벽에 보는 화면입니다. 왼쪽에 판정 갈래를 세우고 품목을 누르면 근거 값이 오른쪽에 열립니다. |
| AWS | 2주차부터 배포와 운영을 맡습니다. 판정 이력과 올린 원본을 S3 에 남기고 RDS 로 PostgreSQL 을 띄웁니다. |
| Kafka | 3주차에 기사가 찍은 입고 기록을 이벤트로 받아 그 품목만 다시 판정합니다. |
| Kubernetes(EKS) | 판정 엔진과 화면 서버를 따로 띄워 새벽 몰림에 맞춰 판정 쪽만 늘립니다. |
| TDD | 허용 결품률 경계와 유예 시간 경계는 테스트를 먼저 쓰고 규칙을 채웁니다. 경계값이 곧 판정이 갈리는 자리입니다. |
| Claude Code | 네 표의 스키마와 경계 케이스 테스트를 에이전트로 먼저 뽑아 놓고 손으로 다듬습니다. 공고가 적은 스펙·컨텍스트·검증 흐름을 그대로 씁니다. |
| MCP | 판정 이력 데이터베이스를 에이전트가 직접 조회하도록 붙여, 어긋난 케이스를 물어보며 규칙을 고칩니다. |
| LLM | 판정이 정해진 뒤 그 품목을 유통사에 알릴 한 줄로 풀어 씁니다. 어느 갈래인지는 규칙이 정합니다. |
✓ pass → 그대로 멈춰도 완결입니다. 더 키우려면 사장님이 손으로 확인한 결과와 맞대 보는 단계로 넘어갑니다.
✓ pass → 여기까지도 완결입니다. 더 키우려면 입고 기록을 이벤트로 받아 곧바로 판정하고 요청 초안까지 뽑는 단계로 확장합니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
verdict사유 라벨 · item_code + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
주문대로 도착정상 입고수량 모자람수량 부족다른 상품으로 바뀜상품 대체약속 시간 넘겨 도착도착 지연재고 없어 빠진 품목빠진 품목새벽 입고 한 품목을 다섯 갈래 중 하나로 판정하고 그 근거가 된 네 표의 값을 함께 남기는 형식입니다. 갈래는 규칙이 정하고 요약 문장만 모델이 씁니다.
{"type":"object","properties":{"order_id":{"type":"string"},"item_code":{"type":"string"},"item_name":{"type":"string"},"verdict":{"type":"string","enum":["주문대로 도착","수량 모자람","다른 상품으로 바뀜","약속 시간 넘겨 도착","재고 없어 빠진 품목"]},"short_qty":{"type":["integer","null"],"description":"order_qty 에서 arrived_qty 를 뺀 모자란 수량. 도착 수량이 비어 있으면 비웁니다"},"shortage_rate":{"type":["number","null"],"description":"short_qty 를 order_qty 로 나눈 값. 소수 셋째 자리에서 반올림하고 allowed_shortage_rate 와 견줍니다"},"late_hours":{"type":["number","null"],"description":"arrived_at 에서 promised_at 을 뺀 시간이며 소수 첫째 자리까지 반올림합니다. 판정과 무관하게 도착 시각이 있으면 채웁니다 — 수량이나 대체로 판정이 갈린 품목도 몇 시간 만에 들어왔는지 이 칸으로 읽고, 약속보다 일찍 왔으면 음수 그대로 둡니다. lead_time_hours 를 넘을 때만 약속 시간 넘겨 도착입니다"},"late_place":{"type":["string","null"],"enum":["창고","배송",null],"description":"picked_at 이 promised_at 보다 늦으면 창고, 아니면 배송입니다"},"substitute_allowed":{"type":["boolean","null"],"description":"바꿔 담은 상품이 주문 상품과 같은 substitute_group 이면 참입니다. 참이면 허용된 바꿔 담기라 다른 상품으로 바뀜을 붙이지 않고 수량·시간 판정으로 넘어갑니다. 바꿔 담은 기록이 없으면 비웁니다"},"missing_stage":{"type":["string","null"],"enum":["창고 재고 없음","피킹 누락","입고 누락",null],"description":"재고 없어 빠진 품목으로 걸린 까닭. vendor_catalog.stock_qty 가 0 이면 창고 재고 없음, 재고가 남았는데 피킹 줄이 없으면 피킹 누락, 피킹 줄은 있는데 입고 줄이 없으면 입고 누락입니다. 다른 판정에서는 비웁니다"},"evidence":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string","enum":["store_order","picking_log","delivery_log","vendor_catalog"]},"field":{"type":"string"},"agg":{"type":["string","null"],"enum":["count","sum","avg","min","max",null]},"value":{"type":["string","number","null"]}},"required":["source","field","value"]}},"summary":{"type":"string"},"hedge":{"type":["string","null"]}},"required":["order_id","item_code","verdict","evidence","summary"]}[
{
"order_id": "ORD-20260312-001",
"item_code": "ITM-1101",
"item_name": "대파 1단",
"verdict": "수량 모자람",
"short_qty": 4,
"shortage_rate": 0.4,
"late_hours": -0.2,
"late_place": "배송",
"substitute_allowed": null,
"evidence": [
{
"source": "store_order",
"field": "order_qty",
"value": 10
},
{
"source": "delivery_log",
"field": "arrived_qty",
"value": 6
},
{
"source": "vendor_catalog",
"field": "allowed_shortage_rate",
"value": 0.05
},
{
"source": "store_order",
"field": "promised_at",
"value": "2026-03-12T05:30:00"
},
{
"source": "delivery_log",
"field": "arrived_at",
"value": "2026-03-12T05:20:00"
}
],
"summary": "주문 10단 가운데 6단만 들어와 허용 결품률 5%를 크게 넘었습니다.",
"hedge": "도착은 약속 시각보다 10분 일렀습니다. 수량이 먼저 걸려 카드는 그쪽으로 세우고, 걸린 시간은 칸에 남깁니다.",
"missing_stage": null
},
{
"order_id": "ORD-20260312-002",
"item_code": "ITM-2201",
"item_name": "국산 삼겹살 2kg",
"verdict": "다른 상품으로 바뀜",
"short_qty": 0,
"shortage_rate": 0,
"late_hours": -0.1,
"late_place": "배송",
"substitute_allowed": false,
"evidence": [
{
"source": "picking_log",
"field": "substituted_item_code",
"value": "ITM-2205"
},
{
"source": "vendor_catalog",
"field": "substitute_group",
"value": "SG-삼겹"
},
{
"source": "vendor_catalog",
"field": "substitute_group",
"value": "SG-수입삼겹"
},
{
"source": "store_order",
"field": "promised_at",
"value": "2026-03-12T06:00:00"
},
{
"source": "delivery_log",
"field": "arrived_at",
"value": "2026-03-12T05:55:00"
}
],
"summary": "국산 삼겹 자리에 수입산이 들어왔고 두 품목이 서로 다른 대체 묶음이라 허용된 바꿔 담기가 아닙니다.",
"hedge": null,
"missing_stage": null
},
{
"order_id": "ORD-20260312-003",
"item_code": "ITM-3301",
"item_name": "우유 1L",
"verdict": "약속 시간 넘겨 도착",
"short_qty": 0,
"shortage_rate": 0,
"late_hours": 2.7,
"late_place": "배송",
"substitute_allowed": null,
"evidence": [
{
"source": "store_order",
"field": "promised_at",
"value": "2026-03-12T05:30:00"
},
{
"source": "delivery_log",
"field": "arrived_at",
"value": "2026-03-12T08:10:00"
},
{
"source": "picking_log",
"field": "picked_at",
"value": "2026-03-12T03:50:00"
},
{
"source": "vendor_catalog",
"field": "lead_time_hours",
"value": 2
}
],
"summary": "창고에서는 03시 50분에 이미 집어 담았는데 도착이 2.7시간 늦어 유예 2시간을 넘겼습니다.",
"hedge": null,
"missing_stage": null
},
{
"order_id": "ORD-20260312-001",
"item_code": "ITM-1102",
"item_name": "청양고추 1kg",
"verdict": "재고 없어 빠진 품목",
"short_qty": 3,
"shortage_rate": 1,
"late_hours": null,
"late_place": null,
"substitute_allowed": null,
"evidence": [
{
"source": "store_order",
"field": "order_qty",
"value": 3
},
{
"source": "vendor_catalog",
"field": "stock_qty",
"value": 0
}
],
"summary": "창고 재고가 0이라 집어 담은 기록도 없이 통째로 빠졌습니다.",
"hedge": null,
"missing_stage": "창고 재고 없음"
},
{
"order_id": "ORD-20260312-001",
"item_code": "ITM-1104",
"item_name": "애호박",
"verdict": "주문대로 도착",
"short_qty": 0,
"shortage_rate": 0,
"late_hours": -0.2,
"late_place": "배송",
"substitute_allowed": null,
"evidence": [
{
"source": "store_order",
"field": "order_qty",
"value": 12
},
{
"source": "delivery_log",
"field": "arrived_qty",
"value": 12
},
{
"source": "delivery_log",
"field": "arrived_at",
"value": "2026-03-12T05:20:00"
}
],
"summary": "주문 12개가 그대로 들어왔고 약속 시각보다 10분 일찍 도착했습니다.",
"hedge": null,
"missing_stage": null
},
{
"order_id": "ORD-20260312-001",
"item_code": "ITM-1105",
"item_name": "깻잎 100장",
"verdict": "주문대로 도착",
"short_qty": 0,
"shortage_rate": 0,
"late_hours": null,
"late_place": null,
"substitute_allowed": null,
"evidence": [
{
"source": "store_order",
"field": "order_qty",
"value": 5
},
{
"source": "delivery_log",
"field": "arrived_qty",
"value": 5
},
{
"source": "delivery_log",
"field": "arrived_at",
"value": null
}
],
"summary": "주문 5개가 그대로 들어왔습니다.",
"hedge": "도착 시각이 비어 있어 늦었는지는 재지 못했고 수량만 맞춰 봤습니다.",
"missing_stage": null
},
{
"order_id": "ORD-20260312-002",
"item_code": "ITM-2203",
"item_name": "돼지 앞다리 2kg",
"verdict": "주문대로 도착",
"short_qty": 0,
"shortage_rate": 0,
"late_hours": -0.1,
"late_place": "배송",
"substitute_allowed": true,
"missing_stage": null,
"evidence": [
{
"source": "picking_log",
"field": "substituted_item_code",
"value": "ITM-2206"
},
{
"source": "vendor_catalog",
"field": "substitute_group",
"value": "SG-앞다리"
},
{
"source": "store_order",
"field": "order_qty",
"value": 5
},
{
"source": "delivery_log",
"field": "arrived_qty",
"value": 5
}
],
"summary": "앞다리 자리에 같은 대체 묶음의 품목이 들어왔고 수량과 시각도 약속대로라 바꿔 담기를 문제 삼지 않습니다.",
"hedge": null
},
{
"order_id": "ORD-20260312-001",
"item_code": "ITM-1106",
"item_name": "당근 5kg",
"verdict": "재고 없어 빠진 품목",
"short_qty": 4,
"shortage_rate": 1,
"late_hours": null,
"late_place": null,
"substitute_allowed": null,
"missing_stage": "피킹 누락",
"evidence": [
{
"source": "store_order",
"field": "order_qty",
"value": 4
},
{
"source": "vendor_catalog",
"field": "stock_qty",
"value": 35
},
{
"source": "vendor_catalog",
"field": "allowed_shortage_rate",
"value": 0.05
}
],
"summary": "주문 4개가 통째로 빠졌는데 유통사 재고는 35개 남아 있어 창고에서 집어 담는 단계가 빠진 것으로 보입니다.",
"hedge": "피킹 줄과 입고 줄이 둘 다 없어 언제 빠졌는지는 이 화면에서 정하지 못하고, 유통사에 물어봐야 갈라집니다."
}
][자동 검증 · 코드가 판정합니다] 1. verdict 가 다섯 값 가운데 하나인지 확인합니다. 판정은 규칙이 정하므로 모델이 새 값을 적으면 그 응답을 버립니다. 2. order_id·item_code·verdict·evidence·summary 다섯 칸이 모두 차 있는지 봅니다. 3. shortage_rate 를 short_qty 나누기 order_qty 로 다시 계산해 소수 둘째 자리까지 반올림한 값과 같은지 봅니다. 그리고 그 값이 vendor_catalog.allowed_shortage_rate 를 **넘었을 때만** 수량 모자람이 붙었는지 맞춰 봅니다. 허용치와 같은 값은 넘긴 것으로 보지 않습니다. 4. late_hours 를 arrived_at 빼기 promised_at 으로 다시 계산해 소수 첫째 자리까지 반올림한 값과 같은지 보고, lead_time_hours 를 넘었을 때만 약속 시간 넘겨 도착이 붙었는지 봅니다. 5. late_hours·late_place 는 **판정과 무관하게 도착 시각이 있으면 채웁니다** — 수량 모자람이나 다른 상품으로 바뀜으로 판정이 갈린 품목도 몇 시간 만에 들어왔는지 이 칸에 남고, 약속보다 일찍 왔으면 음수입니다. 입고 줄의 arrived_at 이 null 이면 둘 다 비웁니다. late_place 는 picked_at 이 promised_at 보다 늦으면 창고, 아니면 배송입니다. 6. 수량 칸은 입고 줄이 있느냐로 갈립니다. 입고 줄이 아예 없으면 도착 수량 0 으로 보고 short_qty 는 order_qty, shortage_rate 는 1.0 입니다. 입고 줄은 있는데 arrived_qty 가 null 이면 못 잰 것이라 short_qty 와 shortage_rate 를 둘 다 비웁니다. 7. substitute_allowed 는 바꿔 담은 기록이 있을 때만 채우고, 주문 품목과 바꿔 담은 품목의 substitute_group 이 같으면 참입니다. 참이면 허용된 바꿔 담기라 다른 상품으로 바뀜을 붙이지 않습니다. 8. missing_stage 는 재고 없어 빠진 품목일 때만 채웁니다. stock_qty 가 0 이면 창고 재고 없음, 재고가 남았는데 피킹 줄이 없으면 피킹 누락, 피킹 줄은 있는데 입고 줄이 없으면 입고 누락입니다. 9. evidence 의 field 이름이 네 표의 컬럼에 실제로 있는 이름인지 대조합니다. 없는 이름을 인용하면 요약이 지어낸 문장이라는 뜻이라 규칙이 써 둔 기본 문장으로 되돌립니다. 10. evidence 의 value 가 인용한 그 줄의 값과 글자 그대로 같은지 봅니다. 11. summary 가 존댓말 한 문장으로 끝나는지 봅니다. [판정 순서 · 한 줄이 두 사유에 걸릴 때] ① 피킹 줄과 입고 줄이 둘 다 없으면 재고 없어 빠진 품목입니다(까닭은 missing_stage 로 나눕니다). ② 바꿔 담은 품목의 substitute_group 이 주문 품목과 다르면 다른 상품으로 바뀜, ③ shortage_rate 가 허용치를 넘으면 수량 모자람, ④ late_hours 가 lead_time_hours 를 넘으면 약속 시간 넘겨 도착 차례로 보고 앞에서 걸리면 뒤는 보지 않습니다. ⑤ 어디에도 안 걸리면 주문대로 도착입니다. 사장님이 그 자리에서 해야 하는 일이 큰 순서입니다 — 통째로 빠진 품목은 아침 장사 전에 다른 데서 구해 와야 하지만, 늦게 온 품목은 이미 왔습니다. ITM-1101 은 수량과 시각에 함께 걸려 이 차례로 갈렸고, 걸린 시간은 late_hours 칸에 그대로 남습니다. [평가 러너 · 2주차] 1. 사장님이 상자를 열어 손으로 확인한 결과를 주문 줄 40개(시드 13행에 증강 27행)마다 정답으로 붙여 판정이 같은지 세고, 어느 판정에서 갈라지는지 유형을 모읍니다. 2. 창고에서 늦었는지 길에서 늦었는지 가른 결과를 유통사 배송 기록과 맞춰 봅니다. [수동 검증 · 사람이 봅니다] 1. 대체 묶음이 같아 허용으로 나온 바꿔 담기가 실제로 그 식당에서 써도 되는 물건인지 표본 20건을 눈으로 봅니다. 2. 피킹 누락으로 뜬 품목이 실제로 창고에서 빠진 것인지 유통사에 확인해 묶음 기준표를 손봅니다.