db = $db ?? \Config\Database::connect(); } /** * @return array{ * ok: bool, * message: string, * barcode: string, * unit: string, * bag_code: string, * bag_name: string, * lot_no: string, * box_code: string, * pack_code: string, * qty_box: int, * qty_pack: int, * qty_sheet: int, * rows: list * } */ public function buildByBarcode(int $lgIdx, string $barcode, bool $queried): array { $empty = $this->emptyResult($barcode); if (! $queried || trim($barcode) === '') { return $empty; } $resolved = $this->resolveBarcode($lgIdx, trim($barcode)); if (! $resolved['ok']) { return array_merge($empty, [ 'message' => (string) ($resolved['message'] ?? '등록되지 않은 바코드입니다.'), ]); } $rows = $this->collectFlowRows($lgIdx, $resolved); usort($rows, static fn (array $a, array $b): int => ($a['sort_ts'] ?? 0) <=> ($b['sort_ts'] ?? 0)); return array_merge($empty, [ 'ok' => true, 'message' => '', 'barcode' => (string) ($resolved['barcode'] ?? $barcode), 'unit' => (string) ($resolved['unit'] ?? ''), 'bag_code' => (string) ($resolved['bag_code'] ?? ''), 'bag_name' => (string) ($resolved['bag_name'] ?? ''), 'lot_no' => (string) ($resolved['lot_no'] ?? ''), 'box_code' => (string) ($resolved['box_code'] ?? ''), 'pack_code' => (string) ($resolved['pack_code'] ?? ''), 'qty_box' => (int) ($resolved['qty_box'] ?? 0), 'qty_pack' => (int) ($resolved['qty_pack'] ?? 0), 'qty_sheet' => (int) ($resolved['qty_sheet'] ?? 0), 'rows' => $rows, ]); } /** * @return array */ public function buildByLotNo(int $lgIdx, string $lotNo, bool $queried): array { $empty = $this->emptyResult(''); if (! $queried || trim($lotNo) === '') { return $empty; } $lotNo = trim($lotNo); if (! $this->db->tableExists('bag_receiving_pack_code')) { return array_merge($empty, ['message' => '바코드(팩) 데이터가 없습니다.']); } $packRows = $this->db->table('bag_receiving_pack_code') ->select('brpc_pack_code, brpc_box_code, brpc_bag_code, brpc_bag_name, brpc_lot_no') ->where('brpc_lg_idx', $lgIdx) ->where('brpc_lot_no', $lotNo) ->limit(500) ->get() ->getResultArray(); if ($packRows === []) { $order = $this->db->table('bag_order') ->where('bo_lg_idx', $lgIdx) ->where('bo_lot_no', $lotNo) ->orderBy('bo_version', 'DESC') ->get() ->getRowArray(); if (! $order) { return array_merge($empty, ['message' => '해당 LOT·바코드를 찾을 수 없습니다.', 'lot_no' => $lotNo]); } return $this->buildLotFromOrderOnly($lgIdx, $lotNo, $order); } $codes = []; $bagCode = ''; $bagName = ''; foreach ($packRows as $p) { $codes[] = (string) ($p['brpc_pack_code'] ?? ''); $box = (string) ($p['brpc_box_code'] ?? ''); if ($box !== '') { $codes[] = $box; } if ($bagCode === '') { $bagCode = (string) ($p['brpc_bag_code'] ?? ''); $bagName = (string) ($p['brpc_bag_name'] ?? ''); } } $codes = array_values(array_unique(array_filter($codes, static fn (string $c): bool => $c !== ''))); $rows = []; foreach ($this->loadReceivingEventsForLot($lgIdx, $lotNo) as $ev) { $rows[] = $ev; } foreach ($this->loadScanEventsForCodes($lgIdx, $codes) as $ev) { $rows[] = $ev; } foreach ($this->loadReturnEventsForCodes($lgIdx, $codes) as $ev) { $rows[] = $ev; } usort($rows, static fn (array $a, array $b): int => ($a['sort_ts'] ?? 0) <=> ($b['sort_ts'] ?? 0)); if (count($rows) > 500) { $rows = array_slice($rows, -500); } return array_merge($empty, [ 'ok' => true, 'lot_no' => $lotNo, 'bag_code' => $bagCode, 'bag_name' => $bagName, 'barcode' => $lotNo, 'rows' => $rows, ]); } /** * @return array */ private function emptyResult(string $barcode): array { return [ 'ok' => false, 'message' => '', 'barcode' => $barcode, 'unit' => '', 'bag_code' => '', 'bag_name' => '', 'lot_no' => '', 'box_code' => '', 'pack_code' => '', 'qty_box' => 0, 'qty_pack' => 0, 'qty_sheet' => 0, 'rows' => [], ]; } /** * @return array{ok: bool, message?: string, barcode?: string, unit?: string, bag_code?: string, bag_name?: string, lot_no?: string, box_code?: string, pack_code?: string, pack_ids?: list, qty_box?: int, qty_pack?: int, qty_sheet?: int} */ private function resolveBarcode(int $lgIdx, string $barcode): array { if (! $this->db->tableExists('bag_receiving_pack_code')) { return ['ok' => false, 'message' => '바코드(팩) 데이터가 없습니다.']; } $pack = $this->db->table('bag_receiving_pack_code') ->where('brpc_lg_idx', $lgIdx) ->where('brpc_pack_code', $barcode) ->get() ->getRowArray(); if ($pack) { return $this->resolvedFromPackRow($barcode, '팩', $pack, 0, 1, (int) ($pack['brpc_sheet_qty'] ?? 0)); } $boxRows = $this->db->table('bag_receiving_pack_code') ->where('brpc_lg_idx', $lgIdx) ->where('brpc_box_code', $barcode) ->get() ->getResultArray(); if ($boxRows !== []) { $first = $boxRows[0]; $sheetQty = 0; foreach ($boxRows as $row) { $sheetQty += (int) ($row['brpc_sheet_qty'] ?? 0); } return $this->resolvedFromPackRow($barcode, '박스', $first, 1, count($boxRows), $sheetQty); } $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) ->get() ->getResultArray(); foreach ($sheetRows as $row) { $start = (string) ($row['brpc_sheet_start_code'] ?? ''); $end = (string) ($row['brpc_sheet_end_code'] ?? ''); if ($this->barcodeInRange($barcode, $start, $end)) { return $this->resolvedFromPackRow($barcode, '낱장', $row, 0, 0, 1); } } return ['ok' => false, 'message' => '등록되지 않은 바코드입니다.']; } /** * @param array $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, qty_box: int, qty_pack: int, qty_sheet: int} */ private function resolvedFromPackRow(string $barcode, string $unit, array $pack, int $qtyBox, int $qtyPack, int $qtySheet): array { return [ 'ok' => true, 'barcode' => $barcode, 'unit' => $unit, 'bag_code' => (string) ($pack['brpc_bag_code'] ?? ''), 'bag_name' => (string) ($pack['brpc_bag_name'] ?? ''), 'lot_no' => (string) ($pack['brpc_lot_no'] ?? ''), 'box_code' => (string) ($pack['brpc_box_code'] ?? ''), 'pack_code' => (string) ($pack['brpc_pack_code'] ?? ''), 'pack_ids' => [(int) ($pack['brpc_idx'] ?? 0)], 'qty_box' => $qtyBox, 'qty_pack' => $qtyPack, 'qty_sheet' => $qtySheet, ]; } /** * @param array $resolved * @return list */ private function collectFlowRows(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']; } $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); } } if ($brIdx > 0) { 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; } /** * @return list */ private function loadReceivingEventsByBrIdx(int $lgIdx, int $brIdx): array { $sql = " SELECT r.br_receive_date, r.br_sender_name, r.br_regdate, o.bo_order_date, c.cp_name, sa.sa_name FROM bag_receiving r LEFT JOIN bag_order o ON o.bo_idx = r.br_bo_idx LEFT JOIN company c ON c.cp_idx = o.bo_company_idx LEFT JOIN sales_agency sa ON sa.sa_idx = o.bo_agency_idx WHERE r.br_lg_idx = ? AND r.br_idx = ? LIMIT 20 "; $rows = []; foreach ($this->db->query($sql, [$lgIdx, $brIdx])->getResultArray() as $r) { $rows[] = $this->makeEvent( (string) ($r['br_receive_date'] ?? ''), (string) ($r['br_regdate'] ?? ''), $this->pickSource($r, '입고처', 'sa_name', 'cp_name', 'br_sender_name'), '입고' ); } return $rows; } /** * @return list */ private function loadReceivingEventsForLot(int $lgIdx, string $lotNo): array { $sql = " SELECT r.br_receive_date, r.br_sender_name, r.br_regdate, r.br_bag_name, c.cp_name, sa.sa_name FROM bag_receiving r INNER JOIN bag_order o ON o.bo_idx = r.br_bo_idx AND o.bo_lot_no = ? LEFT JOIN company c ON c.cp_idx = o.bo_company_idx LEFT JOIN sales_agency sa ON sa.sa_idx = o.bo_agency_idx WHERE r.br_lg_idx = ? ORDER BY r.br_receive_date ASC, r.br_idx ASC LIMIT 200 "; $rows = []; foreach ($this->db->query($sql, [$lotNo, $lgIdx])->getResultArray() as $r) { $label = trim((string) ($r['br_bag_name'] ?? '')); $rows[] = $this->makeEvent( (string) ($r['br_receive_date'] ?? ''), (string) ($r['br_regdate'] ?? ''), $this->pickSource($r, '입고처', 'sa_name', 'cp_name', 'br_sender_name') . ($label !== '' ? ' · ' . $label : ''), '입고' ); } return $rows; } /** * @param list $codes * @return list */ private function loadScanEventsForCodes(int $lgIdx, array $codes): array { if ($codes === [] || ! $this->db->tableExists('bag_sale_scan_code')) { return []; } $placeholders = implode(',', array_fill(0, count($codes), '?')); $params = array_merge([$lgIdx], $codes); $sql = " SELECT b.bssc_regdate, b.bssc_state, b.bssc_code, d.ds_name, d.ds_shop_no FROM bag_sale_scan_code b LEFT JOIN designated_shop d ON d.ds_idx = b.bssc_ds_idx WHERE b.bssc_lg_idx = ? AND b.bssc_code IN ({$placeholders}) ORDER BY b.bssc_regdate ASC LIMIT 200 "; $rows = []; foreach ($this->db->query($sql, $params)->getResultArray() as $r) { $state = strtolower((string) ($r['bssc_state'] ?? '')); $type = $state === 'in_stock' ? '반품입고' : '출고'; $shop = trim((string) ($r['ds_name'] ?? '')); if ($shop === '') { $shop = trim((string) ($r['ds_shop_no'] ?? '')); } if ($shop === '') { $shop = '지정판매소'; } $rows[] = $this->makeEvent( $this->dateOnly((string) ($r['bssc_regdate'] ?? '')), (string) ($r['bssc_regdate'] ?? ''), $shop, $type ); } return $rows; } /** * @param list $codes * @return list */ private function loadReturnEventsForCodes(int $lgIdx, array $codes): array { if ($codes === [] || ! $this->db->tableExists('bag_return_scan_code')) { return []; } $placeholders = implode(',', array_fill(0, count($codes), '?')); $params = array_merge([$lgIdx], $codes); $sql = " SELECT r.brsc_return_date, r.brsc_regdate, r.brsc_code, d.ds_name, d.ds_shop_no FROM bag_return_scan_code r LEFT JOIN designated_shop d ON d.ds_idx = r.brsc_ds_idx WHERE r.brsc_lg_idx = ? AND r.brsc_code IN ({$placeholders}) ORDER BY r.brsc_return_date ASC LIMIT 200 "; $rows = []; foreach ($this->db->query($sql, $params)->getResultArray() as $r) { $shop = trim((string) ($r['ds_name'] ?? '')); if ($shop === '') { $shop = trim((string) ($r['ds_shop_no'] ?? '')); } if ($shop === '') { $shop = '지정판매소'; } $rows[] = $this->makeEvent( (string) ($r['brsc_return_date'] ?? ''), (string) ($r['brsc_regdate'] ?? ''), $shop, '반품' ); } return $rows; } /** * @return list */ private function loadOrderEventsForLot(int $lgIdx, string $lotNo): array { $sql = " SELECT o.bo_order_date, o.bo_regdate, c.cp_name, sa.sa_name FROM bag_order o LEFT JOIN company c ON c.cp_idx = o.bo_company_idx LEFT JOIN sales_agency sa ON sa.sa_idx = o.bo_agency_idx WHERE o.bo_lg_idx = ? AND o.bo_lot_no = ? AND o.bo_status = 'normal' ORDER BY o.bo_version DESC LIMIT 1 "; $r = $this->db->query($sql, [$lgIdx, $lotNo])->getRowArray(); if (! $r) { return []; } return [ $this->makeEvent( (string) ($r['bo_order_date'] ?? ''), (string) ($r['bo_regdate'] ?? ''), $this->pickSource($r, '제작·발주', 'cp_name', 'sa_name'), '발주' ), ]; } /** * @param array $order * @return array */ private function buildLotFromOrderOnly(int $lgIdx, string $lotNo, array $order): array { $rows = $this->loadOrderEventsForLot($lgIdx, $lotNo); $boIdx = (int) ($order['bo_idx'] ?? 0); if ($boIdx > 0) { $sql = " SELECT r.br_receive_date, r.br_sender_name, r.br_regdate, r.br_bag_name, c.cp_name, sa.sa_name FROM bag_receiving r LEFT JOIN bag_order o ON o.bo_idx = r.br_bo_idx LEFT JOIN company c ON c.cp_idx = o.bo_company_idx LEFT JOIN sales_agency sa ON sa.sa_idx = o.bo_agency_idx WHERE r.br_bo_idx = ? ORDER BY r.br_receive_date ASC "; foreach ($this->db->query($sql, [$boIdx])->getResultArray() as $r) { $rows[] = $this->makeEvent( (string) ($r['br_receive_date'] ?? ''), (string) ($r['br_regdate'] ?? ''), $this->pickSource($r, '입고처', 'sa_name', 'cp_name', 'br_sender_name'), '입고' ); } } usort($rows, static fn (array $a, array $b): int => ($a['sort_ts'] ?? 0) <=> ($b['sort_ts'] ?? 0)); return array_merge($this->emptyResult($lotNo), [ 'ok' => true, 'lot_no' => $lotNo, 'barcode' => $lotNo, 'rows' => $rows, ]); } /** * @param array $row */ private function pickSource(array $row, string $default, string ...$keys): string { foreach ($keys as $key) { $v = trim((string) ($row[$key] ?? '')); if ($v !== '') { return $v; } } return $default; } /** * @return array{flow_date: string, counterparty: string, flow_type: string, sort_ts: int} */ private function makeEvent(string $dateYmd, string $sortDatetime, string $counterparty, string $flowType): array { $ts = strtotime($sortDatetime !== '' ? $sortDatetime : $dateYmd); return [ 'flow_date' => $dateYmd !== '' ? $dateYmd : ($ts ? date('Y-m-d', $ts) : ''), 'counterparty' => $counterparty, 'flow_type' => $flowType, 'sort_ts' => $ts !== false ? $ts : 0, ]; } private function dateOnly(string $datetime): string { if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $datetime, $m) === 1) { return $m[1]; } $ts = strtotime($datetime); return $ts ? date('Y-m-d', $ts) : $datetime; } /** * LOT 수불 조회 화면 테스트용 — 등록된 바코드·LOT 샘플 * * @return list */ public function loadTestSamples(int $lgIdx, int $limit = 80): array { $samples = []; $seen = []; $push = static function (array &$samples, array &$seen, string $kind, string $code, string $bagName, string $lotNo, string $state, string $hint) use ($limit): void { $code = trim($code); if ($code === '' || isset($seen[$code]) || count($samples) >= $limit) { return; } $seen[$code] = true; $samples[] = [ 'kind' => $kind, 'code' => $code, 'bag_name' => $bagName, 'lot_no' => $lotNo, 'state' => $state, 'hint' => $hint, ]; }; if ($this->db->tableExists('bag_receiving_pack_code')) { foreach ($this->db->table('bag_receiving_pack_code') ->select('brpc_pack_code, brpc_box_code, brpc_sheet_start_code, brpc_bag_name, brpc_lot_no, brpc_state') ->where('brpc_lg_idx', $lgIdx) ->orderBy('brpc_idx', 'DESC') ->limit(40) ->get() ->getResultArray() as $row) { $state = $this->packStateLabel((string) ($row['brpc_state'] ?? '')); $bagName = (string) ($row['brpc_bag_name'] ?? ''); $lotNo = (string) ($row['brpc_lot_no'] ?? ''); $push($samples, $seen, '팩', (string) ($row['brpc_pack_code'] ?? ''), $bagName, $lotNo, $state, '입고 팩 코드'); } $boxRows = $this->db->query(" SELECT brpc_box_code, MAX(brpc_bag_name) AS brpc_bag_name, MAX(brpc_lot_no) AS brpc_lot_no, MAX(brpc_state) AS brpc_state FROM bag_receiving_pack_code WHERE brpc_lg_idx = ? AND brpc_box_code != '' GROUP BY brpc_box_code ORDER BY MAX(brpc_idx) DESC LIMIT 15 ", [$lgIdx])->getResultArray(); foreach ($boxRows as $row) { $push($samples, $seen, '박스', (string) ($row['brpc_box_code'] ?? ''), (string) ($row['brpc_bag_name'] ?? ''), (string) ($row['brpc_lot_no'] ?? ''), $this->packStateLabel((string) ($row['brpc_state'] ?? '')), '박스 단위 조회'); } $sheetRows = $this->db->table('bag_receiving_pack_code') ->select('brpc_sheet_start_code, brpc_bag_name, brpc_lot_no, brpc_state') ->where('brpc_lg_idx', $lgIdx) ->where('brpc_sheet_start_code !=', '') ->orderBy('brpc_idx', 'DESC') ->limit(15) ->get() ->getResultArray(); foreach ($sheetRows as $row) { $push($samples, $seen, '낱장', (string) ($row['brpc_sheet_start_code'] ?? ''), (string) ($row['brpc_bag_name'] ?? ''), (string) ($row['brpc_lot_no'] ?? ''), $this->packStateLabel((string) ($row['brpc_state'] ?? '')), '낱장 시작 코드'); } $lotRows = $this->db->query(" SELECT DISTINCT brpc_lot_no FROM bag_receiving_pack_code WHERE brpc_lg_idx = ? AND brpc_lot_no != '' ORDER BY brpc_lot_no DESC LIMIT 10 ", [$lgIdx])->getResultArray(); foreach ($lotRows as $row) { $lot = (string) ($row['brpc_lot_no'] ?? ''); $push($samples, $seen, 'LOT', $lot, '(LOT 전체)', $lot, '—', 'lot_no 파라미터·입력 동일'); } } if ($this->db->tableExists('bag_sale_scan_code')) { foreach ($this->db->table('bag_sale_scan_code b') ->select('b.bssc_code, b.bssc_bag_name, b.bssc_unit, b.bssc_state, b.bssc_regdate') ->where('b.bssc_lg_idx', $lgIdx) ->orderBy('b.bssc_regdate', 'DESC') ->limit(20) ->get() ->getResultArray() as $row) { $state = strtolower((string) ($row['bssc_state'] ?? '')) === 'sold' ? '판매' : '반품재고'; $push($samples, $seen, '스캔', (string) ($row['bssc_code'] ?? ''), (string) ($row['bssc_bag_name'] ?? ''), '', $state, '판매·반품 스캔 이력'); } } return $samples; } private function packStateLabel(string $state): string { return match (strtolower($state)) { 'in_stock' => '재고', 'sold' => '판매', default => $state !== '' ? $state : '—', }; } private function barcodeInRange(string $code, string $start, string $end): bool { if ($start === '' || $end === '') { return false; } $extract = static function (string $v): array { if (preg_match('/^(.*?)(\d+)$/', $v, $m) === 1) { return [(string) $m[1], (int) $m[2], strlen((string) $m[2])]; } return ['', -1, 0]; }; [$cp, $cn, $cl] = $extract($code); [$sp, $sn, $sl] = $extract($start); [$ep, $en, $el] = $extract($end); if ($cn >= 0 && $sn >= 0 && $en >= 0 && $cp === $sp && $sp === $ep && $cl === $sl && $sl === $el) { return $cn >= $sn && $cn <= $en; } return strcmp($code, $start) >= 0 && strcmp($code, $end) <= 0; } }