311 lines
15 KiB
PHP
311 lines
15 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" href="/tenant/material/list.php">자재 관리</a></li>
|
||
<li class="nav-item"><a class="nav-link active" 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:140px;">분류</th>
|
||
<th>모델명</th>
|
||
<th style="width:140px;">로트 코드</th>
|
||
<th style="width:140px;">등록일</th>
|
||
<th style="width:160px;" 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="modelModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||
<form class="modal-content needs-validation" id="modelForm" novalidate>
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="modelModalTitle">모델 등록</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="닫기" id="btnCloseX"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<input type="hidden" id="mdlId">
|
||
<div class="row g-3">
|
||
<div class="col-md-3">
|
||
<label class="form-label">분류 <span class="text-danger">*</span></label>
|
||
<select class="form-select" id="mdlCategory" required>
|
||
<option value="">선택</option>
|
||
<option>스크린</option>
|
||
<option>철재</option>
|
||
<option>유리</option>
|
||
</select>
|
||
<div class="invalid-feedback">분류를 선택하세요.</div>
|
||
</div>
|
||
<div class="col-md-5">
|
||
<label class="form-label">모델명 <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="mdlName" required>
|
||
<div class="invalid-feedback">모델명을 입력하세요.</div>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label">로트 코드</label>
|
||
<input type="text" class="form-control" id="mdlLot" placeholder="예: SA, WE">
|
||
</div>
|
||
<div class="col-12">
|
||
<label class="form-label">내용</label>
|
||
<input type="text" class="form-control" id="mdlDesc" placeholder="간단 설명">
|
||
</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" id="btnClose">닫기</button>
|
||
<button type="submit" class="btn btn-primary" id="btnSubmit">등록</button>
|
||
</div>
|
||
</form>
|
||
</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>
|
||
.table > :not(caption) > * > * { vertical-align: middle; }
|
||
/* 부트스트랩 JS 없는 경우의 간단 폴백 */
|
||
.modal.fallback-show { display:block; background:rgba(0,0,0,.45); }
|
||
.modal.fallback-show .modal-dialog {
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
margin: 0 auto;
|
||
position: relative;
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
(function(){
|
||
// ===== 샘플 데이터 =====
|
||
function buildSample(count=64){
|
||
const cats=['스크린','철재','유리'], lots=['SS','SA','SE','WE','TS','TE','DS'];
|
||
const arr=[]; for(let i=1;i<=count;i++){
|
||
arr.push({
|
||
id:i,
|
||
category: cats[i%cats.length],
|
||
name: (i%7===0)? `${cats[i%cats.length]} 비인정` : `${cats[i%cats.length]} K${String(i).padStart(2,'0')}`,
|
||
lot: lots[i%lots.length],
|
||
created_at: new Date(Date.now()-86400000*i).toISOString().slice(0,10),
|
||
desc:''
|
||
});
|
||
} return arr;
|
||
}
|
||
let MODELS = buildSample(97);
|
||
|
||
// ===== 상태/유틸 =====
|
||
let PAGE=1, SIZE=20;
|
||
const $=s=>document.querySelector(s), $$=s=>Array.from(document.querySelectorAll(s));
|
||
const hasBS = ()=> !!window.bootstrap;
|
||
const snack = ()=> hasBS() ? new bootstrap.Toast('#snack') : { show(){ /* no-op */ } };
|
||
|
||
function getFiltered(){
|
||
const cat=$('#filterCategory').value.trim();
|
||
const kw=$('#keyword').value.trim().toLowerCase();
|
||
return MODELS.filter(m=>{
|
||
const okCat=!cat || m.category===cat;
|
||
const hay=(m.name+' '+(m.lot||'')).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;
|
||
$('#listBody').innerHTML = data.slice(start,end).map((m,i)=>`
|
||
<tr data-id="${m.id}">
|
||
<td>${start+i+1}</td>
|
||
<td>${m.category}</td>
|
||
<td>${m.name}</td>
|
||
<td>${m.lot||''}</td>
|
||
<td>${m.created_at}</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="6" class="text-center text-muted py-4">데이터가 없습니다.</td></tr>`;
|
||
|
||
$('#totalInfo').textContent=`총 ${total.toLocaleString()}건 · ${total?(start+1):0}-${Math.min(end,total)} 표시`;
|
||
|
||
const win=7; let sp=Math.max(1,PAGE-Math.floor(win/2)), 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">«</a></li>`;
|
||
html+=`<li class="page-item ${PAGE===1?'disabled':''}"><a class="page-link" href="#" data-go="${PAGE-1}">‹</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}">›</a></li>`;
|
||
html+=`<li class="page-item ${PAGE===pages?'disabled':''}"><a class="page-link" href="#" data-go="last">»</a></li>`;
|
||
html+=`</ul>`; $('#pagerNav').innerHTML=html;
|
||
}
|
||
|
||
// ===== 모달(부트스트랩/폴백) =====
|
||
let mdlModal = null;
|
||
function openModal(){
|
||
try{
|
||
if (hasBS()){
|
||
mdlModal = mdlModal || new bootstrap.Modal('#modelModal');
|
||
mdlModal.show(); return;
|
||
}
|
||
}catch(_){}
|
||
// 폴백
|
||
const m = $('#modelModal');
|
||
m.classList.add('fallback-show','show');
|
||
}
|
||
function closeModal(){
|
||
if (hasBS() && mdlModal){ mdlModal.hide(); return; }
|
||
const m = $('#modelModal');
|
||
m.classList.remove('fallback-show','show');
|
||
}
|
||
$('#btnClose').addEventListener('click', closeModal);
|
||
$('#btnCloseX').addEventListener('click', closeModal);
|
||
|
||
// ===== 이벤트 =====
|
||
// 신규등록
|
||
$('#btnOpenCreate').addEventListener('click', ()=>{
|
||
$('#modelModalTitle').textContent='모델 등록';
|
||
$('#btnSubmit').textContent='등록';
|
||
$('#modelForm').reset();
|
||
$('#mdlId').value='';
|
||
$('#modalHint').textContent='※ 필수 입력: 분류, 모델명';
|
||
openModal();
|
||
});
|
||
|
||
// 저장(등록/수정)
|
||
$('#modelForm').addEventListener('submit', e=>{
|
||
e.preventDefault(); e.stopPropagation();
|
||
e.currentTarget.classList.add('was-validated');
|
||
if(!e.currentTarget.checkValidity()) return;
|
||
|
||
const dto={
|
||
id: $('#mdlId').value ? parseInt($('#mdlId').value,10) : null,
|
||
category: $('#mdlCategory').value,
|
||
name: $('#mdlName').value.trim(),
|
||
lot: $('#mdlLot').value.trim(),
|
||
desc: $('#mdlDesc').value.trim(),
|
||
created_at: new Date().toISOString().slice(0,10)
|
||
};
|
||
|
||
if(dto.id){
|
||
const idx=MODELS.findIndex(x=>x.id===dto.id);
|
||
if(idx>=0) MODELS[idx]={...MODELS[idx], ...dto};
|
||
$('#snackMsg').textContent='수정되었습니다.'; snack().show();
|
||
}else{
|
||
dto.id=(MODELS.reduce((mx,m)=>Math.max(mx,m.id),0)||0)+1;
|
||
MODELS.unshift(dto);
|
||
$('#snackMsg').textContent='등록되었습니다.'; snack().show();
|
||
}
|
||
closeModal();
|
||
PAGE=1; renderList();
|
||
});
|
||
|
||
// 수정/삭제/페이징
|
||
document.addEventListener('click', e=>{
|
||
const a=e.target.closest('#pagerNav a');
|
||
if (a){ 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();
|
||
return;
|
||
}
|
||
if (e.target.classList.contains('btnEdit')){
|
||
const id=parseInt(e.target.closest('tr').dataset.id,10);
|
||
const m=MODELS.find(x=>x.id===id); if(!m) return;
|
||
$('#modelModalTitle').textContent='모델 수정';
|
||
$('#btnSubmit').textContent='수정';
|
||
$('#mdlId').value=m.id; $('#mdlCategory').value=m.category; $('#mdlName').value=m.name;
|
||
$('#mdlLot').value=m.lot||''; $('#mdlDesc').value=m.desc||'';
|
||
$('#modalHint').textContent='필드 수정 후 [수정] 클릭';
|
||
openModal();
|
||
}
|
||
if (e.target.classList.contains('btnDel')){
|
||
const id=parseInt(e.target.closest('tr').dataset.id,10);
|
||
if(!confirm('삭제하시겠습니까?')) return;
|
||
MODELS = MODELS.filter(x=>x.id!==id);
|
||
$('#snackMsg').textContent='삭제되었습니다.'; snack().show();
|
||
renderList();
|
||
}
|
||
});
|
||
|
||
// 검색/필터/표시개수
|
||
$('#btnSearch').addEventListener('click', ()=>{ PAGE=1; renderList(); });
|
||
$('#keyword').addEventListener('keydown', e=>{ if(e.key==='Enter'){ PAGE=1; renderList(); }});
|
||
$('#filterCategory').addEventListener('change', ()=>{ PAGE=1; renderList(); });
|
||
$('#pageSize').addEventListener('change', function(){ SIZE=parseInt(this.value,10)||20; PAGE=1; renderList(); });
|
||
|
||
// 최초
|
||
document.addEventListener('DOMContentLoaded', ()=>{
|
||
const ps=$('#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'; ?>
|