- 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>
279 lines
11 KiB
Markdown
279 lines
11 KiB
Markdown
# 거래 단위 포장재·적재함 추적 시스템 구현 계획
|
||
|
||
> **작성일**: 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` 패턴 준수 (헤더 주석 / 검증 쿼리 / 롤백 섹션)
|