공급사 발주·배송 관리

거래 · 2026년 8월 31일 기준

  • 테스트
  • 주문라인 1:1 분할출고 매칭
  • 합배송 운송장 자동전파
  • 운송장 엑셀 일괄출고
  • 배송완료 단일화(수기=배치)
  • 배송추적 배치
목차 6
  1. 개요
  2. 왜 이 설계가 필요했는가 (Problem)
  3. 테이블 설계 — PO 계층 + 배송 레코드
  4. 상태 머신 — 발주 처리 흐름
  5. 핵심 설계 결정
  6. 설계가 만든 결과 (Result)

개요

공급사가 주문 시점에 자동 분리된 발주(PO)를 받아 처리하는 순방향 워크플로우를 담당. 발주 접수 → 출고(운송장) → 배송중 → 배송완료의 흐름을 분할출고·합배송·엑셀 일괄처리까지 받쳐 설계했다.

PO-라인-배송 계층 주문라인 1:1 분할출고 합배송 자동전파 배송완료 단일화
SKU로 합쳤다가 출고 때 다시 나눔 잔여 누적 차감으로 출고마다 주문라인 지정 번들키 공유 전파 수기 = 배치 공통 5단계

왜 이 설계가 필요했는가 (Problem)


테이블 설계 — PO 계층 + 배송 레코드

ERD (핵심 관계)

ERD (핵심 관계) 다이어그램ERD (핵심 관계) 다이어그램

발주 배송 — 출고 레코드 핵심

분류 담는 것 설계 의도
주문라인 1:1 매칭 주문라인 참조 (NULL 허용) 집계 PO라인이 잃은 “이 출고가 어느 주문라인을 채우는가”를 복원. 마이너스/레거시 PO는 NULL 허용
분할출고 출고 수량 · PO라인 참조(FK) 접수 시 유효수량만큼 출고 수량 1인 레코드로 나눔 → 출고 1건 = 1레코드
합배송 번들키(묶음 식별) · 박스번호 여러 출고를 한 운송장으로. 같은 키 형제에 운송장 자동 전파
배송상태 배송 상태 (준비→배송중→완료, 취소) 출고건별 상태. 접수 시 ‘준비’ 초기값
운송장 택배사 · 송장번호 택배 출고의 송장. 직배송은 송장 없이 상태만 전환
일시 출고 시각 · 완료 시각 출고/완료 시각 (배치 추적 윈도우 기준)

PO 라인 — 취소 3트랙 (귀책 분리)


상태 머신 — 발주 처리 흐름

상태 머신 — 발주 처리 흐름 다이어그램상태 머신 — 발주 처리 흐름 다이어그램

핵심 설계 결정

1. 주문라인 1:1 분할출고 매칭 — 출고 할당기

// 출고 할당 — 주문라인 잔여를 앞에서부터 순회하며 출고량만큼 차감
for (주문라인, 잔여) in 주문라인별 잔여(오름차순):
    if 잔여 ≥ 출고량:
        잔여 −= 출고량
        return 주문라인  // 이 출고 레코드가 매핑될 주문라인
return 없음  // 잔여 정보 없음(마이너스 PO·레거시) → best-effort

2. 합배송 운송장 자동 전파

-- 대표 1건 운송장 입력 → 같은 번들키 형제에 동일 운송장 확장
SELECT 배송 ID FROM 발주 배송
 WHERE 번들키 = :번들키
   AND 배송 상태 = '준비' AND 삭제 아님

3. 운송장 엑셀 일괄출고

// 업로드: 관리번호(A열)에서 배송 ID 추출 → 소유·상태·택배사 검증 → 일괄 출고
배송 ID = 관리번호 앞부분의 숫자 (정규식 ^(\d+))
// 검증: '준비'가 아니면 "이미 출고", 택배사가 이미 있으면 "이미 운송장 등록" → 중복 차단

4. PO 라인 부분취소 — 단가 내림차순 환불 최대화

-- 후보 정렬: 단가(결제금액 / 수량) 내림차순 → 동일단가 FIFO
UPDATE 주문라인
   SET 공급사 취소수량 = 공급사 취소수량 + :취소수량
 WHERE … AND (고객사 취소 + 공급사 취소 + :취소수량) ≤ 주문수량  -- 경쟁 update 가드
-- 갱신 행 수가 0 이면 동시성 충돌 의심 예외 (낙관적 동시성 방어)

5. 직배송 안전망

