공급사 상품 등록·단가 변경 — 개발 명세 (v1)

데이터 모델 + API 계약 · 기획문서/프로토타입 v13 + QA 어드민 실측 구조 기반 · 개발 착수용

0. 범위·전제

1. 데이터 모델

신규 테이블 5개(워크플로우) + 기존 테이블 참조. 컬럼명은 예시(실 DB 컨벤션에 맞춰 조정).

1.1 submission — 제출 건(batch)

컬럼타입설명
idPK
supplier_idFK supplier제출 공급사
typeenumsingle / excel
file_namevarchar null엑셀 원본 파일명(+스토리지 경로)
statusenum(파생)pending / partial / done / rejected (item 집계)
created_at, created_byts, FK user

1.2 submission_item — 등록 상품(신규/매칭)

컬럼타입설명
idPK
submission_idFK
statusenumdraft / pending / approved / rejected / hold
name, supplier_product_namevarchar표시 상품명 / 공급사 상품명
supplier_product_codevarchar공급사 내부코드(공급사간 키 불가)
category_l/m/svarchar대/중/소
weight, qty, origin, manufacturer, brandvarchar중량·수량·원산지·제조사·브랜드
taxable, store, delivery_typeenum과세여부 / 보관(상온·냉장·냉동) / 배송유형(MFC·택배·허브)
price, net_price, discount_baseint단가 / net공급가 / 할인기준가
hero_codevarchar null매칭/발급된 히어로코드 (MD 확정 전 null 가능)
candidatesjson매칭 후보 [{hero_code, name, score}]
imagesjson{main, sub1, sub2} URL
warnsjson["대표 이미지 없음","중량 단위 모호" …]
reject_reasonvarchar null
linked_product_idFK product null승인 시 생성/연결된 실제 상품
created_at, decided_at, decided_by

1.3 change_request — 변경 요청 (단가·재고·품절·정보) 핵심

컬럼타입설명
idPK
product_idFK product대상(승인완료) 상품
supplier_idFK
batch_idFK price_batch null엑셀 일괄이면 묶음
sourceenumsingle / excel
diffjson[{field, label, from, to}] — 변경된 필드만(price·stock_status·qty·origin·name·supplier_product_name·opt …)
apply_atts nullnull=컨펌 즉시 / 값=예약 적용일시
statusenumpending / confirmed / rejected
reasonvarchar요청 사유 / 반려 사유
created_at, created_by, decided_at, decided_by, applied_at

1.4 price_batch — 단가 예약 업데이트 잡 (어드민 「단가 예약 업데이트」 미러)

컬럼타입설명
idPK
supplier_idFK
update_typeenumprice(단가) / comprehensive(종합)
price_typeenum nullnet / net_margin — 수수료·마진 미정 → v1 보류(§9)
schedule_atts null예약 일시(없으면 즉시)
file_name, statusvarchar, enumpending / scheduled / done / failed / rejected
created_at, created_by, done_at히스토리(등록일시·예약일시·완료일시·등록자)

price_batch 1 — N change_request(diff=[price]). 공급사 상품코드로 본인 상품 매칭, 매칭 실패 행은 제외.

1.5 audit_log — 감사 로그

컬럼타입설명
id, atPK, ts
entity_type, entity_idvarcharsubmission_item / change_request / price_batch
actor, actionvarcharsupplier|md · 제출/컨펌/반려/보류/수정후승인 …
before, afterjson필드 단위 전/후

1.6 기존 테이블 (참조·연동)

2. 열거형·상태

열거
submission_item.statusdraft → pending → (approved | rejected | hold)
change_request.statuspending → (confirmed | rejected)
stock_status(판매상태)on_sale(판매중) / soldout_temp(일시품절) / soldout_long(장기품절)
store(보관)room / cold / frozen
delivery_typemfc / 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 상세

6. 인증·권한

7. 에러·예외

상황처리
필수 누락(상품명·카테고리·중량·단가·이미지[신규])400 + 누락 필드 목록
중량 단위 없음경고(warn) + 검수필요 플래그(차단 아님)
엑셀 파싱 오류 행행 단위 분리(⛔), 정상 행만 진행
중복 등록(같은 hero/상품코드)409 또는 확인 플래그
단가 일괄 — 공급사 상품코드 미매칭해당 행 제외(code_not_found), 결과에 표시
컨펌 시 히어로코드 미지정422 — 연결/발급 후 승인

8. 비기능

9. 미정 · 협의 필요