0. 범위·전제
- 이 명세 = 공급사 셀프 등록 + MD 검수 + 단가/변경 업데이트의 워크플로우 레이어. 기존 상품·히어로코드·공급사 테이블은 참조/연동(아래 §1 하단).
- 탑재 위치 = 공급사 상품 관리 어드민(qa-admin 뒤의 운영 어드민). ※
orderhero-ai-v2/backend(사장님 AI)는 별개 시스템.
- 우선순위(미팅 06-18): 단가/변경 업데이트(§3.4~3.5)부터. 상품 등록(§3.1~3.3)은 후속.
- 핵심 원칙: 공급사 제출/변경은 전부 초안·요청 상태 → MD 컨펌 후 반영(자동 반영 없음). 모든 처리는 감사 로그.
1. 데이터 모델
신규 테이블 5개(워크플로우) + 기존 테이블 참조. 컬럼명은 예시(실 DB 컨벤션에 맞춰 조정).
1.1 submission — 제출 건(batch)
| 컬럼 | 타입 | 설명 |
| id | PK | |
| supplier_id | FK supplier | 제출 공급사 |
| type | enum | single / excel |
| file_name | varchar null | 엑셀 원본 파일명(+스토리지 경로) |
| status | enum(파생) | pending / partial / done / rejected (item 집계) |
| created_at, created_by | ts, FK user | |
1.2 submission_item — 등록 상품(신규/매칭)
| 컬럼 | 타입 | 설명 |
| id | PK | |
| submission_id | FK | |
| status | enum | draft / pending / approved / rejected / hold |
| name, supplier_product_name | varchar | 표시 상품명 / 공급사 상품명 |
| supplier_product_code | varchar | 공급사 내부코드(공급사간 키 불가) |
| category_l/m/s | varchar | 대/중/소 |
| weight, qty, origin, manufacturer, brand | varchar | 중량·수량·원산지·제조사·브랜드 |
| taxable, store, delivery_type | enum | 과세여부 / 보관(상온·냉장·냉동) / 배송유형(MFC·택배·허브) |
| price, net_price, discount_base | int | 단가 / net공급가 / 할인기준가 |
| hero_code | varchar null | 매칭/발급된 히어로코드 (MD 확정 전 null 가능) |
| candidates | json | 매칭 후보 [{hero_code, name, score}] |
| images | json | {main, sub1, sub2} URL |
| warns | json | ["대표 이미지 없음","중량 단위 모호" …] |
| reject_reason | varchar null | |
| linked_product_id | FK product null | 승인 시 생성/연결된 실제 상품 |
| created_at, decided_at, decided_by | | |
1.3 change_request — 변경 요청 (단가·재고·품절·정보) 핵심
| 컬럼 | 타입 | 설명 |
| id | PK | |
| product_id | FK product | 대상(승인완료) 상품 |
| supplier_id | FK | |
| batch_id | FK price_batch null | 엑셀 일괄이면 묶음 |
| source | enum | single / excel |
| diff | json | [{field, label, from, to}] — 변경된 필드만(price·stock_status·qty·origin·name·supplier_product_name·opt …) |
| apply_at | ts null | null=컨펌 즉시 / 값=예약 적용일시 |
| status | enum | pending / confirmed / rejected |
| reason | varchar | 요청 사유 / 반려 사유 |
| created_at, created_by, decided_at, decided_by, applied_at | | |
1.4 price_batch — 단가 예약 업데이트 잡 (어드민 「단가 예약 업데이트」 미러)
| 컬럼 | 타입 | 설명 |
| id | PK | |
| supplier_id | FK | |
| update_type | enum | price(단가) / comprehensive(종합) |
| price_type | enum null | net / net_margin — 수수료·마진 미정 → v1 보류(§9) |
| schedule_at | ts null | 예약 일시(없으면 즉시) |
| file_name, status | varchar, enum | pending / scheduled / done / failed / rejected |
| created_at, created_by, done_at | | 히스토리(등록일시·예약일시·완료일시·등록자) |
price_batch 1 — N change_request(diff=[price]). 공급사 상품코드로 본인 상품 매칭, 매칭 실패 행은 제외.
1.5 audit_log — 감사 로그
| 컬럼 | 타입 | 설명 |
| id, at | PK, ts | |
| entity_type, entity_id | varchar | submission_item / change_request / price_batch |
| actor, action | varchar | supplier|md · 제출/컨펌/반려/보류/수정후승인 … |
| before, after | json | 필드 단위 전/후 |
1.6 기존 테이블 (참조·연동)
- product(공급사 상품) — 승인 시 submission_item → product 생성/연결. 단가·재고·품절·이미지는 change_request 컨펌 시 갱신.
- hero_group(히어로코드/그룹) — hero_code·parent_hero·그룹상품명. 매칭/발급 대상.
- supplier — 공급사 + 공급사 상품코드 매핑(재업로드 자동 prefill의 키).
2. 열거형·상태
| 열거 | 값 |
| submission_item.status | draft → pending → (approved | rejected | hold) |
| change_request.status | pending → (confirmed | rejected) |
| stock_status(판매상태) | on_sale(판매중) / soldout_temp(일시품절) / soldout_long(장기품절) |
| store(보관) | room / cold / frozen |
| delivery_type | mfc / parcel / hub |
| warns(예외) | no_image / weight_unit_ambiguous / match_none / weight_outlier / code_not_found |
3. API — 공급사
베이스 /api/supplier · 인증=공급사 토큰(자기 데이터만, §6). 목록은 ?q=&status=&page=&size=.
| 메서드·경로 | 설명 |
GET /products | 본인 상품 목록(검색·필터·페이지) |
GET /products/{id} | 상세 + 변경 이력(audit) + 진행중 변경요청 |
GET /match?name=&weight=&origin=&category= | 히어로코드 매칭 후보 top-N(§5) |
POST /submissions | 단건 등록(초안 제출). body=상품필드+hero_code?(매칭선택) |
POST /submissions/bulk | 엑셀 등록. multipart(file) → 행별 자동매칭+버킷 결과 반환 |
GET /submissions | 등록 이력(제출 건 목록) |
POST /products/{id}/change-requests | 변경 요청(단가·재고·품절·정보 diff) |
POST /price-batches | 엑셀 단가 일괄(업데이트타입·예약·file) → change_request N건 생성 |
GET /price-batches | 단가 업데이트 히스토리(진행상태·예약일시·완료일시) |
예시: 변경 요청 생성 POST /products/{id}/change-requests
// request
{
"source": "single",
"apply_at": null, // null=컨펌 즉시, "2026-07-01T09:00"=예약
"reason": "원가 변동",
"diff": [
{"field":"price","label":"납품단가","from":"1200","to":"1150"},
{"field":"stock_status","label":"판매상태","from":"on_sale","to":"soldout_temp"}
]
}
// response 201
{ "id": 9012, "status": "pending", "product_id": 4737 }
예시: 매칭 GET /match
// GET /match?name=쥬시쿨&weight=180ML&category=가공품/음료
{ "candidates": [
{"hero_code":"HERO-00E516","name":"쥬시쿨 180ML(자두)","weight":"180ML","score":0.92,"image":"…"},
{"hero_code":"HERO-00E517","name":"쥬시쿨 190ML","weight":"190ML","score":0.71}
]} // 없으면 candidates:[] → 신규 플로우
4. API — MD 검수
베이스 /api/admin/supplier-review · 인증=MD role. 메뉴 "공급사 등록 상품 검수".
| 메서드·경로 | 설명 |
GET /queue?type=submission|change&filter=risk | 검수 대기(제출건/변경요청). "수상한 것만" 필터 |
POST /items/{id}/approve | 승인/수정후승인. body=수정필드+hero_code(연결/발급) |
POST /items/{id}/reject | 반려(reason) |
POST /items/{id}/hold | 보류 |
POST /submissions/{id}/bulk-approve | 위험 플래그 없는 건 일괄 승인 |
POST /change-requests/{id}/confirm | 변경 컨펌 → diff를 product에 반영(apply_at 반영) |
POST /change-requests/{id}/reject | 변경 반려(reason) |
POST /change-requests/bulk-confirm | 전체 일괄 컨펌 |
예시: 변경 컨펌 POST /change-requests/{id}/confirm
// 처리: status=confirmed, decided_by/at 기록
// apply_at=null → 즉시 product 필드 반영 / 값 → 스케줄러가 해당 시각에 반영
// audit_log에 before/after(diff) 기록
{ "ok": true, "applied_at": "2026-07-01T09:00", "product_id": 4737 }
5. 매칭 API 상세
- 입력 다필드: 이름 + 중량 + 원산지 + 카테고리. (실측 top-1: 이름만 75% → +중량 86%)
- v1: DB 풀텍스트(이름) + 중량/원산지 가중 필터로 top-N. 인덱스 조회라 단건 ms, 대량 1,000건 수 초.
- v2: 임베딩/벡터(ANN) 도입 — 동의어·표기변형 대응. 응답 형식 동일.
- 자동확정 임계(대량): top-1 단독 금지 — 이름강 + (중량|원산지|바코드) 일치 + top1−top2 점수차 충족 시만. 신선/수산 순수 자동확정 금지.
- 오매칭 비용 큼(가격비교 키 오염) → 보수적 임계.
6. 인증·권한
- 공급사 토큰: 모든
/api/supplier/*는 본인 supplier_id로 스코핑(타사 데이터 접근 차단). 응답·목록은 서버에서 supplier_id 강제 필터.
- MD role:
/api/admin/*는 운영자 권한. (※ 현 AI백엔드 admin은 무인증 PoC — 실 어드민은 SSO/role 적용)
- 공급사는 내부값(히어로코드·net공급가·마진·MFC노출·총량피킹) 읽기/쓰기 불가 — 응답에서 제외.
7. 에러·예외
| 상황 | 처리 |
| 필수 누락(상품명·카테고리·중량·단가·이미지[신규]) | 400 + 누락 필드 목록 |
| 중량 단위 없음 | 경고(warn) + 검수필요 플래그(차단 아님) |
| 엑셀 파싱 오류 행 | 행 단위 분리(⛔), 정상 행만 진행 |
| 중복 등록(같은 hero/상품코드) | 409 또는 확인 플래그 |
| 단가 일괄 — 공급사 상품코드 미매칭 | 해당 행 제외(code_not_found), 결과에 표시 |
| 컨펌 시 히어로코드 미지정 | 422 — 연결/발급 후 승인 |
8. 비기능
- 예약 실행: change_request.apply_at / price_batch.schedule_at → 스케줄러(크론/큐)가 해당 시각에 반영, done_at 기록.
- 감사 로그: 모든 제출·컨펌·반려·수정후승인을 audit_log에 before/after로. 상품 이력 화면이 이걸 조회.
- 동시성: 같은 상품에 변경요청 중복 시 최신 우선/충돌 표시. 컨펌은 멱등.
- 재업로드 prefill: (supplier_id, supplier_product_code) ↔ hero_code 매핑 캐시 → 다음 업로드 자동 연결.
9. 미정 · 협의 필요
- 단가 유형(Net공급가 / Net공급가+마진율) = 수수료·마진 정책 미정 → v1 보류(필드만). 재무·물류 협의.
- 종합 단가 업데이트(comprehensive) 정확한 의미·대상 필드 확인 필요(어드민 기존 동작).
- 바코드(GTIN) 필드 신설 + 백필 = v2/v3(가공식품 정확매칭 인에이블러).
- 착수 전 게이트: 신규 중 기존 매칭 비율(실측 ≈65%), MD 시간배분(인터뷰).