Files
vexplor_dev/docs/plans/transaction-packaging-loading-plan.md
kmh 79962160d0 Update admin pages, API clients, and add transfer plan docs
- 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>
2026-05-21 18:04:11 +09:00

279 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 거래 단위 포장재·적재함 추적 시스템 구현 계획
> **작성일**: 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)
**백엔드 처리 (단일 트랜잭션)**
1. `inbound_mng` / `inbound_detail` INSERT (기존 로직)
2. 적재함 신규 인스턴스면 `transaction_loading` INSERT → `loading_id` 획득
3. 라인 수량을 박스 단위로 분해 → `transaction_packaging` N개 INSERT
- 예: 입고 1030 / BOX-50 → 20 row (quantity=50) + 1 row (quantity=30) = 21 row
4. 라벨 자동 발번: `{문서번호}-P{4자리}` 시퀀스
5. `inventory_stock` += 총합, `inventory_history` INSERT (기존 흐름 그대로)
**응답**: 발번된 라벨 목록 (인쇄 대상)
### 5.2 출고 (Phase 2 범위)
1. 출고 요청 (수주/지시) → 품목 + 수량 입력
2. 라벨 선택 (자동 FEFO/FIFO / 수동 / 바코드 스캔)
3. 박스 전체 출고:
- 해당 row `status='SHIPPED'`, `updated_date=NOW()`
4. 박스 부분 출고 (예: 50 박스 중 20개만 나감):
- `quantity` 50→30 UPDATE
- 라벨/박스는 그대로 유지 (분할 없음)
- `inventory_history` 에 '출고' 한 줄 (품목 단위)
5. `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/status`
- `PATCH /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 (자동/수동/스캔)
- [ ] 부분 출고 `quantity` UPDATE 로직
- [ ] 상태/위치 변경 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_seq` 1, 2 로 구분 확인
- 적재함 없는 입고: `loading_id=NULL` 정상 저장 확인
### 11.3 회귀
- 기존 입고/출고/재고이동 화면 동작 유지
- 재고현황 화면 (`inventory_stock` 기반) 수치 변화 없음
---
## 12. 마이그레이션 파일
- 위치: `db/migrations/add_transaction_packaging_loading.sql`
- 양식: 기존 `add_packaging_to_pop_production.sql` 패턴 준수 (헤더 주석 / 검증 쿼리 / 롤백 섹션)