fix : 모델, BOM 구성 수정
- 설계용 모델, BOM 기능 추가
This commit is contained in:
224
public/tenant/product/bom.php
Normal file
224
public/tenant/product/bom.php
Normal file
@@ -0,0 +1,224 @@
|
||||
<!-- SAM RULES: include=../inc/header.php; base=/tenant; width=1280; js=jQuery+BS5 -->
|
||||
<?php
|
||||
// BOM 템플릿 편집기
|
||||
$CURRENT_SECTION='item';
|
||||
include '../inc/header.php';
|
||||
$modelId = isset($_GET['model_id']) ? (int)$_GET['model_id'] : 0;
|
||||
?>
|
||||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h1 class="h5 m-0">BOM 템플릿 편집기</h1>
|
||||
<div class="text-muted small">모델 ID: <code id="modelIdView"><?php echo $modelId; ?></code> — 모델 버전 선택 후 구성 편집</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="/tenant/item/model_list.php" class="btn btn-outline-secondary"><i class="bi bi-list-ul"></i> 목록</a>
|
||||
<a href="/tenant/item/model_create.php?id=<?php echo $modelId; ?>" class="btn btn-outline-primary"><i class="bi bi-pencil"></i> 모델 수정</a>
|
||||
<button class="btn btn-success" id="btnSave"><i class="bi bi-save"></i> 저장</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-body row g-3 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">모델 버전</label>
|
||||
<select id="modelVersion" class="form-select">
|
||||
<!-- API 연동 전 샘플 -->
|
||||
<option value="v1">v1.0 (DRAFT)</option>
|
||||
<option value="v1r">v1.0 (RELEASED)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">그룹 추가</label>
|
||||
<div class="input-group">
|
||||
<input id="groupName" class="form-control" placeholder="예: 본체, 절곡물" />
|
||||
<button id="btnAddGroup" class="btn btn-outline-primary"><i class="bi bi-plus-lg"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<div class="form-text">※ 그룹 = BOM 템플릿 내 상위 분류 (예: 본체/절곡물/모터/부자재)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h2 class="h6 m-0">BOM 그룹 (템플릿)</h2>
|
||||
<button id="btnExpand" class="btn btn-sm btn-outline-secondary">모두 펼치기</button>
|
||||
</div>
|
||||
<div class="card-body" style="max-height:520px; overflow:auto;">
|
||||
<ul class="list-group" id="groupList">
|
||||
<!-- JS 렌더링: 그룹/하위아이템 -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-8">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h2 class="h6 m-0">아이템 상세</h2>
|
||||
<div class="d-flex gap-2">
|
||||
<button id="btnAddItem" class="btn btn-sm btn-outline-primary"><i class="bi bi-plus-lg"></i> 아이템 추가</button>
|
||||
<button id="btnDelNode" class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i> 선택 삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle" id="itemTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:160px;">구분</th>
|
||||
<th style="width:160px;">참조코드</th>
|
||||
<th>명칭</th>
|
||||
<th style="width:120px;">수량</th>
|
||||
<th style="width:140px;">옵션/조건</th>
|
||||
<th style="width:80px;" class="text-center">삭제</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- JS 렌더링 -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
※ 구분: PRODUCT(서브모델) / MATERIAL(자재) / PART(부품). 옵션/조건은 견적-선택값과 매핑(예: 가이드레일=벽면형).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
(function(){
|
||||
const modelId = Number($('#modelIdView').text()) || 0;
|
||||
|
||||
// 샘플 데이터: 그룹과 아이템
|
||||
let GROUPS = [
|
||||
{id:'g1', name:'본체', items:[{id:'i1', type:'MATERIAL', ref:'SILICA-SET', name:'실리카원단+내화실 세트', qty:1, cond:'-'}, {id:'i2', type:'PRODUCT', ref:'SLAT-JOINT-SET', name:'슬랫+조인트바 세트', qty:1, cond:'-'}]},
|
||||
{id:'g2', name:'절곡물', items:[{id:'i3', type:'PRODUCT', ref:'GUIDE-WALL-C', name:'가이드레일(벽면형/C형)', qty:1, cond:'옵션: 벽면형'}, {id:'i4', type:'PRODUCT', ref:'GUIDE-SIDE-D', name:'가이드레일(측면형/D형)', qty:1, cond:'옵션: 측면형'}]},
|
||||
{id:'g3', name:'모터', items:[{id:'i5', type:'PART', ref:'MOTOR-SET', name:'모터+브라켓', qty:1, cond:'-'}]},
|
||||
{id:'g4', name:'부자재', items:[{id:'i6', type:'PART', ref:'SHAFT-2IN', name:'감기샤프트 2인치', qty:1, cond:'-'}, {id:'i7', type:'PART', ref:'BOX-PIPE', name:'각파이프', qty:2, cond:'-'}]},
|
||||
];
|
||||
|
||||
// 그룹 렌더링
|
||||
function renderGroups(){
|
||||
const $ul = $('#groupList').empty();
|
||||
GROUPS.forEach(g=>{
|
||||
const $li = $(`<li class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="fw-semibold">${g.name}</div>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-secondary btn-rename" data-id="${g.id}"><i class="bi bi-pencil"></i></button>
|
||||
<button class="btn btn-outline-danger btn-del-group" data-id="${g.id}"><i class="bi bi-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</li>`);
|
||||
$li.on('click', function(){ selectGroup(g.id); });
|
||||
$ul.append($li);
|
||||
});
|
||||
}
|
||||
|
||||
let currentGroupId = null;
|
||||
function selectGroup(id){
|
||||
currentGroupId = id;
|
||||
const g = GROUPS.find(x=>x.id===id);
|
||||
renderItems(g ? g.items : []);
|
||||
$('#groupList .list-group-item').removeClass('active');
|
||||
$('#groupList .list-group-item').filter(function(){ return $(this).text().trim().startsWith(g.name); }).addClass('active');
|
||||
}
|
||||
|
||||
function renderItems(items){
|
||||
const $tb = $('#itemTable tbody').empty();
|
||||
items.forEach(it=>{
|
||||
const $tr = $(`<tr data-id="${it.id}">
|
||||
<td>
|
||||
<select class="form-select form-select-sm type">
|
||||
<option ${it.type==='PRODUCT'?'selected':''}>PRODUCT</option>
|
||||
<option ${it.type==='PART'?'selected':''}>PART</option>
|
||||
<option ${it.type==='MATERIAL'?'selected':''}>MATERIAL</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input class="form-control form-control-sm ref" value="${it.ref}"></td>
|
||||
<td><input class="form-control form-control-sm name" value="${it.name}"></td>
|
||||
<td style="width:120px;"><input type="number" step="0.001" class="form-control form-control-sm qty" value="${it.qty}"></td>
|
||||
<td><input class="form-control form-control-sm cond" placeholder="예: 옵션=벽면형" value="${it.cond||''}"></td>
|
||||
<td class="text-center"><button class="btn btn-sm btn-outline-danger btn-del-item"><i class="bi bi-x-lg"></i></button></td>
|
||||
</tr>`);
|
||||
$tb.append($tr);
|
||||
});
|
||||
}
|
||||
|
||||
// 이벤트 바인딩
|
||||
$('#btnAddGroup').on('click', function(e){
|
||||
e.preventDefault();
|
||||
const name = ($('#groupName').val()||'').trim();
|
||||
if(!name) return alert('그룹명을 입력하세요.');
|
||||
const id = 'g'+(Date.now());
|
||||
GROUPS.push({id, name, items:[]});
|
||||
$('#groupName').val('');
|
||||
renderGroups();
|
||||
});
|
||||
|
||||
$('#groupList').on('click', '.btn-del-group', function(e){
|
||||
e.stopPropagation();
|
||||
const id = $(this).data('id');
|
||||
if(!confirm('그룹을 삭제할까요? 하위 아이템도 함께 삭제됩니다.')) return;
|
||||
GROUPS = GROUPS.filter(g=>g.id!==id);
|
||||
currentGroupId = null;
|
||||
renderGroups();
|
||||
renderItems([]);
|
||||
});
|
||||
|
||||
$('#groupList').on('click', '.btn-rename', function(e){
|
||||
e.stopPropagation();
|
||||
const id = $(this).data('id');
|
||||
const g = GROUPS.find(x=>x.id===id);
|
||||
const name = prompt('새 그룹명', g?.name||'');
|
||||
if(name){ g.name = name; renderGroups(); }
|
||||
});
|
||||
|
||||
$('#btnAddItem').on('click', function(){
|
||||
if(!currentGroupId) return alert('먼저 그룹을 선택하세요.');
|
||||
const g = GROUPS.find(x=>x.id===currentGroupId);
|
||||
g.items.push({id:'i'+Date.now(), type:'PART', ref:'', name:'', qty:1, cond:''});
|
||||
renderItems(g.items);
|
||||
});
|
||||
|
||||
$('#itemTable').on('click', '.btn-del-item', function(){
|
||||
const $tr = $(this).closest('tr');
|
||||
const id = $tr.data('id');
|
||||
const g = GROUPS.find(x=>x.id===currentGroupId);
|
||||
g.items = g.items.filter(x=>x.id!==id);
|
||||
renderItems(g.items);
|
||||
});
|
||||
|
||||
// 저장 (템플릿 -> items payload)
|
||||
$('#btnSave').on('click', function(){
|
||||
// collect
|
||||
const payload = {
|
||||
model_id: modelId,
|
||||
version: $('#modelVersion').val(),
|
||||
groups: GROUPS.map(g=>({
|
||||
name: g.name,
|
||||
items: g.items.map(it=>({ type: $('.type', `tr[data-id="${it.id}"]`).val() || it.type,
|
||||
ref: $('.ref', `tr[data-id="${it.id}"]`).val() || it.ref,
|
||||
name: $('.name',`tr[data-id="${it.id}"]`).val() || it.name,
|
||||
qty: Number($('.qty', `tr[data-id="${it.id}"]`).val() || it.qty),
|
||||
cond: $('.cond',`tr[data-id="${it.id}"]`).val() || it.cond }))
|
||||
}))
|
||||
};
|
||||
console.log('BOM TEMPLATE SAVE', payload);
|
||||
alert('샘플: 콘솔에 저장 payload 출력. API 연동 필요');
|
||||
});
|
||||
|
||||
// 초기 렌더
|
||||
renderGroups();
|
||||
if(GROUPS.length) selectGroup(GROUPS[0].id);
|
||||
})();
|
||||
</script>
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
321
public/tenant/product/model.php
Normal file
321
public/tenant/product/model.php
Normal file
@@ -0,0 +1,321 @@
|
||||
<?php
|
||||
// 모델관리
|
||||
$CURRENT_SECTION='item';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
<!--
|
||||
SAM RULES (summary)
|
||||
1) Prototype uses Bootstrap 5 + jQuery; no server changes.
|
||||
2) Layout max-width 1280px; brand blue #2c4a85.
|
||||
3) Keep scope to *Model metadata only* (NO BOM, NO attributes).
|
||||
4) All paths placeholder; wire Ajax later.
|
||||
5) Comment banner for consistency across files.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>모델 등록 | SAM Prototype (Model-only)</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet" />
|
||||
<style>
|
||||
:root{ --brand:#2c4a85; }
|
||||
body{ background:#f7f9fc; }
|
||||
.container{ max-width:1280px; }
|
||||
.card{ border-radius:1rem; box-shadow:0 6px 20px rgba(0,0,0,.05); }
|
||||
.page-title{ display:flex; gap:.75rem; align-items:center; }
|
||||
.page-title .badge{ background:var(--brand); }
|
||||
.required::after{ content:"*"; color:#dc3545; margin-left:4px; }
|
||||
.tag-input{ min-height:38px; border:1px solid #ced4da; border-radius:.5rem; padding:.25rem .5rem; background:#fff; display:flex; gap:.5rem; align-items:center; flex-wrap:wrap; }
|
||||
.tag-chip{ display:inline-flex; align-items:center; gap:.25rem; padding:.25rem .5rem; border-radius:20px; background:#edf2ff; }
|
||||
.tag-chip .remove{ cursor:pointer; }
|
||||
.dropzone{ border:2px dashed #cdd7e1; border-radius:.75rem; padding:16px; text-align:center; background:#fff; cursor:pointer; }
|
||||
.sticky-actions{ position:sticky; bottom:0; background:rgba(255,255,255,.95); backdrop-filter:saturate(120%) blur(4px); border-top:1px solid #e5e7eb; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="py-3 mb-3 border-bottom bg-white">
|
||||
<div class="container d-flex justify-content-between align-items-center">
|
||||
<div class="page-title">
|
||||
<a class="btn btn-outline-secondary" href="#" onclick="history.back();return false;"><i class="bi bi-arrow-left"></i></a>
|
||||
<div>
|
||||
<h1 class="h4 m-0">모델 등록</h1>
|
||||
<div class="text-muted">모델(설계 뼈대) 메타데이터만 입력합니다. ※ BOM/속성은 별도 화면</div>
|
||||
</div>
|
||||
<span class="badge text-bg-primary ms-2">MODEL-ONLY</span>
|
||||
</div>
|
||||
<div>
|
||||
<a class="btn btn-outline-primary" href="#"><i class="bi bi-list-ul me-1"></i>목록</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container pb-5">
|
||||
<form id="modelForm" class="needs-validation" novalidate>
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header bg-white">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<h2 class="h6 m-0">기본정보</h2>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="isActive" checked>
|
||||
<label class="form-check-label" for="isActive">활성화</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label required" for="code">모델 코드</label>
|
||||
<input type="text" class="form-control" id="code" placeholder="예: KSS01" required>
|
||||
<div class="invalid-feedback">모델 코드를 입력하세요.</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label required" for="name">모델명</label>
|
||||
<input type="text" class="form-control" id="name" placeholder="예: 방화셔터 KSS01" required>
|
||||
<div class="invalid-feedback">모델명을 입력하세요.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label required" for="unit">기본 단위</label>
|
||||
<select id="unit" class="form-select" required>
|
||||
<option value="">선택...</option>
|
||||
<option>EA</option>
|
||||
<option>SET</option>
|
||||
<option>M</option>
|
||||
<option>KG</option>
|
||||
</select>
|
||||
<div class="invalid-feedback">기본 단위를 선택하세요.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="tagsInput">태그</label>
|
||||
<div id="tagsInput" class="tag-input" role="group" aria-label="tags">
|
||||
<input id="tagText" type="text" class="form-control border-0 p-0" placeholder="Enter로 태그 추가 (예: 본체, 절곡물, BLDC)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label required" for="categoryName">카테고리</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="categoryName" class="form-control" placeholder="카테고리를 선택하세요" readonly required>
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#categoryModal"><i class="bi bi-diagram-3"></i> 선택</button>
|
||||
</div>
|
||||
<input type="hidden" id="categoryId" />
|
||||
<div class="form-text">※ 모델이 속하는 분류(예: 제품>방화셔터>KSS) — 구성(BOM)과는 별개</div>
|
||||
<div class="invalid-feedback">카테고리를 선택하세요.</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="description">설명</label>
|
||||
<textarea id="description" rows="3" class="form-control" placeholder="모델 개요/용도 등을 입력"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-4">
|
||||
<div class="card-header bg-white d-flex align-items-center justify-content-between">
|
||||
<h2 class="h6 m-0">이미지</h2>
|
||||
<div class="text-muted small">대표 이미지 1장</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col-md-6">
|
||||
<div id="imageDrop" class="dropzone">여기로 드래그하거나 클릭하여 선택</div>
|
||||
<input type="file" id="imageFile" accept="image/*" class="form-control mt-2" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div id="previewImg" class="bg-light border rounded d-flex align-items-center justify-content-center" style="width:120px;height:120px;">
|
||||
<i class="bi bi-image fs-3 text-muted"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sticky-actions mt-4 p-3 d-flex gap-2 justify-content-end bg-white">
|
||||
<button type="button" class="btn btn-outline-secondary" id="btnReset"><i class="bi bi-arrow-counterclockwise"></i> 초기화</button>
|
||||
<button type="button" class="btn btn-outline-primary" id="btnDraft"><i class="bi bi-cloud-arrow-up"></i> 임시저장</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check2-circle"></i> 등록</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header bg-white">
|
||||
<h2 class="h6 m-0">미리보기</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<div id="pvImg" class="bg-light border rounded" style="width:72px;height:72px; display:flex; align-items:center; justify-content:center;">
|
||||
<i class="bi bi-image fs-3 text-muted"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small" id="pvCode">CODE</div>
|
||||
<div class="fw-bold" id="pvName">모델명 미입력</div>
|
||||
<div class="small text-muted" id="pvCategory">카테고리 미선택</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pvTags" class="d-flex flex-wrap gap-1"></div>
|
||||
<hr>
|
||||
<div class="small text-muted">※ 이 화면은 모델 메타데이터만 다룹니다. BOM 구성은 별도의 “BOM 템플릿 편집기”에서 작성하세요.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<!-- Category Modal (sample tree) -->
|
||||
<div class="modal fade" id="categoryModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-diagram-3 me-2"></i>카테고리 선택</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-5">
|
||||
<input id="catSearch" type="search" class="form-control" placeholder="카테고리 검색" />
|
||||
<div class="border rounded p-2 mt-2" style="max-height:320px; overflow:auto;">
|
||||
<ul class="list-unstyled" id="catTree">
|
||||
<li>
|
||||
<label class="d-block"><input type="radio" name="cat" value="100" data-name="제품/방화셔터/KSS" class="form-check-input me-2"> 제품</label>
|
||||
<ul class="ms-4 mt-1">
|
||||
<li>
|
||||
<label class="d-block"><input type="radio" name="cat" value="110" data-name="제품/방화셔터" class="form-check-input me-2"> 방화셔터</label>
|
||||
<ul class="ms-4 mt-1">
|
||||
<li><label class="d-block"><input type="radio" name="cat" value="111" data-name="제품/방화셔터/KSS" class="form-check-input me-2"> KSS</label></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle me-1"></i> 최종(리프) 카테고리를 선택하세요. 이 선택은 모델의 *분류*만 의미합니다.
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small">선택된 카테고리</div>
|
||||
<div id="catPicked" class="fw-semibold">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">취소</button>
|
||||
<button type="button" class="btn btn-primary" id="btnPickCategory"><i class="bi bi-check2"></i> 선택</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div class="position-fixed bottom-0 end-0 p-3" style="z-index:1080;">
|
||||
<div id="saveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<i class="bi bi-check2-circle me-2 text-success"></i>
|
||||
<strong class="me-auto">저장 완료</strong>
|
||||
<small>지금</small>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body">모델 메타데이터가 임시로 저장되었습니다. (콘솔 로그 확인)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
const toast = new bootstrap.Toast(document.getElementById('saveToast'));
|
||||
|
||||
function collectFormData(){
|
||||
const tags = [];
|
||||
$('#pvTags .tag-chip').each(function(){ tags.push($(this).data('tag')); });
|
||||
return {
|
||||
code: $('#code').val().trim(),
|
||||
name: $('#name').val().trim(),
|
||||
unit: $('#unit').val(),
|
||||
is_active: $('#isActive').is(':checked') ? 1 : 0,
|
||||
category_id: $('#categoryId').val() || null,
|
||||
category_path: $('#categoryName').val() || null,
|
||||
description: $('#description').val().trim(),
|
||||
tags
|
||||
};
|
||||
}
|
||||
|
||||
function updatePreview(){
|
||||
$('#pvCode').text($('#code').val() || 'CODE');
|
||||
$('#pvName').text($('#name').val() || '모델명 미입력');
|
||||
$('#pvCategory').text($('#categoryName').val() || '카테고리 미선택');
|
||||
}
|
||||
|
||||
function addTag(text){
|
||||
const tag = text.trim();
|
||||
if(!tag) return;
|
||||
const dup = $('#pvTags .tag-chip').filter(function(){return $(this).data('tag')===tag;}).length>0;
|
||||
if(dup) return;
|
||||
const chip = $(`<span class="tag-chip" data-tag="${tag}"><i class="bi bi-hash"></i>${tag}<i class="bi bi-x-lg remove"></i></span>`);
|
||||
$('#pvTags').append(chip);
|
||||
$('#tagText').val('');
|
||||
}
|
||||
|
||||
$(function(){
|
||||
// live preview
|
||||
$('#code, #name, #categoryName').on('input change', updatePreview);
|
||||
$('#code').on('input', function(){ this.value = this.value.toUpperCase(); });
|
||||
|
||||
// tags
|
||||
$('#tagText').on('keydown', function(e){ if(e.key==='Enter'){ e.preventDefault(); addTag(this.value); } });
|
||||
$('#pvTags').on('click', '.remove', function(){ $(this).closest('.tag-chip').remove(); });
|
||||
|
||||
// category search
|
||||
$('#catSearch').on('input', function(){
|
||||
const q = this.value.trim().toLowerCase();
|
||||
$('#catTree li').each(function(){ $(this).toggle($(this).text().toLowerCase().includes(q)); });
|
||||
});
|
||||
|
||||
// category pick
|
||||
$('input[name="cat"]').on('change', function(){ $('#catPicked').text($(this).data('name')); });
|
||||
$('#btnPickCategory').on('click', function(){
|
||||
const $sel = $('input[name="cat"]:checked');
|
||||
if($sel.length===0){ alert('카테고리를 선택하세요.'); return; }
|
||||
$('#categoryId').val($sel.val());
|
||||
$('#categoryName').val($sel.data('name'));
|
||||
updatePreview();
|
||||
bootstrap.Modal.getInstance(document.getElementById('categoryModal')).hide();
|
||||
});
|
||||
|
||||
// image preview + drop
|
||||
$('#imageDrop').on('click', ()=> $('#imageFile').trigger('click'));
|
||||
$('#imageDrop').on('dragover', e=>{ e.preventDefault(); $(e.currentTarget).addClass('border-primary'); });
|
||||
$('#imageDrop').on('dragleave drop', e=>{ e.preventDefault(); $(e.currentTarget).removeClass('border-primary'); if(e.type==='drop'){ const f=e.originalEvent.dataTransfer.files; if(f && f[0]){ $('#imageFile')[0].files=f; $('#imageFile').trigger('change'); } } });
|
||||
$('#imageFile').on('change', function(){
|
||||
const file = this.files && this.files[0]; if(!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => { $('#previewImg, #pvImg').html(`<img src="${e.target.result}" alt="img" class="img-fluid rounded" style="max-width:100%; max-height:100%;">`); };
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
// submit
|
||||
const form = document.getElementById('modelForm');
|
||||
form.addEventListener('submit', function(ev){
|
||||
ev.preventDefault(); ev.stopPropagation();
|
||||
if(!form.checkValidity()) { form.classList.add('was-validated'); return; }
|
||||
const payload = collectFormData();
|
||||
console.log('MODEL:CREATE payload', payload);
|
||||
toast.show();
|
||||
});
|
||||
|
||||
// draft + reset
|
||||
$('#btnDraft').on('click', function(){ console.log('MODEL:DRAFT payload', collectFormData()); toast.show(); });
|
||||
$('#btnReset').on('click', function(){ form.reset(); $('#pvTags').empty(); updatePreview(); $('#previewImg, #pvImg').html('<i class="bi bi-image fs-3 text-muted"></i>'); });
|
||||
|
||||
updatePreview();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
@@ -4,307 +4,179 @@
|
||||
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>
|
||||
<form id="modelForm" class="needs-validation" novalidate>
|
||||
<div class="row g-4">
|
||||
<!-- LEFT: form fields -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h2 class="h6 m-0">모델 기본정보</h2>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="isActive" checked>
|
||||
<label class="form-check-label" for="isActive">활성화</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label required" for="code">모델 코드</label>
|
||||
<input type="text" class="form-control" id="code" placeholder="예: MDL-2025-001" required>
|
||||
<div class="invalid-feedback">모델 코드를 입력하세요.</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label required" for="name">모델명</label>
|
||||
<input type="text" class="form-control" id="name" placeholder="예: 컨베이어 모듈 A" required>
|
||||
<div class="invalid-feedback">모델명을 입력하세요.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label required" for="unit">기본 단위</label>
|
||||
<select id="unit" class="form-select" required>
|
||||
<option value="">선택...</option>
|
||||
<option>EA</option>
|
||||
<option>SET</option>
|
||||
<option>M</option>
|
||||
<option>KG</option>
|
||||
<option>BOX</option>
|
||||
</select>
|
||||
<div class="invalid-feedback">기본 단위를 선택하세요.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label required" for="categoryName">카테고리</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="categoryName" class="form-control" placeholder="카테고리를 선택하세요" readonly required>
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#categoryModal"><i class="bi bi-diagram-3"></i> 선택</button>
|
||||
</div>
|
||||
<input type="hidden" id="categoryId" />
|
||||
<div class="invalid-feedback">카테고리를 선택하세요.</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="description">설명</label>
|
||||
<textarea id="description" rows="3" class="form-control" placeholder="모델 설명을 입력"></textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="tagsInput">태그</label>
|
||||
<div id="tagsInput" class="tag-input">
|
||||
<input id="tagText" type="text" class="form-control border-0 p-0" placeholder="Enter로 태그 추가" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">대표 이미지</label>
|
||||
<div id="imageDrop" class="dropzone">클릭 또는 드래그하여 선택</div>
|
||||
<input type="file" id="imageFile" accept="image/*" class="form-control mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 d-flex justify-content-end gap-2">
|
||||
<button type="reset" class="btn btn-outline-secondary"><i class="bi bi-arrow-counterclockwise"></i> 초기화</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check2-circle"></i> 등록</button>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- RIGHT: preview -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card preview-card">
|
||||
<div class="card-header bg-white">
|
||||
<h2 class="h6 m-0">미리보기</h2>
|
||||
</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 class="card-body">
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<div id="previewImg" class="bg-light border rounded" style="width:72px;height:72px; display:flex; align-items:center; justify-content:center;">
|
||||
<i class="bi bi-image fs-3 text-muted"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small" id="pvCode">CODE</div>
|
||||
<div class="fw-bold" id="pvName">모델명 미입력</div>
|
||||
<div class="small text-muted" id="pvCategory">카테고리 미선택</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pvTags" class="d-flex flex-wrap gap-1 mb-3"></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" id="btnClose">닫기</button>
|
||||
<button type="submit" class="btn btn-primary" id="btnSubmit">등록</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
<!-- Category Modal -->
|
||||
<div class="modal fade" id="categoryModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-diagram-3 me-2"></i>카테고리 선택</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul class="list-unstyled" id="catTree">
|
||||
<li><label><input type="radio" name="cat" value="101" data-name="제품/모터" class="form-check-input me-2"> 제품 > 모터</label></li>
|
||||
<li><label><input type="radio" name="cat" value="201" data-name="제품/컨베이어" class="form-check-input me-2"> 제품 > 컨베이어</label></li>
|
||||
<li><label><input type="radio" name="cat" value="301" data-name="반제품/프레임" class="form-check-input me-2"> 반제품 > 프레임</label></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">취소</button>
|
||||
<button type="button" class="btn btn-primary" id="btnPickCategory">선택</button>
|
||||
</div>
|
||||
</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 src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<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;
|
||||
$(function(){
|
||||
function updatePreview(){
|
||||
$('#pvCode').text($('#code').val() || 'CODE');
|
||||
$('#pvName').text($('#name').val() || '모델명 미입력');
|
||||
$('#pvCategory').text($('#categoryName').val() || '카테고리 미선택');
|
||||
}
|
||||
let MODELS = buildSample(97);
|
||||
$('#code,#name,#categoryName').on('input change', updatePreview);
|
||||
|
||||
// ===== 상태/유틸 =====
|
||||
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;
|
||||
$('#tagText').on('keydown', function(e){
|
||||
if(e.key==='Enter'){
|
||||
e.preventDefault();
|
||||
const tag=this.value.trim();
|
||||
if(tag){
|
||||
const chip=$(`<span class="tag-chip" data-tag="${tag}"><i class="bi bi-hash"></i>${tag}<i class="bi bi-x-lg remove"></i></span>`);
|
||||
$('#pvTags').append(chip);
|
||||
this.value='';
|
||||
}
|
||||
}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);
|
||||
}
|
||||
});
|
||||
$('#pvTags').on('click','.remove',function(){ $(this).parent().remove(); });
|
||||
|
||||
// ===== 이벤트 =====
|
||||
// 신규등록
|
||||
$('#btnOpenCreate').addEventListener('click', ()=>{
|
||||
$('#modelModalTitle').textContent='모델 등록';
|
||||
$('#btnSubmit').textContent='등록';
|
||||
$('#modelForm').reset();
|
||||
$('#mdlId').value='';
|
||||
$('#modalHint').textContent='※ 필수 입력: 분류, 모델명';
|
||||
openModal();
|
||||
$('input[name="cat"]').on('change', function(){
|
||||
$('#categoryId').val(this.value);
|
||||
$('#categoryName').val($(this).data('name')).trigger('change');
|
||||
});
|
||||
|
||||
// 저장(등록/수정)
|
||||
$('#modelForm').addEventListener('submit', e=>{
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
e.currentTarget.classList.add('was-validated');
|
||||
if(!e.currentTarget.checkValidity()) return;
|
||||
$('#imageFile').on('change', function(){
|
||||
const file=this.files[0];
|
||||
if(file){
|
||||
const reader=new FileReader();
|
||||
reader.onload=e=>$('#previewImg').html(`<img src="${e.target.result}" class="img-fluid rounded" style="max-width:72px; max-height:72px;">`);
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
|
||||
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)
|
||||
$('#imageDrop').on('click', ()=> $('#imageFile').trigger('click'));
|
||||
|
||||
$('#modelForm').on('submit', function(e){
|
||||
e.preventDefault();
|
||||
if(!this.checkValidity()){ this.classList.add('was-validated'); return; }
|
||||
const payload={
|
||||
code:$('#code').val(),
|
||||
name:$('#name').val(),
|
||||
unit:$('#unit').val(),
|
||||
is_active:$('#isActive').is(':checked')?1:0,
|
||||
category_id:$('#categoryId').val(),
|
||||
description:$('#description').val(),
|
||||
tags:$('#pvTags .tag-chip').map((i,e)=>$(e).data('tag')).get()
|
||||
};
|
||||
|
||||
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();
|
||||
console.log('SAVE',payload);
|
||||
alert('모델 등록 데이터 콘솔 확인');
|
||||
});
|
||||
|
||||
// 수정/삭제/페이징
|
||||
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'; ?>
|
||||
|
||||
Reference in New Issue
Block a user