사용자 매뉴얼·번호알기·gov-portal 대시보드와 메뉴 동선·수불 리포트를 보강한다.

- 사용자 매뉴얼: league/commonmark 기반 bag/manual(로그인 전용),
  ManualRenderer + Config\Manual manifest, 콘텐츠 8종, E2E
- 번호알기(봉투번호확인): bag/number-lookup, BagNumberLookup, E2E
- gov-portal 대시보드 시안(기본/strip)·기본코드관리 화면
- 메뉴 관리: 등록·수정 후 메뉴 화면 유지, 수정 버튼 클릭 시 상단 스크롤
- 수불/분석 리포트(LOT 수불·반품/파기·수급계획·추이) 표시 보강
- .gitignore: docs/ → /docs/ 앵커링(최상위 개발문서만 제외, app/Docs는 추적)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
taekyoungc
2026-06-08 00:46:51 +09:00
parent 0f1d414f37
commit 8763876f19
77 changed files with 6139 additions and 182 deletions

View File

@@ -0,0 +1,294 @@
# 봉투·LOT·바코드 코드 체계
종량제 물류 시스템에서 사용하는 **봉투번호(바코드)**, **LOT 번호**, **품목코드** 등이 어디서 만들어지고 무엇을 의미하는지 정리한 문서입니다.
구현 기준: `app/Controllers/Bag.php`, `app/Controllers/Admin/BagOrder.php`, `app/Libraries/BagLotFlowBuilder.php`, `writable/database/*.sql`.
---
## 1. 계층 구조 (한눈에 보기)
```
[지자체] lg_code (예: 110204 남구)
└── [발주] bag_order
├── bo_lot_no … LOT 번호 (제작·추적 단위)
├── bo_uuid … 발주 식별자 (버전 이력)
└── bo_hash … 발주 내용 무결성 (SHA-256)
└── [입고] bag_receiving (br_idx)
└── bag_receiving_pack_code
├── brpc_box_code … 박스 바코드
├── brpc_pack_code … 팩 바코드
├── brpc_sheet_start_code ~ end … 낱장 구간
└── brpc_lot_no … 발주 LOT 복사
└── [판매/반품 스캔]
├── bag_sale_scan_code.bssc_code
└── bag_return_scan_code.brsc_code
```
| 단위 | 예시 | 조회·스캔 용도 |
|------|------|----------------|
| **LOT** | `OQXCKH` | 발주 단위, LOT 수불·디스켓 시드 |
| **박스** | `OQXCKH-000008-B001` | 박스 단위 입출고·LOT 수불 |
| **팩** | `OQXCKH-000008-P299` | 팩 단위 (스캔 가능) |
| **낱장** | `OQXCKH-000008-P299-S00125` | 장(매) 단위 판매·반품·LOT 수불 |
> **참고:** `000008`은 “8번째 봉투”가 아니라 **입고 건 PK(`br_idx`)를 6자리로 채운 값**입니다.
---
## 2. 발주 LOT 번호 (`bo_lot_no`)
### 생성 시점·위치
- **화면:** 관리자 발주 등록·수정 (`Admin\BagOrder::store`)
- **테이블:** `bag_order.bo_lot_no`
### 생성 규칙
1. **신규 발주:** `generateLotNo6()` — 영문 대문자+숫자 **6자리** 난수 (`0-9`, `A-Z`), DB 중복 시 최대 20회 재시도.
2. **실패 시:** 타임스탬프 기반 6자리 fallback (`base_convert(time, 10, 36)`).
3. **발주 수정(재발주):** 기존 건의 `bo_lot_no`**유지** (새 LOT를 붙이지 않음).
4. **입고 시 LOT 미지정:** 주문에 LOT가 없으면 `bag_code`(품목코드)를 LOT 자리에 대체 사용 (`createReceivingPackCodes`).
### 의미
- 제작업체·지자체가 **한 번의 발주 묶음**을 식별하는 번호.
- 입고 후 `bag_receiving_pack_code.brpc_lot_no`에 복사되어 박스/팩/낱장 코드의 **접두어**가 됩니다.
- **LOT-No 디스켓 불출** (`/bag/order/lot-seed`)·바코드 시드 파일명에 사용.
### 예시
| 값 | 설명 |
|----|------|
| `OQXCKH` | 발주 시 부여된 6자리 LOT (시드 예: `writable/barcode-seeds/OQXCKH_v1.seed.json`) |
| `3K9F2A` | 다른 발주 건의 LOT |
---
## 3. 발주 UUID·버전·해시
| 필드 | 형식 | 의미 |
|------|------|------|
| `bo_uuid` | UUID v4 (`xxxxxxxx-xxxx-4xxx-...`) | 동일 발주의 **버전 묶음** 식별자 |
| `bo_version` | 정수 (1부터 증가) | 발주 **개정 이력** (복합 PK: `uuid` + `version`) |
| `bo_hash` | SHA-256 64자 hex | 발주 헤더+품목 JSON의 **무결성 검증**용 |
- 수정·재발주 시 새 `bo_version` 행이 생기고, `bo_hash`가 갱신됩니다.
- 블록체인 연동 시 `SqlLedger``ORDER_CREATE` / `ORDER_UPDATE`와 함께 기록됩니다.
---
## 4. 바코드 시드 파일 (제작업체 연동)
### 생성 시점
- 발주 저장 시: `Admin\BagOrder::generateBarcodeSeedFile()`
- LOT 디스켓 불출 시: `Bag::orderLotSeedGenerate()` (동일 포맷)
### 저장 위치·파일명
- 디렉터리: `writable/barcode-seeds/`
- 파일명: `{bo_lot_no}_v{bo_version}.seed.json`
예: `OQXCKH_v1.seed.json`
### 내용
- 발주·품목 메타를 JSON으로 묶고 **AES-256-CBC**로 암호화.
- AES 키는 **RSA-2048** 공개키로 래핑 (`writable/keys/barcode_seed_*.pem`).
- 평문에는 `lot_no`, `uuid`, `version`, `order`, `items`, `order_hash` 등이 포함됩니다.
### 의미 (운영)
- 레거시/요구사항상 **제작업체 인쇄용 바코드 원시데이터**를 지자체가 발주와 함께 넘기는 용도.
- **현재 웹 입고 바코드**(`bag_receiving_pack_code`)는 아래 5절처럼 **입고 처리 시 서버가 별도 생성**합니다.
(README 「바코드 생성/사용 시점」 참고 — 발주 시점 생성 vs 입고 시점 생성 정책 검토 중)
---
## 5. 입고 시 박스·팩·낱장 바코드 (`bag_receiving_pack_code`)
### 생성 시점·함수
- **함수:** `Bag::createReceivingPackCodes()`
- **호출:** 입고 스캐너·일괄 입고 저장 시 (`receiving/scanner/store`, `receiving/batch/store` 등)
- **조건:** 해당 `br_idx`에 팩 코드가 아직 없을 때만 1회 생성
### 입력·포장 단위
- `packaging_unit` (`pu_pack_per_sheet`, `pu_total_per_box`): 팩당 낱장 수, 박스당 총 낱장 수.
- 입고 낱장 수 `qtySheet`**필요 팩 개수** = `ceil(qtySheet / pack_per_sheet)`.
- **박스당 팩 수** = `total_per_box / pack_per_sheet`.
### 코드 포맷 (구현)
`$lotNo` = 발주 `bo_lot_no` (없으면 품목코드), `$brIdx` = `bag_receiving.br_idx`.
| 구분 | sprintf 패턴 | 예 (`lot=OQXCKH`, `br_idx=8`) |
|------|----------------|-------------------------------|
| 박스 | `{lotNo}-%06d-B%03d` | `OQXCKH-000008-B001` |
| 팩 | `{lotNo}-%06d-P%03d` | `OQXCKH-000008-P001``P299` |
| 낱장 시작 | `{packCode}-S%05d` | `OQXCKH-000008-P299-S00001` |
| 낱장 끝 | `{packCode}-S%05d` | `OQXCKH-000008-P299-S00300` (팩당 300장 예) |
- `%06d``br_idx` 6자리 (000008 = 8번 입고 건).
- `%03d` → 박스·팩 **순번** (001~999).
- `%05d` → 팩 **내부 낱장 순번** (00001~).
### DB 컬럼 의미
| 컬럼 | 설명 |
|------|------|
| `brpc_lot_no` | 발주 LOT |
| `brpc_box_code` | 박스 바코드 |
| `brpc_pack_code` | 팩 바코드 (UNIQUE) |
| `brpc_sheet_start_code` / `brpc_sheet_end_code` | 해당 팩에 속한 낱장 코드 구간 |
| `brpc_sheet_qty` | 그 팩의 낱장 매수 |
| `brpc_state` | `in_stock` / `sold` 등 재고 상태 |
### LOT 수불 조회에서의 해석
- **낱장 번호 입력** → 해당 장의 판매·반품만 + 소속 팩 **입고 1건**.
- **팩 번호** (`…-P299`) → 팩·박스 코드 기준 이력.
- **LOT만** (`lot_no` 파라미터) → 동일 LOT 전체 팩·입고·발주 요약.
---
## 6. 봉투 품목코드 (`code_detail`, 종류 `O`)
### 등록
- **종류:** `code_kind.ck_code = 'O'` (봉투명/품목)
- **테이블:** `code_detail.cd_code`, `cd_name`
- 지자체별 확장: `cd_lg_idx` (0이면 공통)
### 코드 구성 (5자리 숫자 관례)
대구 시드(`code_master_init_daegu.sql`) 기준:
| 자리 | 연계 종류 | 예 |
|------|-----------|-----|
| 앞 2자리 | `E` 봉투구분 (10=일반, 20=공공, 30=무료, 60=음식물…) | `10` |
| 다음 2자리 | `G` 용량 (15=20L, 13=10L…) | `15` |
| 마지막 1자리 | `F` 재질 등 | `2` |
**예:** `10152` = 일반용(`10`) + 20L(`15`) + … → **일반용 20L**
- 발주·입고·재고·판매 집계는 이 코드(`bs_bag_code`, `br_bag_code`, `bi_bag_code` 등)로 묶입니다.
- **바코드(LOT·팩·낱장)와는 별개** — 품목 종류 식별용 마스터 코드입니다.
---
## 7. 지자체·행정 코드
| 코드 | 필드 | 예 | 의미 |
|------|------|-----|------|
| 지자체 | `local_government.lg_code` | `110204` | 대구 남구 (6자리 관례) |
| 발주 구·군 | `bag_order.bo_gugun_code` | `110204` | 발주 시 지자체 `lg_code` 복사 |
| 발주 동 | `bag_order.bo_dong_code` | (동 코드) | 세부 행정 단위 |
---
## 8. 지정판매소 번호 (`ds_shop_no`)
### 자동 부여 (주소 기반)
`Admin\DesignatedShop::resolveDesignatedShopNumberFromAddress()`:
```
판매소번호 = B코드 + C코드 + D코드 + 3자리 일련번호
```
- **B:** 시·도 (`code_kind` = `B`, 주소 매칭)
- **C:** 구·군 (`C`, B 접두 포함)
- **D:** 동 (`D`, C 접두 포함)
- **일련:** 동일 지자체 내 기존 번호 끝 3자리 숫자 최댓값 + 1
### 시드·레거시
- 초기 데이터에 `DS-2024-001` 형태가 있을 수 있음 (수동/이관 데이터).
- **가상계좌:** `ds_va_number` / `ds_va_bank` + `ds_va_account`
### 판매 스캔과의 관계
- 판매·반품 시 `bssc_ds_idx` / `brsc_ds_idx`로 판매소 연결.
- LOT 수불·판매 대장의 **입출고처**에 `ds_name` 표시.
---
## 9. 판매·반품 스캔 코드
### 저장 테이블
| 테이블 | 코드 컬럼 | 생성 시점 |
|--------|-----------|-----------|
| `bag_sale_scan_code` | `bssc_code` | 지정판매소 판매 (`/bag/sale/designated`) |
| `bag_return_scan_code` | `brsc_code` | 지정판매소 반품 (`/bag/sale/designated-return`) |
### 규칙
- 스캔·입력 값 = **입고 시 생성된 바코드** (보통 **낱장** 또는 팩).
- `bssc_state`: `in_stock` → 판매 시 `sold`, 반품 시 다시 `in_stock`.
- **낱장 판매** 시 동일 팩의 다른 낱장을 위해 `bag_receiving_pack_code` 팩 상태는 유지할 수 있음 (코드 주석 참고).
### 주문·판매 집계 코드
- `bag_sale.bs_type`: `sale` / `return` / `cancel`
- `shop_order` / `shop_order_item`: 주문 접수용 (전화·웹 등), 판매소·품목·수량 — **LOT 바코드와 별도 흐름**.
---
## 10. 기타 코드
| 구분 | 예 | 설명 |
|------|-----|------|
| 회원 | `mb_id` | 로그인 ID (시스템 부여·가입) |
| 제작업체 | `company.cp_*` | 발주 `bo_company_idx` |
| 판매 대행소 | `sales_agency.sa_*` | 발주·입고 `bo_agency_idx` |
| 무료 불출 | `bag_issue` | 불출처·수량 (바코드 LOT 체계와 별도) |
| 반품/파기 시드 | `RET-20260508-DS1-001` | 테스트용 반품 스캔 코드 (`bag_dispose_tables.sql`) |
| 파기 | `bag_dispose` | 입고분 파기 이력 (반품/파기 현황 **입고** 탭) |
---
## 11. 화면별 “어떤 코드를 넣나”
| 화면 | 입력·표시 코드 | 데이터 소스 |
|------|----------------|-------------|
| LOT 수불 조회 | 봉투번호(바코드)·`lot_no` | `BagLotFlowBuilder` |
| 지정판매소 판매/반품 | 스캔 바코드 | `bag_sale_scan_code` / `bag_return_scan_code` |
| 반품/파기 현황 출고 | (조회만) | `bag_return_scan_code` |
| 반품/파기 현황 입고 | (조회만) | `bag_dispose` |
| 기간별 봉투 수불 | 품목코드 `O` | 집계 (`BagFlowReportBuilder`) |
| 발주·입고 | LOT, 품목코드 | `bag_order`, `bag_receiving` |
---
## 12. 구현·스키마 참고 파일
| 주제 | 파일 |
|------|------|
| 입고 바코드 생성 | `app/Controllers/Bag.php``createReceivingPackCodes()` |
| LOT 6자리 생성 | `app/Controllers/Admin/BagOrder.php``generateLotNo6()` |
| LOT 수불 조회 | `app/Libraries/BagLotFlowBuilder.php` |
| 팩 코드 테이블 | `writable/database/receiving_pack_code_tables.sql` |
| 발주 테이블 | `writable/database/order_tables.sql` |
| 품목 마스터 | `writable/database/code_master_init_daegu.sql` (종류 `O`) |
| 바코드 시드 샘플 | `writable/barcode-seeds/*.seed.json` |
---
## 13. 자주 묻는 혼동
1. **`OQXCKH-000008-P299``P300`이 비슷해 보이는데 수불이 같다?**
- 이전에는 팩·LOT 전체 이력을 함께 가져왔습니다.
- 수정 후 **낱장 단위**는 `…-P299-Sxxxxx` 형태로 조회하거나, 팩 단위는 P299/P300 각각 조회해야 판매 이력이 갈립니다.
2. **LOT 6자리와 접두 `OQXCKH`가 다른 길이?**
- 발주 LOT는 6자리 규칙이지만, 시드·테스트 데이터에 6자 영문(`OQXCKH`)을 쓴 경우가 있습니다. DB `VARCHAR(50)`에 저장되며, 입고 코드 접두로 그대로 사용됩니다.
3. **품목코드 `10152`와 바코드 `OQXCKH-…`의 관계?**
- `10152`**종류·용량 마스터**.
- `OQXCKH-…`**물리 단위(팩/장) 추적용** 바코드입니다. 같은 품목이라도 LOT·입고 건마다 바코드 접두가 달라집니다.
---
*문서 갱신: 코드 변경 시 `createReceivingPackCodes`, `generateLotNo6`, `BagLotFlowBuilder`를 우선 확인하세요.*