508 lines
26 KiB
PHP
508 lines
26 KiB
PHP
<?php
|
||
// 자재관리 > 리스트
|
||
$CURRENT_SECTION = 'item';
|
||
include '../inc/header.php';
|
||
?>
|
||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||
|
||
<!-- 상단 탭 -->
|
||
<ul class="nav nav-tabs mb-3">
|
||
<li class="nav-item"><a class="nav-link active" href="/tenant/material/list.php">자재 관리</a></li>
|
||
<li class="nav-item"><a class="nav-link" href="/tenant/product/model_list.php">모델 관리</a></li>
|
||
<li class="nav-item"><a class="nav-link" href="/tenant/product/bom_editor.php">BOM 관리</a></li>
|
||
</ul>
|
||
|
||
<!-- 툴바 -->
|
||
<div class="d-flex flex-wrap gap-2 align-items-center mb-2">
|
||
<div class="d-flex align-items-center gap-2">
|
||
<label class="text-muted">분류</label>
|
||
<select class="form-select form-select-sm" id="filterCategory" style="width:160px;">
|
||
<option value="">전체</option>
|
||
<option value="자재">자재</option>
|
||
<option value="부품">부품</option>
|
||
<option value="소재">소재</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="input-group" style="max-width:340px;">
|
||
<input type="text" class="form-control form-control-sm" id="keyword" placeholder="품목명/제조사/코드 검색">
|
||
<button class="btn btn-outline-secondary btn-sm" id="btnSearch">Search</button>
|
||
</div>
|
||
|
||
<div class="d-flex align-items-center gap-2">
|
||
<label class="text-muted">표시</label>
|
||
<select id="pageSize" class="form-select form-select-sm" style="width:80px;">
|
||
<option>10</option><option selected>20</option><option>30</option><option>50</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="ms-auto small text-muted" id="totalInfo">총 0건</div>
|
||
<button class="btn btn-primary btn-sm" id="btnOpenCreate">신규등록</button>
|
||
</div>
|
||
|
||
<!-- 리스트 -->
|
||
<div class="table-responsive border rounded">
|
||
<table class="table table-hover table-striped align-middle m-0">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th style="width:70px;">NO</th>
|
||
<th style="width:120px;">분류</th>
|
||
<th>품목명</th>
|
||
<th style="width:90px;">단위</th>
|
||
<th style="width:140px;">제조사</th>
|
||
<th style="width:120px;">이미지</th>
|
||
<th style="width:140px;" class="text-center">관리</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="listBody"></tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- 페이징 -->
|
||
<div class="d-flex justify-content-center mt-3">
|
||
<nav id="pagerNav" aria-label="Page navigation"></nav>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 등록/수정 모달 -->
|
||
<div class="modal fade" id="materialModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||
<form class="modal-content needs-validation" id="materialForm" novalidate>
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="materialModalTitle">자재 등록</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="닫기"></button>
|
||
</div>
|
||
|
||
<!-- === 모달 본문 (동적 규격/기타 + 품목명 자동생성) === -->
|
||
<div class="modal-body">
|
||
<input type="hidden" id="matId">
|
||
|
||
<!-- 기본정보 -->
|
||
<details open class="mb-3">
|
||
<summary class="fw-semibold">기본정보 입력</summary>
|
||
<div class="row g-3 mt-2">
|
||
<div class="col-md-3">
|
||
<label class="form-label">분류 <span class="text-danger">*</span></label>
|
||
<select class="form-select" id="matCategory" required>
|
||
<option value="">선택</option>
|
||
<option>자재</option>
|
||
<option>부품</option>
|
||
<option>소재</option>
|
||
</select>
|
||
<div class="invalid-feedback">분류를 선택해 주세요.</div>
|
||
</div>
|
||
|
||
<div class="col-md-4">
|
||
<label class="form-label">자재명(베이스) <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="matBaseName" placeholder="예: 절곡판" required>
|
||
<div class="invalid-feedback">자재명을 입력해 주세요.</div>
|
||
</div>
|
||
|
||
<div class="col-md-3">
|
||
<label class="form-label">단위</label>
|
||
<input type="text" class="form-control" id="matUnit" placeholder="EA, mm, kg …">
|
||
</div>
|
||
|
||
<div class="col-12">
|
||
<label class="form-label">품목명(자동생성)</label>
|
||
<input type="text" class="form-control" id="matName" placeholder="자재명 + 규격값/단위" readonly>
|
||
<div class="form-text">규격을 추가하면 자동으로 갱신됩니다. (예) 절곡판 1.5t 20mm</div>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
|
||
<!-- 규격정보 -->
|
||
<details open class="mb-3">
|
||
<summary class="fw-semibold d-flex align-items-center">
|
||
규격정보 입력
|
||
<button type="button" class="btn btn-sm btn-outline-primary ms-2" id="btnAddSpec">+ 추가</button>
|
||
</summary>
|
||
<div id="specRows" class="mt-2"></div>
|
||
</details>
|
||
|
||
<!-- 기타정보 -->
|
||
<details class="mb-3">
|
||
<summary class="fw-semibold d-flex align-items-center">
|
||
기타정보 입력
|
||
<button type="button" class="btn btn-sm btn-outline-primary ms-2" id="btnAddExtra">+ 추가</button>
|
||
</summary>
|
||
<div id="extraRows" class="mt-2"></div>
|
||
</details>
|
||
|
||
<!-- 부가 입력 -->
|
||
<div class="row g-3">
|
||
<div class="col-md-4">
|
||
<label class="form-label">제조사</label>
|
||
<input type="text" class="form-control" id="matMaker">
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label">품목코드</label>
|
||
<input type="text" class="form-control" id="matCode">
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label">안전재고</label>
|
||
<input type="number" class="form-control" id="matSafety" min="0" step="1" value="0">
|
||
</div>
|
||
<div class="col-12">
|
||
<label class="form-label">이미지 업로드</label>
|
||
<input class="form-control" type="file" id="matImages" accept="image/*" multiple>
|
||
<div class="form-text">여러 장 업로드 가능 (프로토타입: 미리보기만)</div>
|
||
</div>
|
||
<div class="col-12">
|
||
<label class="form-label">비고</label>
|
||
<textarea class="form-control" id="matMemo" rows="2"></textarea>
|
||
</div>
|
||
<div class="col-12">
|
||
<div class="d-flex gap-2 flex-wrap" id="previewWrap"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 모달 푸터 -->
|
||
<div class="modal-footer">
|
||
<div class="me-auto small text-muted" id="modalHint"></div>
|
||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">취소</button>
|
||
<button type="submit" class="btn btn-primary" id="btnSubmit">저장</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 이미지 라이트박스 -->
|
||
<div class="modal fade" id="imgModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||
<div class="modal-content bg-dark">
|
||
<div class="modal-body p-0">
|
||
<img id="imgModalImg" src="" alt="preview" class="w-100" style="display:block;">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 토스트 -->
|
||
<div class="position-fixed bottom-0 end-0 p-3" style="z-index:1080">
|
||
<div id="snack" class="toast align-items-center text-bg-primary border-0" role="alert">
|
||
<div class="d-flex">
|
||
<div class="toast-body" id="snackMsg">Saved</div>
|
||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.thumb{width:56px;height:40px;object-fit:cover;border-radius:4px;cursor:zoom-in;border:1px solid rgba(0,0,0,.06)}
|
||
.table > :not(caption) > * > * { vertical-align: middle; }
|
||
details > summary { cursor: pointer; }
|
||
</style>
|
||
|
||
<!-- ===== 페이지 스크립트 (jQuery 불필요) ===== -->
|
||
<script>
|
||
(function(){
|
||
// ---------- 샘플 데이터 ----------
|
||
function buildSampleData(count=173){
|
||
const cats=['자재','부품','소재'], makers=['KG스틸','대한','경동기업','한빛','에이스','대림'], units=['EA','M','KG','매','BOX'];
|
||
return Array.from({length:count}, (_,i)=>({
|
||
id: i+1,
|
||
category: cats[(i+1)%cats.length],
|
||
name: `${cats[(i+1)%cats.length]} 샘플 품목 #${i+1} (${['A','B','C','D'][i%4]})`,
|
||
base_name: '',
|
||
unit: units[i%units.length],
|
||
maker: makers[i%makers.length],
|
||
code: `MAT-${String(i+1).padStart(5,'0')}`,
|
||
safety: (i%7===0)?50:0,
|
||
images: (i%5===0)?[`https://picsum.photos/seed/m${i+1}/120/80`]:[],
|
||
specs:[], extras:[]
|
||
}));
|
||
}
|
||
let MATERIALS = buildSampleData();
|
||
|
||
// ---------- 상태 ----------
|
||
let PAGE = 1, SIZE = 20;
|
||
|
||
// ---------- 헬퍼 ----------
|
||
const $ = sel => document.querySelector(sel);
|
||
const $$ = sel => Array.from(document.querySelectorAll(sel));
|
||
|
||
function getFiltered(){
|
||
const cat = ($('#filterCategory')?.value || '').trim();
|
||
const kw = ($('#keyword')?.value || '').trim().toLowerCase();
|
||
return MATERIALS.filter(m=>{
|
||
const okCat = !cat || m.category===cat;
|
||
const hay = (m.name+' '+(m.maker||'')+' '+(m.code||'')).toLowerCase();
|
||
const okKw = !kw || hay.includes(kw);
|
||
return okCat && okKw;
|
||
});
|
||
}
|
||
|
||
// ---------- 렌더 ----------
|
||
function renderList(){
|
||
const data = getFiltered();
|
||
const total = data.length;
|
||
const pages = Math.max(1, Math.ceil(total / SIZE));
|
||
if (PAGE>pages) PAGE=pages;
|
||
|
||
const start=(PAGE-1)*SIZE, end=start+SIZE;
|
||
const pageData = data.slice(start,end);
|
||
|
||
const body = $('#listBody');
|
||
body.innerHTML = pageData.map((m,i)=>`
|
||
<tr data-id="${m.id}">
|
||
<td>${start+i+1}</td>
|
||
<td>${m.category||''}</td>
|
||
<td>${m.name||''}</td>
|
||
<td>${m.unit||''}</td>
|
||
<td>${m.maker||''}</td>
|
||
<td>${
|
||
(m.images||[]).slice(0,2).map(src=>`<img src="${src}" class="thumb me-1" data-src="${src}">`).join('') +
|
||
((m.images&&m.images.length>2)? `<span class="text-muted small">+${m.images.length-2}</span>`:'')
|
||
}</td>
|
||
<td class="text-center">
|
||
<button class="btn btn-sm btn-outline-primary me-1 btnEdit">수정</button>
|
||
<button class="btn btn-sm btn-outline-danger btnDel">삭제</button>
|
||
</td>
|
||
</tr>
|
||
`).join('') || `<tr><td colspan="7" class="text-center text-muted py-4">데이터가 없습니다.</td></tr>`;
|
||
|
||
// 총건수
|
||
$('#totalInfo').textContent = `총 ${total.toLocaleString()}건 · ${total? (start+1):0}-${Math.min(end,total)} 표시`;
|
||
|
||
renderPager(pages);
|
||
}
|
||
|
||
function renderPager(pages){
|
||
const nav = $('#pagerNav');
|
||
const win=7;
|
||
let sp=Math.max(1, PAGE-Math.floor(win/2));
|
||
let ep=Math.min(pages, sp+win-1);
|
||
sp=Math.max(1, ep-win+1);
|
||
|
||
let html = `<ul class="pagination pagination-sm m-0">`;
|
||
html += `<li class="page-item ${PAGE===1?'disabled':''}">
|
||
<a class="page-link" href="#" data-go="first" aria-label="First">«</a></li>`;
|
||
html += `<li class="page-item ${PAGE===1?'disabled':''}">
|
||
<a class="page-link" href="#" data-go="${PAGE-1}" aria-label="Prev">‹</a></li>`;
|
||
for(let p=sp;p<=ep;p++){
|
||
html += `<li class="page-item ${p===PAGE?'active':''}">
|
||
<a class="page-link" href="#" data-go="${p}">${p}</a></li>`;
|
||
}
|
||
html += `<li class="page-item ${PAGE===pages?'disabled':''}">
|
||
<a class="page-link" href="#" data-go="${PAGE+1}" aria-label="Next">›</a></li>`;
|
||
html += `<li class="page-item ${PAGE===pages?'disabled':''}">
|
||
<a class="page-link" href="#" data-go="last" aria-label="Last">»</a></li>`;
|
||
html += `</ul>`;
|
||
nav.innerHTML = html;
|
||
}
|
||
|
||
// ---------- 규격/기타 동적행 + 품목명 자동생성 ----------
|
||
const UNIT_OPTIONS = ['EA','mm','cm','m','kg','t','L'];
|
||
function specRowTpl(data={label:'규격', value:'', unit:''}) {
|
||
const uid='spec_'+Math.random().toString(36).slice(2,8);
|
||
const opts=UNIT_OPTIONS.map(u=>`<option ${data.unit===u?'selected':''}>${u}</option>`).join('');
|
||
return `
|
||
<div class="row g-2 align-items-center border rounded p-2 mb-2" data-type="spec" id="${uid}">
|
||
<div class="col-auto"><button type="button" class="btn btn-sm btn-outline-secondary btnDelRow">-</button></div>
|
||
<div class="col-md-2"><label class="form-label m-0 small">규격</label>
|
||
<input type="text" class="form-control form-control-sm spec-label" value="${data.label||''}" placeholder="예: 두께"></div>
|
||
<div class="col-md-4"><label class="form-label m-0 small">값</label>
|
||
<input type="text" class="form-control form-control-sm spec-value" value="${data.value||''}" placeholder="예: 1.5"></div>
|
||
<div class="col-md-2"><label class="form-label m-0 small">단위</label>
|
||
<select class="form-select form-select-sm spec-unit"><option value="">선택</option>${opts}</select></div>
|
||
</div>`;
|
||
}
|
||
function extraRowTpl(data={key:'', value:''}) {
|
||
const uid='extra_'+Math.random().toString(36).slice(2,8);
|
||
return `
|
||
<div class="row g-2 align-items-center border rounded p-2 mb-2" data-type="extra" id="${uid}">
|
||
<div class="col-auto"><button type="button" class="btn btn-sm btn-outline-secondary btnDelRow">-</button></div>
|
||
<div class="col-md-3"><label class="form-label m-0 small">항목</label>
|
||
<input type="text" class="form-control form-control-sm extra-key" value="${data.key||''}" placeholder="예: 색상"></div>
|
||
<div class="col-md-6"><label class="form-label m-0 small">값</label>
|
||
<input type="text" class="form-control form-control-sm extra-value" value="${data.value||''}" placeholder="예: 블루"></div>
|
||
</div>`;
|
||
}
|
||
function addSpecRow(data){ $('#specRows').insertAdjacentHTML('beforeend', specRowTpl(data)); wireSpecEvents(); composeName(); }
|
||
function addExtraRow(data){ $('#extraRows').insertAdjacentHTML('beforeend', extraRowTpl(data)); }
|
||
function wireSpecEvents(){
|
||
$$('#specRows .spec-value, #specRows .spec-unit, #matBaseName').forEach(el=>{
|
||
el.oninput = composeName; el.onchange = composeName;
|
||
});
|
||
}
|
||
function composeName(){
|
||
const base = ($('#matBaseName')?.value||'').trim();
|
||
const parts=[];
|
||
$$('#specRows [data-type="spec"]').forEach(row=>{
|
||
const v=row.querySelector('.spec-value')?.value.trim();
|
||
const u=row.querySelector('.spec-unit')?.value.trim();
|
||
if (v) parts.push(u?`${v}${u}`:v);
|
||
});
|
||
$('#matName').value = [base, ...parts].filter(Boolean).join(' ');
|
||
}
|
||
document.addEventListener('click', e=>{
|
||
if (e.target.id==='btnAddSpec') addSpecRow({});
|
||
if (e.target.id==='btnAddExtra') addExtraRow({});
|
||
if (e.target.classList.contains('btnDelRow')){
|
||
e.preventDefault();
|
||
const row=e.target.closest('.row'); row?.parentNode?.removeChild(row);
|
||
composeName();
|
||
}
|
||
});
|
||
|
||
function resetDynamicRows(specs=[], extras=[]){
|
||
$('#specRows').innerHTML=''; $('#extraRows').innerHTML='';
|
||
if (!specs.length) specs=[{}];
|
||
specs.forEach(s=> addSpecRow(s));
|
||
extras.forEach(x=> addExtraRow(x));
|
||
composeName();
|
||
}
|
||
|
||
// ---------- 모달/토스트 ----------
|
||
let materialModal, imgModal, snackToast;
|
||
function ensureBootstrapInstances(){
|
||
// 부트스트랩 JS가 있다면 인스턴스 생성
|
||
if (window.bootstrap){
|
||
materialModal = materialModal || new bootstrap.Modal('#materialModal');
|
||
imgModal = imgModal || new bootstrap.Modal('#imgModal');
|
||
snackToast = snackToast || new bootstrap.Toast('#snack');
|
||
}
|
||
}
|
||
|
||
// ---------- 이벤트 바인딩 ----------
|
||
// 신규등록
|
||
document.getElementById('btnOpenCreate').addEventListener('click', ()=>{
|
||
ensureBootstrapInstances();
|
||
$('#materialModalTitle').textContent='자재 등록';
|
||
$('#btnSubmit').textContent='저장';
|
||
$('#materialForm').reset();
|
||
$('#matId').value='';
|
||
$('#previewWrap').innerHTML='';
|
||
$('#modalHint').textContent='※ 필수 입력: 분류, 자재명(베이스)';
|
||
resetDynamicRows();
|
||
materialModal ? materialModal.show() : document.getElementById('materialModal').classList.add('show');
|
||
});
|
||
|
||
// 이미지 미리보기
|
||
document.getElementById('matImages').addEventListener('change', function(){
|
||
const wrap=$('#previewWrap'); wrap.innerHTML='';
|
||
Array.from(this.files).forEach(f=>{
|
||
const url=URL.createObjectURL(f);
|
||
wrap.insertAdjacentHTML('beforeend', `<img src="${url}" class="thumb">`);
|
||
});
|
||
});
|
||
|
||
// 저장(등록/수정)
|
||
document.getElementById('materialForm').addEventListener('submit', function(e){
|
||
e.preventDefault(); e.stopPropagation();
|
||
this.classList.add('was-validated');
|
||
if (!this.checkValidity()) return;
|
||
|
||
const specs = $$('#specRows [data-type="spec"]').map(row=>({
|
||
label: row.querySelector('.spec-label')?.value.trim() || '규격',
|
||
value: row.querySelector('.spec-value')?.value.trim() || '',
|
||
unit : row.querySelector('.spec-unit')?.value.trim() || ''
|
||
}));
|
||
const extras = $$('#extraRows [data-type="extra"]').map(row=>({
|
||
key: row.querySelector('.extra-key')?.value.trim() || '',
|
||
value: row.querySelector('.extra-value')?.value.trim() || ''
|
||
}));
|
||
|
||
const dto = {
|
||
id: $('#matId').value ? parseInt($('#matId').value,10) : null,
|
||
category: $('#matCategory').value,
|
||
base_name: $('#matBaseName').value.trim(),
|
||
name: $('#matName').value.trim(),
|
||
unit: $('#matUnit').value.trim(),
|
||
maker: $('#matMaker').value.trim(),
|
||
code: $('#matCode').value.trim(),
|
||
safety: parseInt($('#matSafety').value,10) || 0,
|
||
memo: $('#matMemo').value.trim(),
|
||
images: [],
|
||
specs, extras
|
||
};
|
||
const files = $('#matImages').files; for (const f of files) dto.images.push(URL.createObjectURL(f));
|
||
|
||
if (dto.id){
|
||
const idx = MATERIALS.findIndex(m=>m.id===dto.id);
|
||
if (idx>=0) MATERIALS[idx] = {...MATERIALS[idx], ...dto};
|
||
if (snackToast){ $('#snackMsg').textContent='수정되었습니다.'; snackToast.show(); }
|
||
}else{
|
||
dto.id = (MATERIALS.reduce((mx,m)=>Math.max(mx,m.id),0)||0)+1;
|
||
MATERIALS.unshift(dto);
|
||
if (snackToast){ $('#snackMsg').textContent='등록되었습니다.'; snackToast.show(); }
|
||
}
|
||
materialModal?.hide();
|
||
PAGE=1; renderList();
|
||
});
|
||
|
||
// 수정/삭제
|
||
document.addEventListener('click', e=>{
|
||
if (e.target.classList.contains('btnEdit')){
|
||
ensureBootstrapInstances();
|
||
const id = parseInt(e.target.closest('tr').dataset.id,10);
|
||
const m = MATERIALS.find(x=>x.id===id); if(!m) return;
|
||
|
||
$('#materialModalTitle').textContent='자재 수정';
|
||
$('#btnSubmit').textContent='수정';
|
||
|
||
$('#matId').value=m.id;
|
||
$('#matCategory').value=m.category||'';
|
||
$('#matBaseName').value=m.base_name || (m.name||'').split(' ')[0] || '';
|
||
$('#matUnit').value=m.unit||'';
|
||
$('#matName').value=m.name||'';
|
||
$('#matMaker').value=m.maker||'';
|
||
$('#matCode').value=m.code||'';
|
||
$('#matSafety').value=m.safety||0;
|
||
$('#matMemo').value=m.memo||'';
|
||
$('#matImages').value='';
|
||
const wrap=$('#previewWrap'); wrap.innerHTML='';
|
||
(m.images||[]).forEach(src=> wrap.insertAdjacentHTML('beforeend', `<img src="${src}" class="thumb">`));
|
||
|
||
resetDynamicRows(m.specs||[], m.extras||[]);
|
||
materialModal ? materialModal.show() : document.getElementById('materialModal').classList.add('show');
|
||
}
|
||
if (e.target.classList.contains('btnDel')){
|
||
const id = parseInt(e.target.closest('tr').dataset.id,10);
|
||
if (!confirm('삭제하시겠습니까?')) return;
|
||
MATERIALS = MATERIALS.filter(m=>m.id!==id);
|
||
if (snackToast){ $('#snackMsg').textContent='삭제되었습니다.'; snackToast.show(); }
|
||
renderList();
|
||
}
|
||
if (e.target.classList.contains('thumb')){
|
||
ensureBootstrapInstances();
|
||
const src = e.target.dataset.src || e.target.getAttribute('src');
|
||
$('#imgModalImg').setAttribute('src', src);
|
||
imgModal ? imgModal.show() : document.getElementById('imgModal').classList.add('show');
|
||
}
|
||
});
|
||
|
||
// 검색/필터/페이지크기
|
||
document.getElementById('btnSearch').addEventListener('click', ()=>{ PAGE=1; renderList(); });
|
||
document.getElementById('keyword').addEventListener('keydown', e=>{ if(e.key==='Enter'){ PAGE=1; renderList(); }});
|
||
document.getElementById('filterCategory').addEventListener('change', ()=>{ PAGE=1; renderList(); });
|
||
document.getElementById('pageSize').addEventListener('change', function(){ SIZE=parseInt(this.value,10)||20; PAGE=1; renderList(); });
|
||
|
||
// 페이징 클릭
|
||
document.addEventListener('click', (e)=>{
|
||
const a = e.target.closest('#pagerNav a'); if (!a) return;
|
||
e.preventDefault();
|
||
const pages = Math.max(1, Math.ceil(getFiltered().length / SIZE));
|
||
const go = a.dataset.go;
|
||
if (go==='first') PAGE=1;
|
||
else if (go==='last') PAGE=pages;
|
||
else PAGE = Math.min(Math.max(parseInt(go,10)||1,1), pages);
|
||
renderList();
|
||
});
|
||
|
||
// 최초 렌더
|
||
document.addEventListener('DOMContentLoaded', ()=>{
|
||
try{ ensureBootstrapInstances(); }catch(_){}
|
||
const ps=document.getElementById('pageSize'); if(ps) SIZE=parseInt(ps.value,10)||20;
|
||
renderList();
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
<!-- Bootstrap JS (모달 동작 보장용; 이미 로드되어 있어도 괜찮습니다) -->
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||
|
||
<?php include '../inc/footer.php'; ?>
|