feat: 실사 재고 바코드 리스트 레이지 로딩 — 스크롤 시 추가 로딩
- 상위 2,000건 상한 제거 → 초기 200건 렌더 후 스크롤 시 청크(200건) 추가 로딩 - 서버: ajax=1&offset&limit 로 청크 JSON 반환, 합계는 SQL 집계(전체 행 미적재) - 클라: IntersectionObserver 자동 로딩 + '더 보기' 폴백 버튼 - 인쇄 시 전체 먼저 로드 후 출력(부분 인쇄 방지) - fetch는 현재 origin 기준 상대경로로 호출(baseURL/포트 불일치에도 동작) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1324,64 +1324,86 @@ SQL);
|
|||||||
return redirect()->to(site_url('bag/inventory'))->with('error', '지자체를 선택해 주세요.');
|
return redirect()->to(site_url('bag/inventory'))->with('error', '지자체를 선택해 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$db = \Config\Database::connect();
|
$db = \Config\Database::connect();
|
||||||
$bagCode = trim((string) ($this->request->getGet('bag_code') ?? ''));
|
$bagCode = trim((string) ($this->request->getGet('bag_code') ?? ''));
|
||||||
$keyword = trim((string) ($this->request->getGet('keyword') ?? ''));
|
$keyword = trim((string) ($this->request->getGet('keyword') ?? ''));
|
||||||
|
$pageSize = 200; // 레이지 로딩 청크 크기
|
||||||
|
|
||||||
// 품목 선택 옵션 — 현재 재고로 남아있는 품목만
|
// 공통 필터(재고 상태 + 품목/키워드)를 빌더에 적용
|
||||||
$itemOptions = $db->table('bag_receiving_pack_code')
|
$applyFilter = static function ($builder) use ($lgIdx, $bagCode, $keyword) {
|
||||||
->select('brpc_bag_code, MAX(brpc_bag_name) AS bag_name, COUNT(*) AS pack_cnt, SUM(brpc_sheet_qty) AS sheet_qty', false)
|
$builder->where('brpc_lg_idx', $lgIdx)->where('brpc_state', 'in_stock');
|
||||||
->where('brpc_lg_idx', $lgIdx)
|
if ($bagCode !== '') {
|
||||||
->where('brpc_state', 'in_stock')
|
$builder->where('brpc_bag_code', $bagCode);
|
||||||
->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;
|
|
||||||
}
|
}
|
||||||
}
|
if ($keyword !== '') {
|
||||||
$totalBoxes = count($boxSet);
|
$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') {
|
if ((string) ($this->request->getGet('export') ?? '') === '1') {
|
||||||
helper('export');
|
helper('export');
|
||||||
$exportRows = [];
|
$exportRows = [];
|
||||||
foreach ($rows as $r) {
|
foreach ($fetchRows(0, 0) as $r) {
|
||||||
$exportRows[] = [
|
$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_lot_no'] ?? ''),
|
||||||
(string) ($r['brpc_box_code'] ?? ''),
|
(string) ($r['brpc_box_code'] ?? ''),
|
||||||
(string) ($r['brpc_pack_code'] ?? ''),
|
(string) ($r['brpc_pack_code'] ?? ''),
|
||||||
@@ -1399,21 +1421,30 @@ SQL);
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 화면 표시는 성능을 위해 상위 N건까지만(합계·엑셀 저장은 항상 전체 기준)
|
// 품목 선택 옵션 — 현재 재고로 남아있는 품목만
|
||||||
$displayLimit = 2000;
|
$itemOptions = $db->table('bag_receiving_pack_code')
|
||||||
$displayTruncated = count($rows) > $displayLimit;
|
->select('brpc_bag_code, MAX(brpc_bag_name) AS bag_name, COUNT(*) AS pack_cnt, SUM(brpc_sheet_qty) AS sheet_qty', false)
|
||||||
$displayRows = $displayTruncated ? array_slice($rows, 0, $displayLimit) : $rows;
|
->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', [
|
return $this->render('실사 재고 바코드 리스트', 'bag/inventory_stock_barcode_list', [
|
||||||
'bagCode' => $bagCode,
|
'bagCode' => $bagCode,
|
||||||
'keyword' => $keyword,
|
'keyword' => $keyword,
|
||||||
'itemOptions' => $itemOptions,
|
'itemOptions' => $itemOptions,
|
||||||
'rows' => $displayRows,
|
'rows' => $rows,
|
||||||
'totalBoxes' => $totalBoxes,
|
'totalBoxes' => $totalBoxes,
|
||||||
'totalPacks' => $totalPacks,
|
'totalPacks' => $totalPacks,
|
||||||
'totalSheets' => $totalSheets,
|
'totalSheets' => $totalSheets,
|
||||||
'displayTruncated' => $displayTruncated,
|
'pageSize' => $pageSize,
|
||||||
'displayLimit' => $displayLimit,
|
'loadedCount' => count($rows),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,29 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* 실사 재고 바코드 리스트 — 현재 창고에 있어야 하는 봉투 번호(바코드) 목록.
|
* 실사 재고 바코드 리스트 — 현재 창고에 있어야 하는 봉투 번호(바코드) 목록.
|
||||||
|
* 초기 1페이지만 렌더하고, 스크롤 시 AJAX 로 다음 청크를 이어 붙인다(레이지 로딩).
|
||||||
* @var string $bagCode
|
* @var string $bagCode
|
||||||
* @var string $keyword
|
* @var string $keyword
|
||||||
* @var list<array<string,mixed>> $itemOptions
|
* @var list<array<string,mixed>> $itemOptions
|
||||||
* @var list<array<string,mixed>> $rows
|
* @var list<array<string,mixed>> $rows 초기 청크
|
||||||
* @var int $totalBoxes
|
* @var int $totalBoxes
|
||||||
* @var int $totalPacks
|
* @var int $totalPacks
|
||||||
* @var int $totalSheets
|
* @var int $totalSheets
|
||||||
|
* @var int $pageSize
|
||||||
|
* @var int $loadedCount
|
||||||
*/
|
*/
|
||||||
$bagCode = (string) ($bagCode ?? '');
|
$bagCode = (string) ($bagCode ?? '');
|
||||||
$keyword = (string) ($keyword ?? '');
|
$keyword = (string) ($keyword ?? '');
|
||||||
$itemOptions = is_array($itemOptions ?? null) ? $itemOptions : [];
|
$itemOptions = is_array($itemOptions ?? null) ? $itemOptions : [];
|
||||||
$rows = is_array($rows ?? null) ? $rows : [];
|
$rows = is_array($rows ?? null) ? $rows : [];
|
||||||
$totalBoxes = (int) ($totalBoxes ?? 0);
|
$totalBoxes = (int) ($totalBoxes ?? 0);
|
||||||
$totalPacks = (int) ($totalPacks ?? 0);
|
$totalPacks = (int) ($totalPacks ?? 0);
|
||||||
$totalSheets = (int) ($totalSheets ?? 0);
|
$totalSheets = (int) ($totalSheets ?? 0);
|
||||||
$displayTruncated = (bool) ($displayTruncated ?? false);
|
$pageSize = (int) ($pageSize ?? 200);
|
||||||
$displayLimit = (int) ($displayLimit ?? 2000);
|
$loadedCount = (int) ($loadedCount ?? count($rows));
|
||||||
$exportQs = http_build_query(['bag_code' => $bagCode, 'keyword' => $keyword, 'export' => 1]);
|
$baseUrl = site_url('bag/inventory/stock-barcode-list');
|
||||||
|
$exportQs = http_build_query(['bag_code' => $bagCode, 'keyword' => $keyword, 'export' => 1]);
|
||||||
|
$hasMore = $loadedCount < $totalPacks;
|
||||||
?>
|
?>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<section class="border border-gray-300 rounded-lg bg-white p-3 no-print">
|
<section class="border border-gray-300 rounded-lg bg-white p-3 no-print">
|
||||||
@@ -28,12 +33,12 @@ $exportQs = http_build_query(['bag_code' => $bagCode, 'keyword' => $keyw
|
|||||||
<p class="text-xs text-gray-500 mt-0.5">현재 창고에 남아있어야 하는(재고 상태) 봉투 번호를 뽑아 실사 시 실물과 대조하세요.</p>
|
<p class="text-xs text-gray-500 mt-0.5">현재 창고에 남아있어야 하는(재고 상태) 봉투 번호를 뽑아 실사 시 실물과 대조하세요.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 shrink-0">
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
<button type="button" onclick="window.print()" class="border border-gray-300 text-gray-600 px-3 py-1.5 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
<button type="button" id="sbl-print" class="border border-gray-300 text-gray-600 px-3 py-1.5 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||||
<a href="<?= esc(site_url('bag/inventory/stock-barcode-list') . '?' . $exportQs) ?>" class="border border-green-500 text-green-700 px-3 py-1.5 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
|
<a href="<?= esc($baseUrl . '?' . $exportQs) ?>" class="border border-green-500 text-green-700 px-3 py-1.5 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="get" action="<?= esc(site_url('bag/inventory/stock-barcode-list')) ?>" class="flex flex-wrap items-end gap-2 text-sm">
|
<form method="get" action="<?= esc($baseUrl) ?>" class="flex flex-wrap items-end gap-2 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-bold text-gray-600 mb-1">품목</label>
|
<label class="block text-xs font-bold text-gray-600 mb-1">품목</label>
|
||||||
<select name="bag_code" class="border border-gray-300 rounded px-2 py-1 w-56">
|
<select name="bag_code" class="border border-gray-300 rounded px-2 py-1 w-56">
|
||||||
@@ -51,9 +56,9 @@ $exportQs = http_build_query(['bag_code' => $bagCode, 'keyword' => $keyw
|
|||||||
<label class="block text-xs font-bold text-gray-600 mb-1">번호 검색</label>
|
<label class="block text-xs font-bold text-gray-600 mb-1">번호 검색</label>
|
||||||
<input type="text" name="keyword" value="<?= esc($keyword, 'attr') ?>" placeholder="박스/팩/시트번호/LOT" class="border border-gray-300 rounded px-2 py-1 w-56"/>
|
<input type="text" name="keyword" value="<?= esc($keyword, 'attr') ?>" placeholder="박스/팩/시트번호/LOT" class="border border-gray-300 rounded px-2 py-1 w-56"/>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="bg-navy-700 text-white px-4 py-1.5 rounded-sm text-sm hover:opacity-90 transition" style="background:#1f2b4d;">조회</button>
|
<button type="submit" class="text-white px-4 py-1.5 rounded-sm text-sm hover:opacity-90 transition" style="background:#1f2b4d;">조회</button>
|
||||||
<?php if ($bagCode !== '' || $keyword !== ''): ?>
|
<?php if ($bagCode !== '' || $keyword !== ''): ?>
|
||||||
<a href="<?= esc(site_url('bag/inventory/stock-barcode-list')) ?>" class="text-gray-500 hover:underline text-sm pb-1">초기화</a>
|
<a href="<?= esc($baseUrl) ?>" class="text-gray-500 hover:underline text-sm pb-1">초기화</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
@@ -64,17 +69,19 @@ $exportQs = http_build_query(['bag_code' => $bagCode, 'keyword' => $keyw
|
|||||||
<span class="text-gray-600">박스 <b class="text-gray-900"><?= number_format($totalBoxes) ?></b></span>
|
<span class="text-gray-600">박스 <b class="text-gray-900"><?= number_format($totalBoxes) ?></b></span>
|
||||||
<span class="text-gray-600">팩 <b class="text-gray-900"><?= number_format($totalPacks) ?></b></span>
|
<span class="text-gray-600">팩 <b class="text-gray-900"><?= number_format($totalPacks) ?></b></span>
|
||||||
<span class="text-gray-600">총 매수 <b class="text-gray-900"><?= number_format($totalSheets) ?></b></span>
|
<span class="text-gray-600">총 매수 <b class="text-gray-900"><?= number_format($totalSheets) ?></b></span>
|
||||||
<span class="text-gray-400 text-xs ml-auto no-print"><?= esc(date('Y-m-d H:i')) ?> 기준</span>
|
<span class="text-gray-400 text-xs ml-auto no-print"><?= esc(date('Y-m-d H:i')) ?> 기준 · 표시 <b id="sbl-shown"><?= number_format($loadedCount) ?></b>/<?= number_format($totalPacks) ?></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if ($displayTruncated): ?>
|
|
||||||
<div class="mb-2 text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 no-print">
|
|
||||||
전체 <?= number_format($totalPacks) ?>건 중 화면에는 상위 <?= number_format($displayLimit) ?>건만 표시됩니다. 전체는 <b>엑셀저장</b>으로 받거나 <b>품목</b>을 선택해 좁혀 보세요.
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full text-sm border-collapse">
|
<table class="w-full text-sm border-collapse"
|
||||||
|
id="sbl-table"
|
||||||
|
data-endpoint="<?= esc($baseUrl, 'attr') ?>"
|
||||||
|
data-bag-code="<?= esc($bagCode, 'attr') ?>"
|
||||||
|
data-keyword="<?= esc($keyword, 'attr') ?>"
|
||||||
|
data-offset="<?= (int) $loadedCount ?>"
|
||||||
|
data-page-size="<?= (int) $pageSize ?>"
|
||||||
|
data-total="<?= (int) $totalPacks ?>"
|
||||||
|
data-has-more="<?= $hasMore ? '1' : '0' ?>">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-gray-100 text-gray-700">
|
<tr class="bg-gray-100 text-gray-700">
|
||||||
<th class="border border-gray-300 px-2 py-1 text-center w-12">No</th>
|
<th class="border border-gray-300 px-2 py-1 text-center w-12">No</th>
|
||||||
@@ -87,9 +94,9 @@ $exportQs = http_build_query(['bag_code' => $bagCode, 'keyword' => $keyw
|
|||||||
<th class="border border-gray-300 px-2 py-1 text-right w-20">매수</th>
|
<th class="border border-gray-300 px-2 py-1 text-right w-20">매수</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody id="sbl-tbody">
|
||||||
<?php if ($rows === []): ?>
|
<?php if ($rows === []): ?>
|
||||||
<tr><td colspan="8" class="border border-gray-300 px-2 py-6 text-center text-gray-400">재고로 남아있는 바코드가 없습니다.</td></tr>
|
<tr id="sbl-empty"><td colspan="8" class="border border-gray-300 px-2 py-6 text-center text-gray-400">재고로 남아있는 바코드가 없습니다.</td></tr>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($rows as $i => $r): ?>
|
<?php foreach ($rows as $i => $r): ?>
|
||||||
<tr class="hover:bg-blue-50/40">
|
<tr class="hover:bg-blue-50/40">
|
||||||
@@ -117,6 +124,12 @@ $exportQs = http_build_query(['bag_code' => $bagCode, 'keyword' => $keyw
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 레이지 로딩 감시 지점 + 수동 더보기 폴백 -->
|
||||||
|
<div id="sbl-sentinel" class="no-print py-3 text-center text-sm text-gray-400" <?= $hasMore ? '' : 'style="display:none;"' ?>>
|
||||||
|
<span id="sbl-loading" style="display:none;">불러오는 중…</span>
|
||||||
|
<button type="button" id="sbl-more" class="border border-gray-300 text-gray-600 px-4 py-1.5 rounded-sm hover:bg-gray-50 transition">더 보기</button>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -127,3 +140,132 @@ $exportQs = http_build_query(['bag_code' => $bagCode, 'keyword' => $keyw
|
|||||||
table { font-size: 11px; }
|
table { font-size: 11px; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var table = document.getElementById('sbl-table');
|
||||||
|
if (!table) return;
|
||||||
|
var tbody = document.getElementById('sbl-tbody');
|
||||||
|
var sentinel = document.getElementById('sbl-sentinel');
|
||||||
|
var loading = document.getElementById('sbl-loading');
|
||||||
|
var moreBtn = document.getElementById('sbl-more');
|
||||||
|
var shownEl = document.getElementById('sbl-shown');
|
||||||
|
var printBtn = document.getElementById('sbl-print');
|
||||||
|
|
||||||
|
// baseURL(예: 로컬 :8080)과 실제 접속 origin(:8045 등)이 달라도 항상 현재 origin 기준으로 호출
|
||||||
|
var endpoint = table.getAttribute('data-endpoint');
|
||||||
|
try { endpoint = location.origin + new URL(endpoint, location.href).pathname; }
|
||||||
|
catch (e) { endpoint = location.pathname; }
|
||||||
|
var bagCode = table.getAttribute('data-bag-code') || '';
|
||||||
|
var keyword = table.getAttribute('data-keyword') || '';
|
||||||
|
var pageSize = parseInt(table.getAttribute('data-page-size'), 10) || 200;
|
||||||
|
var offset = parseInt(table.getAttribute('data-offset'), 10) || 0;
|
||||||
|
var total = parseInt(table.getAttribute('data-total'), 10) || 0;
|
||||||
|
var hasMore = table.getAttribute('data-has-more') === '1';
|
||||||
|
var busy = false;
|
||||||
|
|
||||||
|
function esc(s) {
|
||||||
|
return String(s == null ? '' : s)
|
||||||
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
function fmt(n) { return (n || 0).toLocaleString(); }
|
||||||
|
|
||||||
|
function rowHtml(r, no) {
|
||||||
|
return '<tr class="hover:bg-blue-50/40">'
|
||||||
|
+ '<td class="border border-gray-300 px-2 py-1 text-center text-gray-500">' + no + '</td>'
|
||||||
|
+ '<td class="border border-gray-300 px-2 py-1">' + esc(r.name) + '</td>'
|
||||||
|
+ '<td class="border border-gray-300 px-2 py-1">' + esc(r.lot) + '</td>'
|
||||||
|
+ '<td class="border border-gray-300 px-2 py-1 font-mono text-xs">' + esc(r.box) + '</td>'
|
||||||
|
+ '<td class="border border-gray-300 px-2 py-1 font-mono text-xs">' + esc(r.pack) + '</td>'
|
||||||
|
+ '<td class="border border-gray-300 px-2 py-1 font-mono text-xs">' + esc(r.start) + '</td>'
|
||||||
|
+ '<td class="border border-gray-300 px-2 py-1 font-mono text-xs">' + esc(r.end) + '</td>'
|
||||||
|
+ '<td class="border border-gray-300 px-2 py-1 text-right">' + fmt(r.qty) + '</td>'
|
||||||
|
+ '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUrl(off, lim) {
|
||||||
|
var p = new URLSearchParams();
|
||||||
|
p.set('ajax', '1');
|
||||||
|
p.set('offset', off);
|
||||||
|
p.set('limit', lim);
|
||||||
|
if (bagCode) p.set('bag_code', bagCode);
|
||||||
|
if (keyword) p.set('keyword', keyword);
|
||||||
|
return endpoint + '?' + p.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendRows(rows) {
|
||||||
|
if (!rows || !rows.length) return;
|
||||||
|
var html = '';
|
||||||
|
for (var i = 0; i < rows.length; i++) {
|
||||||
|
html += rowHtml(rows[i], offset + i + 1);
|
||||||
|
}
|
||||||
|
tbody.insertAdjacentHTML('beforeend', html);
|
||||||
|
offset += rows.length;
|
||||||
|
if (shownEl) shownEl.textContent = fmt(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 다음 청크 한 번 로드
|
||||||
|
function loadMore() {
|
||||||
|
if (busy || !hasMore) return Promise.resolve();
|
||||||
|
busy = true;
|
||||||
|
if (loading) loading.style.display = 'inline';
|
||||||
|
if (moreBtn) moreBtn.style.display = 'none';
|
||||||
|
return fetch(buildUrl(offset, pageSize), { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||||
|
.then(function (res) { return res.json(); })
|
||||||
|
.then(function (data) {
|
||||||
|
appendRows(data.rows);
|
||||||
|
hasMore = !!data.hasMore;
|
||||||
|
if (!hasMore && sentinel) sentinel.style.display = 'none';
|
||||||
|
})
|
||||||
|
.catch(function () { /* 네트워크 오류 시 더보기 버튼 유지 */ })
|
||||||
|
.finally(function () {
|
||||||
|
busy = false;
|
||||||
|
if (loading) loading.style.display = 'none';
|
||||||
|
if (hasMore && moreBtn) moreBtn.style.display = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 남은 전체를 한 번에 로드(인쇄용)
|
||||||
|
function loadAll() {
|
||||||
|
if (!hasMore) return Promise.resolve();
|
||||||
|
busy = true;
|
||||||
|
if (loading) loading.style.display = 'inline';
|
||||||
|
if (moreBtn) moreBtn.style.display = 'none';
|
||||||
|
return fetch(buildUrl(offset, 5000), { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||||
|
.then(function (res) { return res.json(); })
|
||||||
|
.then(function (data) {
|
||||||
|
appendRows(data.rows);
|
||||||
|
hasMore = !!data.hasMore;
|
||||||
|
busy = false;
|
||||||
|
if (hasMore) return loadAll();
|
||||||
|
if (sentinel) sentinel.style.display = 'none';
|
||||||
|
if (loading) loading.style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 스크롤 자동 로딩(IntersectionObserver) — 미지원/iframe 대비 더보기 버튼 폴백 병행
|
||||||
|
if (hasMore && 'IntersectionObserver' in window) {
|
||||||
|
var io = new IntersectionObserver(function (entries) {
|
||||||
|
if (entries[0] && entries[0].isIntersecting) loadMore();
|
||||||
|
}, { rootMargin: '300px' });
|
||||||
|
io.observe(sentinel);
|
||||||
|
}
|
||||||
|
if (moreBtn) moreBtn.addEventListener('click', loadMore);
|
||||||
|
|
||||||
|
// 인쇄 — 전체를 먼저 불러온 뒤 출력(부분만 인쇄되는 것 방지)
|
||||||
|
if (printBtn) {
|
||||||
|
printBtn.addEventListener('click', function () {
|
||||||
|
if (!hasMore) { window.print(); return; }
|
||||||
|
printBtn.disabled = true;
|
||||||
|
var orig = printBtn.textContent;
|
||||||
|
printBtn.textContent = '전체 불러오는 중…';
|
||||||
|
loadAll().then(function () {
|
||||||
|
printBtn.disabled = false;
|
||||||
|
printBtn.textContent = orig;
|
||||||
|
window.print();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user