Files
dnssash/adm/menu_list.php
2026-06-11 18:47:38 +09:00

487 lines
25 KiB
PHP

<?php
$sub_menu = "100290";
require_once './_common.php';
// 💡 [수정] auth_check() -> auth_check_menu() 로 함수명 수정
auth_check_menu($auth, $sub_menu, 'r');
if ($is_admin != 'super') {
alert('최고관리자만 접근 가능합니다.');
}
$g5['title'] = '메뉴 설정';
include_once('./admin.head.php');
// 테이블 자동 생성 로직
if (!isset($g5['menu_table'])) {
die('<meta charset="utf-8">dbconfig.php 파일에 <strong>$g5[\'menu_table\'] = G5_TABLE_PREFIX.\'menu\';</strong> 를 추가해 주세요.');
}
if (!sql_query(" DESCRIBE {$g5['menu_table']} ", false)) {
sql_query(
" CREATE TABLE `{$g5['menu_table']}` (
`me_id` int NOT NULL AUTO_INCREMENT,
`me_parent_id` int NOT NULL DEFAULT '0' COMMENT '부모 메뉴 ID',
`me_depth` tinyint NOT NULL DEFAULT '1' COMMENT '메뉴 깊이 (1뎁스=1)',
`me_code` varchar(255) NOT NULL DEFAULT '',
`me_name` varchar(255) NOT NULL DEFAULT '',
`me_link` varchar(255) NOT NULL DEFAULT '',
`me_target` varchar(255) NOT NULL DEFAULT '',
`me_order` int NOT NULL DEFAULT '0',
`me_use` tinyint NOT NULL DEFAULT '0',
`me_mobile_use` tinyint NOT NULL DEFAULT '0',
`me_level` tinyint NOT NULL DEFAULT '1' COMMENT '접근권한',
`me_level_opt` tinyint NOT NULL DEFAULT '1' COMMENT '권한옵션',
`me_created_by` varchar(20) NOT NULL DEFAULT '' COMMENT '생성자',
`me_updated_by` varchar(20) NOT NULL DEFAULT '' COMMENT '수정자',
`me_created_at` datetime DEFAULT NULL COMMENT '생성일',
`me_updated_at` datetime DEFAULT NULL COMMENT '수정일',
`me_deleted_at` datetime DEFAULT NULL COMMENT '삭제일',
PRIMARY KEY (`me_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ",
true
);
}
if (!sql_query(" DESCRIBE g5_menu_history ", false)) {
sql_query(
" CREATE TABLE `g5_menu_history` (
`mh_id` int NOT NULL AUTO_INCREMENT COMMENT '이력 ID',
`me_id` int NOT NULL COMMENT '메뉴 ID',
`mh_action` enum('생성','수정','삭제') NOT NULL COMMENT '작업 종류',
`mh_data_before` json DEFAULT NULL COMMENT '변경 전 데이터',
`mh_data_after` json DEFAULT NULL COMMENT '변경 후 데이터',
`mh_editor` varchar(20) NOT NULL COMMENT '작업자',
`mh_ip` varchar(100) NOT NULL COMMENT '작업자 IP',
`mh_datetime` datetime NOT NULL COMMENT '작업일시',
PRIMARY KEY (`mh_id`),
KEY `me_id` (`me_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='메뉴 변경 이력' ",
true
);
}
// jsTree 라이브러리를 이 페이지에서만 불러옵니다.
add_stylesheet('<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.2.1/themes/default/style.min.css">', 0);
add_javascript('<script src="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.2.1/jstree.min.js"></script>', 0);
// 1. 모든 메뉴 데이터를 가져옵니다.
$sql = "SELECT * FROM {$g5['menu_table']} WHERE me_deleted_at IS NULL ORDER BY me_parent_id, me_order, me_id";
$result = sql_query($sql);
// 2. jsTree에서 사용할 JSON 데이터 형식으로 가공합니다.
$menu_data_for_tree = [];
while ($row = sql_fetch_array($result)) {
$prefix = ($row['me_depth'] > 1) ? '└ ' : '';
$menu_data_for_tree[] = [
'id' => $row['me_id'],
'parent' => $row['me_parent_id'] > 0 ? $row['me_parent_id'] : '#',
'text' => $prefix . get_text($row['me_name']),
'data' => $row
];
}
// 💡 [핵심] 테마의 page 스킨 폴더 목록을 가져옵니다.
$page_path = G5_THEME_PATH . '/skin/page';
$page_list = [];
if (is_dir($page_path)) {
$handle = opendir($page_path);
while ($file = readdir($handle)) {
if ($file == '.' || $file == '..') {
continue;
}
if (is_dir($page_path . '/' . $file)) {
$page_list[] = $file;
}
}
closedir($handle);
sort($page_list);
}
?>
<!-- 좌/우 분할형 UI를 위한 스타일 -->
<style>
#menu_manager_wrapper { display: flex; gap: 20px; }
#menu_tree_panel { width: 350px; border: 1px solid #ddd; padding: 10px; background: #fff; min-height: 500px; }
#menu_detail_panel { flex-grow: 1; }
#menu_detail_form { border: 1px solid #ddd; padding: 20px; background: #f9f9f9; display: none; /* 초기에는 숨김 */ }
#menu_detail_form .frm_heading { font-size: 1.2em; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 10px; }
#menu_detail_form .frm_line { margin-bottom: 15px; }
#menu_detail_form .frm_line > label { display: inline-block; width: 120px; font-weight: bold; }
#menu_detail_form .frm_input { width: calc(100% - 130px); height: 35px; padding: 0 10px; }
#menu_detail_form select.frm_input { width: auto; min-width: 150px; }
#menu_detail_form .frm_info { font-size: 0.9em; color: #777; margin-top: 5px; }
#welcome_panel { text-align: center; padding-top: 150px; color: #888; }
.jstree-default .jstree-node { font-size: 1.1em; }
.tree_control_box { margin-bottom: 10px; display: flex; gap: 5px; }
</style>
<form name="fmenulist" id="fmenulist" method="post" action="./menu_list_update.php">
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
<div class="local_desc01 local_desc">
<p>
왼쪽 트리에서 메뉴를 드래그하여 순서와 계층을 변경할 수 있습니다. (최대 2뎁스)<br>
메뉴를 클릭하면 오른쪽에서 상세 정보를 수정할 수 있습니다. 모든 변경사항은 '전체 메뉴 저장' 버튼을 눌러야 최종 반영됩니다.
</p>
</div>
<div class="btn_fixed_top">
<input type="submit" value="전체 메뉴 저장" class="btn_submit btn" accesskey="s">
</div>
<div id="menu_manager_wrapper">
<!-- 왼쪽: 메뉴 트리 패널 -->
<div id="menu_tree_panel">
<div class="tree_control_box">
<button type="button" id="add_root_menu" class="btn btn_03">1차 메뉴 추가</button>
<button type="button" id="open_all_nodes" class="btn btn_03">전체열기</button>
<button type="button" id="close_all_nodes" class="btn btn_03">전체닫기</button>
</div>
<div id="menu_tree_container"></div>
</div>
<!-- 오른쪽: 메뉴 상세 정보 패널 -->
<div id="menu_detail_panel">
<!-- 초기 안내 메시지 -->
<div id="welcome_panel">
<h2>메뉴 관리</h2>
<p>왼쪽 트리에서 메뉴를 선택하거나<br>'1차 메뉴 추가' 버튼으로 새 메뉴를 생성하세요.</p>
</div>
<!-- 메뉴 상세 정보 폼 -->
<div id="menu_detail_form">
<input type="hidden" id="me_id" value="">
<h3 class="frm_heading">메뉴 상세 정보</h3>
<div class="frm_line">
<label for="me_name">메뉴명</label>
<input type="text" id="me_name" class="frm_input required" required>
</div>
<div class="frm_line">
<label for="me_link_type">연결 방식</label>
<select id="me_link_type" class="frm_input">
<option value="direct">직접입력</option>
<option value="board">게시판</option>
<option value="content">내용관리</option>
<option value="page">페이지</option> <!-- 💡 [수정] 페이지 옵션 추가 -->
</select>
</div>
<div class="frm_line" id="board_select_wrap" style="display:none;">
<label for="board_shortcut">게시판 선택</label>
<?php
$sql_board = " SELECT bo_table, bo_subject FROM {$g5['board_table']} ORDER BY bo_order, bo_subject ASC ";
$result_board = sql_query($sql_board);
?>
<select id="board_shortcut" class="frm_input">
<option value="">게시판을 선택하세요</option>
<?php
for ($i=0; $row=sql_fetch_array($result_board); $i++) {
echo '<option value="'.$row['bo_table'].'">'.get_text($row['bo_subject']).' ('.$row['bo_table'].')</option>'.PHP_EOL;
}
?>
</select>
</div>
<div class="frm_line" id="content_select_wrap" style="display:none;">
<label for="content_shortcut">내용관리 선택</label>
<?php
$sql_content = " SELECT co_id, co_subject FROM {$g5['content_table']} ORDER BY co_id ASC ";
$result_content = sql_query($sql_content);
?>
<select id="content_shortcut" class="frm_input">
<option value="">내용을 선택하세요</option>
<?php
for ($i=0; $row=sql_fetch_array($result_content); $i++) {
echo '<option value="'.$row['co_id'].'">'.get_text($row['co_subject']).' ('.$row['co_id'].')</option>'.PHP_EOL;
}
?>
</select>
</div>
<!-- 💡 [핵심] 페이지 선택 드롭다운 추가 -->
<div class="frm_line" id="page_select_wrap" style="display:none;">
<label for="page_shortcut">페이지 선택</label>
<select id="page_shortcut" class="frm_input">
<?php if (count($page_list)) { ?>
<option value="">페이지를 선택하세요</option>
<?php foreach ($page_list as $page) { ?>
<option value="<?php echo $page; ?>"><?php echo $page; ?></option>
<?php } ?>
<?php } else { ?>
<option value="">연결할 페이지가 없습니다.</option>
<?php } ?>
</select>
</div>
<div class="frm_line" id="link_input_wrap">
<label for="me_link">링크</label>
<input type="text" id="me_link" class="frm_input required" required>
</div>
<div class="frm_line">
<label for="me_target">새창</label>
<select id="me_target" class="frm_input">
<option value="self">사용안함</option>
<option value="blank">새창</option>
</select>
</div>
<div class="frm_line">
<label>사용여부</label>
<input type="checkbox" id="me_use" value="1"> PC
<input type="checkbox" id="me_mobile_use" value="1" style="margin-left:10px;"> 모바일
</div>
<div class="frm_line">
<label for="me_level">접근권한</label>
<?php echo get_member_level_select('me_level', 1, 10, 1, 'id="me_level" class="frm_input"'); ?>
</div>
<div class="frm_line">
<label for="me_level_opt">접근권한옵션</label>
<?php echo get_member_level_select('me_level_opt', 1, 10, 1, 'id="me_level_opt" class="frm_input"'); ?>
</div>
</div>
</div>
</div>
<div id="form_data_container" style="display:none;"></div>
</form>
<script>
$(function () {
var menu_data = <?php echo json_encode($menu_data_for_tree, JSON_UNESCAPED_UNICODE); ?>;
var page_list = <?php echo json_encode($page_list); ?>; // 💡 페이지 목록을 JS 배열로
var max_depth = 2;
var tree = $('#menu_tree_container');
function toggleLinkFields(type, linkValue) {
$('#link_input_wrap').hide();
$('#board_select_wrap').hide();
$('#content_select_wrap').hide();
$('#page_select_wrap').hide(); // 💡 페이지 선택 숨김
$('#me_link').prop('readonly', false);
if (type === 'board') {
$('#board_select_wrap').show();
$('#me_link').prop('readonly', true);
if(linkValue) {
var boardId = linkValue.split('bo_table=')[1];
$('#board_shortcut').val(boardId || '');
}
} else if (type === 'content') {
$('#content_select_wrap').show();
$('#me_link').prop('readonly', true);
if(linkValue) {
var contentId = linkValue.split('co_id=')[1];
$('#content_shortcut').val(contentId || '');
}
} else if (type === 'page') { // 💡 페이지 타입 추가
$('#page_select_wrap').show();
$('#me_link').prop('readonly', true);
if(linkValue) {
var pageId = linkValue.split('co_id=')[1];
$('#page_shortcut').val(pageId || '');
}
} else { // 'direct'
$('#link_input_wrap').show();
}
}
tree.on('ready.jstree', function(e, data) {
data.instance.open_all();
}).jstree({
'core' : {
'data' : menu_data,
'check_callback' : function (operation, node, node_parent, node_position, more) {
if (operation === "move_node") {
if(node_parent.id === '#') return true;
if(node_parent.parents.length + 1 > max_depth) {
alert(max_depth + '뎁스까지만 메뉴를 구성할 수 있습니다.');
return false;
}
}
return true;
}
},
'plugins' : [ "dnd", "contextmenu" ],
'contextmenu': {
'items': function(node) {
var items = {
'create': {
'label': '서브메뉴 추가',
'action': function (data) {
var inst = $.jstree.reference(data.reference);
var obj = inst.get_node(data.reference);
inst.create_node(obj, { text: '└ 새 메뉴', data: { me_id: 0, me_name: '새 메뉴', me_link: '#', me_target: 'self', me_use: '1', me_mobile_use: '1', me_level: '1', me_level_opt: '1' } }, "last", function (new_node) {
try { inst.edit(new_node); } catch (ex) { setTimeout(function () { inst.edit(new_node); }, 0); }
});
},
'_disabled': (node.parents.length >= max_depth)
},
'rename': { 'label': '이름 바꾸기', 'action': function (data) { $.jstree.reference(data.reference).edit(data.reference); } },
'delete': { 'label': '삭제', 'action': function (data) { if(confirm("이 메뉴와 모든 하위 메뉴가 삭제됩니다.\n정말 삭제하시겠습니까?")) { $.jstree.reference(data.reference).delete_node(data.reference); } } }
};
return items;
}
}
});
var jstree_instance = tree.jstree(true);
$('#open_all_nodes').on('click', function() { jstree_instance.open_all(); });
$('#close_all_nodes').on('click', function() { jstree_instance.close_all(); });
tree.on('select_node.jstree', function (e, data) {
var node_data = data.node.data;
if(node_data) {
$('#welcome_panel').hide();
$('#menu_detail_form').show();
$('#me_id').val(node_data.me_id);
$('#me_name').val(node_data.me_name);
$('#me_link').val(node_data.me_link);
$('#me_target').val(node_data.me_target);
$('#me_use').prop('checked', node_data.me_use == "1");
$('#me_mobile_use').prop('checked', node_data.me_mobile_use == "1");
$('#me_level').val(node_data.me_level || 1);
$('#me_level_opt').val(node_data.me_level_opt || 1);
var link = node_data.me_link || '';
var contentId = link.split('co_id=')[1] || '';
// 💡 [수정] 링크 종류 분석 로직 개선
if (link.includes('/board.php?bo_table=')) {
$('#me_link_type').val('board');
toggleLinkFields('board', link);
} else if (link.includes('/content.php?co_id=') && page_list.includes(contentId)) {
// co_id가 페이지 목록에 있으면 '페이지'로 간주
$('#me_link_type').val('page');
toggleLinkFields('page', link);
} else if (link.includes('/content.php?co_id=')) {
// co_id가 페이지 목록에 없으면 '내용관리'로 간주
$('#me_link_type').val('content');
toggleLinkFields('content', link);
} else {
$('#me_link_type').val('direct');
toggleLinkFields('direct', link);
}
}
});
$('#me_link_type').on('change', function() {
var type = $(this).val();
toggleLinkFields(type, $('#me_link').val());
if(type !== 'direct') {
$('#me_link').val('#').trigger('change');
}
});
$('#board_shortcut').on('change', function() {
var board_table = $(this).val();
var board_url = board_table ? "<?php echo G5_BBS_URL; ?>/board.php?bo_table=" + board_table : '#';
$('#me_link').val(board_url).trigger('change');
});
$('#content_shortcut').on('change', function() {
var co_id = $(this).val();
var content_url = co_id ? "<?php echo G5_BBS_URL; ?>/content.php?co_id=" + co_id : '#';
$('#me_link').val(content_url).trigger('change');
});
// 💡 [핵심] 페이지 선택 시 링크 자동 입력
$('#page_shortcut').on('change', function() {
var page_id = $(this).val();
var page_url = page_id ? "<?php echo G5_BBS_URL; ?>/content.php?co_id=" + page_id : '#';
$('#me_link').val(page_url).trigger('change');
});
$('#menu_detail_form input, #menu_detail_form select').on('change', function() {
var selected_node_id = jstree_instance.get_selected()[0];
if(!selected_node_id) return;
var node = jstree_instance.get_node(selected_node_id);
if (!node.data) node.data = {};
node.data.me_name = $('#me_name').val();
node.data.me_link = $('#me_link').val();
node.data.me_target = $('#me_target').val();
node.data.me_use = $('#me_use').is(':checked') ? "1" : "0";
node.data.me_mobile_use = $('#me_mobile_use').is(':checked') ? "1" : "0";
node.data.me_level = $('#me_level').val();
node.data.me_level_opt = $('#me_level_opt').val();
var is_submenu = node.parents.length > 1;
var display_text = (is_submenu ? '└ ' : '') + node.data.me_name;
jstree_instance.set_text(selected_node_id, display_text);
});
tree.on('rename_node.jstree', function (e, data) {
var new_name = data.text.replace(/^└\s*/, '');
data.node.data.me_name = new_name;
if (data.node.parents.length > 1) {
jstree_instance.set_text(data.node, '└ ' + new_name);
}
});
$('#add_root_menu').on('click', function() {
jstree_instance.create_node('#', { text: '새 메뉴', data: {
me_id: 0, me_name: '새 메뉴', me_link: '#', me_target: 'self', me_use: '1', me_mobile_use: '1', me_level: '1', me_level_opt: '1'
}}, 'last', function(new_node) {
jstree_instance.edit(new_node);
});
});
$('#fmenulist').on('submit', function(e) {
var form_data_div = $('#form_data_container');
form_data_div.empty();
var all_nodes = jstree_instance.get_json('#', { 'flat': true });
var jstree_id_to_temp_id_map = {};
var new_node_counter = 0;
var nodes_with_data = [];
$.each(all_nodes, function(index, simple_node) {
var full_node = jstree_instance.get_node(simple_node.id);
var node_data = full_node.data;
if (!node_data) {
node_data = { me_id: 0, me_name: full_node.text.replace(/^└\s*/, ''), me_link: '#', me_target: 'self', me_use: '1', me_mobile_use: '1', me_level: '1', me_level_opt: '1' };
full_node.data = node_data;
}
var temp_id = node_data.me_id || 0;
if (temp_id == 0 || String(temp_id).indexOf('j') === 0) {
new_node_counter++;
temp_id = 'new_' + new_node_counter;
jstree_id_to_temp_id_map[full_node.id] = temp_id;
}
nodes_with_data.push({
'index': index, 'id': temp_id, 'parent_jstree_id': full_node.parent,
'depth': full_node.parents.length, 'order': index, 'data': node_data
});
});
$.each(nodes_with_data, function(index, node_info) {
var parent_id = 0;
if (node_info.parent_jstree_id !== '#') {
parent_id = jstree_id_to_temp_id_map[node_info.parent_jstree_id] || node_info.parent_jstree_id;
}
var node_data = node_info.data;
var use_pc = node_data.me_use == "1" ? `<input type="hidden" name="me_use[${index}]" value="1">` : '';
var use_mobile = node_data.me_mobile_use == "1" ? `<input type="hidden" name="me_mobile_use[${index}]" value="1">` : '';
form_data_div.append(`<input type="hidden" name="me_id[${index}]" value="${node_info.id}">`);
form_data_div.append(`<input type="hidden" name="me_parent_id[${index}]" value="${parent_id}">`);
form_data_div.append(`<input type="hidden" name="me_depth[${index}]" value="${node_info.depth}">`);
form_data_div.append(`<input type="hidden" name="me_order[${index}]" value="${node_info.order}">`);
form_data_div.append(`<input type="hidden" name="me_name[${index}]" value="${node_data.me_name}">`);
form_data_div.append(`<input type="hidden" name="me_link[${index}]" value="${node_data.me_link}">`);
form_data_div.append(`<input type="hidden" name="me_target[${index}]" value="${node_data.me_target}">`);
form_data_div.append(use_pc);
form_data_div.append(use_mobile);
form_data_div.append(`<input type="hidden" name="me_level[${index}]" value="${node_data.me_level || 1}">`);
form_data_div.append(`<input type="hidden" name="me_level_opt[${index}]" value="${node_data.me_level_opt || 1}">`);
});
return true;
});
});
</script>
<?php
include_once ('./admin.tail.php');
?>