직배송 시작():  // 운송장 없이 배송중 전환
    배송 상태 = 배송중
    출고 시각 = 지금
// 엑셀 출고: 직배송 라인이면 "직배송 상품은 운송장 입력 대상이 아닙니다" 예외
// 배송리스트 조회: 배송 유형이 직배송인 건 제외
// 배송 유형 변경은 '신규 발주' 라인만 허용 (접수 후엔 배송 레코드가 생겨 고정)

6. 배송완료 단일화 — 공통 완료 처리기

배송완료(라인 목록, 처리자):
  1) 발주 배송 → 배송완료
  2) PO라인 배송·라인 상태 → 배송완료
  3) 입고검수 생성 (구매사의 자동 입고 설정에 따라 검수완료/검수대기)
  4) PO라인 검수 상태 집계
  5) PO 헤더 + 주문 상태 동기화
// 입고수량 = 주문수량 − 취소 − 미출고 ≤ 0 이면 입고검수 생성·알림 skip

7. 배송추적 배치


설계가 만든 결과 (Result)

검증 결과

공통 모듈의 PO라인 엔티티 테스트 · 8/8 통과 (property 4종 · 총 32,000 케이스)

불변식 케이스
어떤 순서·분할로 취소해도 누적(고객사+공급사)이 주문수량을 넘지 않고, 초과요청은 필드를 바꾸지 않는다 10,000
누적이 정확히 주문수량과 같아질 때만 ’발주 취소’로 전환된다 10,000
미출고 포함 4항 합계가 주문수량을 넘지 못하고, 같으면 ’배송 완료’로 종결된다 10,000
미출고는 누적이 아니라 덮어쓰기다 (한 번에 확정하는 도메인 규칙) 2,000

거절 시 무변경을 명시적으로 잠갔다

랜덤 시퀀스가 성공·거절 경로를 함께 덮는다

거절 경로를 별도 테스트로 빼지 않고 한 시퀀스 안에서 섞어 검증한다. 랜덤 취소 시퀀스를 걸으며 각 단계마다 —

조건 기대
잔여 ≥ 요청 성공 + 누적 증가
잔여 < 요청 인자 오류 예외 · 필드 무변경

→ 실제 운영에서 벌어지는 순서(부분취소가 여러 번 오다가 한계 초과)를 그대로 재현한다.

주문라인 1:1 분할출고 매칭 — 불변식 검증

불변식 깨지면 무슨 일이 나는가 케이스
보존 — 배분된 총량 + 남은 총량 == 초기 총량 출고 수량이 새거나 늘어 고객별 배송현황·부분정산이 틀어진다 10,000
선택 규칙 — 항상 잔여가 충분한 가장 앞선(오름차순) 주문라인 에 매핑되고, 선택되지 않은 주문라인은 불변 귀속이 어긋나 「이 운송장이 어느 주문 건인가」를 다시 잃는다 10,000
🔴 거절 시 무변경 — 단일 주문라인 잔여를 넘는 출고는 예외이며 어떤 잔여도 바뀌지 않는다 부분 차감이 남아 재시도 시 수량이 이중 소모된다 10,000
소진 — 전량 배분 후엔 어떤 양수 출고도 예외 잔여를 넘어 출고가 잡힌다 2,000

그 밖에 검증한 계약

대상 테스트 고정한 것
엑셀 양식 (3번 결정) 양식 테스트 (관리자 4 · 공급사 3) 필수(연노랑) 표기 열과 파서가 읽는 열이 정확히 일치 — 인덱스 파서라 열 순서·헤더 문자열도 불변이어야 한다 · 관리자·공급사 양식 동형 · 「작성가이드」 기존 안내(합배송·분할출고) 보존
클레임 상태 축 상태 축 정책 테스트 (관리자 1 · 공급사 2) 클레임 상태는 헤더에서 온다 — 라인 상태를 헤더 필드명으로 내보내지 않고, 집계 그룹키도 헤더 상태다(한 신청이 라인 단계별로 쪼개지지 않음)
수거시작 알림 발주 서비스 테스트 (7) 상품명 요약 경계(null·빈 리스트·1건·N건 → 「외 N-1건」) + 알림 데이터에 PO라인 기준 상품명 적재
직거래 배제 직거래 필터 테스트 (5) · 예산 환원 테스트 (2) 직거래 발주가 대행 목록에 섞이지 않고, 부분취소 예산 환원이 직거래 원장으로만 간다 — 상세는 「직거래 트랙」 카드 참조

이 검증이 말하지 않는 것