사용자 매뉴얼·번호알기·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,225 @@
<?php
declare(strict_types=1);
namespace App\Libraries;
/**
* 봉투번호확인(번호알기) — 코드 입력 → 바코드·인쇄숫자·인식번호 분해.
* LOT-입고PK-팩/박스-낱장 형식 및 DB 등록 바코드 지원.
*/
class BagNumberLookup
{
/**
* @return array{
* ok: bool,
* message: string,
* input: string,
* barcode_text: string,
* print_text: string,
* recognition_text: string,
* unit: string,
* bag_code: string,
* bag_name: string
* }
*/
public function resolve(string $raw, ?int $lgIdx = null): array
{
$input = trim($raw);
if ($input === '') {
return $this->fail('코드를 입력해 주세요.', $input);
}
$normalized = strtoupper(preg_replace('/\s+/', '', $input) ?? $input);
$parsed = $this->parseStructuredCode($normalized);
if ($parsed === null && $lgIdx !== null) {
$parsed = $this->resolveFromDatabase($lgIdx, $normalized);
}
if ($parsed === null) {
return $this->fail('인식할 수 없는 코드입니다. 봉투 바코드(LOT-입고번호-팩/박스-낱장) 형식을 확인해 주세요.', $input);
}
return [
'ok' => true,
'message' => '',
'input' => $input,
'barcode_text' => $this->formatRow($parsed['barcode'], 4),
'print_text' => $this->formatRow($parsed['print'], 3),
'recognition_text' => $this->formatRow($parsed['recognition'], 2),
'unit' => (string) ($parsed['unit'] ?? ''),
'bag_code' => (string) ($parsed['bag_code'] ?? ''),
'bag_name' => (string) ($parsed['bag_name'] ?? ''),
];
}
/**
* @param list<string> $parts
*/
private function formatRow(array $parts, int $slots): string
{
$cells = array_pad($parts, $slots, '-');
foreach ($cells as $i => $cell) {
if ($cell === '' || $cell === null) {
$cells[$i] = '-';
}
}
return implode(' ', $cells);
}
/**
* @return array{barcode:list<string>,print:list<string>,recognition:list<string>,unit:string,bag_code?:string,bag_name?:string}|null
*/
private function parseStructuredCode(string $code): ?array
{
if (preg_match('/^([A-Z0-9]+)-(\d{6})-P(\d{3})-S(\d+)$/i', $code, $m) === 1) {
$sheet = str_pad((string) $m[4], 5, '0', STR_PAD_LEFT);
return [
'barcode' => [(string) $m[1], (string) $m[2], 'P' . $m[3], 'S' . $sheet],
'print' => [(string) (int) $m[2], (string) (int) $m[3], (string) (int) $sheet],
'recognition' => [(string) $m[2], 'P' . $m[3]],
'unit' => '낱장',
];
}
if (preg_match('/^([A-Z0-9]+)-(\d{6})-P(\d{3})$/i', $code, $m) === 1) {
return [
'barcode' => [(string) $m[1], (string) $m[2], 'P' . $m[3], '-'],
'print' => [(string) (int) $m[2], (string) (int) $m[3], '-'],
'recognition' => [(string) $m[2], 'P' . $m[3]],
'unit' => '팩',
];
}
if (preg_match('/^([A-Z0-9]+)-(\d{6})-B(\d{3})$/i', $code, $m) === 1) {
return [
'barcode' => [(string) $m[1], (string) $m[2], 'B' . $m[3], '-'],
'print' => [(string) (int) $m[2], (string) (int) $m[3], '-'],
'recognition' => [(string) $m[2], 'B' . $m[3]],
'unit' => '박스',
];
}
if (preg_match('/^([A-Z0-9]{4,8})$/i', $code) === 1) {
return [
'barcode' => [$code, '-', '-', '-'],
'print' => ['-', '-', '-'],
'recognition' => [$code, '-'],
'unit' => 'LOT',
];
}
return null;
}
/**
* @return array{barcode:list<string>,print:list<string>,recognition:list<string>,unit:string,bag_code?:string,bag_name?:string}|null
*/
private function resolveFromDatabase(int $lgIdx, string $code): ?array
{
$db = \Config\Database::connect();
if (! $db->tableExists('bag_receiving_pack_code')) {
return null;
}
$row = $db->table('bag_receiving_pack_code')
->where('brpc_lg_idx', $lgIdx)
->where('brpc_pack_code', $code)
->get()
->getRowArray();
if (is_array($row) && $row !== []) {
return $this->parsedFromPackRow($code, $row, '팩');
}
$boxRows = $db->table('bag_receiving_pack_code')
->where('brpc_lg_idx', $lgIdx)
->where('brpc_box_code', $code)
->limit(1)
->get()
->getRowArray();
if (is_array($boxRows) && $boxRows !== []) {
return $this->parsedFromPackRow($code, $boxRows, '박스');
}
$sheetRow = $db->table('bag_receiving_pack_code')
->where('brpc_lg_idx', $lgIdx)
->where('brpc_sheet_start_code <=', $code)
->where('brpc_sheet_end_code >=', $code)
->limit(1)
->get()
->getRowArray();
if (is_array($sheetRow) && $sheetRow !== []) {
$parsed = $this->parseStructuredCode($code);
if ($parsed !== null) {
$parsed['bag_code'] = (string) ($sheetRow['brpc_bag_code'] ?? '');
$parsed['bag_name'] = (string) ($sheetRow['brpc_bag_name'] ?? '');
return $parsed;
}
}
$exactSheet = $db->table('bag_receiving_pack_code')
->where('brpc_lg_idx', $lgIdx)
->groupStart()
->where('brpc_sheet_start_code', $code)
->orWhere('brpc_sheet_end_code', $code)
->groupEnd()
->limit(1)
->get()
->getRowArray();
if (is_array($exactSheet) && $exactSheet !== []) {
$parsed = $this->parseStructuredCode($code);
if ($parsed !== null) {
$parsed['bag_code'] = (string) ($exactSheet['brpc_bag_code'] ?? '');
$parsed['bag_name'] = (string) ($exactSheet['brpc_bag_name'] ?? '');
return $parsed;
}
}
return null;
}
/**
* @param array<string, mixed> $row
* @return array{barcode:list<string>,print:list<string>,recognition:list<string>,unit:string,bag_code:string,bag_name:string}
*/
private function parsedFromPackRow(string $code, array $row, string $unit): array
{
$parsed = $this->parseStructuredCode($code);
if ($parsed === null) {
$parsed = [
'barcode' => [$code, '-', '-', '-'],
'print' => ['-', '-', '-'],
'recognition' => ['-', '-'],
'unit' => $unit,
];
}
$parsed['unit'] = $unit;
$parsed['bag_code'] = (string) ($row['brpc_bag_code'] ?? '');
$parsed['bag_name'] = (string) ($row['brpc_bag_name'] ?? '');
return $parsed;
}
/**
* @return array{ok:bool,message:string,input:string,barcode_text:string,print_text:string,recognition_text:string,unit:string,bag_code:string,bag_name:string}
*/
private function fail(string $message, string $input): array
{
return [
'ok' => false,
'message' => $message,
'input' => $input,
'barcode_text' => $this->formatRow([], 4),
'print_text' => $this->formatRow([], 3),
'recognition_text' => $this->formatRow([], 2),
'unit' => '',
'bag_code' => '',
'bag_name' => '',
];
}
}