From c5df784f914c7cd386e75883b9ef1663f3a99224 Mon Sep 17 00:00:00 2001 From: taekyoungc Date: Wed, 15 Jul 2026 16:29:50 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=8B=A4=EC=82=AC=20=EC=9E=AC=EA=B3=A0?= =?UTF-8?q?=20=EB=B0=94=EC=BD=94=EB=93=9C=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=A7=80=20=EB=A1=9C=EB=94=A9=20=E2=80=94=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EC=8B=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=A1=9C=EB=94=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 상위 2,000건 상한 제거 → 초기 200건 렌더 후 스크롤 시 청크(200건) 추가 로딩 - 서버: ajax=1&offset&limit 로 청크 JSON 반환, 합계는 SQL 집계(전체 행 미적재) - 클라: IntersectionObserver 자동 로딩 + '더 보기' 폴백 버튼 - 인쇄 시 전체 먼저 로드 후 출력(부분 인쇄 방지) - fetch는 현재 origin 기준 상대경로로 호출(baseURL/포트 불일치에도 동작) Co-Authored-By: Claude Opus 4.8 --- app/Controllers/Bag.php | 159 +++++++++------ .../bag/inventory_stock_barcode_list.php | 186 +++++++++++++++--- 2 files changed, 259 insertions(+), 86 deletions(-) diff --git a/app/Controllers/Bag.php b/app/Controllers/Bag.php index c2517bb..33af166 100644 --- a/app/Controllers/Bag.php +++ b/app/Controllers/Bag.php @@ -1324,64 +1324,86 @@ SQL); return redirect()->to(site_url('bag/inventory'))->with('error', '지자체를 선택해 주세요.'); } - $db = \Config\Database::connect(); - $bagCode = trim((string) ($this->request->getGet('bag_code') ?? '')); - $keyword = trim((string) ($this->request->getGet('keyword') ?? '')); + $db = \Config\Database::connect(); + $bagCode = trim((string) ($this->request->getGet('bag_code') ?? '')); + $keyword = trim((string) ($this->request->getGet('keyword') ?? '')); + $pageSize = 200; // 레이지 로딩 청크 크기 - // 품목 선택 옵션 — 현재 재고로 남아있는 품목만 - $itemOptions = $db->table('bag_receiving_pack_code') - ->select('brpc_bag_code, MAX(brpc_bag_name) AS bag_name, COUNT(*) AS pack_cnt, SUM(brpc_sheet_qty) AS sheet_qty', false) - ->where('brpc_lg_idx', $lgIdx) - ->where('brpc_state', 'in_stock') - ->where('brpc_bag_code !=', '') - ->groupBy('brpc_bag_code') - ->orderBy('brpc_bag_code', 'ASC') - ->get() - ->getResultArray(); - - $builder = $db->table('bag_receiving_pack_code') - ->select('brpc_bag_code, brpc_bag_name, brpc_lot_no, brpc_box_code, brpc_pack_code, brpc_sheet_start_code, brpc_sheet_end_code, brpc_sheet_qty') - ->where('brpc_lg_idx', $lgIdx) - ->where('brpc_state', 'in_stock'); - if ($bagCode !== '') { - $builder->where('brpc_bag_code', $bagCode); - } - if ($keyword !== '') { - $builder->groupStart() - ->like('brpc_box_code', $keyword) - ->orLike('brpc_pack_code', $keyword) - ->orLike('brpc_sheet_start_code', $keyword) - ->orLike('brpc_sheet_end_code', $keyword) - ->orLike('brpc_lot_no', $keyword) - ->groupEnd(); - } - $rows = $builder - ->orderBy('brpc_bag_code', 'ASC') - ->orderBy('brpc_box_code', 'ASC') - ->orderBy('brpc_pack_code', 'ASC') - ->get() - ->getResultArray(); - - // 합계 — 박스 수(고유), 팩 수, 총 매수 - $totalPacks = count($rows); - $totalSheets = 0; - $boxSet = []; - foreach ($rows as $r) { - $totalSheets += (int) ($r['brpc_sheet_qty'] ?? 0); - $bx = trim((string) ($r['brpc_box_code'] ?? '')); - if ($bx !== '') { - $boxSet[$bx] = true; + // 공통 필터(재고 상태 + 품목/키워드)를 빌더에 적용 + $applyFilter = static function ($builder) use ($lgIdx, $bagCode, $keyword) { + $builder->where('brpc_lg_idx', $lgIdx)->where('brpc_state', 'in_stock'); + if ($bagCode !== '') { + $builder->where('brpc_bag_code', $bagCode); } - } - $totalBoxes = count($boxSet); + if ($keyword !== '') { + $builder->groupStart() + ->like('brpc_box_code', $keyword) + ->orLike('brpc_pack_code', $keyword) + ->orLike('brpc_sheet_start_code', $keyword) + ->orLike('brpc_sheet_end_code', $keyword) + ->orLike('brpc_lot_no', $keyword) + ->groupEnd(); + } + return $builder; + }; - // 엑셀 다운로드 — 현재 필터 그대로 + // 필터 적용된 행을 offset/limit(0=전체)로 조회 + $fetchRows = function (int $offset, int $limit) use ($db, $applyFilter): array { + $builder = $applyFilter($db->table('bag_receiving_pack_code')) + ->select('brpc_bag_code, brpc_bag_name, brpc_lot_no, brpc_box_code, brpc_pack_code, brpc_sheet_start_code, brpc_sheet_end_code, brpc_sheet_qty') + ->orderBy('brpc_bag_code', 'ASC') + ->orderBy('brpc_box_code', 'ASC') + ->orderBy('brpc_pack_code', 'ASC'); + if ($limit > 0) { + $builder->limit($limit, max(0, $offset)); + } + return $builder->get()->getResultArray(); + }; + + // 합계 — 전체 기준 SQL 집계(전체 행을 메모리에 올리지 않음) + $agg = $applyFilter($db->table('bag_receiving_pack_code')) + ->select('COUNT(*) AS total_packs, COALESCE(SUM(brpc_sheet_qty),0) AS total_sheets, COUNT(DISTINCT brpc_box_code) AS total_boxes', false) + ->get() + ->getRowArray(); + $totalPacks = (int) ($agg['total_packs'] ?? 0); + $totalSheets = (int) ($agg['total_sheets'] ?? 0); + $totalBoxes = (int) ($agg['total_boxes'] ?? 0); + + // AJAX 레이지 로딩 — 다음 청크만 JSON 으로 반환 + if ((string) ($this->request->getGet('ajax') ?? '') === '1') { + $offset = max(0, (int) ($this->request->getGet('offset') ?? 0)); + $limit = (int) ($this->request->getGet('limit') ?? $pageSize); + $limit = $limit <= 0 ? $pageSize : min($limit, 5000); + $chunk = $fetchRows($offset, $limit); + $out = []; + foreach ($chunk as $r) { + $out[] = [ + 'name' => ($r['brpc_bag_name'] ?? '') !== '' ? (string) $r['brpc_bag_name'] : (string) ($r['brpc_bag_code'] ?? ''), + 'lot' => (string) ($r['brpc_lot_no'] ?? ''), + 'box' => (string) ($r['brpc_box_code'] ?? ''), + 'pack' => (string) ($r['brpc_pack_code'] ?? ''), + 'start' => (string) ($r['brpc_sheet_start_code'] ?? ''), + 'end' => (string) ($r['brpc_sheet_end_code'] ?? ''), + 'qty' => (int) ($r['brpc_sheet_qty'] ?? 0), + ]; + } + $next = $offset + count($chunk); + + return $this->response->setJSON([ + 'rows' => $out, + 'offset' => $next, + 'hasMore' => $next < $totalPacks, + 'total' => $totalPacks, + ]); + } + + // 엑셀 다운로드 — 현재 필터 전체 if ((string) ($this->request->getGet('export') ?? '') === '1') { helper('export'); $exportRows = []; - foreach ($rows as $r) { + foreach ($fetchRows(0, 0) as $r) { $exportRows[] = [ - (string) ($r['brpc_bag_name'] ?? '') !== '' ? (string) $r['brpc_bag_name'] : (string) ($r['brpc_bag_code'] ?? ''), + ($r['brpc_bag_name'] ?? '') !== '' ? (string) $r['brpc_bag_name'] : (string) ($r['brpc_bag_code'] ?? ''), (string) ($r['brpc_lot_no'] ?? ''), (string) ($r['brpc_box_code'] ?? ''), (string) ($r['brpc_pack_code'] ?? ''), @@ -1399,21 +1421,30 @@ SQL); ); } - // 화면 표시는 성능을 위해 상위 N건까지만(합계·엑셀 저장은 항상 전체 기준) - $displayLimit = 2000; - $displayTruncated = count($rows) > $displayLimit; - $displayRows = $displayTruncated ? array_slice($rows, 0, $displayLimit) : $rows; + // 품목 선택 옵션 — 현재 재고로 남아있는 품목만 + $itemOptions = $db->table('bag_receiving_pack_code') + ->select('brpc_bag_code, MAX(brpc_bag_name) AS bag_name, COUNT(*) AS pack_cnt, SUM(brpc_sheet_qty) AS sheet_qty', false) + ->where('brpc_lg_idx', $lgIdx) + ->where('brpc_state', 'in_stock') + ->where('brpc_bag_code !=', '') + ->groupBy('brpc_bag_code') + ->orderBy('brpc_bag_code', 'ASC') + ->get() + ->getResultArray(); + + // 초기 1페이지만 렌더 — 나머지는 스크롤 시 레이지 로딩 + $rows = $fetchRows(0, $pageSize); return $this->render('실사 재고 바코드 리스트', 'bag/inventory_stock_barcode_list', [ - 'bagCode' => $bagCode, - 'keyword' => $keyword, - 'itemOptions' => $itemOptions, - 'rows' => $displayRows, - 'totalBoxes' => $totalBoxes, - 'totalPacks' => $totalPacks, - 'totalSheets' => $totalSheets, - 'displayTruncated' => $displayTruncated, - 'displayLimit' => $displayLimit, + 'bagCode' => $bagCode, + 'keyword' => $keyword, + 'itemOptions' => $itemOptions, + 'rows' => $rows, + 'totalBoxes' => $totalBoxes, + 'totalPacks' => $totalPacks, + 'totalSheets' => $totalSheets, + 'pageSize' => $pageSize, + 'loadedCount' => count($rows), ]); } diff --git a/app/Views/bag/inventory_stock_barcode_list.php b/app/Views/bag/inventory_stock_barcode_list.php index b28de78..1ff6b4f 100644 --- a/app/Views/bag/inventory_stock_barcode_list.php +++ b/app/Views/bag/inventory_stock_barcode_list.php @@ -1,24 +1,29 @@ > $itemOptions - * @var list> $rows + * @var list> $rows 초기 청크 * @var int $totalBoxes * @var int $totalPacks * @var int $totalSheets + * @var int $pageSize + * @var int $loadedCount */ $bagCode = (string) ($bagCode ?? ''); $keyword = (string) ($keyword ?? ''); $itemOptions = is_array($itemOptions ?? null) ? $itemOptions : []; $rows = is_array($rows ?? null) ? $rows : []; -$totalBoxes = (int) ($totalBoxes ?? 0); -$totalPacks = (int) ($totalPacks ?? 0); -$totalSheets = (int) ($totalSheets ?? 0); -$displayTruncated = (bool) ($displayTruncated ?? false); -$displayLimit = (int) ($displayLimit ?? 2000); -$exportQs = http_build_query(['bag_code' => $bagCode, 'keyword' => $keyword, 'export' => 1]); +$totalBoxes = (int) ($totalBoxes ?? 0); +$totalPacks = (int) ($totalPacks ?? 0); +$totalSheets = (int) ($totalSheets ?? 0); +$pageSize = (int) ($pageSize ?? 200); +$loadedCount = (int) ($loadedCount ?? count($rows)); +$baseUrl = site_url('bag/inventory/stock-barcode-list'); +$exportQs = http_build_query(['bag_code' => $bagCode, 'keyword' => $keyword, 'export' => 1]); +$hasMore = $loadedCount < $totalPacks; ?>
@@ -28,12 +33,12 @@ $exportQs = http_build_query(['bag_code' => $bagCode, 'keyword' => $keyw

현재 창고에 남아있어야 하는(재고 상태) 봉투 번호를 뽑아 실사 시 실물과 대조하세요.

- - 엑셀저장 + + 엑셀저장
-
+
- + - 초기화 + 초기화
@@ -64,17 +69,19 @@ $exportQs = http_build_query(['bag_code' => $bagCode, 'keyword' => $keyw 박스 총 매수 - 기준 + 기준 · 표시 / - -
- 전체 건 중 화면에는 상위 건만 표시됩니다. 전체는 엑셀저장으로 받거나 품목을 선택해 좁혀 보세요. -
- -
- +
@@ -87,9 +94,9 @@ $exportQs = http_build_query(['bag_code' => $bagCode, 'keyword' => $keyw - + - + $r): ?> @@ -117,6 +124,12 @@ $exportQs = http_build_query(['bag_code' => $bagCode, 'keyword' => $keyw
No매수
재고로 남아있는 바코드가 없습니다.
재고로 남아있는 바코드가 없습니다.
+ + +
> + + +
@@ -127,3 +140,132 @@ $exportQs = http_build_query(['bag_code' => $bagCode, 'keyword' => $keyw table { font-size: 11px; } } + +