Files
sam-api/public/tenant/product/model_list.php
2025-08-10 02:36:50 +09:00

311 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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'; ?>