322 lines
18 KiB
PHP
322 lines
18 KiB
PHP
<?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'; ?>
|