개요
공급사가 주문 시점에 자동 분리된 발주(PO)를 받아 처리하는 순방향 워크플로우를 담당. 발주 접수 → 출고(운송장) → 배송중 → 배송완료의 흐름을 분할출고·합배송·엑셀 일괄처리까지 받쳐 설계했다.
| PO-라인-배송 계층 |
주문라인 1:1 분할출고 |
합배송 자동전파 |
배송완료 단일화 |
| SKU로 합쳤다가 출고 때 다시 나눔 |
잔여 누적 차감으로 출고마다 주문라인 지정 |
번들키 공유 전파 |
수기 = 배치 공통 5단계 |
왜 이 설계가 필요했는가 (Problem)
테이블 설계 — PO 계층 + 배송 레코드
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) |
직거래 발주가 대행 목록에 섞이지 않고, 부분취소 예산 환원이 직거래 원장으로만 간다 — 상세는 「직거래 트랙」 카드 참조 |
이 검증이 말하지 않는 것