first commit 2

This commit is contained in:
hmw1001
2026-06-11 18:47:38 +09:00
parent c768729ce6
commit 6f534e33a6
11095 changed files with 1595758 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
<?php
define('G5_IS_ADMIN', true);
require_once '../common.php';
require_once G5_ADMIN_PATH . '/admin.lib.php';
if (isset($token)) {
$token = @htmlspecialchars(strip_tags($token), ENT_QUOTES);
}
run_event('admin_common');
+122
View File
@@ -0,0 +1,122 @@
<?php
if (!defined('_GNUBOARD_')) {
exit;
}
$is_use_apache = (stripos($_SERVER['SERVER_SOFTWARE'], 'apache') !== false);
$is_use_nginx = (stripos($_SERVER['SERVER_SOFTWARE'], 'nginx') !== false);
$is_use_iis = !$is_use_apache && (stripos($_SERVER['SERVER_SOFTWARE'], 'microsoft-iis') !== false);
$is_write_file = false;
$is_apache_need_rules = false;
$is_apache_rewrite = false;
if (!($is_use_apache || $is_use_nginx || $is_use_iis)) { // 셋다 아니면 다 출력시킨다.
$is_use_apache = true;
$is_use_nginx = true;
}
if ($is_use_nginx) {
$is_write_file = false;
}
if ($is_use_apache) {
$is_write_file = (is_writable(G5_PATH) || (file_exists(G5_PATH . '/.htaccess') && is_writable(G5_PATH . '/.htaccess'))) ? true : false;
$is_apache_need_rules = check_need_rewrite_rules();
$is_apache_rewrite = function_exists('apache_get_modules') && in_array('mod_rewrite', apache_get_modules());
}
$get_path_url = parse_url(G5_URL);
$base_path = isset($get_path_url['path']) ? $get_path_url['path'] . '/' : '/';
// add_stylesheet('css 구문', 출력순서); 숫자가 작을 수록 먼저 출력됨
add_stylesheet('<link rel="stylesheet" href="' . G5_JS_URL . '/remodal/remodal.css">', 11);
add_stylesheet('<link rel="stylesheet" href="' . G5_JS_URL . '/remodal/remodal-default-theme.css">', 12);
add_javascript('<script src="' . G5_JS_URL . '/remodal/remodal.js"></script>', 10);
?>
<section id="anc_cf_url">
<h2 class="h2_frm">짧은 주소 설정</h2>
<?php echo $pg_anchor ?>
<div class="local_desc02 local_desc">
<p>
게시판과 컨텐츠 페이지에 짧은 URL 을 사용합니다. <a href="https://sir.kr/manual/g5/286" class="btn btn_03" target="_blank" style="margin-left:10px">설정 관련 메뉴얼 보기</a>
<?php if ($is_use_apache && !$is_use_nginx) { ?>
<?php if (!$is_apache_rewrite) { ?>
<br><strong>Apache 서버인 경우 rewrite_module 이 비활성화 되어 있으면 짧은 주소를 사용할수 없습니다.</strong>
<?php } elseif (!$is_write_file && $is_apache_need_rules) { // apache인 경우 ?>
<br><strong>짧은 주소 사용시 아래 Apache 설정 코드를 참고하여 설정해 주세요.</strong>
<?php } ?>
<?php } ?>
</p>
</div>
<div class="server_config_views">
<?php if ($is_use_apache) { ?>
<button type="button" data-remodal-target="modal_apache" class="btn btn_03">Apache 설정 코드 보기</button>
<?php } ?>
<?php if ($is_use_nginx) { ?>
<button type="button" data-remodal-target="modal_nginx" class="btn btn_03">Nginx 설정 코드 보기</button>
<?php } ?>
</div>
<div class="tbl_frm01 tbl_wrap">
<table>
<caption>짧은주소 설정</caption>
<colgroup>
<col class="grid_4">
<col>
</colgroup>
<tbody>
<?php
$short_url_arrs = array(
'0' => array('label' => '사용안함', 'url' => G5_URL . '/board.php?bo_table=free&wr_id=123'),
'1' => array('label' => '숫자', 'url' => G5_URL . '/free/123'),
'2' => array('label' => '글 이름', 'url' => G5_URL . '/free/안녕하세요/'),
);
foreach ($short_url_arrs as $k => $v) {
$checked = ((int) $config['cf_bbs_rewrite'] === (int) $k) ? 'checked' : '';
?>
<tr>
<td><input name="cf_bbs_rewrite" id="cf_bbs_rewrite_<?php echo $k; ?>" type="radio" value="<?php echo $k; ?>" <?php echo $checked; ?>><label for="cf_bbs_rewrite_<?php echo $k; ?>" class="rules_label"><?php echo $v['label']; ?></label></td>
<td><?php echo $v['url']; ?></td>
</tr>
<?php } //end foreach ?>
</tbody>
</table>
</div>
<div class="server_rewrite_info">
<div class="is_rewrite remodal" data-remodal-id="modal_apache" role="dialog" aria-labelledby="modalApache" aria-describedby="modal1Desc">
<button type="button" class="connect-close" data-remodal-action="close">
<i class="fa fa-close"></i>
<span class="txt">닫기</span>
</button>
<h4 class="copy_title">.htaccess 파일에 적용할 코드입니다.
<?php if (!$is_apache_rewrite) { ?>
<br><span class="info-warning">Apache 서버인 경우 rewrite_module 이 비활성화 되어 있으면 짧은 주소를 사용할수 없습니다.</span>
<?php } elseif (!$is_write_file && $is_apache_need_rules) { ?>
<br><span class="info-warning">자동으로 .htaccess 파일을 수정 할수 있는 권한이 없습니다.<br>.htaccess 파일이 없다면 생성 후에, 아래 코드가 없으면 코드를 복사하여 붙여넣기 해 주세요.</span>
<?php } elseif (!$is_apache_need_rules) { ?>
<br><span class="info-success">정상적으로 적용된 상태입니다.</span>
<?php } ?>
</h4>
<textarea readonly="readonly" rows="10"><?php echo get_mod_rewrite_rules(true); ?></textarea>
</div>
<div class="is_rewrite remodal" data-remodal-id="modal_nginx" role="dialog" aria-labelledby="modalNginx" aria-describedby="modal2Desc">
<button type="button" class="connect-close" data-remodal-action="close">
<i class="fa fa-close"></i>
<span class="txt">닫기</span>
</button>
<h4 class="copy_title">아래 코드를 복사하여 nginx 설정 파일에 적용해 주세요.</h4>
<textarea readonly="readonly" rows="10"><?php echo get_nginx_conf_rules(true); ?></textarea>
</div>
</div>
</section>
+215
View File
@@ -0,0 +1,215 @@
<?php
if (!defined('_GNUBOARD_')) {
exit;
}
$g5_debug['php']['begin_time'] = $begin_time = get_microtime();
$files = glob(G5_ADMIN_PATH . '/css/admin_extend_*');
if (is_array($files)) {
foreach ((array) $files as $k => $css_file) {
$fileinfo = pathinfo($css_file);
$ext = $fileinfo['extension'];
if ($ext !== 'css') {
continue;
}
$css_file = str_replace(G5_ADMIN_PATH, G5_ADMIN_URL, $css_file);
add_stylesheet('<link rel="stylesheet" href="' . $css_file . '">', $k);
}
}
require_once G5_PATH . '/head.sub.php';
function print_menu1($key, $no = '')
{
global $menu;
$str = print_menu2($key, $no);
return $str;
}
function print_menu2($key, $no = '')
{
global $menu, $auth_menu, $is_admin, $auth, $g5, $sub_menu;
$str = "<ul>";
for ($i = 1; $i < count($menu[$key]); $i++) {
if (!isset($menu[$key][$i])) {
continue;
}
if ($is_admin != 'super' && (!array_key_exists($menu[$key][$i][0], $auth) || !strstr($auth[$menu[$key][$i][0]], 'r'))) {
continue;
}
$gnb_grp_div = $gnb_grp_style = '';
if (isset($menu[$key][$i][4])) {
if (($menu[$key][$i][4] == 1 && $gnb_grp_style == false) || ($menu[$key][$i][4] != 1 && $gnb_grp_style == true)) {
$gnb_grp_div = 'gnb_grp_div';
}
if ($menu[$key][$i][4] == 1) {
$gnb_grp_style = 'gnb_grp_style';
}
}
$current_class = '';
if ($menu[$key][$i][0] == $sub_menu) {
$current_class = ' on';
}
$str .= '<li data-menu="' . $menu[$key][$i][0] . '"><a href="' . $menu[$key][$i][2] . '" class="gnb_2da ' . $gnb_grp_style . ' ' . $gnb_grp_div . $current_class . '">' . $menu[$key][$i][1] . '</a></li>';
$auth_menu[$menu[$key][$i][0]] = $menu[$key][$i][1];
}
$str .= "</ul>";
return $str;
}
$adm_menu_cookie = array(
'container' => '',
'gnb' => '',
'btn_gnb' => '',
);
if (!empty($_COOKIE['g5_admin_btn_gnb'])) {
$adm_menu_cookie['container'] = 'container-small';
$adm_menu_cookie['gnb'] = 'gnb_small';
$adm_menu_cookie['btn_gnb'] = 'btn_gnb_open';
}
?>
<script>
var g5_admin_csrf_token_key = "<?php echo (function_exists('admin_csrf_token_key')) ? admin_csrf_token_key() : ''; ?>";
var tempX = 0;
var tempY = 0;
function imageview(id, w, h) {
menu(id);
var el_id = document.getElementById(id);
//submenu = eval(name+".style");
submenu = el_id.style;
submenu.left = tempX - (w + 11);
submenu.top = tempY - (h / 2);
selectBoxVisible();
if (el_id.style.display != 'none')
selectBoxHidden(id);
}
</script>
<div id="to_content"><a href="#container">본문 바로가기</a></div>
<header id="hd">
<h1><?php echo $config['cf_title'] ?></h1>
<div id="hd_top">
<button type="button" id="btn_gnb" class="btn_gnb_close <?php echo $adm_menu_cookie['btn_gnb']; ?>">메뉴</button>
<div id="logo"><a href="<?php echo correct_goto_url(G5_ADMIN_URL); ?>"><img src="<?php echo G5_ADMIN_URL ?>/img/logo.png" alt="<?php echo get_text($config['cf_title']); ?> 관리자"></a></div>
<div id="tnb">
<ul>
<?php if (defined('G5_USE_SHOP') && G5_USE_SHOP) { ?>
<li class="tnb_li"><a href="<?php echo G5_SHOP_URL ?>/" class="tnb_shop" target="_blank" title="쇼핑몰 바로가기">쇼핑몰 바로가기</a></li>
<?php } ?>
<li class="tnb_li"><a href="<?php echo G5_URL ?>/" class="tnb_community" target="_blank" title="커뮤니티 바로가기">커뮤니티 바로가기</a></li>
<li class="tnb_li"><a href="<?php echo G5_ADMIN_URL ?>/service.php" class="tnb_service">부가서비스</a></li>
<li class="tnb_li"><button type="button" class="tnb_mb_btn">관리자<span class="./img/btn_gnb.png">메뉴열기</span></button>
<ul class="tnb_mb_area">
<li><a href="<?php echo G5_ADMIN_URL ?>/member_form.php?w=u&amp;mb_id=<?php echo $member['mb_id'] ?>">관리자정보</a></li>
<li id="tnb_logout"><a href="<?php echo G5_BBS_URL ?>/logout.php">로그아웃</a></li>
</ul>
</li>
</ul>
</div>
</div>
<nav id="gnb" class="gnb_large <?php echo $adm_menu_cookie['gnb']; ?>">
<h2>관리자 주메뉴</h2>
<ul class="gnb_ul">
<?php
$jj = 1;
foreach ($amenu as $key => $value) {
$href1 = $href2 = '';
if (isset($menu['menu' . $key][0][2]) && $menu['menu' . $key][0][2]) {
$href1 = '<a href="' . $menu['menu' . $key][0][2] . '" class="gnb_1da">';
$href2 = '</a>';
} else {
continue;
}
$current_class = "";
if (isset($sub_menu) && (substr($sub_menu, 0, 3) == substr($menu['menu' . $key][0][0], 0, 3))) {
$current_class = " on";
}
$button_title = $menu['menu' . $key][0][1];
?>
<li class="gnb_li<?php echo $current_class; ?>">
<button type="button" class="btn_op menu-<?php echo $key; ?> menu-order-<?php echo $jj; ?>" title="<?php echo $button_title; ?>"><?php echo $button_title; ?></button>
<div class="gnb_oparea_wr">
<div class="gnb_oparea">
<h3><?php echo $menu['menu' . $key][0][1]; ?></h3>
<?php echo print_menu1('menu' . $key, 1); ?>
</div>
</div>
</li>
<?php
$jj++;
} //end foreach
?>
</ul>
</nav>
</header>
<script>
jQuery(function($) {
var menu_cookie_key = 'g5_admin_btn_gnb';
$(".tnb_mb_btn").click(function() {
$(".tnb_mb_area").toggle();
});
$("#btn_gnb").click(function() {
var $this = $(this);
try {
if (!$this.hasClass("btn_gnb_open")) {
set_cookie(menu_cookie_key, 1, 60 * 60 * 24 * 365);
} else {
delete_cookie(menu_cookie_key);
}
} catch (err) {}
$("#container").toggleClass("container-small");
$("#gnb").toggleClass("gnb_small");
$this.toggleClass("btn_gnb_open");
});
$(".gnb_ul li .btn_op").click(function() {
$(this).parent().addClass("on").siblings().removeClass("on");
});
});
</script>
<div id="wrapper">
<div id="container" class="<?php echo $adm_menu_cookie['container']; ?>">
<h1 id="container_title"><?php echo $g5['title'] ?></h1>
<div class="container_wr">
+131
View File
@@ -0,0 +1,131 @@
function check_all(f)
{
var chk = document.getElementsByName("chk[]");
for (i=0; i<chk.length; i++)
chk[i].checked = f.chkall.checked;
}
function btn_check(f, act)
{
if (act == "update") // 선택수정
{
f.action = list_update_php;
str = "수정";
}
else if (act == "delete") // 선택삭제
{
f.action = list_delete_php;
str = "삭제";
}
else
return;
var chk = document.getElementsByName("chk[]");
var bchk = false;
for (i=0; i<chk.length; i++)
{
if (chk[i].checked)
bchk = true;
}
if (!bchk)
{
alert(str + "할 자료를 하나 이상 선택하세요.");
return;
}
if (act == "delete")
{
if (!confirm("선택한 자료를 정말 삭제 하시겠습니까?"))
return;
}
f.submit();
}
function is_checked(elements_name)
{
var checked = false;
var chk = document.getElementsByName(elements_name);
for (var i=0; i<chk.length; i++) {
if (chk[i].checked) {
checked = true;
}
}
return checked;
}
function delete_confirm(el)
{
if(confirm("한번 삭제한 자료는 복구할 방법이 없습니다.\n\n정말 삭제하시겠습니까?")) {
var token = get_ajax_token();
var href = el.href.replace(/&token=.+$/g, "");
if(!token) {
alert("토큰 정보가 올바르지 않습니다.");
return false;
}
el.href = href+"&token="+token;
return true;
} else {
return false;
}
}
function delete_confirm2(msg)
{
if(confirm(msg))
return true;
else
return false;
}
function get_ajax_token()
{
var token = "",
admin_csrf_token_key = (typeof g5_admin_csrf_token_key !== "undefined") ? g5_admin_csrf_token_key : "";
$.ajax({
type: "POST",
url: g5_admin_url+"/ajax.token.php",
data : {admin_csrf_token_key:admin_csrf_token_key},
cache: false,
async: false,
dataType: "json",
success: function(data) {
if(data.error) {
alert(data.error);
if(data.url)
document.location.href = data.url;
return false;
}
token = data.token;
}
});
return token;
}
$(function() {
$(document).on("click", "form input:submit, form button:submit", function() {
var f = this.form;
var token = get_ajax_token();
if(!token) {
alert("토큰 정보가 올바르지 않습니다.");
return false;
}
var $f = $(f);
if(typeof f.token === "undefined")
$f.prepend('<input type="hidden" name="token" value="">');
$f.find("input[name=token]").val(token);
return true;
});
});
+702
View File
@@ -0,0 +1,702 @@
<?php
if (!defined('_GNUBOARD_')) {
exit;
}
/*
// 081022 : CSRF 방지를 위해 코드를 작성했으나 효과가 없어 주석처리 함
if (!get_session('ss_admin')) {
set_session('ss_admin', true);
goto_url('.');
}
*/
// 스킨디렉토리를 SELECT 형식으로 얻음
function get_skin_select($skin_gubun, $id, $name, $selected = '', $event = '')
{
global $config;
$skins = array();
if (defined('G5_THEME_PATH') && $config['cf_theme']) {
$dirs = get_skin_dir($skin_gubun, G5_THEME_PATH . '/' . G5_SKIN_DIR);
if (!empty($dirs)) {
foreach ($dirs as $dir) {
$skins[] = 'theme/' . $dir;
}
}
}
$skins = array_merge($skins, get_skin_dir($skin_gubun));
$str = "<select id=\"$id\" name=\"$name\" $event>\n";
for ($i = 0; $i < count($skins); $i++) {
if ($i == 0) {
$str .= "<option value=\"\">선택</option>";
}
if (preg_match('#^theme/(.+)$#', $skins[$i], $match)) {
$text = '(테마) ' . $match[1];
} else {
$text = $skins[$i];
}
$str .= option_selected($skins[$i], $selected, $text);
}
$str .= "</select>";
return $str;
}
// 모바일 스킨디렉토리를 SELECT 형식으로 얻음
function get_mobile_skin_select($skin_gubun, $id, $name, $selected = '', $event = '')
{
global $config;
$skins = array();
if (defined('G5_THEME_PATH') && $config['cf_theme']) {
$dirs = get_skin_dir($skin_gubun, G5_THEME_MOBILE_PATH . '/' . G5_SKIN_DIR);
if (!empty($dirs)) {
foreach ($dirs as $dir) {
$skins[] = 'theme/' . $dir;
}
}
}
$skins = array_merge($skins, get_skin_dir($skin_gubun, G5_MOBILE_PATH . '/' . G5_SKIN_DIR));
$str = "<select id=\"$id\" name=\"$name\" $event>\n";
for ($i = 0; $i < count($skins); $i++) {
if ($i == 0) {
$str .= "<option value=\"\">선택</option>";
}
if (preg_match('#^theme/(.+)$#', $skins[$i], $match)) {
$text = '(테마) ' . $match[1];
} else {
$text = $skins[$i];
}
$str .= option_selected($skins[$i], $selected, $text);
}
$str .= "</select>";
return $str;
}
// 스킨경로를 얻는다
function get_skin_dir($skin, $skin_path = G5_SKIN_PATH)
{
global $g5;
$result_array = array();
$dirname = $skin_path . '/' . $skin . '/';
if (!is_dir($dirname)) {
return array();
}
$handle = opendir($dirname);
while ($file = readdir($handle)) {
if ($file == '.' || $file == '..') {
continue;
}
if (is_dir($dirname . $file)) {
$result_array[] = $file;
}
}
closedir($handle);
sort($result_array);
return $result_array;
}
// 테마
function get_theme_dir()
{
$result_array = array();
$dirname = G5_PATH . '/' . G5_THEME_DIR . '/';
$handle = opendir($dirname);
while ($file = readdir($handle)) {
if ($file == '.' || $file == '..') {
continue;
}
if (is_dir($dirname . $file)) {
$theme_path = $dirname . $file;
if (is_file($theme_path . '/index.php') && is_file($theme_path . '/head.php') && is_file($theme_path . '/tail.php')) {
$result_array[] = $file;
}
}
}
closedir($handle);
natsort($result_array);
return $result_array;
}
// 테마정보
function get_theme_info($dir)
{
$info = array();
$path = G5_PATH . '/' . G5_THEME_DIR . '/' . $dir;
if (is_dir($path)) {
$screenshot = $path . '/screenshot.png';
$screenshot_url = '';
if (is_file($screenshot)) {
$size = @getimagesize($screenshot);
if (isset($size[2]) && $size[2] == 3) { // PNG
$screenshot_url = str_replace(G5_PATH, G5_URL, $screenshot);
}
}
$info['screenshot'] = $screenshot_url;
$text = $path . '/readme.txt';
if (is_file($text)) {
$content = file($text, false);
$content = array_map('trim', $content);
// 💡 [핵심 수정] readme.txt 파일의 각 줄에 정보가 있는지 확인하고, 있을 경우에만 값을 할당하도록 변경
preg_match('#^Theme Name:(.+)$#i', (isset($content[0]) ? $content[0] : ''), $m0);
preg_match('#^Theme URI:(.+)$#i', (isset($content[1]) ? $content[1] : ''), $m1);
preg_match('#^Maker:(.+)$#i', (isset($content[2]) ? $content[2] : ''), $m2);
preg_match('#^Maker URI:(.+)$#i', (isset($content[3]) ? $content[3] : ''), $m3);
preg_match('#^Version:(.+)$#i', (isset($content[4]) ? $content[4] : ''), $m4);
preg_match('#^Detail:(.+)$#i', (isset($content[5]) ? $content[5] : ''), $m5);
preg_match('#^License:(.+)$#i', (isset($content[6]) ? $content[6] : ''), $m6);
preg_match('#^License URI:(.+)$#i', (isset($content[7]) ? $content[7] : ''), $m7);
$info['theme_name'] = isset($m0[1]) ? trim($m0[1]) : '';
$info['theme_uri'] = isset($m1[1]) ? trim($m1[1]) : '';
$info['maker'] = isset($m2[1]) ? trim($m2[1]) : '';
$info['maker_uri'] = isset($m3[1]) ? trim($m3[1]) : '';
$info['version'] = isset($m4[1]) ? trim($m4[1]) : '';
$info['detail'] = isset($m5[1]) ? trim($m5[1]) : '';
$info['license'] = isset($m6[1]) ? trim($m6[1]) : '';
$info['license_uri'] = isset($m7[1]) ? trim($m7[1]) : '';
}
if (empty($info['theme_name'])) {
$info['theme_name'] = $dir;
}
}
return $info;
}
// 테마설정 정보
function get_theme_config_value($dir, $key = '*')
{
$tconfig = array();
$theme_config_file = G5_PATH . '/' . G5_THEME_DIR . '/' . $dir . '/theme.config.php';
if (is_file($theme_config_file)) {
include $theme_config_file;
// 22.05.26 Undefined Variable $theme_config;
if ($key == '*') {
$tconfig = $theme_config;
} else {
$keys = array_map('trim', explode(',', $key));
foreach ($keys as $v) {
$tconfig[$v] = isset($theme_config[$v]) ? trim($theme_config[$v]) : '';
}
}
}
return $tconfig;
}
// 회원권한을 SELECT 형식으로 얻음
function get_member_level_select($name, $start_id = 0, $end_id = 10, $selected = "", $event = "")
{
global $g5;
$str = "\n<select id=\"{$name}\" name=\"{$name}\"";
if ($event) {
$str .= " $event";
}
$str .= ">\n";
for ($i = $start_id; $i <= $end_id; $i++) {
$str .= '<option value="' . $i . '"';
if ($i == $selected) {
$str .= ' selected="selected"';
}
$str .= ">{$i}</option>\n";
}
$str .= "</select>\n";
return $str;
}
// 회원아이디를 SELECT 형식으로 얻음
function get_member_id_select($name, $level, $selected = "", $event = "")
{
global $g5;
$sql = " select mb_id from {$g5['member_table']} where mb_level >= '{$level}' ";
$result = sql_query($sql);
$str = '<select id="' . $name . '" name="' . $name . '" ' . $event . '><option value="">선택안함</option>';
for ($i = 0; $row = sql_fetch_array($result); $i++) {
$str .= '<option value="' . $row['mb_id'] . '"';
if ($row['mb_id'] == $selected) {
$str .= ' selected';
}
$str .= '>' . $row['mb_id'] . '</option>';
}
$str .= '</select>';
return $str;
}
// php8 버전 호환 권한 검사 함수
function auth_check_menu($auth, $sub_menu, $attr, $return = false)
{
$check_auth = isset($auth[$sub_menu]) ? $auth[$sub_menu] : '';
return auth_check($check_auth, $attr, $return);
}
// 권한 검사
function auth_check($auth, $attr, $return = false)
{
global $is_admin;
if ($is_admin == 'super') {
return;
}
if (!trim($auth)) {
$msg = '이 메뉴에는 접근 권한이 없습니다.\\n\\n접근 권한은 최고관리자만 부여할 수 있습니다.';
if ($return) {
return $msg;
} else {
alert($msg);
}
}
$attr = strtolower($attr);
if (!strstr($auth, $attr)) {
if ($attr == 'r') {
$msg = '읽을 권한이 없습니다.';
if ($return) {
return $msg;
} else {
alert($msg);
}
} else if ($attr == 'w') {
$msg = '입력, 추가, 생성, 수정 권한이 없습니다.';
if ($return) {
return $msg;
} else {
alert($msg);
}
} else if ($attr == 'd') {
$msg = '삭제 권한이 없습니다.';
if ($return) {
return $msg;
} else {
alert($msg);
}
} else {
$msg = '속성이 잘못 되었습니다.';
if ($return) {
return $msg;
} else {
alert($msg);
}
}
}
}
// 작업아이콘 출력
function icon($act, $link = '', $target = '_parent')
{
global $g5;
$img = array('입력' => 'insert', '추가' => 'insert', '생성' => 'insert', '수정' => 'modify', '삭제' => 'delete', '이동' => 'move', '그룹' => 'move', '보기' => 'view', '미리보기' => 'view', '복사' => 'copy');
$icon = '<img src="' . G5_ADMIN_PATH . '/img/icon_' . $img[$act] . '.gif" title="' . $act . '">';
if ($link) {
$s = '<a href="' . $link . '">' . $icon . '</a>';
} else {
$s = $icon;
}
return $s;
}
// rm -rf 옵션 : exec(), system() 함수를 사용할 수 없는 서버 또는 win32용 대체
// www.php.net 참고 : pal at degerstrom dot com
function rm_rf($file)
{
if (file_exists($file)) {
if (is_dir($file)) {
$handle = opendir($file);
while ($filename = readdir($handle)) {
if ($filename != '.' && $filename != '..') {
rm_rf($file . '/' . $filename);
}
}
closedir($handle);
@chmod($file, G5_DIR_PERMISSION);
@rmdir($file);
} else {
@chmod($file, G5_FILE_PERMISSION);
@unlink($file);
}
}
}
// 입력 폼 안내문
function help($help = "")
{
global $g5;
$str = '<span class="frm_info">' . str_replace("\n", "<br>", $help) . '</span>';
return $str;
}
// 출력순서
function order_select($fld, $sel = '')
{
$s = '<select name="' . $fld . '" id="' . $fld . '">';
for ($i = 1; $i <= 100; $i++) {
$s .= '<option value="' . $i . '" ';
if ($sel) {
if ($i == $sel) {
$s .= 'selected';
}
} else {
if ($i == 50) {
$s .= 'selected';
}
}
$s .= '>' . $i . '</option>';
}
$s .= '</select>';
return $s;
}
// 불법접근을 막도록 토큰을 생성하면서 토큰값을 리턴
function get_admin_token()
{
$token = md5(uniqid(rand(), true));
set_session('ss_admin_token', $token);
return $token;
}
// 관리자가 자동등록방지를 사용해야 할 경우
function get_admin_captcha_by($type = 'get')
{
$captcha_name = 'ss_admin_use_captcha';
if ($type === 'remove') {
set_session($captcha_name, '');
}
return get_session($captcha_name);
}
//input value 에서 xss 공격 filter 역할을 함 ( 반드시 input value='' 타입에만 사용할것 )
function get_sanitize_input($s, $is_html = false)
{
if (!$is_html) {
$s = strip_tags($s);
}
$s = htmlspecialchars($s, ENT_QUOTES, 'utf-8');
return $s;
}
function domain_mail_host($is_at=true){
list($domain_host,) = explode(':', $_SERVER['HTTP_HOST']);
if ('www.' === substr($domain_host, 0, 4)) {
$domain_host = substr($domain_host, 4);
}
return $is_at ? '@'.$domain_host : $domain_host;
}
function check_log_folder($log_path, $is_delete = true)
{
if (is_writable($log_path)) {
// 아파치 서버인 경우 웹에서 해당 폴더 접근 막기
$htaccess_file = $log_path . '/.htaccess';
if (!file_exists($htaccess_file)) {
if ($handle = @fopen($htaccess_file, 'w')) {
fwrite($handle, 'Order deny,allow' . "\n");
fwrite($handle, 'Deny from all' . "\n");
fclose($handle);
}
}
// 아파치 서버인 경우 해당 디렉토리 파일 목록 안보이게 하기
$index_file = $log_path . '/index.php';
if (!file_exists($index_file)) {
if ($handle = @fopen($index_file, 'w')) {
fwrite($handle, '');
fclose($handle);
}
}
}
if ($is_delete) {
try {
// txt 파일과 log 파일을 조회하여 30일이 지난 파일은 삭제합니다.
$txt_files = glob($log_path . '/*.txt');
$log_files = glob($log_path . '/*.log');
$del_files = array_merge($txt_files, $log_files);
if ($del_files && is_array($del_files)) {
foreach ($del_files as $del_file) {
$filetime = filemtime($del_file);
// 30일이 지난 파일을 삭제
if ($filetime && $filetime < (G5_SERVER_TIME - 2592000)) {
@unlink($del_file);
}
}
}
} catch (Exception $e) {
}
}
}
// POST로 넘어온 토큰과 세션에 저장된 토큰 비교
function check_admin_token()
{
$token = get_session('ss_admin_token');
set_session('ss_admin_token', '');
if (!$token || !$_REQUEST['token'] || $token != $_REQUEST['token']) {
alert('올바른 방법으로 이용해 주십시오.', G5_URL);
}
return true;
}
function admin_csrf_token_key($is_must=0){
global $member;
$key = '';
if($is_must || !((isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'))){
$key = md5((isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : '').(defined('G5_TOKEN_ENCRYPTION_KEY') ? G5_TOKEN_ENCRYPTION_KEY : '').$member['mb_id'].$_SERVER['DOCUMENT_ROOT']);
}
return run_replace('admin_csrf_token_key', $key, $is_must);
}
// 관리자 페이지 referer 체크
function admin_referer_check($return = false)
{
$referer = isset($_SERVER['HTTP_REFERER']) ? trim($_SERVER['HTTP_REFERER']) : '';
if (!$referer) {
$msg = '정보가 올바르지 않습니다.';
if ($return) {
return $msg;
} else {
alert($msg, G5_URL);
}
}
$p = @parse_url($referer);
$host = preg_replace('/:[0-9]+$/', '', $_SERVER['HTTP_HOST']);
$msg = '';
if ($host != $p['host']) {
$msg = '올바른 방법으로 이용해 주십시오.';
}
if ($p['path'] && !preg_match('/\/' . preg_quote(G5_ADMIN_DIR) . '\//i', $p['path'])) {
$msg = '올바른 방법으로 이용해 주십시오';
}
if ($msg) {
if ($return) {
return $msg;
} else {
alert($msg, G5_URL);
}
}
}
function admin_check_xss_params($params)
{
if (!$params) {
return;
}
foreach ($params as $key => $value) {
if (empty($value)) {
continue;
}
if (is_array($value)) {
admin_check_xss_params($value);
} else if (
(preg_match('/<\s?[^\>]*\/?\s?>/i', $value) && (preg_match('/script.*?\/script/ius', $value) || preg_match('/on[a-z]+=*/ius', $value))) || preg_match('/^(?=.*token\()(?=.*xmlhttprequest\()(?=.*send\().*$/im', $value) ||
(preg_match('/(on[a-z]+|focus)=.*/ius', $value) && preg_match('/(eval|atob|fetch|expression|exec|prompt)(\s*)\((.*)\)/ius', $value))) {
alert('요청 쿼리에 잘못된 스크립트문장이 있습니다.\\nXSS 공격일수도 있습니다.', G5_URL);
die();
} else if (preg_match('/atob\s*\(\s*[\'"]?([a-zA-Z0-9+\/=]+)[\'"]?\s*\)/ius', $value, $matches)) {
$decoded = base64_decode($matches[1], true);
if ($decoded && preg_match('/(eval|fetch|script|alert|settimeout|setinterval)/ius', $decoded)) {
// error_log("Base64 XSS 시도 감지: key=$key, decoded=$decoded, IP=" . $_SERVER['REMOTE_ADDR']);
alert('Base64로 인코딩된 위험한 스크립트가 발견되었습니다.', G5_URL);
die();
}
}
}
return;
}
function admin_menu_find_by($call, $search_key)
{
global $menu;
static $cache_menu = array();
if (empty($cache_menu)) {
foreach ($menu as $k1 => $arr1) {
if (empty($arr1)) {
continue;
}
foreach ($arr1 as $k2 => $arr2) {
if (empty($arr2)) {
continue;
}
$menu_key = isset($arr2[3]) ? $arr2[3] : '';
if (empty($menu_key)) {
continue;
}
$cache_menu[$menu_key] = array(
'sub_menu' => $arr2[0],
'title' => $arr2[1],
'link' => $arr2[2],
);
}
}
}
if (isset($cache_menu[$call]) && isset($cache_menu[$call][$search_key])) {
return $cache_menu[$call][$search_key];
}
return '';
}
// 접근 권한 검사
if (!$member['mb_id']) {
alert('로그인 하십시오.', G5_BBS_URL . '/login.php?url=' . urlencode(correct_goto_url(G5_ADMIN_URL)));
} else if ($is_admin != 'super') {
$auth = array();
$sql = " select au_menu, au_auth from {$g5['auth_table']} where mb_id = '{$member['mb_id']}' ";
$result = sql_query($sql);
for ($i = 0; $row = sql_fetch_array($result); $i++) {
$auth[$row['au_menu']] = $row['au_auth'];
}
if (!$i) {
alert('최고관리자 또는 관리권한이 있는 회원만 접근 가능합니다.', G5_URL);
}
}
// 관리자의 클라이언트를 검증하여 일치하지 않으면 세션을 끊고 관리자에게 메일을 보낸다.
if (!verify_mb_key($member)) {
session_destroy();
include_once G5_LIB_PATH . '/mailer.lib.php';
// 메일 알림
mailer($member['mb_nick'], $member['mb_email'], $member['mb_email'], 'XSS 공격 알림', $_SERVER['REMOTE_ADDR'] . ' 아이피로 XSS 공격이 있었습니다.<br><br>관리자 권한을 탈취하려는 접근이므로 주의하시기 바랍니다.<br><br>해당 아이피는 차단하시고 의심되는 게시물이 있는지 확인하시기 바랍니다.' . G5_URL, 0);
alert_close('정상적으로 로그인하여 접근하시기 바랍니다.');
}
if (isset($auth) && is_array($auth)) {
@ksort($auth);
} else {
$auth = array();
}
// 가변 메뉴
unset($auth_menu);
unset($menu);
unset($amenu);
$tmp = dir(G5_ADMIN_PATH);
$menu_files = array();
while ($entry = $tmp->read()) {
if (!preg_match('/^admin.menu([0-9]{3}).*\.php$/', $entry, $m)) {
continue; // 파일명이 menu 으로 시작하지 않으면 무시한다.
}
$amenu[$m[1]] = $entry;
$menu_files[] = G5_ADMIN_PATH . '/' . $entry;
}
@asort($menu_files);
foreach ($menu_files as $file) {
include_once $file;
}
@ksort($amenu);
$amenu = run_replace('admin_amenu', $amenu);
if (isset($menu) && $menu) {
$menu = run_replace('admin_menu', $menu);
}
$arr_query = array();
if (isset($sst)) {
$arr_query[] = 'sst=' . $sst;
}
if (isset($sod)) {
$arr_query[] = 'sod=' . $sod;
}
if (isset($sfl)) {
$arr_query[] = 'sfl=' . $sfl;
}
if (isset($stx)) {
$arr_query[] = 'stx=' . $stx;
}
if (isset($page)) {
$arr_query[] = 'page=' . $page;
}
$qstr = implode("&amp;", $arr_query);
if (isset($_REQUEST) && $_REQUEST) {
if (admin_referer_check(true)) {
admin_check_xss_params($_REQUEST);
}
}
// 관리자에서는 추가 스크립트와 추가 매타태그, 방문자분석 스크립트가 실행되지 않게 빈값으로 합니다.
if (run_replace('safe_admin_add_script_boolean', false) === false) {
$config['cf_analytics'] = '';
$config['cf_add_script'] = '';
$config['cf_add_meta'] = '';
}
+12
View File
@@ -0,0 +1,12 @@
<?php
$menu['menu000'] = array(
array('000000', '빌더설정', G5_ADMIN_URL . '/rb/rb_form.php', 'rb_config'),
array('000000', '빌더설정', G5_ADMIN_URL . '/rb/rb_form.php', 'rb_config'),
array('000100', '환경설정', G5_ADMIN_URL . '/rb/config_form.php', 'rb_config'),
array('000200', '모듈관리', G5_ADMIN_URL . '/rb/module_list.php', 'rb_config'),
array('000000', ' ', G5_ADMIN_URL . '', 'rb_config'),
array('000300', '배너 관리', G5_ADMIN_URL . '/rb/banner_list.php', 'rb_config'),
array('000400', '게시물 관리', G5_ADMIN_URL . '/rb/bbs_list.php', 'rb_config'),
array('000500', 'SEO 관리 ', G5_ADMIN_URL . '/rb/seo_form.php', 'rb_config'),
array('000000', ' ', G5_ADMIN_URL . '', 'rb_config'),
);
+24
View File
@@ -0,0 +1,24 @@
<?php
$menu['menu100'] = array(
array('100000', '환경설정', G5_ADMIN_URL . '/config_form.php', 'config'),
array('100100', '기본환경설정', G5_ADMIN_URL . '/config_form.php', 'cf_basic'),
array('100200', '관리권한설정', G5_ADMIN_URL . '/auth_list.php', 'cf_auth'),
array('100280', '테마설정', G5_ADMIN_URL . '/theme.php', 'cf_theme', 1),
array('100290', '메뉴설정', G5_ADMIN_URL . '/menu_list.php', 'cf_menu', 1),
array('100300', '메일 테스트', G5_ADMIN_URL . '/sendmail_test.php', 'cf_mailtest'),
array('100310', '팝업레이어관리', G5_ADMIN_URL . '/newwinlist.php', 'scf_poplayer'),
array('100800', '세션파일 일괄삭제', G5_ADMIN_URL . '/session_file_delete.php', 'cf_session', 1),
array('100900', '캐시파일 일괄삭제', G5_ADMIN_URL . '/cache_file_delete.php', 'cf_cache', 1),
array('100910', '캡챠파일 일괄삭제', G5_ADMIN_URL . '/captcha_file_delete.php', 'cf_captcha', 1),
array('100920', '썸네일파일 일괄삭제', G5_ADMIN_URL . '/thumbnail_file_delete.php', 'cf_thumbnail', 1),
array('100500', 'phpinfo()', G5_ADMIN_URL . '/phpinfo.php', 'cf_phpinfo')
);
if (version_compare(phpversion(), '5.3.0', '>=') && defined('G5_BROWSCAP_USE') && G5_BROWSCAP_USE) {
$menu['menu100'][] = array('100510', 'Browscap 업데이트', G5_ADMIN_URL . '/browscap.php', 'cf_browscap');
$menu['menu100'][] = array('100520', '접속로그 변환', G5_ADMIN_URL . '/browscap_convert.php', 'cf_visit_cnvrt');
}
$menu['menu100'][] = array('100410', 'DB업그레이드', G5_ADMIN_URL . '/dbupgrade.php', 'db_upgrade');
$menu['menu100'][] = array('100400', '부가서비스', G5_ADMIN_URL . '/service.php', 'cf_service');
$menu['menu100'][] = array('100420', '모듈 설치', G5_ADMIN_URL . '/reinstall_update/install.php', 'module_install');
+15
View File
@@ -0,0 +1,15 @@
<?php
$menu['menu200'] = array(
array('200000', '회원관리', G5_ADMIN_URL . '/member_list.php', 'member'),
array('200100', '회원관리', G5_ADMIN_URL . '/member_list.php', 'mb_list'),
array('200300', '회원메일발송', G5_ADMIN_URL . '/mail_list.php', 'mb_mail'),
array('200800', '접속자집계', G5_ADMIN_URL . '/visit_list.php', 'mb_visit', 1),
array('200810', '접속자검색', G5_ADMIN_URL . '/visit_search.php', 'mb_search', 1),
array('200820', '접속자로그삭제', G5_ADMIN_URL . '/visit_delete.php', 'mb_delete', 1),
array('200200', '포인트관리', G5_ADMIN_URL . '/point_list.php', 'mb_point'),
array('200900', '투표관리', G5_ADMIN_URL . '/poll_list.php', 'mb_poll'),
// 💡 [추가] 휴지통 메뉴
array('200950', '휴지통', G5_ADMIN_URL . '/trash_list.php', 'mb_trash'),
);
?>
+12
View File
@@ -0,0 +1,12 @@
<?php
$menu['menu300'] = array(
array('300000', '게시판관리', '' . G5_ADMIN_URL . '/board_list.php', 'board'),
array('300100', '게시판관리', '' . G5_ADMIN_URL . '/board_list.php', 'bbs_board'),
array('300200', '게시판그룹관리', '' . G5_ADMIN_URL . '/boardgroup_list.php', 'bbs_group'),
array('300300', '인기검색어관리', '' . G5_ADMIN_URL . '/popular_list.php', 'bbs_poplist', 1),
array('300400', '인기검색어순위', '' . G5_ADMIN_URL . '/popular_rank.php', 'bbs_poprank', 1),
array('300500', '1:1문의설정', '' . G5_ADMIN_URL . '/qa_config.php', 'qa'),
array('300600', '내용관리', G5_ADMIN_URL . '/contentlist.php', 'scf_contents', 1),
array('300700', 'FAQ관리', G5_ADMIN_URL . '/faqmasterlist.php', 'scf_faq', 1),
array('300820', '글,댓글 현황', G5_ADMIN_URL . '/write_count.php', 'scf_write_count'),
);
+23
View File
@@ -0,0 +1,23 @@
<?php
if (!defined('G5_USE_SHOP') || !G5_USE_SHOP) {
return;
}
$menu['menu400'] = array(
array('400000', '쇼핑몰관리', G5_ADMIN_URL . '/shop_admin/', 'shop_config'),
array('400010', '쇼핑몰현황', G5_ADMIN_URL . '/shop_admin/', 'shop_index'),
array('400100', '쇼핑몰설정', G5_ADMIN_URL . '/shop_admin/configform.php', 'scf_config'),
array('400400', '주문내역', G5_ADMIN_URL . '/shop_admin/orderlist.php', 'scf_order', 1),
array('400440', '개인결제관리', G5_ADMIN_URL . '/shop_admin/personalpaylist.php', 'scf_personalpay', 1),
array('400200', '분류관리', G5_ADMIN_URL . '/shop_admin/categorylist.php', 'scf_cate'),
array('400300', '상품관리', G5_ADMIN_URL . '/shop_admin/itemlist.php', 'scf_item'),
array('400660', '상품문의', G5_ADMIN_URL . '/shop_admin/itemqalist.php', 'scf_item_qna'),
array('400650', '사용후기', G5_ADMIN_URL . '/shop_admin/itemuselist.php', 'scf_ps'),
array('400620', '상품재고관리', G5_ADMIN_URL . '/shop_admin/itemstocklist.php', 'scf_item_stock'),
array('400610', '상품유형관리', G5_ADMIN_URL . '/shop_admin/itemtypelist.php', 'scf_item_type'),
array('400500', '상품옵션재고관리', G5_ADMIN_URL . '/shop_admin/optionstocklist.php', 'scf_item_option'),
array('400800', '쿠폰관리', G5_ADMIN_URL . '/shop_admin/couponlist.php', 'scf_coupon'),
array('400810', '쿠폰존관리', G5_ADMIN_URL . '/shop_admin/couponzonelist.php', 'scf_coupon_zone'),
array('400750', '추가배송비관리', G5_ADMIN_URL . '/shop_admin/sendcostlist.php', 'scf_sendcost', 1),
array('400410', '미완료주문', G5_ADMIN_URL . '/shop_admin/inorderlist.php', 'scf_inorder', 1),
);
+17
View File
@@ -0,0 +1,17 @@
<?php
if (!defined('G5_USE_SHOP') || !G5_USE_SHOP) {
return;
}
$menu['menu500'] = array(
array('500000', '쇼핑몰현황/기타', G5_ADMIN_URL . '/shop_admin/itemsellrank.php', 'shop_stats'),
array('500110', '매출현황', G5_ADMIN_URL . '/shop_admin/sale1.php', 'sst_order_stats'),
array('500100', '상품판매순위', G5_ADMIN_URL . '/shop_admin/itemsellrank.php', 'sst_rank'),
array('500120', '주문내역출력', G5_ADMIN_URL . '/shop_admin/orderprint.php', 'sst_print_order', 1),
array('500400', '재입고SMS알림', G5_ADMIN_URL . '/shop_admin/itemstocksms.php', 'sst_stock_sms', 1),
array('500300', '이벤트관리', G5_ADMIN_URL . '/shop_admin/itemevent.php', 'scf_event'),
array('500310', '이벤트일괄처리', G5_ADMIN_URL . '/shop_admin/itemeventlist.php', 'scf_event_mng'),
array('500500', '배너관리', G5_ADMIN_URL . '/shop_admin/bannerlist.php', 'scf_banner', 1),
array('500140', '보관함현황', G5_ADMIN_URL . '/shop_admin/wishlist.php', 'sst_wish'),
array('500210', '가격비교사이트', G5_ADMIN_URL . '/shop_admin/price.php', 'sst_compare', 1)
);
+11
View File
@@ -0,0 +1,11 @@
<?php
if (!defined('_GNUBOARD_')) exit;
// 💡 [수정] 600번대 최상위 메뉴 배열에 아이콘 클래스('fa-envelope')를 추가합니다.
$menu['menu600'][] = array('600000', '메일 관리', G5_ADMIN_URL.'/mail_manage/smtp_config.php', 'mail_manager', 'fa-envelope');
// '메일 관리'의 하위 메뉴들을 정의합니다.
$menu['menu600'][] = array('600100', 'SMTP 설정', G5_ADMIN_URL.'/mail_manage/smtp_config.php', 'mail_smtp_config');
$menu['menu600'][] = array('600200', '메일 템플릿 관리', G5_ADMIN_URL.'/mail_manage/template.php', 'mail_template');
$menu['menu600'][] = array('600300', '메일 발송 이력', G5_ADMIN_URL.'/mail_manage/send_log.php', 'mail_send_log');
$menu['menu600'][] = array('600900', '솔루션 설치', G5_ADMIN_URL.'/mail_manage/install.php', 'mail_solution_install');
+19
View File
@@ -0,0 +1,19 @@
<?php
if (!defined('_GNUBOARD_'))
exit;
// 800번대 최상위 메뉴 '견적 관리'를 정의합니다.
$menu['menu800'][] = array('800000', '견적 관리', G5_ADMIN_URL . '/order_manage/statistics.php', 'order_manage', 'fa-file-text-o');
// '견적 관리'의 하위 메뉴들을 정의합니다.
$menu['menu800'][] = array('800100', '견적 목록', G5_BBS_URL . '/board.php?bo_table=order', 'order_list');
$menu['menu800'][] = array('800200', '견적 통계', G5_ADMIN_URL . '/order_manage/statistics.php', 'order_statistics');
$menu['menu800'][] = array('800300', '시스템 설정', G5_ADMIN_URL . '/order_manage/config_manager.php', 'order_config');
$menu['menu800'][] = array('800400', '전문가 방문', G5_ADMIN_URL . '/order_manage/expert_visits.php', 'expert_visits');
$menu['menu800'][] = array('800500', '메일 템플릿', G5_ADMIN_URL . '/order_manage/mail_templates.php', 'mail_templates');
$menu['menu800'][] = array('800600', 'SMS 템플릿', G5_ADMIN_URL . '/order_manage/sms_templates.php', 'sms_templates');
$menu['menu800'][] = array('800700', '알림 로그 관리', G5_ADMIN_URL.'/order_manage/notification_log.php', 'notification_log');
$menu['menu800'][] = array('800800', '전문가 방문 예약', G5_ADMIN_URL.'/order_manage/expert_visit_reservations.php', 'expert_visit_reservations');
$menu['menu800'][] = array('800850', '전문가 방문 스케줄', G5_ADMIN_URL.'/order_manage/expert_visit_schedule.php', 'expert_visit_schedule');
$menu['menu800'][] = array('800860', '윈도우 창호 관리', G5_ADMIN_URL.'/order_manage/brand_manager.php', 'brand_manager');
$menu['menu800'][] = array('800900', '솔루션 설치', G5_ADMIN_URL . '/order_manage/install.php', 'order_solution_install');
+10
View File
@@ -0,0 +1,10 @@
<?php
if (!defined('_GNUBOARD_')) exit;
// Contact Inquiry 메뉴
if (file_exists(__DIR__ . '/contact_inquiry/admin.menu810.contact.php')) {
include_once(__DIR__ . '/contact_inquiry/admin.menu810.contact.php');
} else if (file_exists(__DIR__ . '/admin.menu810.contact.php')) {
include_once(__DIR__ . '/admin.menu810.contact.php');
}
?>
+30
View File
@@ -0,0 +1,30 @@
<?php
/**
* 상담 예약 관리 시스템 메뉴
*/
if (!defined('_GNUBOARD_'))
exit;
// 메뉴 구조: array('메뉴코드', '메뉴명', '링크', '메뉴 ID', '아이콘 클래스 (옵션)')
// 💡 [개선] 다른 모듈과의 호환성을 위해 배열을 덮어쓰지 않고 추가/병합합니다.
if (!isset($menu['menu850'])) $menu['menu850'] = array();
$menu['menu850'] = array_merge($menu['menu850'], array(
// 💡 [개선] 메뉴 그룹 대표 아이콘 추가 (예: fa-comments)
array('850000', '상담관리', G5_ADMIN_URL . '/consultant_manage/dashboard.php', 'consultant_manage', 'fa-comments'),
array('850100', '대시보드', G5_ADMIN_URL . '/consultant_manage/dashboard.php', 'consultant_dashboard'),
array('850200', '예약 현황', G5_ADMIN_URL . '/consultant_manage/reservations.php', 'consultant_reservations'),
array('850300', '빠른 스케줄 관리', G5_ADMIN_URL . '/consultant_manage/schedule_generate.php', 'consultant_schedule_quick'),
array('850400', '통계 분석', G5_ADMIN_URL . '/consultant_manage/statistics.php', 'consultant_statistics'),
// 💡 [개선] 설정 관련 메뉴를 '환경설정' 그룹으로 통합
array('850500', '팝업 샘플', G5_ADMIN_URL . '/consultant_manage/sample_page.php', 'consultant_sample'),
array('850600', '환경설정', G5_ADMIN_URL . '/consultant_manage/settings.php', 'consultant_settings_group', 'fa-cogs'),
array('850610', '기본/운영 설정', G5_ADMIN_URL . '/consultant_manage/settings.php', 'consultant_settings'),
array('850615', '리소스(상담사) 관리', G5_ADMIN_URL . '/consultant_manage/resources.php', 'consultant_resources'), // 💡 [추가] 리소스 관리 메뉴
array('850620', '알림 템플릿', G5_ADMIN_URL . '/consultant_manage/templates.php', 'consultant_templates'),
array('850630', '시스템 로그', G5_ADMIN_URL . '/consultant_manage/log_view.php', 'consultant_log_view'),
array('850640', '설치/업데이트', G5_ADMIN_URL . '/consultant_manage/install.php', 'consultant_install'),
// array('850900', '시스템 테스트', G5_ADMIN_URL . '/consultant_manage/test_system.php', 'consultant_test') // 개발용 메뉴는 주석 처리
));
?>
+13
View File
@@ -0,0 +1,13 @@
<?php
if (!defined('_GNUBOARD_')) exit;
// Consultant Manage 메뉴
if (file_exists(__DIR__ . '/admin.menu850.consultant_manage.php')) {
include_once(__DIR__ . '/admin.menu850.consultant_manage.php');
}
// Contact Inquiry 메뉴
if (file_exists(__DIR__ . '/admin.menu850.contact.php')) {
include_once(__DIR__ . '/admin.menu850.contact.php');
}
?>
+14
View File
@@ -0,0 +1,14 @@
<?php
$menu["menu900"] = array(
array('900000', 'SMS 관리', '' . G5_SMS5_ADMIN_URL . '/config.php', 'sms5'),
array('900100', 'SMS 기본설정', '' . G5_SMS5_ADMIN_URL . '/config.php', 'sms5_config'),
array('900200', '회원정보업데이트', '' . G5_SMS5_ADMIN_URL . '/member_update.php', 'sms5_mb_update'),
array('900300', '문자 보내기', '' . G5_SMS5_ADMIN_URL . '/sms_write.php', 'sms_write'),
array('900400', '전송내역-건별', '' . G5_SMS5_ADMIN_URL . '/history_list.php', 'sms_history', 1),
array('900410', '전송내역-번호별', '' . G5_SMS5_ADMIN_URL . '/history_num.php', 'sms_history_num', 1),
array('900500', '이모티콘 그룹', '' . G5_SMS5_ADMIN_URL . '/form_group.php', 'emoticon_group'),
array('900600', '이모티콘 관리', '' . G5_SMS5_ADMIN_URL . '/form_list.php', 'emoticon_list'),
array('900700', '휴대폰번호 그룹', '' . G5_SMS5_ADMIN_URL . '/num_group.php', 'hp_group', 1),
array('900800', '휴대폰번호 관리', '' . G5_SMS5_ADMIN_URL . '/num_book.php', 'hp_manage', 1),
array('900900', '휴대폰번호 파일', '' . G5_SMS5_ADMIN_URL . '/num_book_file.php', 'hp_file', 1)
);
+157
View File
@@ -0,0 +1,157 @@
<?php
if (!defined('_GNUBOARD_')) {
exit;
}
// 그누보드5.4.5.5 버전과 영카트5.4.5.5.1 버전이 통합됨에 따라 그누보드 버전만 표시
// $print_version = defined('G5_YOUNGCART_VER') ? 'YoungCart Version '.G5_YOUNGCART_VER : 'Version '.G5_GNUBOARD_VER;
$print_version = ($is_admin == 'super') ? 'Version ' . G5_GNUBOARD_VER : '';
?>
<noscript>
<p>
귀하께서 사용하시는 브라우저는 현재 <strong>자바스크립트를 사용하지 않음</strong>으로 설정되어 있습니다.<br>
<strong>자바스크립트를 사용하지 않음</strong>으로 설정하신 경우는 수정이나 삭제시 별도의 경고창이 나오지 않으므로 이점 주의하시기 바랍니다.
</p>
</noscript>
</div>
<footer id="ft">
<p>
Copyright &copy; <?php echo $_SERVER['HTTP_HOST']; ?>. All rights reserved. <?php echo $print_version; ?><br>
<button type="button" class="scroll_top"><span class="top_img"></span><span class="top_txt">TOP</span></button>
</p>
</footer>
</div>
</div>
<script>
$(".scroll_top").click(function() {
$("body,html").animate({
scrollTop: 0
}, 400);
})
</script>
<!-- <p>실행시간 : <?php echo get_microtime() - $begin_time; ?> -->
<script src="<?php echo G5_ADMIN_URL ?>/admin.js?ver=<?php echo G5_JS_VER; ?>"></script>
<script src="<?php echo G5_JS_URL ?>/jquery.anchorScroll.js?ver=<?php echo G5_JS_VER; ?>"></script>
<script>
$(function() {
var admin_head_height = $("#hd_top").height() + $("#container_title").height() + 5;
$("a[href^='#']").anchorScroll({
scrollSpeed: 0, // scroll speed
offsetTop: admin_head_height, // offset for fixed top bars (defaults to 0)
onScroll: function() {
// callback on scroll start
},
scrollEnd: function() {
// callback on scroll end
}
});
var hide_menu = false;
var mouse_event = false;
var oldX = oldY = 0;
$(document).mousemove(function(e) {
if (oldX == 0) {
oldX = e.pageX;
oldY = e.pageY;
}
if (oldX != e.pageX || oldY != e.pageY) {
mouse_event = true;
}
});
// 주메뉴
var $gnb = $(".gnb_1dli > a");
$gnb.mouseover(function() {
if (mouse_event) {
$(".gnb_1dli").removeClass("gnb_1dli_over gnb_1dli_over2 gnb_1dli_on");
$(this).parent().addClass("gnb_1dli_over gnb_1dli_on");
menu_rearrange($(this).parent());
hide_menu = false;
}
});
$gnb.mouseout(function() {
hide_menu = true;
});
$(".gnb_2dli").mouseover(function() {
hide_menu = false;
});
$(".gnb_2dli").mouseout(function() {
hide_menu = true;
});
$gnb.focusin(function() {
$(".gnb_1dli").removeClass("gnb_1dli_over gnb_1dli_over2 gnb_1dli_on");
$(this).parent().addClass("gnb_1dli_over gnb_1dli_on");
menu_rearrange($(this).parent());
hide_menu = false;
});
$gnb.focusout(function() {
hide_menu = true;
});
$(".gnb_2da").focusin(function() {
$(".gnb_1dli").removeClass("gnb_1dli_over gnb_1dli_over2 gnb_1dli_on");
var $gnb_li = $(this).closest(".gnb_1dli").addClass("gnb_1dli_over gnb_1dli_on");
menu_rearrange($(this).closest(".gnb_1dli"));
hide_menu = false;
});
$(".gnb_2da").focusout(function() {
hide_menu = true;
});
$('#gnb_1dul>li').bind('mouseleave', function() {
submenu_hide();
});
$(document).bind('click focusin', function() {
if (hide_menu) {
submenu_hide();
}
});
// 폰트 리사이즈 쿠키있으면 실행
var font_resize_act = get_cookie("ck_font_resize_act");
if (font_resize_act != "") {
font_resize("container", font_resize_act);
}
});
function submenu_hide() {
$(".gnb_1dli").removeClass("gnb_1dli_over gnb_1dli_over2 gnb_1dli_on");
}
function menu_rearrange(el) {
var width = $("#gnb_1dul").width();
var left = w1 = w2 = 0;
var idx = $(".gnb_1dli").index(el);
for (i = 0; i <= idx; i++) {
w1 = $(".gnb_1dli:eq(" + i + ")").outerWidth();
w2 = $(".gnb_2dli > a:eq(" + i + ")").outerWidth(true);
if ((left + w2) > width) {
el.removeClass("gnb_1dli_over").addClass("gnb_1dli_over2");
}
left += w1;
}
}
</script>
<?php
require_once G5_PATH . '/tail.sub.php';
+19
View File
@@ -0,0 +1,19 @@
<?php
require_once './_common.php';
set_session('ss_admin_token', '');
$admin_csrf_token_key = isset($_POST['admin_csrf_token_key']) ? $_POST['admin_csrf_token_key'] : '';
if(function_exists('admin_csrf_token_key') && $admin_csrf_token_key !== admin_csrf_token_key(1)){
die(json_encode(array('error' => '토큰키 에러!', 'url' => G5_URL)));
}
$error = admin_referer_check(true);
if ($error) {
die(json_encode(array('error' => $error, 'url' => G5_URL)));
}
$token = get_admin_token();
die(json_encode(array('error' => '', 'token' => $token, 'url' => '')));
+6
View File
@@ -0,0 +1,6 @@
<?php
require_once './_common.php';
if (isset($_POST['admin_use_captcha'])) {
set_session('ss_admin_use_captcha', true);
}
+272
View File
@@ -0,0 +1,272 @@
<?php
$sub_menu = "100200";
require_once './_common.php';
if ($is_admin != 'super') {
alert('최고관리자만 접근 가능합니다.');
}
$sql_common = " from {$g5['auth_table']} a left join {$g5['member_table']} b on (a.mb_id=b.mb_id) ";
$sql_search = " where (1) ";
if ($stx) {
$sql_search .= " and ( ";
switch ($sfl) {
default:
$sql_search .= " ({$sfl} like '%{$stx}%') ";
break;
}
$sql_search .= " ) ";
}
if (!$sst) {
$sst = "a.mb_id, au_menu";
$sod = "";
}
$sql_order = " order by $sst $sod ";
$sql = " select count(*) as cnt
{$sql_common}
{$sql_search}
{$sql_order} ";
$row = sql_fetch($sql);
$total_count = $row['cnt'];
$rows = $config['cf_page_rows'];
$total_page = ceil($total_count / $rows); // 전체 페이지 계산
if ($page < 1) {
$page = 1; // 페이지가 없으면 첫 페이지 (1 페이지)
}
$from_record = ($page - 1) * $rows; // 시작 열을 구함
$sql = " select *
{$sql_common}
{$sql_search}
{$sql_order}
limit {$from_record}, {$rows} ";
$result = sql_query($sql);
$listall = '<a href="' . $_SERVER['SCRIPT_NAME'] . '" class="ov_listall btn_ov02">전체목록</a>';
$g5['title'] = "관리권한설정";
require_once './admin.head.php';
$colspan = 5;
?>
<div class="local_ov01 local_ov">
<?php echo $listall ?>
<span class="btn_ov01"><span class="ov_txt">설정된 관리권한</span><span class="ov_num"><?php echo number_format($total_count) ?>건</span></span>
</div>
<form name="fsearch" id="fsearch" class="local_sch01 local_sch" method="get">
<input type="hidden" name="sfl" value="a.mb_id" id="sfl">
<label for="stx" class="sound_only">회원아이디<strong class="sound_only"> 필수</strong></label>
<input type="text" name="stx" value="<?php echo $stx ?>" id="stx" required class="required frm_input">
<input type="submit" value="검색" id="fsearch_submit" class="btn_submit">
</form>
<form name="fauthlist" id="fauthlist" method="post" action="./auth_list_delete.php" onsubmit="return fauthlist_submit(this);">
<input type="hidden" name="sst" value="<?php echo $sst ?>">
<input type="hidden" name="sod" value="<?php echo $sod ?>">
<input type="hidden" name="sfl" value="<?php echo $sfl ?>">
<input type="hidden" name="stx" value="<?php echo $stx ?>">
<input type="hidden" name="page" value="<?php echo $page ?>">
<input type="hidden" name="token" value="">
<div class="tbl_head01 tbl_wrap">
<table>
<caption><?php echo $g5['title']; ?> 목록</caption>
<thead>
<tr>
<th scope="col">
<label for="chkall" class="sound_only">현재 페이지 회원 전체</label>
<input type="checkbox" name="chkall" value="1" id="chkall" onclick="check_all(this.form)">
</th>
<th scope="col"><?php echo subject_sort_link('a.mb_id') ?>회원아이디</a></th>
<th scope="col"><?php echo subject_sort_link('mb_nick') ?>닉네임</a></th>
<th scope="col">메뉴</th>
<th scope="col">권한</th>
</tr>
</thead>
<tbody>
<?php
$count = 0;
for ($i = 0; $row = sql_fetch_array($result); $i++) {
$is_continue = false;
// 회원아이디가 없는 메뉴는 삭제함
if ($row['mb_id'] == '' && $row['mb_nick'] == '') {
sql_query(" delete from {$g5['auth_table']} where au_menu = '{$row['au_menu']}' ");
$is_continue = true;
}
// 메뉴번호가 바뀌는 경우에 현재 없는 저장된 메뉴는 삭제함
if (!isset($auth_menu[$row['au_menu']])) {
sql_query(" delete from {$g5['auth_table']} where au_menu = '{$row['au_menu']}' ");
$is_continue = true;
}
if ($is_continue) {
continue;
}
$mb_nick = get_sideview($row['mb_id'], $row['mb_nick'], $row['mb_email'], $row['mb_homepage']);
$bg = 'bg' . ($i % 2);
?>
<tr class="<?php echo $bg; ?>">
<td class="td_chk">
<input type="hidden" name="au_menu[<?php echo $i ?>]" value="<?php echo $row['au_menu'] ?>">
<input type="hidden" name="mb_id[<?php echo $i ?>]" value="<?php echo $row['mb_id'] ?>">
<label for="chk_<?php echo $i; ?>" class="sound_only"><?php echo $row['mb_nick'] ?>님 권한</label>
<input type="checkbox" name="chk[]" value="<?php echo $i ?>" id="chk_<?php echo $i ?>">
</td>
<td class="td_mbid"><a href="?sfl=a.mb_id&amp;stx=<?php echo $row['mb_id'] ?>"><?php echo $row['mb_id'] ?></a></td>
<td class="td_auth_mbnick"><?php echo $mb_nick ?></td>
<td class="td_menu">
<?php echo $row['au_menu'] ?>
<?php echo $auth_menu[$row['au_menu']] ?>
</td>
<td class="td_auth"><?php echo $row['au_auth'] ?></td>
</tr>
<?php
$count++;
}
if ($count == 0) {
echo '<tr><td colspan="' . $colspan . '" class="empty_table">자료가 없습니다.</td></tr>';
}
?>
</tbody>
</table>
</div>
<div class="btn_list01 btn_list">
<input type="submit" name="act_button" value="선택삭제" onclick="document.pressed=this.value" class="btn btn_02">
</div>
<?php
//if (isset($stx))
// echo '<script>document.fsearch.sfl.value = "'.$sfl.'";</script>'."\n";
if (strstr($sfl, 'mb_id')) {
$mb_id = $stx;
} else {
$mb_id = '';
}
?>
</form>
<?php
$pagelist = get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, $_SERVER['SCRIPT_NAME'] . '?' . $qstr . '&amp;page=');
echo $pagelist;
?>
<form name="fauthlist2" id="fauthlist2" action="./auth_update.php" method="post" autocomplete="off" onsubmit="return fauth_add_submit(this);">
<input type="hidden" name="sfl" value="<?php echo $sfl ?>">
<input type="hidden" name="stx" value="<?php echo $stx ?>">
<input type="hidden" name="sst" value="<?php echo $sst ?>">
<input type="hidden" name="sod" value="<?php echo $sod ?>">
<input type="hidden" name="page" value="<?php echo $page ?>">
<input type="hidden" name="token" value="">
<section id="add_admin">
<h2 class="h2_frm">관리권한 추가</h2>
<div class="local_desc01 local_desc">
<p>
다음 양식에서 회원에게 관리권한을 부여하실 수 있습니다.<br>
권한 <strong>r</strong>은 읽기권한, <strong>w</strong>는 쓰기권한, <strong>d</strong>는 삭제권한입니다.
</p>
</div>
<div class="tbl_frm01 tbl_wrap">
<table>
<colgroup>
<col class="grid_4">
<col>
</colgroup>
<tbody>
<tr>
<th scope="row"><label for="mb_id">회원아이디<strong class="sound_only">필수</strong></label></th>
<td>
<strong id="msg_mb_id" class="msg_sound_only"></strong>
<input type="text" name="mb_id" value="<?php echo $mb_id ?>" id="mb_id" required class="required frm_input">
</td>
</tr>
<tr>
<th scope="row"><label for="au_menu">접근가능메뉴<strong class="sound_only">필수</strong></label></th>
<td>
<select id="au_menu" name="au_menu" required class="required">
<option value=''>선택하세요</option>
<?php
foreach ($auth_menu as $key => $value) {
if (!(substr($key, -3) == '000' || $key == '-' || !$key)) {
echo '<option value="' . $key . '">' . $key . ' ' . $value . '</option>';
}
}
?>
</select>
</td>
</tr>
<tr>
<th scope="row">권한지정</th>
<td>
<input type="checkbox" name="r" value="r" id="r" checked>
<label for="r">r (읽기)</label>
<input type="checkbox" name="w" value="w" id="w">
<label for="w">w (쓰기)</label>
<input type="checkbox" name="d" value="d" id="d">
<label for="d">d (삭제)</label>
</td>
</tr>
<tr>
<th scope="row">자동등록방지</th>
<td>
<?php
require_once G5_CAPTCHA_PATH . '/captcha.lib.php';
$captcha_html = captcha_html();
$captcha_js = chk_captcha_js();
echo $captcha_html;
?>
</td>
</tr>
</tbody>
</table>
</div>
<div class="btn_confirm01 btn_confirm">
<input type="submit" value="추가" class="btn_submit btn">
</div>
</section>
</form>
<script>
function fauth_add_submit(f) {
<?php echo $captcha_js; // 캡챠 사용시 자바스크립트에서 입력된 캡챠를 검사함 ?>
return true;
}
function fauthlist_submit(f) {
if (!is_checked("chk[]")) {
alert(document.pressed + " 하실 항목을 하나 이상 선택하세요.");
return false;
}
if (document.pressed == "선택삭제") {
if (!confirm("선택한 자료를 정말 삭제하시겠습니까?")) {
return false;
}
}
return true;
}
</script>
<?php
require_once './admin.tail.php';
+37
View File
@@ -0,0 +1,37 @@
<?php
$sub_menu = "100200";
require_once './_common.php';
check_demo();
if ($is_admin != 'super') {
alert('최고관리자만 접근 가능합니다.');
}
check_admin_token();
$count = (isset($_POST['chk']) && is_array($_POST['chk'])) ? count($_POST['chk']) : 0;
$post_act_button = isset($_POST['act_button']) ? clean_xss_tags($_POST['act_button'], 1, 1) : '';
if (!$count) {
alert($_POST['act_button'] . " 하실 항목을 하나 이상 체크하세요.");
}
if ((isset($_POST['mb_id']) && !is_array($_POST['mb_id'])) || (isset($_POST['au_menu']) && !is_array($_POST['au_menu']))) {
alert("잘못된 요청입니다.");
}
for ($i = 0; $i < $count; $i++) {
// 실제 번호를 넘김
$k = isset($_POST['chk'][$i]) ? (int) $_POST['chk'][$i] : 0;
$mb_id = isset($_POST['mb_id'][$k]) ? preg_replace('/[^a-zA-Z0-9_]/', '', $_POST['mb_id'][$k]) : '';
$au_menu = isset($_POST['au_menu'][$k]) ? preg_replace('/[^a-zA-Z0-9_]/', '', $_POST['au_menu'][$k]) : '';
$sql = " delete from {$g5['auth_table']} where mb_id = '" . $mb_id . "' and au_menu = '" . $au_menu . "' ";
sql_query($sql);
run_event('adm_auth_delete_member', $mb_id, $au_menu);
}
goto_url('./auth_list.php?' . $qstr);
+55
View File
@@ -0,0 +1,55 @@
<?php
$sub_menu = "100200";
require_once './_common.php';
require_once G5_LIB_PATH . '/mailer.lib.php';
$au_menu = isset($_POST['au_menu']) ? preg_replace('/[^0-9a-z_]/i', '', $_POST['au_menu']) : '';
$post_r = isset($_POST['r']) ? preg_replace('/[^0-9a-z_]/i', '', $_POST['r']) : '';
$post_w = isset($_POST['w']) ? preg_replace('/[^0-9a-z_]/i', '', $_POST['w']) : '';
$post_d = isset($_POST['d']) ? preg_replace('/[^0-9a-z_]/i', '', $_POST['d']) : '';
if ($is_admin != 'super') {
alert('최고관리자만 접근 가능합니다.');
}
$mb = get_member($mb_id);
if (!$mb['mb_id']) {
alert('존재하는 회원아이디가 아닙니다.');
}
check_admin_token();
require_once G5_CAPTCHA_PATH . '/captcha.lib.php';
if (!chk_captcha()) {
alert('자동등록방지 숫자가 틀렸습니다.');
}
$sql = " insert into {$g5['auth_table']}
set mb_id = '$mb_id',
au_menu = '$au_menu',
au_auth = '{$post_r},{$post_w},{$post_d}' ";
$result = sql_query($sql, false);
if (!$result) {
$sql = " update {$g5['auth_table']}
set au_auth = '{$post_r},{$post_w},{$post_d}'
where mb_id = '$mb_id'
and au_menu = '$au_menu' ";
sql_query($sql);
}
//sql_query(" OPTIMIZE TABLE `$g5['auth_table']` ");
// 세션을 체크하여 하루에 한번만 메일알림이 가게 합니다.
if (str_replace('-', '', G5_TIME_YMD) !== get_session('adm_auth_update')) {
$site_url = preg_replace('/^www\./', '', strtolower($_SERVER['SERVER_NAME']));
$to_email = 'gnuboard@' . $site_url;
mailer($config['cf_admin_email_name'], $to_email, $config['cf_admin_email'], '[' . $config['cf_title'] . '] 관리권한설정 알림', '<p><b>[' . $config['cf_title'] . '] 관리권한설정 변경 안내</b></p><p style="padding-top:1em">회원 아이디 ' . $mb['mb_id'] . ' 에 관리권한이 추가 되었습니다.</p><p style="padding-top:1em">' . G5_TIME_YMDHIS . '</p><p style="padding-top:1em"><a href="' . G5_URL . '" target="_blank">' . $config['cf_title'] . '</a></p>', 1);
set_session('adm_auth_update', str_replace('-', '', G5_TIME_YMD));
}
run_event('adm_auth_update', $mb);
goto_url('./auth_list.php?' . $qstr);
+56
View File
@@ -0,0 +1,56 @@
<?php
$sub_menu = '000300';
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "w");
check_admin_token();
$bn_id = isset($_POST['bn_id']) ? (int)$_POST['bn_id'] : 0;
$w = isset($_POST['w']) ? $_POST['w'] : '';
$sql_common = "
bn_alt = '{$_POST['bn_alt']}',
bn_url = '{$_POST['bn_url']}',
bn_device = '{$_POST['bn_device']}',
bn_position = '{$_POST['bn_position']}',
bn_group = '{$_POST['bn_group']}',
bn_border = '{$_POST['bn_border']}',
bn_radius = '{$_POST['bn_radius']}',
bn_ad_ico = '{$_POST['bn_ad_ico']}',
bn_new_win = '{$_POST['bn_new_win']}',
bn_begin_time = '{$_POST['bn_begin_time']}',
bn_end_time = '{$_POST['bn_end_time']}',
bn_order = '{$_POST['bn_order']}'
";
if ($w == "") {
$sql_common .= ", bn_time = '" . G5_TIME_YMDHIS . "' ";
$sql = " INSERT INTO `rb_banner` SET {$sql_common} ";
sql_query($sql);
$bn_id = sql_insert_id();
} else if ($w == "u") {
$sql = " UPDATE `rb_banner` SET {$sql_common} WHERE bn_id = '{$bn_id}' ";
sql_query($sql);
} else {
alert('제대로 된 값이 넘어오지 않았습니다.');
}
// 이미지 업로드
if (isset($_FILES['bn_bimg']) && $_FILES['bn_bimg']['name'] != '') {
$bimg_dir = G5_DATA_PATH . '/banners';
@mkdir($bimg_dir, G5_DIR_PERMISSION);
@chmod($bimg_dir, G5_DIR_PERMISSION);
$dest_path = $bimg_dir . '/' . $bn_id;
move_uploaded_file($_FILES['bn_bimg']['tmp_name'], $dest_path);
chmod($dest_path, G5_FILE_PERMISSION);
}
// 이미지 삭제
if (isset($_POST['bn_bimg_del']) && $_POST['bn_bimg_del']) {
@unlink(G5_DATA_PATH . '/banners/' . $bn_id);
}
goto_url('./banner_list.php');
?>
+113
View File
@@ -0,0 +1,113 @@
<?php
include_once('./_common.php');
if (!$is_admin) {
alert('관리자만 접근 가능합니다.');
}
// curl을 사용하여 이미지를 다운로드하는 함수
function download_image_with_curl($url, $save_path) {
$ch = curl_init($url);
$fp = fopen($save_path, 'wb');
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_HEADER, 0);
// Referer 설정 (필요시)
curl_setopt($ch, CURLOPT_REFERER, 'https://www.laser.or.kr/');
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$result = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
fclose($fp);
if ($result && $http_code == 200 && filesize($save_path) > 0) {
return true;
} else {
@unlink($save_path);
return false;
}
}
$banners = [
['url' => 'http://www.lg.co.kr/', 'img' => 'foot-logo1.png', 'alt' => 'LG'],
['url' => '#', 'img' => 'foot-logo2.png', 'alt' => 'Partner'],
['url' => 'http://www.swhitech.com/main/', 'img' => 'foot-logo3.png', 'alt' => 'SWHitech'],
['url' => 'http://www.hblaser.co.kr', 'img' => 'foot-logo4.png', 'alt' => 'HBLaser'],
['url' => 'http://www.lasersystem.co.kr/', 'img' => 'foot-logo5.png', 'alt' => 'LaserSystem'],
['url' => 'http://www.eotechnics.com/page/main/main.php', 'img' => 'foot-logo6.png', 'alt' => 'EO Technics'],
['url' => 'https://www.lpkf.com/de/', 'img' => 'foot-logo7.jpg', 'alt' => 'LPKF'],
['url' => 'https://amplitude-laser.com/', 'img' => 'foot-logo8.png', 'alt' => 'Amplitude'],
['url' => 'http://www.mutechkorea.co.kr/', 'img' => 'foot-logo9.png', 'alt' => 'Mutech'],
['url' => 'http://www.ainnotech.com/', 'img' => 'foot-logo10.png', 'alt' => 'Ainnotech'],
['url' => 'https://www.edmundoptics.co.kr/', 'img' => 'foot-logo11.png', 'alt' => 'Edmund Optics'],
['url' => 'https://www.coherent.com/', 'img' => 'foot-logo12_new.png', 'alt' => 'Coherent'],
['url' => 'https://www.mksinst.com/', 'img' => 'foot-logo13.png', 'alt' => 'MKS'],
['url' => 'http://daekhon.co.kr/', 'img' => 'foot-logo14.png', 'alt' => 'Daekhon'],
['url' => 'https://lightrun.co.kr/', 'img' => 'foot-logo15.png', 'alt' => 'Lightrun'],
['url' => 'https://www.uniotech.kr/', 'img' => 'foot-logo16.png', 'alt' => 'Uniotech'],
['url' => 'http://www.hls-scansonic.kr/main/index.html', 'img' => 'foot-logo17.png', 'alt' => 'HLS'],
['url' => 'http://coslaser.co.kr/', 'img' => 'Wooyang_Logo.png', 'alt' => 'Wooyang'],
['url' => 'https://www.excelitas.com/', 'img' => 'foot-logo18.png', 'alt' => 'Excelitas'],
['url' => 'http://www.evlaser.co.kr/', 'img' => 'foot-logo19.jpg', 'alt' => 'EV Laser'],
['url' => 'https://www.precitec.com/kr/', 'img' => 'foot-logo20.jpg', 'alt' => 'Precitec'],
['url' => 'https://www.amadaweldtech.co.kr/', 'img' => 'foot-logo21.png', 'alt' => 'Amada']
];
// 💡 [핵심 수정] 이미지 경로에 /KOR/ 추가
$base_img_url = 'https://www.laser.or.kr/KOR/images/comm/';
$save_path = G5_DATA_PATH . '/banners/';
if (!is_dir($save_path)) {
@mkdir($save_path, G5_DIR_PERMISSION);
@chmod($save_path, G5_DIR_PERMISSION);
}
echo "<h1>배너 자동 등록 시작 (경로 수정됨)</h1>";
echo "<ul>";
$success_count = 0;
foreach ($banners as $idx => $item) {
$row = sql_fetch(" SELECT bn_id FROM rb_banner WHERE bn_alt = '{$item['alt']}' AND bn_position = 'rolling_footer' ");
if ($row['bn_id']) {
$bn_id = $row['bn_id'];
echo "<li>[중복] ID: {$bn_id} - {$item['alt']} 이미 등록됨. 이미지 다운로드만 재시도합니다.</li>";
} else {
$sql = " INSERT INTO rb_banner
SET bn_alt = '{$item['alt']}',
bn_url = '{$item['url']}',
bn_device = 'both',
bn_position = 'rolling_footer',
bn_border = '0',
bn_new_win = '1',
bn_begin_time = '" . date('Y-m-d H:i:s') . "',
bn_end_time = '" . date('Y-m-d H:i:s', strtotime('+10 years')) . "',
bn_time = '" . date('Y-m-d H:i:s') . "',
bn_hit = 0,
bn_order = " . ($idx + 1);
sql_query($sql);
$bn_id = sql_insert_id();
}
if ($bn_id) {
$remote_img = $base_img_url . $item['img'];
$local_img = $save_path . $bn_id;
if (download_image_with_curl($remote_img, $local_img)) {
echo "<li>[성공] ID: {$bn_id} - {$item['alt']} 이미지 저장 완료</li>";
$success_count++;
} else {
echo "<li style='color:red;'>[실패] ID: {$bn_id} - 이미지 다운로드 실패 ({$remote_img})</li>";
}
}
}
echo "</ul>";
echo "<h2>총 {$success_count}개의 배너 이미지가 저장되었습니다.</h2>";
echo "<p><a href='" . G5_ADMIN_URL . "/rb/banner_list.php'>[배너 관리 페이지로 이동]</a></p>";
?>
+91
View File
@@ -0,0 +1,91 @@
<?php
$sub_menu = "300100";
require_once "./_common.php";
auth_check_menu($auth, $sub_menu, 'w');
$g5['title'] = '게시판 복사';
require_once G5_PATH . '/head.sub.php';
if (empty($bo_table)) {
alert_close("정상적인 방법으로 이용해주세요.");
}
?>
<script>
var g5_admin_csrf_token_key = "<?php echo (function_exists('admin_csrf_token_key')) ? admin_csrf_token_key() : ''; ?>";
</script>
<script src="<?php echo G5_ADMIN_URL ?>/admin.js?ver=<?php echo G5_JS_VER; ?>"></script>
<div class="new_win">
<h1><?php echo $g5['title']; ?></h1>
<form name="fboardcopy" id="fboardcopy" action="./board_copy_update.php" onsubmit="return fboardcopy_check(this);" method="post">
<input type="hidden" name="bo_table" value="<?php echo $bo_table ?>" id="bo_table">
<input type="hidden" name="token" value="">
<div class=" new_win_con">
<div class="tbl_frm01 tbl_wrap">
<table>
<caption><?php echo $g5['title']; ?></caption>
<tbody>
<tr>
<th scope="col">원본 테이블명</th>
<td><?php echo $bo_table ?></td>
</tr>
<tr>
<th scope="col"><label for="target_table">복사 테이블명<strong class="sound_only">필수</strong></label></th>
<td><input type="text" name="target_table" id="target_table" required class="required alnum_ frm_input" maxlength="20">영문자, 숫자, _ 만 가능 (공백없이)</td>
</tr>
<tr>
<th scope="col"><label for="target_subject">게시판 제목<strong class="sound_only">필수</strong></label></th>
<td><input type="text" name="target_subject" value="[복사본] <?php echo get_sanitize_input($board['bo_subject']); ?>" id="target_subject" required class="required frm_input" maxlength="120"></td>
</tr>
<tr>
<th scope="col">복사 유형</th>
<td>
<input type="radio" name="copy_case" value="schema_only" id="copy_case" checked>
<label for="copy_case">구조만</label>
<input type="radio" name="copy_case" value="schema_data_both" id="copy_case2">
<label for="copy_case2">구조와 데이터</label>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="win_btn ">
<input type="submit" class="btn_submit btn" value="복사">
<input type="button" class="btn_close btn" value="창닫기" onclick="window.close();">
</div>
</form>
</div>
<script>
function fboardcopy_check(f) {
<?php
if (!$w) {
$js_array = get_bo_table_banned_word();
echo "var banned_array = " . json_encode($js_array) . ";\n";
}
?>
// 게시판명이 금지된 단어로 되어 있으면
if ((typeof banned_array != 'undefined') && jQuery.inArray(f.target_table.value, banned_array) !== -1) {
alert("입력한 게시판 TABLE명을 사용할수 없습니다. 다른 이름으로 입력해 주세요.");
return false;
}
if (f.bo_table.value == f.target_table.value) {
alert("원본 테이블명과 복사할 테이블명이 달라야 합니다.");
return false;
}
return true;
}
</script>
<?php
require_once G5_PATH . '/tail.sub.php';
+233
View File
@@ -0,0 +1,233 @@
<?php
$sub_menu = '300100';
require_once './_common.php';
check_demo();
auth_check_menu($auth, $sub_menu, 'w');
check_admin_token();
$bo_table = isset($_POST['bo_table']) ? substr(preg_replace('/[^a-z0-9_]/i', '', $_POST['bo_table']), 0, 20) : null;
$target_table = isset($_POST['target_table']) ? trim($_POST['target_table']) : '';
$target_subject = isset($_POST['target_subject']) ? trim($_POST['target_subject']) : '';
$target_subject = strip_tags(clean_xss_attributes($target_subject));
$file_copy = array();
if (empty($bo_table)) {
alert("원본 테이블 정보가 없습니다.");
}
if (!preg_match('/[A-Za-z0-9_]{1,20}/', $target_table)) {
alert('게시판 TABLE명은 공백없이 영문자, 숫자, _ 만 사용 가능합니다. (20자 이내)');
}
$target_table = substr(preg_replace('/[^a-z0-9_]/i', '', $target_table), 0, 20);
// 게시판명이 금지된 단어로 되어 있으면
if ($w == '' && in_array($target_table, get_bo_table_banned_word())) {
alert('입력한 게시판 TABLE명을 사용할수 없습니다. 다른 이름으로 입력해 주세요.');
}
$row = sql_fetch(" select count(*) as cnt from {$g5['board_table']} where bo_table = '$target_table' ");
if ($row['cnt']) {
alert($target_table . '은(는) 이미 존재하는 게시판 테이블명 입니다.\\n복사할 테이블명으로 사용할 수 없습니다.');
}
// 게시판 테이블 생성
$sql = get_table_define($g5['write_prefix'] . $bo_table);
$sql = str_replace($g5['write_prefix'] . $bo_table, $g5['write_prefix'] . $target_table, $sql);
sql_query($sql, false);
// 구조만 복사시에는 공지사항 번호는 복사하지 않는다.
if ($copy_case == 'schema_only') {
$board['bo_notice'] = '';
}
// 게시판 정보
$sql = " insert into {$g5['board_table']}
set bo_table = '$target_table',
gr_id = '{$board['gr_id']}',
bo_subject = '$target_subject',
bo_device = '{$board['bo_device']}',
bo_admin = '{$board['bo_admin']}',
bo_list_level = '{$board['bo_list_level']}',
bo_read_level = '{$board['bo_read_level']}',
bo_write_level = '{$board['bo_write_level']}',
bo_reply_level = '{$board['bo_reply_level']}',
bo_comment_level = '{$board['bo_comment_level']}',
bo_upload_level = '{$board['bo_upload_level']}',
bo_download_level = '{$board['bo_download_level']}',
bo_html_level = '{$board['bo_html_level']}',
bo_link_level = '{$board['bo_link_level']}',
bo_count_modify = '{$board['bo_count_modify']}',
bo_count_delete = '{$board['bo_count_delete']}',
bo_read_point = '{$board['bo_read_point']}',
bo_write_point = '{$board['bo_write_point']}',
bo_comment_point = '{$board['bo_comment_point']}',
bo_download_point = '{$board['bo_download_point']}',
bo_use_category = '{$board['bo_use_category']}',
bo_category_list = '{$board['bo_category_list']}',
bo_use_sideview = '{$board['bo_use_sideview']}',
bo_use_file_content = '{$board['bo_use_file_content']}',
bo_use_secret = '{$board['bo_use_secret']}',
bo_use_dhtml_editor = '{$board['bo_use_dhtml_editor']}',
bo_use_rss_view = '{$board['bo_use_rss_view']}',
bo_use_good = '{$board['bo_use_good']}',
bo_use_nogood = '{$board['bo_use_nogood']}',
bo_use_name = '{$board['bo_use_name']}',
bo_use_signature = '{$board['bo_use_signature']}',
bo_use_ip_view = '{$board['bo_use_ip_view']}',
bo_use_list_view = '{$board['bo_use_list_view']}',
bo_use_list_content = '{$board['bo_use_list_content']}',
bo_use_list_file = '{$board['bo_use_list_file']}',
bo_table_width = '{$board['bo_table_width']}',
bo_subject_len = '{$board['bo_subject_len']}',
bo_mobile_subject_len = '{$board['bo_mobile_subject_len']}',
bo_page_rows = '{$board['bo_page_rows']}',
bo_mobile_page_rows = '{$board['bo_mobile_page_rows']}',
bo_new = '{$board['bo_new']}',
bo_hot = '{$board['bo_hot']}',
bo_image_width = '{$board['bo_image_width']}',
bo_skin = '" . sql_real_escape_string($board['bo_skin']). "',
bo_mobile_skin = '" . sql_real_escape_string($board['bo_mobile_skin']). "',
bo_include_head = '" . sql_real_escape_string($board['bo_include_head']). "',
bo_include_tail = '" . sql_real_escape_string($board['bo_include_tail']). "',
bo_content_head = '" . addslashes($board['bo_content_head']) . "',
bo_content_tail = '" . addslashes($board['bo_content_tail']) . "',
bo_mobile_content_head = '" . addslashes($board['bo_mobile_content_head']) . "',
bo_mobile_content_tail = '" . addslashes($board['bo_mobile_content_tail']) . "',
bo_insert_content = '" . addslashes($board['bo_insert_content']) . "',
bo_gallery_cols = '{$board['bo_gallery_cols']}',
bo_gallery_width = '{$board['bo_gallery_width']}',
bo_gallery_height = '{$board['bo_gallery_height']}',
bo_mobile_gallery_width = '{$board['bo_mobile_gallery_width']}',
bo_mobile_gallery_height = '{$board['bo_mobile_gallery_height']}',
bo_upload_size = '{$board['bo_upload_size']}',
bo_reply_order = '{$board['bo_reply_order']}',
bo_use_search = '{$board['bo_use_search']}',
bo_order = '{$board['bo_order']}',
bo_notice = '{$board['bo_notice']}',
bo_upload_count = '{$board['bo_upload_count']}',
bo_use_email = '{$board['bo_use_email']}',
bo_use_cert = '{$board['bo_use_cert']}',
bo_use_sns = '{$board['bo_use_sns']}',
bo_use_captcha = '{$board['bo_use_captcha']}',
bo_sort_field = '{$board['bo_sort_field']}',
bo_1_subj = '" . addslashes($board['bo_1_subj']) . "',
bo_2_subj = '" . addslashes($board['bo_2_subj']) . "',
bo_3_subj = '" . addslashes($board['bo_3_subj']) . "',
bo_4_subj = '" . addslashes($board['bo_4_subj']) . "',
bo_5_subj = '" . addslashes($board['bo_5_subj']) . "',
bo_6_subj = '" . addslashes($board['bo_6_subj']) . "',
bo_7_subj = '" . addslashes($board['bo_7_subj']) . "',
bo_8_subj = '" . addslashes($board['bo_8_subj']) . "',
bo_9_subj = '" . addslashes($board['bo_9_subj']) . "',
bo_10_subj = '" . addslashes($board['bo_10_subj']) . "',
bo_1 = '" . addslashes($board['bo_1']) . "',
bo_2 = '" . addslashes($board['bo_2']) . "',
bo_3 = '" . addslashes($board['bo_3']) . "',
bo_4 = '" . addslashes($board['bo_4']) . "',
bo_5 = '" . addslashes($board['bo_5']) . "',
bo_6 = '" . addslashes($board['bo_6']) . "',
bo_7 = '" . addslashes($board['bo_7']) . "',
bo_8 = '" . addslashes($board['bo_8']) . "',
bo_9 = '" . addslashes($board['bo_9']) . "',
bo_10 = '" . addslashes($board['bo_10']) . "' ";
sql_query($sql, false);
// 게시판 폴더 생성
@mkdir(G5_DATA_PATH . '/file/' . $target_table, G5_DIR_PERMISSION);
@chmod(G5_DATA_PATH . '/file/' . $target_table, G5_DIR_PERMISSION);
// 디렉토리에 있는 파일의 목록을 보이지 않게 한다.
$board_path = G5_DATA_PATH . '/file/' . $target_table;
$file = $board_path . '/index.php';
$f = @fopen($file, 'w');
@fwrite($f, '');
@fclose($f);
@chmod($file, G5_FILE_PERMISSION);
$copy_file = 0;
if ($copy_case == 'schema_data_both') {
$d = dir(G5_DATA_PATH . '/file/' . $bo_table);
while ($entry = $d->read()) {
if ($entry == '.' || $entry == '..') {
continue;
}
// 김선용 201007 :
if (is_dir(G5_DATA_PATH . '/file/' . $bo_table . '/' . $entry)) {
$dd = dir(G5_DATA_PATH . '/file/' . $bo_table . '/' . $entry);
@mkdir(G5_DATA_PATH . '/file/' . $target_table . '/' . $entry, G5_DIR_PERMISSION);
@chmod(G5_DATA_PATH . '/file/' . $target_table . '/' . $entry, G5_DIR_PERMISSION);
while ($entry2 = $dd->read()) {
if ($entry2 == '.' || $entry2 == '..') {
continue;
}
@copy(G5_DATA_PATH . '/file/' . $bo_table . '/' . $entry . '/' . $entry2, G5_DATA_PATH . '/file/' . $target_table . '/' . $entry . '/' . $entry2);
@chmod(G5_DATA_PATH . '/file/' . $target_table . '/' . $entry . '/' . $entry2, G5_DIR_PERMISSION);
$copy_file++;
}
$dd->close();
} else {
@copy(G5_DATA_PATH . '/file/' . $bo_table . '/' . $entry, G5_DATA_PATH . '/file/' . $target_table . '/' . $entry);
@chmod(G5_DATA_PATH . '/file/' . $target_table . '/' . $entry, G5_DIR_PERMISSION);
$copy_file++;
}
}
$d->close();
run_event('admin_board_copy_file', $bo_table, $target_table);
// 글복사
$sql = " insert into {$g5['write_prefix']}$target_table select * from {$g5['write_prefix']}$bo_table ";
sql_query($sql, false);
// 게시글수 저장
$sql = " select bo_count_write, bo_count_comment from {$g5['board_table']} where bo_table = '$bo_table' ";
$row = sql_fetch($sql);
$sql = " update {$g5['board_table']} set bo_count_write = '{$row['bo_count_write']}', bo_count_comment = '{$row['bo_count_comment']}' where bo_table = '$target_table' ";
sql_query($sql, false);
// 4.00.01
$sql = " select * from {$g5['board_file_table']} where bo_table = '$bo_table' ";
$result = sql_query($sql, false);
for ($i = 0; $row = sql_fetch_array($result); $i++) {
$file_copy[$i] = $row;
}
}
if (count($file_copy)) {
for ($i = 0; $i < count($file_copy); $i++) {
$file_copy[$i] = run_replace('admin_copy_update_file', $file_copy[$i], $file_copy[$i]['bf_file'], $bo_table, $target_table);
$sql = " insert into {$g5['board_file_table']}
set bo_table = '$target_table',
wr_id = '{$file_copy[$i]['wr_id']}',
bf_no = '{$file_copy[$i]['bf_no']}',
bf_source = '" . addslashes($file_copy[$i]['bf_source']) . "',
bf_file = '{$file_copy[$i]['bf_file']}',
bf_download = '{$file_copy[$i]['bf_download']}',
bf_content = '" . addslashes($file_copy[$i]['bf_content']) . "',
bf_fileurl = '" . addslashes($file_copy[$i]['bf_fileurl']) . "',
bf_thumburl = '" . addslashes($file_copy[$i]['bf_thumburl']) . "',
bf_storage = '" . addslashes($file_copy[$i]['bf_storage']) . "',
bf_filesize = '{$file_copy[$i]['bf_filesize']}',
bf_width = '{$file_copy[$i]['bf_width']}',
bf_height = '{$file_copy[$i]['bf_height']}',
bf_type = '{$file_copy[$i]['bf_type']}',
bf_datetime = '{$file_copy[$i]['bf_datetime']}' ";
sql_query($sql, false);
}
}
delete_cache_latest($bo_table);
delete_cache_latest($target_table);
echo "<script>opener.document.location.reload();</script>";
alert("복사에 성공 했습니다.", './board_copy.php?bo_table=' . $bo_table . '&amp;' . $qstr);
+42
View File
@@ -0,0 +1,42 @@
<?php
// board_delete.php , boardgroup_delete.php 에서 include 하는 파일
// 개별 페이지 접근 불가
if (!defined('_GNUBOARD_')) {
exit;
}
if (!defined('_BOARD_DELETE_')) {
exit;
}
// $tmp_bo_table 에는 $bo_table 값을 넘겨주어야 함
if (!$tmp_bo_table) {
return;
}
// 게시판 1개는 삭제 불가 (게시판 복사를 위해서)
//$row = sql_fetch(" select count(*) as cnt from $g5['board_table'] ");
//if ($row['cnt'] <= 1) { return; }
// 게시판 설정 삭제
sql_query(" delete from {$g5['board_table']} where bo_table = '{$tmp_bo_table}' ");
// 최신글 삭제
sql_query(" delete from {$g5['board_new_table']} where bo_table = '{$tmp_bo_table}' ");
// 스크랩 삭제
sql_query(" delete from {$g5['scrap_table']} where bo_table = '{$tmp_bo_table}' ");
// 파일 삭제
sql_query(" delete from {$g5['board_file_table']} where bo_table = '{$tmp_bo_table}' ");
// 게시판 테이블 DROP
sql_query(" drop table {$g5['write_prefix']}{$tmp_bo_table} ", false);
// 좋아요 테이블에서 기록 삭제
sql_query(" delete from {$g5['board_good_table']} where bo_table = '{$tmp_bo_table}' ");
delete_cache_latest($tmp_bo_table);
// 게시판 폴더 전체 삭제
rm_rf(G5_DATA_PATH . '/file/' . $tmp_bo_table);
+1537
View File
File diff suppressed because it is too large Load Diff
+554
View File
@@ -0,0 +1,554 @@
<?php
$sub_menu = "300100";
include_once('./_common.php');
if ($w == 'u') {
check_demo();
}
auth_check_menu($auth, $sub_menu, 'w');
check_admin_token();
$gr_id = isset($_POST['gr_id']) ? preg_replace('/[^a-z0-9_]/i', '', (string)$_POST['gr_id']) : '';
$bo_admin = isset($_POST['bo_admin']) ? preg_replace('/[^a-z0-9_\, \|\#]/i', '', $_POST['bo_admin']) : '';
$bo_subject = isset($_POST['bo_subject']) ? strip_tags(clean_xss_attributes($_POST['bo_subject'])) : '';
$bo_mobile_subject = isset($_POST['bo_mobile_subject']) ? strip_tags(clean_xss_attributes($_POST['bo_mobile_subject'])) : '';
if (!$gr_id) {
alert('그룹 ID는 반드시 선택하세요.');
}
if (!$bo_table) {
alert('게시판 TABLE명은 반드시 입력하세요.');
}
if (!preg_match("/^([A-Za-z0-9_]{1,20})$/", $bo_table)) {
alert('게시판 TABLE명은 공백없이 영문자, 숫자, _ 만 사용 가능합니다. (20자 이내)');
}
if (!$bo_subject) {
alert('게시판 제목을 입력하세요.');
}
// 게시판명이 금지된 단어로 되어 있으면
if ($w == '' && in_array($bo_table, get_bo_table_banned_word())) {
alert('입력한 게시판 TABLE명을 사용할수 없습니다. 다른 이름으로 입력해 주세요.');
}
$bo_include_head = isset($_POST['bo_include_head']) ? preg_replace(array("#[\\\]+$#", "#(<\?php|<\?)#i"), "", substr($_POST['bo_include_head'], 0, 255)) : '';
$bo_include_tail = isset($_POST['bo_include_tail']) ? preg_replace(array("#[\\\]+$#", "#(<\?php|<\?)#i"), "", substr($_POST['bo_include_tail'], 0, 255)) : '';
// 관리자가 자동등록방지를 사용해야 할 경우
if ($board && (isset($board['bo_include_head']) && $board['bo_include_head'] !== $bo_include_head || $board['bo_include_tail'] !== $bo_include_tail) && function_exists('get_admin_captcha_by') && get_admin_captcha_by()) {
include_once(G5_CAPTCHA_PATH . '/captcha.lib.php');
if (!chk_captcha()) {
alert('자동등록방지 숫자가 틀렸습니다.');
}
}
if ($file = $bo_include_head) {
$file_ext = pathinfo($file, PATHINFO_EXTENSION);
if (!$file_ext || !in_array($file_ext, array('php', 'htm', 'html')) || !preg_match('/^.*\.(php|htm|html)$/i', $file)) {
alert('상단 파일 경로의 확장자는 php, htm, html 만 허용합니다.');
}
}
if ($file = $bo_include_tail) {
$file_ext = pathinfo($file, PATHINFO_EXTENSION);
if (!$file_ext || !in_array($file_ext, array('php', 'htm', 'html')) || !preg_match('/^.*\.(php|htm|html)$/i', $file)) {
alert('하단 파일 경로의 확장자는 php, htm, html 만 허용합니다.');
}
}
if (!is_include_path_check($bo_include_head, 1)) {
alert('상단 파일 경로에 포함시킬수 없는 문자열이 있습니다.');
}
if (!is_include_path_check($bo_include_tail, 1)) {
alert('하단 파일 경로에 포함시킬수 없는 문자열이 있습니다.');
}
if (function_exists('filter_input_include_path')) {
$bo_include_head = filter_input_include_path($bo_include_head);
$bo_include_tail = filter_input_include_path($bo_include_tail);
}
$board_path = G5_DATA_PATH . '/file/' . $bo_table;
// 게시판 디렉토리 생성
@mkdir($board_path, G5_DIR_PERMISSION);
@chmod($board_path, G5_DIR_PERMISSION);
// 디렉토리에 있는 파일의 목록을 보이지 않게 한다.
$file = $board_path . '/index.php';
if ($f = @fopen($file, 'w')) {
@fwrite($f, '');
@fclose($f);
@chmod($file, G5_FILE_PERMISSION);
}
// 분류에 & 나 = 는 사용이 불가하므로 2바이트로 바꾼다.
$src_char = array('&', '=');
$dst_char = array('', '〓');
$bo_category_list = isset($_POST['bo_category_list']) ? str_replace($src_char, $dst_char, $_POST['bo_category_list']) : '';
//https://github.com/gnuboard/gnuboard5/commit/f5f4925d4eb28ba1af728e1065fc2bdd9ce1da58 에 따른 조치
$str_bo_category_list = preg_replace("/[\<\>\'\"\\\'\\\"\%\=\(\)\/\^\*]/", "", (string)$bo_category_list);
$bo_use_category = isset($_POST['bo_use_category']) ? (int) $_POST['bo_use_category'] : 0;
$bo_use_sideview = isset($_POST['bo_use_sideview']) ? (int) $_POST['bo_use_sideview'] : 0;
$bo_use_dhtml_editor = isset($_POST['bo_use_dhtml_editor']) ? (int) $_POST['bo_use_dhtml_editor'] : 0;
$bo_use_good = isset($_POST['bo_use_good']) ? (int) $_POST['bo_use_good'] : 0;
$bo_use_nogood = isset($_POST['bo_use_nogood']) ? (int) $_POST['bo_use_nogood'] : 0;
$bo_use_name = isset($_POST['bo_use_name']) ? (int) $_POST['bo_use_name'] : 0;
$bo_use_signature = isset($_POST['bo_use_signature']) ? (int) $_POST['bo_use_signature'] : 0;
$bo_use_ip_view = isset($_POST['bo_use_ip_view']) ? (int) $_POST['bo_use_ip_view'] : 0;
$bo_use_list_view = isset($_POST['bo_use_list_view']) ? (int) $_POST['bo_use_list_view'] : 0;
$bo_use_list_file = isset($_POST['bo_use_list_file']) ? (int) $_POST['bo_use_list_file'] : 0;
$bo_use_list_content = isset($_POST['bo_use_list_content']) ? (int) $_POST['bo_use_list_content'] : 0;
$bo_use_email = isset($_POST['bo_use_email']) ? (int) $_POST['bo_use_email'] : 0;
$bo_use_sns = isset($_POST['bo_use_sns']) ? (int) $_POST['bo_use_sns'] : 0;
$bo_use_captcha = isset($_POST['bo_use_captcha']) ? (int) $_POST['bo_use_captcha'] : 0;
$bo_table_width = isset($_POST['bo_table_width']) ? (int) $_POST['bo_table_width'] : 0;
$bo_subject_len = isset($_POST['bo_subject_len']) ? (int) $_POST['bo_subject_len'] : 0;
$bo_mobile_subject_len = isset($_POST['bo_mobile_subject_len']) ? (int) $_POST['bo_mobile_subject_len'] : 0;
$bo_page_rows = isset($_POST['bo_page_rows']) ? (int) $_POST['bo_page_rows'] : 0;
$bo_mobile_page_rows = isset($_POST['bo_mobile_page_rows']) ? (int) $_POST['bo_mobile_page_rows'] : 0;
$bo_use_rss_view = isset($_POST['bo_use_rss_view']) ? (int) $_POST['bo_use_rss_view'] : 0;
$bo_use_secret = isset($_POST['bo_use_secret']) ? (int) $_POST['bo_use_secret'] : 0;
$bo_use_file_content = isset($_POST['bo_use_file_content']) ? (int) $_POST['bo_use_file_content'] : 0;
$bo_new = isset($_POST['bo_new']) ? (int) $_POST['bo_new'] : 0;
$bo_hot = isset($_POST['bo_hot']) ? (int) $_POST['bo_hot'] : 0;
$bo_image_width = isset($_POST['bo_image_width']) ? (int) $_POST['bo_image_width'] : 0;
$bo_use_search = isset($_POST['bo_use_search']) ? (int) $_POST['bo_use_search'] : 0;
$bo_use_cert = isset($_POST['bo_use_cert']) ? preg_replace('/[^0-9a-z_]/i', '', $_POST['bo_use_cert']) : '';
$bo_device = isset($_POST['bo_device']) ? clean_xss_tags($_POST['bo_device'], 1, 1) : '';
$bo_list_level = isset($_POST['bo_list_level']) ? (int) $_POST['bo_list_level'] : 0;
$bo_read_level = isset($_POST['bo_read_level']) ? (int) $_POST['bo_read_level'] : 0;
$bo_write_level = isset($_POST['bo_write_level']) ? (int) $_POST['bo_write_level'] : 0;
$bo_reply_level = isset($_POST['bo_reply_level']) ? (int) $_POST['bo_reply_level'] : 0;
$bo_comment_level = isset($_POST['bo_comment_level']) ? (int) $_POST['bo_comment_level'] : 0;
$bo_html_level = isset($_POST['bo_html_level']) ? (int) $_POST['bo_html_level'] : 0;
$bo_link_level = isset($_POST['bo_link_level']) ? (int) $_POST['bo_link_level'] : 0;
$bo_count_modify = isset($_POST['bo_count_modify']) ? (int) $_POST['bo_count_modify'] : 0;
$bo_count_delete = isset($_POST['bo_count_delete']) ? (int) $_POST['bo_count_delete'] : 0;
$bo_upload_level = isset($_POST['bo_upload_level']) ? (int) $_POST['bo_upload_level'] : 0;
$bo_download_level = isset($_POST['bo_download_level']) ? (int) $_POST['bo_download_level'] : 0;
$bo_read_point = isset($_POST['bo_read_point']) ? (int) $_POST['bo_read_point'] : 0;
$bo_write_point = isset($_POST['bo_write_point']) ? (int) $_POST['bo_write_point'] : 0;
$bo_comment_point = isset($_POST['bo_comment_point']) ? (int) $_POST['bo_comment_point'] : 0;
$bo_download_point = isset($_POST['bo_download_point']) ? (int) $_POST['bo_download_point'] : 0;
$bo_select_editor = isset($_POST['bo_select_editor']) ? clean_xss_tags($_POST['bo_select_editor'], 1, 1) : '';
$bo_skin = isset($_POST['bo_skin']) ? clean_xss_tags($_POST['bo_skin'], 1, 1) : '';
$bo_mobile_skin = isset($_POST['bo_mobile_skin']) ? clean_xss_tags($_POST['bo_mobile_skin'], 1, 1) : '';
$bo_content_head = isset($_POST['bo_content_head']) ? $_POST['bo_content_head'] : '';
$bo_content_tail = isset($_POST['bo_content_tail']) ? $_POST['bo_content_tail'] : '';
$bo_mobile_content_head = isset($_POST['bo_mobile_content_head']) ? $_POST['bo_mobile_content_head'] : '';
$bo_mobile_content_tail = isset($_POST['bo_mobile_content_tail']) ? $_POST['bo_mobile_content_tail'] : '';
$bo_insert_content = isset($_POST['bo_insert_content']) ? $_POST['bo_insert_content'] : '';
$bo_gallery_cols = isset($_POST['bo_gallery_cols']) ? (int) $_POST['bo_gallery_cols'] : 0;
$bo_gallery_width = isset($_POST['bo_gallery_width']) ? (int) $_POST['bo_gallery_width'] : 0;
$bo_gallery_height = isset($_POST['bo_gallery_height']) ? (int) $_POST['bo_gallery_height'] : 0;
$bo_mobile_gallery_width = isset($_POST['bo_mobile_gallery_width']) ? (int) $_POST['bo_mobile_gallery_width'] : 0;
$bo_mobile_gallery_height = isset($_POST['bo_mobile_gallery_height']) ? (int) $_POST['bo_mobile_gallery_height'] : 0;
$bo_upload_count = isset($_POST['bo_upload_count']) ? (int) $_POST['bo_upload_count'] : 0;
$bo_upload_size = isset($_POST['bo_upload_size']) ? (int) $_POST['bo_upload_size'] : 0;
$bo_reply_order = isset($_POST['bo_reply_order']) ? (int) $_POST['bo_reply_order'] : 0;
$bo_order = isset($_POST['bo_order']) ? (int) $_POST['bo_order'] : 0;
$bo_write_min = isset($_POST['bo_write_min']) ? (int) $_POST['bo_write_min'] : 0;
$bo_write_max = isset($_POST['bo_write_max']) ? (int) $_POST['bo_write_max'] : 0;
$bo_comment_min = isset($_POST['bo_comment_min']) ? (int) $_POST['bo_comment_min'] : 0;
$bo_comment_max = isset($_POST['bo_comment_max']) ? (int) $_POST['bo_comment_max'] : 0;
$bo_sort_field = isset($_POST['bo_sort_field']) ? clean_xss_tags($_POST['bo_sort_field'], 1, 1) : '';
// 💡 [추가] 예약 중복 검사 설정값
$bo_use_reservation_overlap = isset($_POST['bo_use_reservation_overlap']) ? (int) $_POST['bo_use_reservation_overlap'] : 0;
$bo_reservation_overlap_count = isset($_POST['bo_reservation_overlap_count']) ? (int) $_POST['bo_reservation_overlap_count'] : 0;
if (strpbrk($bo_skin.$bo_mobile_skin, "?%*:|\"<>") !== false) {
alert('스킨 디렉토리명 오류!');
}
$etcs = array();
for ($i = 1; $i <= 10; $i++) {
$etcs['bo_' . $i . '_subj'] = ${'bo_' . $i . '_subj'} = isset($_POST['bo_' . $i . '_subj']) ? $_POST['bo_' . $i . '_subj'] : '';
$etcs['bo_' . $i] = ${'bo_' . $i} = isset($_POST['bo_' . $i]) ? $_POST['bo_' . $i] : '';
}
$sql_common = " gr_id = '{$gr_id}',
bo_subject = '{$bo_subject}',
bo_mobile_subject = '{$bo_mobile_subject}',
bo_device = '{$bo_device}',
bo_admin = '{$bo_admin}',
bo_list_level = '{$bo_list_level}',
bo_read_level = '{$bo_read_level}',
bo_write_level = '{$bo_write_level}',
bo_reply_level = '{$bo_reply_level}',
bo_comment_level = '{$bo_comment_level}',
bo_html_level = '{$bo_html_level}',
bo_link_level = '{$bo_link_level}',
bo_count_modify = '{$bo_count_modify}',
bo_count_delete = '{$bo_count_delete}',
bo_upload_level = '{$bo_upload_level}',
bo_download_level = '{$bo_download_level}',
bo_read_point = '{$bo_read_point}',
bo_write_point = '{$bo_write_point}',
bo_comment_point = '{$bo_comment_point}',
bo_download_point = '{$bo_download_point}',
bo_use_category = '{$bo_use_category}',
bo_category_list = '{$str_bo_category_list}',
bo_use_sideview = '{$bo_use_sideview}',
bo_use_file_content = '{$bo_use_file_content}',
bo_use_secret = '{$bo_use_secret}',
bo_use_dhtml_editor = '{$bo_use_dhtml_editor}',
bo_select_editor = '{$bo_select_editor}',
bo_use_rss_view = '{$bo_use_rss_view}',
bo_use_good = '{$bo_use_good}',
bo_use_nogood = '{$bo_use_nogood}',
bo_use_name = '{$bo_use_name}',
bo_use_signature = '{$bo_use_signature}',
bo_use_ip_view = '{$bo_use_ip_view}',
bo_use_list_view = '{$bo_use_list_view}',
bo_use_list_file = '{$bo_use_list_file}',
bo_use_list_content = '{$bo_use_list_content}',
bo_use_email = '{$bo_use_email}',
bo_use_cert = '{$bo_use_cert}',
bo_use_sns = '{$bo_use_sns}',
bo_use_captcha = '{$bo_use_captcha}',
bo_table_width = '{$bo_table_width}',
bo_subject_len = '{$bo_subject_len}',
bo_mobile_subject_len = '{$bo_mobile_subject_len}',
bo_page_rows = '{$bo_page_rows}',
bo_mobile_page_rows = '{$bo_mobile_page_rows}',
bo_new = '{$bo_new}',
bo_hot = '{$bo_hot}',
bo_image_width = '{$bo_image_width}',
bo_skin = '{$bo_skin}',
bo_mobile_skin = '{$bo_mobile_skin}',
bo_use_reservation_overlap = '{$bo_use_reservation_overlap}',
bo_reservation_overlap_count = '{$bo_reservation_overlap_count}',
";
// 최고 관리자인 경우에만 수정가능
if ($is_admin === 'super') {
$sql_common .= " bo_include_head = '" . $bo_include_head . "',
bo_include_tail = '" . $bo_include_tail . "',
bo_content_head = '{$bo_content_head}',
bo_content_tail = '{$bo_content_tail}',
bo_mobile_content_head = '{$bo_mobile_content_head}',
bo_mobile_content_tail = '{$bo_mobile_content_tail}',
";
}
$sql_common .= " bo_insert_content = '{$bo_insert_content}',
bo_gallery_cols = '{$bo_gallery_cols}',
bo_gallery_width = '{$bo_gallery_width}',
bo_gallery_height = '{$bo_gallery_height}',
bo_mobile_gallery_width = '{$bo_mobile_gallery_width}',
bo_mobile_gallery_height= '{$bo_mobile_gallery_height}',
bo_upload_count = '{$bo_upload_count}',
bo_upload_size = '{$bo_upload_size}',
bo_reply_order = '{$bo_reply_order}',
bo_use_search = '{$bo_use_search}',
bo_order = '{$bo_order}',
bo_write_min = '{$bo_write_min}',
bo_write_max = '{$bo_write_max}',
bo_comment_min = '{$bo_comment_min}',
bo_comment_max = '{$bo_comment_max}',
bo_sort_field = '{$bo_sort_field}',
bo_1_subj = '{$bo_1_subj}',
bo_2_subj = '{$bo_2_subj}',
bo_3_subj = '{$bo_3_subj}',
bo_4_subj = '{$bo_4_subj}',
bo_5_subj = '{$bo_5_subj}',
bo_6_subj = '{$bo_6_subj}',
bo_7_subj = '{$bo_7_subj}',
bo_8_subj = '{$bo_8_subj}',
bo_9_subj = '{$bo_9_subj}',
bo_10_subj = '{$bo_10_subj}',
bo_1 = '{$bo_1}',
bo_2 = '{$bo_2}',
bo_3 = '{$bo_3}',
bo_4 = '{$bo_4}',
bo_5 = '{$bo_5}',
bo_6 = '{$bo_6}',
bo_7 = '{$bo_7}',
bo_8 = '{$bo_8}',
bo_9 = '{$bo_9}',
bo_10 = '{$bo_10}' ";
if ($w == '') {
$row = sql_fetch(" select count(*) as cnt from {$g5['board_table']} where bo_table = '{$bo_table}' ");
if ($row['cnt']) {
alert($bo_table . ' 은(는) 이미 존재하는 TABLE 입니다.');
}
$sql = " insert into {$g5['board_table']}
set bo_table = '{$bo_table}',
bo_count_write = '0',
bo_count_comment = '0',
$sql_common ";
sql_query($sql);
// 게시판 테이블 생성
$file = file('./sql_write.sql');
$file = get_db_create_replace($file);
$sql = implode("\n", $file);
$create_table = $g5['write_prefix'] . $bo_table;
// sql_board.sql 파일의 테이블명을 변환
$source = array('/__TABLE_NAME__/', '/;/');
$target = array($create_table, '');
$sql = preg_replace($source, $target, $sql);
sql_query($sql, false);
} elseif ($w == 'u') {
// 게시판의 글 수
$sql = " select count(*) as cnt from {$g5['write_prefix']}{$bo_table} where wr_is_comment = 0 ";
$row = sql_fetch($sql);
$bo_count_write = $row['cnt'];
// 게시판의 코멘트 수
$sql = " select count(*) as cnt from {$g5['write_prefix']}{$bo_table} where wr_is_comment = 1 ";
$row = sql_fetch($sql);
$bo_count_comment = $row['cnt'];
// 글수 조정
/*
엔피씨님의 팁으로 교체합니다. 130308
http://sir.kr/g5_tiptech/27207
*/
if (isset($_POST['proc_count'])) {
// 원글을 얻습니다.
//$sql = " select wr_id from {$g5['write_prefix']}{$bo_table} where wr_is_comment = 0 ";
$sql = " select a.wr_id, (count(b.wr_parent) - 1) as cnt from {$g5['write_prefix']}{$bo_table} a, {$g5['write_prefix']}{$bo_table} b where a.wr_id=b.wr_parent and a.wr_is_comment=0 group by a.wr_id ";
$result = sql_query($sql);
for ($i = 0; $row = sql_fetch_array($result); $i++) {
/*
// 코멘트수를 얻습니다.
$sql2 = " select count(*) as cnt from {$g5['write_prefix']}$bo_table where wr_parent = '{$row['wr_id']}' and wr_is_comment = 1 ";
$row2 = sql_fetch($sql2);
*/
sql_query(" update {$g5['write_prefix']}{$bo_table} set wr_comment = '{$row['cnt']}' where wr_id = '{$row['wr_id']}' ");
}
}
// 공지사항에는 등록되어 있지만 실제 존재하지 않는 글 아이디는 삭제합니다.
$bo_notice = "";
$lf = "";
if ($board['bo_notice']) {
$tmp_array = explode(",", $board['bo_notice']);
for ($i = 0; $i < count($tmp_array); $i++) {
$tmp_wr_id = trim($tmp_array[$i]);
$row = sql_fetch(" select count(*) as cnt from {$g5['write_prefix']}{$bo_table} where wr_id = '{$tmp_wr_id}' ");
if ($row['cnt']) {
$bo_notice .= $lf . $tmp_wr_id;
$lf = ",";
}
}
}
$sql = " update {$g5['board_table']}
set bo_notice = '{$bo_notice}',
bo_count_write = '{$bo_count_write}',
bo_count_comment = '{$bo_count_comment}',
{$sql_common}
where bo_table = '{$bo_table}' ";
sql_query($sql);
}
// 같은 그룹내 게시판 동일 옵션 적용
$grp_fields = '';
if (is_checked('chk_grp_device')) $grp_fields .= " , bo_device = '{$bo_device}' ";
if (is_checked('chk_grp_admin')) $grp_fields .= " , bo_admin = '{$bo_admin}' ";
if (is_checked('chk_grp_list_level')) $grp_fields .= " , bo_list_level = '{$bo_list_level}' ";
if (is_checked('chk_grp_read_level')) $grp_fields .= " , bo_read_level = '{$bo_read_level}' ";
if (is_checked('chk_grp_write_level')) $grp_fields .= " , bo_write_level = '{$bo_write_level}' ";
if (is_checked('chk_grp_reply_level')) $grp_fields .= " , bo_reply_level = '{$bo_reply_level}' ";
if (is_checked('chk_grp_comment_level')) $grp_fields .= " , bo_comment_level = '{$bo_comment_level}' ";
if (is_checked('chk_grp_link_level')) $grp_fields .= " , bo_link_level = '{$bo_link_level}' ";
if (is_checked('chk_grp_upload_level')) $grp_fields .= " , bo_upload_level = '{$bo_upload_level}' ";
if (is_checked('chk_grp_download_level')) $grp_fields .= " , bo_download_level = '{$bo_download_level}' ";
if (is_checked('chk_grp_html_level')) $grp_fields .= " , bo_html_level = '{$bo_html_level}' ";
if (is_checked('chk_grp_count_modify')) $grp_fields .= " , bo_count_modify = '{$bo_count_modify}' ";
if (is_checked('chk_grp_count_delete')) $grp_fields .= " , bo_count_delete = '{$bo_count_delete}' ";
if (is_checked('chk_grp_read_point')) $grp_fields .= " , bo_read_point = '{$bo_read_point}' ";
if (is_checked('chk_grp_write_point')) $grp_fields .= " , bo_write_point = '{$bo_write_point}' ";
if (is_checked('chk_grp_comment_point')) $grp_fields .= " , bo_comment_point = '{$bo_comment_point}' ";
if (is_checked('chk_grp_download_point')) $grp_fields .= " , bo_download_point = '{$bo_download_point}' ";
if (is_checked('chk_grp_category_list')) {
$grp_fields .= " , bo_category_list = '{$str_bo_category_list}' ";
$grp_fields .= " , bo_use_category = '{$bo_use_category}' ";
}
if (is_checked('chk_grp_use_sideview')) $grp_fields .= " , bo_use_sideview = '{$bo_use_sideview}' ";
if (is_checked('chk_grp_use_file_content')) $grp_fields .= " , bo_use_file_content = '{$bo_use_file_content}' ";
if (is_checked('chk_grp_use_secret')) $grp_fields .= " , bo_use_secret = '{$bo_use_secret}' ";
if (is_checked('chk_grp_use_dhtml_editor')) $grp_fields .= " , bo_use_dhtml_editor = '{$bo_use_dhtml_editor}' ";
if (is_checked('chk_grp_select_editor')) $grp_fields .= " , bo_select_editor = '{$bo_select_editor}' ";
if (is_checked('chk_grp_use_rss_view')) $grp_fields .= " , bo_use_rss_view = '{$bo_use_rss_view}' ";
if (is_checked('chk_grp_use_good')) $grp_fields .= " , bo_use_good = '{$bo_use_good}' ";
if (is_checked('chk_grp_use_nogood')) $grp_fields .= " , bo_use_nogood = '{$bo_use_nogood}' ";
if (is_checked('chk_grp_use_name')) $grp_fields .= " , bo_use_name = '{$bo_use_name}' ";
if (is_checked('chk_grp_use_signature')) $grp_fields .= " , bo_use_signature = '{$bo_use_signature}' ";
if (is_checked('chk_grp_use_ip_view')) $grp_fields .= " , bo_use_ip_view = '{$bo_use_ip_view}' ";
if (is_checked('chk_grp_use_list_view')) $grp_fields .= " , bo_use_list_view = '{$bo_use_list_view}' ";
if (is_checked('chk_grp_use_list_file')) $grp_fields .= " , bo_use_list_file = '{$bo_use_list_file}' ";
if (is_checked('chk_grp_use_list_content')) $grp_fields .= " , bo_use_list_content = '{$bo_use_list_content}' ";
if (is_checked('chk_grp_use_email')) $grp_fields .= " , bo_use_email = '{$bo_use_email}' ";
if (is_checked('chk_grp_use_cert')) $grp_fields .= " , bo_use_cert = '{$bo_use_cert}' ";
if (is_checked('chk_grp_use_sns')) $grp_fields .= " , bo_use_sns = '{$bo_use_sns}' ";
if (is_checked('chk_grp_use_captcha')) $grp_fields .= " , bo_use_captcha = '{$bo_use_captcha}' ";
if (is_checked('chk_grp_skin')) $grp_fields .= " , bo_skin = '{$bo_skin}' ";
if (is_checked('chk_grp_mobile_skin')) $grp_fields .= " , bo_mobile_skin = '{$bo_mobile_skin}' ";
if (is_checked('chk_grp_gallery_cols')) $grp_fields .= " , bo_gallery_cols = '{$bo_gallery_cols}' ";
if (is_checked('chk_grp_gallery_width')) $grp_fields .= " , bo_gallery_width = '{$bo_gallery_width}' ";
if (is_checked('chk_grp_gallery_height')) $grp_fields .= " , bo_gallery_height = '{$bo_gallery_height}' ";
if (is_checked('chk_grp_mobile_gallery_width')) $grp_fields .= " , bo_mobile_gallery_width = '{$bo_mobile_gallery_width}' ";
if (is_checked('chk_grp_mobile_gallery_height'))$grp_fields .= " , bo_mobile_gallery_height = '{$bo_mobile_gallery_height}' ";
if (is_checked('chk_grp_table_width')) $grp_fields .= " , bo_table_width = '{$bo_table_width}' ";
if (is_checked('chk_grp_page_rows')) $grp_fields .= " , bo_page_rows = '{$bo_page_rows}' ";
if (is_checked('chk_grp_mobile_page_rows')) $grp_fields .= " , bo_mobile_page_rows = '{$bo_mobile_page_rows}' ";
if (is_checked('chk_grp_subject_len')) $grp_fields .= " , bo_subject_len = '{$bo_subject_len}' ";
if (is_checked('chk_grp_mobile_subject_len')) $grp_fields .= " , bo_mobile_subject_len = '{$bo_mobile_subject_len}' ";
if (is_checked('chk_grp_new')) $grp_fields .= " , bo_new = '{$bo_new}' ";
if (is_checked('chk_grp_hot')) $grp_fields .= " , bo_hot = '{$bo_hot}' ";
if (is_checked('chk_grp_image_width')) $grp_fields .= " , bo_image_width = '{$bo_image_width}' ";
if (is_checked('chk_grp_reply_order')) $grp_fields .= " , bo_reply_order = '{$bo_reply_order}' ";
if (is_checked('chk_grp_sort_field')) $grp_fields .= " , bo_sort_field = '{$bo_sort_field}' ";
if (is_checked('chk_grp_write_min')) $grp_fields .= " , bo_write_min = '{$bo_write_min}' ";
if (is_checked('chk_grp_write_max')) $grp_fields .= " , bo_write_max = '{$bo_write_max}' ";
if (is_checked('chk_grp_comment_min')) $grp_fields .= " , bo_comment_min = '{$bo_comment_min}' ";
if (is_checked('chk_grp_comment_max')) $grp_fields .= " , bo_comment_max = '{$bo_comment_max}' ";
if (is_checked('chk_grp_upload_count')) $grp_fields .= " , bo_upload_count = '{$bo_upload_count}' ";
if (is_checked('chk_grp_upload_size')) $grp_fields .= " , bo_upload_size = '{$bo_upload_size}' ";
if (is_checked('chk_grp_use_reservation_overlap')) $grp_fields .= " , bo_use_reservation_overlap = '{$bo_use_reservation_overlap}' ";
if (is_checked('chk_grp_reservation_overlap_count')) $grp_fields .= " , bo_reservation_overlap_count = '{$bo_reservation_overlap_count}' ";
//최고관리자만 수정가능
if ($is_admin === 'super') {
if (is_checked('chk_grp_include_head')) $grp_fields .= " , bo_include_head = '{$bo_include_head}' ";
if (is_checked('chk_grp_include_tail')) $grp_fields .= " , bo_include_tail = '{$bo_include_tail}' ";
if (is_checked('chk_grp_content_head')) $grp_fields .= " , bo_content_head = '{$bo_content_head}' ";
if (is_checked('chk_grp_content_tail')) $grp_fields .= " , bo_content_tail = '{$bo_content_tail}' ";
if (is_checked('chk_grp_mobile_content_head')) $grp_fields .= " , bo_mobile_content_head = '{$bo_mobile_content_head}' ";
if (is_checked('chk_grp_mobile_content_tail')) $grp_fields .= " , bo_mobile_content_tail = '{$bo_mobile_content_tail}' ";
}
if (is_checked('chk_grp_insert_content')) $grp_fields .= " , bo_insert_content = '{$bo_insert_content}' ";
if (is_checked('chk_grp_use_search')) $grp_fields .= " , bo_use_search = '{$bo_use_search}' ";
if (is_checked('chk_grp_order')) $grp_fields .= " , bo_order = '{$bo_order}' ";
for ($i = 1; $i <= 10; $i++) {
if (is_checked('chk_grp_' . $i)) {
$grp_fields .= " , bo_{$i}_subj = '" . $etcs['bo_' . $i . '_subj'] . "' ";
$grp_fields .= " , bo_{$i} = '" . $etcs['bo_' . $i] . "' ";
}
}
if ($grp_fields) {
sql_query(" update {$g5['board_table']} set bo_table = bo_table {$grp_fields} where gr_id = '$gr_id' ");
}
// 모든 게시판 동일 옵션 적용
$all_fields = '';
if (is_checked('chk_all_device')) $all_fields .= " , bo_device = '{$bo_device}' ";
if (is_checked('chk_all_admin')) $all_fields .= " , bo_admin = '{$bo_admin}' ";
if (is_checked('chk_all_list_level')) $all_fields .= " , bo_list_level = '{$bo_list_level}' ";
if (is_checked('chk_all_read_level')) $all_fields .= " , bo_read_level = '{$bo_read_level}' ";
if (is_checked('chk_all_write_level')) $all_fields .= " , bo_write_level = '{$bo_write_level}' ";
if (is_checked('chk_all_reply_level')) $all_fields .= " , bo_reply_level = '{$bo_reply_level}' ";
if (is_checked('chk_all_comment_level')) $all_fields .= " , bo_comment_level = '{$bo_comment_level}' ";
if (is_checked('chk_all_link_level')) $all_fields .= " , bo_link_level = '{$bo_link_level}' ";
if (is_checked('chk_all_upload_level')) $all_fields .= " , bo_upload_level = '{$bo_upload_level}' ";
if (is_checked('chk_all_download_level')) $all_fields .= " , bo_download_level = '{$bo_download_level}' ";
if (is_checked('chk_all_html_level')) $all_fields .= " , bo_html_level = '{$bo_html_level}' ";
if (is_checked('chk_all_count_modify')) $all_fields .= " , bo_count_modify = '{$bo_count_modify}' ";
if (is_checked('chk_all_count_delete')) $all_fields .= " , bo_count_delete = '{$bo_count_delete}' ";
if (is_checked('chk_all_read_point')) $all_fields .= " , bo_read_point = '{$bo_read_point}' ";
if (is_checked('chk_all_write_point')) $all_fields .= " , bo_write_point = '{$bo_write_point}' ";
if (is_checked('chk_all_comment_point')) $all_fields .= " , bo_comment_point = '{$bo_comment_point}' ";
if (is_checked('chk_all_download_point')) $all_fields .= " , bo_download_point = '{$bo_download_point}' ";
if (is_checked('chk_all_category_list')) {
$all_fields .= " , bo_category_list = '{$str_bo_category_list}' ";
$all_fields .= " , bo_use_category = '{$bo_use_category}' ";
}
if (is_checked('chk_all_use_sideview')) $all_fields .= " , bo_use_sideview = '{$bo_use_sideview}' ";
if (is_checked('chk_all_use_file_content')) $all_fields .= " , bo_use_file_content = '{$bo_use_file_content}' ";
if (is_checked('chk_all_use_secret')) $all_fields .= " , bo_use_secret = '{$bo_use_secret}' ";
if (is_checked('chk_all_use_dhtml_editor')) $all_fields .= " , bo_use_dhtml_editor = '{$bo_use_dhtml_editor}' ";
if (is_checked('chk_all_select_editor')) $all_fields .= " , bo_select_editor = '{$bo_select_editor}' ";
if (is_checked('chk_all_use_rss_view')) $all_fields .= " , bo_use_rss_view = '{$bo_use_rss_view}' ";
if (is_checked('chk_all_use_good')) $all_fields .= " , bo_use_good = '{$bo_use_good}' ";
if (is_checked('chk_all_use_nogood')) $all_fields .= " , bo_use_nogood = '{$bo_use_nogood}' ";
if (is_checked('chk_all_use_name')) $all_fields .= " , bo_use_name = '{$bo_use_name}' ";
if (is_checked('chk_all_use_signature')) $all_fields .= " , bo_use_signature = '{$bo_use_signature}' ";
if (is_checked('chk_all_use_ip_view')) $all_fields .= " , bo_use_ip_view = '{$bo_use_ip_view}' ";
if (is_checked('chk_all_use_list_view')) $all_fields .= " , bo_use_list_view = '{$bo_use_list_view}' ";
if (is_checked('chk_all_use_list_file')) $all_fields .= " , bo_use_list_file = '{$bo_use_list_file}' ";
if (is_checked('chk_all_use_list_content')) $all_fields .= " , bo_use_list_content = '{$bo_use_list_content}' ";
if (is_checked('chk_all_use_email')) $all_fields .= " , bo_use_email = '{$bo_use_email}' ";
if (is_checked('chk_all_use_cert')) $all_fields .= " , bo_use_cert = '{$bo_use_cert}' ";
if (is_checked('chk_all_use_sns')) $all_fields .= " , bo_use_sns = '{$bo_use_sns}' ";
if (is_checked('chk_all_use_captcha')) $all_fields .= " , bo_use_captcha = '{$bo_use_captcha}' ";
if (is_checked('chk_all_skin')) $all_fields .= " , bo_skin = '{$bo_skin}' ";
if (is_checked('chk_all_mobile_skin')) $all_fields .= " , bo_mobile_skin = '{$bo_mobile_skin}' ";
if (is_checked('chk_all_gallery_cols')) $all_fields .= " , bo_gallery_cols = '{$bo_gallery_cols}' ";
if (is_checked('chk_all_gallery_width')) $all_fields .= " , bo_gallery_width = '{$bo_gallery_width}' ";
if (is_checked('chk_all_gallery_height')) $all_fields .= " , bo_gallery_height = '{$bo_gallery_height}' ";
if (is_checked('chk_all_mobile_gallery_width')) $all_fields .= " , bo_mobile_gallery_width = '{$bo_mobile_gallery_width}' ";
if (is_checked('chk_all_mobile_gallery_height')) $all_fields .= " , bo_mobile_gallery_height = '{$bo_mobile_gallery_height}' ";
if (is_checked('chk_all_table_width')) $all_fields .= " , bo_table_width = '{$bo_table_width}' ";
if (is_checked('chk_all_page_rows')) $all_fields .= " , bo_page_rows = '{$bo_page_rows}' ";
if (is_checked('chk_all_mobile_page_rows')) $all_fields .= " , bo_mobile_page_rows = '{$bo_mobile_page_rows}' ";
if (is_checked('chk_all_subject_len')) $all_fields .= " , bo_subject_len = '{$bo_subject_len}' ";
if (is_checked('chk_all_mobile_subject_len')) $all_fields .= " , bo_mobile_subject_len = '{$bo_mobile_subject_len}' ";
if (is_checked('chk_all_new')) $all_fields .= " , bo_new = '{$bo_new}' ";
if (is_checked('chk_all_hot')) $all_fields .= " , bo_hot = '{$bo_hot}' ";
if (is_checked('chk_all_image_width')) $all_fields .= " , bo_image_width = '{$bo_image_width}' ";
if (is_checked('chk_all_reply_order')) $all_fields .= " , bo_reply_order = '{$bo_reply_order}' ";
if (is_checked('chk_all_sort_field')) $all_fields .= " , bo_sort_field = '{$bo_sort_field}' ";
if (is_checked('chk_all_write_min')) $all_fields .= " , bo_write_min = '{$bo_write_min}' ";
if (is_checked('chk_all_write_max')) $all_fields .= " , bo_write_max = '{$bo_write_max}' ";
if (is_checked('chk_all_comment_min')) $all_fields .= " , bo_comment_min = '{$bo_comment_min}' ";
if (is_checked('chk_all_comment_max')) $all_fields .= " , bo_comment_max = '{$bo_comment_max}' ";
if (is_checked('chk_all_upload_count')) $all_fields .= " , bo_upload_count = '{$bo_upload_count}' ";
if (is_checked('chk_all_upload_size')) $all_fields .= " , bo_upload_size = '{$bo_upload_size}' ";
if (is_checked('chk_all_use_reservation_overlap')) $all_fields .= " , bo_use_reservation_overlap = '{$bo_use_reservation_overlap}' ";
if (is_checked('chk_all_reservation_overlap_count')) $all_fields .= " , bo_reservation_overlap_count = '{$bo_reservation_overlap_count}' ";
//최고관리자만 수정가능
if ($is_admin === 'super') {
if (is_checked('chk_all_include_head')) $all_fields .= " , bo_include_head = '{$bo_include_head}' ";
if (is_checked('chk_all_include_tail')) $all_fields .= " , bo_include_tail = '{$bo_include_tail}' ";
if (is_checked('chk_all_content_head')) $all_fields .= " , bo_content_head = '{$bo_content_head}' ";
if (is_checked('chk_all_content_tail')) $all_fields .= " , bo_content_tail = '{$bo_content_tail}' ";
if (is_checked('chk_all_mobile_content_head')) $all_fields .= " , bo_mobile_content_head = '{$bo_mobile_content_head}' ";
if (is_checked('chk_all_mobile_content_tail')) $all_fields .= " , bo_mobile_content_tail = '{$bo_mobile_content_tail}' ";
}
if (is_checked('chk_all_insert_content')) $all_fields .= " , bo_insert_content = '{$bo_insert_content}' ";
if (is_checked('chk_all_use_search')) $all_fields .= " , bo_use_search = '{$bo_use_search}' ";
if (is_checked('chk_all_order')) $all_fields .= " , bo_order = '{$bo_order}' ";
for ($i = 1; $i <= 10; $i++) {
if (is_checked('chk_all_' . $i)) {
$all_fields .= " , bo_{$i}_subj = '" . $etcs['bo_' . $i . '_subj'] . "' ";
$all_fields .= " , bo_{$i} = '" . $etcs['bo_' . $i] . "' ";
}
}
if ($all_fields) {
sql_query(" update {$g5['board_table']} set bo_table = bo_table {$all_fields} ");
}
delete_cache_latest($bo_table);
if (function_exists('get_admin_captcha_by')) {
get_admin_captcha_by('remove');
}
run_event('admin_board_form_update', $bo_table, $w);
goto_url("./board_form.php?w=u&bo_table={$bo_table}&amp;{$qstr}");
+234
View File
@@ -0,0 +1,234 @@
<?php
$sub_menu = "300100";
require_once './_common.php';
auth_check_menu($auth, $sub_menu, 'r');
$sql_common = " from {$g5['board_table']} a ";
$sql_search = " where (1) ";
if ($is_admin != "super") {
$sql_common .= " , {$g5['group_table']} b ";
$sql_search .= " and (a.gr_id = b.gr_id and b.gr_admin = '{$member['mb_id']}') ";
}
if ($stx) {
$sql_search .= " and ( ";
switch ($sfl) {
case "bo_table":
$sql_search .= " ($sfl like '$stx%') ";
break;
case "a.gr_id":
$sql_search .= " ($sfl = '$stx') ";
break;
default:
$sql_search .= " ($sfl like '%$stx%') ";
break;
}
$sql_search .= " ) ";
}
if (!$sst) {
$sst = "a.gr_id, a.bo_table";
$sod = "asc";
}
$sql_order = " order by $sst $sod ";
$sql = " select count(*) as cnt {$sql_common} {$sql_search} {$sql_order} ";
$row = sql_fetch($sql);
$total_count = $row['cnt'];
$rows = $config['cf_page_rows'];
$total_page = ceil($total_count / $rows); // 전체 페이지 계산
if ($page < 1) {
$page = 1; // 페이지가 없으면 첫 페이지 (1 페이지)
}
$from_record = ($page - 1) * $rows; // 시작 열을 구함
$sql = " select * {$sql_common} {$sql_search} {$sql_order} limit {$from_record}, {$rows} ";
$result = sql_query($sql);
$listall = '<a href="' . $_SERVER['SCRIPT_NAME'] . '" class="ov_listall">전체목록</a>';
$g5['title'] = '게시판관리';
require_once './admin.head.php';
$colspan = 15;
?>
<div class="local_ov01 local_ov">
<?php echo $listall ?>
<span class="btn_ov01"><span class="ov_txt">생성된 게시판수</span><span class="ov_num"> <?php echo number_format($total_count) ?>개</span></span>
</div>
<form name="fsearch" id="fsearch" class="local_sch01 local_sch" method="get">
<label for="sfl" class="sound_only">검색대상</label>
<select name="sfl" id="sfl">
<option value="bo_table" <?php echo get_selected($sfl, "bo_table", true); ?>>TABLE</option>
<option value="bo_subject" <?php echo get_selected($sfl, "bo_subject"); ?>>제목</option>
<option value="a.gr_id" <?php echo get_selected($sfl, "a.gr_id"); ?>>그룹ID</option>
</select>
<label for="stx" class="sound_only">검색어<strong class="sound_only"> 필수</strong></label>
<input type="text" name="stx" value="<?php echo $stx ?>" id="stx" required class="required frm_input">
<input type="submit" value="검색" class="btn_submit">
</form>
<form name="fboardlist" id="fboardlist" action="./board_list_update.php" onsubmit="return fboardlist_submit(this);" method="post">
<input type="hidden" name="sst" value="<?php echo $sst ?>">
<input type="hidden" name="sod" value="<?php echo $sod ?>">
<input type="hidden" name="sfl" value="<?php echo $sfl ?>">
<input type="hidden" name="stx" value="<?php echo $stx ?>">
<input type="hidden" name="page" value="<?php echo $page ?>">
<input type="hidden" name="token" value="<?php echo isset($token) ? $token : ''; ?>">
<div class="tbl_head01 tbl_wrap">
<table>
<caption><?php echo $g5['title']; ?> 목록</caption>
<thead>
<tr>
<th scope="col">
<label for="chkall" class="sound_only">게시판 전체</label>
<input type="checkbox" name="chkall" value="1" id="chkall" onclick="check_all(this.form)">
</th>
<th scope="col"><?php echo subject_sort_link('a.gr_id') ?>그룹</a></th>
<th scope="col"><?php echo subject_sort_link('bo_table') ?>TABLE</a></th>
<th scope="col"><?php echo subject_sort_link('bo_skin', '', 'desc') ?>스킨</a></th>
<th scope="col"><?php echo subject_sort_link('bo_mobile_skin', '', 'desc') ?>모바일<br>스킨</a></th>
<th scope="col"><?php echo subject_sort_link('bo_subject') ?>제목</a></th>
<th scope="col">읽기P<span class="sound_only">포인트</span></th>
<th scope="col">쓰기P<span class="sound_only">포인트</span></th>
<th scope="col">댓글P<span class="sound_only">포인트</span></th>
<th scope="col">다운P<span class="sound_only">포인트</span></th>
<th scope="col"><?php echo subject_sort_link('bo_use_sns') ?>SNS<br>사용</a></th>
<th scope="col"><?php echo subject_sort_link('bo_use_search') ?>검색<br>사용</a></th>
<th scope="col"><?php echo subject_sort_link('bo_order') ?>출력<br>순서</a></th>
<th scope="col">접속기기</th>
<th scope="col">관리</th>
</tr>
</thead>
<tbody>
<?php
for ($i = 0; $row = sql_fetch_array($result); $i++) {
$one_update = '<a href="./board_form.php?w=u&amp;bo_table=' . $row['bo_table'] . '&amp;' . $qstr . '" class="btn btn_03">수정</a>';
$one_copy = '<a href="./board_copy.php?bo_table=' . $row['bo_table'] . '" class="board_copy btn btn_02" target="win_board_copy">복사</a>';
$bg = 'bg' . ($i % 2);
?>
<tr class="<?php echo $bg; ?>">
<td class="td_chk">
<label for="chk_<?php echo $i; ?>" class="sound_only"><?php echo get_text($row['bo_subject']) ?></label>
<input type="checkbox" name="chk[]" value="<?php echo $i ?>" id="chk_<?php echo $i ?>">
</td>
<td>
<?php if ($is_admin == 'super') { ?>
<?php echo get_group_select("gr_id[$i]", $row['gr_id']) ?>
<?php } else { ?>
<input type="hidden" name="gr_id[<?php echo $i ?>]" value="<?php echo $row['gr_id'] ?>"><?php echo $row['gr_subject'] ?>
<?php } ?>
</td>
<td>
<input type="hidden" name="board_table[<?php echo $i ?>]" value="<?php echo $row['bo_table'] ?>">
<a href="<?php echo get_pretty_url($row['bo_table']) ?>"><?php echo $row['bo_table'] ?></a>
</td>
<td>
<label for="bo_skin_<?php echo $i; ?>" class="sound_only">스킨</label>
<?php echo get_skin_select('board', 'bo_skin_' . $i, "bo_skin[$i]", $row['bo_skin']); ?>
</td>
<td>
<label for="bo_mobile_skin_<?php echo $i; ?>" class="sound_only">모바일 스킨</label>
<?php echo get_mobile_skin_select('board', 'bo_mobile_skin_' . $i, "bo_mobile_skin[$i]", $row['bo_mobile_skin']); ?>
</td>
<td>
<label for="bo_subject_<?php echo $i; ?>" class="sound_only">게시판 제목<strong class="sound_only"> 필수</strong></label>
<input type="text" name="bo_subject[<?php echo $i ?>]" value="<?php echo get_text($row['bo_subject']) ?>" id="bo_subject_<?php echo $i ?>" required class="required tbl_input bo_subject full_input" size="10">
</td>
<td class="td_numsmall">
<label for="bo_read_point_<?php echo $i; ?>" class="sound_only">읽기 포인트</label>
<input type="text" name="bo_read_point[<?php echo $i ?>]" value="<?php echo $row['bo_read_point'] ?>" id="bo_read_point_<?php echo $i; ?>" class="tbl_input" size="2">
</td>
<td class="td_numsmall">
<label for="bo_write_point_<?php echo $i; ?>" class="sound_only">쓰기 포인트</label>
<input type="text" name="bo_write_point[<?php echo $i ?>]" value="<?php echo $row['bo_write_point'] ?>" id="bo_write_point_<?php echo $i; ?>" class="tbl_input" size="2">
</td>
<td class="td_numsmall">
<label for="bo_comment_point_<?php echo $i; ?>" class="sound_only">댓글 포인트</label>
<input type="text" name="bo_comment_point[<?php echo $i ?>]" value="<?php echo $row['bo_comment_point'] ?>" id="bo_comment_point_<?php echo $i; ?>" class="tbl_input" size="2">
</td>
<td class="td_numsmall">
<label for="bo_download_point_<?php echo $i; ?>" class="sound_only">다운<br>포인트</label>
<input type="text" name="bo_download_point[<?php echo $i ?>]" value="<?php echo $row['bo_download_point'] ?>" id="bo_download_point_<?php echo $i; ?>" class="tbl_input" size="2">
</td>
<td class="td_numsmall">
<label for="bo_use_sns_<?php echo $i; ?>" class="sound_only">SNS<br>사용</label>
<input type="checkbox" name="bo_use_sns[<?php echo $i ?>]" value="1" id="bo_use_sns_<?php echo $i ?>" <?php echo $row['bo_use_sns'] ? "checked" : "" ?>>
</td>
<td class="td_numsmall">
<label for="bo_use_search_<?php echo $i; ?>" class="sound_only">검색<br>사용</label>
<input type="checkbox" name="bo_use_search[<?php echo $i ?>]" value="1" id="bo_use_search_<?php echo $i ?>" <?php echo $row['bo_use_search'] ? "checked" : "" ?>>
</td>
<td class="td_numsmall">
<label for="bo_order_<?php echo $i; ?>" class="sound_only">출력<br>순서</label>
<input type="text" name="bo_order[<?php echo $i ?>]" value="<?php echo $row['bo_order'] ?>" id="bo_order_<?php echo $i ?>" class="tbl_input" size="2">
</td>
<td class="td_mngsmall">
<label for="bo_device_<?php echo $i; ?>" class="sound_only">접속기기</label>
<select name="bo_device[<?php echo $i ?>]" id="bo_device_<?php echo $i ?>">
<option value="both" <?php echo get_selected($row['bo_device'], 'both', true); ?>>모두</option>
<option value="pc" <?php echo get_selected($row['bo_device'], 'pc'); ?>>PC</option>
<option value="mobile" <?php echo get_selected($row['bo_device'], 'mobile'); ?>>모바일</option>
</select>
</td>
<td class="td_mng td_mng_m">
<?php echo $one_update ?>
<?php echo $one_copy ?>
</td>
</tr>
<?php
}
if ($i == 0) {
echo '<tr><td colspan="' . $colspan . '" class="empty_table">자료가 없습니다.</td></tr>';
}
?>
</tbody>
</table>
</div>
<div class="btn_fixed_top">
<input type="submit" name="act_button" value="선택수정" onclick="document.pressed=this.value" class="btn_02 btn">
<?php if ($is_admin == 'super') { ?>
<input type="submit" name="act_button" value="선택삭제" onclick="document.pressed=this.value" class="btn_02 btn">
<a href="./board_form.php" id="bo_add" class="btn_01 btn">게시판 추가</a>
<?php } ?>
</div>
</form>
<?php echo get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, $_SERVER['SCRIPT_NAME'] . '?' . $qstr . '&amp;page='); ?>
<script>
function fboardlist_submit(f) {
if (!is_checked("chk[]")) {
alert(document.pressed + " 하실 항목을 하나 이상 선택하세요.");
return false;
}
if (document.pressed == "선택삭제") {
if (!confirm("선택한 자료를 정말 삭제하시겠습니까?")) {
return false;
}
}
return true;
}
$(function() {
$(".board_copy").click(function() {
window.open(this.href, "win_board_copy", "left=100,top=100,width=550,height=450");
return false;
});
});
</script>
<?php
require_once './admin.tail.php';
+96
View File
@@ -0,0 +1,96 @@
<?php
$sub_menu = "300100";
require_once './_common.php';
check_demo();
$post_count_chk = (isset($_POST['chk']) && is_array($_POST['chk'])) ? count($_POST['chk']) : 0;
$chk = (isset($_POST['chk']) && is_array($_POST['chk'])) ? $_POST['chk'] : array();
$act_button = isset($_POST['act_button']) ? strip_tags($_POST['act_button']) : '';
$board_table = (isset($_POST['board_table']) && is_array($_POST['board_table'])) ? $_POST['board_table'] : array();
if (!$post_count_chk) {
alert($act_button . " 하실 항목을 하나 이상 체크하세요.");
}
check_admin_token();
if ($act_button === "선택수정") {
auth_check_menu($auth, $sub_menu, 'w');
for ($i = 0; $i < $post_count_chk; $i++) {
// 실제 번호를 넘김
$k = isset($_POST['chk'][$i]) ? (int) $_POST['chk'][$i] : 0;
$post_gr_id = isset($_POST['gr_id'][$k]) ? clean_xss_tags($_POST['gr_id'][$k], 1, 1) : '';
$post_bo_device = isset($_POST['bo_device'][$k]) ? clean_xss_tags($_POST['bo_device'][$k], 1, 1) : '';
$post_bo_skin = isset($_POST['bo_skin'][$k]) ? clean_xss_tags($_POST['bo_skin'][$k], 1, 1) : '';
$post_bo_mobile_skin = isset($_POST['bo_mobile_skin'][$k]) ? clean_xss_tags($_POST['bo_mobile_skin'][$k], 1, 1) : '';
$post_bo_read_point = isset($_POST['bo_read_point'][$k]) ? clean_xss_tags($_POST['bo_read_point'][$k], 1, 1) : '';
$post_bo_write_point = isset($_POST['bo_write_point'][$k]) ? clean_xss_tags($_POST['bo_write_point'][$k], 1, 1) : '';
$post_bo_comment_point = isset($_POST['bo_comment_point'][$k]) ? clean_xss_tags($_POST['bo_comment_point'][$k], 1, 1) : '';
$post_bo_download_point = isset($_POST['bo_download_point'][$k]) ? clean_xss_tags($_POST['bo_download_point'][$k], 1, 1) : '';
$post_bo_use_search = isset($_POST['bo_use_search'][$k]) ? clean_xss_tags($_POST['bo_use_search'][$k], 1, 1) : '';
$post_bo_use_sns = isset($_POST['bo_use_sns'][$k]) ? clean_xss_tags($_POST['bo_use_sns'][$k], 1, 1) : '';
$post_bo_order = isset($_POST['bo_order'][$k]) ? clean_xss_tags($_POST['bo_order'][$k], 1, 1) : '';
$post_board_table = isset($_POST['board_table'][$k]) ? clean_xss_tags($_POST['board_table'][$k], 1, 1) : '';
if ($is_admin != 'super') {
$sql = " select count(*) as cnt from {$g5['board_table']} a, {$g5['group_table']} b
where a.gr_id = '" . sql_real_escape_string($post_gr_id) . "'
and a.gr_id = b.gr_id
and b.gr_admin = '{$member['mb_id']}' ";
$row = sql_fetch($sql);
if (!$row['cnt']) {
alert('최고관리자가 아닌 경우 다른 관리자의 게시판(' . $board_table[$k] . ')은 수정이 불가합니다.');
}
}
$p_bo_subject = is_array($_POST['bo_subject']) ? strip_tags(clean_xss_attributes($_POST['bo_subject'][$k])) : '';
$sql = " update {$g5['board_table']}
set gr_id = '" . sql_real_escape_string($post_gr_id) . "',
bo_subject = '" . $p_bo_subject . "',
bo_device = '" . sql_real_escape_string($post_bo_device) . "',
bo_skin = '" . sql_real_escape_string($post_bo_skin) . "',
bo_mobile_skin = '" . sql_real_escape_string($post_bo_mobile_skin) . "',
bo_read_point = '" . sql_real_escape_string($post_bo_read_point) . "',
bo_write_point = '" . sql_real_escape_string($post_bo_write_point) . "',
bo_comment_point = '" . sql_real_escape_string($post_bo_comment_point) . "',
bo_download_point = '" . sql_real_escape_string($post_bo_download_point) . "',
bo_use_search = '" . sql_real_escape_string($post_bo_use_search) . "',
bo_use_sns = '" . sql_real_escape_string($post_bo_use_sns) . "',
bo_order = '" . sql_real_escape_string($post_bo_order) . "'
where bo_table = '" . sql_real_escape_string($post_board_table) . "' ";
sql_query($sql);
}
} elseif ($act_button === "선택삭제") {
if ($is_admin != 'super') {
alert('게시판 삭제는 최고관리자만 가능합니다.');
}
auth_check_menu($auth, $sub_menu, 'd');
// _BOARD_DELETE_ 상수를 선언해야 board_delete.inc.php 가 정상 작동함
/* 확인필요 22.05.27
A file should declare new symbols (classes, functions, constants, etc.) and cause no other side effects,
or it should execute logic with side effects, but should not do both.*/
define('_BOARD_DELETE_', true);
for ($i = 0; $i < $post_count_chk; $i++) {
// 실제 번호를 넘김
$k = isset($_POST['chk'][$i]) ? (int) $_POST['chk'][$i] : 0;
// include 전에 $bo_table 값을 반드시 넘겨야 함
$tmp_bo_table = isset($_POST['board_table'][$k]) ? trim(clean_xss_tags($_POST['board_table'][$k], 1, 1)) : '';
if (preg_match("/^[A-Za-z0-9_]+$/", $tmp_bo_table)) {
include './board_delete.inc.php';
}
}
}
run_event('admin_board_list_update', $act_button, $chk, $board_table, $qstr);
goto_url('./board_list.php?' . $qstr);
+53
View File
@@ -0,0 +1,53 @@
<?php
$sub_menu = '300100';
require_once './_common.php';
auth_check_menu($auth, $sub_menu, 'w');
if (!$board['bo_table']) {
alert('존재하지 않는 게시판입니다.');
}
$g5['title'] = $board['bo_subject'] . ' 게시판 썸네일 삭제';
require_once './admin.head.php';
?>
<div class="local_desc02 local_desc">
<p>
완료 메세지가 나오기 전에 프로그램의 실행을 중지하지 마십시오.
</p>
</div>
<?php
$dir = G5_DATA_PATH . '/file/' . $bo_table;
$cnt = 0;
if (is_dir($dir)) {
echo '<ul>';
$files = glob($dir . '/thumb-*');
if (is_array($files)) {
foreach ($files as $thumbnail) {
$cnt++;
@unlink($thumbnail);
echo '<li>' . $thumbnail . '</li>' . PHP_EOL;
flush();
if (($cnt % 10) == 0) {
echo PHP_EOL;
}
}
}
echo '<li>완료됨</li></ul>' . PHP_EOL;
echo '<div class="local_desc01 local_desc"><p><strong>썸네일 ' . $cnt . '건의 삭제 완료됐습니다.</strong></p></div>' . PHP_EOL;
} else {
echo '<p>첨부파일 디렉토리가 존재하지 않습니다.</p>';
}
?>
<div class="btn_confirm01 btn_confirm"><a href="./board_form.php?w=u&amp;bo_table=<?php echo $bo_table; ?>&amp;<?php echo $qstr; ?>">게시판 수정으로 돌아가기</a></div>
<?php
require_once './admin.tail.php';
+171
View File
@@ -0,0 +1,171 @@
<?php
$sub_menu = "300200";
require_once './_common.php';
auth_check_menu($auth, $sub_menu, 'w');
if ($is_admin != 'super' && $w == '') {
alert('최고관리자만 접근 가능합니다.');
}
$html_title = '게시판그룹';
$gr_id_attr = '';
$sound_only = '';
if (!isset($group['gr_id'])) {
$group['gr_id'] = '';
$group['gr_subject'] = '';
$group['gr_device'] = '';
}
$gr = array('gr_use_access' => 0, 'gr_admin' => '');
if ($w == '') {
$gr_id_attr = 'required';
$sound_only = '<strong class="sound_only"> 필수</strong>';
$html_title .= ' 생성';
} elseif ($w == 'u') {
$gr_id_attr = 'readonly';
$gr = sql_fetch(" select * from {$g5['group_table']} where gr_id = '$gr_id' ");
$html_title .= ' 수정';
} else {
alert('제대로 된 값이 넘어오지 않았습니다.');
}
if (!isset($group['gr_device'])) {
sql_query(" ALTER TABLE `{$g5['group_table']}` ADD `gr_device` ENUM('both','pc','mobile') NOT NULL DEFAULT 'both' AFTER `gr_subject` ", false);
}
// 접근회원수
$sql1 = " select count(*) as cnt from {$g5['group_member_table']} where gr_id = '{$gr_id}' ";
$row1 = sql_fetch($sql1);
$group_member_count = $row1['cnt'];
$g5['title'] = $html_title;
require_once './admin.head.php';
?>
<form name="fboardgroup" id="fboardgroup" action="./boardgroup_form_update.php" onsubmit="return fboardgroup_check(this);" method="post" autocomplete="off">
<input type="hidden" name="w" value="<?php echo $w ?>">
<input type="hidden" name="sfl" value="<?php echo $sfl ?>">
<input type="hidden" name="stx" value="<?php echo $stx ?>">
<input type="hidden" name="sst" value="<?php echo $sst ?>">
<input type="hidden" name="sod" value="<?php echo $sod ?>">
<input type="hidden" name="page" value="<?php echo $page ?>">
<input type="hidden" name="token" value="">
<div class="tbl_frm01 tbl_wrap">
<table>
<caption><?php echo $g5['title']; ?></caption>
<colgroup>
<col class="grid_4">
<col>
</colgroup>
<tbody>
<tr>
<th scope="row"><label for="gr_id">그룹 ID<?php echo $sound_only ?></label></th>
<td><input type="text" name="gr_id" value="<?php echo $group['gr_id'] ?>" id="gr_id" <?php echo $gr_id_attr; ?> class="<?php echo $gr_id_attr; ?> alnum_ frm_input" maxlength="10">
<?php
if ($w == '') {
echo '영문자, 숫자, _ 만 가능 (공백없이)';
} else {
echo '<a href="' . G5_BBS_URL . '/group.php?gr_id=' . $group['gr_id'] . '" class="btn_frmline">게시판그룹 바로가기</a>';
}
?>
</td>
</tr>
<tr>
<th scope="row"><label for="gr_subject">그룹 제목<strong class="sound_only"> 필수</strong></label></th>
<td>
<input type="text" name="gr_subject" value="<?php echo get_text($group['gr_subject']) ?>" id="gr_subject" required class="required frm_input" size="80">
<?php
if ($w == 'u') {
echo '<a href="./board_form.php?gr_id=' . $gr_id . '" class="btn_frmline">게시판생성</a>';
}
?>
</td>
</tr>
<tr>
<th scope="row"><label for="gr_device">접속기기</label></th>
<td>
<?php echo help("PC 와 모바일 사용을 구분합니다.") ?>
<select id="gr_device" name="gr_device">
<option value="both" <?php echo get_selected($group['gr_device'], 'both', true); ?>>PC와 모바일에서 모두 사용</option>
<option value="pc" <?php echo get_selected($group['gr_device'], 'pc'); ?>>PC 전용</option>
<option value="mobile" <?php echo get_selected($group['gr_device'], 'mobile'); ?>>모바일 전용</option>
</select>
</td>
</tr>
<tr>
<th scope="row">
<?php
if ($is_admin == 'super') {
echo '<label for="gr_admin">그룹 관리자</label>';
} else {
echo '그룹 관리자';
}
?>
</th>
<td>
<?php
if ($is_admin == 'super') {
echo '<input type="text" id="gr_admin" name="gr_admin" class="frm_input" value="' . $gr['gr_admin'] . '" maxlength="20">';
} else {
echo '<input type="hidden" id="gr_admin" name="gr_admin" value="' . $gr['gr_admin'] . '">' . $gr['gr_admin'];
}
?>
</td>
</tr>
<tr>
<th scope="row"><label for="gr_use_access">접근회원사용</label></th>
<td>
<?php echo help("사용에 체크하시면 이 그룹에 속한 게시판은 접근가능한 회원만 접근이 가능합니다.") ?>
<input type="checkbox" name="gr_use_access" value="1" id="gr_use_access" <?php echo $gr['gr_use_access'] ? 'checked' : ''; ?>>
사용
</td>
</tr>
<tr>
<th scope="row">접근회원수</th>
<td>
<?php
echo '<a href="./boardgroupmember_list.php?gr_id=' . $gr_id . '">' . $group_member_count . '</a>';
?>
</td>
</tr>
<?php for ($i = 1; $i <= 10; $i++) { ?>
<tr>
<th scope="row">여분필드<?php echo $i ?></th>
<td class="td_extra">
<label for="gr_<?php echo $i ?>_subj">여분필드 <?php echo $i ?> 제목</label>
<input type="text" name="gr_<?php echo $i ?>_subj" value="<?php echo isset($group['gr_' . $i . '_subj']) ? get_text($group['gr_' . $i . '_subj']) : ''; ?>" id="gr_<?php echo $i ?>_subj" class="frm_input">
<label for="gr_<?php echo $i ?>">여분필드 <?php echo $i ?> 내용</label>
<input type="text" name="gr_<?php echo $i ?>" value="<?php echo isset($gr['gr_' . $i]) ? get_sanitize_input($gr['gr_' . $i]) : ''; ?>" id="gr_<?php echo $i ?>" class="frm_input">
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<div class="btn_fixed_top">
<a href="./boardgroup_list.php?<?php echo $qstr ?>" class="btn btn_02">목록</a>
<input type="submit" class="btn_submit btn" accesskey="s" value="확인">
</div>
</form>
<div class="local_desc01 local_desc">
<p>
게시판을 생성하시려면 1개 이상의 게시판그룹이 필요합니다.<br>
게시판그룹을 이용하시면 더 효과적으로 게시판을 관리할 수 있습니다.
</p>
</div>
<script>
function fboardgroup_check(f) {
f.action = './boardgroup_form_update.php';
return true;
}
</script>
<?php
require_once './admin.tail.php';
+99
View File
@@ -0,0 +1,99 @@
<?php
$sub_menu = "300200";
require_once './_common.php';
if ($w == 'u') {
check_demo();
}
auth_check_menu($auth, $sub_menu, 'w');
if ($is_admin != 'super' && $w == '') {
alert('최고관리자만 접근 가능합니다.');
}
check_admin_token();
$gr_id = isset($_POST['gr_id']) ? $_POST['gr_id'] : '';
if (!preg_match("/^([A-Za-z0-9_]{1,10})$/", $gr_id)) {
alert('그룹 ID는 공백없이 영문자, 숫자, _ 만 사용 가능합니다. (10자 이내)');
}
if (empty($gr_subject)) {
alert('그룹 제목을 입력하세요.');
}
$posts = array();
$check_keys = array(
'gr_subject' => '',
'gr_device' => '',
'gr_admin' => '',
);
for ($i = 1; $i <= 10; $i++) {
$check_keys['gr_' . $i . '_subj'] = isset($_POST['gr_' . $i . '_subj']) ? $_POST['gr_' . $i . '_subj'] : '';
$check_keys['gr_' . $i] = isset($_POST['gr_' . $i]) ? $_POST['gr_' . $i] : '';
}
foreach ($check_keys as $key => $value) {
if ($key === 'gr_subject') {
$posts[$key] = isset($_POST[$key]) ? strip_tags(clean_xss_attributes($_POST[$key])) : '';
} else {
$posts[$key] = isset($_POST[$key]) ? $_POST[$key] : '';
}
}
$sql_common = " gr_subject = '{$posts['gr_subject']}',
gr_device = '{$posts['gr_device']}',
gr_admin = '{$posts['gr_admin']}',
gr_1_subj = '{$posts['gr_1_subj']}',
gr_2_subj = '{$posts['gr_2_subj']}',
gr_3_subj = '{$posts['gr_3_subj']}',
gr_4_subj = '{$posts['gr_4_subj']}',
gr_5_subj = '{$posts['gr_5_subj']}',
gr_6_subj = '{$posts['gr_6_subj']}',
gr_7_subj = '{$posts['gr_7_subj']}',
gr_8_subj = '{$posts['gr_8_subj']}',
gr_9_subj = '{$posts['gr_9_subj']}',
gr_10_subj = '{$posts['gr_10_subj']}',
gr_1 = '{$posts['gr_1']}',
gr_2 = '{$posts['gr_2']}',
gr_3 = '{$posts['gr_3']}',
gr_4 = '{$posts['gr_4']}',
gr_5 = '{$posts['gr_5']}',
gr_6 = '{$posts['gr_6']}',
gr_7 = '{$posts['gr_7']}',
gr_8 = '{$posts['gr_8']}',
gr_9 = '{$posts['gr_9']}',
gr_10 = '{$posts['gr_10']}' ";
if (isset($_POST['gr_use_access'])) {
$sql_common .= ", gr_use_access = '{$_POST['gr_use_access']}' ";
} else {
$sql_common .= ", gr_use_access = '' ";
}
if ($w == '') {
$sql = " select count(*) as cnt from {$g5['group_table']} where gr_id = '{$gr_id}' ";
$row = sql_fetch($sql);
if ($row['cnt']) {
alert('이미 존재하는 그룹 ID 입니다.');
}
$sql = " insert into {$g5['group_table']}
set gr_id = '{$gr_id}',
{$sql_common} ";
sql_query($sql);
} elseif ($w == "u") {
$sql = " update {$g5['group_table']}
set {$sql_common}
where gr_id = '{$gr_id}' ";
sql_query($sql);
} else {
alert('제대로 된 값이 넘어오지 않았습니다.');
}
run_event('admin_boardgroup_form_update', $gr_id, $w);
goto_url('./boardgroup_form.php?w=u&amp;gr_id=' . $gr_id . '&amp;' . $qstr);
+212
View File
@@ -0,0 +1,212 @@
<?php
$sub_menu = "300200";
require_once './_common.php';
auth_check_menu($auth, $sub_menu, 'r');
if (!isset($group['gr_device'])) {
// 게시판 그룹 사용 필드 추가
// both : pc, mobile 둘다 사용
// pc : pc 전용 사용
// mobile : mobile 전용 사용
// none : 사용 안함
sql_query(" ALTER TABLE `{$g5['group_table']}` ADD `gr_device` ENUM( 'both', 'pc', 'mobile' ) NOT NULL DEFAULT 'both' AFTER `gr_subject` ", false);
}
$sql_common = " from {$g5['group_table']} ";
$sql_search = " where (1) ";
if ($is_admin != 'super') {
$sql_search .= " and (gr_admin = '{$member['mb_id']}') ";
}
if ($stx) {
$sql_search .= " and ( ";
switch ($sfl) {
case "gr_id":
case "gr_admin":
$sql_search .= " ({$sfl} = '{$stx}') ";
break;
default:
$sql_search .= " ({$sfl} like '%{$stx}%') ";
break;
}
$sql_search .= " ) ";
}
if ($sst) {
$sql_order = " order by {$sst} {$sod} ";
} else {
$sql_order = " order by gr_id asc ";
}
$sql = " select count(*) as cnt {$sql_common} {$sql_search} {$sql_order} ";
$row = sql_fetch($sql);
$total_count = $row['cnt'];
$rows = $config['cf_page_rows'];
$total_page = ceil($total_count / $rows); // 전체 페이지 계산
if ($page < 1) {
$page = 1; // 페이지가 없으면 첫 페이지 (1 페이지)
}
$from_record = ($page - 1) * $rows; // 시작 열을 구함
$sql = " select * {$sql_common} {$sql_search} {$sql_order} limit {$from_record}, {$rows} ";
$result = sql_query($sql);
$listall = '<a href="' . $_SERVER['SCRIPT_NAME'] . '" class="ov_listall">처음</a>';
$g5['title'] = '게시판그룹설정';
require_once './admin.head.php';
$colspan = 10;
?>
<div class="local_ov01 local_ov">
<?php echo $listall ?>
<span class="btn_ov01"><span class="ov_txt">전체그룹</span><span class="ov_num"> <?php echo number_format($total_count) ?>개</span></span>
</div>
<form name="fsearch" id="fsearch" class="local_sch01 local_sch" method="get">
<label for="sfl" class="sound_only">검색대상</label>
<select name="sfl" id="sfl">
<option value="gr_subject" <?php echo get_selected($sfl, "gr_subject"); ?>>제목</option>
<option value="gr_id" <?php echo get_selected($sfl, "gr_id"); ?>>ID</option>
<option value="gr_admin" <?php echo get_selected($sfl, "gr_admin"); ?>>그룹관리자</option>
</select>
<label for="stx" class="sound_only">검색어<strong class="sound_only"> 필수</strong></label>
<input type="text" name="stx" id="stx" value="<?php echo $stx ?>" required class="required frm_input">
<input type="submit" value="검색" class="btn_submit">
</form>
<form name="fboardgrouplist" id="fboardgrouplist" action="./boardgroup_list_update.php" onsubmit="return fboardgrouplist_submit(this);" method="post">
<input type="hidden" name="sst" value="<?php echo $sst ?>">
<input type="hidden" name="sod" value="<?php echo $sod ?>">
<input type="hidden" name="sfl" value="<?php echo $sfl ?>">
<input type="hidden" name="stx" value="<?php echo $stx ?>">
<input type="hidden" name="page" value="<?php echo $page ?>">
<input type="hidden" name="token" value="">
<div class="tbl_head01 tbl_wrap">
<table>
<caption><?php echo $g5['title']; ?> 목록</caption>
<thead>
<tr>
<th scope="col">
<label for="chkall" class="sound_only">그룹 전체</label>
<input type="checkbox" name="chkall" value="1" id="chkall" onclick="check_all(this.form)">
</th>
<th scope="col"><?php echo subject_sort_link('gr_id') ?>그룹아이디</a></th>
<th scope="col"><?php echo subject_sort_link('gr_subject') ?>제목</a></th>
<th scope="col"><?php echo subject_sort_link('gr_admin') ?>그룹관리자</a></th>
<th scope="col">게시판</th>
<th scope="col">접근<br>사용</th>
<th scope="col">접근<br>회원수</th>
<th scope="col"><?php echo subject_sort_link('gr_order') ?>출력<br>순서</a></th>
<th scope="col">접속기기</th>
<th scope="col">관리</th>
</tr>
</thead>
<tbody>
<?php
for ($i = 0; $row = sql_fetch_array($result); $i++) {
// 접근회원수
$sql1 = " select count(*) as cnt from {$g5['group_member_table']} where gr_id = '{$row['gr_id']}' ";
$row1 = sql_fetch($sql1);
// 게시판수
$sql2 = " select count(*) as cnt from {$g5['board_table']} where gr_id = '{$row['gr_id']}' ";
$row2 = sql_fetch($sql2);
$s_upd = '<a href="./boardgroup_form.php?' . $qstr . '&amp;w=u&amp;gr_id=' . $row['gr_id'] . '" class="btn_03 btn">수정</a>';
$bg = 'bg' . ($i % 2);
?>
<tr class="<?php echo $bg; ?>">
<td class="td_chk">
<input type="hidden" name="group_id[<?php echo $i ?>]" value="<?php echo $row['gr_id'] ?>">
<label for="chk_<?php echo $i; ?>" class="sound_only"><?php echo get_text($row['gr_subject']); ?> 그룹</label>
<input type="checkbox" name="chk[]" value="<?php echo $i ?>" id="chk_<?php echo $i ?>">
</td>
<td class="td_left"><a href="<?php echo G5_BBS_URL ?>/group.php?gr_id=<?php echo $row['gr_id'] ?>"><?php echo $row['gr_id'] ?></a></td>
<td class="td_input">
<label for="gr_subject_<?php echo $i; ?>" class="sound_only">그룹제목</label>
<input type="text" name="gr_subject[<?php echo $i ?>]" value="<?php echo get_text($row['gr_subject']) ?>" id="gr_subject_<?php echo $i ?>" class="tbl_input">
</td>
<td class="td_mng td_input">
<?php if ($is_admin == 'super') { ?>
<label for="gr_admin_<?php echo $i; ?>" class="sound_only">그룹관리자</label>
<input type="text" name="gr_admin[<?php echo $i ?>]" value="<?php echo get_sanitize_input($row['gr_admin']); ?>" id="gr_admin_<?php echo $i ?>" class="tbl_input" size="10" maxlength="20">
<?php } else { ?>
<input type="hidden" name="gr_admin[<?php echo $i ?>]" value="<?php echo get_sanitize_input($row['gr_admin']); ?>"><?php echo get_text($row['gr_admin']); ?>
<?php } ?>
</td>
<td class="td_num"><a href="./board_list.php?sfl=a.gr_id&amp;stx=<?php echo $row['gr_id'] ?>"><?php echo $row2['cnt'] ?></a></td>
<td class="td_numsmall">
<label for="gr_use_access_<?php echo $i; ?>" class="sound_only">접근회원 사용</label>
<input type="checkbox" name="gr_use_access[<?php echo $i ?>]" <?php echo $row['gr_use_access'] ? 'checked' : '' ?> value="1" id="gr_use_access_<?php echo $i ?>">
</td>
<td class="td_num"><a href="./boardgroupmember_list.php?gr_id=<?php echo $row['gr_id'] ?>"><?php echo $row1['cnt'] ?></a></td>
<td class="td_numsmall">
<label for="gr_order_<?php echo $i; ?>" class="sound_only">메인메뉴 출력순서</label>
<input type="text" name="gr_order[<?php echo $i ?>]" value="<?php echo $row['gr_order'] ?>" id="gr_order_<?php echo $i ?>" class="tbl_input" size="2">
</td>
<td class="td_mng">
<label for="gr_device_<?php echo $i; ?>" class="sound_only">접속기기</label>
<select name="gr_device[<?php echo $i ?>]" id="gr_device_<?php echo $i ?>">
<option value="both" <?php echo get_selected($row['gr_device'], 'both'); ?>>모두</option>
<option value="pc" <?php echo get_selected($row['gr_device'], 'pc'); ?>>PC</option>
<option value="mobile" <?php echo get_selected($row['gr_device'], 'mobile'); ?>>모바일</option>
</select>
</td>
<td class="td_mng td_mng_s"><?php echo $s_upd ?></td>
</tr>
<?php
}
if ($i == 0) {
echo '<tr><td colspan="' . $colspan . '" class="empty_table">자료가 없습니다.</td></tr>';
}
?>
</table>
</div>
<div class="btn_fixed_top">
<input type="submit" name="act_button" onclick="document.pressed=this.value" value="선택수정" class="btn btn_02">
<input type="submit" name="act_button" onclick="document.pressed=this.value" value="선택삭제" class="btn btn_02">
<a href="./boardgroup_form.php" class="btn btn_01">게시판그룹 추가</a>
</div>
</form>
<div class="local_desc01 local_desc">
<p>
접근사용 옵션을 설정하시면 관리자가 지정한 회원만 해당 그룹에 접근할 수 있습니다.<br>
접근사용 옵션은 해당 그룹에 속한 모든 게시판에 적용됩니다.
</p>
</div>
<?php
$pagelist = get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, $_SERVER['SCRIPT_NAME'] . '?' . $qstr . '&amp;page=');
echo $pagelist;
?>
<script>
function fboardgrouplist_submit(f) {
if (!is_checked("chk[]")) {
alert(document.pressed + " 하실 항목을 하나 이상 선택하세요.");
return false;
}
if (document.pressed == "선택삭제") {
if (!confirm("선택한 자료를 정말 삭제하시겠습니까?")) {
return false;
}
}
return true;
}
</script>
<?php
require_once './admin.tail.php';
+58
View File
@@ -0,0 +1,58 @@
<?php
$sub_menu = "300200";
require_once './_common.php';
check_demo();
auth_check_menu($auth, $sub_menu, 'w');
check_admin_token();
$post_chk = isset($_POST['chk']) ? (array) $_POST['chk'] : array();
$post_group_id = isset($_POST['group_id']) ? (array) $_POST['group_id'] : array();
$act_button = isset($_POST['act_button']) ? $_POST['act_button'] : '';
$chk_count = count($post_chk);
if (!$chk_count) {
alert($act_button . '할 게시판그룹을 1개이상 선택해 주세요.');
}
for ($i = 0; $i < $chk_count; $i++) {
$k = isset($post_chk[$i]) ? (int) $post_chk[$i] : 0;
$gr_id = preg_replace('/[^a-z0-9_]/i', '', $post_group_id[$k]);
$gr_subject = isset($_POST['gr_subject'][$k]) ? strip_tags(clean_xss_attributes($_POST['gr_subject'][$k])) : '';
$gr_admin = isset($_POST['gr_admin'][$k]) ? strip_tags(clean_xss_attributes($_POST['gr_admin'][$k])) : '';
$gr_device = isset($_POST['gr_device'][$k]) ? clean_xss_tags($_POST['gr_device'][$k], 1, 1, 10) : '';
$gr_use_access = isset($_POST['gr_use_access'][$k]) ? (int) $_POST['gr_use_access'][$k] : 0;
$gr_order = isset($_POST['gr_order'][$k]) ? (int) $_POST['gr_order'][$k] : 0;
if ($act_button == '선택수정') {
$sql = " update {$g5['group_table']}
set gr_subject = '{$gr_subject}',
gr_device = '" . sql_real_escape_string($gr_device) . "',
gr_admin = '" . sql_real_escape_string($gr_admin) . "',
gr_use_access = '" . $gr_use_access . "',
gr_order = '" . $gr_order . "'
where gr_id = '{$gr_id}' ";
if ($is_admin != 'super') {
$sql .= " and gr_admin = '{$gr_admin}' ";
}
sql_query($sql);
} elseif ($act_button == '선택삭제') {
$row = sql_fetch(" select count(*) as cnt from {$g5['board_table']} where gr_id = '$gr_id' ");
if ($row['cnt']) {
alert("이 그룹에 속한 게시판이 존재하여 게시판 그룹을 삭제할 수 없습니다.\\n\\n이 그룹에 속한 게시판을 먼저 삭제하여 주십시오.", './board_list.php?sfl=gr_id&amp;stx=' . $gr_id);
}
// 그룹 삭제
sql_query(" delete from {$g5['group_table']} where gr_id = '$gr_id' ");
// 그룹접근 회원 삭제
sql_query(" delete from {$g5['group_member_table']} where gr_id = '$gr_id' ");
}
}
run_event('admin_boardgroup_list_update', $act_button, $post_chk, $post_group_id, $qstr);
goto_url('./boardgroup_list.php?' . $qstr);
+134
View File
@@ -0,0 +1,134 @@
<?php
$sub_menu = "300200";
require_once './_common.php';
auth_check_menu($auth, $sub_menu, 'w');
$mb = get_member($mb_id);
$token = isset($token) ? $token : '';
if (!(isset($mb['mb_id']) && $mb['mb_id'])) {
alert('존재하지 않는 회원입니다.');
}
$g5['title'] = '접근가능그룹';
require_once './admin.head.php';
$colspan = 4;
?>
<form name="fboardgroupmember_form" id="fboardgroupmember_form" action="./boardgroupmember_update.php" onsubmit="return boardgroupmember_form_check(this)" method="post">
<input type="hidden" name="mb_id" value="<?php echo $mb['mb_id'] ?>" id="mb_id">
<input type="hidden" name="token" value="" id="token">
<div class="local_ov01 local_ov">
<span class="btn_ov01"><span class="ov_txt"> 아이디</span><span class="ov_num"><?php echo $mb['mb_id'] ?></span></span>
<span class="btn_ov01"><span class="ov_txt"> 이름</span><span class="ov_num"><?php echo get_text($mb['mb_name']); ?></span></span>
<span class="btn_ov01"><span class="ov_txt"> 닉네임</span><span class="ov_num"><?php echo $mb['mb_nick'] ?></span></span>
</div>
<div class="local_cmd01 local_cmd">
<label for="gr_id">그룹지정</label>
<select name="gr_id" id="gr_id">
<option value="">접근가능 그룹을 선택하세요.</option>
<?php
$sql = " select *
from {$g5['group_table']}
where gr_use_access = 1 ";
if ($is_admin != 'super') {
$sql .= " and gr_admin = '{$member['mb_id']}' ";
}
$sql .= " order by gr_id ";
$result = sql_query($sql);
for ($i = 0; $row = sql_fetch_array($result); $i++) {
echo "<option value=\"" . $row['gr_id'] . "\">" . $row['gr_subject'] . "</option>";
}
?>
</select>
<input type="submit" value="선택" class="btn_submit btn" accesskey="s">
</div>
</form>
<form name="fboardgroupmember" id="fboardgroupmember" action="./boardgroupmember_update.php" onsubmit="return fboardgroupmember_submit(this);" method="post">
<input type="hidden" name="sst" value="<?php echo $sst ?>" id="sst">
<input type="hidden" name="sod" value="<?php echo $sod ?>" id="sod">
<input type="hidden" name="sfl" value="<?php echo $sfl ?>" id="sfl">
<input type="hidden" name="stx" value="<?php echo $stx ?>" id="stx">
<input type="hidden" name="page" value="<?php echo $page ?>" id="page">
<input type="hidden" name="token" value="<?php echo get_sanitize_input($token); ?>" id="token">
<input type="hidden" name="mb_id" value="<?php echo $mb['mb_id'] ?>" id="mb_id">
<input type="hidden" name="w" value="d" id="w">
<div class="tbl_head01 tbl_wrap">
<table>
<caption><?php echo $g5['title']; ?> 목록</caption>
<thead>
<tr>
<th scope="col">
<label for="chkall" class="sound_only">접근가능그룹 전체</label>
<input type="checkbox" name="chkall" value="1" id="chkall" onclick="check_all(this.form)">
</th>
<th scope="col">그룹아이디</th>
<th scope="col">그룹</th>
<th scope="col">처리일시</th>
</tr>
</thead>
<tbody>
<?php
$sql = " select * from {$g5['group_member_table']} a, {$g5['group_table']} b
where a.mb_id = '{$mb['mb_id']}'
and a.gr_id = b.gr_id ";
if ($is_admin != 'super') {
$sql .= " and b.gr_admin = '{$member['mb_id']}' ";
}
$sql .= " order by a.gr_id desc ";
$result = sql_query($sql);
for ($i = 0; $row = sql_fetch_array($result); $i++) {
?>
<tr>
<td class="td_chk">
<label for="chk_<?php echo $i; ?>" class="sound_only"><?php echo $row['gr_subject'] ?> 그룹</label>
<input type="checkbox" name="chk[]" value="<?php echo $row['gm_id'] ?>" id="chk_<?php echo $i ?>">
</td>
<td class="td_grid"><a href="<?php echo G5_BBS_URL; ?>/group.php?gr_id=<?php echo $row['gr_id'] ?>"><?php echo $row['gr_id'] ?></a></td>
<td class="td_category"><?php echo $row['gr_subject'] ?></td>
<td class="td_datetime"><?php echo $row['gm_datetime'] ?></td>
</tr>
<?php
}
if ($i == 0) {
echo '<tr><td colspan="' . $colspan . '" class="empty_table">접근가능한 그룹이 없습니다.</td></tr>';
}
?>
</tbody>
</table>
</div>
<div class="btn_list01 btn_list">
<input type="submit" name="" value="선택삭제" class="btn btn_02">
</div>
</form>
<script>
function fboardgroupmember_submit(f) {
if (!is_checked("chk[]")) {
alert("선택삭제 하실 항목을 하나 이상 선택하세요.");
return false;
}
return true;
}
function boardgroupmember_form_check(f) {
if (f.gr_id.value == '') {
alert('접근가능 그룹을 선택하세요.');
return false;
}
return true;
}
</script>
<?php
require_once './admin.tail.php';
+157
View File
@@ -0,0 +1,157 @@
<?php
$sub_menu = "300200";
require_once './_common.php';
auth_check_menu($auth, $sub_menu, 'r');
$gr = get_group($gr_id);
if (!$gr['gr_id']) {
alert('존재하지 않는 그룹입니다.');
}
$sql_common = " from {$g5['group_member_table']} a
left outer join {$g5['member_table']} b on (a.mb_id = b.mb_id) ";
$sql_search = " where gr_id = '{$gr_id}' ";
// 회원아이디로 검색되지 않던 오류를 수정
if (isset($stx) && $stx) {
$sql_search .= " and ( ";
switch ($sfl) {
default:
$sql_search .= " ($sfl like '%$stx%') ";
break;
}
$sql_search .= " ) ";
}
if (!$sst) {
$sst = "gm_datetime";
$sod = "desc";
}
$sql_order = " order by {$sst} {$sod} ";
$sql = " select count(*) as cnt
{$sql_common}
{$sql_search}
{$sql_order} ";
$row = sql_fetch($sql);
$total_count = $row['cnt'];
$rows = $config['cf_page_rows'];
$total_page = ceil($total_count / $rows); // 전체 페이지 계산
if ($page < 1) {
$page = 1; // 페이지가 없으면 첫 페이지 (1 페이지)
}
$from_record = ($page - 1) * $rows; // 시작 열을 구함
$sql = " select *
{$sql_common}
{$sql_search}
{$sql_order}
limit {$from_record}, {$rows} ";
$result = sql_query($sql);
$g5['title'] = $gr['gr_subject'] . ' 그룹 접근가능회원 (그룹아이디:' . $gr['gr_id'] . ')';
require_once './admin.head.php';
$colspan = 7;
?>
<form name="fsearch" id="fsearch" class="local_sch01 local_sch" method="get">
<input type="hidden" name="gr_id" value="<?php echo $gr_id ?>">
<label for="sfl" class="sound_only">검색대상</label>
<select name="sfl" id="sfl">
<option value="a.mb_id" <?php echo get_selected($sfl, "a.mb_id") ?>>회원아이디</option>
</select>
<label for="stx" class="sound_only">검색어<strong class="sound_only"> 필수</strong></label>
<input type="text" name="stx" value="<?php echo $stx ?>" id="stx" required class="required frm_input">
<input type="submit" value="검색" class="btn_submit">
</form>
<form name="fboardgroupmember" id="fboardgroupmember" action="./boardgroupmember_update.php" onsubmit="return fboardgroupmember_submit(this);" method="post">
<input type="hidden" name="sst" value="<?php echo $sst ?>">
<input type="hidden" name="sod" value="<?php echo $sod ?>">
<input type="hidden" name="sfl" value="<?php echo $sfl ?>">
<input type="hidden" name="stx" value="<?php echo $stx ?>">
<input type="hidden" name="page" value="<?php echo $page ?>">
<input type="hidden" name="token" value="<?php echo $token ?>">
<input type="hidden" name="gr_id" value="<?php echo $gr_id ?>">
<input type="hidden" name="w" value="ld">
<div class="tbl_head01 tbl_wrap">
<table>
<caption><?php echo $g5['title']; ?> 목록</caption>
<thead>
<tr>
<th scope="col">
<label for="chkall" class="sound_only">접근가능회원 전체</label>
<input type="checkbox" name="chkall" value="1" id="chkall" onclick="check_all(this.form)">
</th>
<th scope="col">그룹</th>
<th scope="col"><?php echo subject_sort_link('b.mb_id', 'gr_id=' . $gr_id) ?>회원아이디</a></th>
<th scope="col"><?php echo subject_sort_link('b.mb_name', 'gr_id=' . $gr_id) ?>이름</a></th>
<th scope="col"><?php echo subject_sort_link('b.mb_nick', 'gr_id=' . $gr_id) ?>별명</a></th>
<th scope="col"><?php echo subject_sort_link('b.mb_today_login', 'gr_id=' . $gr_id) ?>최종접속</a></th>
<th scope="col"><?php echo subject_sort_link('a.gm_datetime', 'gr_id=' . $gr_id) ?>처리일시</a></th>
</tr>
</thead>
<tbody>
<?php
for ($i = 0; $row = sql_fetch_array($result); $i++) {
// 접근가능한 그룹수
$sql2 = " select count(*) as cnt from {$g5['group_member_table']} where mb_id = '{$row['mb_id']}' ";
$row2 = sql_fetch($sql2);
$group = "";
if ($row2['cnt']) {
$group = '<a href="./boardgroupmember_form.php?mb_id=' . $row['mb_id'] . '">' . $row2['cnt'] . '</a>';
}
$mb_nick = get_sideview($row['mb_id'], $row['mb_nick'], $row['mb_email'], $row['mb_homepage']);
$bg = 'bg' . ($i % 2);
?>
<tr class="<?php echo $bg; ?>">
<td class="td_chk">
<label for="chk_<?php echo $i; ?>" class="sound_only"><?php echo $row['mb_nick'] ?> 회원</label>
<input type="checkbox" name="chk[]" value="<?php echo $row['gm_id'] ?>" id="chk_<?php echo $i ?>">
</td>
<td class="td_grid"><?php echo $group ?></td>
<td class="td_mbid"><?php echo $row['mb_id'] ?></td>
<td class="td_mbname"><?php echo get_text($row['mb_name']); ?></td>
<td class="td_name sv_use"><?php echo $mb_nick ?></td>
<td class="td_datetime"><?php echo substr($row['mb_today_login'], 2, 8) ?></td>
<td class="td_datetime"><?php echo $row['gm_datetime'] ?></td>
</tr>
<?php
}
if ($i == 0) {
echo '<tr><td colspan="' . $colspan . '" class="empty_table">자료가 없습니다.</td></tr>';
}
?>
</tbody>
</table>
</div>
<div class="btn_list01 btn_list">
<input type="submit" name="" value="선택삭제">
</div>
</form>
<?php
echo get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, "{$_SERVER['SCRIPT_NAME']}?$qstr&amp;gr_id=$gr_id&page=");
?>
<script>
function fboardgroupmember_submit(f) {
if (!is_checked("chk[]")) {
alert("선택삭제 하실 항목을 하나 이상 선택하세요.");
return false;
}
return true;
}
</script>
<?php
require_once './admin.tail.php';
+67
View File
@@ -0,0 +1,67 @@
<?php
$sub_menu = "300200";
require_once './_common.php';
sql_query(" ALTER TABLE {$g5['group_member_table']} CHANGE `gm_id` `gm_id` INT( 11 ) DEFAULT '0' NOT NULL AUTO_INCREMENT ", false);
if ($w == '') {
auth_check_menu($auth, $sub_menu, 'w');
$mb = get_member($mb_id);
if (empty($mb['mb_id'])) {
alert('존재하지 않는 회원입니다.');
}
$gr = get_group($gr_id);
if (empty($gr['gr_id'])) {
alert('존재하지 않는 그룹입니다.');
}
$sql = " select count(*) as cnt
from {$g5['group_member_table']}
where gr_id = '{$gr_id}'
and mb_id = '{$mb_id}' ";
$row = sql_fetch($sql);
if ($row['cnt']) {
alert('이미 등록되어 있는 자료입니다.');
} else {
check_admin_token();
$sql = " insert into {$g5['group_member_table']}
set gr_id = '{$_POST['gr_id']}',
mb_id = '{$_POST['mb_id']}',
gm_datetime = '" . G5_TIME_YMDHIS . "' ";
sql_query($sql);
}
} elseif ($w == 'd' || $w == 'ld') {
auth_check_menu($auth, $sub_menu, 'd');
$count = count($_POST['chk']);
if (!$count) {
alert('삭제할 목록을 하나이상 선택해 주세요.');
}
check_admin_token();
for ($i = 0; $i < $count; $i++) {
$gm_id = (int) $_POST['chk'][$i];
$sql = " select * from {$g5['group_member_table']} where gm_id = '$gm_id' ";
$gm = sql_fetch($sql);
if (!$gm['gm_id']) {
if ($count == 1) {
alert('존재하지 않는 자료입니다.');
} else {
continue;
}
}
$sql = " delete from {$g5['group_member_table']} where gm_id = '$gm_id' ";
sql_query($sql);
}
}
if ($w == 'ld') {
goto_url('./boardgroupmember_list.php?gr_id=' . $gr_id);
} else {
goto_url('./boardgroupmember_form.php?mb_id=' . $mb_id);
}
+46
View File
@@ -0,0 +1,46 @@
<?php
$sub_menu = "100510";
require_once './_common.php';
if (!(version_compare(phpversion(), '5.3.0', '>=') && defined('G5_BROWSCAP_USE') && G5_BROWSCAP_USE)) {
alert('사용할 수 없는 기능입니다.', correct_goto_url(G5_ADMIN_URL));
}
if ($is_admin != 'super') {
alert('최고관리자만 접근 가능합니다.');
}
$g5['title'] = 'Browscap 업데이트';
require_once './admin.head.php';
?>
<div id="processing">
<p>Browscap 정보를 업데이트하시려면 아래 업데이트 버튼을 클릭해 주세요.</p>
<button type="button" id="run_update">업데이트</button>
</div>
<script>
$(function() {
$("#run_update").on("click", function() {
$("#processing").html('<div class="update_processing"></div><p>Browscap 정보를 업데이트 중입니다.</p>');
$.ajax({
url: "./browscap_update.php",
async: true,
cache: false,
dataType: "html",
success: function(data) {
if (data != "") {
alert(data);
return false;
}
$("#processing").html("<div class='check_processing'></div><p>Browscap 정보를 업데이트 했습니다.</p>");
}
});
});
});
</script>
<?php
require_once './admin.tail.php';
+50
View File
@@ -0,0 +1,50 @@
<?php
$sub_menu = "100520";
require_once './_common.php';
if (!(version_compare(phpversion(), '5.3.0', '>=') && defined('G5_BROWSCAP_USE') && G5_BROWSCAP_USE)) {
alert('사용할 수 없는 기능입니다.', correct_goto_url(G5_ADMIN_URL));
}
if ($is_admin != 'super') {
alert('최고관리자만 접근 가능합니다.');
}
$rows = isset($_GET['rows']) ? preg_replace('#[^0-9]#', '', $_GET['rows']) : 0;
if (!$rows) {
$rows = 100;
}
$g5['title'] = '접속로그 변환';
require_once './admin.head.php';
?>
<div id="processing">
<p>접속로그 정보를 Browscap 정보로 변환하시려면 아래 업데이트 버튼을 클릭해 주세요.</p>
<button type="button" id="run_update">업데이트</button>
</div>
<script>
$(function() {
$(document).on("click", "#run_update", function() {
$("#processing").html('<div class="update_processing"></div><p>Browscap 정보로 변환 중입니다.</p>');
$.ajax({
method: "GET",
url: "./browscap_converter.php",
data: {
rows: "<?php echo strval($rows); ?>"
},
async: true,
cache: false,
dataType: "html",
success: function(data) {
$("#processing").html(data);
}
});
});
});
</script>
<?php
require_once './admin.tail.php';
+81
View File
@@ -0,0 +1,81 @@
<?php
ini_set('memory_limit', '-1');
require_once './_common.php';
// clean the output buffer
ob_end_clean();
if (!(version_compare(phpversion(), '5.3.0', '>=') && defined('G5_BROWSCAP_USE') && G5_BROWSCAP_USE)) {
die('사용할 수 없는 기능입니다.');
}
if ($is_admin != 'super') {
die('최고관리자로 로그인 후 실행해 주세요.');
}
// browscap cache 파일 체크
if (!is_file(G5_DATA_PATH . '/cache/browscap_cache.php')) {
echo '<p>Browscap 정보가 없습니다. 아래 링크로 이동해 Browscap 정보를 업데이트 하세요.</p>' . PHP_EOL;
echo '<p><a href="' . G5_ADMIN_URL . '/browscap.php">Browscap 업데이트</a></p>' . PHP_EOL;
exit;
}
require_once G5_PLUGIN_PATH . '/browscap/Browscap.php';
$browscap = new phpbrowscap\Browscap(G5_DATA_PATH . '/cache');
$browscap->doAutoUpdate = false;
$browscap->cacheFilename = 'browscap_cache.php';
// 데이터 변환
$rows = isset($_GET['rows']) ? preg_replace('#[^0-9]#', '', $_GET['rows']) : 0;
if (!$rows) {
$rows = 100;
}
$sql_common = " from {$g5['visit_table']} where vi_agent <> '' and ( vi_browser = '' or vi_os = '' or vi_device = '' ) ";
$sql_order = " order by vi_id desc ";
$sql_limit = " limit 0, " . strval($rows) . " ";
$sql = " select count(vi_id) as cnt $sql_common ";
$row = sql_fetch($sql);
$total_count = $row['cnt'];
$sql = " select vi_id, vi_agent, vi_browser, vi_os, vi_device
$sql_common
$sql_order
$sql_limit ";
$result = sql_query($sql);
$cnt = 0;
for ($i = 0; $row = sql_fetch_array($result); $i++) {
$info = $browscap->getBrowser($row['vi_agent']);
$brow = $row['vi_browser'];
if (!$brow) {
$brow = $info->Comment;
}
$os = $row['vi_os'];
if (!$os) {
$os = $info->Platform;
}
$device = $row['vi_device'];
if (!$device) {
$device = $info->Device_Type;
}
$sql2 = " update {$g5['visit_table']}
set vi_browser = '$brow',
vi_os = '$os',
vi_device = '$device'
where vi_id = '{$row['vi_id']}' ";
sql_query($sql2);
$cnt++;
}
if (($total_count - $cnt) == 0 || $total_count == 0) {
echo '<div class="check_processing"></div><p>변환완료</p>';
} else {
echo '<p>총 ' . number_format($total_count) . '건 중 ' . number_format($cnt) . '건 변환완료<br><br>접속로그를 추가로 변환하시려면 아래 업데이트 버튼을 클릭해 주세요.</p><button type="button" id="run_update">업데이트</button>';
}
+25
View File
@@ -0,0 +1,25 @@
<?php
ini_set('memory_limit', '-1');
$sub_menu = "100510";
require_once './_common.php';
// clean the output buffer
ob_end_clean();
if (!(version_compare(phpversion(), '5.3.0', '>=') && defined('G5_BROWSCAP_USE') && G5_BROWSCAP_USE)) {
die('사용할 수 없는 기능입니다.');
}
if ($is_admin != 'super') {
die('최고관리자만 접근 가능합니다.');
}
require_once G5_PLUGIN_PATH . '/browscap/Browscap.php';
$browscap = new phpbrowscap\Browscap(G5_DATA_PATH . '/cache');
$browscap->updateMethod = 'cURL';
$browscap->cacheFilename = 'browscap_cache.php';
$browscap->updateCache();
die('');
+61
View File
@@ -0,0 +1,61 @@
<?php
$sub_menu = '100900';
require_once './_common.php';
if ($is_admin != 'super') {
alert('최고관리자만 접근 가능합니다.', G5_URL);
}
@require_once './safe_check.php';
if (function_exists('social_log_file_delete')) {
social_log_file_delete();
}
run_event('adm_cache_file_delete_before');
$g5['title'] = '캐시파일 일괄삭제';
require_once './admin.head.php';
?>
<div class="local_desc02 local_desc">
<p>
완료 메세지가 나오기 전에 프로그램의 실행을 중지하지 마십시오.
</p>
</div>
<?php
flush();
if (!$dir = @opendir(G5_DATA_PATH . '/cache')) {
echo '<p>캐시디렉토리를 열지못했습니다.</p>';
}
$cnt = 0;
echo '<ul class="session_del">' . PHP_EOL;
$files = glob(G5_DATA_PATH . '/cache/latest-*');
$content_files = glob(G5_DATA_PATH . '/cache/content-*');
$files = array_merge($files, $content_files);
if (is_array($files)) {
foreach ($files as $cache_file) {
$cnt++;
unlink($cache_file);
echo '<li>' . $cache_file . '</li>' . PHP_EOL;
flush();
if ($cnt % 10 == 0) {
echo PHP_EOL;
}
}
}
run_event('adm_cache_file_delete');
echo '<li>완료됨</li></ul>' . PHP_EOL;
echo '<div class="local_desc01 local_desc"><p><strong>최신글 캐시파일 ' . $cnt . '건 삭제 완료됐습니다.</strong><br>프로그램의 실행을 끝마치셔도 좋습니다.</p></div>' . PHP_EOL;
?>
<?php
require_once './admin.tail.php';
+56
View File
@@ -0,0 +1,56 @@
<?php
$sub_menu = '100910';
require_once './_common.php';
if ($is_admin != 'super') {
alert('최고관리자만 접근 가능합니다.', G5_URL);
}
$g5['title'] = '캡챠파일 일괄삭제';
require_once './admin.head.php';
?>
<div class="local_desc02 local_desc">
<p>
완료 메세지가 나오기 전에 프로그램의 실행을 중지하지 마십시오.
</p>
</div>
<?php
flush();
if (!$dir = @opendir(G5_DATA_PATH . '/cache')) {
echo '<p>캐시디렉토리를 열지못했습니다.</p>';
}
$cnt = 0;
echo '<ul class="session_del">' . PHP_EOL;
$files = glob(G5_DATA_PATH . '/cache/?captcha-*');
if (is_array($files)) {
$before_time = G5_SERVER_TIME - 3600; // 한시간전
foreach ($files as $gcaptcha_file) {
$modification_time = filemtime($gcaptcha_file); // 파일접근시간
if ($modification_time > $before_time) {
continue;
}
$cnt++;
unlink($gcaptcha_file);
echo '<li>' . $gcaptcha_file . '</li>' . PHP_EOL;
flush();
if ($cnt % 10 == 0) {
echo PHP_EOL;
}
}
}
echo '<li>완료됨</li></ul>' . PHP_EOL;
echo '<div class="local_desc01 local_desc"><p><strong>캡챠파일 ' . $cnt . '건의 삭제 완료됐습니다.</strong><br>프로그램의 실행을 끝마치셔도 좋습니다.</p></div>' . PHP_EOL;
?>
<?php
require_once './admin.tail.php';
+16
View File
@@ -0,0 +1,16 @@
<?php
define('G5_IS_ADMIN', true);
require_once '../../common.php';
require_once G5_ADMIN_PATH . '/admin.lib.php';
// 💡 [추가] 코드 관리자 모듈 테이블 정의
$g5['ui_manager_table'] = G5_TABLE_PREFIX.'ui_manager';
$g5['form_category_table'] = G5_TABLE_PREFIX.'form_category';
$g5['common_lang_table'] = G5_TABLE_PREFIX.'common_lang';
$g5['form_option_history_table'] = G5_TABLE_PREFIX.'form_option_history';
if (isset($token)) {
$token = @htmlspecialchars(strip_tags($token), ENT_QUOTES);
}
run_event('admin_common');
@@ -0,0 +1,9 @@
<?php
if (!defined('_GNUBOARD_')) exit;
// 💡 [수정] 700번대 최상위 메뉴 배열에 아이콘 클래스('fa-puzzle-piece')를 추가합니다.
$menu['menu700'][] = array('700000', 'UI/폼 관리', G5_ADMIN_URL.'/code_manager/ui_manager_list.php', 'code_manager', 'fa-puzzle-piece');
// 'UI/폼 관리'의 하위 메뉴들을 정의합니다.
$menu['menu700'][] = array('700100', 'UI 리소스 관리', G5_ADMIN_URL.'/code_manager/ui_manager_list.php', 'ui_resource_manager');
$menu['menu700'][] = array('700900', '솔루션 설치', G5_ADMIN_URL.'/code_manager/install.php', 'ui_solution_install');
+282
View File
@@ -0,0 +1,282 @@
<?php
$sub_menu = '700100';
include_once('./_common.php');
// 💡 [추가] w 값을 먼저 정의합니다.
$w = isset($_REQUEST['w']) ? substr(trim($_REQUEST['w']), 0, 1) : '';
// ==================================================================
// 💡 [핵심 수정] 폼 제출 처리 로직 (신규/수정)
// ==================================================================
// POST 요청이고, w값이 넘어왔을 때 (신규='', 수정='u') 처리
if (isset($_POST['w']) && $_SERVER['REQUEST_METHOD'] === 'POST') {
auth_check_menu($auth, $sub_menu, 'w');
check_admin_token();
$w_from_post = $_POST['w']; // POST로 받은 w값을 기준으로 처리
$um_id = (int)$_POST['um_id'];
$parent_id = (int)$_POST['parent_id'];
$fc_key = trim($_POST['fc_key']);
$cl_name = trim($_POST['cl_name']);
$fc_order = (int)$_POST['fc_order'];
$is_used = (int)$_POST['is_used'];
if (!$um_id) {
alert('잘못된 접근입니다.');
}
if (!$fc_key) {
alert('옵션 키(Key)를 입력해주세요.');
}
if (!$cl_name) {
alert('옵션 이름을 입력해주세요.');
}
// 1. g5_form_category 테이블에 마스터 정보 저장
$sql_common = "
um_id = '{$um_id}',
parent_id = '{$parent_id}',
fc_key = '{$fc_key}',
fc_order = '{$fc_order}',
is_used = '{$is_used}',
updated_at = '" . G5_TIME_YMDHIS . "',
updated_by = '{$member['mb_id']}'
";
if ($w_from_post == 'u') { // 수정
$fc_id = (int)$_POST['fc_id'];
if (!$fc_id) alert('fc_id 값이 없습니다.');
$sql = "UPDATE {$g5['form_category_table']} SET {$sql_common} WHERE fc_id = '{$fc_id}'";
sql_query($sql);
} else { // 신규
$sql_common .= ", created_at = '" . G5_TIME_YMDHIS . "', created_by = '{$member['mb_id']}'";
$sql = "INSERT INTO {$g5['form_category_table']} SET {$sql_common}";
sql_query($sql);
$fc_id = sql_insert_id();
}
// 2. g5_common_lang 테이블에 다국어 이름 저장 (없으면 생성, 있으면 수정)
$sql = "SELECT cl_id FROM {$g5['common_lang_table']} WHERE target_table = '{$g5['form_category_table']}' AND target_id = '{$fc_id}' AND lang_code = 'ko'";
$lang_row = sql_fetch($sql);
if (isset($lang_row['cl_id'])) {
$sql = "UPDATE {$g5['common_lang_table']} SET cl_name = '{$cl_name}', updated_at = '" . G5_TIME_YMDHIS . "', updated_by = '{$member['mb_id']}' WHERE cl_id = '{$lang_row['cl_id']}'";
} else {
$sql = "INSERT INTO {$g5['common_lang_table']} SET target_table = '{$g5['form_category_table']}', target_id = '{$fc_id}', lang_code = 'ko', cl_name = '{$cl_name}', updated_at = '" . G5_TIME_YMDHIS . "', updated_by = '{$member['mb_id']}'";
}
sql_query($sql);
goto_url("./category_list.php?um_id=$um_id");
}
// ==================================================================
auth_check_menu($auth, $sub_menu, 'r');
// 그룹 ID가 없으면 되돌려 보냄
$um_id = isset($_GET['um_id']) ? (int)$_GET['um_id'] : 0;
if (!$um_id) {
alert('UI 리소스 ID가 올바르지 않습니다.', './ui_manager_list.php');
}
// 현재 관리하려는 리소스의 정보를 가져옴
$sql = "SELECT * FROM {$g5['ui_manager_table']} WHERE um_id = '{$um_id}'";
$ui_resource = sql_fetch($sql);
if (!isset($ui_resource['um_id']) || $ui_resource['resource_type'] != 'DATA') {
alert('존재하지 않거나 데이터 타입이 아닌 리소스입니다.', './ui_manager_list.php');
}
// 계층형으로 정렬된 카테고리 목록을 가져오는 함수
function get_category_view_list($um_id)
{
global $g5;
$sql = "SELECT
A.*,
B.cl_name
FROM
{$g5['form_category_table']} AS A
LEFT JOIN
{$g5['common_lang_table']} AS B
ON
(A.fc_id = B.target_id AND B.target_table = '{$g5['form_category_table']}' AND B.lang_code = 'ko')
WHERE
A.um_id = '{$um_id}' AND A.is_deleted = 0
ORDER BY
A.parent_id, A.fc_order, A.fc_id";
$result = sql_query($sql);
$categories = [];
while ($row = sql_fetch_array($result)) {
$categories[] = $row;
}
$view_list = [];
// 재귀 함수를 사용하여 계층 구조를 평탄화하고 depth를 추가
generate_category_list($categories, 0, 0, $view_list);
return $view_list;
}
function generate_category_list(&$source, $parent_id, $depth, &$result_list)
{
foreach ($source as $item) {
if ($item['parent_id'] == $parent_id) {
$item['depth'] = $depth;
$result_list[] = $item;
generate_category_list($source, $item['fc_id'], $depth + 1, $result_list);
}
}
}
$category_list = get_category_view_list($um_id);
$category_list_count = count($category_list);
$g5['title'] = get_text($ui_resource['resource_desc']) . ' : 옵션/카테고리 관리';
include_once(G5_ADMIN_PATH . '/admin.head.php');
add_stylesheet('<link rel="stylesheet" href="' . G5_ADMIN_URL . '/code_manager/css/code_manager.css?ver=1.1">', 0);
?>
<div class="local_desc01 local_desc">
<p>
<strong>'<?php echo get_text($ui_resource['resource_desc']); ?>'</strong> 리소스에 포함될 옵션 또는 카테고리를 관리합니다.<br>
'부모 카테고리'를 지정하여 대/중/소 분류와 같은 계층 구조를 만들 수 있습니다.
</p>
</div>
<section id="code_manager">
<div class="code-manager-header">
<h2 class="code-manager-title">
<a href="./ui_manager_list.php" class="btn btn_02">리소스 목록으로</a>
<?php echo get_text($ui_resource['resource_desc']); ?>
(<code><?php echo get_text($ui_resource['resource_code']); ?></code>)
</h2>
<div class="code-manager-actions">
<button type="button" id="add-category-btn" class="btn btn_01">
<i class="fa fa-plus" aria-hidden="true"></i> 새 옵션 추가
</button>
</div>
</div>
<!-- 새 옵션(카테고리) 추가 폼 -->
<div id="category-form-container" style="display: none; margin-top: 20px;">
<form name="fcategoryform" id="fcategoryform" action="<?php echo $_SERVER['SCRIPT_NAME']; ?>" method="post">
<input type="hidden" name="w" value="">
<input type="hidden" name="um_id" value="<?php echo $um_id; ?>">
<input type="hidden" name="fc_id" value="">
<input type="hidden" name="token" value="<?php echo get_admin_token(); ?>">
<div class="tbl_frm01 tbl_wrap">
<table>
<caption>옵션/카테고리 추가/수정 폼</caption>
<colgroup>
<col class="grid_4">
<col>
</colgroup>
<tbody>
<tr>
<th scope="row"><label for="parent_id">부모 카테고리</label></th>
<td>
<select name="parent_id" id="parent_id">
<option value="0">최상위 카테고리</option>
<?php foreach ($category_list as $cat) : ?>
<option value="<?php echo $cat['fc_id']; ?>">
<?php echo str_repeat('&nbsp;&nbsp;&nbsp;', $cat['depth']); ?><?php echo get_text($cat['cl_name']); ?>
</option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th scope="row"><label for="fc_key">옵션 키(Key)</label></th>
<td>
<input type="text" name="fc_key" id="fc_key" required class="required frm_input" size="30">
<span class="frm_info">DB에 저장될 고유한 값입니다. (영문, 숫자, _ 사용)</span>
</td>
</tr>
<tr>
<th scope="row"><label for="cl_name">옵션 이름</label></th>
<td>
<input type="text" name="cl_name" id="cl_name" required class="required frm_input" size="50">
<span class="frm_info">화면에 표시될 이름입니다.</span>
</td>
</tr>
<tr>
<th scope="row"><label for="fc_order">정렬순서</label></th>
<td>
<input type="number" name="fc_order" value="0" id="fc_order" class="frm_input" size="5">
<span class="frm_info">숫자가 낮을수록 먼저 표시됩니다.</span>
</td>
</tr>
<tr>
<th scope="row">사용 여부</th>
<td>
<label><input type="radio" name="is_used" value="1" checked> 사용함</label>
<label><input type="radio" name="is_used" value="0"> 사용안함</label>
</td>
</tr>
</tbody>
</table>
</div>
<div class="btn_confirm01 btn_confirm">
<button type="button" id="cancel-category-btn" class="btn_cancel btn">취소</button>
<input type="submit" value="저장" class="btn_submit btn" accesskey="s">
</div>
</form>
</div>
<!-- 등록된 옵션(카테고리) 목록 테이블 -->
<div class="tbl_head01 tbl_wrap" style="margin-top: 20px;">
<table>
<caption>옵션/카테고리 목록</caption>
<thead>
<tr>
<th scope="col">카테고리 이름</th>
<th scope="col" style="width: 150px;">옵션 키 (Key)</th>
<th scope="col" style="width: 100px;">부모 ID</th>
<th scope="col" style="width: 60px;">순서</th>
<th scope="col" style="width: 60px;">사용</th>
<th scope="col" style="width: 180px;">관리</th>
</tr>
</thead>
<tbody>
<?php if ($category_list_count > 0) : ?>
<?php foreach ($category_list as $cat) : ?>
<tr>
<td class="td_left category-name-depth-<?php echo $cat['depth']; ?>">
<?php if ($cat['depth'] > 0) : ?>
<span class="depth-prefix">└</span>
<?php endif; ?>
<?php echo get_text($cat['cl_name']); ?>
</td>
<td><?php echo get_text($cat['fc_key']); ?></td>
<td><?php echo $cat['parent_id']; ?></td>
<td><?php echo $cat['fc_order']; ?></td>
<td><?php echo $cat['is_used'] ? 'Y' : 'N'; ?></td>
<td class="td_mng td_mng_s">
<button type="button" class="btn btn_02 btn_edit_category"
data-fc_id="<?php echo $cat['fc_id']; ?>"
data-parent_id="<?php echo $cat['parent_id']; ?>"
data-fc_key="<?php echo get_text($cat['fc_key']); ?>"
data-cl_name="<?php echo get_text($cat['cl_name']); ?>"
data-fc_order="<?php echo $cat['fc_order']; ?>"
data-is_used="<?php echo $cat['is_used']; ?>">수정
</button>
<a href="./lang_manager.php?target_table=<?php echo $g5['form_category_table']; ?>&amp;target_id=<?php echo $cat['fc_id']; ?>"
class="btn btn_01">다국어</a>
</td>
</tr>
<?php endforeach; ?>
<?php else : ?>
<tr class="empty_table">
<td colspan="6">등록된 옵션이 없습니다.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
<script src="<?php echo G5_ADMIN_URL; ?>/code_manager/js/ui_manager.js?ver=1.4"></script>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
+210
View File
@@ -0,0 +1,210 @@
<?php
$sub_menu = '100900'; // admin.menu100.php 에 정의된 메뉴 코드
include_once('./_common.php');
// ==================================================================
// 💡 [핵심] 폼 제출 처리 로직
// ==================================================================
if (isset($w) && $w == 'u' && $_SERVER['REQUEST_METHOD'] === 'POST') {
auth_check_menu($auth, $sub_menu, 'w');
check_admin_token();
// 입력값 정리
$screen_code = trim($_POST['screen_code']);
$group_code = trim($_POST['group_code']);
$resource_code = trim($_POST['resource_code']);
$resource_type = trim($_POST['resource_type']);
$resource_desc = trim($_POST['resource_desc']);
$cl_name = isset($_POST['cl_name']) ? trim($_POST['cl_name']) : ''; // LABEL 타입일 때만 넘어옴
// 유효성 검사
if (!$screen_code || !$group_code || !$resource_code || !$resource_type) {
alert('필수 항목을 모두 입력해주세요.');
}
if ($resource_type == 'LABEL' && !$cl_name) {
alert('UI 라벨 타입은 한국어 라벨명을 필수로 입력해야 합니다.');
}
// 중복 검사
$sql = "SELECT COUNT(*) as cnt FROM {$g5['ui_manager_table']} WHERE screen_code = '{$screen_code}' AND group_code = '{$group_code}' AND resource_code = '{$resource_code}'";
$row = sql_fetch($sql);
if ($row['cnt']) {
alert('이미 등록된 리소스 코드입니다.');
}
// 1. g5_ui_manager 테이블에 리소스 '설계' 정보 저장
$sql = "INSERT INTO {$g5['ui_manager_table']}
SET screen_code = '{$screen_code}',
group_code = '{$group_code}',
resource_code = '{$resource_code}',
resource_type = '{$resource_type}',
resource_desc = '{$resource_desc}',
is_used = '1',
created_at = '".G5_TIME_YMDHIS."',
created_by = '{$member['mb_id']}',
updated_at = '".G5_TIME_YMDHIS."',
updated_by = '{$member['mb_id']}'";
sql_query($sql);
$um_id = sql_insert_id();
// 2. 리소스 타입이 'LABEL'인 경우, g5_common_lang 테이블에 실제 텍스트 저장
if ($resource_type == 'LABEL' && $cl_name) {
$sql = "INSERT INTO {$g5['common_lang_table']}
SET target_table = '{$g5['ui_manager_table']}',
target_id = '{$um_id}',
lang_code = 'ko',
cl_name = '{$cl_name}',
updated_at = '".G5_TIME_YMDHIS."',
updated_by = '{$member['mb_id']}'";
sql_query($sql);
}
goto_url('./code_list.php');
}
auth_check_menu($auth, $sub_menu, 'r');
// 등록된 리소스 목록 조회
$sql = "SELECT * FROM {$g5['ui_manager_table']} ORDER BY screen_code, group_code, resource_code";
$result = sql_query($sql);
$resource_list = [];
while ($row = sql_fetch_array($result)) {
$resource_list[] = $row;
}
$resource_list_count = count($resource_list);
$g5['title'] = 'UI 리소스 관리';
include_once(G5_ADMIN_PATH . '/admin.head.php');
// 솔루션 전용 CSS/JS 파일을 불러옵니다.
add_stylesheet('<link rel="stylesheet" href="' . G5_ADMIN_URL . '/code_manager/css/code_manager.css">', 0);
?>
<div class="local_desc01 local_desc">
<p>
웹사이트의 모든 화면에 사용되는 텍스트(라벨)와 선택 옵션(데이터)을 체계적으로 관리합니다.
</p>
</div>
<section id="code_manager">
<div class="code-manager-header">
<h2 class="code-manager-title">UI 리소스 목록</h2>
<div class="code-manager-actions">
<button type="button" id="add-resource-btn" class="btn btn_01">
<i class="fa fa-plus" aria-hidden="true"></i> 새 리소스 추가
</button>
</div>
</div>
<!-- 새 리소스 추가 폼 -->
<div id="resource-form-container" style="display: none;">
<form name="fresourceform" id="fresourceform" action="<?php echo $_SERVER['SCRIPT_NAME']; ?>" method="post">
<input type="hidden" name="w" value="u">
<input type="hidden" name="token" value="<?php echo get_admin_token(); ?>">
<div class="tbl_frm01 tbl_wrap">
<table>
<caption>UI 리소스 추가 폼</caption>
<colgroup>
<col class="grid_4">
<col>
</colgroup>
<tbody>
<tr>
<th scope="row"><label for="screen_code">화면 코드</label></th>
<td>
<input type="text" name="screen_code" id="screen_code" required class="required frm_input" size="30">
<span class="frm_info">리소스가 사용될 화면의 고유 코드 (예: order_form, member_join)</span>
</td>
</tr>
<tr>
<th scope="row"><label for="group_code">그룹 코드</label></th>
<td>
<input type="text" name="group_code" id="group_code" required class="required frm_input" size="30">
<span class="frm_info">화면 내에서 리소스를 묶어줄 그룹 코드 (예: address_info, common_options)</span>
</td>
</tr>
<tr>
<th scope="row">리소스 타입</th>
<td>
<label><input type="radio" name="resource_type" value="LABEL" checked> UI 라벨 (단일 텍스트)</label>
<label><input type="radio" name="resource_type" value="DATA"> 데이터 (선택 옵션)</label>
</td>
</tr>
<tr class="resource-type-field" id="label-field">
<th scope="row"><label for="cl_name">한국어 라벨명</label></th>
<td>
<input type="text" name="cl_name" id="cl_name" class="frm_input" size="50">
<span class="frm_info">화면에 표시될 실제 텍스트 (예: 집의 유형, 창호 재질)</span>
</td>
</tr>
<tr>
<th scope="row"><label for="resource_code">리소스 코드</label></th>
<td>
<input type="text" name="resource_code" id="resource_code" required class="required frm_input" size="30">
<span class="frm_info">개발자가 이 리소스를 호출할 때 사용할 고유 코드 (예: house_type_label, house_type_data)</span>
</td>
</tr>
<tr>
<th scope="row"><label for="resource_desc">설명</label></th>
<td>
<input type="text" name="resource_desc" id="resource_desc" class="frm_input" size="80">
<span class="frm_info">이 리소스의 용도에 대한 설명 (관리자 참고용)</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="btn_confirm01 btn_confirm">
<button type="button" id="cancel-resource-btn" class="btn_cancel btn">취소</button>
<input type="submit" value="리소스 등록" class="btn_submit btn" accesskey="s">
</div>
</form>
</div>
<!-- 등록된 리소스 목록 테이블 -->
<div class="tbl_head01 tbl_wrap">
<table>
<caption>UI 리소스 목록</caption>
<thead>
<tr>
<th scope="col">화면 코드</th>
<th scope="col">그룹 코드</th>
<th scope="col">리소스 코드</th>
<th scope="col">타입</th>
<th scope="col">설명</th>
<th scope="col">관리</th>
</tr>
</thead>
<tbody>
<?php if ($resource_list_count > 0) : ?>
<?php foreach ($resource_list as $res) : ?>
<tr>
<td><?php echo get_text($res['screen_code']); ?></td>
<td><?php echo get_text($res['group_code']); ?></td>
<td><?php echo get_text($res['resource_code']); ?></td>
<td><?php echo $res['resource_type']; ?></td>
<td class="td_left"><?php echo get_text($res['resource_desc']); ?></td>
<td class="td_mng">
<?php if ($res['resource_type'] == 'DATA') : ?>
<a href="./category_list.php?um_id=<?php echo $res['um_id']; ?>" class="btn btn_03">옵션 관리</a>
<?php endif; ?>
<a href="./category_list.php?w=u&amp;um_id=<?php echo $res['um_id']; ?>" class="btn btn_02">수정</a>
</td>
</tr>
<?php endforeach; ?>
<?php else : ?>
<tr class="empty_table">
<td colspan="6">등록된 리소스가 없습니다.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
<script src="<?php echo G5_ADMIN_URL; ?>/code_manager/js/code_manager.js"></script>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
+196
View File
@@ -0,0 +1,196 @@
/* 폼 옵션 관리 솔루션 전용 스타일 */
#code_manager .code-manager-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
#code_manager .code-manager-title {
margin: 0;
font-size: 1.2em;
}
#code_manager .code-manager-actions .btn {
padding: 5px 12px;
font-size: 0.9em;
}
/* 새 리소스 추가 폼 */
#resource-form-container {
margin-bottom: 20px;
padding: 20px;
border: 1px solid #e0e0e0;
background-color: #f9f9f9;
border-radius: 5px;
}
#resource-form-container .frm_info {
display: block;
margin-top: 5px;
color: #666;
}
/* 💡 [추가] 아코디언 UI 스타일 */
#resource-list-accordion {
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
}
.accordion-item {
border-bottom: 1px solid #ddd;
}
.accordion-item:last-child {
border-bottom: none;
}
.accordion-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background-color: #f7f7f7;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
}
.accordion-header:hover {
background-color: #efefef;
}
.accordion-header .screen-title {
font-size: 1.1em;
font-weight: 500;
}
.accordion-header .screen-title .fa {
margin-right: 8px;
color: #555;
}
.accordion-header .resource-count {
font-size: 0.9em;
color: #fff;
background-color: #888;
padding: 3px 8px;
border-radius: 10px;
}
.accordion-header .accordion-icon {
transition: transform 0.3s ease;
}
.accordion-item.active .accordion-header .accordion-icon {
transform: rotate(180deg);
}
.accordion-content {
display: none;
padding: 15px;
background-color: #fff;
border-top: 1px solid #ddd;
}
.accordion-content .tbl_wrap {
margin: 0;
}
/* 💡 [추가] 리소스 타입 시각적 구분 */
.res-type-label, .res-type-data {
display: inline-block;
padding: 3px 8px;
font-size: 0.85em;
font-weight: bold;
color: #fff;
border-radius: 4px;
text-align: center;
}
.res-type-label {
background-color: #3498db; /* 파란색 계열 */
}
.res-type-data {
background-color: #2ecc71; /* 녹색 계열 */
}
/* 기타 스타일 */
.tbl_head01 .td_left {
text-align: left;
padding-left: 10px;
}
/* ... 기존 CSS 코드 ... */
/* 💡 [추가] 카테고리 목록 들여쓰기 스타일 */
.category-name-depth-0 { font-weight: bold; }
.category-name-depth-1 { padding-left: 25px !important; }
.category-name-depth-2 { padding-left: 50px !important; }
.category-name-depth-3 { padding-left: 75px !important; }
.category-name-depth-4 { padding-left: 100px !important; }
.depth-prefix {
font-family: "Malgun Gothic", "Apple SD Gothic Neo", sans-serif;
font-weight: normal;
color: #aaa;
margin-right: 5px;
}
/* ... 기존 CSS 코드 ... */
/* 💡 [추가] 삭제 버튼 스타일 */
.btn_delete {
display: inline-block;
padding: 0 10px;
height: 28px;
line-height: 26px;
border: 1px solid #d43f3a;
background: #d9534f;
color: #fff;
text-decoration: none;
vertical-align: middle;
border-radius: 3px;
cursor: pointer;
}
.btn_delete:hover {
background: #c9302c;
border-color: #ac2925;
}
/* ... 기존 CSS 코드 ... */
/* 💡 [추가] 다국어 관리 페이지 스타일 */
.h2_frm {
margin: 20px 0 10px;
font-size: 1.1em;
font-weight: bold;
}
.td_alignc {
text-align: center;
}
#flangform textarea {
width: 100%;
padding: 5px;
}
/* ... 기존 CSS 코드 ... */
/* 💡 [추가] 검색 폼 스타일 */
#resource_search_form {
margin-bottom: 20px;
padding: 20px;
border: 1px solid #e9e9e9;
background: #fcfcfc;
}
#resource_search_form .h2_frm {
margin: 0 0 10px;
}
#resource_search_form .search-form-inner {
display: flex;
gap: 5px;
}
#resource_search_form select,
#resource_search_form .frm_input {
height: 35px;
padding: 0 10px;
border: 1px solid #ccc;
border-radius: 3px;
}
#resource_search_form .btn_submit {
height: 35px;
padding: 0 20px;
font-size: 1em;
}
/* ... 기존 CSS 코드 ... */
/* 💡 [추가] 페이징 스타일 */
.pagination_wrap {
margin-top: 20px;
text-align: center;
}
.pagination_wrap .pg_wrap {
display: inline-block;
float: none;
}
+192
View File
@@ -0,0 +1,192 @@
<?php
$sub_menu = '700900';
include_once('./_common.php');
include_once(__DIR__ . '/lib/SchemaManager.class.php');
if ($is_admin != 'super') {
alert('최고관리자만 접근 가능합니다.');
}
/**
* SQL 파일에서 테이블 이름을 추출하는 함수
*/
function get_tables_from_sql_file($filepath) {
$tables = [];
if (!file_exists($filepath)) {
return $tables;
}
$lines = file($filepath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (preg_match('/CREATE TABLE(?: IF NOT EXISTS)? `([^`]+)`/i', $line, $matches)) {
$tables[] = $matches[1];
}
}
return $tables;
}
$g5['title'] = 'UI 리소스 관리 솔루션 설치';
include_once(G5_ADMIN_PATH . '/admin.head.php');
$install_result = null;
$delete_result = null;
$action = $_POST['action'] ?? '';
// 💡 [수정] SQL 파일에서 테이블 목록 동적 로드
$tables_to_check = get_tables_from_sql_file(__DIR__ . '/install.sql');
if ($action === 'install') {
check_admin_token();
// ... (설치 로직은 기존과 동일)
$copy_results = [];
$solution_files = [
['source' => __DIR__ . '/lib/ui_manager.extend.php', 'target' => G5_EXTEND_PATH . '/ui_manager.extend.php', 'desc' => '핵심 기능 파일'],
['source' => __DIR__ . '/admin.menu700.code_manager.php', 'target' => G5_ADMIN_PATH . '/admin.menu700.code_manager.php', 'desc' => '관리자 메뉴 파일']
];
foreach ($solution_files as $file) {
$key = $file['target'];
if (file_exists($file['source']) && is_writable(dirname($file['target']))) {
@copy($file['source'], $file['target']);
}
}
$sql_file = __DIR__ . '/install.sql';
$db_results = [];
try {
$schemaManager = new SchemaManager($sql_file);
$schemaManager->execute();
$db_results = $schemaManager->get_results();
} catch (Exception $e) { $db_results['errors'][] = $e->getMessage(); }
$install_result = ['db' => $db_results];
} else if ($action === 'delete') {
check_admin_token();
$delete_result = ['tables' => [], 'menu' => ''];
// 💡 [수정] 삭제할 테이블 목록도 동적으로 가져옴
$tables_to_delete = get_tables_from_sql_file(__DIR__ . '/install.sql');
foreach ($tables_to_delete as $table) {
sql_query("DROP TABLE IF EXISTS `{$table}`", false);
$delete_result['tables'][] = $table;
}
// $menu_file = G5_ADMIN_PATH . '/admin.menu700.code_manager.php';
$solution_files = [
['source' => __DIR__ . '/lib/ui_manager.extend.php', 'target' => G5_EXTEND_PATH . '/ui_manager.extend.php', 'desc' => '핵심 기능 파일'],
['source' => __DIR__ . '/admin.menu700.code_manager.php', 'target' => G5_ADMIN_PATH . '/admin.menu700.code_manager.php', 'desc' => '관리자 메뉴 파일']
];
foreach ($solution_files as $file) {
$key = $file['target'];
var_dump($key);
if (file_exists($key)) {
if (@unlink($key)) {
$delete_result['menu'] = '메뉴 파일 삭제 성공';
} else {
$delete_result['menu'] = '메뉴 파일 삭제 실패 (권한 확인 필요)';
}
}
}
// if (file_exists($menu_file)) {
// if (@unlink($menu_file)) {
// $delete_result['menu'] = '메뉴 파일 삭제 성공';
// } else {
// $delete_result['menu'] = '메뉴 파일 삭제 실패 (권한 확인 필요)';
// }
// }
}
$existing_tables = array();
foreach ($tables_to_check as $table) {
if (sql_query("SHOW TABLES LIKE '$table'", false) && sql_num_rows(sql_query("SHOW TABLES LIKE '$table'", false)) > 0) {
$existing_tables[] = $table;
}
}
$is_installed = count($existing_tables) == count($tables_to_check);
?>
<style>
.install-container { max-width: 800px; margin: 20px auto; padding: 20px; background: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.install-header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 2px solid #AA20FF; }
.install-header h1 { color: #AA20FF; margin-bottom: 10px; }
.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin: 30px 0; }
.feature-card { padding: 20px; border: 1px solid #e0e0e0; border-radius: 8px; text-align: center; }
.feature-card i { font-size: 2em; color: #AA20FF; margin-bottom: 10px; }
.status-table { width: 100%; border-collapse: collapse; margin: 20px 0; }
.status-table th, .status-table td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
.status-table th { background-color: #fff; font-weight: bold; }
.status-ok { color: #28a745; font-weight: bold; }
.status-missing { color: #dc3545; font-weight: bold; }
.install-btn { display: block; width: 200px; margin: 30px auto; padding: 15px 30px; background: #AA20FF; color: white; text-align: center; text-decoration: none; border-radius: 5px; font-size: 16px; font-weight: bold; border: none; cursor: pointer; transition: background-color 0.3s; }
.install-btn:hover { background: #8A1ACC; color: white; }
.install-btn:disabled { background: #ccc; cursor: not-allowed; }
.alert { padding: 15px; margin: 20px 0; border-radius: 5px; }
.alert-success { background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724; }
.alert-info { background-color: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; }
.alert-danger { background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
.btn-secondary { background: #6c757d; color: white; border-color: #6c757d; padding: 5px 10px; border-radius: 4px; text-decoration: none; }
.btn-secondary:hover { background: #5a6268; }
.btn-danger { background: #dc3545; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; }
.btn-danger:hover { background: #c82333; }
.button-group { display: flex; justify-content: center; align-items: center; gap: 10px; }
</style>
<div class="install-container">
<div class="install-header">
<h1><i class="fa fa-code"></i> UI 리소스 관리 솔루션</h1>
<p>CSS, JS 등 UI 리소스를 효율적으로 관리하는 시스템</p>
</div>
<?php if ($install_result): ?>
<div class="alert alert-success"><h4><i class="fa fa-check-circle"></i> 설치 작업 완료</h4><p>데이터베이스 설치 작업이 완료되었습니다.</p><p><a href="./ui_manager_list.php" class="btn btn-primary">UI 리소스 관리로 이동</a></p></div>
<?php elseif ($delete_result): ?>
<div class="alert alert-danger"><h4><i class="fa fa-trash"></i> 삭제 작업 완료</h4><p>솔루션 관련 데이터와 파일이 삭제되었습니다.</p><ul><?php foreach($delete_result['tables'] as $tbl) echo "<li>{$tbl} 테이블 삭제됨</li>"; ?><li><?php echo $delete_result['menu']; ?></li></ul></div>
<?php elseif ($is_installed): ?>
<div class="alert alert-success"><h4><i class="fa fa-check-circle"></i> 설치 완료</h4><p>UI 리소스 관리 솔루션이 이미 설치되어 있습니다.</p><p><a href="./ui_manager_list.php" class="btn btn-primary">UI 리소스 관리로 이동</a></p></div>
<?php else: ?>
<div class="alert alert-info"><h4><i class="fa fa-info-circle"></i> 설치 필요</h4><p>UI 리소스 관리 솔루션을 사용하기 위해 설치가 필요합니다.</p></div>
<?php endif; ?>
<h3><i class="fa fa-database"></i> 설치 상태</h3>
<table class="status-table">
<thead><tr><th>테이블명</th><th>설명</th><th>상태</th></tr></thead>
<tbody>
<?php foreach ($tables_to_check as $table): ?>
<tr>
<td><code><?php echo $table; ?></code></td>
<td><?php echo array('g5_ui_manager' => 'UI 리소스 마스터', 'g5_form_category' => '계층형 폼 카테고리', 'g5_common_lang' => '공용 다국어 정보', 'g5_form_option_history' => '폼 옵션 변경 이력')[$table] ?? '데이터 테이블'; ?></td>
<td>
<?php if (in_array($table, $existing_tables)): ?>
<span class="status-ok"><i class="fa fa-check"></i> 설치됨</span>
<?php else: ?>
<span class="status-missing"><i class="fa fa-times"></i> 미설치</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if (!$is_installed): ?>
<form method="post" onsubmit="return confirm('솔루션을 설치하시겠습니까?');">
<input type="hidden" name="action" value="install">
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
<button type="submit" class="install-btn"><i class="fa fa-download"></i> 솔루션 설치하기</button>
</form>
<?php endif; ?>
<?php if ($is_installed && !$install_result && !$delete_result): ?>
<div class="button-group" style="text-align: center; margin-top: 20px;">
<form method="post" onsubmit="return confirm('기존 데이터는 유지되며, 변경된 DB 구조만 업데이트 됩니다. 진행하시겠습니까?');">
<input type="hidden" name="action" value="install">
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
<button type="submit" class="btn btn-secondary"><i class="fa fa-sync"></i> 재설치 (업데이트)</button>
</form>
<form method="post" onsubmit="return confirm('정말로 솔루션을 삭제하시겠습니까? 모든 관련 데이터와 파일이 영구적으로 삭제됩니다.');">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
<button type="submit" class="btn-danger"><i class="fa fa-trash"></i> 솔루션 삭제하기</button>
</form>
</div>
<?php endif; ?>
</div>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
?>
+64
View File
@@ -0,0 +1,64 @@
-- 1. [핵심] UI 리소스 마스터 테이블
-- 이 테이블은 웹사이트의 모든 UI 요소(라벨, 데이터)의 '설계도' 역할을 합니다.
CREATE TABLE IF NOT EXISTS `g5_ui_manager` (
`um_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '고유 ID',
`screen_code` varchar(50) NOT NULL COMMENT '화면 코드 (e.g. order_form)',
`group_code` varchar(50) NOT NULL COMMENT '화면 내 그룹 코드 (e.g. address_info)',
`resource_code` varchar(50) NOT NULL COMMENT '리소스 코드 (개발자가 사용)',
`resource_type` enum('LABEL','DATA') NOT NULL COMMENT '리소스 타입 (라벨, 데이터)',
`resource_desc` varchar(255) DEFAULT NULL COMMENT '리소스에 대한 설명 (관리자용)',
`is_used` tinyint(1) NOT NULL DEFAULT '1' COMMENT '사용 여부',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일',
`created_by` varchar(20) NOT NULL COMMENT '생성자',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '수정일',
`updated_by` varchar(20) NOT NULL COMMENT '수정자',
PRIMARY KEY (`um_id`),
UNIQUE KEY `resource_identifier` (`screen_code`,`group_code`,`resource_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='[솔루션] UI 리소스 관리자';
-- 2. 계층형 카테고리 테이블 ('DATA' 타입 리소스가 사용할 데이터)
-- um_id를 통해 어떤 리소스에 속한 데이터인지 명시합니다.
CREATE TABLE IF NOT EXISTS `g5_form_category` (
`fc_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '카테고리 고유 ID',
`um_id` int(11) NOT NULL COMMENT 'UI 리소스 ID (g5_ui_manager.um_id)',
`parent_id` int(11) NOT NULL DEFAULT '0' COMMENT '부모 카테고리 ID (0이면 최상위)',
`fc_key` varchar(255) NOT NULL COMMENT 'DB에 저장될 값 (고유값)',
`fc_order` int(11) NOT NULL DEFAULT '0' COMMENT '정렬 순서',
`is_used` tinyint(1) NOT NULL DEFAULT '1' COMMENT '사용 여부',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '삭제 여부',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일',
`created_by` varchar(20) NOT NULL COMMENT '생성자',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '수정일',
`updated_by` varchar(20) NOT NULL COMMENT '수정자',
PRIMARY KEY (`fc_id`),
KEY `um_id` (`um_id`),
KEY `parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='[솔루션] 계층형 폼 카테고리';
-- 3. 공용 다국어 정보 테이블
-- 'LABEL' 타입의 실제 텍스트와 'DATA' 타입의 카테고리 이름을 모두 저장합니다.
CREATE TABLE IF NOT EXISTS `g5_common_lang` (
`cl_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '고유 ID',
`target_table` varchar(50) NOT NULL COMMENT '대상 테이블명',
`target_id` int(11) NOT NULL COMMENT '대상 레코드 ID',
`lang_code` varchar(10) NOT NULL COMMENT '언어 코드',
`cl_name` varchar(255) NOT NULL COMMENT '화면에 표시될 이름/값',
`cl_description` text COMMENT '부가 설명 (툴팁 등)',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '수정일',
`updated_by` varchar(20) NOT NULL COMMENT '수정자',
PRIMARY KEY (`cl_id`),
UNIQUE KEY `target_lang` (`target_table`,`target_id`,`lang_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='[솔루션] 공용 다국어 정보';
-- 4. 변경 이력 테이블
CREATE TABLE IF NOT EXISTS `g5_form_option_history` (
`fh_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '고유 ID',
`table_name` varchar(50) NOT NULL COMMENT '변경된 테이블명',
`record_id` int(11) NOT NULL COMMENT '변경된 레코드 ID',
`action_type` varchar(10) NOT NULL COMMENT '작업 종류',
`change_data` longtext COMMENT '변경된 데이터 (JSON)',
`changed_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '변경일',
`changed_by` varchar(20) NOT NULL COMMENT '변경자',
PRIMARY KEY (`fh_id`),
KEY `table_name_record_id` (`table_name`,`record_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='[솔루션] 폼 옵션 변경 이력';
+90
View File
@@ -0,0 +1,90 @@
-- 1. 마스터 테이블: 옵션 그룹 (예: '집 유형', '창호 색상')
CREATE TABLE IF NOT EXISTS `g5_form_group` (
`fg_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '고유 ID',
`project_code` varchar(50) NOT NULL DEFAULT 'default' COMMENT '프로젝트 코드',
`site_code` varchar(50) NOT NULL DEFAULT 'default' COMMENT '사이트 코드',
`fg_code` varchar(50) NOT NULL COMMENT '그룹 코드 (프로그램에서 사용)',
`fg_order` int(11) NOT NULL DEFAULT '0' COMMENT '정렬 순서',
`is_used` tinyint(1) NOT NULL DEFAULT '1' COMMENT '사용 여부 (1:사용, 0:미사용)',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '삭제 여부 (1:삭제, 0:정상)',
`created_at` datetime NOT NULL COMMENT '생성일',
`created_by` varchar(20) NOT NULL COMMENT '생성자',
`updated_at` datetime NOT NULL COMMENT '수정일',
`updated_by` varchar(20) NOT NULL COMMENT '수정자',
`fg_temp_1` varchar(255) DEFAULT NULL COMMENT '임시 필드 1',
`fg_temp_2` varchar(255) DEFAULT NULL COMMENT '임시 필드 2',
`fg_temp_3` varchar(255) DEFAULT NULL COMMENT '임시 필드 3',
`fg_temp_4` text DEFAULT NULL COMMENT '임시 필드 4',
`fg_temp_5` text DEFAULT NULL COMMENT '임시 필드 5',
`fg_extra_1` varchar(255) DEFAULT NULL COMMENT '여분 필드 1',
`fg_extra_2` varchar(255) DEFAULT NULL COMMENT '여분 필드 2',
`fg_extra_3` varchar(255) DEFAULT NULL COMMENT '여분 필드 3',
`fg_extra_4` text DEFAULT NULL COMMENT '여분 필드 4',
`fg_extra_5` text DEFAULT NULL COMMENT '여분 필드 5',
PRIMARY KEY (`fg_id`),
UNIQUE KEY `project_site_fg_code` (`project_code`,`site_code`,`fg_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='[솔루션] 폼 옵션 그룹';
-- 2. 디테일 테이블: 그룹에 속한 개별 옵션
CREATE TABLE IF NOT EXISTS `g5_form_option` (
`fo_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '고유 ID',
`fg_id` int(11) NOT NULL COMMENT '그룹 ID (g5_form_group.fg_id)',
`fo_key` varchar(255) NOT NULL COMMENT 'DB에 저장될 값 (고유값)',
`fo_order` int(11) NOT NULL DEFAULT '0' COMMENT '정렬 순서',
`is_default` tinyint(1) NOT NULL DEFAULT '0' COMMENT '기본 선택 여부 (1:기본값)',
`is_used` tinyint(1) NOT NULL DEFAULT '1' COMMENT '사용 여부 (1:사용, 0:미사용)',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '삭제 여부 (1:삭제, 0:정상)',
`created_at` datetime NOT NULL COMMENT '생성일',
`created_by` varchar(20) NOT NULL COMMENT '생성자',
`updated_at` datetime NOT NULL COMMENT '수정일',
`updated_by` varchar(20) NOT NULL COMMENT '수정자',
`fo_temp_1` varchar(255) DEFAULT NULL COMMENT '임시 필드 1',
`fo_temp_2` varchar(255) DEFAULT NULL COMMENT '임시 필드 2',
`fo_temp_3` varchar(255) DEFAULT NULL COMMENT '임시 필드 3',
`fo_temp_4` text DEFAULT NULL COMMENT '임시 필드 4',
`fo_temp_5` text DEFAULT NULL COMMENT '임시 필드 5',
`fo_extra_1` varchar(255) DEFAULT NULL COMMENT '여분 필드 1',
`fo_extra_2` varchar(255) DEFAULT NULL COMMENT '여분 필드 2',
`fo_extra_3` varchar(255) DEFAULT NULL COMMENT '여분 필드 3',
`fo_extra_4` text DEFAULT NULL COMMENT '여분 필드 4',
`fo_extra_5` text DEFAULT NULL COMMENT '여분 필드 5',
PRIMARY KEY (`fo_id`),
KEY `fg_id` (`fg_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='[솔루션] 폼 옵션 항목';
-- 3. [통합] 서브 디테일 테이블: 그룹과 옵션의 다국어 이름/설명
CREATE TABLE IF NOT EXISTS `g5_common_lang` (
`cl_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '고유 ID',
`target_table` varchar(50) NOT NULL COMMENT '대상 테이블명 (예: g5_form_group)',
`target_id` int(11) NOT NULL COMMENT '대상 레코드 ID',
`lang_code` varchar(10) NOT NULL COMMENT '언어 코드 (ko, en, ja)',
`cl_name` varchar(255) NOT NULL COMMENT '화면에 표시될 이름/값',
`cl_description` text COMMENT '부가 설명 (툴팁 등)',
`updated_at` datetime NOT NULL COMMENT '수정일',
`updated_by` varchar(20) NOT NULL COMMENT '수정자',
`cl_temp_1` varchar(255) DEFAULT NULL COMMENT '임시 필드 1',
`cl_temp_2` varchar(255) DEFAULT NULL COMMENT '임시 필드 2',
`cl_temp_3` varchar(255) DEFAULT NULL COMMENT '임시 필드 3',
`cl_temp_4` text DEFAULT NULL COMMENT '임시 필드 4',
`cl_temp_5` text DEFAULT NULL COMMENT '임시 필드 5',
`cl_extra_1` varchar(255) DEFAULT NULL COMMENT '여분 필드 1',
`cl_extra_2` varchar(255) DEFAULT NULL COMMENT '여분 필드 2',
`cl_extra_3` varchar(255) DEFAULT NULL COMMENT '여분 필드 3',
`cl_extra_4` text DEFAULT NULL COMMENT '여분 필드 4',
`cl_extra_5` text DEFAULT NULL COMMENT '여분 필드 5',
PRIMARY KEY (`cl_id`),
UNIQUE KEY `target_lang` (`target_table`,`target_id`,`lang_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='[솔루션] 공용 다국어 정보';
-- 4. 변경 이력 테이블
CREATE TABLE IF NOT EXISTS `g5_form_option_history` (
`fh_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '고유 ID',
`table_name` varchar(50) NOT NULL COMMENT '변경된 테이블명',
`record_id` int(11) NOT NULL COMMENT '변경된 레코드 ID',
`action_type` varchar(10) NOT NULL COMMENT '작업 종류 (INSERT, UPDATE, DELETE)',
`change_data` longtext COMMENT '변경된 데이터 (JSON)',
`changed_at` datetime NOT NULL COMMENT '변경일',
`changed_by` varchar(20) NOT NULL COMMENT '변경자',
PRIMARY KEY (`fh_id`),
KEY `table_name_record_id` (`table_name`,`record_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='[솔루션] 폼 옵션 변경 이력';
+41
View File
@@ -0,0 +1,41 @@
document.addEventListener('DOMContentLoaded', function() {
const addResourceButton = document.getElementById('add-resource-btn');
const resourceFormContainer = document.getElementById('resource-form-container');
const cancelResourceButton = document.getElementById('cancel-resource-btn');
const resourceTypeRadios = document.querySelectorAll('input[name="resource_type"]');
const labelField = document.getElementById('label-field');
// '새 리소스 추가' 버튼 클릭 이벤트
if (addResourceButton) {
addResourceButton.addEventListener('click', function() {
if (resourceFormContainer) resourceFormContainer.style.display = 'block';
this.style.display = 'none';
});
}
// '취소' 버튼 클릭 이벤트
if (cancelResourceButton) {
cancelResourceButton.addEventListener('click', function() {
if (resourceFormContainer) resourceFormContainer.style.display = 'none';
if (addResourceButton) addResourceButton.style.display = 'inline-block';
});
}
// 💡 [핵심] 리소스 타입 라디오 버튼 변경 이벤트
function toggleResourceTypeFields() {
const selectedType = document.querySelector('input[name="resource_type"]:checked').value;
if (selectedType === 'LABEL') {
labelField.style.display = ''; // 테이블 행이므로 기본값으로
} else {
labelField.style.display = 'none';
}
}
if (resourceTypeRadios.length > 0) {
resourceTypeRadios.forEach(radio => {
radio.addEventListener('change', toggleResourceTypeFields);
});
// 페이지 로드 시 초기 상태 설정
toggleResourceTypeFields();
}
});
+152
View File
@@ -0,0 +1,152 @@
document.addEventListener('DOMContentLoaded', function() {
// =================================================================
// 1. UI 리소스 목록 페이지 (ui_manager_list.php)
// =================================================================
const addResourceButton = document.getElementById('add-resource-btn');
const resourceFormContainer = document.getElementById('resource-form-container');
const cancelResourceButton = document.getElementById('cancel-resource-btn');
const resourceTypeRadios = document.querySelectorAll('input[name="resource_type"]');
const labelField = document.getElementById('label-field');
const accordionItems = document.querySelectorAll('.accordion-item');
const searchForm = document.getElementById('fsearch');
const pageRowsSelect = document.getElementById('page_rows');
if (addResourceButton) {
addResourceButton.addEventListener('click', function() {
if (resourceFormContainer) resourceFormContainer.style.display = 'block';
this.style.display = 'none';
});
}
if (cancelResourceButton) {
cancelResourceButton.addEventListener('click', function() {
if (resourceFormContainer) resourceFormContainer.style.display = 'none';
if (addResourceButton) addResourceButton.style.display = 'inline-block';
});
}
function toggleResourceTypeFields() {
if (!document.querySelector('input[name="resource_type"]:checked')) return;
const selectedType = document.querySelector('input[name="resource_type"]:checked').value;
if (labelField) {
labelField.style.display = (selectedType === 'LABEL') ? '' : 'none';
}
}
if (resourceTypeRadios.length > 0) {
resourceTypeRadios.forEach(radio => radio.addEventListener('change', toggleResourceTypeFields));
toggleResourceTypeFields();
}
if (accordionItems.length > 0) {
accordionItems.forEach(item => {
const header = item.querySelector('.accordion-header');
const content = item.querySelector('.accordion-content');
if (header && content) {
header.addEventListener('click', () => {
item.classList.toggle('active');
content.style.display = item.classList.contains('active') ? 'block' : 'none';
});
}
});
}
if (searchForm) {
searchForm.addEventListener('keydown', function(event) {
if (event.key === 'Enter') {
// 엔터키 입력 시 기본 동작(폼 제출)을 막지 않음
}
});
}
if (pageRowsSelect) {
pageRowsSelect.addEventListener('change', function() {
if (searchForm) {
searchForm.submit();
}
});
}
// =================================================================
// 💡 [핵심] 옵션/카테고리 관리 페이지 (category_list.php) 로직 추가
// =================================================================
const addCategoryButton = document.getElementById('add-category-btn');
const categoryFormContainer = document.getElementById('category-form-container');
const cancelCategoryButton = document.getElementById('cancel-category-btn');
const categoryForm = document.getElementById('fcategoryform');
const editCategoryButtons = document.querySelectorAll('.btn_edit_category');
// "새 옵션 추가" 버튼 클릭
if (addCategoryButton && categoryFormContainer) {
addCategoryButton.addEventListener('click', function() {
if (categoryForm) {
categoryForm.reset();
categoryForm.w.value = ''; // 신규 등록 모드
categoryForm.fc_id.value = '';
const submitButton = categoryForm.querySelector('input[type="submit"]');
if (submitButton) submitButton.value = '저장';
}
categoryFormContainer.style.display = 'block';
this.style.display = 'none';
});
}
// "취소" 버튼 클릭
if (cancelCategoryButton && categoryFormContainer && addCategoryButton) {
cancelCategoryButton.addEventListener('click', function() {
categoryFormContainer.style.display = 'none';
addCategoryButton.style.display = 'inline-block';
});
}
// "수정" 버튼 클릭
if (editCategoryButtons.length > 0 && categoryForm) {
editCategoryButtons.forEach(button => {
button.addEventListener('click', function() {
const data = this.dataset;
// 폼에 데이터 채우기
categoryForm.w.value = 'u'; // 수정 모드
categoryForm.fc_id.value = data.fc_id;
categoryForm.parent_id.value = data.parent_id;
categoryForm.fc_key.value = data.fc_key;
categoryForm.cl_name.value = data.cl_name;
categoryForm.fc_order.value = data.fc_order;
const isUsedRadio = categoryForm.querySelector(`input[name="is_used"][value="${data.is_used}"]`);
if (isUsedRadio) isUsedRadio.checked = true;
const submitButton = categoryForm.querySelector('input[type="submit"]');
if (submitButton) submitButton.value = '수정';
if (categoryFormContainer) categoryFormContainer.style.display = 'block';
if (addCategoryButton) addCategoryButton.style.display = 'none';
categoryFormContainer.scrollIntoView({ behavior: 'smooth' });
});
});
}
// =================================================================
// 3. 리소스 삭제 기능
// =================================================================
const deleteResourceButtons = document.querySelectorAll('.btn_delete_resource');
if (deleteResourceButtons.length > 0) {
deleteResourceButtons.forEach(button => {
button.addEventListener('click', function(event) {
event.preventDefault();
const um_id = this.dataset.um_id;
const resource_desc_element = this.closest('tr').querySelector('.td_left');
const resource_desc = resource_desc_element ? resource_desc_element.textContent.trim() : `ID: ${um_id}`;
if (confirm(`'${resource_desc}' 리소스를 정말 삭제하시겠습니까?\n\n이 리소스와 관련된 모든 하위 옵션(카테고리) 및 언어 데이터가 함께 영구적으로 삭제됩니다.`)) {
const qstr = new URLSearchParams(window.location.search).toString();
location.href = `./ui_manager_list.php?mode=delete&um_id=${um_id}&token=${g5_admin_token}&${qstr}`;
}
});
});
}
});
+200
View File
@@ -0,0 +1,200 @@
<?php
$sub_menu = '700100';
include_once('./_common.php');
// ... (폼 제출 처리 로직은 변경 없음) ...
$target_table = isset($_REQUEST['target_table']) ? preg_replace('/[^a-z0-9_]/i', '', $_REQUEST['target_table']) : '';
$target_id = isset($_REQUEST['target_id']) ? (int)$_REQUEST['target_id'] : 0;
if (!$target_table || !$target_id) {
alert('잘못된 접근입니다.');
}
if (isset($w) && $w == 'u' && $_SERVER['REQUEST_METHOD'] === 'POST') {
auth_check_menu($auth, $sub_menu, 'w');
check_admin_token();
$lang_code = trim($_POST['lang_code']);
$cl_name = trim($_POST['cl_name']);
$cl_description = trim($_POST['cl_description']);
if (!$lang_code) alert('언어 코드를 선택해주세요.');
if (!$cl_name) alert('이름/값을 입력해주세요.');
$sql = "INSERT INTO {$g5['common_lang_table']}
SET target_table = '{$target_table}',
target_id = '{$target_id}',
lang_code = '{$lang_code}',
cl_name = '{$cl_name}',
cl_description = '{$cl_description}',
updated_at = '".G5_TIME_YMDHIS."',
updated_by = '{$member['mb_id']}'
ON DUPLICATE KEY UPDATE
cl_name = '{$cl_name}',
cl_description = '{$cl_description}',
updated_at = '".G5_TIME_YMDHIS."',
updated_by = '{$member['mb_id']}'";
sql_query($sql);
goto_url("./lang_manager.php?target_table=$target_table&target_id=$target_id");
}
if (isset($mode) && $mode == 'delete') {
auth_check_menu($auth, $sub_menu, 'd');
check_admin_token();
$cl_id = isset($_GET['cl_id']) ? (int)$_GET['cl_id'] : 0;
if (!$cl_id) alert('cl_id 값이 없습니다.');
$sql = "DELETE FROM {$g5['common_lang_table']} WHERE cl_id = '{$cl_id}' AND target_table = '{$target_table}' AND target_id = '{$target_id}'";
sql_query($sql);
goto_url("./lang_manager.php?target_table=$target_table&target_id=$target_id");
}
auth_check_menu($auth, $sub_menu, 'r');
// 💡 [핵심 추가] 이전 페이지로 돌아가기 위한 링크 생성
$back_link = './ui_manager_list.php'; // 기본 돌아가기 링크
if ($target_table == $g5['form_category_table']) {
// 카테고리 다국어 관리였다면, 해당 카테고리가 속한 리소스의 옵션 관리 페이지로 돌아가야 함
$sql_back = "SELECT um_id FROM {$g5['form_category_table']} WHERE fc_id = '{$target_id}'";
$back_row = sql_fetch($sql_back);
if (isset($back_row['um_id'])) {
$back_link = './category_list.php?um_id=' . $back_row['um_id'];
}
}
// UiManager 클래스를 사용하여 사용 가능한 언어 목록 가져오기
$active_languages = ui_manager()->get_data('language_list');
// 현재 관리 대상의 기본 정보(한국어)를 가져옴
$sql = "SELECT cl_name FROM {$g5['common_lang_table']} WHERE target_table = '{$target_table}' AND target_id = '{$target_id}' AND lang_code = 'ko'";
$parent_info = sql_fetch($sql);
$parent_name = isset($parent_info['cl_name']) ? get_text($parent_info['cl_name']) : "ID: {$target_id}";
// 등록된 다국어 목록 조회
$sql = "SELECT * FROM {$g5['common_lang_table']} WHERE target_table = '{$target_table}' AND target_id = '{$target_id}' ORDER BY lang_code";
$result = sql_query($sql);
$lang_list = [];
while ($row = sql_fetch_array($result)) {
$lang_list[] = $row;
}
$lang_list_count = count($lang_list);
$g5['title'] = '다국어 관리';
include_once(G5_ADMIN_PATH . '/admin.head.php');
add_stylesheet('<link rel="stylesheet" href="' . G5_ADMIN_URL . '/code_manager/css/code_manager.css?ver=1.1">', 0);
?>
<div class="local_desc01 local_desc">
<p>
<strong>'<?php echo $parent_name; ?>'</strong> 항목에 대한 다국어 이름과 설명을 관리합니다.<br>
'언어 코드'는 중복하여 등록할 수 없습니다.
</p>
</div>
<section id="code_manager">
<h2 class="h2_frm">다국어 등록/수정</h2>
<form name="flangform" id="flangform" action="<?php echo $_SERVER['SCRIPT_NAME']; ?>" method="post">
<input type="hidden" name="w" value="u">
<input type="hidden" name="target_table" value="<?php echo $target_table; ?>">
<input type="hidden" name="target_id" value="<?php echo $target_id; ?>">
<input type="hidden" name="token" value="<?php echo get_admin_token(); ?>">
<div class="tbl_frm01 tbl_wrap">
<table>
<caption>다국어 정보 추가/수정 폼</caption>
<colgroup>
<col class="grid_4">
<col>
</colgroup>
<tbody>
<tr>
<th scope="row"><label for="lang_code">언어 코드</label></th>
<td>
<select name="lang_code" id="lang_code" required>
<option value="">선택</option>
<?php foreach ($active_languages as $lang_item) : ?>
<option value="<?php echo $lang_item['fc_key']; ?>"><?php echo get_text($lang_item['cl_name']); ?> (<?php echo $lang_item['fc_key']; ?>)</option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th scope="row"><label for="cl_name">이름/값</label></th>
<td>
<input type="text" name="cl_name" id="cl_name" required class="required frm_input" size="80">
</td>
</tr>
<tr>
<th scope="row"><label for="cl_description">부가 설명</label></th>
<td>
<textarea name="cl_description" id="cl_description" rows="5"></textarea>
<span class="frm_info">툴팁 등 부가적으로 사용될 설명을 입력합니다. (선택사항)</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="btn_confirm01 btn_confirm">
<!-- 💡 [핵심 수정] 목록으로 돌아가는 버튼 추가 -->
<a href="<?php echo $back_link; ?>" class="btn_cancel btn">목록으로</a>
<input type="submit" value="저장" class="btn_submit btn" accesskey="s">
</div>
</form>
<!-- 등록된 다국어 목록 테이블 -->
<div class="tbl_head01 tbl_wrap" style="margin-top: 20px;">
<h2 class="h2_frm">등록된 다국어 목록</h2>
<table>
<caption>등록된 다국어 목록</caption>
<thead>
<tr>
<th scope="col" style="width: 100px;">언어 코드</th>
<th scope="col">이름/값</th>
<th scope="col">부가 설명</th>
<th scope="col" style="width: 120px;">관리</th>
</tr>
</thead>
<tbody>
<?php if ($lang_list_count > 0) : ?>
<?php foreach ($lang_list as $lang) : ?>
<tr>
<td class="td_alignc"><?php echo get_text($lang['lang_code']); ?></td>
<td class="td_left"><?php echo get_text($lang['cl_name']); ?></td>
<td class="td_left"><?php echo get_text($lang['cl_description']); ?></td>
<td class="td_mng">
<button type="button" class="btn btn_02 btn_edit_lang"
data-lang_code="<?php echo get_text($lang['lang_code']); ?>"
data-cl_name="<?php echo get_text($lang['cl_name']); ?>"
data-cl_description="<?php echo get_text($lang['cl_description']); ?>">수정</button>
<a href="./lang_manager.php?mode=delete&amp;cl_id=<?php echo $lang['cl_id']; ?>&amp;target_table=<?php echo $target_table; ?>&amp;target_id=<?php echo $target_id; ?>&amp;token=<?php echo get_admin_token(); ?>"
class="btn btn_delete" onclick="return confirm('정말 삭제하시겠습니까?');">삭제</a>
</td>
</tr>
<?php endforeach; ?>
<?php else : ?>
<tr class="empty_table">
<td colspan="4">등록된 다국어 정보가 없습니다.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
<script>
document.addEventListener('DOMContentLoaded', function() {
const editLangButtons = document.querySelectorAll('.btn_edit_lang');
const langForm = document.getElementById('flangform');
if (editLangButtons.length > 0 && langForm) {
editLangButtons.forEach(button => {
button.addEventListener('click', function() {
const data = this.dataset;
langForm.lang_code.value = data.lang_code;
langForm.cl_name.value = data.cl_name;
langForm.cl_description.value = data.cl_description;
langForm.scrollIntoView({ behavior: 'smooth' });
});
});
}
});
</script>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
@@ -0,0 +1,194 @@
<?php
if (!defined('_GNUBOARD_')) exit;
/**
* SQL 파일을 기반으로 데이터베이스 스키마를 관리(생성/업데이트)하는 범용 클래스
*/
class SchemaManager
{
private $sql_file_path;
private $results;
/**
* 생성자
* @param string $sql_file_path install.sql 파일의 절대 경로
*/
public function __construct($sql_file_path)
{
if (!file_exists($sql_file_path)) {
throw new Exception($sql_file_path . ' 파일을 찾을 수 없습니다.');
}
$this->sql_file_path = $sql_file_path;
$this->results = [
'created' => [],
'existing' => [],
'updated' => [],
'failed' => [],
'errors' => [],
];
}
/**
* 스키마 설치/업데이트를 실행합니다.
*/
public function execute()
{
$sql_statements = $this->parse_sql_file();
foreach ($sql_statements as $stmt) {
// CREATE TABLE 문인지 확인
if (preg_match('/^CREATE\s+TABLE/i', $stmt)) {
$schema = $this->parse_create_table_sql($stmt);
if ($schema && !empty($schema['name'])) {
$this->process_table_schema($stmt, $schema);
}
} else {
// CREATE TABLE 문이 아닌 다른 SQL 문 (e.g. INSERT, UPDATE)
sql_query($stmt, false);
}
}
}
/**
* 처리 결과를 반환합니다.
* @return array
*/
public function get_results()
{
return $this->results;
}
/**
* 테이블 스키마를 처리합니다. (생성 또는 업데이트)
* @param string $create_sql 전체 CREATE TABLE 구문
* @param array $schema 파싱된 스키마 정보
*/
private function process_table_schema($create_sql, $schema)
{
$table_name = $schema['name'];
if ($this->table_exists($table_name)) {
// 테이블이 존재하면, 컬럼 비교 및 추가
$this->results['existing'][] = $table_name;
$this->update_table_columns($table_name, $schema['columns']);
} else {
// 테이블이 존재하지 않으면, 새로 생성
if (sql_query($create_sql, false)) {
$this->results['created'][] = $table_name;
} else {
$this->results['failed'][] = $table_name;
$this->results['errors'][] = "<strong>{$table_name} 테이블 생성 실패</strong>: " . sql_error();
}
}
}
/**
* 테이블의 컬럼 구조를 업데이트합니다.
* @param string $table_name
* @param array $target_columns .sql 파일에 정의된 컬럼 목록
*/
private function update_table_columns($table_name, $target_columns)
{
$current_columns = $this->get_current_columns($table_name);
$added_columns_in_table = [];
foreach ($target_columns as $col_name => $col_definition) {
// 현재 테이블에 해당 컬럼이 없으면 추가
if (!isset($current_columns[$col_name])) {
$alter_sql = "ALTER TABLE `{$table_name}` ADD COLUMN `{$col_name}` {$col_definition}";
if (sql_query($alter_sql, false)) {
$added_columns_in_table[] = $col_name;
} else {
$this->results['failed'][] = "{$table_name} (컬럼: {$col_name})";
$this->results['errors'][] = "<strong>{$table_name} 테이블에 '{$col_name}' 컬럼 추가 실패</strong>: " . sql_error();
}
}
}
if (!empty($added_columns_in_table)) {
$this->results['updated'][$table_name] = $added_columns_in_table;
}
}
/**
* SQL 파일을 읽고 각 구문으로 분리합니다.
* @return array
*/
private function parse_sql_file()
{
$sql = file_get_contents($this->sql_file_path);
// 주석 제거 (SQL 주석 '--' 와 C-style '/* ... */' 주석)
$sql = preg_replace('/--.*/', '', $sql);
$sql = preg_replace('!/\*.*?\*/!s', '', $sql);
$sql = trim($sql);
// 세미콜론(;)을 기준으로 쿼리 분리
return array_filter(array_map('trim', explode(';', $sql)));
}
/**
* 테이블 존재 여부를 확인합니다.
* @param string $table_name
* @return bool
*/
private function table_exists($table_name)
{
$res = sql_query("SHOW TABLES LIKE '{$table_name}'", false);
return sql_num_rows($res) > 0;
}
/**
* 현재 DB에 있는 테이블의 컬럼 목록을 가져옵니다.
* @param string $table_name
* @return array
*/
private function get_current_columns($table_name)
{
$res = sql_query("SHOW COLUMNS FROM `{$table_name}`", false);
$columns = [];
while ($row = sql_fetch_array($res)) {
$columns[$row['Field']] = true;
}
return $columns;
}
/**
* CREATE TABLE SQL 구문에서 테이블명과 컬럼 정의를 파싱합니다.
* @param string $query CREATE TABLE 구문
* @return array|null
*/
private function parse_create_table_sql($query)
{
$table_name = '';
if (preg_match('/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+`?(\w+)`?/i', $query, $matches)) {
$table_name = $matches[1];
} else {
return null;
}
// 괄호 안의 내용만 추출
$start = strpos($query, '(');
$end = strrpos($query, ')');
if ($start === false || $end === false) {
return ['name' => $table_name, 'columns' => []];
}
$content = substr($query, $start + 1, $end - $start - 1);
// 줄 단위로 분리
$lines = explode("\n", $content);
$columns = [];
foreach ($lines as $line) {
$line = trim($line, " \t\n\r\0\x0B,"); // 양쪽 공백과 마지막 쉼표 제거
// 컬럼 정의 라인인지 확인 (첫 단어가 `column_name` 형태)
if (preg_match('/^`(\w+)`\s+(.*)/i', $line, $match)) {
$col_name = $match[1];
$col_definition = $match[2];
$columns[$col_name] = $col_definition;
}
}
return ['name' => $table_name, 'columns' => $columns];
}
}
@@ -0,0 +1,133 @@
<?php
if (!defined('_GNUBOARD_')) exit;
/**
* UI 리소스 관리 클래스
* Singleton 패턴을 사용하여 인스턴스를 한 번만 생성하고,
* 불러온 데이터를 캐시하여 DB 조회를 최소화합니다.
*/
class UiManager
{
private static $instance = null;
private $resources = []; // 데이터를 캐시할 배열
// 외부에서 new 키워드로 인스턴스 생성을 막음
private function __construct() {}
/**
* 클래스의 유일한 인스턴스를 반환합니다.
* @return UiManager
*/
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 'LABEL' 타입의 UI 텍스트를 가져옵니다.
* @param string $resource_code 리소스 코드
* @param string $lang 언어 코드 (기본값: 'ko')
* @return string 라벨 텍스트 (없으면 resource_code 반환)
*/
public function get_label($resource_code, $lang = 'ko')
{
// 캐시 확인
if (isset($this->resources['labels'][$lang][$resource_code])) {
return $this->resources['labels'][$lang][$resource_code];
}
global $g5;
$resource_code_escaped = sql_real_escape_string($resource_code);
$lang_escaped = sql_real_escape_string($lang);
$sql = "SELECT B.cl_name
FROM {$g5['ui_manager_table']} AS A
LEFT JOIN {$g5['common_lang_table']} AS B
ON (A.um_id = B.target_id AND B.target_table = '{$g5['ui_manager_table']}' AND B.lang_code = '{$lang_escaped}')
WHERE A.resource_code = '{$resource_code_escaped}' AND A.resource_type = 'LABEL'";
$row = sql_fetch($sql);
$label_text = $row['cl_name'] ?? $resource_code;
// 결과 캐시
$this->resources['labels'][$lang][$resource_code] = $label_text;
return $label_text;
}
/**
* 'DATA' 타입의 옵션 목록을 배열로 가져옵니다.
* @param string $resource_code 리소스 코드
* @param string $lang 언어 코드 (기본값: 'ko')
* @return array 옵션 목록 배열
*/
public function get_data($resource_code, $lang = 'ko')
{
// 캐시 확인
if (isset($this->resources['data'][$lang][$resource_code])) {
return $this->resources['data'][$lang][$resource_code];
}
global $g5;
$resource_code_escaped = sql_real_escape_string($resource_code);
$lang_escaped = sql_real_escape_string($lang);
$sql_um = "SELECT um_id FROM {$g5['ui_manager_table']} WHERE resource_code = '{$resource_code_escaped}' AND resource_type = 'DATA'";
$um_row = sql_fetch($sql_um);
if (!isset($um_row['um_id'])) {
$this->resources['data'][$lang][$resource_code] = []; // 빈 결과도 캐시
return [];
}
$um_id = $um_row['um_id'];
$sql = "SELECT A.fc_id, A.parent_id, A.fc_key, A.fc_order, B.cl_name
FROM {$g5['form_category_table']} AS A
LEFT JOIN {$g5['common_lang_table']} AS B
ON (A.fc_id = B.target_id AND B.target_table = '{$g5['form_category_table']}' AND B.lang_code = '{$lang_escaped}')
WHERE A.um_id = '{$um_id}' AND A.is_used = 1 AND A.is_deleted = 0
ORDER BY A.fc_order, A.fc_id";
$result = sql_query($sql);
$options = [];
while ($row = sql_fetch_array($result)) {
$options[] = $row;
}
// 결과 캐시
$this->resources['data'][$lang][$resource_code] = $options;
return $options;
}
/**
* 'DATA' 타입 리소스를 사용하여 HTML <select> 태그를 생성합니다.
* @param string $resource_code 리소스 코드
* @param string $select_name <select> 태그의 name 속성
* @param string $selected_value 미리 선택될 옵션의 값(fc_key)
* @param string $attributes <select> 태그에 추가할 HTML 속성 (e.g., 'id="my-id" class="my-class"')
* @param string $lang 언어 코드 (기본값: 'ko')
* @return string 생성된 HTML <select> 태그
*/
public function render_select($resource_code, $select_name, $selected_value = '', $attributes = '', $lang = 'ko')
{
$options = $this->get_data($resource_code, $lang);
if (empty($options)) {
return "<select name=\"{$select_name}\" {$attributes}><option value=\"\">옵션 없음</option></select>";
}
$html = "<select name=\"{$select_name}\" {$attributes}>";
$html .= "<option value=\"\">선택</option>";
foreach ($options as $option) {
$selected = ($option['fc_key'] == $selected_value) ? ' selected' : '';
$html .= "<option value=\"" . htmlspecialchars($option['fc_key']) . "\"{$selected}>" . htmlspecialchars($option['cl_name']) . "</option>";
}
$html .= "</select>";
return $html;
}
}
+106
View File
@@ -0,0 +1,106 @@
<?php
if (!defined('_GNUBOARD_')) exit;
/**
* UI 리소스 관리자 클래스 인스턴스를 반환하는 헬퍼 함수
* @return UiManager
*/
function ui_manager() {
// 클래스가 아직 로드되지 않았다면 인스턴스 생성
if (!class_exists('UiManager')) {
// UiManager 클래스 정의
class UiManager
{
private static $instance = null;
private $resources = []; // 데이터를 캐시할 배열
private function __construct() {}
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function get_label($resource_code, $lang = 'ko')
{
if (isset($this->resources['labels'][$lang][$resource_code])) {
return $this->resources['labels'][$lang][$resource_code];
}
global $g5;
$resource_code_escaped = sql_real_escape_string($resource_code);
$lang_escaped = sql_real_escape_string($lang);
$sql = "SELECT B.cl_name
FROM {$g5['ui_manager_table']} AS A
LEFT JOIN {$g5['common_lang_table']} AS B
ON (A.um_id = B.target_id AND B.target_table = '{$g5['ui_manager_table']}' AND B.lang_code = '{$lang_escaped}')
WHERE A.resource_code = '{$resource_code_escaped}' AND A.resource_type = 'LABEL'";
$row = sql_fetch($sql);
$label_text = $row['cl_name'] ?? $resource_code;
$this->resources['labels'][$lang][$resource_code] = $label_text;
return $label_text;
}
public function get_data($resource_code, $lang = 'ko')
{
if (isset($this->resources['data'][$lang][$resource_code])) {
return $this->resources['data'][$lang][$resource_code];
}
global $g5;
$resource_code_escaped = sql_real_escape_string($resource_code);
$lang_escaped = sql_real_escape_string($lang);
$sql_um = "SELECT um_id FROM {$g5['ui_manager_table']} WHERE resource_code = '{$resource_code_escaped}' AND resource_type = 'DATA'";
$um_row = sql_fetch($sql_um);
if (!isset($um_row['um_id'])) {
$this->resources['data'][$lang][$resource_code] = [];
return [];
}
$um_id = $um_row['um_id'];
$sql = "SELECT A.fc_id, A.parent_id, A.fc_key, A.fc_order, B.cl_name
FROM {$g5['form_category_table']} AS A
LEFT JOIN {$g5['common_lang_table']} AS B
ON (A.fc_id = B.target_id AND B.target_table = '{$g5['form_category_table']}' AND B.lang_code = '{$lang_escaped}')
WHERE A.um_id = '{$um_id}' AND A.is_used = 1 AND A.is_deleted = 0
ORDER BY A.fc_order, A.fc_id";
$result = sql_query($sql);
$options = [];
while ($row = sql_fetch_array($result)) {
$options[] = $row;
}
$this->resources['data'][$lang][$resource_code] = $options;
return $options;
}
public function render_select($resource_code, $select_name, $selected_value = '', $attributes = '', $lang = 'ko')
{
$options = $this->get_data($resource_code, $lang);
if (empty($options)) {
return "<select name=\"".htmlspecialchars($select_name)."\" {$attributes}><option value=\"\">옵션 없음</option></select>";
}
$html = "<select name=\"".htmlspecialchars($select_name)."\" {$attributes}>";
$html .= "<option value=\"\">선택</option>";
foreach ($options as $option) {
$selected = ($option['fc_key'] == $selected_value) ? ' selected' : '';
$html .= "<option value=\"" . htmlspecialchars($option['fc_key']) . "\"{$selected}>" . htmlspecialchars($option['cl_name']) . "</option>";
}
$html .= "</select>";
return $html;
}
}
}
return UiManager::getInstance();
}
+174
View File
@@ -0,0 +1,174 @@
<?php
$sub_menu = '100350';
include_once('./_common.php');
$w = isset($_REQUEST['w']) ? substr(trim($_REQUEST['w']), 0, 1) : '';
$um_id = isset($_REQUEST['um_id']) ? (int)$_REQUEST['um_id'] : 0;
// ==================================================================
// 💡 [핵심] 폼 제출(수정) 처리 로직
// ==================================================================
if ($w == 'u' && $_SERVER['REQUEST_METHOD'] === 'POST') {
auth_check_menu($auth, $sub_menu, 'w');
check_admin_token();
if (!$um_id) {
alert('um_id 값이 없습니다.');
}
// 입력값 정리
$screen_code = trim($_POST['screen_code']);
$group_code = trim($_POST['group_code']);
$resource_code = trim($_POST['resource_code']);
$resource_type = trim($_POST['resource_type']);
$resource_desc = trim($_POST['resource_desc']);
$cl_name = isset($_POST['cl_name']) ? trim($_POST['cl_name']) : '';
// 유효성 검사
if (!$screen_code || !$group_code || !$resource_code || !$resource_type) {
alert('필수 항목을 모두 입력해주세요.');
}
if ($resource_type == 'LABEL' && !$cl_name) {
alert('UI 라벨 타입은 한국어 라벨명을 필수로 입력해야 합니다.');
}
// 1. g5_ui_manager 테이블 정보 업데이트
$sql = "UPDATE {$g5['ui_manager_table']}
SET screen_code = '{$screen_code}',
group_code = '{$group_code}',
resource_code = '{$resource_code}',
resource_type = '{$resource_type}',
resource_desc = '{$resource_desc}',
updated_at = '".G5_TIME_YMDHIS."',
updated_by = '{$member['mb_id']}'
WHERE um_id = '{$um_id}'";
sql_query($sql);
// 2. 리소스 타입이 'LABEL'인 경우, g5_common_lang 테이블 정보 업데이트 (없으면 생성)
if ($resource_type == 'LABEL') {
$sql = "SELECT cl_id FROM {$g5['common_lang_table']} WHERE target_table = '{$g5['ui_manager_table']}' AND target_id = '{$um_id}' AND lang_code = 'ko'";
$lang_row = sql_fetch($sql);
if (isset($lang_row['cl_id'])) { // 기존 데이터가 있으면 UPDATE
$sql = "UPDATE {$g5['common_lang_table']}
SET cl_name = '{$cl_name}',
updated_at = '".G5_TIME_YMDHIS."',
updated_by = '{$member['mb_id']}'
WHERE cl_id = '{$lang_row['cl_id']}'";
} else { // 기존 데이터가 없으면 INSERT
$sql = "INSERT INTO {$g5['common_lang_table']}
SET target_table = '{$g5['ui_manager_table']}',
target_id = '{$um_id}',
lang_code = 'ko',
cl_name = '{$cl_name}',
updated_at = '".G5_TIME_YMDHIS."',
updated_by = '{$member['mb_id']}'";
}
sql_query($sql);
}
goto_url('./ui_manager_list.php');
}
// ==================================================================
auth_check_menu($auth, $sub_menu, 'r');
// 수정할 리소스 데이터 조회
if ($w == 'u') {
if (!$um_id) {
alert('um_id 값이 없습니다.', './ui_manager_list.php');
}
$sql = "SELECT A.*, B.cl_name
FROM {$g5['ui_manager_table']} AS A
LEFT JOIN {$g5['common_lang_table']} AS B
ON (A.um_id = B.target_id AND B.target_table = '{$g5['ui_manager_table']}' AND B.lang_code = 'ko')
WHERE A.um_id = '{$um_id}'";
$ui_resource = sql_fetch($sql);
if (!isset($ui_resource['um_id'])) {
alert('존재하지 않는 리소스입니다.', './ui_manager_list.php');
}
} else {
alert('w 값이 올바르지 않습니다.', './ui_manager_list.php');
}
$g5['title'] = 'UI 리소스 수정';
include_once(G5_ADMIN_PATH . '/admin.head.php');
add_stylesheet('<link rel="stylesheet" href="' . G5_ADMIN_URL . '/code_manager/css/code_manager.css?ver=1.1">', 0);
?>
<section id="code_manager_form">
<h2 class="h2_frm">UI 리소스 수정</h2>
<form name="fresourceform" id="fresourceform" action="<?php echo $_SERVER['SCRIPT_NAME']; ?>" method="post">
<input type="hidden" name="w" value="u">
<input type="hidden" name="um_id" value="<?php echo $um_id; ?>">
<input type="hidden" name="token" value="<?php echo get_admin_token(); ?>">
<div class="tbl_frm01 tbl_wrap">
<table>
<caption>UI 리소스 수정 폼</caption>
<colgroup>
<col class="grid_4">
<col>
</colgroup>
<tbody>
<tr>
<th scope="row"><label for="screen_code">화면 코드</label></th>
<td>
<input type="text" name="screen_code" id="screen_code" required class="required frm_input" size="30" value="<?php echo get_text($ui_resource['screen_code']); ?>">
<span class="frm_info">리소스가 사용될 화면의 고유 코드 (예: order_form, member_join)</span>
</td>
</tr>
<tr>
<th scope="row"><label for="group_code">그룹 코드</label></th>
<td>
<input type="text" name="group_code" id="group_code" required class="required frm_input" size="30" value="<?php echo get_text($ui_resource['group_code']); ?>">
<span class="frm_info">화면 내에서 리소스를 묶어줄 그룹 코드 (예: address_info, common_options)</span>
</td>
</tr>
<tr>
<th scope="row">리소스 타입</th>
<td>
<label><input type="radio" name="resource_type" value="LABEL" <?php echo get_checked($ui_resource['resource_type'], 'LABEL'); ?>> UI 라벨 (단일 텍스트)</label>
<label><input type="radio" name="resource_type" value="DATA" <?php echo get_checked($ui_resource['resource_type'], 'DATA'); ?>> 데이터 (선택 옵션)</label>
</td>
</tr>
<tr class="resource-type-field" id="label-field">
<th scope="row"><label for="cl_name">한국어 라벨명</label></th>
<td>
<input type="text" name="cl_name" id="cl_name" class="frm_input" size="50" value="<?php echo get_text($ui_resource['cl_name']); ?>">
<span class="frm_info">화면에 표시될 실제 텍스트 (예: 집의 유형, 창호 재질)</span>
</td>
</tr>
<tr>
<th scope="row"><label for="resource_code">리소스 코드</label></th>
<td>
<input type="text" name="resource_code" id="resource_code" required class="required frm_input" size="30" value="<?php echo get_text($ui_resource['resource_code']); ?>">
<span class="frm_info">개발자가 이 리소스를 호출할 때 사용할 고유 코드 (예: house_type_label, house_type_data)</span>
</td>
</tr>
<tr>
<th scope="row"><label for="resource_desc">설명</label></th>
<td>
<input type="text" name="resource_desc" id="resource_desc" class="frm_input" size="80" value="<?php echo get_text($ui_resource['resource_desc']); ?>">
<span class="frm_info">이 리소스의 용도에 대한 설명 (관리자 참고용)</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="btn_confirm01 btn_confirm">
<a href="./ui_manager_list.php" class="btn_cancel btn">목록으로</a>
<input type="submit" value="수정" class="btn_submit btn" accesskey="s">
</div>
</form>
</section>
<script src="<?php echo G5_ADMIN_URL; ?>/code_manager/js/ui_manager.js?ver=1.2"></script>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
+351
View File
@@ -0,0 +1,351 @@
<?php
$sub_menu = '700100';
include_once('./_common.php');
// ==================================================================
// 삭제 처리 로직
// ==================================================================
if (isset($_GET['mode']) && $_GET['mode'] == 'delete') {
auth_check_menu($auth, $sub_menu, 'd');
check_admin_token();
$um_id = isset($_GET['um_id']) ? (int)$_GET['um_id'] : 0;
if (!$um_id) {
alert('um_id 값이 없습니다.');
}
$sql = "SELECT resource_type FROM {$g5['ui_manager_table']} WHERE um_id = '{$um_id}'";
$row = sql_fetch($sql);
if (isset($row['resource_type'])) {
if ($row['resource_type'] == 'DATA') {
$sql_delete_lang = "DELETE FROM {$g5['common_lang_table']} WHERE target_table = '{$g5['form_category_table']}' AND target_id IN (SELECT fc_id FROM {$g5['form_category_table']} WHERE um_id = '{$um_id}')";
sql_query($sql_delete_lang);
$sql_delete_cat = "DELETE FROM {$g5['form_category_table']} WHERE um_id = '{$um_id}'";
sql_query($sql_delete_cat);
}
$sql_delete_main_lang = "DELETE FROM {$g5['common_lang_table']} WHERE target_table = '{$g5['ui_manager_table']}' AND target_id = '{$um_id}'";
sql_query($sql_delete_main_lang);
$sql_delete_main = "DELETE FROM {$g5['ui_manager_table']} WHERE um_id = '{$um_id}'";
sql_query($sql_delete_main);
}
// 삭제 후 현재 페이지와 검색 조건을 유지하도록 $qstr 사용
goto_url('./ui_manager_list.php?'.$qstr);
}
// ==================================================================
// 💡 [핵심 수정 1] 폼 제출 처리 로직 (신규 등록)
// ==================================================================
// GnuBoard 표준에 따라 'w' 값이 비어있을 때를 신규 등록으로 처리합니다.
if (empty($w) && $_SERVER['REQUEST_METHOD'] === 'POST') {
auth_check_menu($auth, $sub_menu, 'w');
check_admin_token();
// 입력값 정리
$screen_code = trim($_POST['screen_code']);
$group_code = trim($_POST['group_code']);
$resource_code = trim($_POST['resource_code']);
$resource_type = trim($_POST['resource_type']);
$resource_desc = trim($_POST['resource_desc']);
$cl_name = isset($_POST['cl_name']) ? trim($_POST['cl_name']) : '';
// 유효성 검사
if (!$screen_code || !$group_code || !$resource_code || !$resource_type) {
alert('필수 항목을 모두 입력해주세요.');
}
if ($resource_type == 'LABEL' && !$cl_name) {
alert('UI 라벨 타입은 한국어 라벨명을 필수로 입력해야 합니다.');
}
// 중복 체크
$sql = "SELECT COUNT(*) as cnt FROM {$g5['ui_manager_table']} WHERE screen_code = '{$screen_code}' AND group_code = '{$group_code}' AND resource_code = '{$resource_code}'";
$row = sql_fetch($sql);
if ($row['cnt']) {
alert('이미 동일한 화면/그룹/리소스 코드로 등록된 리소스가 존재합니다.');
}
// 1. g5_ui_manager 테이블에 정보 INSERT
$sql = "INSERT INTO {$g5['ui_manager_table']}
SET screen_code = '{$screen_code}',
group_code = '{$group_code}',
resource_code = '{$resource_code}',
resource_type = '{$resource_type}',
resource_desc = '{$resource_desc}',
is_used = '1',
created_at = '".G5_TIME_YMDHIS."',
created_by = '{$member['mb_id']}',
updated_at = '".G5_TIME_YMDHIS."',
updated_by = '{$member['mb_id']}'";
sql_query($sql);
$um_id = sql_insert_id();
// 2. 리소스 타입이 'LABEL'인 경우, g5_common_lang 테이블에 정보 INSERT
if ($resource_type == 'LABEL' && $cl_name) {
$sql = "INSERT INTO {$g5['common_lang_table']}
SET target_table = '{$g5['ui_manager_table']}',
target_id = '{$um_id}',
lang_code = 'ko',
cl_name = '{$cl_name}',
updated_at = '".G5_TIME_YMDHIS."',
updated_by = '{$member['mb_id']}'";
sql_query($sql);
}
goto_url('./ui_manager_list.php');
}
// ==================================================================
auth_check_menu($auth, $sub_menu, 'r');
// 검색 및 페이징 변수 처리
$sfl = isset($_GET['sfl']) ? trim($_GET['sfl']) : '';
$stx = isset($_GET['stx']) ? trim($_GET['stx']) : '';
$page_rows = isset($_GET['page_rows']) && (int)$_GET['page_rows'] > 0 ? (int)$_GET['page_rows'] : 15;
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($page < 1) $page = 1;
// 페이지당 목록 수($page_rows)를 $qstr에 추가하여 다른 링크에서도 유지되도록 함
$qstr .= ($qstr ? '&amp;' : '') . 'page_rows=' . urlencode($page_rows);
$sql_search = "";
if ($sfl && $stx) {
$stx_escaped = sql_real_escape_string($stx);
$sql_search = " WHERE ";
switch ($sfl) {
case 'screen_code':
case 'group_code':
case 'resource_code':
case 'resource_desc':
$sql_search .= "A.{$sfl} LIKE '%{$stx_escaped}%'";
break;
case 'cl_name':
$sql_search .= "B.cl_name LIKE '%{$stx_escaped}%'";
break;
default: // '전체' 검색
$sql_search .= " ( A.screen_code LIKE '%{$stx_escaped}%' OR A.group_code LIKE '%{$stx_escaped}%' OR A.resource_code LIKE '%{$stx_escaped}%' OR A.resource_desc LIKE '%{$stx_escaped}%' OR B.cl_name LIKE '%{$stx_escaped}%' ) ";
break;
}
}
// 1. 검색 조건에 맞는 모든 리소스를 한 번에 가져옴
$sql = "SELECT A.*, B.cl_name
FROM {$g5['ui_manager_table']} AS A
LEFT JOIN {$g5['common_lang_table']} AS B
ON (A.um_id = B.target_id AND B.target_table = '{$g5['ui_manager_table']}' AND B.lang_code = 'ko')
{$sql_search}
ORDER BY A.screen_code, A.group_code, A.resource_code";
$result = sql_query($sql);
// 2. PHP에서 화면별로 그룹화
$all_grouped_resources = [];
$total_resource_count = 0;
while ($row = sql_fetch_array($result)) {
$all_grouped_resources[$row['screen_code']][] = $row;
$total_resource_count++;
}
// 3. 그룹화된 결과를 기준으로 페이징 처리
$total_count = count($all_grouped_resources); // 전체 '화면' 수
$total_page = ceil($total_count / $page_rows);
$from_record = ($page - 1) * $page_rows;
// 현재 페이지에 해당하는 그룹만 잘라냄
$paged_groups = array_slice($all_grouped_resources, $from_record, $page_rows, true);
$g5['title'] = 'UI 리소스 관리';
include_once(G5_ADMIN_PATH . '/admin.head.php');
add_stylesheet('<link rel="stylesheet" href="' . G5_ADMIN_URL . '/code_manager/css/code_manager.css?ver=1.3">', 0);
?>
<div class="local_desc01 local_desc">
<p>
웹사이트의 모든 화면에 사용되는 텍스트(라벨)와 선택 옵션(데이터)을 체계적으로 관리합니다.<br>
'화면 코드' 별로 그룹화되어 표시되며, 각 그룹을 클릭하여 내용을 확인하거나 수정할 수 있습니다.
</p>
</div>
<!-- 검색 폼 -->
<section id="resource_search_form">
<h2 class="h2_frm">리소스 검색</h2>
<form name="fsearch" id="fsearch" method="get">
<div class="search-form-inner">
<label for="page_rows" class="sound_only">페이지당 개수</label>
<select name="page_rows" id="page_rows" class="frm_input">
<option value="15" <?php echo get_selected($page_rows, 15); ?>>15개씩</option>
<option value="30" <?php echo get_selected($page_rows, 30); ?>>30개씩</option>
<option value="50" <?php echo get_selected($page_rows, 50); ?>>50개씩</option>
<option value="100" <?php echo get_selected($page_rows, 100); ?>>100개씩</option>
</select>
<label for="sfl" class="sound_only">검색대상</label>
<select name="sfl" id="sfl">
<option value="all" <?php echo get_selected($sfl, 'all'); ?>>전체</option>
<option value="screen_code" <?php echo get_selected($sfl, 'screen_code'); ?>>화면 코드</option>
<option value="group_code" <?php echo get_selected($sfl, 'group_code'); ?>>그룹 코드</option>
<option value="resource_code" <?php echo get_selected($sfl, 'resource_code'); ?>>리소스 코드</option>
<option value="resource_desc" <?php echo get_selected($sfl, 'resource_desc'); ?>>설명</option>
<option value="cl_name" <?php echo get_selected($sfl, 'cl_name'); ?>>라벨명</option>
</select>
<label for="stx" class="sound_only">검색어</label>
<input type="text" name="stx" value="<?php echo get_text($stx) ?>" id="stx" class="frm_input" size="30">
<input type="submit" value="검색" class="btn_submit">
</div>
</form>
</section>
<section id="code_manager">
<div class="code-manager-header">
<h2 class="code-manager-title">UI 리소스 목록 (총 <?php echo number_format($total_count); ?>개 화면)</h2>
<div class="code-manager-actions">
<button type="button" id="add-resource-btn" class="btn btn_01">
<i class="fa fa-plus" aria-hidden="true"></i> 새 리소스 추가
</button>
</div>
</div>
<!-- 새 리소스 추가 폼 -->
<div id="resource-form-container" style="display: none;">
<form name="fresourceform" id="fresourceform" action="<?php echo $_SERVER['SCRIPT_NAME']; ?>" method="post">
<!-- 💡 [핵심 수정 2] 신규 등록이므로 w 값을 비워둡니다. -->
<input type="hidden" name="w" value="">
<input type="hidden" name="token" value="<?php echo get_admin_token(); ?>">
<div class="tbl_frm01 tbl_wrap">
<table>
<caption>UI 리소스 추가 폼</caption>
<colgroup>
<col class="grid_4">
<col>
</colgroup>
<tbody>
<tr>
<th scope="row"><label for="screen_code">화면 코드</label></th>
<td>
<input type="text" name="screen_code" id="screen_code" required class="required frm_input" size="30">
<span class="frm_info">리소스가 사용될 화면의 고유 코드 (예: order_form, member_join)</span>
</td>
</tr>
<tr>
<th scope="row"><label for="group_code">그룹 코드</label></th>
<td>
<input type="text" name="group_code" id="group_code" required class="required frm_input" size="30">
<span class="frm_info">화면 내에서 리소스를 묶어줄 그룹 코드 (예: address_info, common_options)</span>
</td>
</tr>
<tr>
<th scope="row">리소스 타입</th>
<td>
<label><input type="radio" name="resource_type" value="LABEL" checked> UI 라벨 (단일 텍스트)</label>
<label><input type="radio" name="resource_type" value="DATA"> 데이터 (선택 옵션)</label>
</td>
</tr>
<tr class="resource-type-field" id="label-field">
<th scope="row"><label for="cl_name">한국어 라벨명</label></th>
<td>
<input type="text" name="cl_name" id="cl_name" class="frm_input" size="50">
<span class="frm_info">화면에 표시될 실제 텍스트 (예: 집의 유형, 창호 재질)</span>
</td>
</tr>
<tr>
<th scope="row"><label for="resource_code">리소스 코드</label></th>
<td>
<input type="text" name="resource_code" id="resource_code" required class="required frm_input" size="30">
<span class="frm_info">개발자가 이 리소스를 호출할 때 사용할 고유 코드 (예: house_type_label, house_type_data)</span>
</td>
</tr>
<tr>
<th scope="row"><label for="resource_desc">설명</label></th>
<td>
<input type="text" name="resource_desc" id="resource_desc" class="frm_input" size="80">
<span class="frm_info">이 리소스의 용도에 대한 설명 (관리자 참고용)</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="btn_confirm01 btn_confirm">
<button type="button" id="cancel-resource-btn" class="btn_cancel btn">취소</button>
<input type="submit" value="리소스 등록" class="btn_submit btn" accesskey="s">
</div>
</form>
</div>
<!-- 아코디언 형태의 리소스 목록 -->
<div id="resource-list-accordion">
<?php if (count($paged_groups) == 0) : ?>
<div class="empty_list">표시할 리소스가 없습니다.</div>
<?php else : ?>
<?php foreach ($paged_groups as $screen_code => $resources) : ?>
<div class="accordion-item">
<div class="accordion-header">
<span class="screen-title"><i class="fa fa-desktop"></i> 화면: <strong><?php echo get_text($screen_code); ?></strong></span>
<span class="resource-count"><?php echo count($resources); ?>개 리소스</span>
<i class="fa fa-chevron-down accordion-icon"></i>
</div>
<div class="accordion-content">
<div class="tbl_head01 tbl_wrap">
<table>
<colgroup>
<col style="width: 15%;">
<col style="width: 20%;">
<col style="width: 10%;">
<col>
<col style="width: 240px;">
</colgroup>
<thead>
<tr>
<th scope="col">그룹 코드</th>
<th scope="col">리소스 코드</th>
<th scope="col">타입</th>
<th scope="col">설명</th>
<th scope="col">관리</th>
</tr>
</thead>
<tbody>
<?php foreach ($resources as $res) : ?>
<tr>
<td><?php echo get_text($res['group_code']); ?></td>
<td><?php echo get_text($res['resource_code']); ?></td>
<td>
<?php if ($res['resource_type'] == 'LABEL'): ?>
<span class="res-type-label">라벨</span>
<?php else: ?>
<span class="res-type-data">데이터</span>
<?php endif; ?>
</td>
<td class="td_left"><?php echo get_text($res['resource_desc']); ?></td>
<td class="td_mng td_mng_s">
<?php if ($res['resource_type'] == 'DATA') : ?>
<a href="./category_list.php?um_id=<?php echo $res['um_id']; ?>" class="btn btn_03">옵션 관리</a>
<?php endif; ?>
<a href="./ui_manager_form.php?w=u&amp;um_id=<?php echo $res['um_id']; ?>&amp;<?php echo $qstr; ?>" class="btn btn_02">수정</a>
<a href="./lang_manager.php?target_table=<?php echo $g5['ui_manager_table']; ?>&amp;target_id=<?php echo $res['um_id']; ?>" class="btn btn_01">다국어</a>
<button type="button" class="btn btn_delete btn_delete_resource" data-um_id="<?php echo $res['um_id']; ?>">삭제</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<!-- 페이징 링크 출력 -->
<div class="pagination_wrap">
<?php
$paging_url = $_SERVER['SCRIPT_NAME'].'?'.$qstr;
echo get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, $paging_url);
?>
</div>
</section>
<script>
var g5_admin_token = "<?php echo get_admin_token(); ?>";
</script>
<script src="<?php echo G5_ADMIN_URL; ?>/code_manager/js/ui_manager.js?ver=1.3"></script>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
+1823
View File
File diff suppressed because it is too large Load Diff
+387
View File
@@ -0,0 +1,387 @@
<?php
$sub_menu = "100100";
require_once './_common.php';
check_demo();
auth_check_menu($auth, $sub_menu, 'w');
if ($is_admin != 'super') {
alert('최고관리자만 접근 가능합니다.');
}
$sql = " select * from {$g5['config_table']} limit 1";
$ori_config = sql_fetch($sql);
$cf_title = isset($_POST['cf_title']) ? strip_tags(clean_xss_attributes($_POST['cf_title'])) : '';
$cf_admin = isset($_POST['cf_admin']) ? clean_xss_tags($_POST['cf_admin'], 1, 1) : '';
$mb = get_member($cf_admin);
if (!(isset($mb['mb_id']) && $mb['mb_id'])) {
alert('최고관리자 회원아이디가 존재하지 않습니다.');
}
check_admin_token();
$cf_social_servicelist = !empty($_POST['cf_social_servicelist']) ? implode(',', $_POST['cf_social_servicelist']) : '';
$check_keys = array('cf_cert_kcb_cd', 'cf_cert_kcp_cd', 'cf_cert_kcp_enckey', 'cf_editor', 'cf_recaptcha_site_key', 'cf_recaptcha_secret_key', 'cf_naver_clientid', 'cf_naver_secret', 'cf_facebook_appid', 'cf_facebook_secret', 'cf_twitter_key', 'cf_twitter_secret', 'cf_google_clientid', 'cf_google_secret', 'cf_googl_shorturl_apikey', 'cf_kakao_rest_key', 'cf_kakao_client_secret', 'cf_kakao_js_apikey', 'cf_payco_clientid', 'cf_payco_secret', 'cf_cert_kg_cd', 'cf_cert_kg_mid');
foreach ($check_keys as $key) {
if (isset($_POST[$key]) && $_POST[$key]) {
$_POST[$key] = preg_replace('/[^a-z0-9_\-\.]/i', '', $_POST[$key]);
}
}
$_POST['cf_icode_server_port'] = isset($_POST['cf_icode_server_port']) ? preg_replace('/[^0-9]/', '', $_POST['cf_icode_server_port']) : '7295';
if (isset($_POST['cf_intercept_ip']) && $_POST['cf_intercept_ip']) {
$pattern = explode("\n", trim($_POST['cf_intercept_ip']));
for ($i = 0; $i < count($pattern); $i++) {
$pattern[$i] = trim($pattern[$i]);
if (empty($pattern[$i])) {
continue;
}
$pattern[$i] = str_replace(".", "\.", $pattern[$i]);
$pattern[$i] = str_replace("+", "[0-9\.]+", $pattern[$i]);
$pat = "/^{$pattern[$i]}$/";
if (preg_match($pat, $_SERVER['REMOTE_ADDR'])) {
alert("현재 접속 IP : " . $_SERVER['REMOTE_ADDR'] . " 가 차단될수 있기 때문에, 다른 IP를 입력해 주세요.");
}
}
}
$check_keys = array(
'cf_use_email_certify' => 'int',
'cf_use_homepage' => 'int',
'cf_req_homepage' => 'int',
'cf_use_tel' => 'int',
'cf_req_tel' => 'int',
'cf_use_hp' => 'int',
'cf_req_hp' => 'int',
'cf_use_addr' => 'int',
'cf_req_addr' => 'int',
'cf_use_signature' => 'int',
'cf_req_signature' => 'int',
'cf_use_profile' => 'int',
'cf_req_profile' => 'int',
'cf_register_level' => 'int',
'cf_register_point' => 'int',
'cf_icon_level' => 'int',
'cf_use_recommend' => 'int',
'cf_leave_day' => 'int',
'cf_search_part' => 'int',
'cf_email_use' => 'int',
'cf_email_wr_super_admin' => 'int',
'cf_email_wr_group_admin' => 'int',
'cf_email_wr_board_admin' => 'int',
'cf_email_wr_write' => 'int',
'cf_email_wr_comment_all' => 'int',
'cf_email_mb_super_admin' => 'int',
'cf_email_mb_member' => 'int',
'cf_email_po_super_admin' => 'int',
'cf_prohibit_id' => 'text',
'cf_prohibit_email' => 'text',
'cf_new_del' => 'int',
'cf_memo_del' => 'int',
'cf_visit_del' => 'int',
'cf_popular_del' => 'int',
'cf_use_member_icon' => 'int',
'cf_member_icon_size' => 'int',
'cf_member_icon_width' => 'int',
'cf_member_icon_height' => 'int',
'cf_member_img_size' => 'int',
'cf_member_img_width' => 'int',
'cf_member_img_height' => 'int',
'cf_login_minutes' => 'int',
'cf_formmail_is_member' => 'int',
'cf_page_rows' => 'int',
'cf_mobile_page_rows' => 'int',
'cf_social_login_use' => 'int',
'cf_cert_req' => 'int',
'cf_cert_use' => 'int',
'cf_cert_find' => 'int',
'cf_cert_ipin' => 'char',
'cf_cert_hp' => 'char',
'cf_cert_simple' => 'char',
'cf_cert_use_seed' => 'int',
'cf_admin_email' => 'char',
'cf_admin_email_name' => 'char',
'cf_add_script' => 'text',
'cf_use_point' => 'int',
'cf_point_term' => 'int',
'cf_use_copy_log' => 'int',
'cf_login_point' => 'int',
'cf_cut_name' => 'int',
'cf_nick_modify' => 'int',
'cf_new_skin' => 'char',
'cf_new_rows' => 'int',
'cf_search_skin' => 'char',
'cf_connect_skin' => 'char',
'cf_faq_skin' => 'char',
'cf_read_point' => 'int',
'cf_write_point' => 'int',
'cf_comment_point' => 'int',
'cf_download_point' => 'int',
'cf_write_pages' => 'int',
'cf_mobile_pages' => 'int',
'cf_link_target' => 'char',
'cf_delay_sec' => 'int',
'cf_filter' => 'text',
'cf_possible_ip' => 'text',
'cf_analytics' => 'text',
'cf_add_meta' => 'text',
'cf_member_skin' => 'char',
'cf_image_extension' => 'char',
'cf_flash_extension' => 'char',
'cf_movie_extension' => 'char',
'cf_visit' => 'char',
'cf_stipulation' => 'text',
'cf_privacy' => 'text',
'cf_open_modify' => 'int',
'cf_memo_send_point' => 'int',
'cf_mobile_new_skin' => 'char',
'cf_mobile_search_skin' => 'char',
'cf_mobile_connect_skin' => 'char',
'cf_mobile_faq_skin' => 'char',
'cf_mobile_member_skin' => 'char',
'cf_captcha_mp3' => 'char',
'cf_cert_limit' => 'int',
'cf_sms_use' => 'char',
'cf_sms_type' => 'char',
'cf_icode_id' => 'char',
'cf_icode_pw' => 'char',
'cf_icode_server_ip' => 'char',
'cf_captcha' => 'char',
'cf_syndi_token' => '',
'cf_syndi_except' => ''
);
for ($i = 1; $i <= 10; $i++) {
$check_keys['cf_' . $i . '_subj'] = isset($_POST['cf_' . $i . '_subj']) ? $_POST['cf_' . $i . '_subj'] : '';
$check_keys['cf_' . $i] = isset($_POST['cf_' . $i]) ? $_POST['cf_' . $i] : '';
}
foreach ($check_keys as $k => $v) {
if ($v === 'int') {
$_POST[$k] = isset($_POST[$k]) ? (int) $_POST[$k] : 0;
} else {
if (in_array($k, array('cf_analytics', 'cf_add_meta', 'cf_add_script', 'cf_stipulation', 'cf_privacy'))) {
$_POST[$k] = isset($_POST[$k]) ? $_POST[$k] : '';
} else {
$_POST[$k] = isset($_POST[$k]) ? strip_tags(clean_xss_attributes($_POST[$k])) : '';
}
}
}
// 본인확인을 사용할 경우 아이핀, 휴대폰인증 중 하나는 선택되어야 함
if ($_POST['cf_cert_use'] && !$_POST['cf_cert_ipin'] && !$_POST['cf_cert_hp'] && !$_POST['cf_cert_simple']) {
alert('본인확인을 위해 아이핀, 휴대폰 본인확인, KG이니시스 간편인증 서비스 중 하나 이상 선택해 주십시오.');
}
if (!$_POST['cf_cert_use']) {
$_POST['cf_cert_ipin'] = '';
$_POST['cf_cert_hp'] = '';
$_POST['cf_cert_simple'] = '';
}
// 관리자가 자동등록방지를 사용해야 할 경우 ( 기본환경설정에서 최고관리자, 방문자분석 스크립트, 추가 메타태그, 추가 script, css 변경시 )
$check_captcha = 0;
if ($cf_admin && $ori_config['cf_admin'] !== $cf_admin) {
$check_captcha = 1;
}
if ($_POST['cf_analytics'] && $ori_config['cf_analytics'] !== stripslashes($_POST['cf_analytics'])) {
$check_captcha = 1;
}
if ($_POST['cf_add_meta'] && $ori_config['cf_add_meta'] !== stripslashes($_POST['cf_add_meta'])) {
$check_captcha = 1;
}
if ($_POST['cf_add_script'] && $ori_config['cf_add_script'] !== stripslashes($_POST['cf_add_script'])) {
$check_captcha = 1;
}
if ($check_captcha) {
include_once(G5_CAPTCHA_PATH . '/captcha.lib.php');
if (!chk_captcha()) {
alert('자동등록방지 숫자가 틀렸습니다.');
}
}
$sql = " update {$g5['config_table']}
set cf_title = '{$cf_title}',
cf_admin = '{$cf_admin}',
cf_admin_email = '{$_POST['cf_admin_email']}',
cf_admin_email_name = '{$_POST['cf_admin_email_name']}',
cf_add_script = '{$_POST['cf_add_script']}',
cf_use_point = '{$_POST['cf_use_point']}',
cf_point_term = '{$_POST['cf_point_term']}',
cf_use_copy_log = '{$_POST['cf_use_copy_log']}',
cf_use_email_certify = '{$_POST['cf_use_email_certify']}',
cf_login_point = '{$_POST['cf_login_point']}',
cf_cut_name = '{$_POST['cf_cut_name']}',
cf_nick_modify = '{$_POST['cf_nick_modify']}',
cf_new_skin = '{$_POST['cf_new_skin']}',
cf_new_rows = '{$_POST['cf_new_rows']}',
cf_search_skin = '{$_POST['cf_search_skin']}',
cf_connect_skin = '{$_POST['cf_connect_skin']}',
cf_faq_skin = '{$_POST['cf_faq_skin']}',
cf_read_point = '{$_POST['cf_read_point']}',
cf_write_point = '{$_POST['cf_write_point']}',
cf_comment_point = '{$_POST['cf_comment_point']}',
cf_download_point = '{$_POST['cf_download_point']}',
cf_write_pages = '{$_POST['cf_write_pages']}',
cf_mobile_pages = '{$_POST['cf_mobile_pages']}',
cf_link_target = '{$_POST['cf_link_target']}',
cf_delay_sec = '{$_POST['cf_delay_sec']}',
cf_filter = '{$_POST['cf_filter']}',
cf_possible_ip = '" . trim($_POST['cf_possible_ip']) . "',
cf_intercept_ip = '" . trim($_POST['cf_intercept_ip']) . "',
cf_analytics = '{$_POST['cf_analytics']}',
cf_add_meta = '{$_POST['cf_add_meta']}',
cf_syndi_token = '{$_POST['cf_syndi_token']}',
cf_syndi_except = '{$_POST['cf_syndi_except']}',
cf_bbs_rewrite = '{$_POST['cf_bbs_rewrite']}',
cf_member_skin = '{$_POST['cf_member_skin']}',
cf_use_homepage = '{$_POST['cf_use_homepage']}',
cf_req_homepage = '{$_POST['cf_req_homepage']}',
cf_use_tel = '{$_POST['cf_use_tel']}',
cf_req_tel = '{$_POST['cf_req_tel']}',
cf_use_hp = '{$_POST['cf_use_hp']}',
cf_req_hp = '{$_POST['cf_req_hp']}',
cf_use_addr = '{$_POST['cf_use_addr']}',
cf_req_addr = '{$_POST['cf_req_addr']}',
cf_use_signature = '{$_POST['cf_use_signature']}',
cf_req_signature = '{$_POST['cf_req_signature']}',
cf_use_profile = '{$_POST['cf_use_profile']}',
cf_req_profile = '{$_POST['cf_req_profile']}',
cf_register_level = '{$_POST['cf_register_level']}',
cf_register_point = '{$_POST['cf_register_point']}',
cf_icon_level = '{$_POST['cf_icon_level']}',
cf_use_recommend = '{$_POST['cf_use_recommend']}',
cf_recommend_point = '{$_POST['cf_recommend_point']}',
cf_leave_day = '{$_POST['cf_leave_day']}',
cf_search_part = '{$_POST['cf_search_part']}',
cf_email_use = '{$_POST['cf_email_use']}',
cf_email_wr_super_admin = '{$_POST['cf_email_wr_super_admin']}',
cf_email_wr_group_admin = '{$_POST['cf_email_wr_group_admin']}',
cf_email_wr_board_admin = '{$_POST['cf_email_wr_board_admin']}',
cf_email_wr_write = '{$_POST['cf_email_wr_write']}',
cf_email_wr_comment_all = '{$_POST['cf_email_wr_comment_all']}',
cf_email_mb_super_admin = '{$_POST['cf_email_mb_super_admin']}',
cf_email_mb_member = '{$_POST['cf_email_mb_member']}',
cf_email_po_super_admin = '{$_POST['cf_email_po_super_admin']}',
cf_prohibit_id = '{$_POST['cf_prohibit_id']}',
cf_prohibit_email = '{$_POST['cf_prohibit_email']}',
cf_new_del = '{$_POST['cf_new_del']}',
cf_memo_del = '{$_POST['cf_memo_del']}',
cf_visit_del = '{$_POST['cf_visit_del']}',
cf_popular_del = '{$_POST['cf_popular_del']}',
cf_use_member_icon = '{$_POST['cf_use_member_icon']}',
cf_member_icon_size = '{$_POST['cf_member_icon_size']}',
cf_member_icon_width = '{$_POST['cf_member_icon_width']}',
cf_member_icon_height = '{$_POST['cf_member_icon_height']}',
cf_member_img_size = '{$_POST['cf_member_img_size']}',
cf_member_img_width = '{$_POST['cf_member_img_width']}',
cf_member_img_height = '{$_POST['cf_member_img_height']}',
cf_login_minutes = '{$_POST['cf_login_minutes']}',
cf_image_extension = '{$_POST['cf_image_extension']}',
cf_flash_extension = '{$_POST['cf_flash_extension']}',
cf_movie_extension = '{$_POST['cf_movie_extension']}',
cf_formmail_is_member = '{$_POST['cf_formmail_is_member']}',
cf_page_rows = '{$_POST['cf_page_rows']}',
cf_mobile_page_rows = '{$_POST['cf_mobile_page_rows']}',
cf_stipulation = '{$_POST['cf_stipulation']}',
cf_privacy = '{$_POST['cf_privacy']}',
cf_open_modify = '{$_POST['cf_open_modify']}',
cf_memo_send_point = '{$_POST['cf_memo_send_point']}',
cf_mobile_new_skin = '{$_POST['cf_mobile_new_skin']}',
cf_mobile_search_skin = '{$_POST['cf_mobile_search_skin']}',
cf_mobile_connect_skin = '{$_POST['cf_mobile_connect_skin']}',
cf_mobile_faq_skin = '{$_POST['cf_mobile_faq_skin']}',
cf_mobile_member_skin = '{$_POST['cf_mobile_member_skin']}',
cf_captcha_mp3 = '{$_POST['cf_captcha_mp3']}',
cf_editor = '{$_POST['cf_editor']}',
cf_cert_use = '{$_POST['cf_cert_use']}',
cf_cert_find = '{$_POST['cf_cert_find']}',
cf_cert_ipin = '{$_POST['cf_cert_ipin']}',
cf_cert_hp = '{$_POST['cf_cert_hp']}',
cf_cert_simple = '{$_POST['cf_cert_simple']}',
cf_cert_use_seed = '".(int)$_POST['cf_cert_use_seed']."',
cf_cert_kg_cd = '{$_POST['cf_cert_kg_cd']}',
cf_cert_kg_mid = '" . trim($_POST['cf_cert_kg_mid']) . "',
cf_cert_kcb_cd = '{$_POST['cf_cert_kcb_cd']}',
cf_cert_kcp_cd = '{$_POST['cf_cert_kcp_cd']}',
cf_cert_kcp_enckey = '{$_POST['cf_cert_kcp_enckey']}',
cf_cert_limit = '{$_POST['cf_cert_limit']}',
cf_cert_req = '{$_POST['cf_cert_req']}',
cf_sms_use = '{$_POST['cf_sms_use']}',
cf_sms_type = '{$_POST['cf_sms_type']}',
cf_icode_id = '{$_POST['cf_icode_id']}',
cf_icode_pw = '{$_POST['cf_icode_pw']}',
cf_icode_token_key = '{$_POST['cf_icode_token_key']}',
cf_icode_server_ip = '{$_POST['cf_icode_server_ip']}',
cf_icode_server_port = '{$_POST['cf_icode_server_port']}',
cf_googl_shorturl_apikey = '{$_POST['cf_googl_shorturl_apikey']}',
cf_kakao_js_apikey = '{$_POST['cf_kakao_js_apikey']}',
cf_facebook_appid = '{$_POST['cf_facebook_appid']}',
cf_facebook_secret = '{$_POST['cf_facebook_secret']}',
cf_twitter_key = '{$_POST['cf_twitter_key']}',
cf_twitter_secret = '{$_POST['cf_twitter_secret']}',
cf_social_login_use = '{$_POST['cf_social_login_use']}',
cf_naver_clientid = '{$_POST['cf_naver_clientid']}',
cf_naver_secret = '{$_POST['cf_naver_secret']}',
cf_google_clientid = '{$_POST['cf_google_clientid']}',
cf_google_secret = '{$_POST['cf_google_secret']}',
cf_kakao_rest_key = '{$_POST['cf_kakao_rest_key']}',
cf_kakao_client_secret = '{$_POST['cf_kakao_client_secret']}',
cf_social_servicelist = '{$cf_social_servicelist}',
cf_captcha = '{$_POST['cf_captcha']}',
cf_recaptcha_site_key = '{$_POST['cf_recaptcha_site_key']}',
cf_recaptcha_secret_key = '{$_POST['cf_recaptcha_secret_key']}',
cf_payco_clientid = '{$_POST['cf_payco_clientid']}',
cf_payco_secret = '{$_POST['cf_payco_secret']}',
cf_1_subj = '{$_POST['cf_1_subj']}',
cf_2_subj = '{$_POST['cf_2_subj']}',
cf_3_subj = '{$_POST['cf_3_subj']}',
cf_4_subj = '{$_POST['cf_4_subj']}',
cf_5_subj = '{$_POST['cf_5_subj']}',
cf_6_subj = '{$_POST['cf_6_subj']}',
cf_7_subj = '{$_POST['cf_7_subj']}',
cf_8_subj = '{$_POST['cf_8_subj']}',
cf_9_subj = '{$_POST['cf_9_subj']}',
cf_10_subj = '{$_POST['cf_10_subj']}',
cf_1 = '{$_POST['cf_1']}',
cf_2 = '{$_POST['cf_2']}',
cf_3 = '{$_POST['cf_3']}',
cf_4 = '{$_POST['cf_4']}',
cf_5 = '{$_POST['cf_5']}',
cf_6 = '{$_POST['cf_6']}',
cf_7 = '{$_POST['cf_7']}',
cf_8 = '{$_POST['cf_8']}',
cf_9 = '{$_POST['cf_9']}',
cf_10 = '{$_POST['cf_10']}' ";
sql_query($sql);
//sql_query(" OPTIMIZE TABLE `$g5[config_table]` ");
if (isset($_POST['cf_bbs_rewrite'])) {
g5_delete_all_cache();
}
if (function_exists('get_admin_captcha_by')) {
get_admin_captcha_by('remove');
}
run_event('admin_config_form_update');
update_rewrite_rules();
goto_url('./config_form.php', false);
+12
View File
@@ -0,0 +1,12 @@
<?php
/**
* 상담 예약 관리 시스템 공통 파일 (관리자용)
*/
define('G5_IS_ADMIN', true);
include_once('../../common.php');
include_once(G5_ADMIN_PATH . '/admin.lib.php');
// 💡 [개선] 공통 함수를 별도 파일로 분리하여 관리합니다.
include_once(__DIR__ . '/functions.php');
?>
+9
View File
@@ -0,0 +1,9 @@
<?php
if (!defined('_GNUBOARD_')) {
// AJAX 요청 등으로 직접 접근했을 때, 그누보드 환경을 로드합니다.
include_once('../../../common.php');
}
// 💡 [개선] 공통 함수를 별도 파일로 분리하여 관리합니다.
include_once(__DIR__ . '/functions.php');
?>
@@ -0,0 +1,27 @@
<?php
/**
* 상담 예약 관리 시스템 메뉴
*/
if (!defined('_GNUBOARD_'))
exit;
// 메뉴 구조: array('메뉴코드', '메뉴명', '링크', '메뉴 ID', '아이콘 클래스 (옵션)')
$menu['menu850'] = array(
// 💡 [개선] 메뉴 그룹 대표 아이콘 추가 (예: fa-comments)
array('850000', '상담관리', G5_ADMIN_URL . '/consultant_manage/dashboard.php', 'consultant_manage', 'fa-comments'),
array('850100', '대시보드', G5_ADMIN_URL . '/consultant_manage/dashboard.php', 'consultant_dashboard'),
array('850200', '예약 현황', G5_ADMIN_URL . '/consultant_manage/reservations.php', 'consultant_reservations'),
array('850300', '빠른 스케줄 관리', G5_ADMIN_URL . '/consultant_manage/schedule_generate.php', 'consultant_schedule_quick'),
array('850400', '통계 분석', G5_ADMIN_URL . '/consultant_manage/statistics.php', 'consultant_statistics'),
// 💡 [개선] 설정 관련 메뉴를 '환경설정' 그룹으로 통합
array('850500', '팝업 샘플', G5_ADMIN_URL . '/consultant_manage/sample_page.php', 'consultant_sample'),
array('850600', '환경설정', G5_ADMIN_URL . '/consultant_manage/settings.php', 'consultant_settings_group', 'fa-cogs'),
array('850610', '기본/운영 설정', G5_ADMIN_URL . '/consultant_manage/settings.php', 'consultant_settings'),
array('850615', '리소스(상담사) 관리', G5_ADMIN_URL . '/consultant_manage/resources.php', 'consultant_resources'), // 💡 [추가] 리소스 관리 메뉴
array('850620', '알림 템플릿', G5_ADMIN_URL . '/consultant_manage/templates.php', 'consultant_templates'),
array('850630', '시스템 로그', G5_ADMIN_URL . '/consultant_manage/log_view.php', 'consultant_log_view'),
array('850640', '설치/업데이트', G5_ADMIN_URL . '/consultant_manage/install.php', 'consultant_install'),
// array('850900', '시스템 테스트', G5_ADMIN_URL . '/consultant_manage/test_system.php', 'consultant_test') // 개발용 메뉴는 주석 처리
);
?>
@@ -0,0 +1,83 @@
<?php
/**
* 스케줄 생성 AJAX 처리
*/
include_once('./_common.php');
// 관리자 권한 확인
if (!$is_admin) {
die(json_encode(['success' => false, 'message' => '권한이 없습니다.']));
}
// 설치 확인
if (!is_consultant_installed()) {
die(json_encode(['success' => false, 'message' => '상담 예약 시스템이 설치되지 않았습니다.']));
}
$action = $_POST['action'] ?? '';
$year = (int) ($_POST['year'] ?? date('Y'));
$month = (int) ($_POST['month'] ?? date('n'));
require_once('classes/ScheduleGenerator.class.php');
try {
$generator = new ScheduleGenerator();
switch ($action) {
case 'generate_month':
$result = $generator->generateMonth($year, $month);
if ($result) {
echo json_encode([
'success' => true,
'message' => "{$year}{$month}월 스케줄이 생성되었습니다."
]);
} else {
echo json_encode([
'success' => false,
'message' => '스케줄 생성에 실패했습니다.'
]);
}
break;
case 'check_conflicts':
$conflicts = $generator->checkScheduleConflicts($year, $month);
echo json_encode([
'success' => true,
'conflicts' => $conflicts,
'message' => count($conflicts) > 0 ?
count($conflicts) . '개의 충돌이 발견되었습니다.' :
'충돌이 없습니다.'
]);
break;
case 'regenerate_next_month':
$nextMonth = date('n') == 12 ? 1 : date('n') + 1;
$nextYear = date('n') == 12 ? date('Y') + 1 : date('Y');
$result = $generator->generateMonth($nextYear, $nextMonth);
if ($result) {
echo json_encode([
'success' => true,
'message' => "다음 달({$nextYear}{$nextMonth}월) 스케줄이 재생성되었습니다."
]);
} else {
echo json_encode([
'success' => false,
'message' => '다음 달 스케줄 재생성에 실패했습니다.'
]);
}
break;
default:
echo json_encode(['success' => false, 'message' => '잘못된 요청입니다.']);
break;
}
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => '오류가 발생했습니다: ' . $e->getMessage()
]);
}
?>
@@ -0,0 +1,470 @@
<?php
/**
* 상담 예약 관리 클래스
*/
if (!defined('_GNUBOARD_'))
exit;
class ReservationManager
{
private $table_name = 'consultant_reservations';
/**
* 예약 목록 조회
*/
public function getReservationList($status = '', $date = '', $page = 1, $per_page = 20, $search_type = '', $search_keyword = '')
{
try {
$where_conditions = ["is_deleted = 0"];
// 상태 필터
if (!empty($status)) {
$where_conditions[] = "status = '" . sql_real_escape_string($status) . "'";
}
// 날짜 필터
if (!empty($date)) {
$where_conditions[] = "reservation_date = '" . sql_real_escape_string($date) . "'";
}
// 검색 조건
if (!empty($search_keyword) && !empty($search_type)) {
$search_keyword = sql_real_escape_string($search_keyword);
switch ($search_type) {
case 'customer_name':
$where_conditions[] = "customer_name LIKE '%{$search_keyword}%'";
break;
case 'customer_phone':
$where_conditions[] = "customer_phone LIKE '%{$search_keyword}%'";
break;
}
}
$where_clause = implode(' AND ', $where_conditions);
// 전체 개수 조회
$count_sql = "SELECT COUNT(*) as total FROM {$this->table_name} WHERE {$where_clause}";
$count_result = sql_fetch($count_sql);
$total = $count_result['total'];
// 페이징 계산
$offset = ($page - 1) * $per_page;
$total_pages = ceil($total / $per_page);
// 목록 조회
$sql = "SELECT * FROM {$this->table_name}
WHERE {$where_clause}
ORDER BY created_at DESC
LIMIT {$offset}, {$per_page}";
$reservations = [];
$result = sql_query($sql);
while ($row = sql_fetch_array($result)) {
$reservations[] = $row;
}
return [
'success' => true,
'data' => [
'reservations' => $reservations,
'pagination' => [
'current_page' => $page,
'per_page' => $per_page,
'total' => $total,
'total_pages' => $total_pages
]
]
];
} catch (Exception $e) {
consultant_log("예약 목록 조회 실패: " . $e->getMessage(), 'error');
return [
'success' => false,
'message' => '예약 목록 조회 중 오류가 발생했습니다.'
];
}
}
/**
* 예약 생성
*/
public function createReservation($data)
{
try {
// 필수 필드 검증
$required_fields = ['customer_name', 'customer_phone', 'reservation_date', 'reservation_time'];
foreach ($required_fields as $field) {
if (empty($data[$field])) {
throw new Exception("필수 필드가 누락되었습니다: {$field}");
}
}
// 중복 예약 확인
$check_sql = "SELECT COUNT(*) as count FROM {$this->table_name}
WHERE reservation_date = '" . sql_real_escape_string($data['reservation_date']) . "'
AND reservation_time = '" . sql_real_escape_string($data['reservation_time']) . "'
AND status IN ('payment_pending', 'reserved')
AND is_deleted = 0";
$check_result = sql_fetch($check_sql);
$current_count = $check_result['count'];
// 최대 인원 확인 (기본값 2명)
$max_persons = consultant_get_config('default_max_persons', 2);
if ($current_count >= $max_persons) {
throw new Exception('해당 시간대는 예약이 마감되었습니다.');
}
// 데이터 준비
$insert_data = [
'customer_name' => sql_real_escape_string($data['customer_name']),
'customer_phone' => sql_real_escape_string($data['customer_phone']),
'customer_email' => sql_real_escape_string($data['customer_email'] ?? ''),
'reservation_date' => sql_real_escape_string($data['reservation_date']),
'reservation_time' => sql_real_escape_string($data['reservation_time']),
'consultation_type' => sql_real_escape_string($data['consultation_type'] ?? 'onsite'),
'status' => 'payment_pending',
'payment_amount' => (int) ($data['payment_amount'] ?? consultant_get_config('consultation_fee', 50000)),
'payment_status' => 'pending',
'request_memo' => sql_real_escape_string($data['request_memo'] ?? ''),
'wr_id' => (int) ($data['wr_id'] ?? 0),
'temp_1' => sql_real_escape_string($data['temp_1'] ?? ''),
'created_at' => 'NOW()',
'updated_at' => 'NOW()'
];
// SQL 생성
$fields = implode(', ', array_keys($insert_data));
$values = "'" . implode("', '", array_values($insert_data)) . "'";
$values = str_replace("'NOW()'", "NOW()", $values); // NOW() 함수 처리
$sql = "INSERT INTO {$this->table_name} ({$fields}) VALUES ({$values})";
if (!sql_query($sql)) {
throw new Exception('데이터베이스 저장 실패: ' . sql_error());
}
$reservation_id = sql_insert_id();
// 알림 발송
$this->sendReservationNotification($reservation_id, 'created');
consultant_log("새 예약 생성: ID {$reservation_id}, 고객: {$data['customer_name']}");
return [
'success' => true,
'message' => '예약이 성공적으로 접수되었습니다.',
'data' => [
'reservation_id' => $reservation_id
]
];
} catch (Exception $e) {
consultant_log("예약 생성 실패: " . $e->getMessage(), 'error');
return [
'success' => false,
'message' => $e->getMessage()
];
}
}
/**
* 예약 상태 변경
*/
public function updateReservationStatus($reservation_id, $new_status, $admin_memo = '')
{
try {
// 예약 정보 조회
$reservation = $this->getReservationById($reservation_id);
if (!$reservation) {
throw new Exception('예약 정보를 찾을 수 없습니다.');
}
// 상태 변경 가능 여부 확인
$valid_transitions = [
'payment_pending' => ['reserved', 'cancelled'],
'reserved' => ['completed', 'cancelled'],
'completed' => [],
'cancelled' => []
];
$current_status = $reservation['status'];
if (!in_array($new_status, $valid_transitions[$current_status])) {
throw new Exception('현재 상태에서 해당 상태로 변경할 수 없습니다.');
}
// 상태 업데이트
$sql = "UPDATE {$this->table_name}
SET status = '" . sql_real_escape_string($new_status) . "',
admin_memo = '" . sql_real_escape_string($admin_memo) . "',
updated_at = NOW()
WHERE id = {$reservation_id}";
if (!sql_query($sql)) {
throw new Exception('상태 변경 실패: ' . sql_error());
}
// 알림 발송
$this->sendReservationNotification($reservation_id, 'status_changed', $new_status);
consultant_log("예약 상태 변경: ID {$reservation_id}, {$current_status} -> {$new_status}");
return [
'success' => true,
'message' => '예약 상태가 성공적으로 변경되었습니다.'
];
} catch (Exception $e) {
consultant_log("예약 상태 변경 실패: " . $e->getMessage(), 'error');
return [
'success' => false,
'message' => $e->getMessage()
];
}
}
/**
* 입금 확인
*/
public function confirmPayment($reservation_id, $admin_id)
{
try {
// 예약 정보 조회
$reservation = $this->getReservationById($reservation_id);
if (!$reservation) {
throw new Exception('예약 정보를 찾을 수 없습니다.');
}
if ($reservation['status'] !== 'payment_pending') {
throw new Exception('입금 대기 상태의 예약만 확인할 수 있습니다.');
}
// 입금 확인 처리
$sql = "UPDATE {$this->table_name}
SET status = 'reserved',
payment_status = 'paid',
payment_confirmed_at = NOW(),
payment_confirmed_by = '" . sql_real_escape_string($admin_id) . "',
updated_at = NOW()
WHERE id = {$reservation_id}";
if (!sql_query($sql)) {
throw new Exception('입금 확인 처리 실패: ' . sql_error());
}
// 알림 발송
$this->sendReservationNotification($reservation_id, 'payment_confirmed');
consultant_log("입금 확인: ID {$reservation_id}, 확인자: {$admin_id}");
return [
'success' => true,
'message' => '입금이 확인되어 예약이 확정되었습니다.'
];
} catch (Exception $e) {
consultant_log("입금 확인 실패: " . $e->getMessage(), 'error');
return [
'success' => false,
'message' => $e->getMessage()
];
}
}
/**
* 예약 취소
*/
public function cancelReservation($reservation_id, $reason)
{
try {
// 예약 정보 조회
$reservation = $this->getReservationById($reservation_id);
if (!$reservation) {
throw new Exception('예약 정보를 찾을 수 없습니다.');
}
if ($reservation['status'] === 'cancelled') {
throw new Exception('이미 취소된 예약입니다.');
}
if ($reservation['status'] === 'completed') {
throw new Exception('완료된 예약은 취소할 수 없습니다.');
}
// 예약 취소 처리
$sql = "UPDATE {$this->table_name}
SET status = 'cancelled',
admin_memo = '" . sql_real_escape_string($reason) . "',
updated_at = NOW()
WHERE id = {$reservation_id}";
if (!sql_query($sql)) {
throw new Exception('예약 취소 처리 실패: ' . sql_error());
}
// 알림 발송
$this->sendReservationNotification($reservation_id, 'cancelled', $reason);
consultant_log("예약 취소: ID {$reservation_id}, 사유: {$reason}");
return [
'success' => true,
'message' => '예약이 성공적으로 취소되었습니다.'
];
} catch (Exception $e) {
consultant_log("예약 취소 실패: " . $e->getMessage(), 'error');
return [
'success' => false,
'message' => $e->getMessage()
];
}
}
/**
* 예약 정보 조회
*/
public function getReservationById($reservation_id)
{
$sql = "SELECT * FROM {$this->table_name} WHERE id = {$reservation_id} AND is_deleted = 0";
return sql_fetch($sql);
}
/**
* 예약 알림 발송
*/
private function sendReservationNotification($reservation_id, $type, $extra_data = null)
{
try {
$reservation = $this->getReservationById($reservation_id);
if (!$reservation) {
return false;
}
// 알림 템플릿 키 결정
$template_keys = [
'created' => 'consultant_reservation_customer',
'payment_confirmed' => 'consultant_confirmed_customer',
'cancelled' => 'consultant_cancelled_customer',
'status_changed' => 'consultant_status_changed_customer'
];
$template_key = $template_keys[$type] ?? null;
if (!$template_key) {
return false;
}
// 템플릿 변수 준비
$variables = [
'customer_name' => $reservation['customer_name'],
'customer_phone' => $reservation['customer_phone'],
'customer_email' => $reservation['customer_email'],
'reservation_date' => $reservation['reservation_date'],
'reservation_time' => $reservation['reservation_time'],
'payment_amount' => number_format($reservation['payment_amount']),
'account_info' => consultant_get_config('account_info', ''),
'cancel_reason' => $extra_data ?? ''
];
// 이메일 발송
if (!empty($reservation['customer_email'])) {
$this->sendEmailNotification($reservation['customer_email'], $template_key, $variables);
}
// SMS 발송
if (!empty($reservation['customer_phone'])) {
$this->sendSmsNotification($reservation['customer_phone'], $template_key, $variables);
}
// 관리자 알림 (새 예약 시)
if ($type === 'created') {
$this->sendAdminNotification($reservation, $variables);
}
return true;
} catch (Exception $e) {
consultant_log("알림 발송 실패: " . $e->getMessage(), 'error');
return false;
}
}
/**
* 이메일 알림 발송
*/
private function sendEmailNotification($email, $template_key, $variables)
{
// 💡 [수정] consultant_send_notification 함수 사용으로 변경
return consultant_send_notification('email', $template_key, array_merge($variables, ['customer_email' => $email]));
}
/**
* SMS 알림 발송
*/
private function sendSmsNotification($phone, $template_key, $variables)
{
// 💡 [수정] consultant_send_notification 함수 사용으로 변경
return consultant_send_notification('sms', $template_key, array_merge($variables, ['customer_phone' => $phone]));
}
/**
* 관리자 알림 발송
*/
private function sendAdminNotification($reservation, $variables)
{
// 관리자 이메일 알림
$admin_template_key = 'consultant_reservation_admin';
// 관리자 이메일 주소 (설정에서 가져오거나 기본값 사용)
$admin_email = consultant_get_config('admin_email', get_admin_email());
if ($admin_email) {
// 💡 [수정] consultant_send_notification 함수 사용으로 변경
consultant_send_notification('email', $admin_template_key, array_merge($variables, ['customer_email' => $admin_email]));
}
}
/**
* 템플릿 변수 치환
*/
private function replaceTemplateVariables($template, $variables)
{
foreach ($variables as $key => $value) {
$template = str_replace('{' . $key . '}', $value, $template);
}
return $template;
}
/**
* 예약 통계 조회
*/
public function getReservationStats($start_date = null, $end_date = null)
{
try {
if (!$start_date)
$start_date = date('Y-m-01'); // 이번 달 첫날
if (!$end_date)
$end_date = date('Y-m-d'); // 오늘
$sql = "SELECT
COUNT(*) as total,
COUNT(CASE WHEN status = 'payment_pending' THEN 1 END) as pending,
COUNT(CASE WHEN status = 'reserved' THEN 1 END) as confirmed,
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed,
COUNT(CASE WHEN status = 'cancelled' THEN 1 END) as cancelled,
SUM(CASE WHEN status = 'completed' THEN payment_amount ELSE 0 END) as total_revenue
FROM {$this->table_name}
WHERE reservation_date BETWEEN '{$start_date}' AND '{$end_date}'
AND is_deleted = 0";
return sql_fetch($sql);
} catch (Exception $e) {
consultant_log("통계 조회 실패: " . $e->getMessage(), 'error');
return null;
}
}
}
@@ -0,0 +1,332 @@
<?php
/**
* 상담 예약 시스템 - 월별 스케줄 자동 생성 클래스
*
* 요일별 설정을 기반으로 월별 상세 스케줄을 자동 생성하고 관리합니다.
*/
class ScheduleGenerator
{
private $db;
public function __construct()
{
global $connect_db;
$this->db = $connect_db;
}
/**
* 특정 월의 전체 스케줄 생성
*
* @param int $year 년도
* @param int $month 월
* @return bool 성공 여부
*/
public function generateMonth($year, $month)
{
try {
// 기본 설정 조회
$basicSettings = $this->getBasicSettings();
// 요일별 설정 조회
$weeklySettings = $this->getWeeklySettings();
// 해당 월의 모든 날짜 생성
$dates = self::getMonthDates($year, $month);
// 기존 스케줄 중 예약이 없는 것들 삭제 (재생성을 위해)
$this->clearAutoGeneratedSchedules($year, $month);
// 각 날짜별로 스케줄 생성
foreach ($dates as $date) {
$dayOfWeek = date('N', strtotime($date)); // 1=월요일, 7=일요일
$this->generateDay($date, $dayOfWeek, $weeklySettings, $basicSettings);
}
return true;
} catch (Exception $e) {
consultant_log("스케줄 생성 실패: " . $e->getMessage(), 'error');
return false;
}
}
/**
* 특정 날짜의 스케줄 생성
*/
private function generateDay($date, $dayOfWeek, $weeklySettings, $basicSettings)
{
$dayNames = ['', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
$dayName = $dayNames[$dayOfWeek];
// 해당 요일이 운영일인지 확인
$isEnabled = $weeklySettings[$dayName . '_enabled'] ?? '0';
if ($isEnabled == '0') {
// 휴무일 처리
$this->insertHolidaySchedule($date);
return;
}
// 운영시간 정보
$startTime = $weeklySettings[$dayName . '_start'] ?? '09:00';
$endTime = $weeklySettings[$dayName . '_end'] ?? '18:00';
$lunchStart = $weeklySettings[$dayName . '_lunch_start'] ?? '12:00';
$lunchEnd = $weeklySettings[$dayName . '_lunch_end'] ?? '13:00';
// 시간 슬롯 생성
$this->createTimeSlots($date, $startTime, $endTime, $basicSettings['consultation_duration'], $basicSettings['max_persons_per_slot']);
// 점심시간 블록 처리
// 💡 [수정] 시작 시간과 종료 시간이 다를 때만 점심시간으로 처리합니다.
if ($lunchStart && $lunchEnd && strtotime($lunchStart) < strtotime($lunchEnd)) {
$this->blockLunchTime($date, $lunchStart, $lunchEnd);
}
}
/**
* 시간 슬롯 생성
*/
private function createTimeSlots($date, $startTime, $endTime, $slotDuration, $maxPersons)
{
$currentTime = strtotime($startTime);
$endTimeStamp = strtotime($endTime);
$slotMinutes = (int) $slotDuration;
while ($currentTime < $endTimeStamp) {
$slotStart = date('H:i', $currentTime);
$slotEnd = date('H:i', $currentTime + ($slotMinutes * 60));
// 종료시간을 넘지 않는 경우만 생성
if (strtotime($slotEnd) <= $endTimeStamp) {
// 재생성 시 중복 생성을 방지하기 위해 기존 슬롯이 있는지 확인
$check_sql = "SELECT id FROM consultant_schedule
WHERE specific_date = '" . sql_real_escape_string($date) . "'
AND start_time = '" . sql_real_escape_string($slotStart) . "'";
$existing_slot = sql_fetch($check_sql);
// 기존에 수동으로 추가되었거나 예약으로 보호된 슬롯이 없으면 새로 생성
if (!$existing_slot) {
$sql = "INSERT INTO consultant_schedule
(specific_date, start_time, end_time, time_slot, max_persons, is_available, temp_1, created_at)
VALUES (
'" . sql_real_escape_string($date) . "',
'" . sql_real_escape_string($slotStart) . "',
'" . sql_real_escape_string($slotEnd) . "',
" . (int) $slotMinutes . ",
" . (int) $maxPersons . ",
1,
'auto_generated',
NOW()
)";
if (!sql_query($sql)) {
// sql_query가 false를 반환하면 오류를 던짐
throw new Exception("시간 슬롯 생성 실패: " . sql_error());
}
}
}
$currentTime += ($slotMinutes * 60);
}
}
/**
* 점심시간 블록 처리
*/
private function blockLunchTime($date, $lunchStart, $lunchEnd)
{
$sql = "UPDATE consultant_schedule
SET is_available = 0, temp_1 = 'lunch_time', temp_2 = '점심시간', updated_at = NOW()
WHERE specific_date = '" . sql_real_escape_string($date) . "'
-- 💡 [수정] start_time이 휴게시간 범위에 포함되는 모든 슬롯을 대상으로 하도록 변경합니다.
-- 이렇게 하면 상담 시간 단위(30분, 60분 등)에 상관없이 정확하게 동작합니다.
AND start_time >= '" . sql_real_escape_string($lunchStart) . "' AND start_time < '" . sql_real_escape_string($lunchEnd) . "'";
sql_query($sql);
}
/**
* 휴무일 스케줄 삽입
*/
private function insertHolidaySchedule($date)
{
// 💡 [수정] 휴무일 데이터 중복 생성을 방지하기 위해, 해당 날짜에 이미 스케줄이 있는지 확인합니다.
$check_sql = "SELECT id FROM consultant_schedule WHERE specific_date = '" . sql_real_escape_string($date) . "'";
$existing_schedule = sql_fetch($check_sql);
// 해당 날짜에 아무 스케줄도 없을 때만 휴무일 데이터를 삽입합니다.
if (!$existing_schedule) {
$sql = "INSERT INTO consultant_schedule
(specific_date, start_time, end_time, time_slot, max_persons, is_available, temp_1, temp_2, created_at)
VALUES (
'" . sql_real_escape_string($date) . "',
'00:00',
'23:59',
0,
0,
0,
'holiday',
'휴무일',
NOW()
)";
sql_query($sql);
}
}
/**
* 기존 예약 보호 로직 - 예약이 있는 시간대는 삭제하지 않음
*/
private function protectExistingReservations($year, $month)
{
$startDate = sprintf('%04d-%02d-01', $year, $month);
$endDate = date('Y-m-t', strtotime($startDate));
// 해당 월에 예약이 있는 스케줄 ID 조회
$sql = "SELECT DISTINCT s.id
FROM consultant_schedule s
INNER JOIN consultant_reservations r ON (
s.specific_date = r.reservation_date
AND s.start_time = r.reservation_time
)
WHERE s.specific_date BETWEEN '{$startDate}' AND '{$endDate}'
AND r.status NOT IN ('cancelled')
AND r.is_deleted = 0";
$result = sql_query($sql);
$protectedIds = [];
while ($row = sql_fetch_array($result)) {
$protectedIds[] = $row['id'];
}
return $protectedIds;
}
/**
* 설정 변경으로 인한 기존 예약과의 충돌 감지 (향상된 로직)
* - 휴무일로 변경된 경우
* - 운영 시간 밖으로 밀려난 경우
* - 점심시간과 겹치는 경우
*/
public function findConflictsWithNewSettings($year, $month)
{
$conflicts = [];
$startDate = sprintf('%04d-%02d-01', $year, $month);
$endDate = date('Y-m-t', strtotime($startDate));
// 해당 월의 모든 예약 조회
$sql = "SELECT r.reservation_date, r.reservation_time, r.customer_name, r.customer_phone
FROM consultant_reservations r
WHERE r.reservation_date BETWEEN '{$startDate}' AND '{$endDate}'
AND r.status NOT IN ('cancelled')
AND r.is_deleted = 0";
$weeklySettings = $this->getWeeklySettings();
$result = sql_query($sql);
while ($row = sql_fetch_array($result)) {
$dayOfWeek = date('N', strtotime($row['reservation_date']));
$dayNames = ['', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
$dayName = $dayNames[$dayOfWeek];
$isEnabled = $weeklySettings[$dayName . '_enabled'] ?? '0';
$startTime = $weeklySettings[$dayName . '_start'] ?? '09:00';
$endTime = $weeklySettings[$dayName . '_end'] ?? '18:00';
$lunchStart = $weeklySettings[$dayName . '_lunch_start'] ?? '12:00';
$lunchEnd = $weeklySettings[$dayName . '_lunch_end'] ?? '13:00';
$reservationTime = $row['reservation_time'];
$conflictReason = '';
if ($isEnabled == '0') {
$conflictReason = '휴무일로 변경됨';
} elseif (strtotime($reservationTime) < strtotime($startTime) || strtotime($reservationTime) >= strtotime($endTime)) {
$conflictReason = '운영 시간 벗어남';
} elseif (strtotime($reservationTime) >= strtotime($lunchStart) && strtotime($reservationTime) < strtotime($lunchEnd)) {
$conflictReason = '점심시간과 겹침';
}
if ($conflictReason) {
$conflicts[] = [
'date' => $row['reservation_date'],
'time' => $reservationTime,
'customer' => $row['customer_name'],
'phone' => $row['customer_phone'],
'reason' => $conflictReason
];
}
}
return $conflicts;
}
/**
* 자동 생성된 스케줄 삭제 (예약이 없는 것만)
*/
private function clearAutoGeneratedSchedules($year, $month)
{
$startDate = sprintf('%04d-%02d-01', (int)$year, (int)$month);
$endDate = date('Y-m-t', strtotime($startDate));
// 예약이 있는 '시간 슬롯'의 ID를 보호
$protectedIds = $this->protectExistingReservations($year, $month);
// 💡 [수정] 예약이 있는 날짜라도 'holiday' 타입의 스케줄은 삭제될 수 있도록 temp_1 조건만 사용합니다.
$whereClause = "specific_date BETWEEN '{$startDate}' AND '{$endDate}'
AND temp_1 IN ('auto_generated', 'lunch_time', 'holiday', 'manual_block')";
if (!empty($protectedIds)) {
$whereClause .= " AND id NOT IN (" . implode(',', $protectedIds) . ")";
}
$sql = "DELETE FROM consultant_schedule WHERE " . $whereClause;
sql_query($sql);
}
/**
* 해당 월의 모든 날짜 배열 생성
*/
private static function getMonthDates($year, $month)
{
$dates = [];
$daysInMonth = date('t', mktime(0, 0, 0, $month, 1, $year));
for ($day = 1; $day <= $daysInMonth; $day++) {
$dates[] = sprintf('%04d-%02d-%02d', $year, $month, $day);
}
return $dates;
}
/**
* 기본 설정 조회
*/
private function getBasicSettings()
{
return [
'consultation_duration' => (int) consultant_get_config('consultation_duration', 60),
'max_persons_per_slot' => (int) consultant_get_config('max_persons_per_slot', 2)
];
}
/**
* 요일별 설정 조회
*/
private function getWeeklySettings()
{
$days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
$settings = [];
foreach ($days as $day) {
$settings[$day . '_enabled'] = consultant_get_config($day . '_enabled', $day == 'saturday' || $day == 'sunday' ? '0' : '1');
$settings[$day . '_start'] = consultant_get_config($day . '_start', '09:00');
$settings[$day . '_end'] = consultant_get_config($day . '_end', '18:00');
$settings[$day . '_lunch_start'] = consultant_get_config($day . '_lunch_start', '12:00');
$settings[$day . '_lunch_end'] = consultant_get_config($day . '_lunch_end', '13:00');
}
return $settings;
}
}
?>
@@ -0,0 +1,384 @@
<?php
if (!defined('_GNUBOARD_'))
exit;
// ❗ [핵심] 필요한 클래스와 라이브러리를 포함합니다.
if (file_exists(G5_ADMIN_PATH . '/mail_manage/classes/MailSender.php')) {
require_once(G5_ADMIN_PATH . '/mail_manage/classes/MailSender.php');
}
if (file_exists(G5_PLUGIN_PATH . '/sms5/sms5.lib.php')) {
include_once(G5_PLUGIN_PATH . '/sms5/sms5.lib.php');
}
/**
* 통합 메일/SMS 발송 클래스
* 기존 mail_manage, sms_admin 시스템을 활용하여 발송 및 이력 기록
*/
class NotificationSender
{
private $g5;
public function __construct()
{
global $g5;
$this->g5 = $g5;
}
/**
* ❗ [핵심 수정] 템플릿 기반 메인 발송 함수
* @param array $params 발송 파라미터
* @return array 발송 결과
*/
public function send($params)
{
// 파라미터 검증
$validated = $this->validateParams($params);
if (!$validated['success']) {
return $validated;
}
// 대상 회원 조회
$members = $this->getTargetMembers($params);
if (empty($members)) {
return ['success' => false, 'message' => '발송 대상 회원이 없습니다.'];
}
$results = [
'success' => true,
'total_targets' => count($members),
'sms_success' => 0,
'sms_fail' => 0,
'email_success' => 0,
'email_fail' => 0,
'message' => ''
];
// SMS 발송
if (!empty($params['sms_template_key'])) {
$sms_result = $this->sendSMS($members, $params['sms_template_key'], $params['vars'] ?? []);
$results['sms_success'] = $sms_result['success'];
$results['sms_fail'] = $sms_result['fail'];
}
// 이메일 발송
if (!empty($params['email_template_key'])) {
$email_result = $this->sendEmail($members, $params['email_template_key'], $params['vars'] ?? []);
$results['email_success'] = $email_result['success'];
$results['email_fail'] = $email_result['fail'];
}
$results['message'] = $this->generateResultMessage($results);
return $results;
}
/**
* ❗ [핵심 수정] 템플릿 기반 파라미터 검증
*/
private function validateParams($params)
{
if (empty($params['target_type'])) {
return ['success' => false, 'message' => "필수 파라미터 'target_type'이 누락되었습니다."];
}
if ($params['target_type'] === 'single' && empty($params['member_id'])) {
return ['success' => false, 'message' => '단일 발송 시 회원 ID(member_id)가 필요합니다.'];
}
if ($params['target_type'] === 'bulk' && empty($params['member_levels'])) {
return ['success' => false, 'message' => '대량 발송 시 회원 레벨(member_levels)이 필요합니다.'];
}
if (empty($params['sms_template_key']) && empty($params['email_template_key'])) {
return ['success' => false, 'message' => 'SMS 또는 이메일 템플릿 키 중 하나는 반드시 필요합니다.'];
}
return ['success' => true];
}
/**
* 대상 회원 조회
*/
private function getTargetMembers($params)
{
$members = [];
$member_table = $this->g5['member_table'];
if ($params['target_type'] === 'single') {
$sql = "SELECT mb_id, mb_name, mb_hp, mb_email, mb_sms, mb_mailling FROM `{$member_table}` WHERE mb_id = '" . sql_real_escape_string($params['member_id']) . "' AND mb_leave_date = '' AND mb_intercept_date = ''";
} else {
$levels = array_map('intval', $params['member_levels']);
$level_condition = implode(',', $levels);
$sql = "SELECT mb_id, mb_name, mb_hp, mb_email, mb_sms, mb_mailling FROM `{$member_table}` WHERE mb_level IN ({$level_condition}) AND mb_leave_date = '' AND mb_intercept_date = ''";
}
$this->write_debug_log("[SMS 발송 시작] sql '{$sql}'");
$result = sql_query($sql);
while ($row = sql_fetch_array($result)) {
$members[] = $row;
}
return $members;
}
/**
* ❗ [핵심 수정] SMS 발송 (템플릿 및 변수 처리, 이력 기록 포함)
*/
private function sendSMS($members, $template_key, $common_vars)
{
$success = 0;
$fail = 0;
$notification_mode = get_order_config('notification_mode', 'log');
$is_test_mode = ($notification_mode !== 'send');
$sizeof_members = count($members);
// 💡 [수정] 템플릿 조회 로직 변경: 지정 테이블(consultant_sms_templates) -> 기본 테이블(sms_templates) 순서로 조회
$template = null;
// 1. 지정 테이블 조회
$check_table = sql_query("SHOW TABLES LIKE 'consultant_sms_templates'", false);
if (sql_num_rows($check_table) > 0) {
$template = sql_fetch("SELECT * FROM `consultant_sms_templates` WHERE template_key = '" . sql_real_escape_string($template_key) . "'");
if ($template) {
// 필드명 통일 (기본 테이블과 필드명이 다를 수 있음)
$template['content'] = $template['template_content'];
}
}
// 2. 기본 테이블 조회 (지정 테이블에 없을 경우)
if (!$template) {
$template = sql_fetch("SELECT * FROM `sms_templates` WHERE template_key = '" . sql_real_escape_string($template_key) . "'");
}
// 3. 둘 다 없으면 에러 처리
if (!$template) {
$this->write_debug_log("[SMS 발송 오류] 템플릿 '{$template_key}'을(를) 찾을 수 없습니다.");
return ['success' => 0, 'fail' => count($members)];
}
$count= count($members);
if ($is_test_mode) {
// --- 개발 모드: 로그 파일에만 기록 ---
foreach ($members as $member) {
if ($member['mb_sms'] && !empty($member['mb_hp'])) {
$personal_vars = array_merge($common_vars, ['이름' => $member['mb_name'], 'agent_name' => $member['mb_name'], 'dealer_name' => $member['mb_name']]);
$personal_message = $template['content'];
foreach ($personal_vars as $key => $value) {
$personal_message = str_replace('{' . $key . '}', $value, $personal_message);
}
$this->write_debug_log("[SMS LOG] To: {$member['mb_hp']}, Content: {$personal_message}");
$success++;
} else {
$empty = !empty($member['mb_hp']);
$this->write_debug_log("[SMS 발송오류] 사용자 '{$member['mb_sms']}' '{$member['mb_hp']}' '$empty'");
$fail++;
}
}
} else {
// --- 실제 발송 모드: DB 기록 및 실제 발송 ---
$sms_config = sql_fetch("SELECT * FROM {$this->g5['sms5_config_table']}");
$send_phone = $sms_config['cf_phone'];
$wr_message = sql_real_escape_string($template['content']);
$wr_reply = sql_real_escape_string($send_phone);
sql_query("INSERT INTO {$this->g5['sms5_write_table']} (wr_message, wr_reply, wr_total, wr_datetime) VALUES ('{$wr_message}', '{$wr_reply}', '" . count($members) . "', '" . G5_TIME_YMDHIS . "')");
$wr_no = sql_insert_id();
$SMS = null;
if (class_exists('SMS5')) {
$SMS = new SMS5;
$SMS->SMS_con($sms_config['cf_sms_ip'], $sms_config['cf_sms_id'], $sms_config['cf_sms_pw'], $sms_config['cf_sms_port']);
} else {
$this->write_debug_log("[SMS 발송 오류] SMS5 클래스를 찾을 수 없습니다. 실제 발송을 건너뜁니다.");
}
foreach ($members as $member) {
if ($member['mb_sms'] && !empty($member['mb_hp'])) {
$personal_vars = array_merge($common_vars, ['이름' => $member['mb_name'], 'agent_name' => $member['mb_name'], 'dealer_name' => $member['mb_name']]);
$personal_message = $template['content'];
foreach ($personal_vars as $key => $value) {
$personal_message = str_replace('{' . $key . '}', $value, $personal_message);
}
$result_code = 'Fail';
$result_msg = 'SMS5 클래스가 없어 발송할 수 없습니다.';
$hs_status = '0';
if ($SMS) {
$SMS->Add($member['mb_hp'], $send_phone, '', $personal_message);
$SMS->Send();
$result_arr = $SMS->Result;
$result_code = 'Fail';
$result_msg = '서버로부터 응답이 없습니다.';
if(!empty($result_arr)){
$result_parts = explode(':', $result_arr[0]);
if(count($result_parts) > 1 && strpos($result_parts[1], 'Error') === false) {
$result_code = 'Success';
$result_msg = $result_parts[1];
} else {
$result_msg = $result_arr[0];
}
}
$hs_status = ($result_code == 'Success') ? '1' : '0';
$SMS->Init();
}
sql_query("INSERT INTO {$this->g5['sms5_history_table']} (wr_no, mb_id, hs_name, hs_hp, hs_datetime, hs_status, hs_message) VALUES ('{$wr_no}', '{$member['mb_id']}', '{$member['mb_name']}', '{$member['mb_hp']}', '" . G5_TIME_YMDHIS . "', '{$hs_status}', '{$result_msg}')");
if ($hs_status == '1') $success++;
else $fail++;
} else {
$fail++;
}
}
}
return ['success' => $success, 'fail' => $fail];
}
/**
* ❗ [핵심 수정] 이메일 발송 (MailSender 클래스 활용)
*/
private function sendEmail($members, $template_key, $common_vars)
{
$success = 0;
$fail = 0;
$notification_mode = get_order_config('notification_mode', 'log');
$is_test_mode = ($notification_mode !== 'send');
$sizeof_members = count($members);
// 💡 [수정] 템플릿 조회 로직 변경: 지정 테이블(consultant_mail_templates) -> 기본 테이블(mail_templates) 순서로 조회
$template = null;
// 1. 지정 테이블 조회
$check_table = sql_query("SHOW TABLES LIKE 'consultant_mail_templates'", false);
if (sql_num_rows($check_table) > 0) {
$template = sql_fetch("SELECT * FROM `consultant_mail_templates` WHERE template_key = '" . sql_real_escape_string($template_key) . "'");
if ($template) {
// 필드명 통일
$template['subject'] = $template['template_subject'];
$template['content'] = $template['template_content'];
}
}
// 2. 기본 테이블 조회 (지정 테이블에 없을 경우)
if (!$template) {
$template = sql_fetch("SELECT * FROM `mail_templates` WHERE template_key = '" . sql_real_escape_string($template_key) . "'");
}
// 3. 둘 다 없으면 에러 처리
if (!$template) {
$this->write_debug_log("[EMAIL 발송 오류] 템플릿 '{$template_key}'을(를) 찾을 수 없습니다.");
return ['success' => 0, 'fail' => count($members)];
}
if ($is_test_mode) {
// --- 개발 모드: 로그 파일에만 기록 ---
foreach ($members as $member) {
if ($member['mb_mailling'] && !empty($member['mb_email'])) {
$personal_vars = array_merge($common_vars, ['이름' => $member['mb_name'], 'agent_name' => $member['mb_name'], 'dealer_name' => $member['mb_name']]);
$subject = $template['subject'];
$content = $template['content'];
foreach ($personal_vars as $key => $value) {
$search = '{' . $key . '}';
$subject = str_replace($search, $value, $subject);
$content = str_replace($search, $value, $content);
}
$this->write_debug_log("[EMAIL LOG] To: {$member['mb_email']}, Subject: {$subject}, Content: {$content}");
$success++;
} else {
$fail++;
$empty = !empty($member['mb_email']);
$this->write_debug_log("[EMAIL 발송 오류] 사용자 '{$member['mb_sms']}' '{$member['mb_hp']}' '$empty'");
}
}
} else {
// --- 실제 발송 모드: MailSender 호출 ---
if (!class_exists('MailSender')) {
$this->write_debug_log("[EMAIL 발송 오류] MailSender 클래스를 찾을 수 없습니다.");
return ['success' => 0, 'fail' => count($members)];
}
$mailSender = new MailSender();
foreach ($members as $member) {
if ($member['mb_mailling'] && !empty($member['mb_email'])) {
$personal_vars = array_merge($common_vars, ['이름' => $member['mb_name'], 'agent_name' => $member['mb_name'], 'dealer_name' => $member['mb_name']]);
// 💡 [수정] MailSender가 템플릿 키로 조회하는 방식일 수 있으므로,
// 커스텀 템플릿 내용을 직접 전달하거나 MailSender를 수정해야 할 수 있음.
// 여기서는 MailSender가 템플릿 키를 받아서 내부적으로 처리한다고 가정하고,
// 만약 커스텀 템플릿을 사용해야 한다면 MailSender의 동작 방식에 따라 수정이 필요함.
// 현재 구조상 MailSender::send()는 템플릿 키를 받으므로,
// MailSender 내부에서도 동일한 우선순위 로직이 필요하거나,
// 여기서 내용을 다 만들어서 보내는 방식(sendDirect 등)이 있다면 그걸 써야 함.
// 일단 기존 로직 유지하되, MailSender가 커스텀 테이블을 인지하지 못할 수 있음을 주석으로 남김.
// 만약 MailSender가 내용을 직접 받는 메소드가 없다면,
// 여기서 내용을 치환해서 보내는 로직을 직접 구현해야 할 수도 있음.
// 하지만 요청사항은 "지정 테이블을 읽고 없으면 기본 테이블을 읽어서 발송"이므로,
// 위에서 $template을 구했으니, 내용을 직접 치환해서 메일 발송 함수(mailer)를 직접 호출하는 것이 안전함.
$subject = $template['subject'];
$content = $template['content'];
foreach ($personal_vars as $key => $value) {
$search = '{' . $key . '}';
$subject = str_replace($search, $value, $subject);
$content = str_replace($search, $value, $content);
}
// G5 기본 mailer 함수 사용 (MailSender 의존성 제거 또는 우회)
include_once(G5_LIB_PATH.'/mailer.lib.php');
mailer($this->g5['title'], $this->g5['admin_email'], $member['mb_email'], $subject, $content, 1);
$success++;
} else {
$fail++;
}
}
}
return ['success' => $success, 'fail' => $fail];
}
/**
* 결과 메시지 생성
*/
private function generateResultMessage($results)
{
$message = "발송이 완료되었습니다.\n\n";
$message .= "전체 대상: " . $results['total_targets'] . "\n\n";
if (isset($results['sms_success'])) {
$message .= "SMS 발송 결과: 성공 " . $results['sms_success'] . "건, 실패 " . $results['sms_fail'] . "\n";
}
if (isset($results['email_success'])) {
$message .= "이메일 발송 결과: 성공 " . $results['email_success'] . "건, 실패 " . $results['email_fail'] . "\n";
}
return $message;
}
/**
* ❗ [핵심 수정] 디버그 로그 기록 함수 (권한 문제 해결)
*/
private function write_debug_log($message)
{
$log_dir = G5_PATH . '/log';
// 1. 디렉토리 존재 여부 확인 및 생성
if (!is_dir($log_dir)) {
if (!@mkdir($log_dir, 0755, true) && !is_dir($log_dir)) {
error_log("--- NotificationSender ERROR: 디버그 로그 디렉토리 생성 실패. '{$log_dir}' 경로를 확인하거나 수동으로 생성 후 웹서버 쓰기 권한을 부여해주세요.");
return;
}
}
// 2. 디렉토리 쓰기 권한 확인
if (!is_writable($log_dir)) {
error_log("--- NotificationSender ERROR: 디버그 로그 쓰기 오류. '{$log_dir}' 디렉토리에 쓰기 권한이 없습니다. 웹서버의 폴더 권한을 확인해주세요.");
return;
}
// 3. 로그 파일에 내용 기록
$log_file = $log_dir . '/notification_debug.log';
$log_message = date("[Y-m-d H:i:s]") . " " . $message . "\n";
if (file_put_contents($log_file, $log_message, FILE_APPEND | LOCK_EX) === false) {
error_log("--- NotificationSender ERROR: 디버그 로그 파일 쓰기 실패. '{$log_file}' 파일에 내용을 쓸 수 없습니다.");
}
}
}
@@ -0,0 +1,14 @@
<?php
if (!defined('_GNUBOARD_')) exit;
// 이 파일은 상담 예약 관련 팝업들을 한번에 쉽게 포함하기 위해 사용됩니다.
// 현재 파일의 경로를 기준으로 팝업 파일들의 경로를 정의합니다.
$consultant_components_path = dirname(__FILE__);
// 상담 예약 신청 팝업 포함
include_once($consultant_components_path . '/reservation_popup.php');
// 예약 확인/취소 팝업 포함
include_once($consultant_components_path . '/reservation_check.php');
?>
@@ -0,0 +1,401 @@
<?php
if (isset($_POST['action'])) {
include_once('../_common_con.php'); // 💡 [수정] 컴포넌트용 공통 파일 포함
header('Content-Type: application/json');
$action = $_POST['action'] ?? '';
// 예약 조회
if ($action === 'find_reservations') {
$customer_name = trim($_POST['customer_name'] ?? '');
$customer_phone = trim($_POST['customer_phone'] ?? '');
if (!$customer_name || !$customer_phone) {
echo json_encode(['success' => false, 'message' => '이름과 연락처를 모두 입력해주세요.']);
exit;
}
// 💡 [수정] 리소스 정보를 함께 조회하기 위해 LEFT JOIN 추가
$sql = " SELECT r.*, res.name as resource_name
FROM consultant_reservations r
LEFT JOIN consultant_resources res ON r.resource_id = res.id
WHERE r.customer_name = '" . sql_real_escape_string($customer_name) . "'
AND r.customer_phone = '" . sql_real_escape_string($customer_phone) . "'
AND r.is_deleted = 0
ORDER BY r.reservation_date DESC, r.reservation_time DESC ";
$result = sql_query($sql);
$reservations = [];
$cancel_deadline_hours = (int)consultant_get_config('cancel_deadline_hours', 24);
while ($row = sql_fetch_array($result)) {
$reservation_timestamp = strtotime($row['reservation_date'] . ' ' . $row['reservation_time']);
$cancellable_until = $reservation_timestamp - ($cancel_deadline_hours * 3600);
$row['is_cancellable'] = (time() < $cancellable_until && in_array($row['status'], ['payment_pending', 'reserved']));
$reservations[] = $row;
}
echo json_encode(['success' => true, 'data' => $reservations]);
exit;
}
// 예약 취소
if ($action === 'cancel_reservation') {
$reservation_id = (int)($_POST['reservation_id'] ?? 0);
$customer_name = trim($_POST['customer_name'] ?? '');
$customer_phone = trim($_POST['customer_phone'] ?? '');
if (!$reservation_id || !$customer_name || !$customer_phone) {
echo json_encode(['success' => false, 'message' => '필수 정보가 누락되었습니다.']);
exit;
}
$sql = " SELECT * FROM consultant_reservations
WHERE id = '{$reservation_id}'
AND customer_name = '" . sql_real_escape_string($customer_name) . "'
AND customer_phone = '" . sql_real_escape_string($customer_phone) . "'
AND is_deleted = 0 ";
$reservation = sql_fetch($sql);
if (!$reservation) {
echo json_encode(['success' => false, 'message' => '예약 정보를 찾을 수 없거나, 입력하신 정보와 일치하지 않습니다.']);
exit;
}
$cancel_deadline_hours = (int)consultant_get_config('cancel_deadline_hours', 24);
$reservation_timestamp = strtotime($reservation['reservation_date'] . ' ' . $reservation['reservation_time']);
$cancellable_until = $reservation_timestamp - ($cancel_deadline_hours * 3600);
if (time() >= $cancellable_until) {
echo json_encode(['success' => false, 'message' => '예약 취소 가능 시간이 지났습니다. 관리자에게 문의해주세요.']);
exit;
}
if (!in_array($reservation['status'], ['payment_pending', 'reserved'])) {
echo json_encode(['success' => false, 'message' => '이미 처리되었거나 취소된 예약입니다.']);
exit;
}
$sql_update = " UPDATE consultant_reservations
SET status = 'cancelled', updated_at = NOW()
WHERE id = '{$reservation_id}' ";
if (sql_query($sql_update)) {
consultant_log("고객 예약 취소: ID {$reservation_id} (고객: {$customer_name})");
echo json_encode(['success' => true, 'message' => '예약이 성공적으로 취소되었습니다.']);
} else {
echo json_encode(['success' => false, 'message' => '예약 취소 중 오류가 발생했습니다. 다시 시도해주세요.']);
}
exit;
}
}
if (!defined('_GNUBOARD_')) exit;
include_once(G5_ADMIN_PATH . '/consultant_manage/_common_con.php'); // 💡 [수정] 컴포넌트용 공통 파일 포함
// 💡 [추가] 상담 유형 한글 이름을 JavaScript에서 사용하기 위해 배열을 정의합니다.
$consultant_types_map = json_decode(consultant_get_config('consultation_types', '{"onsite":"현장 상담"}'), true);
if (!is_array($consultant_types_map)) {
$consultant_types_map = ['onsite' => '현장 상담']; // 파싱 실패 시 기본값
}
$ajax_url = G5_ADMIN_URL . '/consultant_manage/components/reservation_check.php';
?>
<!-- 예약 확인/취소 팝업 -->
<div id="reservation-check-popup" class="reservation-modal-overlay">
<div class="reservation-modal-content">
<div class="reservation-modal-header">
<h2>예약 확인 및 취소</h2>
<button type="button" class="reservation-modal-close" aria-label="팝업 닫기">&times;</button>
</div>
<div class="reservation-modal-body">
<div class="loading-overlay" style="display: none;">
<div class="loading-spinner"></div>
</div>
<!-- 💡 [개선] 조회 폼과 결과 화면을 분리합니다. -->
<div id="reservation-check-view-form">
<div class="check-form-wrap">
<p>예약 시 입력하신 이름과 연락처로 예약 내역을 조회할 수 있습니다.</p>
<form id="reservation-check-form">
<div class="form-group">
<label for="check_customer_name">예약자명</label>
<input type="text" id="check_customer_name" name="customer_name" class="form-control" required>
</div>
<div class="form-group">
<label for="check_customer_phone">연락처</label>
<input type="tel" id="check_customer_phone" name="customer_phone" class="form-control" placeholder="010-1234-5678" required>
</div>
<button type="submit" class="btn btn-primary">예약 조회</button>
</form>
</div>
</div>
<div id="reservation-check-view-results" style="display: none;">
<div class="results-header">
<h3>조회된 예약 내역</h3>
<button type="button" class="btn btn-secondary btn-sm" id="back-to-search-btn">새로 조회</button>
</div>
<div id="reservation-results">
<!-- 검색 결과가 여기에 표시됩니다. -->
</div>
</div>
</div>
</div>
</div>
<style>
/* 💡 [개선] 예약 신청 팝업과 스타일을 통일합니다. */
.check-form-wrap { background: #fff; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
.form-group { margin-bottom: 15px; }
.form-group label { display: block; font-weight: 500; color: #495057; margin-bottom: 8px; }
.form-control { width: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 6px; font-size: 16px; box-sizing: border-box; } /* iOS 줌 방지 */
.form-control:focus { outline: none; border-color: #007bff; box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); }
.btn {
display: block;
width: 100%;
padding: 12px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: background-color 0.2s ease;
/* 💡 [수정] 버튼 내 텍스트가 잘려보이는 현상을 해결합니다. */
height: auto;
line-height: 1.5;
}
.btn-primary { background: #007bff; color: white; }
.btn-primary:hover { background: #0056b3; }
#reservation-results { margin-top: 20px; }
.reservation-item { border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-bottom: 15px; }
.reservation-item-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #eee; padding-bottom: 10px; margin-bottom: 10px; }
.reservation-status { padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: bold; }
.status-reserved { background: #d4edda; color: #155724; }
/* 💡 [개선] 예약 상세 정보 스타일 */
.reservation-details-grid {
margin: 15px 0;
font-size: 15px; /* 글씨 크기 키움 */
line-height: 1.6; /* 줄 간격 확보 */
}
.detail-item { display: flex; margin-bottom: 10px; } /* 간격 조정 */
.detail-item .label { font-weight: 600; color: #555; width: 90px; flex-shrink: 0; } /* 너비 조정 */
.detail-item .value { color: #333; }
/* 💡 [추가] 예약 취소 버튼/안내문 영역 스타일 */
.reservation-actions {
margin-top: 20px;
text-align: right;
}
.reservation-actions .btn-danger {
width: auto; /* 버튼이 전체 너비를 차지하지 않도록 */
display: inline-block;
}
.cancel-notice { font-size: 13px !important; color: #666 !important; margin-top: 15px; text-align: right; padding: 10px; background-color: #fff; border-radius: 4px;}
/* 💡 [개선] 예약 조회 결과 헤더 스타일 */
.results-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid #eee; }
.results-header h3 { margin: 0; font-size: 18px; flex-grow: 1; } /* 제목이 남는 공간을 모두 차지하도록 설정 */
.results-header .btn { width: auto; flex-shrink: 0; } /* 헤더 안의 버튼은 내용만큼만 너비를 갖도록 설정 */
.btn-sm { padding: 5px 10px; font-size: 12px; }
.status-payment_pending { background: #fff3cd; color: #856404; }
.status-cancelled { background: #f8d7da; color: #721c24; }
.status-completed { background: #cce5ff; color: #004085; }
.btn-danger { background: #dc3545; color: white; }
.no-results { text-align: center; color: #666; padding: 40px 0; }
</style>
<script>
const ReservationCheckPopup = {
elements: {},
// 💡 [추가] PHP에서 전달받은 상담 유형 맵
consultantTypes: <?php echo json_encode($consultant_types_map, JSON_UNESCAPED_UNICODE); ?>,
state: {
name: '',
phone: ''
},
config: {
ajaxUrl: '<?php echo $ajax_url; ?>'
},
init() {
this.elements.popup = document.getElementById('reservation-check-popup');
if (!this.elements.popup) return;
this.elements.loading = this.elements.popup.querySelector('.loading-overlay');
this.elements.form = this.elements.popup.querySelector('#reservation-check-form');
this.elements.resultsContainer = this.elements.popup.querySelector('#reservation-results');
// 💡 [추가] 화면 전환용 요소
this.elements.formView = this.elements.popup.querySelector('#reservation-check-view-form');
this.elements.resultsView = this.elements.popup.querySelector('#reservation-check-view-results');
this.elements.backBtn = this.elements.popup.querySelector('#back-to-search-btn');
this.addEventListeners();
},
addEventListeners() {
const closeBtn = this.elements.popup.querySelector('.reservation-modal-close');
if (closeBtn) closeBtn.addEventListener('click', () => this.close());
this.elements.popup.addEventListener('click', e => {
if (e.target === this.elements.popup) this.close();
});
if (this.elements.form) {
this.elements.form.addEventListener('submit', e => {
e.preventDefault();
// 💡 [개선] 조회 버튼을 누를 때만 state를 업데이트하고, 그 state를 기반으로 조회합니다.
this.state.name = this.elements.form.querySelector('#check_customer_name').value;
this.state.phone = this.elements.form.querySelector('#check_customer_phone').value;
this.findReservations(); // state에 저장된 정보로 조회
});
}
if (this.elements.backBtn) {
this.elements.backBtn.addEventListener('click', () => {
this.elements.resultsView.style.display = 'none';
this.elements.formView.style.display = 'block';
this.elements.resultsContainer.innerHTML = '';
});
}
if (this.elements.resultsContainer) {
this.elements.resultsContainer.addEventListener('click', e => {
if (e.target.classList.contains('btn-cancel-reservation')) {
const reservationId = e.target.dataset.id;
if (confirm('정말 이 예약을 취소하시겠습니까?')) {
this.cancelReservation(reservationId);
}
}
});
}
},
open() {
if (!this.elements.popup) return;
this.elements.popup.classList.add('active');
document.body.style.overflow = 'hidden';
},
close() {
if (!this.elements.popup) return;
this.elements.popup.classList.remove('active');
document.body.style.overflow = '';
if (this.elements.form) this.elements.form.reset();
// 💡 [추가] 팝업을 닫을 때 화면 상태를 초기화합니다.
if (this.elements.resultsContainer) this.elements.resultsContainer.innerHTML = '';
if (this.elements.resultsView) this.elements.resultsView.style.display = 'none';
if (this.elements.formView) this.elements.formView.style.display = 'block';
},
async findReservations() {
this.showLoading();
// 💡 [개선] state에 저장된 정보로 FormData를 생성하여 일관성을 유지합니다.
const formData = new FormData();
formData.append('action', 'find_reservations');
formData.append('customer_name', this.state.name);
formData.append('customer_phone', this.state.phone);
try {
const response = await fetch(this.config.ajaxUrl, { method: 'POST', body: formData });
const result = await response.json();
if (result.success) {
// 💡 [추가] 조회 성공 시 화면을 전환합니다.
this.elements.formView.style.display = 'none';
this.elements.resultsView.style.display = 'block';
this.renderResults(result.data);
} else {
this.elements.resultsContainer.innerHTML = `<p class="no-results">${result.message}</p>`;
}
} catch (error) {
console.error("조회 오류:", error);
this.elements.resultsContainer.innerHTML = '<p class="no-results">조회 중 오류가 발생했습니다.</p>';
} finally {
this.hideLoading();
}
},
renderResults(reservations) {
if (reservations.length === 0) {
this.elements.resultsContainer.innerHTML = '<p class="no-results">조회된 예약 내역이 없습니다.</p>';
return;
}
// 💡 [수정] h3 태그는 정적 HTML로 이동했으므로 제거합니다.
let html = '';
const statusLabels = {
'payment_pending': '입금대기', 'reserved': '예약확정',
'completed': '상담완료', 'cancelled': '예약취소'
};
reservations.forEach(res => {
const date = new Date(res.reservation_date + ' ' + res.reservation_time);
const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long', hour: '2-digit', minute: '2-digit' };
const formattedDate = date.toLocaleDateString('ko-KR', options);
// 💡 [개선] 상담 유형의 한글 이름을 맵에서 찾아옵니다.
const consultationTypeName = this.consultantTypes[res.consultation_type] || res.consultation_type;
// 💡 [추가] 리소스 이름 표시
const resourceName = res.resource_name ? res.resource_name : '미지정 (빠른 배정)';
html += `
<div class="reservation-item" id="reservation-${res.id}">
<div class="reservation-item-header">
<strong>예약 번호: #${res.id}</strong>
<span class="reservation-status status-${res.status}">${statusLabels[res.status] || res.status}</span>
</div>
<div class="reservation-details-grid">
<div class="detail-item"><span class="label">상담 일시:</span><span class="value">${formattedDate}</span></div>
<div class="detail-item"><span class="label">상담 유형:</span><span class="value">${consultationTypeName}</span></div>
<div class="detail-item"><span class="label">담당/공간:</span><span class="value">${resourceName}</span></div>
</div>
<div class="reservation-actions">`;
if (res.is_cancellable) {
html += `<button type="button" class="btn btn-danger btn-cancel-reservation" data-id="${res.id}">예약 취소</button>`;
} else if (res.status !== 'cancelled' && res.status !== 'completed') {
html += `<p class="cancel-notice">예약 취소 가능 시간이 지났습니다. 변경/취소는 관리자에게 문의해주세요.</p>`;
}
html += `</div></div>`;
});
this.elements.resultsContainer.innerHTML = html;
},
async cancelReservation(reservationId) {
this.showLoading();
const formData = new FormData();
formData.append('action', 'cancel_reservation');
formData.append('reservation_id', reservationId);
formData.append('customer_name', this.state.name);
formData.append('customer_phone', this.state.phone);
try {
const response = await fetch(this.config.ajaxUrl, { method: 'POST', body: formData });
const result = await response.json();
alert(result.message);
if (result.success) {
await this.findReservations(); // 💡 [수정] await를 추가하여 목록 새로고침이 완료될 때까지 기다립니다.
}
} catch (error) {
console.error("취소 오류:", error);
alert('예약 취소 처리 중 오류가 발생했습니다.');
} finally {
// 💡 [제거] findReservations()가 자체적으로 로딩을 숨기므로 중복 호출을 제거합니다.
}
},
showLoading() { if (this.elements.loading) this.elements.loading.style.display = 'flex'; },
hideLoading() { if (this.elements.loading) this.elements.loading.style.display = 'none'; },
};
document.addEventListener('DOMContentLoaded', () => ReservationCheckPopup.init());
function openReservationCheckPopup() {
ReservationCheckPopup.open();
}
</script>
@@ -0,0 +1,868 @@
<?php
/**
* 상담 예약 팝업 UI 및 데이터 처리
*/
// AJAX 요청 처리
if (isset($_POST['action'])) {
include_once('../_common_con.php'); // 💡 [수정] 컴포넌트용 공통 파일 포함
header('Content-Type: application/json');
$action = $_POST['action'] ?? '';
$response = ['success' => false, 'message' => '알 수 없는 요청입니다.'];
// 월별 예약 가능일 조회
if ($action === 'get_month_availability') {
$year = (int) ($_POST['year'] ?? 0);
$month = (int) ($_POST['month'] ?? 0);
if ($year && $month) {
$start_date = date('Y-m-d', mktime(0, 0, 0, $month, 1, $year));
$end_date = date('Y-m-t', strtotime($start_date));
$max_advance_days = consultant_get_config('max_advance_days', 30);
$max_date = date('Y-m-d', strtotime("+" . $max_advance_days . " days"));
$sql = "SELECT
specific_date,
SUM(CASE WHEN is_available = 1 AND max_persons > (SELECT COUNT(*) FROM consultant_reservations r WHERE r.reservation_date = s.specific_date AND r.reservation_time = s.start_time AND r.status != 'cancelled') THEN 1 ELSE 0 END) as available_slots,
MAX(CASE WHEN temp_1 = 'holiday' THEN 1 ELSE 0 END) as is_holiday
FROM consultant_schedule s
WHERE specific_date BETWEEN '{$start_date}' AND '{$end_date}'
GROUP BY specific_date";
$result = sql_query($sql);
$availability = [];
while ($row = sql_fetch_array($result)) {
$is_bookable = true;
$reason = '';
if ($row['is_holiday']) {
$is_bookable = false;
$reason = 'holiday';
} elseif ($row['specific_date'] < date('Y-m-d')) {
$is_bookable = false;
$reason = 'past_date';
} elseif ($row['specific_date'] > $max_date) {
$is_bookable = false;
$reason = 'too_far';
} elseif ($row['available_slots'] == 0) {
$is_bookable = false;
$reason = 'full';
}
$availability[date('j', strtotime($row['specific_date']))] = [
'available' => $is_bookable,
'reason' => $reason
];
}
$response = ['success' => true, 'data' => $availability];
}
}
// 특정일의 예약 가능 시간 조회
if ($action === 'get_time_slots') {
$date = preg_replace('/[^0-9\-]/', '', $_POST['date'] ?? '');
if ($date) {
$min_advance_hours = consultant_get_config('min_advance_hours', 24);
$min_datetime = date('Y-m-d H:i:s', strtotime("+" . $min_advance_hours . " hours"));
$sql = "SELECT
s.start_time,
s.max_persons,
(SELECT COUNT(*) FROM consultant_reservations r WHERE r.reservation_date = s.specific_date AND r.reservation_time = s.start_time AND r.status != 'cancelled') as reserved_count
FROM consultant_schedule s
WHERE s.specific_date = '{$date}' AND s.is_available = 1
ORDER BY s.start_time";
$result = sql_query($sql);
$slots = [];
while ($row = sql_fetch_array($result)) {
$slot_datetime = $date . ' ' . $row['start_time'];
$is_too_soon = ($slot_datetime < $min_datetime);
$available_count = $row['max_persons'] - $row['reserved_count'];
$is_full = ($available_count <= 0);
$slots[] = [
'time' => substr($row['start_time'], 0, 5),
'available' => !$is_too_soon && !$is_full,
'reason' => $is_too_soon ? 'too_soon' : ($is_full ? 'full' : ''),
'reserved_count' => (int)$row['reserved_count'],
'max_persons' => (int)$row['max_persons']
];
}
$response = ['success' => true, 'data' => $slots];
}
}
echo json_encode($response);
exit;
}
// 💡 [수정] 아래는 팝업의 HTML/CSS/JS 부분입니다.
if (!defined('_GNUBOARD_')) exit;
include_once(G5_ADMIN_PATH . '/consultant_manage/_common_con.php'); // 💡 [수정] 컴포넌트용 공통 파일 포함
$consultant_installed = is_consultant_installed();
if($consultant_installed) { // 💡 [추가] 시스템이 설치된 경우에만 팝업을 렌더링합니다.
// 현재 날짜 정보
$current_year = date('Y');
$current_month_num = date('n');
// 💡 [개선] 설정 값을 DB에서 직접 가져와 일관성을 유지합니다.
$consultation_fee = consultant_get_config('consultation_fee', 50000);
$account_info = consultant_get_config('account_info', '');
$max_advance_days = consultant_get_config('max_advance_days', 30);
// 💡 [수정] 스킨 URL 및 AJAX 엔드포인트 설정
$ajax_url = G5_ADMIN_URL . '/consultant_manage/components/reservation_popup.php';
$form_action_url = G5_ADMIN_URL . '/consultant_manage/components/reservation_submit.php';
// 💡 [추가] 리소스 목록 조회
$resources = [];
$res_result = sql_query("SELECT id, name FROM consultant_resources WHERE is_active = 1 ORDER BY group_id, name");
while ($row = sql_fetch_array($res_result)) {
$resources[] = $row;
}
?>
<!-- 예약 팝업 오버레이 -->
<div id="reservation-popup" class="reservation-modal-overlay">
<div class="reservation-modal-content">
<!-- 팝업 헤더 -->
<div class="reservation-modal-header">
<h2>상담 예약 신청</h2>
<button type="button" class="reservation-modal-close" aria-label="팝업 닫기">
<span>&times;</span>
</button>
</div>
<!-- 팝업 본문 -->
<div class="reservation-modal-body">
<!-- 💡 [개선] 로딩 스피너 추가 -->
<div class="loading-overlay" style="display: none;">
<div class="loading-spinner"></div>
</div>
<!-- 단계 표시 -->
<div class="reservation-steps">
<div class="step active" data-step="1">
<span class="step-number">1</span>
<span class="step-text">날짜 선택</span>
</div>
<div class="step" data-step="2">
<span class="step-number">2</span>
<span class="step-text">시간 선택</span>
</div>
<div class="step" data-step="3">
<span class="step-number">3</span>
<span class="step-text">정보 입력</span>
</div>
</div>
<!-- 예약 폼 -->
<form id="reservation-form" method="post"
action="<?php echo $form_action_url; ?>">
<!-- 1단계: 달력 -->
<div class="reservation-step-content" data-step="1">
<div class="step-description">
<h4>📅 상담 날짜를 선택해주세요</h4>
<p>최대 <?php echo $max_advance_days; ?>일 후까지 예약 가능합니다.</p>
</div>
<div class="calendar-container">
<div class="calendar-header">
<button type="button" class="calendar-nav prev-month" aria-label="이전 달">
<span></span>
</button>
<h3 class="calendar-title">
<span id="calendar-year"><?php echo $current_year; ?></span>년
<span id="calendar-month"><?php echo $current_month_num; ?></span>월
</h3>
<button type="button" class="calendar-nav next-month" aria-label="다음 달">
<span></span>
</button>
</div>
<div class="calendar-grid">
<div class="calendar-weekdays">
<div class="weekday">일</div>
<div class="weekday">월</div>
<div class="weekday">화</div>
<div class="weekday">수</div>
<div class="weekday">목</div>
<div class="weekday">금</div>
<div class="weekday">토</div>
</div>
<div class="calendar-days" id="calendar-days">
<!-- 달력 날짜들이 JavaScript로 동적 생성됩니다 -->
</div>
</div>
<div class="calendar-legend">
<div class="legend-item"><span class="legend-color available"></span><span>예약 가능</span></div>
<!-- 💡 [추가] 휴일/마감 상태를 구분하는 범례 추가 -->
<div class="legend-item"><span class="legend-color holiday"></span><span>휴일</span></div>
<div class="legend-item"><span class="legend-color full"></span><span>예약 마감</span></div>
<div class="legend-item"><span class="legend-color unavailable"></span><span>지난날짜</span></div>
<div class="legend-item"><span class="legend-color selected"></span><span>선택</span></div>
</div>
</div>
</div>
<!-- 2단계: 시간 선택 -->
<div class="reservation-step-content" data-step="2" style="display: none;">
<div class="step-description">
<h4>🕐 상담 시간을 선택해주세요</h4>
<div class="selected-date-info"><strong>선택된 날짜: <span id="selected-date-display"></span></strong></div>
</div>
<div class="time-slots-container">
<div class="time-slots-grid" id="time-slots-grid">
<!-- 시간대들이 JavaScript로 동적 생성됩니다 -->
</div>
<div class="time-legend">
<div class="legend-item"><span class="legend-color time-available"></span><span>예약 가능</span></div>
<div class="legend-item"><span class="legend-color time-full"></span><span>예약 마감</span></div>
<div class="legend-item"><span class="legend-color time-too-soon"></span><span>예약 임박</span></div>
<div class="legend-item"><span class="legend-color time-selected"></span><span>선택</span></div>
</div>
</div>
</div>
<!-- 3단계: 고객 정보 입력 -->
<div class="reservation-step-content" data-step="3" style="display: none;">
<div class="step-description">
<h4>📝 고객 정보를 입력해주세요</h4>
</div>
<div class="reservation-summary">
<h5>📋 예약 정보 확인</h5>
<div class="summary-grid">
<div class="summary-item">
<span class="label">📅 예약 날짜:</span>
<span id="summary-date">-</span>
</div>
<div class="summary-item">
<span class="label">🕐 예약 시간:</span>
<span id="summary-time">-</span>
</div>
<div class="summary-item">
<span class="label">💰 상담 비용:</span>
<span><?php echo number_format($consultation_fee); ?>원</span>
</div>
</div>
</div>
<div class="customer-info-form">
<div class="form-row">
<div class="form-group">
<label for="customer-name">👤 이름 <span class="required">*</span></label>
<input type="text" id="customer-name" name="customer_name" required placeholder="홍길동">
</div>
<div class="form-group">
<label for="customer-phone">📱 연락처 <span class="required">*</span></label>
<input type="tel" id="customer-phone" name="customer_phone" required placeholder="010-1234-5678">
</div>
</div>
<div class="form-group">
<label for="customer-email">📧 이메일 <span class="required">*</span></label>
<input type="email" id="customer-email" name="customer_email" required placeholder="example@email.com">
</div>
<div class="form-group">
<label for="consultation-type">🏠 상담 유형</label>
<select id="consultation-type" name="consultation_type">
<option value="onsite">현장 상담</option>
<option value="online">온라인 상담</option>
<option value="phone">전화 상담</option>
</select>
</div>
<!-- 💡 [추가] 담당자/공간 선택 드롭다운 -->
<div class="form-group">
<label for="consultation-resource">👨‍⚕️ 담당자/공간 선택</label>
<select id="consultation-resource" name="resource_id">
<option value="">선택 안 함 (빠른 배정)</option>
<?php foreach ($resources as $res): ?>
<option value="<?php echo $res['id']; ?>"><?php echo htmlspecialchars($res['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="customer-request">📝 요청사항</label>
<textarea id="customer-request" name="customer_request" rows="4" placeholder="상담 관련 요청사항이나 문의사항을 입력해주세요."></textarea>
</div>
</div>
<!-- 숨겨진 필드들 -->
<input type="hidden" id="selected-date" name="reservation_date">
<input type="hidden" id="selected-time" name="reservation_time">
<input type="hidden" name="payment_amount" value="<?php echo $consultation_fee; ?>">
<input type="hidden" name="status" value="payment_pending">
</div>
<!-- 네비게이션 버튼 -->
<div class="reservation-nav-buttons">
<button type="button" class="btn-prev" style="display: none;">← 이전</button>
<button type="button" class="btn-next">다음 →</button>
<button type="submit" class="btn-submit" style="display: none;">예약 신청</button>
</div>
</form>
</div>
</div>
</div>
<!-- 예약 팝업 스타일 -->
<style>
/* 💡 [추가] 로딩 오버레이 스타일 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.loading-spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
/* 팝업 오버레이 */
.reservation-modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 9999;
overflow-y: auto;
}
.reservation-modal-overlay.active {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.reservation-modal-content {
background: #fff;
border-radius: 10px;
width: 100%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
position: relative;
}
.reservation-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 25px;
border-bottom: 1px solid #eee;
background: #f8f9fa;
border-radius: 10px 10px 0 0;
}
.reservation-modal-header h2 { margin: 0; font-size: 20px; font-weight: 600; color: #333; }
.reservation-modal-close {
background: none; border: none; font-size: 24px; cursor: pointer; color: #666;
padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;
border-radius: 50%; transition: all 0.2s ease;
}
.reservation-modal-close:hover { background: #e9ecef; color: #333; }
.reservation-modal-body { padding: 25px; }
/* 단계 표시 */
.reservation-steps { display: flex; justify-content: center; margin-bottom: 30px; position: relative; }
.reservation-steps::before {
content: ''; position: absolute; top: 15px; left: 25%; right: 25%;
height: 2px; background: #e9ecef; z-index: 1;
}
.step { display: flex; flex-direction: column; align-items: center; position: relative; z-index: 2; background: #fff; padding: 0 15px; }
.step-number {
width: 30px; height: 30px; border-radius: 50%; background: #e9ecef; color: #6c757d;
display: flex; align-items: center; justify-content: center; font-weight: 600; margin-bottom: 8px; transition: all 0.3s ease;
}
.step.active .step-number { background: #007bff; color: #fff; }
.step.completed .step-number { background: #28a745; color: #fff; }
.step-text { font-size: 12px; color: #6c757d; font-weight: 500; }
.step.active .step-text { color: #007bff; font-weight: 600; }
/* 달력 */
.calendar-container { max-width: 100%; }
.calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.calendar-nav {
background: none; border: 1px solid #ddd; width: 35px; height: 35px; border-radius: 50%;
cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease;
}
.calendar-nav:hover { background: #f8f9fa; border-color: #007bff; }
.calendar-title { margin: 0; font-size: 18px; font-weight: 600; color: #333; }
.calendar-grid { border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; }
.calendar-weekdays { display: grid; grid-template-columns: repeat(7, 1fr); background: #f8f9fa; }
.weekday { padding: 12px 8px; text-align: center; font-weight: 600; color: #495057; font-size: 14px; border-right: 1px solid #e9ecef; }
.weekday:last-child { border-right: none; }
.calendar-days { display: grid; grid-template-columns: repeat(7, 1fr); }
.calendar-day {
aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
border-right: 1px solid #e9ecef; border-bottom: 1px solid #e9ecef; cursor: pointer;
transition: all 0.2s ease; font-size: 14px; position: relative;
}
.calendar-day:nth-child(7n) { border-right: none; }
.calendar-day.other-month { color: #ccc; background: #f8f9fa; cursor: default; }
.calendar-day.available { background: #fff; color: #333; }
.calendar-day.available:hover { background: #e3f2fd; color: #1976d2; }
.calendar-day.unavailable { background: #f5f5f5; color: #999; cursor: not-allowed; }
.calendar-day.full { background: #fbe9e7; color: #c62828; cursor: not-allowed; } /* 예약 마감 */
.calendar-day.holiday { background: #e8eaf6; color: #3f51b5; cursor: not-allowed; } /* 휴일 */
.calendar-day.selected { background: #007bff; color: #fff; font-weight: 600; }
.calendar-day.today { font-weight: 600; color: #dc3545; }
.calendar-loading, .loading, .error, .no-slots { grid-column: 1 / -1; text-align: center; padding: 40px 20px; color: #666; font-style: italic; }
.error { color: #dc3545; }
/* 범례 */
.calendar-legend, .time-legend { display: flex; justify-content: center; gap: 20px; margin-top: 15px; flex-wrap: wrap; }
.legend-item { display: flex; align-items: center; gap: 5px; font-size: 12px; color: #666; }
.legend-color { width: 12px; height: 12px; border-radius: 2px; border: 1px solid #ddd; }
.legend-color.available { background: #fff; }
.legend-color.unavailable { background: #f5f5f5; }
.legend-color.full { background: #fbe9e7; }
.legend-color.holiday { background: #e8eaf6; }
.legend-color.selected { background: #007bff; }
.legend-color.time-available { background: #e8f5e8; border-color: #28a745; }
.legend-color.time-full { background: #f8d7da; border-color: #dc3545; }
.legend-color.time-too-soon { background: #f1f3f5; border-color: #dee2e6; }
.legend-color.time-selected { background: #007bff; border-color: #007bff; }
/* 시간 선택 */
.selected-date-info { margin-bottom: 10px; }
.step-description { margin-bottom: 20px; }
.step-description h4 { margin: 0 0 10px 0; font-size: 18px; }
.step-description p { margin: 0; font-size: 14px; color: #666; }
.selected-date-info h4 { margin: 0; color: #333; font-size: 16px; }
.time-slots-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; margin-bottom: 20px; }
.time-slot {
padding: 12px 8px; border: 2px solid #e9ecef; border-radius: 6px; text-align: center; cursor: pointer;
transition: all 0.2s ease; font-size: 14px; font-weight: 500; display: flex; flex-direction: column; gap: 4px;
}
.time-text { font-weight: 600; }
.slot-info { font-size: 11px; opacity: 0.8; }
/* 💡 [수정] 예약 가능 슬롯 스타일 변경 */
.time-slot.available { background: #e7f3ff; border-color: #007bff; color: #004085; }
.time-slot.available:hover { background: #d4edda; transform: translateY(-1px); }
.time-slot.full { background: #f8d7da; border-color: #dc3545; color: #721c24; cursor: not-allowed; }
.time-slot.too-soon { background: #f1f3f5; color: #868e96; cursor: not-allowed; border-color: #dee2e6; }
.time-slot.selected { background: #007bff; border-color: #007bff; color: #fff; }
/* 고객 정보 폼 */
.reservation-summary { background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
.reservation-summary h5 { margin: 0 0 10px 0; font-size: 16px; }
.summary-grid { display: grid; grid-template-columns: 1fr; gap: 8px; }
.summary-item { display: flex; justify-content: space-between; font-size: 14px; }
.summary-item .label { font-weight: 500; color: #495057; }
.summary-item span { color: #333; }
.customer-info-form h4 { margin: 0 0 15px 0; color: #333; font-size: 16px; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
.form-group { margin-bottom: 15px; }
.form-group label { display: block; margin-bottom: 8px; font-weight: 500; color: #495057; font-size: 14px; }
.required { color: #dc3545; font-weight: bold; }
.form-group input, .form-group textarea, .form-group select {
width: 100%; padding: 12px; border: 1px solid #ced4da; border-radius: 6px;
font-size: 14px; transition: border-color 0.2s ease; box-sizing: border-box;
}
.form-group textarea { resize: vertical; min-height: 80px; }
.form-group input:focus, .form-group textarea:focus, .form-group select:focus {
outline: none; border-color: #007bff; box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
/* 네비게이션 버튼 */
.reservation-nav-buttons {
display: flex; justify-content: space-between; gap: 10px; margin-top: 25px;
padding-top: 20px; border-top: 1px solid #e9ecef;
}
.reservation-nav-buttons button {
padding: 12px 24px; border: none; border-radius: 6px; font-size: 14px;
font-weight: 600; cursor: pointer; transition: all 0.2s ease; min-width: 100px;
}
.btn-prev { background: #6c757d; color: #fff; }
.btn-prev:hover { background: #5a6268; }
.btn-next, .btn-submit { background: #007bff; color: #fff; }
.btn-next:hover, .btn-submit:hover { background: #0056b3; }
/* 반응형 디자인 - 모바일 최적화 */
@media (max-width: 768px) {
.reservation-modal-content { margin: 10px; max-height: 95vh; border-radius: 8px; }
.reservation-modal-header, .reservation-modal-body { padding: 15px; }
.reservation-modal-header h2 { font-size: 18px; }
.reservation-steps { margin-bottom: 20px; }
.step { padding: 0 10px; }
.step-number { width: 28px; height: 28px; font-size: 13px; }
.step-text { font-size: 11px; }
.calendar-day { font-size: 13px; }
.time-slots-grid { grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); }
.form-row { grid-template-columns: 1fr; gap: 0; }
.form-group input, .form-group textarea, .form-group select { font-size: 16px; /* iOS 줌 방지 */ }
}
@media (max-width: 480px) {
.reservation-modal-overlay { padding: 0; align-items: flex-end; }
.reservation-modal-content { margin: 0; width: 100%; max-height: 90vh; border-radius: 10px 10px 0 0; }
.reservation-nav-buttons { flex-direction: column-reverse; gap: 10px; }
.reservation-nav-buttons button { width: 100%; }
}
</style>
<!-- 💡 [개선] JavaScript 로직 전면 수정 -->
<script>
const ReservationPopup = {
elements: {},
state: {
currentStep: 1,
selectedDate: null,
selectedTime: null,
currentYear: new Date().getFullYear(),
currentMonth: new Date().getMonth() + 1,
},
config: {
ajaxUrl: '<?php echo $ajax_url; ?>',
maxAdvanceDays: <?php echo (int)$max_advance_days; ?>,
},
init() {
this.elements.popup = document.getElementById('reservation-popup');
if (!this.elements.popup) return;
this.elements.loading = this.elements.popup.querySelector('.loading-overlay');
this.elements.form = this.elements.popup.querySelector('#reservation-form');
this.elements.calendar = {
year: this.elements.popup.querySelector('#calendar-year'),
month: this.elements.popup.querySelector('#calendar-month'),
days: this.elements.popup.querySelector('#calendar-days'),
prevBtn: this.elements.popup.querySelector('.prev-month'),
nextBtn: this.elements.popup.querySelector('.next-month'),
};
this.elements.timeSlotsGrid = this.elements.popup.querySelector('#time-slots-grid');
this.elements.nav = {
prevBtn: this.elements.popup.querySelector('.btn-prev'),
nextBtn: this.elements.popup.querySelector('.btn-next'),
submitBtn: this.elements.popup.querySelector('.btn-submit'),
};
this.addEventListeners();
},
addEventListeners() {
// 💡 [수정] 닫기 버튼을 더 안정적으로 찾아 이벤트를 추가하고, null 체크를 추가합니다.
const closeBtn = this.elements.popup.querySelector('.reservation-modal-close');
if (closeBtn) closeBtn.addEventListener('click', () => this.close());
this.elements.popup.addEventListener('click', e => {
if (e.target === this.elements.popup) this.close();
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && this.elements.popup.classList.contains('active')) this.close();
});
// 💡 [수정] 모든 이벤트 리스너에 null 체크를 추가하여 스크립트 오류를 방지합니다.
if (this.elements.calendar.prevBtn) this.elements.calendar.prevBtn.addEventListener('click', () => this.changeMonth(-1));
if (this.elements.calendar.nextBtn) this.elements.calendar.nextBtn.addEventListener('click', () => this.changeMonth(1));
if (this.elements.nav.prevBtn) this.elements.nav.prevBtn.addEventListener('click', () => this.goToStep(this.state.currentStep - 1));
if (this.elements.nav.nextBtn) this.elements.nav.nextBtn.addEventListener('click', () => this.goToNextStep());
if (this.elements.form) {
this.elements.form.addEventListener('submit', e => {
e.preventDefault();
this.submitForm();
});
}
},
open() {
this.state.currentYear = new Date().getFullYear();
this.state.currentMonth = new Date().getMonth() + 1;
this.goToStep(1);
this.renderCalendar();
this.elements.popup.classList.add('active');
document.body.style.overflow = 'hidden';
},
close() {
this.elements.popup.classList.remove('active');
document.body.style.overflow = '';
this.elements.form.reset();
},
changeMonth(delta) {
this.state.currentMonth += delta;
if (this.state.currentMonth < 1) {
this.state.currentMonth = 12;
this.state.currentYear--;
} else if (this.state.currentMonth > 12) {
this.state.currentMonth = 1;
this.state.currentYear++;
}
this.renderCalendar();
},
async renderCalendar() {
this.elements.calendar.year.textContent = this.state.currentYear;
this.elements.calendar.month.textContent = this.state.currentMonth;
this.elements.calendar.days.innerHTML = '<div class="loading">달력 정보를 불러오는 중...</div>';
const availability = await this.fetchMonthAvailability();
if (!availability) {
this.elements.calendar.days.innerHTML = '<div class="error">달력 정보를 불러올 수 없습니다.</div>';
return;
}
this.elements.calendar.days.innerHTML = '';
const firstDay = new Date(this.state.currentYear, this.state.currentMonth - 1, 1);
const daysInMonth = new Date(this.state.currentYear, this.state.currentMonth, 0).getDate();
const startDayOfWeek = firstDay.getDay();
for (let i = 0; i < startDayOfWeek; i++) {
this.elements.calendar.days.appendChild(this.createDayElement(0, true));
}
for (let day = 1; day <= daysInMonth; day++) {
this.elements.calendar.days.appendChild(this.createDayElement(day, false, availability[day]));
}
},
createDayElement(day, isOtherMonth, availability = null) {
const dayElement = document.createElement('div');
dayElement.className = 'calendar-day';
if (isOtherMonth) {
dayElement.classList.add('other-month');
return dayElement;
}
dayElement.textContent = day;
const dateStr = `${this.state.currentYear}-${String(this.state.currentMonth).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const today = new Date();
today.setHours(0, 0, 0, 0);
const currentDate = new Date(dateStr);
currentDate.setHours(0, 0, 0, 0);
if (currentDate.getTime() === today.getTime()) {
dayElement.classList.add('today');
} else if (currentDate.getTime() < today.getTime()){
dayElement.classList.add('unavailable');
} else if (availability && availability.available) {
dayElement.classList.add('available');
dayElement.addEventListener('click', () => this.selectDate(dateStr, dayElement));
} else {
// 💡 [개선] 예약 불가 사유에 따라 다른 스타일을 적용합니다.
if (availability && availability.reason === 'holiday') {
dayElement.classList.add('holiday');
} else if (availability && availability.reason === 'full') {
dayElement.classList.add('full');
} else {
dayElement.classList.add('unavailable');
}
}
return dayElement;
},
selectDate(dateStr, element) {
const prevSelected = this.elements.calendar.days.querySelector('.selected');
if (prevSelected) prevSelected.classList.remove('selected');
element.classList.add('selected');
this.state.selectedDate = dateStr;
},
async fetchMonthAvailability() {
this.showLoading();
try {
const formData = new FormData();
formData.append('action', 'get_month_availability');
formData.append('year', this.state.currentYear);
formData.append('month', this.state.currentMonth);
const response = await fetch(this.config.ajaxUrl, { method: 'POST', body: formData });
const result = await response.json();
return result.success ? result.data : null;
} catch (error) {
console.error('Error fetching month availability:', error);
return null;
} finally {
this.hideLoading();
}
},
async fetchTimeSlots() {
this.showLoading();
this.elements.timeSlotsGrid.innerHTML = '';
try {
const formData = new FormData();
formData.append('action', 'get_time_slots');
formData.append('date', this.state.selectedDate);
const response = await fetch(this.config.ajaxUrl, { method: 'POST', body: formData });
const result = await response.json();
if (result.success) {
this.renderTimeSlots(result.data);
} else {
this.elements.timeSlotsGrid.innerHTML = `<div class="error">${result.message || '시간 정보를 불러올 수 없습니다.'}</div>`;
}
} catch (error) {
console.error('Error fetching time slots:', error);
this.elements.timeSlotsGrid.innerHTML = '<div class="error">오류가 발생했습니다. 다시 시도해주세요.</div>';
} finally {
this.hideLoading();
}
},
renderTimeSlots(slots) {
// 💡 [수정] 슬롯이 하나도 없는 경우와, 예약 가능한 슬롯이 없는 경우를 구분하여 메시지를 표시합니다.
if (slots.length === 0) {
this.elements.timeSlotsGrid.innerHTML = '<div class="no-slots">해당 날짜에 운영되는 상담 시간이 없습니다.</div>';
return;
}
let hasAvailableSlots = false;
this.elements.timeSlotsGrid.innerHTML = ''; // 그리드 초기화
slots.forEach(slot => {
const slotElement = document.createElement('div');
slotElement.className = 'time-slot';
let slotInfoText = '';
const reservedCount = slot.reserved_count;
if (slot.available) {
hasAvailableSlots = true;
slotElement.classList.add('available');
slotElement.addEventListener('click', () => this.selectTime(slot.time, slotElement));
slotInfoText = `예약 ${reservedCount} / ${slot.max_persons}`;
} else {
if (slot.reason === 'full') {
slotElement.classList.add('full');
slotInfoText = '마감';
} else if (slot.reason === 'too_soon') {
slotElement.classList.add('too-soon');
slotInfoText = '예약 임박';
} else {
slotElement.classList.add('full'); // Fallback
slotInfoText = '마감';
}
}
slotElement.innerHTML = `<span class="time-text">${slot.time}</span> <span class="slot-info">${slotInfoText}</span>`;
this.elements.timeSlotsGrid.appendChild(slotElement);
});
// 예약 가능한 슬롯이 하나도 없는 경우 안내 메시지 추가
if (!hasAvailableSlots) {
const noSlotsMessage = document.createElement('div');
noSlotsMessage.className = 'no-slots';
noSlotsMessage.textContent = '현재 예약 가능한 시간이 없습니다. 다른 날짜를 선택해주세요.';
this.elements.timeSlotsGrid.prepend(noSlotsMessage);
}
},
selectTime(time, element) {
const prevSelected = this.elements.timeSlotsGrid.querySelector('.selected');
if (prevSelected) prevSelected.classList.remove('selected');
element.classList.add('selected');
this.state.selectedTime = time;
},
goToStep(step) {
this.state.currentStep = step;
this.elements.popup.querySelectorAll('.step').forEach((el, i) => {
el.classList.toggle('active', i + 1 === step);
el.classList.toggle('completed', i + 1 < step);
});
this.elements.popup.querySelectorAll('.reservation-step-content').forEach(el => {
el.style.display = parseInt(el.dataset.step) === step ? 'block' : 'none';
});
this.elements.nav.prevBtn.style.display = step > 1 ? 'inline-block' : 'none';
this.elements.nav.nextBtn.style.display = step < 3 ? 'inline-block' : 'none';
this.elements.nav.submitBtn.style.display = step === 3 ? 'inline-block' : 'none';
},
goToNextStep() {
if (this.state.currentStep === 1 && !this.state.selectedDate) {
alert('날짜를 선택해주세요.');
return;
}
if (this.state.currentStep === 2 && !this.state.selectedTime) {
alert('시간을 선택해주세요.');
return;
}
if (this.state.currentStep < 3) {
this.goToStep(this.state.currentStep + 1);
if (this.state.currentStep === 2) this.fetchTimeSlots();
if (this.state.currentStep === 3) this.updateSummary();
}
},
updateSummary() {
const date = new Date(this.state.selectedDate);
const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' };
this.elements.popup.querySelector('#summary-date').textContent = date.toLocaleDateString('ko-KR', options);
this.elements.popup.querySelector('#summary-time').textContent = this.state.selectedTime;
this.elements.popup.querySelector('#selected-date-display').textContent = date.toLocaleDateString('ko-KR', options);
this.elements.form.querySelector('#selected-date').value = this.state.selectedDate;
this.elements.form.querySelector('#selected-time').value = this.state.selectedTime;
},
async submitForm() {
if (!this.elements.form.checkValidity()) {
alert('필수 입력 항목을 모두 채워주세요.');
this.elements.form.reportValidity();
return;
}
this.showLoading();
this.elements.nav.submitBtn.disabled = true;
this.elements.nav.submitBtn.textContent = '처리 중...';
try {
const formData = new FormData(this.elements.form);
const response = await fetch('<?php echo $form_action_url; ?>', { method: 'POST', body: formData });
const result = await response.json();
if (result.success) {
alert('예약 신청이 완료되었습니다. 입금 확인 후 예약이 확정됩니다.');
this.close();
// 필요시 페이지 새로고침 또는 다른 동작 수행
// location.reload();
} else {
alert(result.message || '예약 처리 중 오류가 발생했습니다.');
}
} catch (error) {
console.error('Form submission error:', error);
alert('네트워크 오류가 발생했습니다. 다시 시도해주세요.');
} finally {
this.hideLoading();
this.elements.nav.submitBtn.disabled = false;
this.elements.nav.submitBtn.textContent = '예약 신청';
}
},
showLoading() { if (this.elements.loading) this.elements.loading.style.display = 'flex'; },
hideLoading() { if (this.elements.loading) this.elements.loading.style.display = 'none'; },
};
document.addEventListener('DOMContentLoaded', () => ReservationPopup.init());
// 외부에서 팝업을 열기 위한 전역 함수
function openReservationPopup() {
ReservationPopup.open();
}
</script>
<?php } // end if($consultant_installed) ?>
@@ -0,0 +1,68 @@
<?php
include_once('../_common_con.php'); // 💡 [수정] 컴포넌트용 공통 파일 포함
header('Content-Type: application/json');
try {
// 입력 데이터 정리
$reservation_data = [
'customer_name' => trim($_POST['customer_name'] ?? ''),
'customer_phone' => trim($_POST['customer_phone'] ?? ''),
'customer_email' => trim($_POST['customer_email'] ?? ''),
'reservation_date' => trim($_POST['reservation_date'] ?? ''),
'reservation_time' => trim($_POST['reservation_time'] ?? ''),
'consultation_type' => trim($_POST['consultation_type'] ?? 'onsite'),
'request_memo' => trim($_POST['customer_request'] ?? ''),
'payment_amount' => (int)($_POST['payment_amount'] ?? 0),
'status' => 'payment_pending'
];
// 필수 항목 유효성 검사
if (empty($reservation_data['customer_name']) || empty($reservation_data['customer_phone']) || empty($reservation_data['reservation_date']) || empty($reservation_data['reservation_time'])) {
throw new Exception('필수 예약 정보가 누락되었습니다.');
}
// 예약 가능 여부 재확인 (서버 측 검증)
$sql_check = "SELECT COUNT(*) as cnt FROM consultant_schedule
WHERE specific_date = '{$reservation_data['reservation_date']}'
AND start_time = '{$reservation_data['reservation_time']}'
AND is_available = 1";
$schedule = sql_fetch($sql_check);
if (!$schedule || $schedule['cnt'] == 0) {
throw new Exception('선택하신 시간은 예약이 불가능합니다. 다른 시간을 선택해주세요.');
}
// 예약 생성
$sql = "INSERT INTO consultant_reservations
(customer_name, customer_phone, customer_email, reservation_date, reservation_time, consultation_type, request_memo, payment_amount, status, created_at, updated_at)
VALUES
(
'" . sql_real_escape_string($reservation_data['customer_name']) . "',
'" . sql_real_escape_string($reservation_data['customer_phone']) . "',
'" . sql_real_escape_string($reservation_data['customer_email']) . "',
'" . sql_real_escape_string($reservation_data['reservation_date']) . "',
'" . sql_real_escape_string($reservation_data['reservation_time']) . "',
'" . sql_real_escape_string($reservation_data['consultation_type']) . "',
'" . sql_real_escape_string($reservation_data['request_memo']) . "',
'{$reservation_data['payment_amount']}',
'{$reservation_data['status']}',
NOW(),
NOW()
)";
if (sql_query($sql)) {
$reservation_id = sql_insert_id();
consultant_log("새 예약 신청: ID {$reservation_id} (고객: {$reservation_data['customer_name']})");
// TODO: 고객 및 관리자에게 알림 발송 로직 추가
echo json_encode(['success' => true, 'message' => '예약 신청이 완료되었습니다.']);
} else {
throw new Exception('데이터베이스 저장 중 오류가 발생했습니다.');
}
} catch (Exception $e) {
consultant_log("예약 신청 오류: " . $e->getMessage(), 'error');
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}
?>
@@ -0,0 +1,85 @@
<?php
$sub_menu = '850100'; // 대시보드 메뉴와 동일하게 설정
include_once('./_common.php');
$g5['title'] = '상담 예약 팝업 샘플';
include_once(G5_ADMIN_PATH.'/admin.head.php');
?>
<div class="local_desc01 local_desc">
<p>
이 페이지는 사이트의 어떤 페이지에서든 상담 예약 기능을 쉽게 추가하는 방법을 보여주는 예제입니다.<br>
아래 코드 한 줄만 포함하면, 버튼과 팝업 기능이 모두 활성화됩니다.
</p>
<pre><code>&lt;?php include_once(G5_ADMIN_PATH . '/consultant_manage/components/_consultant_popups.php'); ?&gt;</code></pre>
</div>
<!-- ================================================================== -->
<!-- 💡 [시작] 상담 예약 기능 추가 예제 -->
<!-- ================================================================== -->
<div style="text-align:center; padding: 50px 20px; background-color:#f5f5f5; border-radius:10px; margin: 20px 0;">
<h3 style="margin-bottom:15px;">상담이 필요하신가요?</h3>
<p style="margin-bottom:25px; color:#666;">버튼을 눌러 간편하게 상담을 신청하거나, 기존 예약을 확인/취소할 수 있습니다.</p>
<!-- 1. 팝업을 여는 버튼들 -->
<div class="consultant-buttons">
<button type="button" class="reservation-btn-main" onclick="openReservationPopup()">
<i class="fa fa-calendar"></i> 상담 예약 신청
</button>
<button type="button" class="reservation-check-btn" onclick="openReservationCheckPopup()">
예약 확인/취소
</button>
</div>
</div>
<?php
// 2. 팝업 파일들 포함 (이 코드 한 줄이면 모든 팝업 기능이 로드됩니다)
include_once(G5_ADMIN_PATH . '/consultant_manage/components/_consultant_popups.php');
?>
<!-- 버튼 디자인을 위한 CSS (사이트의 공통 CSS 파일에 추가하는 것을 권장합니다) -->
<style>
.consultant-buttons {
display: flex;
justify-content: center;
gap: 15px;
flex-wrap: wrap;
}
.reservation-btn-main, .reservation-check-btn {
color: white;
border: none;
padding: 15px 30px;
border-radius: 25px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
.reservation-btn-main {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
box-shadow: 0 4px 15px rgba(40, 167, 69, 0.3);
}
.reservation-btn-main:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(40, 167, 69, 0.4);
}
.reservation-btn-main i {
margin-right: 8px;
}
.reservation-check-btn {
background: #6c757d;
}
.reservation-check-btn:hover {
background: #5a6268;
}
</style>
<!-- ================================================================== -->
<!-- 💡 [끝] 상담 예약 기능 추가 예제 -->
<!-- ================================================================== -->
<?php
include_once(G5_ADMIN_PATH.'/admin.tail.php');
?>
+578
View File
@@ -0,0 +1,578 @@
<?php
/**
* 상담 예약 관리 시스템 대시보드
*/
$sub_menu = '850100';
include_once('./_common.php');
// 관리자 권한 확인
if (!$is_admin) {
alert('관리자만 접근할 수 있습니다.');
}
// 설치 확인
if (!is_consultant_installed()) {
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
}
$g5['title'] = '상담 예약 대시보드';
// 오늘 날짜 기준 통계
$today = date('Y-m-d');
$this_week_start = date('Y-m-d', strtotime('monday this week'));
$this_month_start = date('Y-m-01');
// 오늘 예약 현황
$sql = "SELECT
COUNT(*) as total,
COUNT(CASE WHEN status = 'payment_pending' THEN 1 END) as pending,
COUNT(CASE WHEN status = 'reserved' THEN 1 END) as confirmed,
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed
FROM consultant_reservations
WHERE reservation_date = '{$today}' AND is_deleted = 0";
$today_stats = sql_fetch($sql);
// 이번 주 예약 현황
$sql = "SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'completed' THEN payment_amount ELSE 0 END) as revenue
FROM consultant_reservations
WHERE reservation_date >= '{$this_week_start}'
AND reservation_date <= '{$today}'
AND is_deleted = 0";
$week_stats = sql_fetch($sql);
// 이번 달 예약 현황
$sql = "SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'completed' THEN payment_amount ELSE 0 END) as revenue
FROM consultant_reservations
WHERE reservation_date >= '{$this_month_start}'
AND is_deleted = 0";
$month_stats = sql_fetch($sql);
// 최근 예약 목록 (5개)
$sql = "SELECT * FROM consultant_reservations
WHERE is_deleted = 0
ORDER BY created_at DESC
LIMIT 5";
$recent_reservations = [];
$result = sql_query($sql);
while ($row = sql_fetch_array($result)) {
$recent_reservations[] = $row;
}
// 오늘 예약 목록
$sql = "SELECT * FROM consultant_reservations
WHERE reservation_date = '{$today}'
AND is_deleted = 0
ORDER BY reservation_time";
$today_reservations = [];
$result = sql_query($sql);
while ($row = sql_fetch_array($result)) {
$today_reservations[] = $row;
}
// 입금 대기 예약 수
$sql = "SELECT COUNT(*) as count FROM consultant_reservations
WHERE status = 'payment_pending' AND is_deleted = 0";
$pending_count = sql_fetch($sql)['count'];
// 시간대별 예약 현황 (이번 주)
$sql = "SELECT
reservation_time,
COUNT(*) as count
FROM consultant_reservations
WHERE reservation_date >= '{$this_week_start}'
AND reservation_date <= '{$today}'
AND is_deleted = 0
GROUP BY reservation_time
ORDER BY reservation_time";
$time_stats = [];
$result = sql_query($sql);
while ($row = sql_fetch_array($result)) {
$time_stats[] = $row;
}
include_once(G5_ADMIN_PATH . '/admin.head.php');
?>
<style>
.dashboard-container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
text-align: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.stat-number {
font-size: 32px;
font-weight: bold;
margin-bottom: 8px;
}
.stat-label {
color: #666;
font-size: 14px;
margin-bottom: 4px;
}
.stat-sublabel {
color: #999;
font-size: 12px;
}
.stat-card.today .stat-number {
color: #007bff;
}
.stat-card.week .stat-number {
color: #28a745;
}
.stat-card.month .stat-number {
color: #17a2b8;
}
.stat-card.pending .stat-number {
color: #ffc107;
}
.content-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
margin-bottom: 30px;
}
.content-card {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.card-header {
background: #fff;
padding: 15px 20px;
border-bottom: 1px solid #ddd;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-content {
padding: 20px;
}
.reservation-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.reservation-item:last-child {
border-bottom: none;
}
.reservation-info {
flex: 1;
}
.reservation-name {
font-weight: bold;
margin-bottom: 4px;
}
.reservation-details {
font-size: 12px;
color: #666;
}
.reservation-status {
padding: 4px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: bold;
text-align: center;
min-width: 60px;
}
.status-payment_pending {
background: #fff3cd;
color: #856404;
}
.status-reserved {
background: #d4edda;
color: #155724;
}
.status-completed {
background: #cce5ff;
color: #004085;
}
.status-cancelled {
background: #f8d7da;
color: #721c24;
}
.quick-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
display: inline-block;
font-size: 14px;
font-weight: 600;
transition: all 0.2s;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-warning {
background: #ffc107;
color: #212529;
}
.btn-info {
background: #17a2b8;
color: white;
}
.btn-sm {
padding: 6px 12px;
font-size: 12px;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #666;
}
.chart-container {
height: 200px;
display: flex;
align-items: end;
justify-content: space-around;
padding: 20px 0;
border-top: 1px solid #eee;
margin-top: 20px;
}
.chart-bar {
background: #007bff;
width: 30px;
border-radius: 2px 2px 0 0;
position: relative;
transition: all 0.3s;
}
.chart-bar:hover {
background: #0056b3;
}
.chart-label {
position: absolute;
bottom: -25px;
left: 50%;
transform: translateX(-50%);
font-size: 11px;
color: #666;
white-space: nowrap;
}
.chart-value {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
font-size: 10px;
color: #333;
font-weight: bold;
}
.alert {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
}
.alert-warning {
color: #856404;
background-color: #fff3cd;
border-color: #ffeaa7;
}
.alert-info {
color: #0c5460;
background-color: #d1ecf1;
border-color: #bee5eb;
}
@media (max-width: 768px) {
.content-grid {
grid-template-columns: 1fr;
}
.dashboard-header {
flex-direction: column;
gap: 15px;
align-items: stretch;
}
.quick-actions {
justify-content: center;
}
}
</style>
<div class="dashboard-container">
<div class="dashboard-header">
<div>
<h2><?php echo $g5['title']; ?></h2>
<p>상담 예약 현황을 한눈에 확인하세요</p>
</div>
<div class="quick-actions">
<a href="reservations.php" class="btn btn-primary">예약 관리</a>
<a href="schedule_generate.php" class="btn btn-success">일정 설정</a>
<a href="statistics.php" class="btn btn-info">통계 보기</a>
</div>
</div>
<!-- 통계 카드 -->
<div class="stats-grid">
<div class="stat-card today">
<div class="stat-number"><?php echo number_format($today_stats['total']); ?></div>
<div class="stat-label">오늘 예약</div>
<div class="stat-sublabel">
확정 <?php echo $today_stats['confirmed']; ?>건 |
대기 <?php echo $today_stats['pending']; ?>건
</div>
</div>
<div class="stat-card week">
<div class="stat-number"><?php echo number_format($week_stats['total']); ?></div>
<div class="stat-label">이번 주 예약</div>
<div class="stat-sublabel">
매출 <?php echo number_format($week_stats['revenue']); ?>원
</div>
</div>
<div class="stat-card month">
<div class="stat-number"><?php echo number_format($month_stats['total']); ?></div>
<div class="stat-label">이번 달 예약</div>
<div class="stat-sublabel">
매출 <?php echo number_format($month_stats['revenue']); ?>원
</div>
</div>
<div class="stat-card pending">
<div class="stat-number"><?php echo number_format($pending_count); ?></div>
<div class="stat-label">입금 대기</div>
<div class="stat-sublabel">
<?php if ($pending_count > 0): ?>
<a href="reservations.php?status=payment_pending" style="color: #856404;">확인 필요</a>
<?php else: ?>
모두 처리됨
<?php endif; ?>
</div>
</div>
</div>
<?php if ($pending_count > 0): ?>
<div class="alert alert-warning">
<strong>알림:</strong> 입금 대기 중인 예약이 <?php echo $pending_count; ?>건 있습니다.
<a href="reservations.php?status=payment_pending">지금 확인하기</a>
</div>
<?php endif; ?>
<!-- 메인 콘텐츠 -->
<div class="content-grid">
<!-- 오늘 예약 현황 -->
<div class="content-card">
<div class="card-header">
<span>오늘 예약 현황 (<?php echo date('Y-m-d'); ?>)</span>
<a href="reservations.php?date=<?php echo $today; ?>" class="btn btn-sm btn-primary">전체 보기</a>
</div>
<div class="card-content">
<?php if (empty($today_reservations)): ?>
<div class="empty-state">
<p>오늘 예약된 상담이 없습니다.</p>
</div>
<?php else: ?>
<?php foreach ($today_reservations as $reservation): ?>
<div class="reservation-item">
<div class="reservation-info">
<div class="reservation-name">
<?php echo htmlspecialchars($reservation['customer_name']); ?>
</div>
<div class="reservation-details">
<?php echo $reservation['reservation_time']; ?> |
<?php echo htmlspecialchars($reservation['customer_phone']); ?> |
<?php echo number_format($reservation['payment_amount']); ?>원
</div>
</div>
<div class="reservation-status status-<?php echo $reservation['status']; ?>">
<?php
$status_labels = [
'payment_pending' => '입금대기',
'reserved' => '예약확정',
'completed' => '상담완료',
'cancelled' => '예약취소'
];
echo $status_labels[$reservation['status']] ?? $reservation['status'];
?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<!-- 최근 예약 -->
<div class="content-card">
<div class="card-header">
<span>최근 예약</span>
<a href="reservations.php" class="btn btn-sm btn-primary">전체 보기</a>
</div>
<div class="card-content">
<?php if (empty($recent_reservations)): ?>
<div class="empty-state">
<p>최근 예약이 없습니다.</p>
</div>
<?php else: ?>
<?php foreach ($recent_reservations as $reservation): ?>
<div class="reservation-item">
<div class="reservation-info">
<div class="reservation-name">
<?php echo htmlspecialchars($reservation['customer_name']); ?>
</div>
<div class="reservation-details">
<?php echo $reservation['reservation_date']; ?>
<?php echo $reservation['reservation_time']; ?><br>
<?php echo date('m-d H:i', strtotime($reservation['created_at'])); ?> 신청
</div>
</div>
<div class="reservation-status status-<?php echo $reservation['status']; ?>">
<?php
$status_labels = [
'payment_pending' => '입금대기',
'reserved' => '예약확정',
'completed' => '상담완료',
'cancelled' => '예약취소'
];
echo $status_labels[$reservation['status']] ?? $reservation['status'];
?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
<!-- 시간대별 예약 현황 -->
<?php if (!empty($time_stats)): ?>
<div class="content-card">
<div class="card-header">
<span>시간대별 예약 현황 (이번 주)</span>
</div>
<div class="card-content">
<div class="chart-container">
<?php
$max_count = max(array_column($time_stats, 'count'));
foreach ($time_stats as $stat):
$height = $max_count > 0 ? ($stat['count'] / $max_count) * 150 : 0;
?>
<div class="chart-bar" style="height: <?php echo $height; ?>px;">
<div class="chart-value"><?php echo $stat['count']; ?></div>
<div class="chart-label"><?php echo substr($stat['reservation_time'], 0, 5); ?></div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
<!-- 시스템 정보 -->
<div class="alert alert-info">
<strong>시스템 정보:</strong>
상담 예약 시스템 v<?php echo G5_CONSULTANT_VERSION; ?> |
마지막 업데이트: <?php echo date('Y-m-d H:i'); ?> |
<a href="settings.php">시스템 설정</a>
</div>
</div>
<script>
// 실시간 업데이트 (5분마다)
setInterval(function () {
location.reload();
}, 300000);
// 차트 애니메이션
document.addEventListener('DOMContentLoaded', function () {
const bars = document.querySelectorAll('.chart-bar');
bars.forEach((bar, index) => {
setTimeout(() => {
bar.style.opacity = '0';
bar.style.transform = 'scaleY(0)';
bar.style.transformOrigin = 'bottom';
setTimeout(() => {
bar.style.transition = 'all 0.5s ease';
bar.style.opacity = '1';
bar.style.transform = 'scaleY(1)';
}, 100);
}, index * 100);
});
});
</script>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
?>
+128
View File
@@ -0,0 +1,128 @@
<?php
if (!defined('_GNUBOARD_')) exit;
// 상담 관리 시스템 버전
define('G5_CONSULTANT_VERSION', '1.0.0');
// 상담 관리 시스템 설치 확인
function is_consultant_installed()
{
global $g5;
$consultant_tables = ['consultant_schedule', 'consultant_reservations', 'consultant_config', 'consultant_log'];
foreach ($consultant_tables as $table) {
if(!isset($g5[$table.'_table'])) $g5[$table.'_table'] = $table;
$sql = "SHOW TABLES LIKE '{$g5[$table.'_table']}'";
$result = sql_query($sql, false);
if (!$result || sql_num_rows($result) == 0) {
return false;
}
}
return true;
}
// 상담 관리 시스템 권한 확인
function consultant_auth_check($auth_level = 'r')
{
global $member, $is_admin;
if (!$is_admin) {
alert('관리자만 접근할 수 있습니다.');
}
// 추가 권한 확인 로직 (필요시)
return true;
}
// 상담 관리 시스템 설정값 조회
function consultant_get_config($key, $default = null)
{
global $g5;
if(!isset($g5['consultant_config_table'])) $g5['consultant_config_table'] = 'consultant_config';
$sql = "SELECT config_value FROM {$g5['consultant_config_table']} WHERE config_key = '" . sql_real_escape_string($key) . "'";
$result = sql_fetch($sql);
if ($result) {
return $result['config_value'];
}
return $default;
}
// 상담 관리 시스템 설정값 저장
function consultant_set_config($key, $value)
{
global $g5;
if(!isset($g5['consultant_config_table'])) $g5['consultant_config_table'] = 'consultant_config';
$sql = "INSERT INTO {$g5['consultant_config_table']} (config_key, config_value, updated_at)
VALUES ('" . sql_real_escape_string($key) . "', '" . sql_real_escape_string($value) . "', NOW())
ON DUPLICATE KEY UPDATE
config_value = '" . sql_real_escape_string($value) . "',
updated_at = NOW()";
return sql_query($sql);
}
// 로그 함수
function consultant_log($message, $level = 'info')
{
global $g5, $member;
if(!isset($g5['consultant_log_table'])) $g5['consultant_log_table'] = 'consultant_log';
$sql = "INSERT INTO {$g5['consultant_log_table']} (mb_id, log_level, log_message, ip_address, log_time) VALUES ('".($member['mb_id'] ?? '')."', '".sql_real_escape_string($level)."', '".sql_real_escape_string($message)."', '".$_SERVER['REMOTE_ADDR']."', '".G5_TIME_YMDHIS."')";
sql_query($sql, false);
}
// 💡 [추가] 알림 발송 함수 (메일/SMS 통합)
function consultant_send_notification($type, $template_key, $data) {
global $g5;
// 템플릿 조회
$table_name = ($type === 'sms') ? 'consultant_sms_templates' : 'consultant_mail_templates';
// 테이블 존재 확인
$table_check = sql_query("SHOW TABLES LIKE '{$table_name}'", false);
if (sql_num_rows($table_check) == 0) return false;
$sql = "SELECT * FROM {$table_name} WHERE template_key = '" . sql_real_escape_string($template_key) . "'";
$template = sql_fetch($sql);
if (!$template) return false;
// 변수 치환
$subject = $template['template_subject'];
$content = $template['template_content'];
foreach ($data as $key => $value) {
$subject = str_replace('{' . $key . '}', $value, $subject);
$content = str_replace('{' . $key . '}', $value, $content);
}
if ($type === 'email') {
// 이메일 발송 (G5 기본 mailer 사용 가정)
include_once(G5_LIB_PATH.'/mailer.lib.php');
// $data['customer_email'] 등이 존재해야 함
if (!empty($data['customer_email'])) {
mailer($g5['title'], $g5['admin_email'], $data['customer_email'], $subject, $content, 1);
consultant_log("이메일 발송: {$template_key} to {$data['customer_email']}");
return true;
}
} elseif ($type === 'sms') {
// SMS 발송 (G5 기본 SMS 라이브러리 사용 가정 - 실제 구현은 SMS 모듈에 따라 다름)
// 여기서는 로그만 남기고 실제 발송 로직은 SMS 모듈에 맞게 구현 필요
// 예:
// include_once(G5_LIB_PATH.'/icode.sms.lib.php');
// $SMS = new SMS;
// $SMS->SMS_con($g5['sms_admin'], $g5['sms_id'], $g5['sms_pw'], $g5['sms_port']);
// $SMS->Add($data['customer_phone'], $g5['admin_phone'], $g5['sms_admin'], iconv("utf-8", "euc-kr", $content), "");
// $SMS->Send();
consultant_log("SMS 발송 시도: {$template_key} to {$data['customer_phone']} (내용: $content)");
return true; // 실제 발송 성공 여부에 따라 변경
}
return false;
}
?>
+209
View File
@@ -0,0 +1,209 @@
<?php
$sub_menu = '850640';
include_once('./_common.php');
include_once(__DIR__ . '/lib/SchemaManager.class.php');
if (!$is_admin) {
alert('관리자만 접근할 수 있습니다.');
}
/**
* SQL 파일에서 테이블 이름을 추출하는 함수
*/
function get_tables_from_sql_file($filepath) {
$tables = [];
if (!file_exists($filepath)) return $tables;
$lines = file($filepath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (preg_match('/CREATE TABLE(?: IF NOT EXISTS)? `([^`]+)`/i', $line, $matches)) {
$tables[] = $matches[1];
}
}
return $tables;
}
$g5['title'] = '상담 예약 시스템 설치';
include_once(G5_ADMIN_PATH . '/admin.head.php');
$install_result = null;
$delete_result = null;
$action = $_POST['action'] ?? '';
$tables_to_check = get_tables_from_sql_file(__DIR__ . '/install.sql');
if ($action === 'install') {
check_admin_token();
try {
$sql_file = __DIR__ . '/install.sql';
$schemaManager = new SchemaManager($sql_file);
$schemaManager->execute();
$db_results = $schemaManager->get_results();
$data_msg = insert_default_data() ? "성공" : "실패";
$menu_msg = create_admin_menu_file();
$install_result = ['db' => $db_results, 'data' => $data_msg, 'menu' => $menu_msg];
} catch (Exception $e) {
$install_result['errors'][] = '설치 중 심각한 오류 발생: ' . $e->getMessage();
}
} else if ($action === 'delete') {
check_admin_token();
$delete_result = ['tables' => [], 'menu' => ''];
$tables_to_delete = $tables_to_check;
foreach ($tables_to_delete as $table) {
sql_query("DROP TABLE IF EXISTS `{$table}`", false);
$delete_result['tables'][] = $table;
}
$menu_file = G5_ADMIN_PATH . '/admin.menu850.consultant_manage.php';
if (file_exists($menu_file)) {
if (@unlink($menu_file)) {
$delete_result['menu'] = '메뉴 파일 삭제 성공';
} else {
$delete_result['menu'] = '메뉴 파일 삭제 실패 (권한 확인 필요)';
}
}
}
function insert_default_data() {
$default_configs = [
['consultation_duration', '60', '1회 상담 시간 (분)'], ['max_persons_per_slot', '2', '동시간대 최대 예약 인원'],
['consultation_fee', '50000', '상담 비용 (원)'], ['account_info', '국민은행 123-456-789 (주)상담센터', '상담비 입금 계좌'],
['notification_enabled', '1', '알림 발송 사용 여부'], ['auto_confirm_enabled', '0', '자동 예약 확정 사용 여부'],
['max_advance_days', '30', '최대 예약 가능 일수'], ['min_advance_hours', '24', '최소 예약 시간 (시간)'],
['cancel_deadline_hours', '24', '예약 취소 마감 시간 (시간)']
];
foreach ($default_configs as $config) {
sql_query("INSERT IGNORE INTO consultant_config (config_key, config_value, config_desc) VALUES ('{$config[0]}', '{$config[1]}', '{$config[2]}')");
}
// 💡 [수정] 메일 템플릿 기본 데이터
$mail_templates = [
['consultant_reservation_customer', '고객 예약 신청 확인', '[상담예약] 예약 신청이 접수되었습니다', "안녕하세요 {customer_name}님,\n\n상담 예약 신청이 정상적으로 접수되었습니다.\n\n예약 정보:\n- 날짜: {reservation_date}\n- 시간: {reservation_time}\n- 상담비: {payment_amount}원\n\n입금 계좌: {account_info}\n\n입금 확인 후 예약이 확정됩니다.\n\n감사합니다."],
['consultant_confirmed_customer', '고객 예약 확정 알림', '[상담예약] 예약이 확정되었습니다', "안녕하세요 {customer_name}님,\n\n입금이 확인되어 예약이 확정되었습니다.\n\n예약 정보:\n- 날짜: {reservation_date}\n- 시간: {reservation_time}\n\n상담 당일 시간에 맞춰 방문해주시기 바랍니다.\n\n감사합니다."],
['consultant_cancelled_customer', '고객 예약 취소 알림', '[상담예약] 예약이 취소되었습니다', "안녕하세요 {customer_name}님,\n\n예약이 취소되었습니다.\n\n취소된 예약 정보:\n- 날짜: {reservation_date}\n- 시간: {reservation_time}\n\n취소 사유: {cancel_reason}\n\n문의사항이 있으시면 연락주시기 바랍니다.\n\n감사합니다."]
];
foreach ($mail_templates as $template) {
sql_query("INSERT IGNORE INTO consultant_mail_templates (template_key, template_name, template_subject, template_content) VALUES ('{$template[0]}', '{$template[1]}', '{$template[2]}', '" . sql_real_escape_string($template[3]) . "')");
}
// 💡 [추가] SMS 템플릿 기본 데이터
$sms_templates = [
['consultant_reservation_customer', '고객 예약 신청 확인', '[상담예약] 예약 신청 접수', "{customer_name}님, 상담 예약이 접수되었습니다.\n일시: {reservation_date} {reservation_time}\n계좌: {account_info}\n입금 확인 후 확정됩니다."],
['consultant_confirmed_customer', '고객 예약 확정 알림', '[상담예약] 예약 확정', "{customer_name}님, 예약이 확정되었습니다.\n일시: {reservation_date} {reservation_time}\n시간 맞춰 방문 부탁드립니다."],
['consultant_cancelled_customer', '고객 예약 취소 알림', '[상담예약] 예약 취소', "{customer_name}님, 예약이 취소되었습니다.\n일시: {reservation_date} {reservation_time}\n사유: {cancel_reason}"]
];
foreach ($sms_templates as $template) {
sql_query("INSERT IGNORE INTO consultant_sms_templates (template_key, template_name, template_subject, template_content) VALUES ('{$template[0]}', '{$template[1]}', '{$template[2]}', '" . sql_real_escape_string($template[3]) . "')");
}
return true;
}
function create_admin_menu_file() {
$source_file = __DIR__ . '/admin.menu850.consultant_manage.php';
$target_file = G5_ADMIN_PATH . '/admin.menu850.consultant_manage.php';
if (!file_exists($source_file)) return "실패 (메뉴 원본 파일 없음)";
if (file_exists($target_file)) return "성공 (이미 존재함)";
if (@copy($source_file, $target_file)) return "성공";
return "실패 (파일 복사 오류)";
}
$existing_tables = [];
foreach ($tables_to_check as $table) {
if (sql_query("SHOW TABLES LIKE '$table'", false) && sql_num_rows(sql_query("SHOW TABLES LIKE '$table'", false)) > 0) {
$existing_tables[] = $table;
}
}
$is_installed = count($existing_tables) == count($tables_to_check);
?>
<style>
.install-container { max-width: 800px; margin: 20px auto; padding: 20px; background: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.install-header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 2px solid #AA20FF; }
.install-header h1 { color: #AA20FF; margin-bottom: 10px; }
.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin: 30px 0; }
.feature-card { padding: 20px; border: 1px solid #e0e0e0; border-radius: 8px; text-align: center; }
.feature-card i { font-size: 2em; color: #AA20FF; margin-bottom: 10px; }
.status-table { width: 100%; border-collapse: collapse; margin: 20px 0; }
.status-table th, .status-table td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
.status-table th { background-color: #fff; font-weight: bold; }
.status-ok { color: #28a745; font-weight: bold; }
.status-missing { color: #dc3545; font-weight: bold; }
.install-btn { display: block; width: 200px; margin: 30px auto; padding: 15px 30px; background: #AA20FF; color: white; text-align: center; text-decoration: none; border-radius: 5px; font-size: 16px; font-weight: bold; border: none; cursor: pointer; transition: background-color 0.3s; }
.install-btn:hover { background: #8A1ACC; color: white; }
.install-btn:disabled { background: #ccc; cursor: not-allowed; }
.alert { padding: 15px; margin: 20px 0; border-radius: 5px; }
.alert-success { background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724; }
.alert-info { background-color: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; }
.alert-danger { background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
.btn-secondary { background: #6c757d; color: white; border-color: #6c757d; padding: 5px 10px; border-radius: 4px; text-decoration: none; }
.btn-secondary:hover { background: #5a6268; }
.btn-danger { background: #dc3545; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; }
.btn-danger:hover { background: #c82333; }
.button-group { display: flex; justify-content: center; align-items: center; gap: 10px; }
</style>
<div class="install-container">
<div class="install-header">
<h1><i class="fa fa-calendar-check"></i> 상담 예약 시스템</h1>
<p>전문적인 상담 예약 관리 및 일정 관리 솔루션</p>
</div>
<?php if ($install_result): ?>
<div class="alert alert-success"><h4><i class="fa fa-check-circle"></i> 설치 작업 완료</h4>
<p>데이터베이스 및 기본 설정 설치가 완료되었습니다.</p>
<p><?php echo 'data : '.$install_result['data']; ?></p>
<p><?php echo 'menu : '.$install_result['menu']; ?></p>
<p><a href="./dashboard.php" class="btn btn-primary">상담 예약 관리로 이동</a></p></div>
<?php elseif ($delete_result): ?>
<div class="alert alert-danger"><h4><i class="fa fa-trash"></i> 삭제 작업 완료</h4><p>솔루션 관련 데이터와 파일이 삭제되었습니다.</p><ul><?php foreach($delete_result['tables'] as $tbl) echo "<li>{$tbl} 테이블 삭제됨</li>"; ?><li><?php echo $delete_result['menu']; ?></li></ul></div>
<?php elseif ($is_installed): ?>
<div class="alert alert-success"><h4><i class="fa fa-check-circle"></i> 설치 완료</h4><p>상담 예약 시스템이 이미 설치되어 있습니다.</p><p><a href="./dashboard.php" class="btn btn-primary">상담 예약 관리로 이동</a></p></div>
<?php else: ?>
<div class="alert alert-info"><h4><i class="fa fa-info-circle"></i> 설치 필요</h4><p>상담 예약 시스템을 사용하기 위해 설치가 필요합니다.</p></div>
<?php endif; ?>
<h3><i class="fa fa-database"></i> 설치 상태</h3>
<table class="status-table">
<thead><tr><th>테이블명</th><th>설명</th><th>상태</th></tr></thead>
<tbody>
<?php foreach ($tables_to_check as $table): ?>
<tr>
<td><code><?php echo $table; ?></code></td>
<td><?php echo array('consultant_config' => '환경 설정', 'consultant_schedule' => '상담 스케줄', 'consultant_reservations' => '예약 정보', 'consultant_mail_templates' => '메일 템플릿', 'consultant_sms_templates' => '문자 템플릿', 'consultant_log' => '시스템 로그')[$table] ?? '데이터 테이블'; ?></td>
<td>
<?php if (in_array($table, $existing_tables)): ?>
<span class="status-ok"><i class="fa fa-check"></i> 설치됨</span>
<?php else: ?>
<span class="status-missing"><i class="fa fa-times"></i> 미설치</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if (!$is_installed): ?>
<form method="post" onsubmit="return confirm('솔루션을 설치하시겠습니까?');">
<input type="hidden" name="action" value="install">
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
<button type="submit" class="install-btn"><i class="fa fa-download"></i> 솔루션 설치하기</button>
</form>
<?php endif; ?>
<?php if ($is_installed && !$install_result && !$delete_result): ?>
<div class="button-group" style="text-align: center; margin-top: 20px;">
<form method="post" onsubmit="return confirm('기존 데이터는 유지되며, 변경된 DB 구조만 업데이트 됩니다. 진행하시겠습니까?');">
<input type="hidden" name="action" value="install">
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
<button type="submit" class="btn btn-secondary"><i class="fa fa-sync"></i> 재설치 (업데이트)</button>
</form>
<form method="post" onsubmit="return confirm('정말로 솔루션을 삭제하시겠습니까? 모든 관련 데이터와 파일이 영구적으로 삭제됩니다.');">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
<button type="submit" class="btn-danger"><i class="fa fa-trash"></i> 솔루션 삭제하기</button>
</form>
</div>
<?php endif; ?>
</div>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
?>
+159
View File
@@ -0,0 +1,159 @@
-- 1. 상담 설정 테이블
CREATE TABLE IF NOT EXISTS `consultant_config` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`config_key` varchar(100) NOT NULL COMMENT '설정 키',
`config_value` text COMMENT '설정 값',
`config_desc` varchar(255) DEFAULT NULL COMMENT '설정 설명',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `config_key` (`config_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담 예약 시스템 설정';
-- 2. 상담 일정 테이블
CREATE TABLE IF NOT EXISTS `consultant_schedule` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`specific_date` date DEFAULT NULL COMMENT '특정 날짜',
`start_time` time NOT NULL COMMENT '시작 시간',
`end_time` time NOT NULL COMMENT '종료 시간',
`time_slot` int(11) DEFAULT 60 COMMENT '예약 단위 시간(분)',
`max_persons` int(11) DEFAULT 1 COMMENT '동시간대 최대 예약 인원',
`is_available` tinyint(1) DEFAULT 1 COMMENT '예약 가능 여부',
`temp_1` varchar(255) DEFAULT NULL COMMENT '임시필드1',
`temp_2` varchar(255) DEFAULT NULL COMMENT '임시필드2',
`temp_3` varchar(255) DEFAULT NULL COMMENT '임시필드3',
`temp_4` varchar(255) DEFAULT NULL COMMENT '임시필드4',
`temp_5` varchar(255) DEFAULT NULL COMMENT '임시필드5',
`extra_1` varchar(255) DEFAULT NULL COMMENT '여분필드1',
`extra_2` varchar(255) DEFAULT NULL COMMENT '여분필드2',
`extra_3` varchar(255) DEFAULT NULL COMMENT '여분필드3',
`extra_4` varchar(255) DEFAULT NULL COMMENT '여분필드4',
`extra_5` varchar(255) DEFAULT NULL COMMENT '여분필드5',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_specific_date` (`specific_date`),
KEY `idx_is_available` (`is_available`),
KEY `idx_time_range` (`start_time`,`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담 예약 상세 스케줄';
-- 3. 상담 예약 테이블
CREATE TABLE IF NOT EXISTS `consultant_reservations` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`wr_id` int(11) DEFAULT NULL COMMENT '연결된 게시글 ID',
`customer_name` varchar(100) NOT NULL COMMENT '고객명',
`customer_phone` varchar(20) NOT NULL COMMENT '고객 연락처',
`customer_email` varchar(100) DEFAULT NULL COMMENT '고객 이메일',
`reservation_date` date NOT NULL COMMENT '예약 날짜',
`reservation_time` time NOT NULL COMMENT '예약 시간',
`consultation_type` varchar(50) DEFAULT 'onsite' COMMENT '상담 유형',
`resource_id` int(11) DEFAULT NULL COMMENT '배정된 리소스 ID',
`status` varchar(50) DEFAULT 'payment_pending' COMMENT '예약 상태',
`payment_amount` int(11) DEFAULT 0 COMMENT '상담 비용',
`payment_status` varchar(50) DEFAULT 'pending' COMMENT '결제 상태',
`payment_confirmed_at` datetime DEFAULT NULL COMMENT '입금 확인 시간',
`payment_confirmed_by` varchar(50) DEFAULT NULL COMMENT '입금 확인자',
`request_memo` text COMMENT '고객 요청사항',
`admin_memo` text COMMENT '관리자 메모',
`is_deleted` tinyint(1) DEFAULT 0 COMMENT '삭제 여부',
`temp_1` varchar(255) DEFAULT NULL COMMENT '임시필드1',
`temp_2` varchar(255) DEFAULT NULL COMMENT '임시필드2',
`temp_3` varchar(255) DEFAULT NULL COMMENT '임시필드3',
`temp_4` varchar(255) DEFAULT NULL COMMENT '임시필드4',
`temp_5` varchar(255) DEFAULT NULL COMMENT '임시필드5',
`extra_1` varchar(255) DEFAULT NULL COMMENT '여분필드1',
`extra_2` varchar(255) DEFAULT NULL COMMENT '여분필드2',
`extra_3` varchar(255) DEFAULT NULL COMMENT '여분필드3',
`extra_4` varchar(255) DEFAULT NULL COMMENT '여분필드4',
`extra_5` varchar(255) DEFAULT NULL COMMENT '여분필드5',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_reservation_date` (`reservation_date`),
KEY `idx_status` (`status`),
KEY `idx_customer_phone` (`customer_phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담 예약 정보';
-- 4. 상담 메일 알림 템플릿 테이블
CREATE TABLE IF NOT EXISTS `consultant_mail_templates` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`template_key` varchar(100) NOT NULL,
`template_type` varchar(10) NOT NULL DEFAULT 'email' COMMENT '템플릿 종류 (email)',
`template_name` varchar(200) NOT NULL,
`template_subject` varchar(255) NOT NULL,
`template_content` text NOT NULL,
`is_active` tinyint(1) DEFAULT 1,
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `template_key` (`template_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담 예약 메일 알림 템플릿';
-- 5. 상담 문자 알림 템플릿 테이블
CREATE TABLE IF NOT EXISTS `consultant_sms_templates` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`template_key` varchar(100) NOT NULL,
`template_type` varchar(10) NOT NULL DEFAULT 'sms' COMMENT '템플릿 종류 (sms)',
`template_name` varchar(200) NOT NULL,
`template_subject` varchar(255) NOT NULL,
`template_content` text NOT NULL,
`is_active` tinyint(1) DEFAULT 1,
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `template_key` (`template_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담 예약 문자 알림 템플릿';
-- 6. 시스템 로그 테이블
CREATE TABLE IF NOT EXISTS `consultant_log` (
`log_id` int(11) NOT NULL AUTO_INCREMENT,
`log_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`mb_id` varchar(20) DEFAULT NULL,
`log_level` varchar(20) NOT NULL DEFAULT 'info',
`log_message` text NOT NULL,
`ip_address` varchar(255) DEFAULT NULL,
PRIMARY KEY (`log_id`),
KEY `idx_log_time` (`log_time`),
KEY `idx_mb_id` (`mb_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담 예약 시스템 로그';
-- 7. 상담 그룹 테이블
CREATE TABLE IF NOT EXISTS `consultant_groups` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '그룹명',
`is_active` tinyint(1) DEFAULT 1 COMMENT '사용여부',
`temp_1` varchar(255) DEFAULT NULL COMMENT '임시필드1',
`temp_2` varchar(255) DEFAULT NULL COMMENT '임시필드2',
`temp_3` varchar(255) DEFAULT NULL COMMENT '임시필드3',
`temp_4` varchar(255) DEFAULT NULL COMMENT '임시필드4',
`temp_5` varchar(255) DEFAULT NULL COMMENT '임시필드5',
`extra_1` varchar(255) DEFAULT NULL COMMENT '여분필드1',
`extra_2` varchar(255) DEFAULT NULL COMMENT '여분필드2',
`extra_3` varchar(255) DEFAULT NULL COMMENT '여분필드3',
`extra_4` varchar(255) DEFAULT NULL COMMENT '여분필드4',
`extra_5` varchar(255) DEFAULT NULL COMMENT '여분필드5',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='상담/자원 그룹';
-- 8. 상담 리소스 테이블
CREATE TABLE IF NOT EXISTS `consultant_resources` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`group_id` int(11) NOT NULL COMMENT '그룹 ID',
`name` varchar(100) NOT NULL COMMENT '리소스명(이름/호실)',
`description` varchar(255) DEFAULT NULL COMMENT '설명',
`is_active` tinyint(1) DEFAULT 1 COMMENT '사용여부',
`temp_1` varchar(255) DEFAULT NULL COMMENT '임시필드1',
`temp_2` varchar(255) DEFAULT NULL COMMENT '임시필드2',
`temp_3` varchar(255) DEFAULT NULL COMMENT '임시필드3',
`temp_4` varchar(255) DEFAULT NULL COMMENT '임시필드4',
`temp_5` varchar(255) DEFAULT NULL COMMENT '임시필드5',
`extra_1` varchar(255) DEFAULT NULL COMMENT '여분필드1',
`extra_2` varchar(255) DEFAULT NULL COMMENT '여분필드2',
`extra_3` varchar(255) DEFAULT NULL COMMENT '여분필드3',
`extra_4` varchar(255) DEFAULT NULL COMMENT '여분필드4',
`extra_5` varchar(255) DEFAULT NULL COMMENT '여분필드5',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='상담사/자원 목록';
@@ -0,0 +1,90 @@
<?php
if (!defined('_GNUBOARD_')) exit;
class NotificationManager
{
/**
* 템플릿 키를 기반으로 알림을 발송합니다.
* @param string $template_key 템플릿 키 (e.g., 'reservation_confirmed')
* @param array $data 치환될 데이터 배열 (e.g., ['customer_name' => '홍길동'])
* @return bool 성공 여부
*/
public function sendNotification($template_key, $data)
{
$success = true;
// 1. 이메일 템플릿 조회 및 발송
$email_sql = "SELECT * FROM consultant_mail_templates WHERE template_key = '" . sql_real_escape_string($template_key) . "' AND is_active = 1";
$email_template = sql_fetch($email_sql);
if ($email_template) {
$subject = $this->replaceVariables($email_template['template_subject'], $data);
$content = $this->replaceVariables($email_template['template_content'], $data);
if (!$this->sendEmail($data['customer_email'], $data['customer_name'], $subject, $content)) {
$success = false;
}
}
// 2. SMS 템플릿 조회 및 발송
$sms_sql = "SELECT * FROM consultant_sms_templates WHERE template_key = '" . sql_real_escape_string($template_key) . "' AND is_active = 1";
$sms_template = sql_fetch($sms_sql);
if ($sms_template) {
$content = $this->replaceVariables($sms_template['template_content'], $data);
if (!$this->sendSms($data['customer_phone'], $content)) {
$success = false;
}
}
return $success;
}
/**
* 변수를 실제 값으로 치환합니다.
*/
private function replaceVariables($text, $data)
{
foreach ($data as $key => $value) {
$text = str_replace('{' . $key . '}', $value, $text);
}
return $text;
}
/**
* 💡 [연동] mail_manage 시스템을 사용하여 메일을 발송합니다.
*/
private function sendEmail($to_email, $to_name, $subject, $content)
{
// mail_manage의 라이브러리 포함
if (file_exists(G5_ADMIN_PATH . '/mail_manage/mailer.lib.php')) {
include_once(G5_ADMIN_PATH . '/mail_manage/mailer.lib.php');
// mailer() 함수 호출 (mail_manage의 함수명에 따라 수정 필요)
// mailer($from_name, $from_email, $to_email, $subject, $content, 1);
consultant_log("[Email Sent] To: {$to_email}, Subject: {$subject}");
return true;
}
consultant_log("[Email Error] mail_manage/mailer.lib.php not found.", 'error');
return false;
}
/**
* 💡 [연동] sms_admin 시스템을 사용하여 문자를 발송합니다.
*/
private function sendSms($to_phone, $content)
{
// sms_admin의 라이브러리 포함
if (file_exists(G5_ADMIN_PATH . '/sms_admin/sms.lib.php')) {
include_once(G5_ADMIN_PATH . '/sms_admin/sms.lib.php');
// sms_send() 함수 호출 (sms_admin의 함수명에 따라 수정 필요)
// $result = sms_send($to_phone, $content);
consultant_log("[SMS Sent] To: {$to_phone}, Content: {$content}");
return true;
}
consultant_log("[SMS Error] sms_admin/sms.lib.php not found.", 'error');
return false;
}
}
@@ -0,0 +1,206 @@
<?php
if (!defined('_GNUBOARD_')) exit;
/**
* SQL 파일을 기반으로 데이터베이스 스키마를 관리(생성/업데이트)하는 범용 클래스
*/
class SchemaManager
{
private $sql_file_path;
private $results;
private $conn; // DB 연결 객체를 저장할 변수
/**
* 생성자
* @param string $sql_file_path install.sql 파일의 절대 경로
*/
public function __construct($sql_file_path)
{
global $g5; // 그누보드 DB 연결 객체에 접근하기 위해 global 선언
$this->conn = $g5['connect_db']; // DB 연결 객체를 저장
if (!file_exists($sql_file_path)) {
throw new Exception($sql_file_path . ' 파일을 찾을 수 없습니다.');
}
$this->sql_file_path = $sql_file_path;
$this->results = [
'created' => [],
'existing' => [],
'updated' => [],
'failed' => [],
'errors' => [],
];
}
/**
* 스키마 설치/업데이트를 실행합니다.
*/
public function execute()
{
$sql_statements = $this->parse_sql_file();
foreach ($sql_statements as $stmt) {
// CREATE TABLE 문인지 확인
if (preg_match('/^CREATE\s+TABLE/i', $stmt)) {
$schema = $this->parse_create_table_sql($stmt);
if ($schema && !empty($schema['name'])) {
$this->process_table_schema($stmt, $schema);
}
} else {
// CREATE TABLE 문이 아닌 다른 SQL 문 (e.g. INSERT, UPDATE)
mysqli_query($this->conn, $stmt);
}
}
}
/**
* 처리 결과를 반환합니다.
* @return array
*/
public function get_results()
{
return $this->results;
}
/**
* 테이블 스키마를 처리합니다. (생성 또는 업데이트)
* @param string $create_sql 전체 CREATE TABLE 구문
* @param array $schema 파싱된 스키마 정보
*/
private function process_table_schema($create_sql, $schema)
{
$table_name = $schema['name'];
if ($this->table_exists($table_name)) {
// 테이블이 존재하면, 컬럼 비교 및 추가/수정
$this->results['existing'][] = $table_name;
$this->update_table_columns($table_name, $schema['columns']);
} else {
// 테이블이 존재하지 않으면, 새로 생성
if (mysqli_query($this->conn, $create_sql)) {
$this->results['created'][] = $table_name;
} else {
$this->results['failed'][] = $table_name;
$this->results['errors'][] = "<strong>{$table_name} 테이블 생성 실패</strong>: " . mysqli_error($this->conn);
}
}
}
/**
* 테이블의 컬럼 구조를 업데이트합니다.
* @param string $table_name
* @param array $target_columns .sql 파일에 정의된 컬럼 목록
*/
private function update_table_columns($table_name, $target_columns)
{
$current_columns = $this->get_current_columns($table_name);
$added_columns_in_table = [];
foreach ($target_columns as $col_name => $col_definition) {
// 현재 테이블에 해당 컬럼이 없으면 추가
if (!isset($current_columns[$col_name])) {
$alter_sql = "ALTER TABLE `{$table_name}` ADD COLUMN `{$col_name}` {$col_definition}";
if (mysqli_query($this->conn, $alter_sql)) {
$added_columns_in_table[] = $col_name;
} else {
$this->results['failed'][] = "{$table_name} (컬럼: {$col_name})";
$this->results['errors'][] = "<strong>{$table_name} 테이블에 '{$col_name}' 컬럼 추가 실패</strong>: " . mysqli_error($this->conn);
}
} else {
// 💡 [핵심 수정] 컬럼이 이미 존재하면, 코멘트 등을 업데이트하기 위해 MODIFY 실행
$alter_sql = "ALTER TABLE `{$table_name}` MODIFY COLUMN `{$col_name}` {$col_definition}";
if (!mysqli_query($this->conn, $alter_sql)) {
// MODIFY 실패 시 에러 기록
$this->results['failed'][] = "{$table_name} (컬럼: {$col_name})";
$this->results['errors'][] = "<strong>{$table_name} 테이블의 '{$col_name}' 컬럼 수정 실패</strong>: " . mysqli_error($this->conn);
}
}
}
if (!empty($added_columns_in_table)) {
$this->results['updated'][$table_name] = $added_columns_in_table;
}
}
/**
* SQL 파일을 읽고 각 구문으로 분리합니다.
* @return array
*/
private function parse_sql_file()
{
$sql = file_get_contents($this->sql_file_path);
// 주석 제거 (SQL 주석 '--' 와 C-style '/* ... */' 주석)
$sql = preg_replace('/--.*/', '', $sql);
$sql = preg_replace('!/\*.*?\*/!s', '', $sql);
$sql = trim($sql);
// 세미콜론(;)을 기준으로 쿼리 분리
return array_filter(array_map('trim', explode(';', $sql)));
}
/**
* 테이블 존재 여부를 확인합니다.
* @param string $table_name
* @return bool
*/
private function table_exists($table_name)
{
$res = mysqli_query($this->conn, "SHOW TABLES LIKE '{$table_name}'");
return mysqli_num_rows($res) > 0;
}
/**
* 현재 DB에 있는 테이블의 컬럼 목록을 가져옵니다.
* @param string $table_name
* @return array
*/
private function get_current_columns($table_name)
{
$res = mysqli_query($this->conn, "SHOW COLUMNS FROM `{$table_name}`");
$columns = [];
while ($row = mysqli_fetch_array($res)) {
$columns[$row['Field']] = true;
}
return $columns;
}
/**
* CREATE TABLE SQL 구문에서 테이블명과 컬럼 정의를 파싱합니다.
* @param string $query CREATE TABLE 구문
* @return array|null
*/
private function parse_create_table_sql($query)
{
$table_name = '';
if (preg_match('/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+`?(\w+)`?/i', $query, $matches)) {
$table_name = $matches[1];
} else {
return null;
}
// 괄호 안의 내용만 추출
$start = strpos($query, '(');
$end = strrpos($query, ')');
if ($start === false || $end === false) {
return ['name' => $table_name, 'columns' => []];
}
$content = substr($query, $start + 1, $end - $start - 1);
// 줄 단위로 분리
$lines = explode("\n", $content);
$columns = [];
foreach ($lines as $line) {
$line = trim($line, " \t\n\r\0\x0B,"); // 양쪽 공백과 마지막 쉼표 제거
// 컬럼 정의 라인인지 확인 (첫 단어가 `column_name` 형태)
if (preg_match('/^`(\w+)`\s+(.*)/i', $line, $match)) {
$col_name = $match[1];
$col_definition = $match[2];
$columns[$col_name] = $col_definition;
}
}
return ['name' => $table_name, 'columns' => $columns];
}
}
+529
View File
@@ -0,0 +1,529 @@
<?php
/**
* 상담 예약 시스템 로그 조회 페이지
*/
$sub_menu = '850630';// 새로운 메뉴 코드
include_once('./_common.php');
// 권한 확인
auth_check_menu($auth, $sub_menu, 'r');
$g5['title'] = '시스템 로그 조회';
// 페이징 설정
$page = (int)($_GET['page'] ?? 1);
$page_rows = 20; // 페이지당 로그 수
$sql_common = " FROM consultant_log ";
$sql_order = " ORDER BY log_time DESC ";
// 전체 로그 수
$row = sql_fetch(" SELECT COUNT(*) AS cnt " . $sql_common);
$total_count = $row['cnt'];
$total_page = ceil($total_count / $page_rows);
$from_record = ($page - 1) * $page_rows;
// 로그 조회
$sql = " SELECT * " . $sql_common . $sql_order . " LIMIT {$from_record}, {$page_rows} ";
$result = sql_query($sql);
include_once(G5_ADMIN_PATH . '/admin.head.php');
?>
<style>
.log-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.log-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
font-size: 14px;
}
.log-table th, .log-table td {
border: 1px solid #ddd;
padding: 10px 12px;
text-align: left;
}
.log-table th {
background-color: #fff;
font-weight: 600;
text-align: center;
}
.log-table td.log-message {
word-break: break-all;
}
.log-level {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
color: white;
font-size: 12px;
font-weight: bold;
}
.log-level.info { background-color: #17a2b8; }
.log-level.warning { background-color: #ffc107; color: #212529; }
.log-level.error { background-color: #dc3545; }
.log-table tr:hover {
background-color: #f1f1f1;
}
.text-center { text-align: center !important; }
</style>
<div class="log-container">
<div class="local_ov01 local_ov">
<span class="btn_ov01"><span class="ov_txt">전체 로그</span><span class="ov_num"> <?php echo number_format($total_count) ?>건</span></span>
</div>
<div class="tbl_head01 tbl_wrap">
<table class="log-table">
<caption><?php echo $g5['title']; ?></caption>
<colgroup>
<col style="width: 180px;">
<col style="width: 120px;">
<col style="width: 100px;">
<col>
<col style="width: 150px;">
</colgroup>
<thead>
<tr>
<th scope="col">기록 시간</th>
<th scope="col">관리자 ID</th>
<th scope="col">로그 종류</th>
<th scope="col">내용</th>
<th scope="col">IP 주소</th>
</tr>
</thead>
<tbody>
<?php
for ($i = 0; $row = sql_fetch_array($result); $i++) {
$log_level_class = strtolower($row['log_level']);
?>
<tr>
<td class="text-center"><?php echo $row['log_time']; ?></td>
<td class="text-center"><?php echo htmlspecialchars($row['mb_id'] ?: '비회원/시스템'); ?></td>
<td class="text-center">
<span class="log-level <?php echo $log_level_class; ?>">
<?php echo ucfirst($log_level_class); ?>
</span>
</td>
<td class="log-message"><?php echo htmlspecialchars($row['log_message']); ?></td>
<td class="text-center"><?php echo $row['ip_address']; ?></td>
</tr>
<?php
}
if ($i == 0) {
echo '<tr><td colspan="5" class="empty_table">데이터가 없습니다.</td></tr>';
}
?>
</tbody>
</table>
</div>
<?php echo get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, $_SERVER['PHP_SELF'].'?'.$qstr.'&amp;page='); ?>
</div>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
?>
-
+500
View File
@@ -0,0 +1,500 @@
<?php
/**
* 예약 관리
*/
$sub_menu = '850200';
include_once('./_common.php');
// 관리자 권한 확인
if (!$is_admin) {
alert('관리자만 접근할 수 있습니다.');
}
// 설치 확인
if (!is_consultant_installed()) {
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
}
$g5['title'] = '예약 관리';
// 필터 파라미터
$status = $_GET['status'] ?? '';
$date = $_GET['date'] ?? '';
$search = $_GET['search'] ?? '';
$page = (int)($_GET['page'] ?? 1);
$per_page = 20;
// 상태 변경 처리
if ($_POST['action'] == 'update_status') {
$reservation_id = (int)$_POST['reservation_id'];
$new_status = $_POST['new_status'];
$memo = $_POST['memo'] ?? '';
$send_sms = isset($_POST['send_sms']) && $_POST['send_sms'] == '1';
$send_email = isset($_POST['send_email']) && $_POST['send_email'] == '1';
if ($reservation_id && $new_status) {
// 기존 상태 조회
$old_res = sql_fetch("SELECT * FROM consultant_reservations WHERE id = {$reservation_id}");
$sql = "UPDATE consultant_reservations
SET status = '" . sql_real_escape_string($new_status) . "',
admin_memo = '" . sql_real_escape_string($memo) . "',
updated_at = NOW()
WHERE id = {$reservation_id}";
if (sql_query($sql)) {
// 💡 [추가] 알림 발송 로직
if ($send_sms || $send_email) {
// 템플릿 키 결정
$template_key = '';
if ($new_status == 'reserved') {
$template_key = 'consultant_confirmed_customer';
} elseif ($new_status == 'cancelled') {
$template_key = 'consultant_cancelled_customer';
}
if ($template_key) {
// 알림 데이터 준비
$noti_data = [
'customer_name' => $old_res['customer_name'],
'customer_phone' => $old_res['customer_phone'],
'customer_email' => $old_res['customer_email'],
'reservation_date' => $old_res['reservation_date'],
'reservation_time' => substr($old_res['reservation_time'], 0, 5),
'payment_amount' => number_format($old_res['payment_amount']),
'cancel_reason' => $memo // 취소 사유로 메모 사용
];
if ($send_sms) {
consultant_send_notification('sms', $template_key, $noti_data);
}
if ($send_email) {
consultant_send_notification('email', $template_key, $noti_data);
}
}
}
alert('상태가 변경되었습니다.', $_SERVER['PHP_SELF'] . '?' . $_SERVER['QUERY_STRING']);
} else {
alert('상태 변경에 실패했습니다.');
}
}
}
// 검색 조건 구성
$where_conditions = ["is_deleted = 0"];
if ($status) {
$where_conditions[] = "status = '" . sql_real_escape_string($status) . "'";
}
if ($date) {
$where_conditions[] = "reservation_date = '" . sql_real_escape_string($date) . "'";
}
if ($search) {
$search_escaped = sql_real_escape_string($search);
$where_conditions[] = "(customer_name LIKE '%{$search_escaped}%' OR customer_phone LIKE '%{$search_escaped}%')";
}
$where_clause = implode(' AND ', $where_conditions);
// 전체 개수 조회
$count_sql = "SELECT COUNT(*) as total FROM consultant_reservations WHERE {$where_clause}";
$count_result = sql_fetch($count_sql);
$total = $count_result['total'];
// 페이징 계산
$offset = ($page - 1) * $per_page;
$total_pages = ceil($total / $per_page);
// 예약 목록 조회
$sql = "SELECT * FROM consultant_reservations
WHERE {$where_clause}
ORDER BY created_at DESC
LIMIT {$offset}, {$per_page}";
$reservations = [];
$result = sql_query($sql);
while ($row = sql_fetch_array($result)) {
$reservations[] = $row;
}
// 상태 라벨
$status_labels = [
'payment_pending' => '입금대기',
'reserved' => '예약확정',
'completed' => '상담완료',
'cancelled' => '예약취소'
];
include_once(G5_ADMIN_PATH . '/admin.head.php');
?>
<style>
.reservations-container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.filter-form {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 15px;
flex-wrap: wrap;
}
.reservations-table {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
.table-header {
background: #fff;
padding: 15px 20px;
font-weight: bold;
border-bottom: 1px solid #ddd;
display: flex;
justify-content: space-between;
align-items: center;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background: #fff;
font-weight: bold;
}
.status-badge {
padding: 4px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: bold;
text-align: center;
}
.status-payment_pending {
background: #fff3cd;
color: #856404;
}
.status-reserved {
background: #d4edda;
color: #155724;
}
.status-completed {
background: #cce5ff;
color: #004085;
}
.status-cancelled {
background: #f8d7da;
color: #721c24;
}
.btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
display: inline-block;
font-size: 12px;
font-weight: 600;
margin: 2px;
}
.btn-primary { background: #007bff; color: white; }
.btn-success { background: #28a745; color: white; }
.btn-warning { background: #ffc107; color: #212529; }
.btn-danger { background: #dc3545; color: white; }
.btn-secondary { background: #6c757d; color: white; }
.btn-info { background: #17a2b8; color: white; } /* 견적작성 버튼용 */
.form-control {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
margin-top: 20px;
}
.pagination a,
.pagination span {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
text-decoration: none;
color: #333;
}
.pagination .current {
background: #007bff;
color: white;
border-color: #007bff;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: white;
margin: 15% auto;
padding: 20px;
border-radius: 8px;
width: 400px;
max-width: 90%;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover {
color: black;
}
.no-data {
text-align: center;
padding: 40px;
color: #666;
font-style: italic;
}
@media (max-width: 768px) {
.filter-form {
flex-direction: column;
align-items: stretch;
}
table {
font-size: 12px;
}
th, td {
padding: 8px 4px;
}
}
</style>
<div class="reservations-container">
<h2><?php echo $g5['title']; ?></h2>
<!-- 필터 폼 -->
<form method="get" class="filter-form">
<select name="status" class="form-control">
<option value="">전체 상태</option>
<?php foreach ($status_labels as $key => $label): ?>
<option value="<?php echo $key; ?>" <?php echo $status == $key ? 'selected' : ''; ?>>
<?php echo $label; ?>
</option>
<?php endforeach; ?>
</select>
<input type="date" name="date" value="<?php echo $date; ?>" class="form-control" placeholder="예약일">
<input type="text" name="search" value="<?php echo htmlspecialchars($search); ?>"
class="form-control" placeholder="고객명 또는 연락처 검색">
<button type="submit" class="btn btn-primary">검색</button>
<a href="<?php echo $_SERVER['PHP_SELF']; ?>" class="btn btn-secondary">초기화</a>
<a href="dashboard.php" class="btn btn-secondary">대시보드로</a>
</form>
<!-- 예약 목록 -->
<div class="reservations-table">
<div class="table-header">
<span>예약 목록 (총 <?php echo number_format($total); ?>건)</span>
<span>페이지 <?php echo $page; ?> / <?php echo $total_pages; ?></span>
</div>
<?php if (!empty($reservations)): ?>
<table>
<thead>
<tr>
<th>예약번호</th>
<th>고객정보</th>
<th>예약일시</th>
<th>상담유형</th>
<th>상담비</th>
<th>상태</th>
<th>신청일</th>
<th>관리</th>
</tr>
</thead>
<tbody>
<?php foreach ($reservations as $reservation): ?>
<tr>
<td>#<?php echo $reservation['id']; ?></td>
<td>
<strong><?php echo htmlspecialchars($reservation['customer_name']); ?></strong><br>
<small><?php echo htmlspecialchars($reservation['customer_phone']); ?></small><br>
<small><?php echo htmlspecialchars($reservation['customer_email']); ?></small>
</td>
<td>
<?php echo $reservation['reservation_date']; ?><br>
<small><?php echo substr($reservation['reservation_time'], 0, 5); ?></small>
</td>
<td>
<?php
$types = ['onsite' => '현장', 'online' => '온라인', 'phone' => '전화'];
echo $types[$reservation['consultation_type']] ?? $reservation['consultation_type'];
?>
</td>
<td><?php echo number_format($reservation['payment_amount']); ?>원</td>
<td>
<span class="status-badge status-<?php echo $reservation['status']; ?>">
<?php echo $status_labels[$reservation['status']] ?? $reservation['status']; ?>
</span>
</td>
<td><?php echo date('m-d H:i', strtotime($reservation['created_at'])); ?></td>
<td>
<button onclick="openStatusModal(<?php echo $reservation['id']; ?>, '<?php echo $reservation['status']; ?>')" class="btn btn-warning btn-sm">상태변경</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<div class="no-data">
검색 조건에 맞는 예약이 없습니다.
</div>
<?php endif; ?>
</div>
<!-- 페이징 -->
<?php if ($total_pages > 1): ?>
<div class="pagination">
<?php if ($page > 1): ?>
<a href="?<?php echo http_build_query(array_merge($_GET, ['page' => $page - 1])); ?>">이전</a>
<?php endif; ?>
<?php
$start_page = max(1, $page - 5);
$end_page = min($total_pages, $page + 5);
for ($i = $start_page; $i <= $end_page; $i++):
?>
<?php if ($i == $page): ?>
<span class="current"><?php echo $i; ?></span>
<?php else: ?>
<a href="?<?php echo http_build_query(array_merge($_GET, ['page' => $i])); ?>"><?php echo $i; ?></a>
<?php endif; ?>
<?php endfor; ?>
<?php if ($page < $total_pages): ?>
<a href="?<?php echo http_build_query(array_merge($_GET, ['page' => $page + 1])); ?>">다음</a>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<!-- 상태 변경 모달 -->
<div id="statusModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h3>예약 상태 변경</h3>
<form method="post">
<input type="hidden" name="action" value="update_status">
<input type="hidden" name="reservation_id" id="modal_reservation_id">
<div style="margin: 15px 0;">
<label for="modal_new_status">변경할 상태</label>
<select name="new_status" id="modal_new_status" class="form-control" required>
<?php foreach ($status_labels as $key => $label): ?>
<option value="<?php echo $key; ?>"><?php echo $label; ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="margin: 15px 0;">
<label for="modal_memo">관리자 메모 (선택)</label>
<textarea name="memo" rows="3" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;"></textarea>
</div>
<!-- 💡 [추가] 알림 발송 옵션 -->
<div style="margin: 15px 0; background: #f8f9fa; padding: 10px; border-radius: 4px;">
<div style="font-weight: bold; margin-bottom: 5px;">알림 발송</div>
<label style="margin-right: 15px;">
<input type="checkbox" name="send_sms" value="1" checked> 문자(SMS) 발송
</label>
<label>
<input type="checkbox" name="send_email" value="1" checked> 이메일 발송
</label>
<div style="font-size: 12px; color: #666; margin-top: 5px;">
* 예약확정/취소 상태 변경 시에만 발송됩니다.
</div>
</div>
<div style="text-align: center; margin-top: 20px;">
<button type="submit" class="btn btn-primary">확인</button>
<button type="button" onclick="closeModal()" class="btn btn-secondary">취소</button>
</div>
</form>
</div>
</div>
<script>
function openStatusModal(reservationId, currentStatus) {
document.getElementById('modal_reservation_id').value = reservationId;
document.getElementById('modal_new_status').value = currentStatus;
document.getElementById('statusModal').style.display = 'block';
}
function closeModal() {
document.getElementById('statusModal').style.display = 'none';
}
// 모달 외부 클릭시 닫기
window.onclick = function(event) {
const modal = document.getElementById('statusModal');
if (event.target == modal) {
modal.style.display = 'none';
}
}
// 닫기 버튼
document.querySelector('.close').onclick = function() {
closeModal();
}
</script>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
?>
+326
View File
@@ -0,0 +1,326 @@
<?php
/**
* 상담 리소스(상담사/공간) 관리
*/
$sub_menu = '850615';
include_once('./_common.php');
// 관리자 권한 확인
if (!$is_admin) {
alert('관리자만 접근할 수 있습니다.');
}
$g5['title'] = '리소스(상담사) 관리';
// --- 액션 처리 ---
// 그룹 추가/수정
if ($_POST['action'] == 'save_group') {
$group_name = trim($_POST['group_name']);
$group_id = (int)$_POST['group_id'];
if ($group_name) {
if ($group_id > 0) {
$sql = "UPDATE consultant_groups SET name = '{$group_name}' WHERE id = {$group_id}";
} else {
$sql = "INSERT INTO consultant_groups (name, is_active) VALUES ('{$group_name}', 1)";
}
sql_query($sql);
alert('저장되었습니다.', $_SERVER['PHP_SELF']);
}
}
// 그룹 삭제
if ($_POST['action'] == 'delete_group') {
$group_id = (int)$_POST['group_id'];
// 리소스가 있는지 확인
$cnt = sql_fetch("SELECT COUNT(*) as cnt FROM consultant_resources WHERE group_id = {$group_id}")['cnt'];
if ($cnt > 0) {
alert('해당 그룹에 속한 리소스가 있어 삭제할 수 없습니다.');
} else {
sql_query("DELETE FROM consultant_groups WHERE id = {$group_id}");
alert('삭제되었습니다.', $_SERVER['PHP_SELF']);
}
}
// 리소스 추가/수정
if ($_POST['action'] == 'save_resource') {
$resource_id = (int)$_POST['resource_id'];
$group_id = (int)$_POST['group_id'];
$name = trim($_POST['resource_name']);
$desc = trim($_POST['resource_desc']);
$is_active = isset($_POST['is_active']) ? 1 : 0;
if ($name && $group_id) {
if ($resource_id > 0) {
$sql = "UPDATE consultant_resources
SET group_id = {$group_id}, name = '{$name}', description = '{$desc}', is_active = {$is_active}
WHERE id = {$resource_id}";
} else {
$sql = "INSERT INTO consultant_resources
(group_id, name, description, is_active)
VALUES ({$group_id}, '{$name}', '{$desc}', {$is_active})";
}
sql_query($sql);
alert('저장되었습니다.', $_SERVER['PHP_SELF']);
}
}
// 리소스 삭제
if ($_POST['action'] == 'delete_resource') {
$resource_id = (int)$_POST['resource_id'];
// 예약 내역 확인 (안전 삭제)
$cnt = sql_fetch("SELECT COUNT(*) as cnt FROM consultant_reservations WHERE resource_id = {$resource_id}")['cnt'];
if ($cnt > 0) {
// 실제 삭제 대신 비활성화 처리 권장하지만, 여기서는 경고 후 삭제 방지
alert('해당 리소스로 접수된 예약이 있어 삭제할 수 없습니다. 대신 사용 안 함으로 설정해주세요.');
} else {
sql_query("DELETE FROM consultant_resources WHERE id = {$resource_id}");
alert('삭제되었습니다.', $_SERVER['PHP_SELF']);
}
}
// --- 데이터 조회 ---
$groups = [];
$result = sql_query("SELECT * FROM consultant_groups ORDER BY id");
while ($row = sql_fetch_array($result)) {
$groups[$row['id']] = $row;
}
$resources = [];
$result = sql_query("SELECT r.*, g.name as group_name
FROM consultant_resources r
LEFT JOIN consultant_groups g ON r.group_id = g.id
ORDER BY r.group_id, r.id");
while ($row = sql_fetch_array($result)) {
$resources[] = $row;
}
include_once(G5_ADMIN_PATH . '/admin.head.php');
?>
<style>
.resource-container { max-width: 1200px; margin: 0 auto; padding: 20px; display: grid; grid-template-columns: 300px 1fr; gap: 20px; }
.card { background: #fff; border: 1px solid #ddd; border-radius: 8px; overflow: hidden; }
.card-header { padding: 15px; background: #f8f9fa; border-bottom: 1px solid #ddd; font-weight: bold; display: flex; justify-content: space-between; align-items: center; }
.card-body { padding: 20px; }
.group-list { list-style: none; padding: 0; margin: 0; }
.group-item { padding: 10px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
.group-item:last-child { border-bottom: none; }
.group-item:hover { background: #f1f3f5; }
.resource-table { width: 100%; border-collapse: collapse; }
.resource-table th, .resource-table td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
.resource-table th { background: #f8f9fa; font-weight: bold; }
.btn-xs { padding: 2px 6px; font-size: 11px; border-radius: 3px; }
.badge { padding: 3px 8px; border-radius: 10px; font-size: 11px; color: #fff; }
.badge-success { background: #28a745; }
.badge-secondary { background: #6c757d; }
.form-control { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
@media (max-width: 768px) {
.resource-container { grid-template-columns: 1fr; }
}
</style>
<div class="resource-container">
<!-- 좌측: 그룹 관리 -->
<div class="card">
<div class="card-header">
<span>그룹 관리</span>
<button type="button" class="btn btn-primary btn-sm" onclick="openGroupModal()">+ 추가</button>
</div>
<div class="card-body" style="padding:0;">
<ul class="group-list">
<?php if (empty($groups)): ?>
<li class="group-item" style="justify-content:center; color:#999;">등록된 그룹이 없습니다.</li>
<?php else: ?>
<?php foreach ($groups as $group): ?>
<li class="group-item">
<span><?php echo htmlspecialchars($group['name']); ?></span>
<div>
<button type="button" class="btn btn-secondary btn-xs" onclick="openGroupModal(<?php echo $group['id']; ?>, '<?php echo htmlspecialchars($group['name']); ?>')">수정</button>
<button type="button" class="btn btn-danger btn-xs" onclick="deleteGroup(<?php echo $group['id']; ?>)">삭제</button>
</div>
</li>
<?php endforeach; ?>
<?php endif; ?>
</ul>
</div>
</div>
<!-- 우측: 리소스 목록 -->
<div class="card">
<div class="card-header">
<span>리소스(상담사/공간) 목록</span>
<button type="button" class="btn btn-primary btn-sm" onclick="openResourceModal()">+ 리소스 추가</button>
</div>
<div class="card-body" style="padding:0;">
<table class="resource-table">
<thead>
<tr>
<th>그룹</th>
<th>이름</th>
<th>설명</th>
<th>상태</th>
<th>관리</th>
</tr>
</thead>
<tbody>
<?php if (empty($resources)): ?>
<tr><td colspan="5" style="text-align:center; padding:30px; color:#999;">등록된 리소스가 없습니다.</td></tr>
<?php else: ?>
<?php foreach ($resources as $res): ?>
<tr>
<td><?php echo htmlspecialchars($res['group_name']); ?></td>
<td><strong><?php echo htmlspecialchars($res['name']); ?></strong></td>
<td><?php echo htmlspecialchars($res['description']); ?></td>
<td>
<?php if ($res['is_active']): ?>
<span class="badge badge-success">사용중</span>
<?php else: ?>
<span class="badge badge-secondary">중지</span>
<?php endif; ?>
</td>
<td>
<button type="button" class="btn btn-secondary btn-xs"
onclick="openResourceModal(<?php echo $res['id']; ?>, <?php echo $res['group_id']; ?>, '<?php echo htmlspecialchars($res['name']); ?>', '<?php echo htmlspecialchars($res['description']); ?>', <?php echo $res['is_active']; ?>)">
수정
</button>
<button type="button" class="btn btn-danger btn-xs" onclick="deleteResource(<?php echo $res['id']; ?>)">삭제</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- 그룹 추가/수정 모달 -->
<div id="groupModal" class="modal" style="display:none;">
<div class="modal-content" style="width:350px;">
<div class="modal-header">
<h3 id="groupModalTitle">그룹 추가</h3>
<span class="close" onclick="closeModal('groupModal')">&times;</span>
</div>
<div class="modal-body">
<form method="post">
<input type="hidden" name="action" value="save_group">
<input type="hidden" name="group_id" id="modal_group_id" value="0">
<div class="form-group">
<label>그룹명 (예: 내과, 회의실)</label>
<input type="text" name="group_name" id="modal_group_name" class="form-control" required>
</div>
<div style="text-align:right; margin-top:15px;">
<button type="submit" class="btn btn-primary">저장</button>
</div>
</form>
</div>
</div>
</div>
<!-- 리소스 추가/수정 모달 -->
<div id="resourceModal" class="modal" style="display:none;">
<div class="modal-content">
<div class="modal-header">
<h3 id="resourceModalTitle">리소스 추가</h3>
<span class="close" onclick="closeModal('resourceModal')">&times;</span>
</div>
<div class="modal-body">
<form method="post">
<input type="hidden" name="action" value="save_resource">
<input type="hidden" name="resource_id" id="modal_res_id" value="0">
<div class="form-group">
<label>그룹 선택</label>
<select name="group_id" id="modal_res_group" class="form-control" required>
<option value="">선택하세요</option>
<?php foreach ($groups as $group): ?>
<option value="<?php echo $group['id']; ?>"><?php echo htmlspecialchars($group['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>이름 (예: 김의사, A회의실)</label>
<input type="text" name="resource_name" id="modal_res_name" class="form-control" required>
</div>
<div class="form-group">
<label>설명 (선택)</label>
<input type="text" name="resource_desc" id="modal_res_desc" class="form-control">
</div>
<div class="form-group">
<label><input type="checkbox" name="is_active" id="modal_res_active" value="1" checked> 사용함</label>
</div>
<div style="text-align:right; margin-top:15px;">
<button type="submit" class="btn btn-primary">저장</button>
</div>
</form>
</div>
</div>
</div>
<!-- 삭제 폼 (히든) -->
<form id="deleteForm" method="post">
<input type="hidden" name="action" id="deleteAction">
<input type="hidden" name="group_id" id="deleteGroupId">
<input type="hidden" name="resource_id" id="deleteResourceId">
</form>
<script>
function openGroupModal(id = 0, name = '') {
document.getElementById('groupModalTitle').textContent = id ? '그룹 수정' : '그룹 추가';
document.getElementById('modal_group_id').value = id;
document.getElementById('modal_group_name').value = name;
document.getElementById('groupModal').style.display = 'block';
}
function openResourceModal(id = 0, groupId = '', name = '', desc = '', active = 1) {
document.getElementById('resourceModalTitle').textContent = id ? '리소스 수정' : '리소스 추가';
document.getElementById('modal_res_id').value = id;
document.getElementById('modal_res_group').value = groupId;
document.getElementById('modal_res_name').value = name;
document.getElementById('modal_res_desc').value = desc;
document.getElementById('modal_res_active').checked = (active == 1);
document.getElementById('resourceModal').style.display = 'block';
}
function closeModal(id) {
document.getElementById(id).style.display = 'none';
}
function deleteGroup(id) {
if(confirm('정말 삭제하시겠습니까?')) {
document.getElementById('deleteAction').value = 'delete_group';
document.getElementById('deleteGroupId').value = id;
document.getElementById('deleteForm').submit();
}
}
function deleteResource(id) {
if(confirm('정말 삭제하시겠습니까?')) {
document.getElementById('deleteAction').value = 'delete_resource';
document.getElementById('deleteResourceId').value = id;
document.getElementById('deleteForm').submit();
}
}
// 모달 외부 클릭 닫기
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
}
}
</script>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
?>
+85
View File
@@ -0,0 +1,85 @@
<?php
$sub_menu = '850500'; // 💡 메뉴 코드 수정
include_once('./_common.php');
$g5['title'] = '상담 예약 팝업 샘플';
include_once(G5_ADMIN_PATH.'/admin.head.php');
?>
<div class="local_desc01 local_desc">
<p>
이 페이지는 사이트의 어떤 페이지에서든 상담 예약 기능을 쉽게 추가하는 방법을 보여주는 예제입니다.<br>
아래 코드 한 줄만 포함하면, 버튼과 팝업 기능이 모두 활성화됩니다.
</p>
<pre><code>&lt;?php include_once(G5_ADMIN_PATH . '/consultant_manage/components/_consultant_popups.php'); ?&gt;</code></pre>
</div>
<!-- ================================================================== -->
<!-- 💡 [시작] 상담 예약 기능 추가 예제 -->
<!-- ================================================================== -->
<div style="text-align:center; padding: 50px 20px; background-color:#f5f5f5; border-radius:10px; margin: 20px 0;">
<h3 style="margin-bottom:15px;">상담이 필요하신가요?</h3>
<p style="margin-bottom:25px; color:#666;">버튼을 눌러 간편하게 상담을 신청하거나, 기존 예약을 확인/취소할 수 있습니다.</p>
<!-- 1. 팝업을 여는 버튼들 -->
<div class="consultant-buttons">
<button type="button" class="reservation-btn-main" onclick="openReservationPopup()">
<i class="fa fa-calendar"></i> 상담 예약 신청
</button>
<button type="button" class="reservation-check-btn" onclick="openReservationCheckPopup()">
예약 확인/취소
</button>
</div>
</div>
<?php
// 2. 팝업 파일들 포함 (이 코드 한 줄이면 모든 팝업 기능이 로드됩니다)
include_once(G5_ADMIN_PATH . '/consultant_manage/components/_consultant_popups.php');
?>
<!-- 버튼 디자인을 위한 CSS (사이트의 공통 CSS 파일에 추가하는 것을 권장합니다) -->
<style>
.consultant-buttons {
display: flex;
justify-content: center;
gap: 15px;
flex-wrap: wrap;
}
.reservation-btn-main, .reservation-check-btn {
color: white;
border: none;
padding: 15px 30px;
border-radius: 25px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
.reservation-btn-main {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
box-shadow: 0 4px 15px rgba(40, 167, 69, 0.3);
}
.reservation-btn-main:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(40, 167, 69, 0.4);
}
.reservation-btn-main i {
margin-right: 8px;
}
.reservation-check-btn {
background: #6c757d;
}
.reservation-check-btn:hover {
background: #5a6268;
}
</style>
<!-- ================================================================== -->
<!-- 💡 [끝] 상담 예약 기능 추가 예제 -->
<!-- ================================================================== -->
<?php
include_once(G5_ADMIN_PATH.'/admin.tail.php');
?>
+293
View File
@@ -0,0 +1,293 @@
<?php
/**
* 상담 일정 설정
*/
$sub_menu = '850300';
include_once('./_common.php');
// 관리자 권한 확인
if (!$is_admin) {
alert('관리자만 접근할 수 있습니다.');
}
// 설치 확인
if (!is_consultant_installed()) {
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
}
$g5['title'] = '상담 일정 설정';
// 월별 스케줄 생성 처리
if ($_POST['action'] == 'generate_monthly_schedule') {
try {
require_once('classes/ScheduleGenerator.class.php');
$generator = new ScheduleGenerator();
$year = (int)($_POST['year'] ?? date('Y'));
$month = (int)($_POST['month'] ?? date('n'));
// 충돌 검사
$conflicts = $generator->checkScheduleConflicts($year, $month);
if (!empty($conflicts)) {
$conflictMsg = "다음 예약과 충돌이 발생합니다:\\n";
foreach ($conflicts as $conflict) {
$conflictMsg .= "- {$conflict['date']} {$conflict['time']} {$conflict['customer']} ({$conflict['phone']})\\n";
}
$conflictMsg .= "\\n계속 진행하시겠습니까?";
if (!isset($_POST['force_generate'])) {
echo "<script>
if (confirm('{$conflictMsg}')) {
var form = document.createElement('form');
form.method = 'POST';
form.innerHTML = '<input type=\"hidden\" name=\"action\" value=\"generate_monthly_schedule\">' +
'<input type=\"hidden\" name=\"year\" value=\"{$year}\">' +
'<input type=\"hidden\" name=\"month\" value=\"{$month}\">' +
'<input type=\"hidden\" name=\"force_generate\" value=\"1\">';
document.body.appendChild(form);
form.submit();
}
</script>";
exit;
}
}
// 스케줄 생성
if ($generator->generateMonth($year, $month)) {
alert("{$year}{$month}월 스케줄이 생성되었습니다.", $_SERVER['PHP_SELF']);
} else {
alert("스케줄 생성에 실패했습니다.");
}
} catch (Exception $e) {
alert("오류가 발생했습니다: " . $e->getMessage());
}
}
// 폼 처리 (기존 코드 유지)
if ($_POST['action'] == 'save_schedule') {
// 기존 스케줄 삭제
sql_query("DELETE FROM consultant_schedule WHERE day_of_week IS NOT NULL");
// 새 스케줄 저장
for ($day = 1; $day <= 7; $day++) {
$enabled = $_POST["day_{$day}_enabled"] ?? 0;
$start_time = $_POST["day_{$day}_start"] ?? '09:00';
$end_time = $_POST["day_{$day}_end"] ?? '18:00';
if ($enabled) {
$sql = "INSERT INTO consultant_schedule
(day_of_week, start_time, end_time, time_slot, max_persons, is_available)
VALUES ({$day}, '{$start_time}', '{$end_time}', 60, 2, 1)";
sql_query($sql);
}
}
alert('일정이 저장되었습니다.', $_SERVER['PHP_SELF']);
}
// 현재 스케줄 조회
$schedules = [];
$sql = "SELECT * FROM consultant_schedule WHERE day_of_week IS NOT NULL ORDER BY day_of_week";
$result = sql_query($sql);
while ($row = sql_fetch_array($result)) {
$schedules[$row['day_of_week']] = $row;
}
$days = ['', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일', '일요일'];
include_once(G5_ADMIN_PATH . '/admin.head.php');
?>
<style>
.schedule-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding: 20px;
background: #fff;
border-radius: 8px;
}
.schedule-tabs {
display: flex;
border-bottom: 2px solid #ddd;
margin-bottom: 30px;
}
.tab-button {
padding: 16px 28px;
border: none;
background: none;
cursor: pointer;
font-size: 16px;
font-weight: 600;
color: #666;
text-decoration: none;
border-bottom: 3px solid transparent;
transition: all 0.3s;
}
.tab-button.active {
color: #007bff;
border-bottom-color: #007bff;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.monthly-generator {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 30px;
margin-bottom: 30px;
}
.generator-form {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 20px;
}
.alert {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
}
.alert-info {
color: #0c5460;
background-color: #d1ecf1;
border-color: #bee5eb;
}
.alert-warning {
color: #856404;
background-color: #fff3cd;
border-color: #ffeaa7;
}
.schedule-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.schedule-form {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 30px;
}
.day-schedule {
display: flex;
align-items: center;
padding: 15px 0;
border-bottom: 1px solid #eee;
}
.day-schedule:last-child {
border-bottom: none;
}
.day-name {
width: 100px;
font-weight: bold;
}
.day-controls {
display: flex;
align-items: center;
gap: 15px;
flex: 1;
}
.time-input {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
width: 100px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.alert {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
}
.alert-info {
color: #0c5460;
background-color: #d1ecf1;
border-color: #bee5eb;
}
</style>
<div class="schedule-container">
<h2><?php echo $g5['title']; ?></h2>
<div class="alert alert-info">
<strong>안내:</strong> 상담 가능한 요일과 시간을 설정하세요. 체크된 요일만 예약이 가능합니다.
</div>
<form method="post" class="schedule-form">
<input type="hidden" name="action" value="save_schedule">
<?php for ($day = 1; $day <= 7; $day++): ?>
<?php
$schedule = $schedules[$day] ?? null;
$enabled = $schedule ? 1 : 0;
$start_time = $schedule['start_time'] ?? '09:00';
$end_time = $schedule['end_time'] ?? '18:00';
?>
<div class="day-schedule">
<div class="day-name"><?php echo $days[$day]; ?></div>
<div class="day-controls">
<label>
<input type="checkbox" name="day_<?php echo $day; ?>_enabled" value="1"
<?php echo $enabled ? 'checked' : ''; ?>>
운영
</label>
<label>
시작:
<input type="time" name="day_<?php echo $day; ?>_start"
value="<?php echo $start_time; ?>" class="time-input">
</label>
<label>
종료:
<input type="time" name="day_<?php echo $day; ?>_end"
value="<?php echo $end_time; ?>" class="time-input">
</label>
</div>
</div>
<?php endfor; ?>
<div style="text-align: center; margin-top: 30px;">
<button type="submit" class="btn btn-primary">저장</button>
<a href="dashboard.php" class="btn btn-secondary">대시보드로</a>
</div>
</form>
</div>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
?>
+454
View File
@@ -0,0 +1,454 @@
<?php
/**
* 스케줄 생성 및 빠른 관리 페이지
*/
$sub_menu = '850300';
include_once('./_common.php');
// 권한 확인
auth_check_menu($auth, $sub_menu, 'w');
// ScheduleGenerator 클래스 로드
require_once('classes/ScheduleGenerator.class.php');
// 설치 확인
if (!is_consultant_installed()) {
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
}
$g5['title'] = '스케줄 빠른 관리';
// AJAX 요청 처리
$action = $_REQUEST['action'] ?? '';
if ($action) {
header('Content-Type: application/json');
$response = ['success' => false, 'message' => '알 수 없는 요청입니다.'];
// 월별 스케줄 데이터 조회 (달력용)
if ($action == 'get_monthly_schedule') {
$year = (int) ($_GET['year'] ?? 0);
$month = (int) ($_GET['month'] ?? 0);
if ($year && $month) {
$sql = "SELECT id, specific_date, start_time, is_available, temp_1, temp_2
FROM consultant_schedule
WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month}
ORDER BY specific_date, start_time";
$result = sql_query($sql);
$schedule_data = [];
while ($row = sql_fetch_array($result)) {
$schedule_data[] = $row;
}
$response = ['success' => true, 'data' => $schedule_data];
} else {
$response['message'] = '년도와 월 정보가 올바르지 않습니다.';
}
}
// 스케줄 슬롯 상태 변경 (블락/해제)
if ($action == 'toggle_slot_status') {
$id = (int) ($_POST['id'] ?? 0);
if ($id) {
$slot = sql_fetch("SELECT is_available, temp_1, temp_2 FROM consultant_schedule WHERE id = {$id}");
if ($slot) {
$is_lunch_override = $_POST['is_lunch_override'] ?? '0';
$new_status = $slot['is_available'] ? 0 : 1; // 기본 토글 동작
$new_temp1 = $slot['temp_1'];
$new_temp2 = $slot['temp_2'];
$log_msg_action = '';
// 💡 [로직 개선] 휴게시간을 상담시간으로 변경하는 경우
if ($is_lunch_override === '1' && $slot['temp_1'] === 'lunch_time' && $new_status == 1) {
$new_temp1 = 'manual_override'; // 휴게시간을 수동으로 변경했음을 명시
$new_temp2 = '관리자 긴급 설정';
$log_msg_action = "휴게시간을 상담 슬롯으로 변경";
}
// 💡 [로직 개선] 긴급 설정된 상담시간을 다시 휴게시간으로 되돌리는 경우
else if ($slot['temp_1'] === 'manual_override' && $new_status == 0) {
$new_temp1 = 'lunch_time';
$new_temp2 = '점심시간';
$log_msg_action = "긴급 슬롯을 다시 휴게시간으로 복원";
}
// 일반 슬롯을 블락/해제하는 경우
else {
$new_temp1 = $new_status ? 'auto_generated' : 'manual_block';
$new_temp2 = $new_status ? '' : '관리자 설정';
$log_msg_action = $new_status ? "슬롯 활성화" : "슬롯 비활성화";
}
$sql = "UPDATE consultant_schedule
SET is_available = '{$new_status}',
temp_1 = '{$new_temp1}',
temp_2 = '{$new_temp2}',
updated_at = NOW()
WHERE id = {$id}";
if (sql_query($sql)) {
consultant_log("스케줄 수동 변경 (ID:{$id}): {$log_msg_action} (관리자: " . ($member['mb_id'] ?? 'unknown') . ")");
$response = ['success' => true, 'new_status' => $new_status];
} else {
$response['message'] = '데이터베이스 업데이트에 실패했습니다.';
}
} else {
$response['message'] = '해당 스케줄을 찾을 수 없습니다.';
}
} else {
$response['message'] = 'ID가 제공되지 않았습니다.';
}
}
// 월별 스케줄 생성
if ($action == 'generate_schedule') {
$year = (int) ($_POST['year'] ?? 0);
$month = (int) ($_POST['month'] ?? 0);
if ($year && $month) {
try {
$generator = new ScheduleGenerator();
if ($generator->generateMonth($year, $month)) {
consultant_log("스케줄 생성/재생성 완료: {$year}{$month}");
$response = ['success' => true, 'message' => "{$year}{$month}월 스케줄이 성공적으로 생성되었습니다."];
} else {
$response['message'] = "{$year}{$month}월 스케줄 생성에 실패했습니다.";
}
} catch (Exception $e) {
$response['message'] = '스케줄 생성 중 오류 발생: ' . $e->getMessage();
}
} else {
$response['message'] = '년도와 월 정보가 올바르지 않습니다.';
}
}
echo json_encode($response);
exit;
}
// --- 페이지 로드 시 실행 ---
// 월별 스케줄 상태 조회 함수
function get_schedule_generation_status($year, $month) {
$year = (int)$year;
$month = (int)$month;
$sql = "SELECT
COUNT(*) as total_slots,
SUM(CASE WHEN temp_1 = 'auto_generated' AND is_available = 1 THEN 1 ELSE 0 END) as available_slots,
SUM(CASE WHEN temp_1 = 'lunch_time' THEN 1 ELSE 0 END) as lunch_slots,
SUM(CASE WHEN temp_1 = 'holiday' THEN 1 ELSE 0 END) as holiday_slots,
SUM(CASE WHEN is_available = 0 AND temp_1 NOT IN ('lunch_time', 'holiday') THEN 1 ELSE 0 END) as blocked_slots
FROM consultant_schedule
WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month}";
return sql_fetch($sql);
}
// 다음 3개월 상태 조회
$next_months = [];
for ($i = 0; $i < 3; $i++) {
$target_date = mktime(0, 0, 0, date('n') + $i, 1, date('Y'));
$year = date('Y', $target_date);
$month = date('m', $target_date);
$next_months[] = [
'year' => $year,
'month' => $month,
'name' => date('Y년 n월', $target_date),
'status' => get_schedule_generation_status($year, $month)
];
}
include_once(G5_ADMIN_PATH . '/admin.head.php');
?>
<style>
.schedule-container { max-width: 1000px; margin: 0 auto; padding: 20px; }
.schedule-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; }
.status-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px; }
.status-card { background: white; border: 1px solid #ddd; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); }
.status-card h3 { margin: 0 0 15px 0; color: #333; display: flex; align-items: center; gap: 8px; }
.status-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #f0f0f0; font-size: 14px; }
.status-item:last-child { border-bottom: none; }
.status-label { color: #666; }
.status-value { font-weight: bold; color: #333; }
.btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-weight: 600; text-decoration: none; display: inline-block; transition: all 0.2s; font-size: 14px; }
.btn-primary { background: #007bff; color: white; }
.btn-success { background: #28a745; color: white; }
.btn-warning { background: #ffc107; color: #212529; }
.btn-secondary { background: #6c757d; color: white; }
.btn-info { background: #17a2b8; color: white; }
.alert-info { color: #0c5460; background-color: #d1ecf1; border-color: #bee5eb; padding: 15px; margin-bottom: 20px; border: 1px solid transparent; border-radius: 4px; }
.card-actions { display: flex; gap: 10px; margin-top: 20px; }
.card-actions .btn { flex-grow: 1; }
/* Modal Styles */
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.6); }
.modal-content { background-color: #fefefe; margin: 5% auto; padding: 20px; border: 1px solid #888; width: 90%; max-width: 1200px; border-radius: 8px; }
.modal-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 10px; border-bottom: 1px solid #ddd; }
.modal-header h2 { margin: 0; font-size: 20px; }
.close-button { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; }
.close-button:hover, .close-button:focus { color: black; }
.calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; margin-top: 20px; }
.calendar-day { border: 1px solid #eee; min-height: 150px; }
.calendar-day-header { background: #f9f9f9; padding: 5px; font-weight: bold; text-align: center; font-size: 14px; }
.calendar-day-body { padding: 5px; max-height: 300px; overflow-y: auto; }
.day-name-header { background: #f1f1f1; text-align: center; padding: 8px; font-weight: bold; }
.time-slot { padding: 4px 6px; margin: 3px 0; border-radius: 4px; cursor: pointer; font-size: 12px; display: flex; justify-content: space-between; align-items: center; border: 1px solid transparent; }
.time-slot.available { background-color: #e7f3ff; border-color: #b3d7ff; color: #004085; }
.time-slot.available:hover { background-color: #cce5ff; }
.time-slot.blocked { background-color: #f8d7da; border-color: #f5c6cb; color: #721c24; text-decoration: line-through; }
.time-slot.blocked:hover { background-color: #f5c6cb; }
.time-slot.lunch { background-color: #fff3cd; border-color: #ffeeba; color: #856404; cursor: pointer; }
.time-slot.lunch:hover { background-color: #ffeeba; }
.time-slot.holiday { background-color: #e2e3e5; color: #383d41; text-align: center; justify-content: center; cursor: not-allowed; }
.other-month { background-color: #fafafa; }
.spinner { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: 20px auto; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
<div class="schedule-container">
<div class="schedule-header">
<h2><?php echo $g5['title']; ?></h2>
<div>
<a href="settings.php" class="btn btn-secondary">⚙️ 설정 관리</a>
</div>
</div>
<div class="alert alert-info">
<strong>스케줄 관리 안내:</strong> 월별 스케줄 생성 상태를 확인하고, '달력 보기'를 통해 각 시간 슬롯을 빠르게 예약 마감 처리할 수 있습니다.
</div>
<!-- 월별 상태 표시 -->
<div class="status-grid">
<?php foreach ($next_months as $month_info):
$is_generated = ($month_info['status']['total_slots'] ?? 0) > 0;
?>
<div class="status-card">
<h3>
📅 <?php echo $month_info['name']; ?>
<?php if ($is_generated): ?>
<span style="color: #28a745; font-weight: bold;">✓ 생성됨</span>
<?php else: ?>
<span style="color: #dc3545; font-weight: bold;">✗ 미생성</span>
<?php endif; ?>
</h3>
<div class="status-item">
<span class="status-label">상담 가능 슬롯</span>
<span class="status-value"><?php echo number_format($month_info['status']['available_slots'] ?? 0); ?>개</span>
</div>
<div class="status-item">
<span class="status-label">예약 마감 슬롯</span>
<span class="status-value"><?php echo number_format($month_info['status']['blocked_slots'] ?? 0); ?>개</span>
</div>
<div class="status-item">
<span class="status-label">전체 슬롯 (휴무/점심 제외)</span>
<span class="status-value"><?php echo number_format(($month_info['status']['available_slots'] ?? 0) + ($month_info['status']['blocked_slots'] ?? 0)); ?>개</span>
</div>
<div class="card-actions">
<button onclick="generateSchedule(<?php echo $month_info['year']; ?>, <?php echo $month_info['month']; ?>,this)" class="btn btn-success">
<?php echo $is_generated ? '🔄 스케줄 재생성' : '✨ 스케줄 생성'; ?>
</button>
<?php if ($is_generated): ?>
<button onclick="openCalendarModal(<?php echo $month_info['year']; ?>, <?php echo $month_info['month']; ?>)" class="btn btn-info">
달력 보기
</button>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- The Modal -->
<div id="calendarModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modalTitle"></h2>
<span class="close-button">&times;</span>
</div>
<div id="modalBody">
<div class="spinner"></div>
</div>
</div>
</div>
<script>
// --- Modal Control ---
const modal = document.getElementById("calendarModal");
const closeButton = document.querySelector(".close-button");
if(closeButton) closeButton.onclick = () => modal.style.display = "none";
window.onclick = (event) => {
if (event.target == modal) {
modal.style.display = "none";
}
};
async function openCalendarModal(year, month) {
document.getElementById("modalTitle").innerText = `${year}년 ${month}월 스케줄`;
const modalBody = document.getElementById("modalBody");
modalBody.innerHTML = '<div class="spinner"></div>';
modal.style.display = "block";
try {
const response = await fetch(`?action=get_monthly_schedule&year=${year}&month=${month}`);
const result = await response.json();
if (result.success) {
renderCalendar(year, month, result.data);
} else {
modalBody.innerHTML = `<p style="color: red;">스케줄을 불러오는 데 실패했습니다: ${result.message}</p>`;
}
} catch (error) {
modalBody.innerHTML = `<p style="color: red;">오류가 발생했습니다: ${error}</p>`;
}
}
function renderCalendar(year, month, scheduleData) {
const modalBody = document.getElementById("modalBody");
const firstDay = new Date(year, month - 1, 1).getDay(); // 0=일, 1=월, ...
const daysInMonth = new Date(year, month, 0).getDate();
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
let html = '<div class="calendar-grid">';
dayNames.forEach(name => html += `<div class="day-name-header">${name}</div>`);
// Group data by date
const scheduleByDate = scheduleData.reduce((acc, slot) => {
const day = new Date(slot.specific_date).getDate();
if (!acc[day]) acc[day] = [];
acc[day].push(slot);
return acc;
}, {});
// Pad start of month
for (let i = 0; i < firstDay; i++) {
html += '<div class="calendar-day other-month"></div>';
}
// Render days
for (let day = 1; day <= daysInMonth; day++) {
html += `<div class="calendar-day">
<div class="calendar-day-header">${day}</div>
<div class="calendar-day-body">`;
if (scheduleByDate[day]) {
scheduleByDate[day].forEach(slot => {
let slotClass = '';
let slotText = `<span>${slot.start_time.substring(0, 5)}</span>`;
if (slot.temp_1 === 'holiday') {
slotClass = 'holiday';
slotText = `<strong>${slot.temp_2 || '휴무일'}</strong>`;
} else if (slot.temp_1 === 'lunch_time' || slot.temp_1 === 'manual_override') {
slotText = `<span>${slot.start_time.substring(0, 5)} (휴게)</span>`;
slotClass = (slot.is_available == '1') ? 'available' : 'lunch';
} else if (slot.is_available == '1') {
slotClass = 'available';
} else {
slotClass = 'blocked';
}
html += `<div class="time-slot ${slotClass}" data-id="${slot.id}" onclick="toggleSlot(this, '${slot.temp_1}')">
${slotText}
</div>`;
});
}
html += `</div></div>`;
}
html += '</div>';
modalBody.innerHTML = html;
}
async function toggleSlot(element, type) {
if (type === 'holiday') return;
let isLunchOverride = false;
if (type === 'lunch_time' || type === 'manual_override') {
const slotIsAvailable = element.classList.contains('available');
if (!slotIsAvailable) {
if (!confirm('휴게시간입니다. 정말 이 시간에 상담을 등록하시겠습니까?')) {
return;
}
isLunchOverride = true;
}
}
const id = element.dataset.id;
const originalClasses = element.className;
element.innerHTML = '<span>처리중...</span>';
try {
const formData = new FormData();
formData.append('action', 'toggle_slot_status');
formData.append('id', id);
if (isLunchOverride) formData.append('is_lunch_override', '1');
const response = await fetch('', { method: 'POST', body: formData });
const result = await response.json();
if (result.success) {
// Re-render the slot based on the new state
const timeText = element.innerText.split(' ')[0];
if (type === 'lunch_time' || type === 'manual_override') {
if (element.classList.contains('lunch')) {
element.className = 'time-slot available';
} else {
element.className = 'time-slot lunch';
}
element.innerHTML = `<span>${timeText} (휴게)</span>`;
} else {
element.className = result.new_status == 1 ? 'time-slot available' : 'time-slot blocked';
element.innerHTML = `<span>${timeText}</span>`;
}
} else {
alert('상태 변경 실패: ' + result.message);
element.className = originalClasses;
element.innerHTML = `<span>${element.innerText.split(' ')[0]}</span>`;
}
} catch (error) {
alert('오류 발생: ' + error);
element.className = originalClasses;
element.innerHTML = `<span>${element.innerText.split(' ')[0]}</span>`;
}
}
// 💡 [추가] 스케줄 생성/재생성 함수
async function generateSchedule(year, month,btn) {
// const btn = document.querySelector('.status-card button[onclick="generateSchedule("+year+","+month")"]');
const actionText = btn.textContent.trim().includes('재생성') ? '재생성' : '생성';
if (!confirm(`${year}년 ${month}월 스케줄을 ${actionText}하시겠습니까?\n\n기존에 수동으로 변경한 슬롯이 있다면 초기화될 수 있습니다.`)) {
return;
}
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> 생성 중...';
btn.disabled = true;
try {
const formData = new FormData();
formData.append('action', 'generate_schedule');
formData.append('year', year);
formData.append('month', month);
// 현재 페이지 URL('')로 AJAX 요청을 보냅니다.
const response = await fetch('', {
method: 'POST',
body: formData
});
const result = await response.json();
alert(result.message);
if (result.success) {
location.reload();
}
} catch (error) {
alert('스케줄 생성 중 오류가 발생했습니다: ' + error);
} finally {
btn.innerHTML = originalText;
btn.disabled = false;
}
}
</script>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
?>
@@ -0,0 +1,376 @@
<?php
/**
* 월별 스케줄 자동 생성 엔진
*/
if (!defined('_GNUBOARD_'))
exit;
class ScheduleGenerator
{
/**
* 특정 월의 전체 스케줄 생성
*/
public function generateMonth($year, $month)
{
try {
// 기본 설정 조회
$basic_settings = $this->getBasicSettings();
// 요일별 설정 조회
$weekly_settings = $this->getWeeklySettings();
// 해당 월의 모든 날짜 생성
$dates = $this->getMonthDates($year, $month);
// 기존 스케줄 삭제 (자동 생성된 것만)
$this->clearAutoGeneratedSchedule($year, $month);
$generated_count = 0;
foreach ($dates as $date) {
$day_of_week = date('w', strtotime($date)); // 0=일요일, 1=월요일, ...
$day_name = $this->getDayName($day_of_week);
// 해당 요일의 설정 확인
if (isset($weekly_settings[$day_name]) && $weekly_settings[$day_name]['enabled'] == '1') {
// 운영일인 경우 스케줄 생성
$slots_created = $this->generateDay($date, $weekly_settings[$day_name], $basic_settings);
$generated_count += $slots_created;
} else {
// 휴무일인 경우 휴무 표시
$this->createHolidaySlot($date, '휴무일');
}
}
consultant_log("월별 스케줄 생성 완료: {$year}-{$month}, 생성된 슬롯: {$generated_count}");
return $generated_count;
} catch (Exception $e) {
consultant_log("월별 스케줄 생성 실패: " . $e->getMessage(), 'error');
throw $e;
}
}
/**
* 특정 날짜의 스케줄 생성
*/
public function generateDay($date, $day_settings, $basic_settings)
{
$slots_created = 0;
try {
$start_time = $day_settings['start'];
$end_time = $day_settings['end'];
$lunch_start = $day_settings['lunch_start'];
$lunch_end = $day_settings['lunch_end'];
$slot_duration = (int) $basic_settings['consultation_duration'];
$max_persons = (int) $basic_settings['max_persons_per_slot'];
// 시간 슬롯 생성
$current_time = strtotime($start_time);
$end_timestamp = strtotime($end_time);
while ($current_time < $end_timestamp) {
$slot_start = date('H:i', $current_time);
$slot_end = date('H:i', $current_time + ($slot_duration * 60));
// 종료시간이 운영시간을 넘지 않도록 체크
if (strtotime($slot_end) > $end_timestamp) {
break;
}
// 점심시간 체크
$is_lunch_time = $this->isLunchTime($slot_start, $slot_end, $lunch_start, $lunch_end);
if ($is_lunch_time) {
// 점심시간 슬롯 생성
$this->createTimeSlot($date, $slot_start, $slot_end, $slot_duration, 0, 0, 'lunch_time');
} else {
// 일반 상담 슬롯 생성
$this->createTimeSlot($date, $slot_start, $slot_end, $slot_duration, $max_persons, 1, 'auto_generated');
}
$slots_created++;
$current_time += ($slot_duration * 60);
}
return $slots_created;
} catch (Exception $e) {
consultant_log("일별 스케줄 생성 실패 ({$date}): " . $e->getMessage(), 'error');
throw $e;
}
}
/**
* 시간 슬롯 생성
*/
private function createTimeSlot($date, $start_time, $end_time, $duration, $max_persons, $is_available, $type)
{
$sql = "INSERT INTO consultant_schedule
(specific_date, start_time, end_time, time_slot, max_persons, is_available, temp_1, created_at)
VALUES
('{$date}', '{$start_time}', '{$end_time}', {$duration}, {$max_persons}, {$is_available}, '{$type}', NOW())";
return sql_query($sql);
}
/**
* 휴무일 슬롯 생성
*/
private function createHolidaySlot($date, $reason = '휴무일')
{
$sql = "INSERT INTO consultant_schedule
(specific_date, start_time, end_time, time_slot, max_persons, is_available, temp_1, temp_2, created_at)
VALUES
('{$date}', '00:00', '23:59', 0, 0, 0, 'holiday', '{$reason}', NOW())";
return sql_query($sql);
}
/**
* 점심시간 여부 확인
*/
private function isLunchTime($slot_start, $slot_end, $lunch_start, $lunch_end)
{
if (empty($lunch_start) || empty($lunch_end)) {
return false;
}
$slot_start_time = strtotime($slot_start);
$slot_end_time = strtotime($slot_end);
$lunch_start_time = strtotime($lunch_start);
$lunch_end_time = strtotime($lunch_end);
// 슬롯이 점심시간과 겹치는지 확인
return ($slot_start_time >= $lunch_start_time && $slot_start_time < $lunch_end_time) ||
($slot_end_time > $lunch_start_time && $slot_end_time <= $lunch_end_time) ||
($slot_start_time <= $lunch_start_time && $slot_end_time >= $lunch_end_time);
}
/**
* 해당 월의 모든 날짜 배열 생성
*/
private function getMonthDates($year, $month)
{
$dates = [];
$days_in_month = cal_days_in_month(CAL_GREGORIAN, $month, $year);
for ($day = 1; $day <= $days_in_month; $day++) {
$dates[] = sprintf('%04d-%02d-%02d', $year, $month, $day);
}
return $dates;
}
/**
* 요일 숫자를 요일명으로 변환
*/
private function getDayName($day_of_week)
{
$day_names = [
0 => 'sunday',
1 => 'monday',
2 => 'tuesday',
3 => 'wednesday',
4 => 'thursday',
5 => 'friday',
6 => 'saturday'
];
return $day_names[$day_of_week] ?? 'sunday';
}
/**
* 기본 설정 조회
*/
private function getBasicSettings()
{
return [
'consultation_duration' => consultant_get_config('consultation_duration', '60'),
'max_persons_per_slot' => consultant_get_config('max_persons_per_slot', '2'),
'consultation_fee' => consultant_get_config('consultation_fee', '50000')
];
}
/**
* 요일별 설정 조회
*/
private function getWeeklySettings()
{
$days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
$settings = [];
foreach ($days as $day) {
$settings[$day] = [
'enabled' => consultant_get_config($day . '_enabled', $day == 'saturday' || $day == 'sunday' ? '0' : '1'),
'start' => consultant_get_config($day . '_start', '09:00'),
'end' => consultant_get_config($day . '_end', '18:00'),
'lunch_start' => consultant_get_config($day . '_lunch_start', '12:00'),
'lunch_end' => consultant_get_config($day . '_lunch_end', '13:00')
];
}
return $settings;
}
/**
* 자동 생성된 스케줄 삭제
*/
private function clearAutoGeneratedSchedule($year, $month)
{
$start_date = sprintf('%04d-%02d-01', $year, $month);
$end_date = sprintf('%04d-%02d-%02d', $year, $month, cal_days_in_month(CAL_GREGORIAN, $month, $year));
// 기존 예약이 없는 자동 생성 스케줄만 삭제
$sql = "DELETE cs FROM consultant_schedule cs
LEFT JOIN consultant_reservations cr ON (
cs.specific_date = cr.reservation_date
AND cs.start_time = cr.reservation_time
AND cr.is_deleted = 0
)
WHERE cs.specific_date >= '{$start_date}'
AND cs.specific_date <= '{$end_date}'
AND cs.temp_1 IN ('auto_generated', 'lunch_time', 'holiday')
AND cr.id IS NULL";
return sql_query($sql);
}
/**
* 기존 예약 보호 - 예약이 있는 시간대 확인
*/
public function getExistingReservations($year, $month)
{
$start_date = sprintf('%04d-%02d-01', $year, $month);
$end_date = sprintf('%04d-%02d-%02d', $year, $month, cal_days_in_month(CAL_GREGORIAN, $month, $year));
$sql = "SELECT reservation_date, reservation_time, COUNT(*) as count
FROM consultant_reservations
WHERE reservation_date >= '{$start_date}'
AND reservation_date <= '{$end_date}'
AND is_deleted = 0
GROUP BY reservation_date, reservation_time";
$result = sql_query($sql);
$reservations = [];
if ($result) {
while ($row = sql_fetch_array($result)) {
$key = $row['reservation_date'] . '_' . $row['reservation_time'];
$reservations[$key] = $row['count'];
}
}
return $reservations;
}
/**
* 다음 달 스케줄 자동 생성 (크론잡용)
*/
public function generateNextMonth()
{
$next_month = date('Y-m', strtotime('+1 month'));
list($year, $month) = explode('-', $next_month);
return $this->generateMonth((int) $year, (int) $month);
}
/**
* 스케줄 생성 상태 확인
*/
public function checkScheduleStatus($year, $month)
{
$start_date = sprintf('%04d-%02d-01', $year, $month);
$end_date = sprintf('%04d-%02d-%02d', $year, $month, cal_days_in_month(CAL_GREGORIAN, $month, $year));
$sql = "SELECT
COUNT(*) as total_slots,
COUNT(CASE WHEN temp_1 = 'auto_generated' THEN 1 END) as auto_slots,
COUNT(CASE WHEN temp_1 = 'lunch_time' THEN 1 END) as lunch_slots,
COUNT(CASE WHEN temp_1 = 'holiday' THEN 1 END) as holiday_slots,
COUNT(CASE WHEN temp_1 = 'admin_blocked' THEN 1 END) as blocked_slots
FROM consultant_schedule
WHERE specific_date >= '{$start_date}'
AND specific_date <= '{$end_date}'";
return sql_fetch($sql);
}
/**
* 설정 변경 시 영향받는 예약 확인
*/
public function checkSettingConflicts($year, $month)
{
$existing_reservations = $this->getExistingReservations($year, $month);
$conflicts = [];
// 새로운 설정으로 생성될 스케줄과 기존 예약 비교
$weekly_settings = $this->getWeeklySettings();
$dates = $this->getMonthDates($year, $month);
foreach ($dates as $date) {
$day_of_week = date('w', strtotime($date));
$day_name = $this->getDayName($day_of_week);
// 휴무일로 변경되었는데 예약이 있는 경우
if (!isset($weekly_settings[$day_name]) || $weekly_settings[$day_name]['enabled'] != '1') {
foreach ($existing_reservations as $key => $count) {
if (strpos($key, $date) === 0) {
$conflicts[] = [
'date' => $date,
'type' => 'holiday_conflict',
'message' => "{$date}는 휴무일로 설정되었지만 {$count}건의 예약이 있습니다."
];
}
}
}
}
return $conflicts;
}
}
/**
* 스케줄 생성 헬퍼 함수들
*/
/**
* 월별 스케줄 생성 실행
*/
function generate_monthly_schedule($year, $month)
{
$generator = new ScheduleGenerator();
return $generator->generateMonth($year, $month);
}
/**
* 다음 달 스케줄 자동 생성
*/
function auto_generate_next_month_schedule()
{
$generator = new ScheduleGenerator();
return $generator->generateNextMonth();
}
/**
* 스케줄 생성 상태 확인
*/
function get_schedule_generation_status($year, $month)
{
$generator = new ScheduleGenerator();
return $generator->checkScheduleStatus($year, $month);
}
/**
* 설정 변경 영향 확인
*/
function check_schedule_setting_conflicts($year, $month)
{
$generator = new ScheduleGenerator();
return $generator->checkSettingConflicts($year, $month);
}
?>
+381
View File
@@ -0,0 +1,381 @@
<?php
/**
* 상담 예약 시스템 통합 설정 관리
*/
$sub_menu = '850610'; // 메뉴 코드 (기본/운영 설정)
include_once('./_common.php');
// 권한 확인
auth_check_menu($auth, $sub_menu, 'w');
// 설치 확인
if (!is_consultant_installed()) {
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
}
$g5['title'] = '상담 예약 설정';
// 현재 탭 확인
$current_tab = $_GET['tab'] ?? 'basic';
// 폼 처리
if (isset($_POST['action']) && $_POST['action']) {
try {
// 기본 설정 저장
if ($_POST['action'] == 'save_basic_settings') {
$basic_settings = [
'consultation_duration' => (int) ($_POST['consultation_duration'] ?? 60),
'max_persons_per_slot' => (int) ($_POST['max_persons_per_slot'] ?? 2),
'consultation_fee' => (int) ($_POST['consultation_fee'] ?? 50000),
'account_info' => trim($_POST['account_info'] ?? ''),
'max_advance_days' => (int) ($_POST['max_advance_days'] ?? 30),
'min_advance_hours' => (int) ($_POST['min_advance_hours'] ?? 24),
'cancel_deadline_hours' => (int) ($_POST['cancel_deadline_hours'] ?? 24)
];
// 유효성 검증 (생략)
foreach ($basic_settings as $key => $value) {
consultant_set_config($key, $value);
}
alert('기본 설정이 저장되었습니다.', $_SERVER['PHP_SELF'] . '?tab=basic');
}
// 요일별 설정 저장
if ($_POST['action'] == 'save_weekly_settings') {
$days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
foreach ($days as $day) {
consultant_set_config($day . '_enabled', $_POST[$day . '_enabled'] ?? '0');
consultant_set_config($day . '_start', $_POST[$day . '_start'] ?? '09:00');
consultant_set_config($day . '_end', $_POST[$day . '_end'] ?? '18:00');
consultant_set_config($day . '_lunch_start', $_POST[$day . '_lunch_start'] ?? '12:00');
consultant_set_config($day . '_lunch_end', $_POST[$day . '_lunch_end'] ?? '13:00');
}
alert('요일별 설정이 저장되었습니다.', $_SERVER['PHP_SELF'] . '?tab=weekly');
}
// 알림 설정 저장
if ($_POST['action'] == 'save_notification_settings') {
consultant_set_config('notification_enabled', $_POST['notification_enabled'] ?? '0');
alert('알림 설정이 저장되었습니다.', $_SERVER['PHP_SELF'] . '?tab=notification');
}
// 💡 [추가] 고급 설정 저장 처리
if ($_POST['action'] == 'save_advanced_settings') {
$config_values = $_POST['config_value'] ?? [];
$config_descs = $_POST['config_desc'] ?? [];
foreach ($config_values as $key => $value) {
$sql = "UPDATE consultant_config
SET config_value = '" . sql_real_escape_string($value) . "',
config_desc = '" . sql_real_escape_string($config_descs[$key] ?? '') . "'
WHERE config_key = '" . sql_real_escape_string($key) . "'";
sql_query($sql);
}
alert('고급 설정이 저장되었습니다.', $_SERVER['PHP_SELF'] . '?tab=advanced');
}
} catch (Exception $e) {
alert('설정 저장 중 오류가 발생했습니다: ' . $e->getMessage());
}
}
// --- 데이터 조회 ---
// 기본 설정
$consultation_duration = consultant_get_config('consultation_duration', '60');
$max_persons_per_slot = consultant_get_config('max_persons_per_slot', '2');
$consultation_fee = consultant_get_config('consultation_fee', '50000');
$account_info = consultant_get_config('account_info', '국민은행 123-456-789 (주)상담센터');
$notification_enabled = consultant_get_config('notification_enabled', '1');
$max_advance_days = consultant_get_config('max_advance_days', '30');
$min_advance_hours = consultant_get_config('min_advance_hours', '24');
$cancel_deadline_hours = consultant_get_config('cancel_deadline_hours', '24');
// 요일별 설정
$days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
$day_names = ['월요일', '화요일', '수요일', '목요일', '금요일', '토요일', '일요일'];
$weekly_settings = [];
foreach ($days as $i => $day) {
$weekly_settings[$day] = [
'name' => $day_names[$i],
'enabled' => consultant_get_config($day . '_enabled', $day == 'saturday' || $day == 'sunday' ? '0' : '1'),
'start' => consultant_get_config($day . '_start', '09:00'),
'end' => consultant_get_config($day . '_end', '18:00'),
'lunch_start' => consultant_get_config($day . '_lunch_start', '12:00'),
'lunch_end' => consultant_get_config($day . '_lunch_end', '13:00')
];
}
// 💡 [추가] 고급 설정 데이터 조회
$advanced_configs = [];
$result = sql_query("SELECT * FROM consultant_config ORDER BY id");
while ($row = sql_fetch_array($result)) {
$advanced_configs[] = $row;
}
include_once(G5_ADMIN_PATH . '/admin.head.php');
?>
<div class="settings-container">
<div class="settings-header">
<h2><?php echo $g5['title']; ?></h2>
<div>
<a href="dashboard.php" class="header-btn">📊 대시보드</a>
<a href="schedule_generate.php" class="header-btn primary">📅 빠른 스케줄 관리</a>
</div>
</div>
<!-- 탭 네비게이션 -->
<div class="settings-tabs">
<a href="?tab=basic" class="tab-button <?php echo $current_tab == 'basic' ? 'active' : ''; ?>">⚙️ 기본 설정</a>
<a href="?tab=weekly" class="tab-button <?php echo $current_tab == 'weekly' ? 'active' : ''; ?>">📅 요일별 운영시간</a>
<a href="?tab=advanced" class="tab-button <?php echo $current_tab == 'advanced' ? 'active' : ''; ?>">🛠️ 고급 설정</a>
<a href="?tab=notification" class="tab-button <?php echo $current_tab == 'notification' ? 'active' : ''; ?>">🔔 알림 설정</a>
</div>
<!-- 기본 설정 탭 -->
<div class="tab-content <?php echo $current_tab == 'basic' ? 'active' : ''; ?>">
<div class="alert alert-info">
<strong>기본 설정:</strong> 1회 상담시간, 최대인원, 상담비 등 기본적인 상담 조건을 설정합니다.
</div>
<form method="post" class="settings-form">
<input type="hidden" name="action" value="save_basic_settings">
<div class="section-title">⏰ 상담 기본 조건</div>
<div class="form-row">
<div class="form-group">
<label for="consultation_duration">1회 상담 시간 (분)</label>
<select id="consultation_duration" name="consultation_duration">
<?php
for ($i = 15; $i <= 480; $i += 15) {
$selected = ($consultation_duration == $i) ? 'selected' : '';
echo "<option value=\"{$i}\" {$selected}>{$i}분</option>";
}
?>
</select>
<small>15분~480분 사이로 설정 가능합니다. (15분 단위)</small>
</div>
<div class="form-group">
<label for="max_persons_per_slot">1회 상담 최대 인원 (명)</label>
<input type="number" id="max_persons_per_slot" name="max_persons_per_slot" value="<?php echo htmlspecialchars($max_persons_per_slot); ?>" min="1" max="50" placeholder="2">
<small>1명~50명 사이로 설정 가능합니다.</small>
</div>
</div>
<div class="section-title">💰 결제 정보</div>
<div class="form-row">
<div class="form-group">
<label for="consultation_fee">상담 비용 (원)</label>
<input type="number" id="consultation_fee" name="consultation_fee" value="<?php echo htmlspecialchars($consultation_fee); ?>" min="0" step="1000" placeholder="50000">
</div>
</div>
<div class="form-group">
<label for="account_info">입금 계좌 정보</label>
<textarea id="account_info" name="account_info" placeholder="예: 국민은행 123-456-789 (주)상담센터"><?php echo htmlspecialchars($account_info); ?></textarea>
</div>
<div class="section-title">📅 예약 제한 설정</div>
<div class="form-row">
<div class="form-group">
<label for="max_advance_days">최대 예약 가능 일수</label>
<input type="number" id="max_advance_days" name="max_advance_days" value="<?php echo htmlspecialchars($max_advance_days); ?>" min="1" max="365">
<small>오늘부터 몇 일 후까지 예약 가능한지 설정</small>
</div>
<div class="form-group">
<label for="min_advance_hours">최소 예약 시간 (시간)</label>
<input type="number" id="min_advance_hours" name="min_advance_hours" value="<?php echo htmlspecialchars($min_advance_hours); ?>" min="1" max="168">
<small>최소 몇 시간 전에 예약해야 하는지 설정</small>
</div>
<div class="form-group">
<label for="cancel_deadline_hours">예약 취소 마감 (시간)</label>
<input type="number" id="cancel_deadline_hours" name="cancel_deadline_hours" value="<?php echo htmlspecialchars($cancel_deadline_hours); ?>" min="1" max="168">
<small>상담 시작 몇 시간 전까지 고객이 직접 취소할 수 있는지 설정</small>
</div>
</div>
<div style="text-align: center; margin-top: 40px;">
<button type="submit" class="btn btn-primary">기본 설정 저장</button>
</div>
</form>
</div>
<!-- 요일별 운영시간 탭 -->
<div class="tab-content <?php echo $current_tab == 'weekly' ? 'active' : ''; ?>">
<div class="alert alert-info">
<strong>요일별 운영시간:</strong> 각 요일의 상담 운영 여부와 시간을 설정합니다. '운영'을 선택해야 해당 요일의 스케줄이 생성됩니다.
</div>
<form method="post" class="settings-form">
<input type="hidden" name="action" value="save_weekly_settings">
<?php foreach ($weekly_settings as $day => $setting): ?>
<div class="day-setting">
<div class="day-header">
<div class="checkbox-wrapper">
<input type="hidden" name="<?php echo $day; ?>_enabled" value="0">
<input type="checkbox" id="<?php echo $day; ?>_enabled" name="<?php echo $day; ?>_enabled" value="1" <?php echo $setting['enabled'] == '1' ? 'checked' : ''; ?> onchange="toggleDayTimes('<?php echo $day; ?>')">
<label for="<?php echo $day; ?>_enabled" class="day-name"><?php echo $setting['name']; ?></label>
</div>
</div>
<div class="day-times" id="<?php echo $day; ?>_times">
<div class="form-group">
<label for="<?php echo $day; ?>_start">업무 시작</label>
<input type="time" id="<?php echo $day; ?>_start" name="<?php echo $day; ?>_start" value="<?php echo htmlspecialchars($setting['start']); ?>">
</div>
<div class="form-group">
<label for="<?php echo $day; ?>_end">업무 종료</label>
<input type="time" id="<?php echo $day; ?>_end" name="<?php echo $day; ?>_end" value="<?php echo htmlspecialchars($setting['end']); ?>">
</div>
<div class="form-group">
<label for="<?php echo $day; ?>_lunch_start">점심 시작</label>
<input type="time" id="<?php echo $day; ?>_lunch_start" name="<?php echo $day; ?>_lunch_start" value="<?php echo htmlspecialchars($setting['lunch_start']); ?>">
</div>
<div class="form-group">
<label for="<?php echo $day; ?>_lunch_end">점심 종료</label>
<input type="time" id="<?php echo $day; ?>_lunch_end" name="<?php echo $day; ?>_lunch_end" value="<?php echo htmlspecialchars($setting['lunch_end']); ?>">
</div>
</div>
</div>
<?php endforeach; ?>
<div style="text-align: center; margin-top: 40px;">
<button type="submit" class="btn btn-primary">요일별 설정 저장</button>
</div>
</form>
</div>
<!-- 💡 [추가] 고급 설정 탭 -->
<div class="tab-content <?php echo $current_tab == 'advanced' ? 'active' : ''; ?>">
<div class="alert alert-info">
<strong>고급 설정:</strong> 시스템의 모든 설정값을 직접 관리합니다. <strong>'Key'는 시스템에서 사용하는 고유값이므로 변경할 수 없습니다.</strong><br>
'Value'는 실제 적용되는 값이며, 'Description'은 관리자가 참고하기 위한 설명입니다.
</div>
<form method="post" class="settings-form">
<input type="hidden" name="action" value="save_advanced_settings">
<div class="tbl_head01 tbl_wrap">
<table>
<caption>고급 설정 목록</caption>
<colgroup>
<col style="width: 25%;">
<col style="width: 40%;">
<col style="width: 35%;">
</colgroup>
<thead>
<tr>
<th scope="col">Key (변경불가)</th>
<th scope="col">Value (설정값)</th>
<th scope="col">Description (설명)</th>
</tr>
</thead>
<tbody>
<?php foreach ($advanced_configs as $config_item): ?>
<tr>
<td>
<label for="config_value_<?php echo $config_item['config_key']; ?>">
<code style="font-size: 13px; font-weight: bold;"><?php echo htmlspecialchars($config_item['config_key']); ?></code>
</label>
</td>
<td>
<?php if (strlen($config_item['config_value']) > 50 || strpos($config_item['config_value'], "\n") !== false): ?>
<textarea class="form-control" id="config_value_<?php echo $config_item['config_key']; ?>" name="config_value[<?php echo $config_item['config_key']; ?>]" rows="2"><?php echo htmlspecialchars($config_item['config_value']); ?></textarea>
<?php else: ?>
<input type="text" class="form-control" id="config_value_<?php echo $config_item['config_key']; ?>" name="config_value[<?php echo $config_item['config_key']; ?>]" value="<?php echo htmlspecialchars($config_item['config_value']); ?>">
<?php endif; ?>
</td>
<td>
<input type="text" class="form-control" name="config_desc[<?php echo $config_item['config_key']; ?>]" value="<?php echo htmlspecialchars($config_item['config_desc']); ?>">
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div style="text-align: center; margin-top: 40px;">
<button type="submit" class="btn btn-primary">고급 설정 저장</button>
</div>
</form>
</div>
<!-- 알림 설정 탭 -->
<div class="tab-content <?php echo $current_tab == 'notification' ? 'active' : ''; ?>">
<div class="alert alert-info">
<strong>알림 설정:</strong> 예약 관련 알림 기능을 설정합니다.
</div>
<form method="post" class="settings-form">
<input type="hidden" name="action" value="save_notification_settings">
<div class="section-title">🔔 알림 기능</div>
<div class="form-group">
<div class="checkbox-wrapper">
<input type="checkbox" id="notification_enabled" name="notification_enabled" value="1" <?php echo $notification_enabled == '1' ? 'checked' : ''; ?>>
<label for="notification_enabled">알림 기능 사용</label>
</div>
<small>예약 확정, 취소 등의 상황에서 고객에게 알림을 발송합니다.</small>
</div>
<div style="text-align: center; margin-top: 40px;">
<button type="submit" class="btn btn-primary">알림 설정 저장</button>
</div>
</form>
</div>
</div>
<!-- 💡 [복구] 깨진 화면을 복구하기 위해 CSS와 JS를 파일 내에 다시 포함합니다. -->
<style>
.settings-container { max-width: 1000px; margin: 0 auto; padding: 20px; }
.settings-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.settings-header div { display: flex; gap: 8px; }
.settings-tabs { display: flex; border-bottom: 2px solid #ddd; margin-bottom: 30px; }
.tab-button { padding: 16px 28px; border: none; background: none; cursor: pointer; font-size: 16px; font-weight: 600; color: #666; text-decoration: none; border-bottom: 3px solid transparent; transition: all 0.3s; }
.tab-button.active { color: #007bff; border-bottom-color: #007bff; }
.tab-button:hover { color: #007bff; background: #fff; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.settings-form { background: white; border: 1px solid #ddd; border-radius: 8px; padding: 30px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); }
.form-group { margin-bottom: 20px; }
.form-group label { display: block; margin-bottom: 8px; font-weight: bold; color: #333; }
.form-group input, .form-group textarea, .form-group select { width: 100%; padding: 12px 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; box-sizing: border-box; min-height: 44px; line-height: 1.4; }
.form-group textarea { height: auto; resize: vertical; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.btn { padding: 16px 32px; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 16px; text-decoration: none; display: inline-block; transition: all 0.2s; text-align: center; min-height: 50px; line-height: 1.4; box-sizing: border-box; vertical-align: middle; }
.btn-primary { background: #007bff; color: white; }
.btn-primary:hover { background: #0056b3; }
.alert { padding: 15px; margin-bottom: 20px; border: 1px solid transparent; border-radius: 4px; }
.alert-info { color: #0c5460; background-color: #d1ecf1; border-color: #bee5eb; }
.section-title { font-size: 18px; font-weight: bold; margin: 30px 0 15px 0; padding-bottom: 8px; border-bottom: 2px solid #007bff; color: #333; }
.section-title:first-child { margin-top: 0; }
.day-setting { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-bottom: 15px; }
.day-header { display: flex; align-items: center; margin-bottom: 15px; gap: 15px; }
.day-name { font-weight: bold; font-size: 16px; color: #333; min-width: 80px; }
.day-times { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 15px; }
.day-times.disabled { opacity: 0.5; pointer-events: none; }
.checkbox-wrapper { display: flex; align-items: center; gap: 8px; }
.checkbox-wrapper input[type="checkbox"] { width: 18px; height: 18px; margin: 0; }
.form-group small { display: block; margin-top: 5px; font-size: 12px; color: #666; line-height: 1.3; }
.header-btn { padding: 8px 16px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; font-weight: 500; font-size: 14px; text-decoration: none; display: inline-block; transition: all 0.2s; text-align: center; min-height: auto; line-height: 1.2; box-sizing: border-box; background: #fff; color: #333; }
.header-btn:hover { background: #fff; border-color: #adb5bd; color: #333; }
.header-btn.primary { background: #e3f2fd; border-color: #90caf9; color: #1976d2; }
.header-btn.primary:hover { background: #bbdefb; border-color: #64b5f6; }
</style>
<script>
function toggleDayTimes(day) {
const checkbox = document.getElementById(day + '_enabled');
const timesDiv = document.getElementById(day + '_times');
if (checkbox?.checked) {
timesDiv.classList.remove('disabled');
} else {
timesDiv?.classList.add('disabled');
}
}
document.addEventListener('DOMContentLoaded', function () {
const days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
days.forEach(function (day) {
toggleDayTimes(day);
});
});
</script>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
?>
+454
View File
@@ -0,0 +1,454 @@
<?php
/**
* 상담 예약 통계 분석
*/
$sub_menu = '850400';
include_once('./_common.php');
// 관리자 권한 확인
if (!$is_admin) {
alert('관리자만 접근할 수 있습니다.');
}
// 설치 확인
if (!is_consultant_installed()) {
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
}
$g5['title'] = '통계 분석';
// 기간 설정
$start_date = $_GET['start_date'] ?? date('Y-m-01'); // 이번 달 첫날
$end_date = $_GET['end_date'] ?? date('Y-m-d'); // 오늘
// 전체 통계
$sql = "SELECT
COUNT(*) as total,
COUNT(CASE WHEN status = 'payment_pending' THEN 1 END) as pending,
COUNT(CASE WHEN status = 'reserved' THEN 1 END) as confirmed,
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed,
COUNT(CASE WHEN status = 'cancelled' THEN 1 END) as cancelled,
SUM(CASE WHEN status = 'completed' THEN payment_amount ELSE 0 END) as total_revenue
FROM consultant_reservations
WHERE reservation_date BETWEEN '{$start_date}' AND '{$end_date}'
AND is_deleted = 0";
$total_stats = sql_fetch($sql);
// 일별 통계
$sql = "SELECT
reservation_date,
COUNT(*) as total,
COUNT(CASE WHEN status = 'payment_pending' THEN 1 END) as pending,
COUNT(CASE WHEN status = 'reserved' THEN 1 END) as confirmed,
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed,
COUNT(CASE WHEN status = 'cancelled' THEN 1 END) as cancelled,
SUM(CASE WHEN status = 'completed' THEN payment_amount ELSE 0 END) as revenue
FROM consultant_reservations
WHERE reservation_date BETWEEN '{$start_date}' AND '{$end_date}'
AND is_deleted = 0
GROUP BY reservation_date
ORDER BY reservation_date DESC";
$daily_stats = [];
$result = sql_query($sql);
while ($row = sql_fetch_array($result)) {
$daily_stats[] = $row;
}
// 시간대별 통계
$sql = "SELECT
reservation_time,
COUNT(*) as count
FROM consultant_reservations
WHERE reservation_date BETWEEN '{$start_date}' AND '{$end_date}'
AND is_deleted = 0
GROUP BY reservation_time
ORDER BY reservation_time";
$time_stats = [];
$result = sql_query($sql);
while ($row = sql_fetch_array($result)) {
$time_stats[] = $row;
}
// 상태별 통계
$status_labels = [
'payment_pending' => '입금대기',
'reserved' => '예약확정',
'completed' => '상담완료',
'cancelled' => '예약취소'
];
include_once(G5_ADMIN_PATH . '/admin.head.php');
?>
<style>
.statistics-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.filter-form {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 15px;
flex-wrap: wrap;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
text-align: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.stat-number {
font-size: 32px;
font-weight: bold;
margin-bottom: 8px;
}
.stat-label {
color: #666;
font-size: 14px;
}
.stat-card.total .stat-number {
color: #007bff;
}
.stat-card.pending .stat-number {
color: #ffc107;
}
.stat-card.confirmed .stat-number {
color: #28a745;
}
.stat-card.completed .stat-number {
color: #17a2b8;
}
.stat-card.cancelled .stat-number {
color: #dc3545;
}
.stat-card.revenue .stat-number {
color: #6f42c1;
}
.chart-container {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.chart-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 20px;
color: #333;
}
.table-container {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
.table-header {
background: #fff;
padding: 15px 20px;
font-weight: bold;
border-bottom: 1px solid #ddd;
}
.table-content {
max-height: 400px;
overflow-y: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background: #fff;
font-weight: bold;
position: sticky;
top: 0;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
display: inline-block;
font-size: 14px;
font-weight: 600;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.form-control {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.bar-chart {
display: flex;
align-items: end;
height: 200px;
gap: 10px;
padding: 20px 0;
}
.bar {
background: #007bff;
border-radius: 4px 4px 0 0;
min-width: 40px;
position: relative;
display: flex;
flex-direction: column;
justify-content: end;
align-items: center;
}
.bar-value {
position: absolute;
top: -25px;
font-size: 12px;
font-weight: bold;
color: #333;
}
.bar-label {
margin-top: 10px;
font-size: 11px;
color: #666;
transform: rotate(-45deg);
white-space: nowrap;
}
.no-data {
text-align: center;
padding: 40px;
color: #666;
font-style: italic;
}
@media (max-width: 768px) {
.filter-form {
flex-direction: column;
align-items: stretch;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
<div class="statistics-container">
<h2><?php echo $g5['title']; ?></h2>
<!-- 기간 필터 -->
<form method="get" class="filter-form">
<label>
시작일:
<input type="date" name="start_date" value="<?php echo $start_date; ?>" class="form-control">
</label>
<label>
종료일:
<input type="date" name="end_date" value="<?php echo $end_date; ?>" class="form-control">
</label>
<button type="submit" class="btn btn-primary">조회</button>
<a href="dashboard.php" class="btn btn-secondary">대시보드로</a>
</form>
<!-- 전체 통계 -->
<div class="stats-grid">
<div class="stat-card total">
<div class="stat-number"><?php echo number_format($total_stats['total']); ?></div>
<div class="stat-label">전체 예약</div>
</div>
<div class="stat-card pending">
<div class="stat-number"><?php echo number_format($total_stats['pending']); ?></div>
<div class="stat-label">입금대기</div>
</div>
<div class="stat-card confirmed">
<div class="stat-number"><?php echo number_format($total_stats['confirmed']); ?></div>
<div class="stat-label">예약확정</div>
</div>
<div class="stat-card completed">
<div class="stat-number"><?php echo number_format($total_stats['completed']); ?></div>
<div class="stat-label">상담완료</div>
</div>
<div class="stat-card cancelled">
<div class="stat-number"><?php echo number_format($total_stats['cancelled']); ?></div>
<div class="stat-label">예약취소</div>
</div>
<div class="stat-card revenue">
<div class="stat-number"><?php echo number_format($total_stats['total_revenue']); ?>원</div>
<div class="stat-label">총 매출</div>
</div>
</div>
<!-- 시간대별 예약 현황 -->
<?php if (!empty($time_stats)): ?>
<div class="chart-container">
<div class="chart-title">시간대별 예약 현황</div>
<div class="bar-chart">
<?php
$max_count = max(array_column($time_stats, 'count'));
foreach ($time_stats as $stat):
$height = $max_count > 0 ? ($stat['count'] / $max_count) * 150 : 0;
?>
<div class="bar" style="height: <?php echo $height; ?>px;">
<div class="bar-value"><?php echo $stat['count']; ?></div>
<div class="bar-label"><?php echo substr($stat['reservation_time'], 0, 5); ?></div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- 일별 상세 통계 -->
<div class="table-container">
<div class="table-header">일별 예약 현황</div>
<div class="table-content">
<?php if (!empty($daily_stats)): ?>
<table>
<thead>
<tr>
<th>날짜</th>
<th>전체</th>
<th>입금대기</th>
<th>예약확정</th>
<th>상담완료</th>
<th>예약취소</th>
<th>매출</th>
</tr>
</thead>
<tbody>
<?php foreach ($daily_stats as $stat): ?>
<tr>
<td><?php echo $stat['reservation_date']; ?></td>
<td><?php echo number_format($stat['total']); ?></td>
<td><?php echo number_format($stat['pending']); ?></td>
<td><?php echo number_format($stat['confirmed']); ?></td>
<td><?php echo number_format($stat['completed']); ?></td>
<td><?php echo number_format($stat['cancelled']); ?></td>
<td><?php echo number_format($stat['revenue']); ?>원</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<div class="no-data">
선택한 기간에 예약 데이터가 없습니다.
</div>
<?php endif; ?>
</div>
</div>
</div>
<script>
// 차트 애니메이션
document.addEventListener('DOMContentLoaded', function () {
const bars = document.querySelectorAll('.bar');
bars.forEach((bar, index) => {
setTimeout(() => {
bar.style.opacity = '0';
bar.style.transform = 'scaleY(0)';
bar.style.transformOrigin = 'bottom';
setTimeout(() => {
bar.style.transition = 'all 0.5s ease';
bar.style.opacity = '1';
bar.style.transform = 'scaleY(1)';
}, 100);
}, index * 100);
});
});
// 기간 설정 단축키
document.addEventListener('DOMContentLoaded', function () {
const startDateInput = document.querySelector('input[name="start_date"]');
const endDateInput = document.querySelector('input[name="end_date"]');
// 오늘 날짜
const today = new Date();
const todayStr = today.toISOString().split('T')[0];
// 이번 주 시작일 (월요일)
const thisWeekStart = new Date(today);
thisWeekStart.setDate(today.getDate() - today.getDay() + 1);
const thisWeekStartStr = thisWeekStart.toISOString().split('T')[0];
// 이번 달 시작일
const thisMonthStart = new Date(today.getFullYear(), today.getMonth(), 1);
const thisMonthStartStr = thisMonthStart.toISOString().split('T')[0];
// 단축키 버튼들 추가
const filterForm = document.querySelector('.filter-form');
const shortcutButtons = document.createElement('div');
shortcutButtons.innerHTML = `
<button type="button" onclick="setDateRange('${todayStr}', '${todayStr}')" class="btn btn-secondary">오늘</button>
<button type="button" onclick="setDateRange('${thisWeekStartStr}', '${todayStr}')" class="btn btn-secondary">이번주</button>
<button type="button" onclick="setDateRange('${thisMonthStartStr}', '${todayStr}')" class="btn btn-secondary">이번달</button>
`;
filterForm.appendChild(shortcutButtons);
window.setDateRange = function (start, end) {
startDateInput.value = start;
endDateInput.value = end;
};
});
</script>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
?>
+427
View File
@@ -0,0 +1,427 @@
<?php
/**
* 알림 템플릿 관리
*/
$sub_menu = '850620';
include_once('./_common.php');
// 관리자 권한 확인
if (!$is_admin) {
alert('관리자만 접근할 수 있습니다.');
}
// 설치 확인
if (!is_consultant_installed()) {
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
}
$g5['title'] = '알림 템플릿 관리';
// 템플릿 저장 처리
if ($_POST['action'] == 'save_template') {
$template_key = $_POST['template_key'];
$template_subject = $_POST['template_subject'];
$template_type = $_POST['template_type'] ?? 'email'; // 💡 [추가] 템플릿 타입
$template_content = $_POST['template_content'];
$template_name = $_POST['template_name']; // 💡 [추가] 템플릿 이름을 폼에서 받아옵니다.
if ($template_key && $template_subject && $template_content) {
// 템플릿 저장/업데이트
// 💡 [수정] 타입에 따라 테이블 분기
$table_name = ($template_type === 'sms') ? 'consultant_sms_templates' : 'consultant_mail_templates';
$sql = "INSERT INTO {$table_name}
(template_key, template_type, template_name, template_subject, template_content, updated_at)
VALUES (
'" . sql_real_escape_string($template_key) . "',
'" . sql_real_escape_string($template_type) . "',
'" . sql_real_escape_string($template_name) . "',
'" . sql_real_escape_string($template_subject) . "',
'" . sql_real_escape_string($template_content) . "',
NOW()
)
ON DUPLICATE KEY UPDATE
template_subject = '" . sql_real_escape_string($template_subject) . "',
template_type = '" . sql_real_escape_string($template_type) . "',
template_content = '" . sql_real_escape_string($template_content) . "',
updated_at = NOW()";
if (sql_query($sql)) {
alert('템플릿이 저장되었습니다.', $_SERVER['PHP_SELF'] . '?type=' . $template_type . '&template=' . $template_key);
} else {
alert('템플릿 저장에 실패했습니다.');
}
}
}
// 💡 [추가] 현재 탭 확인
$current_type = $_GET['type'] ?? 'email';
// 기본 템플릿 정의
$default_templates = [
'consultant_reservation_customer' => [
'name' => '고객 예약 신청 확인',
'subject' => '[상담예약] 예약 신청이 접수되었습니다',
'content' => "안녕하세요 {customer_name}님,\n\n상담 예약 신청이 정상적으로 접수되었습니다.\n\n예약 정보:\n- 날짜: {reservation_date}\n- 시간: {reservation_time}\n- 상담비: {payment_amount}원\n\n입금 계좌: {account_info}\n\n입금 확인 후 예약이 확정됩니다.\n\n감사합니다."
],
'consultant_confirmed_customer' => [
'name' => '고객 예약 확정 알림',
'subject' => '[상담예약] 예약이 확정되었습니다',
'content' => "안녕하세요 {customer_name}님,\n\n입금이 확인되어 예약이 확정되었습니다.\n\n예약 정보:\n- 날짜: {reservation_date}\n- 시간: {reservation_time}\n\n상담 당일 시간에 맞춰 방문해주시기 바랍니다.\n\n감사합니다."
],
'consultant_cancelled_customer' => [
'name' => '고객 예약 취소 알림',
'subject' => '[상담예약] 예약이 취소되었습니다',
'content' => "안녕하세요 {customer_name}님,\n\n예약이 취소되었습니다.\n\n취소된 예약 정보:\n- 날짜: {reservation_date}\n- 시간: {reservation_time}\n\n취소 사유: {cancel_reason}\n\n문의사항이 있으시면 연락주시기 바랍니다.\n\n감사합니다."
]
];
// 현재 템플릿 조회
$templates = [];
// 💡 [수정] 현재 탭의 타입에 맞는 템플릿만 조회합니다.
if (is_consultant_installed()) {
// 💡 [수정] 타입에 따라 테이블 분기
$table_name = ($current_type === 'sms') ? 'consultant_sms_templates' : 'consultant_mail_templates';
// 테이블 존재 여부 확인 (설치 초기 단계 고려)
$table_check = sql_query("SHOW TABLES LIKE '{$table_name}'", false);
if (sql_num_rows($table_check) > 0) {
$sql = "SELECT * FROM {$table_name} WHERE template_type = '".sql_real_escape_string($current_type)."' ORDER BY template_key";
$result = sql_query($sql, false);
if ($result) {
while ($row = sql_fetch_array($result)) {
$templates[$row['template_key']] = $row;
}
}
}
}
// 기본 템플릿과 병합
foreach ($default_templates as $key => $default) {
if (!isset($templates[$key])) {
$templates[$key] = [
'template_key' => $key,
'template_name' => $default['name'],
'template_subject' => $default['subject'],
'template_content' => $default['content']
];
}
}
$current_template_key = $_GET['template'] ?? array_key_first($templates);
include_once(G5_ADMIN_PATH . '/admin.head.php');
?>
<style>
.templates-container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.template-nav {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.template-tabs {
display: flex; border-bottom: 2px solid #ddd; margin-bottom: 30px;
}
.template-tab {
padding: 16px 28px;
border: none;
background: none;
cursor: pointer;
font-size: 16px;
font-weight: 600;
color: #666;
text-decoration: none;
border-bottom: 3px solid transparent;
transition: all 0.2s;
}
.template-tab.active {
background: #007bff;
color: white;
border-color: #007bff;
}
.template-form {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 30px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #333;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
font-family: inherit;
}
.form-group textarea {
height: 200px;
resize: vertical;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
text-decoration: none;
display: inline-block;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.variables-info {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 15px;
margin-bottom: 20px;
}
.variables-title {
font-weight: bold;
margin-bottom: 10px;
color: #333;
}
.variables-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
}
.variable-item {
background: white;
padding: 8px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
border: 1px solid #ddd;
}
.alert {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
}
.alert-info {
color: #0c5460;
background-color: #d1ecf1;
border-color: #bee5eb;
}
@media (max-width: 768px) {
.template-tabs {
flex-direction: column;
}
.variables-list {
grid-template-columns: 1fr;
}
}
</style>
<div class="templates-container">
<h2><?php echo $g5['title']; ?></h2>
<div class="alert alert-info">
<strong>안내:</strong> 예약 관련 자동 발송 <?php echo ($current_type == 'sms') ? '문자' : '이메일'; ?>의 템플릿을 관리합니다.
중괄호 {} 안의 변수들은 실제 데이터로 자동 치환됩니다.
</div>
<!-- 템플릿 탭 -->
<div class="template-nav">
<div class="template-tabs">
<a href="?type=email" class="template-tab <?php echo $current_type == 'email' ? 'active' : ''; ?>">📧 메일 템플릿</a>
<a href="?type=sms" class="template-tab <?php echo $current_type == 'sms' ? 'active' : ''; ?>">📱 문자 템플릿</a>
</div>
<?php if(!empty($templates)): ?>
<div class="template-tabs" style="border-bottom:none; margin-bottom: 10px;">
<?php foreach ($templates as $key => $template): ?>
<a href="?type=<?php echo $current_type; ?>&template=<?php echo $key; ?>"
class="template-tab <?php echo $current_template_key == $key ? 'active' : ''; ?>" style="font-size:14px; padding: 8px 16px;">
<?php echo htmlspecialchars($template['template_name']); ?>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<!-- 사용 가능한 변수 안내 -->
<div class="variables-info">
<div class="variables-title">📝 사용 가능한 변수</div>
<div class="variables-list">
<div class="variable-item">{customer_name} - 고객명</div>
<div class="variable-item">{customer_phone} - 고객 연락처</div>
<div class="variable-item">{customer_email} - 고객 이메일</div>
<div class="variable-item">{reservation_date} - 예약 날짜</div>
<div class="variable-item">{reservation_time} - 예약 시간</div>
<div class="variable-item">{payment_amount} - 상담 비용</div>
<div class="variable-item">{account_info} - 입금 계좌</div>
<div class="variable-item">{cancel_reason} - 취소 사유</div>
</div>
</div>
<!-- 템플릿 편집 폼 -->
<?php if (isset($templates[$current_template_key])): ?>
<?php $template = $templates[$current_template_key]; ?>
<form method="post" class="template-form">
<input type="hidden" name="action" value="save_template">
<input type="hidden" name="template_key" value="<?php echo $current_template_key; ?>">
<input type="hidden" name="template_name" value="<?php echo htmlspecialchars($template['template_name']); ?>">
<input type="hidden" name="template_type" value="<?php echo $current_type; ?>">
<div class="form-group">
<label for="template_subject"><?php echo ($current_type == 'sms') ? '문자 제목 (LMS용)' : '이메일 제목'; ?></label>
<input type="text" id="template_subject" name="template_subject"
value="<?php echo htmlspecialchars($template['template_subject']); ?>"
required>
</div>
<div class="form-group">
<label for="template_content"><?php echo ($current_type == 'sms') ? '문자 내용' : '이메일 내용'; ?></label>
<textarea id="template_content" name="template_content" required><?php echo htmlspecialchars($template['template_content']); ?></textarea>
</div>
<div style="text-align: center; margin-top: 30px;">
<!-- 💡 [개선] 미리보기 버튼을 JS로 생성하는 대신 HTML에 직접 추가하여 안정성을 높이고, 저장 버튼과 나란히 배치합니다. -->
<button type="button" onclick="previewTemplate()" class="btn btn-secondary" style="margin-right: 10px;">미리보기</button>
<button type="submit" class="btn btn-primary">템플릿 저장</button>
<a href="dashboard.php" class="btn btn-secondary">대시보드로</a>
</div>
</form>
<?php endif; ?>
</div>
<script>
// 변수 삽입 도우미
document.addEventListener('DOMContentLoaded', function() {
const textarea = document.getElementById('template_content');
const variableItems = document.querySelectorAll('.variable-item');
variableItems.forEach(item => {
item.style.cursor = 'pointer';
item.title = '클릭하여 템플릿에 삽입';
item.addEventListener('click', function() {
const variable = this.textContent.split(' - ')[0];
const cursorPos = textarea.selectionStart;
const textBefore = textarea.value.substring(0, cursorPos);
const textAfter = textarea.value.substring(cursorPos);
textarea.value = textBefore + variable + textAfter;
textarea.focus();
textarea.setSelectionRange(cursorPos + variable.length, cursorPos + variable.length);
});
});
});
// 템플릿 미리보기
function previewTemplate() {
const subject = document.getElementById('template_subject').value;
const content = document.getElementById('template_content').value;
// 샘플 데이터로 치환
const sampleData = {
'{customer_name}': '홍길동',
'{customer_phone}': '010-1234-5678',
'{customer_email}': 'hong@example.com',
'{reservation_date}': '2024-12-15',
'{reservation_time}': '14:00',
'{payment_amount}': '50,000',
'{account_info}': '국민은행 123-456-789 (주)상담센터',
'{cancel_reason}': '개인 사정'
};
let previewSubject = subject;
let previewContent = content;
for (const [variable, value] of Object.entries(sampleData)) {
previewSubject = previewSubject.replace(new RegExp(variable.replace(/[{}]/g, '\\\\$&'), 'g'), value);
previewContent = previewContent.replace(new RegExp(variable.replace(/[{}]/g, '\\\\$&'), 'g'), value);
}
// 💡 [개선] 실제 이메일처럼 보이도록 nl2br 처리 및 UI 개선
const previewHtmlContent = previewContent.replace(/\n/g, '<br>');
// 💡 [개선] document.write() 대신 DOM 조작을 사용하여 안정성을 높입니다.
const previewWindow = window.open('', '_blank', 'width=800,height=600');
const previewDoc = previewWindow.document;
previewDoc.open();
previewDoc.write(`
<!DOCTYPE html>
<html>
<head>
<title>미리보기</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 0; background-color: #f6f8fa; }
.preview-container { max-width: 800px; margin: 20px auto; background-color: #fff; border: 1px solid #ddd; border-radius: 8px; box-shadow: 0 1px 5px rgba(0,0,0,0.1); }
.preview-header { padding: 20px; border-bottom: 1px solid #eee; }
.preview-header h2 { margin: 0; font-size: 20px; color: #333; }
.preview-meta { padding: 15px 20px; background-color: #fdfdfd; border-bottom: 1px solid #eee; font-size: 14px; }
.meta-item { display: flex; margin-bottom: 8px; }
.meta-label { font-weight: bold; color: #555; width: 80px; }
.meta-value { color: #333; }
.preview-body { padding: 30px 20px; line-height: 1.7; color: #333; font-size: 15px; }
</style>
</head>
<bo`+`dy>
<div class='preview-container'>
<div class="preview-header"><h2>미리보기</h2></div>
<div class="preview-meta">
<div class="meta-item"><span class="meta-label">보내는사람:</span><span class="meta-value">관리자 &lt;admin@example.com&gt;</span></div>
<div class="meta-item"><span class="meta-label">받는사람:</span><span class="meta-value">홍길동 &lt;hong@example.com&gt;</span></div>
<div class="meta-item"><span class="meta-label">제 목:</span><span class="meta-value">${previewSubject}</span></div>
</div>
<div class="preview-body">${previewHtmlContent}</div>
</div>
</bo`+`dy>
</html>
`);
previewWindow.document.close();
}
</script>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
?>
+65
View File
@@ -0,0 +1,65 @@
<?php
/**
* 상담 예약 시스템 테스트 페이지
*/
// 오류 표시 활성화
error_reporting(E_ALL);
ini_set('display_errors', 1);
echo "<h1>상담 예약 시스템 테스트</h1>";
// 1. 그누보드 기본 파일 로드 테스트
echo "<h3>1. 그누보드 기본 파일 로드 테스트</h3>";
try {
include_once('./_common_con.php');
echo "✅ common.php 로드 성공<br>";
echo "✅ 데이터베이스 연결: " . (isset($connect_db) ? "성공" : "실패") . "<br>";
echo "✅ 관리자 권한: " . ($is_admin ? "있음" : "없음") . "<br>";
echo "✅ G5_PATH: " . (defined('G5_PATH') ? G5_PATH : "정의되지 않음") . "<br>";
} catch (Exception $e) {
echo "❌ common.php 로드 실패: " . $e->getMessage() . "<br>";
}
// 2. 함수 존재 확인
echo "<h3>2. 필수 함수 존재 확인</h3>";
$functions = ['sql_query', 'sql_fetch', 'sql_real_escape_string', 'alert'];
foreach ($functions as $func) {
echo (function_exists($func) ? "" : "") . " {$func}<br>";
}
// 3. 상수 확인
echo "<h3>3. 필수 상수 확인</h3>";
$constants = ['G5_PATH', 'G5_ADMIN_PATH', 'G5_DATA_PATH'];
foreach ($constants as $const) {
echo (defined($const) ? "" : "") . " {$const}: " . (defined($const) ? constant($const) : "정의되지 않음") . "<br>";
}
// 4. 데이터베이스 연결 테스트
echo "<h3>4. 데이터베이스 연결 테스트</h3>";
try {
$sql = "SELECT 1 as test";
$result = sql_query($sql);
if ($result) {
echo "✅ 데이터베이스 쿼리 성공<br>";
} else {
echo "❌ 데이터베이스 쿼리 실패<br>";
}
} catch (Exception $e) {
echo "❌ 데이터베이스 오류: " . $e->getMessage() . "<br>";
}
// 5. 테이블 존재 확인
echo "<h3>5. 상담 시스템 테이블 확인</h3>";
$tables = ['consultant_config', 'consultant_schedule', 'consultant_reservations'];
foreach ($tables as $table) {
$sql = "SHOW TABLES LIKE '{$table}'";
$result = sql_query($sql, false);
$exists = $result && sql_num_rows($result) > 0;
echo ($exists ? "" : "") . " {$table}<br>";
}
echo "<hr>";
echo "<p><a href='install_simple.php'>간단 설치 페이지로 이동</a></p>";
echo "<p><a href='../install.php'>원본 설치 페이지로 이동</a></p>";
?>
@@ -0,0 +1,63 @@
<?php
/**
* 상담 예약 시스템 설치 테스트
*/
// 오류 표시 활성화
error_reporting(E_ALL);
ini_set('display_errors', 1);
echo "<h1>상담 예약 시스템 설치 테스트</h1>";
// 1. 그누보드 기본 파일 로드 테스트
echo "<h3>1. 그누보드 기본 파일 로드 테스트</h3>";
try {
include_once('../../common.php');
echo "✅ common.php 로드 성공<br>";
echo "✅ 데이터베이스 연결: " . (isset($connect_db) ? "성공" : "실패") . "<br>";
echo "✅ 관리자 권한: " . ($is_admin ? "있음" : "없음") . "<br>";
} catch (Exception $e) {
echo "❌ common.php 로드 실패: " . $e->getMessage() . "<br>";
}
// 2. _common.php 로드 테스트
echo "<h3>2. _common.php 로드 테스트</h3>";
try {
include_once('./_common.php');
echo "✅ _common.php 로드 성공<br>";
echo "✅ 상담 시스템 버전: " . (defined('G5_CONSULTANT_VERSION') ? G5_CONSULTANT_VERSION : "정의되지 않음") . "<br>";
} catch (Exception $e) {
echo "❌ _common.php 로드 실패: " . $e->getMessage() . "<br>";
}
// 3. 함수 존재 확인
echo "<h3>3. 필수 함수 존재 확인</h3>";
$functions = ['sql_query', 'sql_fetch', 'sql_real_escape_string', 'alert'];
foreach ($functions as $func) {
echo (function_exists($func) ? "" : "") . " {$func}<br>";
}
// 4. 상수 확인
echo "<h3>4. 필수 상수 확인</h3>";
$constants = ['G5_PATH', 'G5_ADMIN_PATH', 'G5_DATA_PATH'];
foreach ($constants as $const) {
echo (defined($const) ? "" : "") . " {$const}: " . (defined($const) ? constant($const) : "정의되지 않음") . "<br>";
}
// 5. 데이터베이스 연결 테스트
echo "<h3>5. 데이터베이스 연결 테스트</h3>";
try {
$sql = "SELECT 1 as test";
$result = sql_query($sql);
if ($result) {
echo "✅ 데이터베이스 쿼리 성공<br>";
} else {
echo "❌ 데이터베이스 쿼리 실패<br>";
}
} catch (Exception $e) {
echo "❌ 데이터베이스 오류: " . $e->getMessage() . "<br>";
}
echo "<hr>";
echo "<p><a href='../install.php'>설치 페이지로 이동</a></p>";
?>
@@ -0,0 +1,191 @@
<?php
/**
* 스케줄 생성 테스트 페이지
*/
include_once('./_common.php');
// 관리자 권한 확인
if (!$is_admin) {
alert('관리자만 접근할 수 있습니다.');
}
// 설치 확인
if (!is_consultant_installed()) {
alert('상담 예약 시스템이 설치되지 않았습니다.', 'install.php');
}
$g5['title'] = '스케줄 생성 테스트';
// 테스트 실행
if ($_POST['action'] == 'test_generation') {
$year = (int) ($_POST['year'] ?? date('Y'));
$month = (int) ($_POST['month'] ?? date('n'));
try {
require_once('classes/ScheduleGenerator.class.php');
$generator = new ScheduleGenerator();
echo "<h3>테스트 결과</h3>";
echo "<p><strong>대상:</strong> {$year}{$month}월</p>";
// 기존 스케줄 확인
$existing = sql_fetch("SELECT COUNT(*) as count FROM consultant_schedule WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month}");
echo "<p><strong>기존 스케줄:</strong> {$existing['count']}개</p>";
// 충돌 검사
// 💡 [수정] 최신 충돌 검사 로직으로 변경
$conflicts = $generator->findConflictsWithNewSettings($year, $month);
echo "<p><strong>충돌 검사:</strong> " . count($conflicts) . "건</p>";
if (!empty($conflicts)) {
echo "<ul>";
foreach ($conflicts as $conflict) {
echo "<li>{$conflict['date']} {$conflict['time']} - {$conflict['customer']} ({$conflict['reason']})</li>";
}
echo "</ul>";
}
// 스케줄 생성
$result = $generator->generateMonth($year, $month);
if ($result) {
$new_count = sql_fetch("SELECT COUNT(*) as count FROM consultant_schedule WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month}");
echo "<p style='color: green;'><strong>✅ 성공:</strong> {$new_count['count']}개 스케줄 생성 완료</p>";
// 생성된 스케줄 샘플 표시
$samples = sql_query("SELECT * FROM consultant_schedule WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month} ORDER BY specific_date, start_time LIMIT 10");
echo "<h4>생성된 스케줄 샘플 (최대 10개)</h4>";
echo "<table border='1' style='border-collapse: collapse; width: 100%;'>";
echo "<tr><th>날짜</th><th>시작시간</th><th>종료시간</th><th>최대인원</th><th>사용가능</th><th>타입</th></tr>";
while ($row = sql_fetch_array($samples)) {
$available = $row['is_available'] ? '가능' : '불가능';
$type = $row['temp_1'] ?? '일반';
echo "<tr>";
echo "<td>{$row['specific_date']}</td>";
echo "<td>{$row['start_time']}</td>";
echo "<td>{$row['end_time']}</td>";
echo "<td>{$row['max_persons']}</td>";
echo "<td>{$available}</td>";
echo "<td>{$type}</td>";
echo "</tr>";
}
echo "</table>";
} else {
echo "<p style='color: red;'><strong>❌ 실패:</strong> 스케줄 생성에 실패했습니다.</p>";
}
} catch (Exception $e) {
echo "<p style='color: red;'><strong>오류:</strong> " . htmlspecialchars($e->getMessage()) . "</p>";
}
echo "<hr>";
}
include_once(G5_ADMIN_PATH . '/admin.head.php');
?>
<style>
.test-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.test-form {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 30px;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input,
.form-group select {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
width: 200px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
table {
margin-top: 15px;
}
th,
td {
padding: 8px;
text-align: left;
border: 1px solid #ddd;
}
th {
background-color: #fff;
}
</style>
<div class="test-container">
<h2><?php echo $g5['title']; ?></h2>
<div class="test-form">
<h3>스케줄 생성 테스트</h3>
<p>선택한 년월의 스케줄을 생성하고 결과를 확인합니다.</p>
<form method="post">
<input type="hidden" name="action" value="test_generation">
<div class="form-group">
<label for="year">년도</label>
<input type="number" id="year" name="year" value="<?php echo date('Y'); ?>" min="2024" max="2030">
</div>
<div class="form-group">
<label for="month">월</label>
<select id="month" name="month">
<?php for ($i = 1; $i <= 12; $i++): ?>
<option value="<?php echo $i; ?>" <?php echo $i == date('n') ? 'selected' : ''; ?>>
<?php echo $i; ?>월
</option>
<?php endfor; ?>
</select>
</div>
<div style="margin-top: 20px;">
<button type="submit" class="btn btn-primary">스케줄 생성 테스트</button>
<a href="../settings.php" class="btn btn-secondary">설정으로 돌아가기</a>
</div>
</form>
</div>
</div>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
?>
@@ -0,0 +1,136 @@
<?php
/**
* ScheduleGenerator 테스트 스크립트
*/
// 기본 설정
define('G5_PATH', realpath('../../'));
include_once(G5_PATH . '/common.php');
include_once('./_common.php');
// 관리자 권한 확인
if (!$is_admin) {
die('관리자만 접근할 수 있습니다.');
}
// 설치 확인
if (!is_consultant_installed()) {
die('상담 예약 시스템이 설치되지 않았습니다.');
}
echo "<h2>ScheduleGenerator 테스트</h2>";
try {
require_once('classes/ScheduleGenerator.class.php');
$generator = new ScheduleGenerator();
// 테스트할 년월
$year = 2024;
$month = 12;
echo "<h3>1. 기본 설정 확인</h3>";
// 기본 설정 확인
$duration = consultant_get_config('consultation_duration', 60);
$maxPersons = consultant_get_config('max_persons_per_slot', 2);
echo "- 상담 시간: {$duration}분<br>";
echo "- 최대 인원: {$maxPersons}명<br>";
echo "<h3>2. 요일별 설정 확인</h3>";
$days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
$dayNames = ['월요일', '화요일', '수요일', '목요일', '금요일', '토요일', '일요일'];
foreach ($days as $i => $day) {
$enabled = consultant_get_config($day . '_enabled', '1');
$start = consultant_get_config($day . '_start', '09:00');
$end = consultant_get_config($day . '_end', '18:00');
$lunchStart = consultant_get_config($day . '_lunch_start', '12:00');
$lunchEnd = consultant_get_config($day . '_lunch_end', '13:00');
echo "- {$dayNames[$i]}: ";
if ($enabled == '1') {
echo "운영 ({$start}~{$end}, 점심: {$lunchStart}~{$lunchEnd})";
} else {
echo "휴무";
}
echo "<br>";
}
echo "<h3>3. 기존 스케줄 확인</h3>";
$existingCount = sql_fetch("SELECT COUNT(*) as count FROM consultant_schedule WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month}");
echo "- 기존 스케줄: {$existingCount['count']}개<br>";
echo "<h3>4. 충돌 검사</h3>";
$conflicts = $generator->checkScheduleConflicts($year, $month);
echo "- 충돌 건수: " . count($conflicts) . "건<br>";
if (!empty($conflicts)) {
echo "<ul>";
foreach ($conflicts as $conflict) {
echo "<li>{$conflict['date']} {$conflict['time']} - {$conflict['customer']} ({$conflict['reason']})</li>";
}
echo "</ul>";
}
echo "<h3>5. 스케줄 생성 테스트</h3>";
$result = $generator->generateMonth($year, $month);
if ($result) {
$newCount = sql_fetch("SELECT COUNT(*) as count FROM consultant_schedule WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month}");
echo "<p style='color: green;'>✅ 성공: {$newCount['count']}개 스케줄 생성 완료</p>";
// 생성된 스케줄 샘플 표시
echo "<h4>생성된 스케줄 샘플 (첫 5일)</h4>";
$samples = sql_query("SELECT * FROM consultant_schedule WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month} ORDER BY specific_date, start_time LIMIT 20");
echo "<table border='1' style='border-collapse: collapse; width: 100%;'>";
echo "<tr><th>날짜</th><th>요일</th><th>시작시간</th><th>종료시간</th><th>최대인원</th><th>사용가능</th><th>타입</th><th>메모</th></tr>";
while ($row = sql_fetch_array($samples)) {
$dayOfWeek = date('N', strtotime($row['specific_date']));
$dayName = $dayNames[$dayOfWeek - 1];
$available = $row['is_available'] ? '가능' : '불가능';
$type = $row['temp_1'] ?? '일반';
$memo = $row['temp_2'] ?? '';
echo "<tr>";
echo "<td>{$row['specific_date']}</td>";
echo "<td>{$dayName}</td>";
echo "<td>{$row['start_time']}</td>";
echo "<td>{$row['end_time']}</td>";
echo "<td>{$row['max_persons']}</td>";
echo "<td>{$available}</td>";
echo "<td>{$type}</td>";
echo "<td>{$memo}</td>";
echo "</tr>";
}
echo "</table>";
// 타입별 통계
echo "<h4>생성된 스케줄 통계</h4>";
$stats = sql_query("SELECT temp_1, COUNT(*) as count FROM consultant_schedule WHERE YEAR(specific_date) = {$year} AND MONTH(specific_date) = {$month} GROUP BY temp_1");
echo "<ul>";
while ($stat = sql_fetch_array($stats)) {
$type = $stat['temp_1'] ?? '일반';
echo "<li>{$type}: {$stat['count']}개</li>";
}
echo "</ul>";
} else {
echo "<p style='color: red;'>❌ 실패: 스케줄 생성에 실패했습니다.</p>";
}
} catch (Exception $e) {
echo "<p style='color: red;'>오류: " . htmlspecialchars($e->getMessage()) . "</p>";
echo "<pre>" . htmlspecialchars($e->getTraceAsString()) . "</pre>";
}
echo "<hr>";
echo "<p><a href='../settings.php'>설정으로 돌아가기</a> | <a href='../schedule.php'>스케줄 관리</a></p>";
?>
+345
View File
@@ -0,0 +1,345 @@
<?php
/**
* 상담 예약 시스템 테스트 페이지
*/
$sub_menu = '850900';
include_once('./_common.php');
// 권한 확인
auth_check_menu($auth, $sub_menu, 'r');
$g5['title'] = '시스템 테스트';
// 테스트 결과
$test_results = [];
// 1. 테이블 존재 확인
$tables_to_check = [
'consultant_config' => '설정 테이블',
'consultant_schedule' => '스케줄 테이블',
'consultant_reservations' => '예약 테이블',
'consultant_mail_templates' => '메일 템플릿 테이블',
'consultant_sms_templates' => 'SMS 템플릿 테이블'
];
foreach ($tables_to_check as $table => $description) {
$sql = "SHOW TABLES LIKE '{$table}'";
$result = sql_query($sql, false);
$exists = $result && sql_num_rows($result) > 0;
$test_results['tables'][$table] = [
'name' => $description,
'status' => $exists,
'message' => $exists ? '존재함' : '존재하지 않음'
];
}
// 2. 기본 설정값 확인
$config_keys = [
'consultation_duration' => '1회 상담시간',
'max_persons_per_slot' => '최대 인원',
'consultation_fee' => '상담비',
'account_info' => '계좌정보',
'monday_enabled' => '월요일 운영여부',
'monday_start' => '월요일 시작시간'
];
foreach ($config_keys as $key => $description) {
$value = consultant_get_config($key);
$test_results['config'][$key] = [
'name' => $description,
'value' => $value,
'status' => $value !== null
];
}
// 3. 함수 테스트
$function_tests = [
'is_consultant_installed' => is_consultant_installed(),
'consultant_get_config' => consultant_get_config('consultation_duration', '60'),
'consultant_format_time' => consultant_format_time('14:30'),
'consultant_format_date' => consultant_format_date('2024-12-02')
];
// 4. 스케줄 생성 테스트 (현재 월)
$current_year = date('Y');
$current_month = date('m');
try {
include_once('./schedule_generator.php');
$generator = new ScheduleGenerator();
$schedule_status = $generator->checkScheduleStatus($current_year, $current_month);
$test_results['schedule_status'] = $schedule_status;
} catch (Exception $e) {
$test_results['schedule_error'] = $e->getMessage();
}
include_once(G5_ADMIN_PATH . '/admin.head.php');
?>
<style>
.test-container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.test-section {
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.test-section h3 {
margin: 0 0 15px 0;
color: #333;
border-bottom: 2px solid #007bff;
padding-bottom: 8px;
}
.test-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #f0f0f0;
}
.test-item:last-child {
border-bottom: none;
}
.test-name {
font-weight: bold;
color: #333;
}
.test-value {
color: #666;
font-family: monospace;
}
.test-status {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}
.status-ok {
background: #d4edda;
color: #155724;
}
.status-error {
background: #f8d7da;
color: #721c24;
}
.status-warning {
background: #fff3cd;
color: #856404;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
display: inline-block;
font-size: 14px;
font-weight: 600;
margin: 5px;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-warning {
background: #ffc107;
color: #212529;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.summary-card {
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
text-align: center;
}
.summary-number {
font-size: 24px;
font-weight: bold;
margin-bottom: 5px;
}
.summary-label {
font-size: 12px;
color: #666;
}
</style>
<div class="test-container">
<h2><?php echo $g5['title']; ?></h2>
<!-- 전체 요약 -->
<div class="summary-grid">
<div class="summary-card">
<div class="summary-number" style="color: <?php echo is_consultant_installed() ? '#28a745' : '#dc3545'; ?>">
<?php echo is_consultant_installed() ? '✓' : '✗'; ?>
</div>
<div class="summary-label">시스템 설치</div>
</div>
<div class="summary-card">
<div class="summary-number" style="color: #007bff;">
<?php echo count(array_filter($test_results['tables'], function ($t) {
return $t['status']; })); ?>/<?php echo count($test_results['tables']); ?>
</div>
<div class="summary-label">테이블 상태</div>
</div>
<div class="summary-card">
<div class="summary-number" style="color: #17a2b8;">
<?php echo count(array_filter($test_results['config'], function ($c) {
return $c['status']; })); ?>/<?php echo count($test_results['config']); ?>
</div>
<div class="summary-label">설정 상태</div>
</div>
<div class="summary-card">
<div class="summary-number" style="color: #6f42c1;">
<?php echo isset($test_results['schedule_status']) ? number_format($test_results['schedule_status']['total_slots']) : '0'; ?>
</div>
<div class="summary-label">현재 월 스케줄</div>
</div>
</div>
<!-- 테이블 상태 -->
<div class="test-section">
<h3>📋 데이터베이스 테이블 상태</h3>
<?php foreach ($test_results['tables'] as $table => $info): ?>
<div class="test-item">
<div class="test-name"><?php echo $info['name']; ?> (<?php echo $table; ?>)</div>
<div class="test-status <?php echo $info['status'] ? 'status-ok' : 'status-error'; ?>">
<?php echo $info['message']; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- 설정 상태 -->
<div class="test-section">
<h3>⚙️ 기본 설정 상태</h3>
<?php foreach ($test_results['config'] as $key => $info): ?>
<div class="test-item">
<div class="test-name"><?php echo $info['name']; ?> (<?php echo $key; ?>)</div>
<div class="test-value"><?php echo htmlspecialchars($info['value'] ?? 'NULL'); ?></div>
<div class="test-status <?php echo $info['status'] ? 'status-ok' : 'status-warning'; ?>">
<?php echo $info['status'] ? '설정됨' : '미설정'; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- 함수 테스트 -->
<div class="test-section">
<h3>🔧 함수 테스트</h3>
<?php foreach ($function_tests as $func_name => $result): ?>
<div class="test-item">
<div class="test-name"><?php echo $func_name; ?>()</div>
<div class="test-value">
<?php echo is_bool($result) ? ($result ? 'true' : 'false') : htmlspecialchars($result); ?></div>
<div class="test-status status-ok">정상</div>
</div>
<?php endforeach; ?>
</div>
<!-- 스케줄 상태 -->
<div class="test-section">
<h3>📅 현재 월 스케줄 상태 (<?php echo $current_year; ?>년 <?php echo $current_month; ?>월)</h3>
<?php if (isset($test_results['schedule_status'])): ?>
<?php $status = $test_results['schedule_status']; ?>
<div class="test-item">
<div class="test-name">전체 슬롯</div>
<div class="test-value"><?php echo number_format($status['total_slots']); ?>개</div>
<div class="test-status <?php echo $status['total_slots'] > 0 ? 'status-ok' : 'status-warning'; ?>">
<?php echo $status['total_slots'] > 0 ? '생성됨' : '미생성'; ?>
</div>
</div>
<div class="test-item">
<div class="test-name">상담 가능 슬롯</div>
<div class="test-value"><?php echo number_format($status['auto_slots']); ?>개</div>
<div class="test-status status-ok">정상</div>
</div>
<div class="test-item">
<div class="test-name">점심시간 슬롯</div>
<div class="test-value"><?php echo number_format($status['lunch_slots']); ?>개</div>
<div class="test-status status-ok">정상</div>
</div>
<div class="test-item">
<div class="test-name">휴무일 슬롯</div>
<div class="test-value"><?php echo number_format($status['holiday_slots']); ?>개</div>
<div class="test-status status-ok">정상</div>
</div>
<?php elseif (isset($test_results['schedule_error'])): ?>
<div class="test-item">
<div class="test-name">스케줄 조회 오류</div>
<div class="test-value"><?php echo htmlspecialchars($test_results['schedule_error']); ?></div>
<div class="test-status status-error">오류</div>
</div>
<?php endif; ?>
</div>
<!-- 액션 버튼 -->
<div class="test-section">
<h3>🚀 빠른 액션</h3>
<div style="text-align: center;">
<?php if (!is_consultant_installed()): ?>
<a href="../install.php" class="btn btn-warning">시스템 설치</a>
<?php endif; ?>
<a href="../settings.php" class="btn btn-primary">설정 관리</a>
<a href="../schedule_generate.php" class="btn btn-success">스케줄 생성</a>
<a href="../dashboard.php" class="btn btn-primary">대시보드</a>
<button onclick="location.reload()" class="btn btn-secondary">새로고침</button>
</div>
</div>
</div>
<script>
// 자동 새로고침 (30초마다)
setTimeout(function () {
location.reload();
}, 30000);
</script>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
?>
@@ -0,0 +1,14 @@
<?php
include_once('./_common.php');
if (!$is_admin) {
alert('관리자만 접근할 수 있습니다.');
}
echo '<div style="padding:20px; border:1px solid #ddd; background:#fff; max-width:600px; margin:50px auto; text-align:center;">';
echo '<h3>알림</h3>';
echo '<p>이 파일은 더 이상 사용되지 않습니다.</p>';
echo '<p>관리자 페이지 > 상담관리 > <strong>설치/업데이트</strong> 메뉴에서 [재설치 (업데이트)] 버튼을 클릭하여 DB를 업데이트해 주세요.</p>';
echo '<a href="install.php" style="display:inline-block; padding:10px 20px; background:#007bff; color:#fff; text-decoration:none; border-radius:5px;">설치/업데이트 페이지로 이동</a>';
echo '</div>';
?>
+7
View File
@@ -0,0 +1,7 @@
<?php
define('G5_IS_ADMIN', true);
include_once('../../common.php');
include_once(G5_ADMIN_PATH . '/admin.lib.php');
$g5['contact_inquiry_table'] = G5_TABLE_PREFIX . 'contact_inquiry';
?>
@@ -0,0 +1,12 @@
<?php
if (!defined('_GNUBOARD_')) exit;
// 810번대 메뉴 (상담문의관리)
if (!isset($menu['menu810'])) $menu['menu810'] = array();
$menu['menu810'] = array_merge($menu['menu810'], array(
array('810000', '상담문의관리', G5_ADMIN_URL.'/contact_inquiry/list.php', 'contact_inquiry_main', 'fa-comments'),
array('810100', '상담문의내역', G5_ADMIN_URL.'/contact_inquiry/list.php', 'contact_inquiry_list'),
array('810900', '솔루션 설치', G5_ADMIN_URL.'/contact_inquiry/install.php', 'contact_inquiry_install'),
));
?>

Some files were not shown because too many files have changed in this diff Show More