Files
sam-kd/modelsTree/modelsTree.php
hskwon aca1767eb9 초기 커밋: 5130 레거시 시스템
- URL 하드코딩 → .env APP_URL 기반 동적 URL로 변경
- DB 연결 하드코딩 → .env 기반으로 변경
- MySQL strict mode DATE 오류 수정
2025-12-10 20:14:31 +09:00

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();"> &times; 닫기 </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">
&nbsp;&nbsp;&nbsp;
<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">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<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">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<?= 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>