사용자 매뉴얼·번호알기·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

@@ -202,11 +202,21 @@ class BagLotFlowBuilder
return $this->resolvedFromPackRow($barcode, '박스', $first, 1, count($boxRows), $sheetQty);
}
$exactSheet = $this->db->table('bag_receiving_pack_code')
->where('brpc_lg_idx', $lgIdx)
->where('brpc_sheet_start_code', $barcode)
->limit(1)
->get()
->getRowArray();
if (is_array($exactSheet) && $exactSheet !== []) {
return $this->resolvedFromPackRow($barcode, '낱장', $exactSheet, 0, 0, 1);
}
$sheetRows = $this->db->table('bag_receiving_pack_code')
->where('brpc_lg_idx', $lgIdx)
->where('brpc_sheet_start_code <=', $barcode)
->where('brpc_sheet_end_code >=', $barcode)
->limit(50)
->where('brpc_sheet_start_code !=', '')
->where('brpc_sheet_end_code !=', '')
->limit(200)
->get()
->getResultArray();
foreach ($sheetRows as $row) {
@@ -217,9 +227,109 @@ class BagLotFlowBuilder
}
}
$fromScan = $this->resolveBarcodeFromScanTables($lgIdx, $barcode);
if ($fromScan !== null) {
return $fromScan;
}
return ['ok' => false, 'message' => '등록되지 않은 바코드입니다.'];
}
/**
* 판매·반품 스캔에만 있는 낱장 코드(입고 팩 테이블 미등록) 조회
*
* @return array{ok: true, barcode: string, unit: string, bag_code: string, bag_name: string, lot_no: string, box_code: string, pack_code: string, pack_ids: list<int>, qty_box: int, qty_pack: int, qty_sheet: int}|null
*/
private function resolveBarcodeFromScanTables(int $lgIdx, string $barcode): ?array
{
$bagCode = '';
$bagName = '';
if ($this->db->tableExists('bag_sale_scan_code')) {
$sale = $this->db->table('bag_sale_scan_code')
->select('bssc_bag_code, bssc_bag_name')
->where('bssc_lg_idx', $lgIdx)
->where('bssc_code', $barcode)
->orderBy('bssc_regdate', 'DESC')
->limit(1)
->get()
->getRowArray();
if (is_array($sale) && $sale !== []) {
$bagCode = (string) ($sale['bssc_bag_code'] ?? '');
$bagName = (string) ($sale['bssc_bag_name'] ?? '');
}
}
if ($bagCode === '' && $this->db->tableExists('bag_return_scan_code')) {
$ret = $this->db->table('bag_return_scan_code')
->select('brsc_bag_code, brsc_bag_name')
->where('brsc_lg_idx', $lgIdx)
->where('brsc_code', $barcode)
->orderBy('brsc_regdate', 'DESC')
->limit(1)
->get()
->getRowArray();
if (is_array($ret) && $ret !== []) {
$bagCode = (string) ($ret['brsc_bag_code'] ?? '');
$bagName = (string) ($ret['brsc_bag_name'] ?? '');
}
}
if ($bagCode === '' && $bagName === '') {
return null;
}
$packRow = $this->findPackRowContainingSheet($lgIdx, $barcode);
return [
'ok' => true,
'barcode' => $barcode,
'unit' => '낱장',
'bag_code' => $bagCode,
'bag_name' => $bagName,
'lot_no' => (string) ($packRow['brpc_lot_no'] ?? ''),
'box_code' => (string) ($packRow['brpc_box_code'] ?? ''),
'pack_code' => (string) ($packRow['brpc_pack_code'] ?? ''),
'pack_ids' => isset($packRow['brpc_idx']) ? [(int) $packRow['brpc_idx']] : [],
'qty_box' => 0,
'qty_pack' => 0,
'qty_sheet' => 1,
];
}
/**
* @return array<string, mixed>
*/
private function findPackRowContainingSheet(int $lgIdx, string $barcode): array
{
if (! $this->db->tableExists('bag_receiving_pack_code')) {
return [];
}
$exact = $this->db->table('bag_receiving_pack_code')
->where('brpc_lg_idx', $lgIdx)
->where('brpc_sheet_start_code', $barcode)
->limit(1)
->get()
->getRowArray();
if (is_array($exact) && $exact !== []) {
return $exact;
}
foreach ($this->db->table('bag_receiving_pack_code')
->where('brpc_lg_idx', $lgIdx)
->where('brpc_sheet_start_code !=', '')
->where('brpc_sheet_end_code !=', '')
->limit(200)
->get()
->getResultArray() as $row) {
$start = (string) ($row['brpc_sheet_start_code'] ?? '');
$end = (string) ($row['brpc_sheet_end_code'] ?? '');
if ($this->barcodeInRange($barcode, $start, $end)) {
return $row;
}
}
return [];
}
/**
* @param array<string, mixed> $pack
* @return array{ok: true, barcode: string, unit: string, bag_code: string, bag_name: string, lot_no: string, box_code: string, pack_code: string, pack_ids: list<int>, qty_box: int, qty_pack: int, qty_sheet: int}
@@ -247,30 +357,62 @@ class BagLotFlowBuilder
* @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
*/
private function collectFlowRows(int $lgIdx, array $resolved): array
{
$unit = (string) ($resolved['unit'] ?? '');
return match ($unit) {
'낱장' => $this->collectFlowRowsForSheet($lgIdx, $resolved),
'박스' => $this->collectFlowRowsForBox($lgIdx, $resolved),
default => $this->collectFlowRowsForPack($lgIdx, $resolved),
};
}
/**
* 낱장: 해당 바코드 판매·반품만 + 소속 팩 입고 1건(발주·동일 LOT 전체 이력 제외)
*
* @param array<string, mixed> $resolved
* @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
*/
private function collectFlowRowsForSheet(int $lgIdx, array $resolved): array
{
$rows = [];
$codes = [$resolved['barcode']];
if (($resolved['pack_code'] ?? '') !== '' && ! in_array($resolved['pack_code'], $codes, true)) {
$codes[] = $resolved['pack_code'];
}
if (($resolved['box_code'] ?? '') !== '' && ! in_array($resolved['box_code'], $codes, true)) {
$codes[] = $resolved['box_code'];
$barcode = trim((string) ($resolved['barcode'] ?? ''));
if ($barcode === '') {
return [];
}
$brIdx = 0;
if ($this->db->tableExists('bag_receiving_pack_code')) {
$packCode = (string) ($resolved['pack_code'] ?? '');
if ($packCode !== '') {
$p = $this->db->table('bag_receiving_pack_code')
->select('brpc_br_idx')
->where('brpc_lg_idx', $lgIdx)
->where('brpc_pack_code', $packCode)
->get()
->getRowArray();
$brIdx = (int) ($p['brpc_br_idx'] ?? 0);
$brIdx = $this->receivingBrIdxForPackCode($lgIdx, (string) ($resolved['pack_code'] ?? ''));
if ($brIdx > 0) {
foreach ($this->loadReceivingEventsByBrIdx($lgIdx, $brIdx) as $ev) {
$rows[] = $ev;
}
}
foreach ($this->loadScanEventsForCodes($lgIdx, [$barcode]) as $ev) {
$rows[] = $ev;
}
foreach ($this->loadReturnEventsForCodes($lgIdx, [$barcode]) as $ev) {
$rows[] = $ev;
}
return $rows;
}
/**
* 팩: 팩·낱장 바코드 스캔 + 입고 + LOT 발주
*
* @param array<string, mixed> $resolved
* @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
*/
private function collectFlowRowsForPack(int $lgIdx, array $resolved): array
{
$rows = [];
$codes = array_values(array_unique(array_filter([
(string) ($resolved['barcode'] ?? ''),
(string) ($resolved['pack_code'] ?? ''),
], static fn (string $c): bool => $c !== '')));
$brIdx = $this->receivingBrIdxForPackCode($lgIdx, (string) ($resolved['pack_code'] ?? ''));
if ($brIdx > 0) {
foreach ($this->loadReceivingEventsByBrIdx($lgIdx, $brIdx) as $ev) {
$rows[] = $ev;
@@ -294,6 +436,91 @@ class BagLotFlowBuilder
return $rows;
}
/**
* 박스: 박스·소속 팩 코드 스캔 + 입고(박스 내 팩) + LOT 발주
*
* @param array<string, mixed> $resolved
* @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
*/
private function collectFlowRowsForBox(int $lgIdx, array $resolved): array
{
$rows = [];
$boxCode = (string) ($resolved['box_code'] ?? '');
$codes = array_values(array_unique(array_filter([
(string) ($resolved['barcode'] ?? ''),
$boxCode,
(string) ($resolved['pack_code'] ?? ''),
], static fn (string $c): bool => $c !== '')));
if ($boxCode !== '' && $this->db->tableExists('bag_receiving_pack_code')) {
$packCodes = $this->db->table('bag_receiving_pack_code')
->select('brpc_pack_code')
->where('brpc_lg_idx', $lgIdx)
->where('brpc_box_code', $boxCode)
->get()
->getResultArray();
foreach ($packCodes as $p) {
$pc = (string) ($p['brpc_pack_code'] ?? '');
if ($pc !== '' && ! in_array($pc, $codes, true)) {
$codes[] = $pc;
}
}
}
$seenBr = [];
if ($boxCode !== '' && $this->db->tableExists('bag_receiving_pack_code')) {
$brRows = $this->db->table('bag_receiving_pack_code')
->select('brpc_br_idx')
->distinct()
->where('brpc_lg_idx', $lgIdx)
->where('brpc_box_code', $boxCode)
->get()
->getResultArray();
foreach ($brRows as $brRow) {
$brIdx = (int) ($brRow['brpc_br_idx'] ?? 0);
if ($brIdx <= 0 || isset($seenBr[$brIdx])) {
continue;
}
$seenBr[$brIdx] = true;
foreach ($this->loadReceivingEventsByBrIdx($lgIdx, $brIdx) as $ev) {
$rows[] = $ev;
}
}
}
foreach ($this->loadScanEventsForCodes($lgIdx, $codes) as $ev) {
$rows[] = $ev;
}
foreach ($this->loadReturnEventsForCodes($lgIdx, $codes) as $ev) {
$rows[] = $ev;
}
$lotNo = (string) ($resolved['lot_no'] ?? '');
if ($lotNo !== '') {
foreach ($this->loadOrderEventsForLot($lgIdx, $lotNo) as $ev) {
$rows[] = $ev;
}
}
return $rows;
}
private function receivingBrIdxForPackCode(int $lgIdx, string $packCode): int
{
if ($packCode === '' || ! $this->db->tableExists('bag_receiving_pack_code')) {
return 0;
}
$p = $this->db->table('bag_receiving_pack_code')
->select('brpc_br_idx')
->where('brpc_lg_idx', $lgIdx)
->where('brpc_pack_code', $packCode)
->get()
->getRowArray();
return (int) ($p['brpc_br_idx'] ?? 0);
}
/**
* @return list<array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int}>
*/