- Update logistics/inbound-outbound pages across 9 companies - Update production/result and production/work-instruction admin pages - Add inventoryTransfer API client and enhance packaging/popInventoryAdjust/popInventoryMove clients - Add transaction-packaging-loading-plan docs - Add AdjustHistoryModal for COMPANY_9 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 KiB
11 KiB
거래 단위 포장재·적재함 추적 시스템 구현 계획
작성일: 2026-05-13 작성자: mhkim@wace.me 선행 적용 대상: COMPANY_7 성격: 신규 도메인 (입고/출고/재고이동 공통)
1. 배경
1.1 현재 한계
pkg_unit/loading_unit마스터에 포장재·적재함 정의는 등록 가능- 입고/출고/재고이동 시 "어떤 라인이 어떤 포장재로 어떤 적재함에 담겼는지" 저장하는 트랜잭션 테이블이 없음
- POP 장바구니 UI(
InboundCartPage,OutboundCartPage)에서 입력은 받지만 DB 영속화되지 않음
1.2 도입 목표
- 입고/출고/재고이동 등 모든 트랜잭션에서 포장재·적재함 사용 내역을 영속화
- 박스(라벨) 단위 개체 추적 — 부분 출고, LOT 분리, 유통기한, 적재함 이동을 박스 단위로 관리
- 신규 트랜잭션 영역이 추가되어도 DDL 변경 없이 확장 (polymorphic
source_type+source_id패턴)
2. 현황 분석
2.1 기존 마스터 테이블 (재사용)
| 테이블 | 역할 |
|---|---|
pkg_unit |
포장재 마스터 (BOX-L, BOX-M 등) |
pkg_unit_item |
포장재-품목 매핑 |
loading_unit |
적재함 마스터 (LU-PAL, LU-CRT 등) |
loading_unit_pkg |
적재함-포장재 매핑 |
2.2 신규 테이블 (이 계획서 범위)
| 테이블 | 역할 | 카디널리티 |
|---|---|---|
transaction_loading |
적재함 사용 인스턴스 | 작업 헤더당 N건 (loading_seq 로 동종 적재함 구분) |
transaction_packaging |
박스(라벨) 단위 포장 사용 | 박스 1개 = row 1개 |
2.3 영향 받는 기존 테이블
| 테이블 | 영향 |
|---|---|
inbound_detail |
컬럼 변경 없음. 컨트롤러에서 패키징 분해 로직 추가만 |
outbound_mng |
동일 |
inventory_stock |
유지 — 옵션 1(이중 관리), 트랜잭션 단위로 동시 갱신 |
inventory_history |
유지 — 품목 단위 이력만 기록 (박스 단위 이력은 Phase 3 의 별도 테이블로) |
3. 핵심 설계 결정
| 항목 | 결정 | 근거 |
|---|---|---|
| 박스 단위 모델 | 박스 1개 = transaction_packaging row 1개 |
부분 출고/LOT/유통기한을 박스별로 독립 관리하기 위해 |
pkg_count 컬럼 |
삭제 (항상 1) | 박스 1개 = row 1개라 불필요 |
quantity 의미 |
박스 1개에 실제 담긴 수량 | 잔량 박스도 같은 모델로 자연 표현 (예: 마지막 박스 quantity=30) |
| 라벨 형식 | {문서번호}-P{4자리} (예: INB-2026-001-P0001) |
단순/가독성/문서 추적 용이 |
| 라벨 UNIQUE | (company_code, package_label) |
회사 단위 유일 |
| 라벨 동시성 | 입고 트랜잭션 내 SELECT MAX(seq)+1 + UNIQUE 제약 retry |
별도 sequence 객체 불필요 |
| 적재함 없는 입고 | loading_id NULL 허용 |
LU-DIRECT 가상 적재함 미도입 (마스터 오염 방지) |
| 적재함 인스턴스 구분 | loading_code + loading_seq 조합 |
같은 종류 적재함을 한 작업에 여러 개 따로 사용하는 케이스 |
| 부분 출고 | quantity UPDATE + 로그 |
라벨 분할(-S1) 미사용 — 형식 폭주 방지, 박스 라벨 안정성 |
| 출고 처리 | row 의 status='SHIPPED' 변경 |
Phase 1 단순. 이력 추적은 Phase 3 의 transaction_package_movement 로 |
| 재고 동기화 | 옵션 1 (이중 관리) | 기존 화면 영향 최소. 같은 트랜잭션 내 inventory_stock + transaction_packaging 동시 갱신 |
| 잔량(미포장) | 별도 처리 안 함 (마지막 박스 quantity 가 작은 형태로 흡수) | pkg_code=NULL 행/is_remainder 플래그 불필요 |
| 포장 중첩 | 미지원 (loading_id 1단계만) |
1차 범위 단순화 |
| 바코드 인쇄 | 1차는 텍스트 라벨 / 2차 ZPL 연동 | 출시 범위 축소 |
4. 테이블 스키마
4.1 transaction_loading — 적재함 사용 인스턴스
| 컬럼 | 타입 | 설명 |
|---|---|---|
| id | VARCHAR(500) PK | UUID |
| company_code | VARCHAR(500) NOT NULL | 멀티테넌시 |
| source_type | VARCHAR(500) NOT NULL | 'inbound', 'outbound', 'inventory_move' 등 (헤더 레벨) |
| source_doc_id | VARCHAR(500) NOT NULL | inbound_mng.id / outbound_mng.id 등 헤더 ID |
| loading_code | VARCHAR(500) NOT NULL | loading_unit.loading_code |
| loading_seq | INT | 같은 적재함 종류를 한 작업에서 여러 개 쓸 때 구분 (1, 2, 3...) |
| loading_name | VARCHAR(500) | 마스터 스냅샷 |
| memo | VARCHAR(500) | 비고 |
| status | VARCHAR(500) | (예약) 'ACTIVE'/'CLOSED' 등 |
| writer | VARCHAR(500) | |
| created_by | VARCHAR(500) | |
| created_date | TIMESTAMP DEFAULT NOW() | |
| updated_date | TIMESTAMP DEFAULT NOW() |
인덱스:
(company_code, source_type, source_doc_id)(company_code, loading_code)
4.2 transaction_packaging — 박스(라벨) 단위 포장 사용
| 컬럼 | 타입 | 설명 |
|---|---|---|
| id | VARCHAR(500) PK | UUID |
| company_code | VARCHAR(500) NOT NULL | 멀티테넌시 |
| source_type | VARCHAR(500) NOT NULL | 'inbound_detail', 'outbound_detail', 'inventory_move_detail' 등 (라인 레벨) |
| source_id | VARCHAR(500) NOT NULL | 디테일 라인의 id |
| package_label | VARCHAR(500) NOT NULL | 라벨 (예: INB-2026-001-P0001) |
| pkg_code | VARCHAR(500) NOT NULL | pkg_unit.pkg_code |
| quantity | NUMERIC(18,4) NOT NULL | 이 박스 1개에 실제 담긴 수량 |
| loading_id | VARCHAR(500) | transaction_loading.id — NULL 허용 (적재함 없는 입고) |
| seq_no | INT | 라인 내 박스 순번 (1, 2, 3, ...) |
| status | VARCHAR(500) DEFAULT 'STORED' | STORED / SHIPPED / SCRAPPED / DAMAGED |
| lot_no | VARCHAR(500) | 로트번호 (옵션) |
| expire_date | DATE | 유통기한 (옵션) |
| writer | VARCHAR(500) | |
| created_by | VARCHAR(500) | |
| created_date | TIMESTAMP DEFAULT NOW() | |
| updated_date | TIMESTAMP DEFAULT NOW() |
제약:
- UNIQUE
(company_code, package_label)
인덱스:
(company_code, source_type, source_id)(company_code, loading_id)(company_code, package_label)(company_code, status)
5. 입고/출고/재고이동 흐름
5.1 입고 등록 (자동 분해)
사용자 입력 (집계형 UX 유지)
- 품목 라인별 입고 수량
- 포장재 + 단위당 수량 + 박스 개수
- 적재함 선택 (없으면 NULL)
백엔드 처리 (단일 트랜잭션)
inbound_mng/inbound_detailINSERT (기존 로직)- 적재함 신규 인스턴스면
transaction_loadingINSERT →loading_id획득 - 라인 수량을 박스 단위로 분해 →
transaction_packagingN개 INSERT- 예: 입고 1030 / BOX-50 → 20 row (quantity=50) + 1 row (quantity=30) = 21 row
- 라벨 자동 발번:
{문서번호}-P{4자리}시퀀스 inventory_stock+= 총합,inventory_historyINSERT (기존 흐름 그대로)
응답: 발번된 라벨 목록 (인쇄 대상)
5.2 출고 (Phase 2 범위)
- 출고 요청 (수주/지시) → 품목 + 수량 입력
- 라벨 선택 (자동 FEFO/FIFO / 수동 / 바코드 스캔)
- 박스 전체 출고:
- 해당 row
status='SHIPPED',updated_date=NOW()
- 해당 row
- 박스 부분 출고 (예: 50 박스 중 20개만 나감):
quantity50→30 UPDATE- 라벨/박스는 그대로 유지 (분할 없음)
inventory_history에 '출고' 한 줄 (품목 단위)
inventory_stock차감 (기존 흐름)
5.3 재고이동 (Phase 2)
- 박스의
loading_id만 새 인스턴스로 UPDATE inventory_stock의 창고/위치 컬럼 동기화
6. API 명세 (Phase 1 범위)
| 메서드 | 경로 | 역할 |
|---|---|---|
| POST | /api/packaging/labels/generate |
입고 시 라벨 자동 발번 (입고 컨트롤러 내부 호출) |
| GET | /api/packaging |
라벨 조회 (viewMode=summary 집계 / detail 박스 목록) |
| GET | /api/packaging/:label |
라벨 1건 상세 |
| GET | /api/loading |
적재함 인스턴스 조회 |
수정 API:
POST /api/inbound— 자동 분해 로직 추가 (단일 트랜잭션 안에서 transaction_packaging/loading INSERT)
Phase 2 추가 예정:
PATCH /api/packaging/:id/statusPATCH /api/packaging/:id/move
7. 프론트엔드 영향
7.1 적용 범위 (1차)
frontend/app/(main)/COMPANY_7/pop/_components/inbound/*frontend/app/(main)/COMPANY_7/logistics/packaging/page.tsx(조회)- 검증 완료 후 COMPANY_8/9/16/28/29/30 등으로 확대
7.2 수정 대상 파일
| 파일 | 변경 내용 |
|---|---|
pop/_components/inbound/InboundCartPage.tsx |
백엔드 응답으로 받은 라벨 목록 표시 영역 추가 |
pop/_components/inbound/NumberPadModal.tsx |
동작 변경 없음 (UI 그대로, 백엔드가 분해) |
logistics/packaging/page.tsx |
라벨 조회 뷰 추가 (집계/상세 토글) |
lib/api/packaging.ts |
신규 엔드포인트 클라이언트 추가 |
8. 단계별 로드맵
Phase 1 — MVP (이번 계획서 범위)
- DDL 마이그레이션 적용
- 백엔드
packagingService.ts신규 (분해/발번) receivingController.ts에 패키징 INSERT 통합- 라벨 조회 API
- 프론트 라벨 결과 표시
- COMPANY_7 검증
Phase 2 — 운영 안정화
- 출고 시 라벨 선택 UI (자동/수동/스캔)
- 부분 출고
quantityUPDATE 로직 - 상태/위치 변경 API
- 재고이동에서 박스 단위 이동
Phase 3 — 고도화
transaction_package_movement이력 테이블- 바코드 인쇄 (ZPL)
- 스캐너 입력 연동
- FEFO/FIFO 자동 선택
9. 위험 및 완화책
| 위험 | 완화책 |
|---|---|
inventory_stock 과 transaction_packaging 정합성 불일치 |
모든 변경을 단일 DB 트랜잭션으로 묶음. 서비스 레이어에 단일 진입점 강제 |
| 동시 입고 시 라벨 시퀀스 충돌 | UNIQUE 제약 + 트랜잭션 내 retry |
| 적재함 NULL 허용으로 쿼리 분기 증가 | LEFT JOIN transaction_loading 패턴으로 통일 |
| COMPANY_7 외 회사 확대 시 코드 중복 | 공통 라이브러리화 후 회사별 페이지에서 import |
10. 기존 데이터 처리
- 신규 입고부터만 적용 — 기존 inbound 데이터는 마이그레이션하지 않음
- 운영 중 변경된 입고를 박스 단위로 풀어내는 것은 입력 의도가 불명확해 데이터 왜곡 위험
- 필요 시 Phase 3 에 별도 마이그레이션 스크립트 작성 검토
11. 검증 계획
11.1 단위
- 박스 분해 함수: 1030 / 50 → 21 row (20×50, 1×30)
- 라벨 발번: 동시 호출 시 충돌 없이 순차 발번
- 트랜잭션 롤백: 한쪽 실패 시
inventory_stock/transaction_packaging모두 원복
11.2 통합 (COMPANY_7)
- POP 입고 장바구니 → 등록 → 라벨 21개 발번 → 조회 화면에서 21개 표시 확인
- 잔량 케이스 (1030/50=21박스): 마지막 박스 quantity=30 표시 확인
- 같은 종류 적재함 2개 따로 사용:
loading_seq1, 2 로 구분 확인 - 적재함 없는 입고:
loading_id=NULL정상 저장 확인
11.3 회귀
- 기존 입고/출고/재고이동 화면 동작 유지
- 재고현황 화면 (
inventory_stock기반) 수치 변화 없음
12. 마이그레이션 파일
- 위치:
db/migrations/add_transaction_packaging_loading.sql - 양식: 기존
add_packaging_to_pop_production.sql패턴 준수 (헤더 주석 / 검증 쿼리 / 롤백 섹션)