- URL 하드코딩 → .env APP_URL 기반 동적 URL로 변경 - DB 연결 하드코딩 → .env 기반으로 변경 - MySQL strict mode DATE 오류 수정
449 lines
16 KiB
PHP
449 lines
16 KiB
PHP
<?php
|
|
require_once($_SERVER['DOCUMENT_ROOT'].'/session.php');
|
|
require_once($_SERVER['DOCUMENT_ROOT'].'/lib/mydb.php');
|
|
$pdo = db_connect();
|
|
|
|
$title_message = '경동기업 4단계 BOM 구성';
|
|
|
|
// 1) DB에서 1~4레벨 카테고리 전부 SELECT
|
|
$l1_data = $pdo->query("SELECT * FROM {$DB}.category_l1 ORDER BY sortOrder, id")->fetchAll(PDO::FETCH_ASSOC);
|
|
$l2_data = $pdo->query("SELECT * FROM {$DB}.category_l2 ORDER BY sortOrder, id")->fetchAll(PDO::FETCH_ASSOC);
|
|
$l3_data = $pdo->query("SELECT * FROM {$DB}.category_l3 ORDER BY sortOrder, id")->fetchAll(PDO::FETCH_ASSOC);
|
|
$l4_data = $pdo->query("SELECT * FROM {$DB}.category_l4 ORDER BY sortOrder, id")->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// 2) PHP에서 parent_id별로 배열화
|
|
$l2_map = [];
|
|
foreach($l2_data as $row2){
|
|
$l2_map[$row2['parent_id']][] = $row2;
|
|
}
|
|
$l3_map = [];
|
|
foreach($l3_data as $row3){
|
|
$l3_map[$row3['parent_id']][] = $row3;
|
|
}
|
|
$l4_map = [];
|
|
foreach($l4_data as $row4){
|
|
$l4_map[$row4['parent_id']][] = $row4;
|
|
}
|
|
|
|
include $_SERVER['DOCUMENT_ROOT'] . '/load_header.php'; // common.php 포함되어 있음
|
|
|
|
?>
|
|
|
|
<title><?=$title_message?></title>
|
|
<style>
|
|
.tree-node {
|
|
margin-left: 1.25rem; /* Bootstrap의 1rem = 16px -> 약 20px */
|
|
}
|
|
.toggle-btn {
|
|
cursor: pointer;
|
|
color: #0d6efd; /* 부트스트랩 primary 색상 */
|
|
margin-right: 0.25rem;
|
|
}
|
|
.d-none {
|
|
display: none !important;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="container mt-4">
|
|
<div class="card justify-content-center text-center mt-3 mb-2">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-center align-items-center mb-2">
|
|
<h4 class="mx-1"><?=$title_message?></h4>
|
|
<button type="button" class="btn btn-dark btn-sm mx-3" onclick='location.reload();' > <i class="bi bi-arrow-clockwise"></i> </button>
|
|
<button type="button" class="btn btn-success btn-sm mx-1" onclick="addCategory(1,0)"> <i class="bi bi-plus-square"></i> 최상위 추가 </button>
|
|
<button type="button" class="btn btn-primary btn-sm mx-1" onclick="expandAllTree();"> <i class="bi bi-plus-square"></i> 전체 펼치기 </button>
|
|
<button type="button" class="btn btn-secondary btn-sm mx-1" onclick="foldAllTree();"> <i class="bi bi-plus-square"></i> 전체 접기 </button>
|
|
<button type="button" class="btn btn-dark btn-sm mx-5" onclick="self.close();"> × 닫기 </button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 레벨1 목록 -->
|
|
<?php foreach($l1_data as $l1): ?>
|
|
<div class="tree-node mb-2">
|
|
<!-- 펼치기/접기 아이콘 -->
|
|
<span class="toggle-btn" data-target="l1-<?= $l1['id'] ?>" >
|
|
<i class="bi bi-caret-right-fill"></i>
|
|
</span>
|
|
<strong><?= htmlspecialchars($l1['name']) ?></strong>
|
|
|
|
<!-- 수정/삭제/하위추가 버튼들 -->
|
|
<!-- 1단계 항목의 복사 버튼 (아이콘: bi-files) -->
|
|
<button class="btn btn-sm btn-outline-secondary ms-1 mb-1" style="padding:2"
|
|
onclick="copyCategory(1, <?= $l1['id'] ?>)">
|
|
<i class="bi bi-files"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-info ms-2 mb-1" style="padding : 2"
|
|
onclick="editCategory(1, <?= $l1['id'] ?> , '<?= $l1['name'] ?>')">
|
|
<i class="bi bi-pencil-square"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-danger ms-1 mb-1" style="padding : 2"
|
|
onclick="deleteCategory(1, <?= $l1['id'] ?>)">
|
|
<i class="bi bi-trash3"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-primary ms-1 mb-1" style="padding : 2"
|
|
onclick="addCategory(2, <?= $l1['id'] ?>)">
|
|
<i class="bi bi-plus-square"></i>
|
|
</button>
|
|
|
|
<!-- L2 목록 -->
|
|
<div id="l1-<?= $l1['id'] ?>" class="d-none">
|
|
<?php if(isset($l2_map[$l1['id']])):
|
|
foreach($l2_map[$l1['id']] as $l2):
|
|
$l2_id = $l2['id'];
|
|
?>
|
|
<div class="tree-node mb-2">
|
|
|
|
<span class="toggle-btn" data-target="l2-<?= $l2_id ?>">
|
|
<i class="bi bi-caret-right-fill"></i>
|
|
</span>
|
|
<?= htmlspecialchars($l2['name']) ?>
|
|
|
|
<button class="btn btn-sm btn-outline-info ms-2 mb-1" style="padding : 2"
|
|
onclick="editCategory(2, <?= $l2_id ?>, '<?= $l2['name'] ?>')">
|
|
<i class="bi bi-pencil-square"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-danger ms-1 mb-1" style="padding : 2"
|
|
onclick="deleteCategory(2, <?= $l2_id ?>)">
|
|
<i class="bi bi-trash3"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-primary ms-1 mb-1" style="padding : 2"
|
|
onclick="addCategory(3, <?= $l2_id ?>)">
|
|
<i class="bi bi-plus-square"></i>
|
|
</button>
|
|
|
|
<!-- L3 목록 -->
|
|
<div id="l2-<?= $l2_id ?>" class="d-none">
|
|
<?php if(isset($l3_map[$l2_id])):
|
|
foreach($l3_map[$l2_id] as $l3):
|
|
$l3_id = $l3['id'];
|
|
?>
|
|
<div class="tree-node mb-2">
|
|
|
|
<span class="toggle-btn" data-target="l3-<?= $l3_id ?>">
|
|
<i class="bi bi-caret-right-fill"></i>
|
|
</span>
|
|
<?= htmlspecialchars($l3['name']) ?>
|
|
|
|
<button class="btn btn-sm btn-outline-info ms-2 mb-1" style="padding : 2"
|
|
onclick="editCategory(3, <?= $l3_id ?>, '<?= $l3['name'] ?>')">
|
|
<i class="bi bi-pencil-square"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-danger ms-1 mb-1" style="padding : 2"
|
|
onclick="deleteCategory(3, <?= $l3_id ?>)">
|
|
<i class="bi bi-trash3"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-primary ms-1 mb-1" style="padding : 2"
|
|
onclick="addCategory(4, <?= $l3_id ?>)">
|
|
<i class="bi bi-plus-square"></i>
|
|
</button>
|
|
|
|
<!-- L4 목록 -->
|
|
<div id="l3-<?= $l3_id ?>" class="d-none">
|
|
<?php if(isset($l4_map[$l3_id])):
|
|
foreach($l4_map[$l3_id] as $l4):
|
|
$l4_id = $l4['id'];
|
|
?>
|
|
<div class="tree-node mb-2">
|
|
|
|
<?= htmlspecialchars($l4['name']) ?>
|
|
|
|
<button class="btn btn-sm btn-outline-info ms-2 mb-1" style="padding : 2"
|
|
onclick="editCategory(4, <?= $l4_id ?>, '<?= $l4['name'] ?>')">
|
|
<i class="bi bi-pencil-square"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-danger ms-1 mb-1" style="padding : 2"
|
|
onclick="deleteCategory(4, <?= $l4_id ?>)">
|
|
<i class="bi bi-trash3"></i>
|
|
</button>
|
|
</div>
|
|
<?php endforeach; endif; ?>
|
|
</div>
|
|
<!-- end L4 -->
|
|
|
|
</div>
|
|
<?php endforeach; endif; ?>
|
|
</div>
|
|
<!-- end L3 -->
|
|
</div>
|
|
<?php endforeach; endif; ?>
|
|
</div>
|
|
<!-- end L2 -->
|
|
</div>
|
|
<?php endforeach; ?>
|
|
|
|
</div>
|
|
|
|
<!-- Bootstrap Modal -->
|
|
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="editModalLabel">카테고리 수정/추가</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="닫기"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" id="catLevel" value="">
|
|
<input type="hidden" id="catId" value="">
|
|
<input type="hidden" id="catParent" name="catParent" value="">
|
|
<div class="mb-3">
|
|
<label for="catName" class="form-label">이름</label>
|
|
<input type="text" class="form-control" id="catName" name="catName" placeholder="카테고리명" autocomplete="off">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-primary mx-1" onclick="saveCategory()">저장</button>
|
|
<button type="button" class="btn btn-secondary mx-1" data-bs-dismiss="modal">취소</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 로딩 오버레이 숨김 + 트리 토글 + SweetAlert2 알림 -->
|
|
<script>
|
|
$(document).ready(function(){
|
|
// 로딩 hide
|
|
var loader = document.getElementById('loadingOverlay');
|
|
if(loader) loader.style.display = 'none';
|
|
|
|
// // 트리 토글 (부트스트랩 아이콘 caret 교체)
|
|
// document.querySelectorAll('.toggle-btn').forEach(btn => {
|
|
// btn.addEventListener('click', () => {
|
|
// const targetId = btn.getAttribute('data-target');
|
|
// const sub = document.getElementById(targetId);
|
|
// if(sub) {
|
|
// if(sub.classList.contains('d-none')){
|
|
// sub.classList.remove('d-none');
|
|
// btn.innerHTML = '<i class="bi bi-caret-down-fill"></i>';
|
|
// } else {
|
|
// sub.classList.add('d-none');
|
|
// btn.innerHTML = '<i class="bi bi-caret-right-fill"></i>';
|
|
// }
|
|
// }
|
|
// });
|
|
// });
|
|
|
|
// // [추가 요구사항] 초기에 전체 트리를 펼치기
|
|
// expandAllTree();
|
|
});
|
|
|
|
// 트리를 전부 접는 함수
|
|
function foldAllTree(){
|
|
// 모든 .d-none 제거하고, 아이콘을 caret-down-fill 로 변경
|
|
document.querySelectorAll('[id^="l1-"], [id^="l2-"], [id^="l3-"]').forEach(subDiv => {
|
|
subDiv.classList.add('d-none');
|
|
});
|
|
// toggle-btn 아이콘도 다 down으로
|
|
document.querySelectorAll('.toggle-btn').forEach(btn => {
|
|
btn.innerHTML = '<i class="bi bi-caret-right-fill"></i>';
|
|
});
|
|
}
|
|
// 트리를 전부 펼치는 함수
|
|
function expandAllTree(){
|
|
// 모든 .d-none 제거하고, 아이콘을 caret-down-fill 로 변경
|
|
document.querySelectorAll('[id^="l1-"], [id^="l2-"], [id^="l3-"]').forEach(subDiv => {
|
|
subDiv.classList.remove('d-none');
|
|
});
|
|
// toggle-btn 아이콘도 다 down으로
|
|
document.querySelectorAll('.toggle-btn').forEach(btn => {
|
|
btn.innerHTML = '<i class="bi bi-caret-down-fill"></i>';
|
|
});
|
|
}
|
|
|
|
// 추가
|
|
function addCategory(level, parentId){
|
|
document.getElementById('catLevel').value = level;
|
|
document.getElementById('catId').value = 0; // insert
|
|
document.getElementById('catParent').value = parentId;
|
|
document.getElementById('catName').value = '';
|
|
const myModal = new bootstrap.Modal(document.getElementById('editModal'));
|
|
myModal.show();
|
|
}
|
|
|
|
// 수정
|
|
function editCategory(level, id, name){
|
|
document.getElementById('catLevel').value = level;
|
|
document.getElementById('catId').value = id;
|
|
document.getElementById('catName').value = name ;
|
|
const myModal = new bootstrap.Modal(document.getElementById('editModal'));
|
|
myModal.show();
|
|
}
|
|
|
|
// 삭제
|
|
function deleteCategory(level, id){
|
|
Swal.fire({
|
|
title: '삭제 확인',
|
|
text: '정말 삭제하시겠습니까?',
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonColor: '#3085d6',
|
|
cancelButtonColor: '#d33',
|
|
confirmButtonText: '삭제',
|
|
cancelButtonText: '취소'
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
fetch(`deleteCategory.php?level=${level}&id=${id}`)
|
|
.then(res=>res.json())
|
|
.then(data=>{
|
|
if(data.result==='ok'){
|
|
Swal.fire({
|
|
icon: 'success',
|
|
title: '삭제완료',
|
|
text: '카테고리가 삭제되었습니다.'
|
|
}).then(()=> location.reload());
|
|
} else {
|
|
Swal.fire({
|
|
icon: 'error',
|
|
title: '삭제실패',
|
|
text: data.msg
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// 저장
|
|
function saveCategory(){
|
|
const level = document.getElementById('catLevel').value;
|
|
const id = document.getElementById('catId').value;
|
|
const parentId = document.getElementById('catParent').value;
|
|
const name = document.getElementById('catName').value.trim();
|
|
|
|
if(name === ''){
|
|
Swal.fire({
|
|
icon: 'warning',
|
|
title: '입력확인',
|
|
text: '이름을 입력하세요.'
|
|
});
|
|
return;
|
|
}
|
|
|
|
fetch('saveCategory.php',{
|
|
method:'POST',
|
|
headers:{ 'Content-Type':'application/json' },
|
|
body: JSON.stringify({ level, id, parentId, name })
|
|
})
|
|
.then(r=>r.json())
|
|
.then(d=>{
|
|
if(d.result==='ok'){
|
|
Swal.fire({
|
|
icon: 'success',
|
|
title: '저장완료',
|
|
text: '카테고리 등록/수정되었습니다.'
|
|
}).then(()=> location.reload());
|
|
} else {
|
|
Swal.fire({
|
|
icon: 'error',
|
|
title: '오류',
|
|
text: d.msg
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// 복사 카테고리
|
|
function copyCategory(level, id) {
|
|
Swal.fire({
|
|
title: '복사 확인',
|
|
text: '해당 1단계와 그 하위 항목 전체를 복사하시겠습니까?',
|
|
icon: 'question',
|
|
showCancelButton: true,
|
|
confirmButtonColor: '#3085d6',
|
|
cancelButtonColor: '#d33',
|
|
confirmButtonText: '복사',
|
|
cancelButtonText: '취소'
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
fetch(`copyCategory.php?level=${level}&id=${id}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if(data.result==='ok'){
|
|
Swal.fire({
|
|
icon: 'success',
|
|
title: '복사완료',
|
|
text: '하위 단계까지 복사되었습니다.'
|
|
}).then(()=> location.reload());
|
|
} else {
|
|
Swal.fire({
|
|
icon: 'error',
|
|
title: '복사실패',
|
|
text: data.msg
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
</script>
|
|
|
|
|
|
<script>
|
|
// [기존] 트리 토글 (부트스트랩 아이콘 caret 교체) + 쿠키 업데이트 기능 추가
|
|
function updateTreeStateCookie(){
|
|
let expandedNodes = [];
|
|
// 모든 토글 버튼을 순회하면서 대상 요소가 보이면 배열에 추가
|
|
document.querySelectorAll('.toggle-btn').forEach(btn => {
|
|
const targetId = btn.getAttribute('data-target');
|
|
const sub = document.getElementById(targetId);
|
|
if(sub && !sub.classList.contains('d-none')){
|
|
expandedNodes.push(targetId);
|
|
}
|
|
});
|
|
// 쿠키에 저장 (유효시간 10분)
|
|
setCookie("treeState", JSON.stringify(expandedNodes), 10);
|
|
}
|
|
|
|
document.querySelectorAll('.toggle-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const targetId = btn.getAttribute('data-target');
|
|
const sub = document.getElementById(targetId);
|
|
if(sub) {
|
|
if(sub.classList.contains('d-none')){
|
|
sub.classList.remove('d-none');
|
|
btn.innerHTML = '<i class="bi bi-caret-down-fill"></i>';
|
|
} else {
|
|
sub.classList.add('d-none');
|
|
btn.innerHTML = '<i class="bi bi-caret-right-fill"></i>';
|
|
}
|
|
// 토글 동작 후 현재 트리 상태를 쿠키에 저장
|
|
updateTreeStateCookie();
|
|
}
|
|
});
|
|
});
|
|
|
|
// 쿠키에서 트리 상태 복원 함수
|
|
function restoreTreeStateFromCookie(){
|
|
let cookieVal = getCookie("treeState");
|
|
if(cookieVal){
|
|
try{
|
|
let expandedNodes = JSON.parse(cookieVal);
|
|
expandedNodes.forEach(targetId => {
|
|
let sub = document.getElementById(targetId);
|
|
if(sub && sub.classList.contains('d-none')){
|
|
sub.classList.remove('d-none');
|
|
// 해당 토글 버튼의 아이콘도 down으로 변경
|
|
document.querySelectorAll(`.toggle-btn[data-target="${targetId}"]`).forEach(btn => {
|
|
btn.innerHTML = '<i class="bi bi-caret-down-fill"></i>';
|
|
});
|
|
}
|
|
});
|
|
} catch(e){
|
|
console.error("Error parsing treeState cookie", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 페이지 로드시 트리 상태 복원
|
|
document.addEventListener("DOMContentLoaded", function(){
|
|
restoreTreeStateFromCookie();
|
|
});
|
|
</script>
|
|
|
|
|
|
</body>
|
|
</html>
|