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
+13
View File
@@ -0,0 +1,13 @@
<?php
define('G5_IS_ADMIN', true);
include_once ('../../common.php');
include_once(G5_ADMIN_PATH.'/admin.lib.php');
// [추가] 메일 관리 모듈 테이블 정의
$g5['mail_smtp_config_table'] = G5_TABLE_PREFIX.'mail_smtp_config';
$g5['mail_smtp_change_log_table'] = G5_TABLE_PREFIX.'mail_smtp_change_log';
$g5['mail_template_table'] = G5_TABLE_PREFIX.'mail_template';
$g5['mail_template_vars_table'] = G5_TABLE_PREFIX.'mail_template_vars';
$g5['mail_template_change_log_table'] = G5_TABLE_PREFIX.'mail_template_change_log';
$g5['mail_send_log_table'] = G5_TABLE_PREFIX.'mail_send_log';
//add_stylesheet('<link rel="stylesheet" href="'.G5_SMS5_ADMIN_URL.'/css/sms5.css">', 0);
+23
View File
@@ -0,0 +1,23 @@
<?php
if (!defined('_GNUBOARD_')) exit;
$menu = array(
'title' => '메일 관리',
'link' => '',
'submenu' => array(
array(
'title' => 'SMTP 설정',
'link' => G5_ADMIN_URL . '/mail_manage/smtp_config.php',
),
array(
'title' => '메일 템플릿 관리',
'link' => G5_ADMIN_URL . '/mail_manage/template.php',
),
array(
'title' => '메일 발송 이력 관리',
'link' => G5_ADMIN_URL . '/mail_manage/send_log.php',
),
),
);
return $menu;
@@ -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');
@@ -0,0 +1,41 @@
<?php
// 이 파일은 AJAX 요청을 처리하므로 화면 출력이 없어야 합니다.
header('Content-Type: application/json');
// 그누보드 환경을 로드하여 DB 접속 및 보안 기능을 사용합니다.
// [수정] __DIR__ 을 사용하여 현재 파일의 절대 경로를 기준으로 _common.php를 포함합니다.
include_once(__DIR__."/_common.php");
// 최고 관리자만 이 기능에 접근할 수 있도록 제한합니다.
if ($is_admin != 'super') {
echo json_encode(['error' => '접근 권한이 없습니다.']);
exit;
}
// GET 파라미터로 받은 템플릿 코드를 가져옵니다.
$template_code = isset($_GET['code']) ? trim($_GET['code']) : '';
if (!$template_code) {
echo json_encode(['error' => '템플릿 코드가 전송되지 않았습니다.']);
exit;
}
// 템플릿 정보를 다루는 클래스를 포함합니다.
require_once(__DIR__ . '/classes/TemplateManager.php');
$templateManager = new TemplateManager();
// 코드로 템플릿 정보를 조회하여 ID를 얻습니다.
$template = $templateManager->getByCode($template_code);
if (!$template) {
echo json_encode(['error' => "템플릿을 찾을 수 없습니다: " . htmlspecialchars($template_code)]);
exit;
}
// 템플릿 ID를 사용하여 연관된 변수와 기본값들을 가져옵니다.
$variables = $templateManager->getVarsByTemplateId($template['id']);
// 성공적으로 조회된 변수 목록을 JSON 형식으로 반환합니다.
echo json_encode(['success' => true, 'variables' => $variables]);
exit;
+91
View File
@@ -0,0 +1,91 @@
<?php
/**
* 파일명: ajax_resend_mail.php
* 설명: 발송 이력에 있는 실패한 메일을 재발송하는 AJAX API
*/
// [추가] 디버깅을 위한 강력한 오류 처리기
// 모든 종류의 PHP 오류(Fatal Error 포함)를 감지하여 JSON 형식으로 출력합니다.
// 이렇게 하면 JavaScript에서 '서버 응답 오류' 대신 구체적인 오류 메시지를 받을 수 있습니다.
/*ini_set('display_errors', 0); // 오류를 직접 출력하지 않도록 설정
error_reporting(E_ALL);*/
// [수정] 상위 폴더에 있는 _common.php를 포함하도록 경로를 수정합니다.
include_once(__DIR__ . "/_common.php");
header('Content-Type: application/json');
$response = [
'success' => false,
'message' => '알 수 없는 오류가 발생했습니다.'
];
try {
// 1. 권한 및 토큰 확인
if ($is_admin != 'super') {
throw new Exception('최고 관리자만 접근 가능합니다.');
}
// 2. 입력값 확인
// [수정] 단일 ID 대신 ID 배열을 받습니다.
$log_ids = $_POST['ids'] ?? [];
if (empty($log_ids) || !is_array($log_ids)) {
throw new Exception('재발송할 메일을 선택해주세요.');
}
// 3. 클래스 포함 및 객체 생성
// [수정] 상위 폴더의 classes 디렉토리에 있는 클래스 파일을 포함하도록 경로를 수정합니다.
require_once(__DIR__ . '/classes/SendLogManager.php');
require_once(__DIR__ . '/classes/MailSender.php');
$logManager = new SendLogManager();
$mailSender = new MailSender();
$success_count = 0;
$fail_count = 0;
$total_count = count($log_ids);
foreach ($log_ids as $log_id) {
$log_id = (int)$log_id;
if (!$log_id) continue;
// 4. 원본 로그 데이터 가져오기
$original_log = $logManager->getById($log_id);
// [수정] 성공한 메일도 재발송 가능하도록 상태 체크 조건을 제거합니다.
if (!$original_log) {
$fail_count++;
continue;
}
// 5. 메일 재발송 실행
$success = $mailSender->sendRaw(
$original_log['to_email'],
$original_log['subject'],
$original_log['body'],
$log_id // 원본 로그 ID를 전달하여 이력 추적
);
if ($success) {
$success_count++;
} else {
$fail_count++;
}
}
// 최종 결과 메시지 생성
if ($success_count > 0) {
$response['success'] = true;
$message = "{$total_count}건 중 {$success_count}건의 메일을 성공적으로 재발송했습니다.";
if ($fail_count > 0) {
$message .= "\n{$fail_count}건은 실패했습니다. 발송 이력을 확인하세요.";
}
$response['message'] = $message;
} else {
// MailSender에서 이미 로그를 남기므로, 간단한 실패 메시지만 전달
throw new Exception("{$total_count}건의 메일 재발송에 모두 실패했습니다.");
}
} catch (Exception $e) {
$response['message'] = $e->getMessage();
}
echo json_encode($response);
exit;
+103
View File
@@ -0,0 +1,103 @@
<?php
/**
* 파일명: ajax_universal_send.php
* 설명: AJAX 요청을 받아 MailSender 클래스를 통해 템플릿 기반 메일을 발송하는 범용 API
*/
// 💡 [수정] AJAX 파일이 직접 호출될 때 경로 문제를 원천적으로 해결하기 위해,
// 그누보드 최상위 common.php 파일을 먼저 포함합니다.
// __DIR__ 를 사용하여 현재 파일의 위치를 기준으로 절대 경로를 만듭니다.
define('G5_IS_ADMIN', true);
include_once(__DIR__.'/../../common.php');
// common.php가 포함된 후에는 G5_ADMIN_PATH 상수를 사용할 수 있습니다.
// include_once(G5_ADMIN_PATH.'/admin.lib.php');
// 이제 나머지 코드를 실행합니다.
header('Content-Type: application/json');
// 💡 [추가] 메일 발송에 필요한 클래스를 안전하게 포함합니다.
// 이 코드는 common.php가 로드된 후에 실행되므로 G5_PHPMAILER_PATH 같은 상수를 사용할 수 있습니다.
require_once(__DIR__ . '/classes/MailSender.php');
// --- 입력 데이터 처리 ---
$template_code = trim($_POST['template_code'] ?? '');
$to_email = $_POST['to_email'] ?? ''; // 수신자 이메일 (문자열 또는 배열)
$cc_email = $_POST['cc_email'] ?? []; // 참조 이메일 (문자열 또는 배열)
$bcc_email = $_POST['bcc_email'] ?? []; // 숨은 참조 이메일 (문자열 또는 배열)
$vars = $_POST['variables'] ?? []; // 템플릿에 치환될 변수 배열
$response = [
'success' => false,
'message' => '알 수 없는 오류가 발생했습니다.'
];
try {
// 템플릿 코드는 필수입니다.
if (empty($template_code)) {
throw new Exception('메일 발송에 필요한 템플릿 코드가 지정되지 않았습니다.');
}
// 수신자 이메일($to_email)이 지정되지 않은 경우, 사이트의 최고 관리자에게 발송합니다.
// (예: 홈페이지 문의 양식)
if (empty($to_email)) {
global $config;
$to_email = $config['cf_admin_email'];
if (empty($to_email)) {
throw new Exception('수신자 이메일이 지정되지 않았고, 사이트 기본 관리자 이메일도 설정되지 않았습니다.');
}
}
// [추가] 클라이언트에서 'ADMIN_EMAIL'이라는 특별한 값을 보내면,
// 서버 설정(config.php)에 저장된 실제 관리자 이메일로 치환합니다.
// 이렇게 하면 클라이언트 JS 소스코드에 관리자 이메일이 노출되지 않습니다.
global $config;
$admin_email = $config['cf_admin_email'];
// cc_email 처리
if (!empty($cc_email)) {
if (is_array($cc_email)) {
foreach ($cc_email as $key => $email) {
if ($email === 'ADMIN_EMAIL') {
$cc_email[$key] = $admin_email;
}
}
} else if ($cc_email === 'ADMIN_EMAIL') {
$cc_email = $admin_email;
}
}
// bcc_email 처리
if (!empty($bcc_email)) {
if (is_array($bcc_email)) {
foreach ($bcc_email as $key => $email) {
if ($email === 'ADMIN_EMAIL') {
$bcc_email[$key] = $admin_email;
}
}
} else if ($bcc_email === 'ADMIN_EMAIL') {
$bcc_email = $admin_email;
}
}
// --- 메일 발송 실행 ---
$mailSender = new MailSender();
// send 메서드 시그니처 변경에 맞춰 인자 전달
$success = $mailSender->send($template_code, $to_email, $vars, $cc_email, $bcc_email);
if ($success) {
$response['success'] = true;
$response['message'] = '요청하신 메일이 성공적으로 발송되었습니다.';
} else {
// MailSender 클래스 내부에서 이미 로그를 기록하므로, 사용자에게는 간단한 메시지만 전달합니다.
throw new Exception('메일 발송에 실패했습니다. 시스템 로그를 확인해주세요.');
}
} catch (Exception $e) {
// 예외 발생 시 에러 메시지를 응답에 담습니다.
$response['message'] = $e->getMessage();
}
// 최종 결과를 JSON 형태로 출력합니다.
echo json_encode($response);
exit;
+13
View File
@@ -0,0 +1,13 @@
.tbl_frm01 {
width: 100%;
border-collapse: collapse;
}
.tbl_frm01 th, .tbl_frm01 td {
border: 1px solid #ccc;
padding: 8px;
}
.tbl_frm01 th {
background: #eee;
}
+40
View File
@@ -0,0 +1,40 @@
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.preview-btn').forEach(btn => {
btn.addEventListener('click', () => {
const base64 = btn.getAttribute('data-content');
const decodedHtml = atob(base64);
previewTemplate(decodedHtml);
});
});
});
function previewTemplate(htmlContent) {
const popup = window.open('', '미리보기', 'width=800,height=600,scrollbars=yes');
if (!popup) {
alert('팝업이 차단되었습니다. 브라우저의 팝업 차단을 해제하고 다시 시도해 주세요.');
return;
}
const popupContent = `
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>템플릿 미리보기</title>
<style>
body { font-family: sans-serif; padding: 20px; line-height: 1.6; }
</style>
</head>
<body>
${htmlContent}
</body>
</html>
`;
popup.document.write(popupContent);
popup.document.close();
}
+105
View File
@@ -0,0 +1,105 @@
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('fmaillog');
if (!form) return;
const tableWrap = document.querySelector('.tbl_wrap');
// [수정] AJAX URL을 PHP에서 정의한 전역 변수(ajax_resend_url)로 변경하여 경로 오류를 해결합니다.
const resend_url = ajax_resend_url;
const token = form.querySelector('input[name="token"]')?.value || '';
// 전체 선택/해제
const chkAll = document.getElementById('chkall');
if (chkAll) {
chkAll.addEventListener('click', function() {
const checkboxes = form.querySelectorAll('input[name="chk[]"]:not(:disabled)');
checkboxes.forEach(checkbox => {
checkbox.checked = this.checked;
});
});
}
// 선택 재발송 버튼
const bulkResendBtn = document.getElementById('bulk-resend-btn');
if (bulkResendBtn) {
bulkResendBtn.addEventListener('click', function() {
const checkedItems = form.querySelectorAll('input[name="chk[]"]:checked');
if (checkedItems.length === 0) {
alert('재발송할 항목을 하나 이상 선택해주세요.');
return;
}
if (confirm(`선택된 ${checkedItems.length}개의 메일을 재발송하시겠습니까?`)) {
const ids = Array.from(checkedItems).map(cb => cb.value);
resendMails(ids, this);
}
});
}
// 개별 재발송 및 미리보기 (이벤트 위임)
tableWrap.addEventListener('click', function(e) {
const target = e.target;
// 개별 재발송
if (target.classList.contains('resend-btn')) {
const logId = target.dataset.id;
if (logId && confirm('이 메일을 재발송하시겠습니까?')) {
resendMails([logId], target);
}
}
// 미리보기
if (target.classList.contains('preview-btn')) {
const content = target.dataset.content;
if (content) {
try {
const decodedContent = decodeURIComponent(atob(content).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
const popup = window.open('', '메일 미리보기', 'width=800,height=700,scrollbars=yes');
popup.document.write(decodedContent);
popup.document.close();
} catch (error) {
alert('미리보기 내용을 여는 데 실패했습니다.');
console.error('Base64 디코딩 오류:', error);
}
}
}
});
// 메일 재발송 실행 함수
function resendMails(ids, buttonElement) {
const originalText = buttonElement.textContent;
buttonElement.disabled = true;
buttonElement.textContent = '전송중...';
const formData = new FormData();
ids.forEach(id => formData.append('ids[]', id));
formData.append('token', token);
fetch(resend_url, {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) throw new Error('서버 응답 오류');
return response.json();
})
.then(data => {
// [디버깅 개선] alert()는 긴 메시지를 자를 수 있으므로, console.log로 전체 메시지를 확인합니다.
console.log('서버 응답:', data);
alert(data.message);
if (data.success) {
window.location.reload();
} else {
buttonElement.disabled = false;
buttonElement.textContent = originalText;
}
})
.catch(error => {
console.error('재발송 오류:', error);
alert('재발송 처리 중 오류가 발생했습니다.');
buttonElement.disabled = false;
buttonElement.textContent = originalText;
});
}
});
@@ -0,0 +1,89 @@
document.addEventListener('DOMContentLoaded', function() {
const templateSelect = document.getElementById('template_code');
const varContainer = document.getElementById('variable_fields');
if (templateSelect) {
templateSelect.addEventListener('change', function() {
const selectedCode = this.value;
varContainer.innerHTML = ''; // 기존 필드 초기화
varContainer.style.display = 'none'; // 컨테이너 숨김
if (!selectedCode) {
return;
}
// AJAX 요청으로 선택된 템플릿의 변수 가져오기
fetch(`./ajax_get_template_vars.php?code=${selectedCode}`)
.then(response => {
// [개선] 네트워크 오류 발생 시, 더 명확한 에러를 throw합니다.
if (!response.ok) {
throw new Error('네트워크 응답이 올바르지 않습니다.');
}
return response.json();
})
.then(data => {
if (data.error) {
alert(data.error);
return;
}
if (data.success && Object.keys(data.variables).length > 0) {
// [개선] 테이블 구조를 명확하게 생성합니다.
const table = document.createElement('table');
const colgroup = document.createElement('colgroup');
const col1 = document.createElement('col');
col1.className = 'grid_4';
const col2 = document.createElement('col');
colgroup.appendChild(col1);
colgroup.appendChild(col2);
table.appendChild(colgroup);
const tbody = document.createElement('tbody');
// 제목 행 추가
const caption = document.createElement('caption');
caption.textContent = '치환 변수 값 입력';
caption.className = 'sound_only'; // 스크린리더만 읽도록 처리
table.appendChild(caption);
for (const varName in data.variables) {
const defaultValue = data.variables[varName];
const tr = document.createElement('tr');
const th = document.createElement('th');
th.scope = 'row';
const label = document.createElement('label');
// [개선] label의 for 속성값에 특수문자가 없도록 varName을 사용합니다.
label.htmlFor = `variable_${varName}`;
label.textContent = `{${varName}}`;
th.appendChild(label);
const td = document.createElement('td');
const input = document.createElement('input');
input.type = 'text';
input.name = `variables[${varName}]`;
input.id = `variable_${varName}`;
input.className = 'frm_input';
input.style.width = '95%';
input.value = defaultValue;
input.placeholder = `${varName}에 치환될 값을 입력하세요`;
td.appendChild(input);
tr.appendChild(th);
tr.appendChild(td);
tbody.appendChild(tr);
}
table.appendChild(tbody);
varContainer.appendChild(table);
varContainer.style.display = 'block';
}
})
.catch(error => {
console.error('변수 정보를 가져오는 중 오류가 발생했습니다:', error);
alert('변수 정보를 가져오는 데 실패했습니다. 개발자 콘솔을 확인해주세요.');
});
});
}
});
+155
View File
@@ -0,0 +1,155 @@
////////////////////////////// 템플릿폼
// 이메일 내용에서 {변수명}을 감지하여 입력 필드를 생성/제거하는 함수
function detectVariables(content) {
if (!content) return;
// [수정] 정규식의 \w가 한글을 포함하지 못하므로, [a-zA-Z0-9_가-힣]로 변경하여 한글 변수도 감지하도록 합니다.
const matches = [...new Set((content.match(/{([a-zA-Z0-9_가-힣]+)}/g) || []))];
const container = document.getElementById('variableInputs');
const existingVars = new Set();
const newVars = new Set(matches);
container.querySelectorAll('input[data-var]').forEach(input => {
existingVars.add(`{${input.dataset.var}}`);
});
// 새로 발견된 변수는 입력 필드 추가
newVars.forEach(tag => {
if (!existingVars.has(tag)) {
const varName = tag.replace(/[{}]/g, '');
const label = document.createElement('label');
label.textContent = varName;
const input = document.createElement('input');
input.type = 'text';
input.className = 'frm_input';
input.name = `variables[${varName}]`;
input.dataset.var = varName;
input.value = (typeof serverVars !== 'undefined' && serverVars[varName]) ? serverVars[varName] : '';
input.style.width = '95%';
input.placeholder = `${varName}의 기본값 입력`;
container.appendChild(label);
container.appendChild(input);
}
});
// 내용에서 삭제된 변수는 입력 필드 제거
existingVars.forEach(tag => {
if (!newVars.has(tag)) {
const varName = tag.replace(/[{}]/g, '');
const inputToRemove = container.querySelector(`input[data-var="${varName}"]`);
if (inputToRemove) {
if (inputToRemove.previousElementSibling && inputToRemove.previousElementSibling.tagName === 'LABEL') {
inputToRemove.previousElementSibling.remove();
}
inputToRemove.remove();
}
}
});
}
// '변수 새로고침' 버튼 클릭 시 실행될 함수
function refreshVariables() {
var content = '';
// 1. 스마트 에디터 2.0
if (typeof oEditors !== 'undefined' && oEditors.getById['content']) {
content = oEditors.getById['content'].getContents();
}
// 2. CHEditor5
else if (typeof ed_content !== 'undefined') {
content = ed_content.outputBodyHTML();
}
else {
alert('에디터가 로드되지 않았거나 지원하지 않는 에디터입니다.');
return;
}
detectVariables(content);
alert('변수 목록을 새로고침했습니다.');
}
// 미리보기 기능
function previewTemplate() {
let html = '';
// 1. 스마트 에디터 2.0
if (typeof oEditors !== 'undefined' && oEditors.getById['content']) {
html = oEditors.getById['content'].getContents();
}
// 2. CHEditor5
else if (typeof ed_content !== 'undefined') {
html = ed_content.outputBodyHTML();
}
else {
alert('에디터가 로드되지 않았습니다.');
return;
}
const variableInputs = document.querySelectorAll('#variableInputs input');
variableInputs.forEach(input => {
const varName = input.dataset.var;
const value = input.value;
if (value) {
const regex = new RegExp('{' + varName.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '}', 'g');
html = html.replace(regex, value);
}
});
const popup = window.open('', '미리보기', 'width=800,height=600,scrollbars=yes');
popup.document.write('<html><head><title>템플릿 미리보기</title></head><body>' + html + '</body></html>');
popup.document.close();
}
// 폼 전송 시 에디터 내용 업데이트 및 유효성 검사
function form_check(f) {
// 1. 스마트 에디터 2.0
if (typeof oEditors !== 'undefined' && oEditors.getById['content']) {
oEditors.getById['content'].exec("UPDATE_CONTENTS_FIELD", []);
sanitizeEditorContent();
}
// 2. CHEditor5
else if (typeof ed_content !== 'undefined') {
f.content.value = ed_content.outputBodyHTML();
}
// 에디터 내용이 비어있는지 체크
const content = jQuery.trim(f.content.value);
const emptyPatterns = ['', '<p>&nbsp;</p>', '<p><br></p>', '<div><br></div>', '&nbsp;'];
// 태그 제거 후 공백 체크
const textContent = content.replace(/<[^>]*>?/gm, '').trim();
if (textContent === '' && emptyPatterns.includes(content.toLowerCase())) {
alert('내용을 입력해 주십시오.');
if (typeof oEditors !== 'undefined') {
oEditors.getById['content'].exec("FOCUS");
} else if (typeof ed_content !== 'undefined') {
ed_content.returnFalse();
} else {
f.content.focus();
}
return false;
}
return true;
}
// 에디터 내용 정제 - 빈 값 판별 및 공백 처리 (스마트 에디터용)
function sanitizeEditorContent() {
if (typeof oEditors === 'undefined' || !oEditors.getById['content']) return;
const contentEl = document.getElementById('content');
const html = oEditors.getById['content'].getIR();
oEditors.getById['content'].exec('UPDATE_CONTENTS_FIELD', []);
const val = contentEl.value.toLowerCase().replace(/^\s*|\s*$/g, '');
const emptyPatterns = ['&nbsp;', '<p>&nbsp;</p>', '<p><br></p>', '<div><br></div>', '<p></p>', '<br>', ''];
if (emptyPatterns.includes(val)) {
contentEl.value = '';
}
}
+209
View File
@@ -0,0 +1,209 @@
document.addEventListener("DOMContentLoaded", function () {
// 에디터 로드 대기 (setTimeout으로 안정성 확보)
setTimeout(function() {
// 1. 스마트 에디터 2.0 (SmartEditor2)
if (typeof oEditors !== 'undefined' && oEditors.getById['content']) {
detectVariables(oEditors.getById['content'].getContents());
// '이메일 내용' 에디터의 플레이스홀더 기능 구현
const contentEditor = oEditors.getById['content'];
const placeholderHTML = '<p>이곳에 메일 본문 내용을 입력하세요.</p>';
// 에디터의 실제 body 요소를 가져옵니다.
try {
const editorBody = contentEditor.oApp.getWYSIWYGDocument().body;
// 에디터에 포커스가 갔을 때 (클릭 또는 탭)
const handleFocus = function() {
const currentHTML = contentEditor.getContents().trim();
if (currentHTML === placeholderHTML) {
contentEditor.setContents('');
}
};
// 에디터에서 포커스가 벗어났을 때
const handleBlur = function() {
const currentIR = contentEditor.getIR().trim();
if (currentIR === '' || currentIR === '<p>&nbsp;</p>' || currentIR === '<p><br></p>') {
contentEditor.setContents(placeholderHTML);
}
};
if(editorBody) {
editorBody.addEventListener('focus', handleFocus);
editorBody.addEventListener('blur', handleBlur);
}
} catch(e) {
console.log("SmartEditor2 body access failed: " + e.message);
}
}
// 2. CHEditor5
else if (typeof ed_content !== 'undefined') {
// CHEditor는 로드 완료 후 실행해야 하므로, 에디터 객체의 메서드를 사용하거나 잠시 대기
// 여기서는 이미 로드되었다고 가정하고 내용 가져오기 시도
var content = ed_content.outputBodyHTML();
detectVariables(content);
}
}, 1000); // CHEditor 로딩 시간을 고려하여 1초로 늘림
});
// 이메일 내용에서 {변수명}을 감지하여 입력 필드를 생성/제거하는 함수
function detectVariables(content) {
if (!content) return;
// [수정] 정규식의 \w가 한글을 포함하지 못하므로, [a-zA-Z0-9_가-힣]로 변경하여 한글 변수도 감지하도록 합니다.
const matches = [...new Set((content.match(/{([a-zA-Z0-9_가-힣]+)}/g) || []))];
const container = document.getElementById('variableInputs');
const existingVars = new Set();
const newVars = new Set(matches);
container.querySelectorAll('input[data-var]').forEach(input => {
existingVars.add(`{${input.dataset.var}}`);
});
// 새로 발견된 변수는 입력 필드 추가
newVars.forEach(tag => {
if (!existingVars.has(tag)) {
const varName = tag.replace(/[{}]/g, '');
const label = document.createElement('label');
label.textContent = varName;
const input = document.createElement('input');
input.type = 'text';
input.className = 'frm_input';
input.name = `variables[${varName}]`;
input.dataset.var = varName;
// serverVars는 template_form.php에서 PHP 변수를 JS로 변환하여 제공합니다.
input.value = (typeof serverVars !== 'undefined' && serverVars[varName]) ? serverVars[varName] : '';
input.style.width = '95%';
input.placeholder = `${varName}의 기본값 입력`;
container.appendChild(label);
container.appendChild(input);
}
});
// 내용에서 삭제된 변수는 입력 필드 제거
existingVars.forEach(tag => {
if (!newVars.has(tag)) {
const varName = tag.replace(/[{}]/g, '');
const inputToRemove = container.querySelector(`input[data-var="${varName}"]`);
if (inputToRemove) {
if (inputToRemove.previousElementSibling && inputToRemove.previousElementSibling.tagName === 'LABEL') {
inputToRemove.previousElementSibling.remove();
}
inputToRemove.remove();
}
}
});
}
// '변수 새로고침' 버튼 클릭 시 실행될 함수
function refreshVariables() {
var content = '';
// 1. 스마트 에디터 2.0
if (typeof oEditors !== 'undefined' && oEditors.getById['content']) {
content = oEditors.getById['content'].getContents();
}
// 2. CHEditor5
else if (typeof ed_content !== 'undefined') {
content = ed_content.outputBodyHTML();
}
else {
alert('에디터가 로드되지 않았거나 지원하지 않는 에디터입니다.');
return;
}
detectVariables(content);
alert('변수 목록을 새로고침했습니다.');
}
// 미리보기 기능
function previewTemplate() {
let headerHtml = '';
let bodyHtml = '';
let footerHtml = '';
// 1. 스마트 에디터 2.0
if (typeof oEditors !== 'undefined' && oEditors.getById['content']) {
headerHtml = oEditors.getById['header_html'] ? oEditors.getById['header_html'].getContents() : '';
bodyHtml = oEditors.getById['content'].getContents();
footerHtml = oEditors.getById['footer_html'] ? oEditors.getById['footer_html'].getContents() : '';
}
// 2. CHEditor5
else if (typeof ed_content !== 'undefined') {
headerHtml = typeof ed_header_html !== 'undefined' ? ed_header_html.outputBodyHTML() : '';
bodyHtml = ed_content.outputBodyHTML();
footerHtml = typeof ed_footer_html !== 'undefined' ? ed_footer_html.outputBodyHTML() : '';
}
else {
alert('에디터가 로드되지 않았습니다.');
return;
}
const variableInputs = document.querySelectorAll('#variableInputs input');
// 입력된 모든 변수 테스트 값을 헤더, 본문, 푸터에 각각 적용합니다.
variableInputs.forEach(input => {
const varName = input.dataset.var;
const value = input.value;
if (value) {
// 정규식에 'g' 플래그를 사용하여 문서 전체의 변수를 모두 치환합니다.
const regex = new RegExp('{' + varName.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '}', 'g');
headerHtml = headerHtml.replace(regex, value);
bodyHtml = bodyHtml.replace(regex, value);
footerHtml = footerHtml.replace(regex, value);
}
});
// 헤더 + 본문 + 푸터를 합쳐서 최종 HTML을 만듭니다.
const finalHtml = headerHtml + bodyHtml + footerHtml;
const popup = window.open('', '미리보기', 'width=800,height=600,scrollbars=yes');
popup.document.write(finalHtml);
popup.document.close();
}
// 폼 전송 시 에디터 내용 업데이트 및 유효성 검사
function form_check(f) {
// 1. 스마트 에디터 2.0
if (typeof oEditors !== 'undefined' && oEditors.getById['content']) {
oEditors.getById['header_html'].exec("UPDATE_CONTENTS_FIELD", []);
oEditors.getById['content'].exec("UPDATE_CONTENTS_FIELD", []);
oEditors.getById['footer_html'].exec("UPDATE_CONTENTS_FIELD", []);
}
// 2. CHEditor5
else if (typeof ed_content !== 'undefined') {
// CHEditor는 outputBodyHTML()로 내용을 가져와서 textarea에 넣어줘야 함
if(typeof ed_header_html !== 'undefined') f.header_html.value = ed_header_html.outputBodyHTML();
f.content.value = ed_content.outputBodyHTML();
if(typeof ed_footer_html !== 'undefined') f.footer_html.value = ed_footer_html.outputBodyHTML();
}
else {
// 에디터가 없거나 로드되지 않은 경우 (일반 textarea 사용 시)
// 별도 처리 없음
}
// 에디터 내용이 비어있는지 체크
const content = f.content.value.trim();
const emptyPatterns = ['', '<p>&nbsp;</p>', '<p><br></p>', '<div><br></div>', '&nbsp;'];
// 태그 제거 후 공백 체크
const textContent = content.replace(/<[^>]*>?/gm, '').trim();
if (textContent === '' && emptyPatterns.includes(content.toLowerCase())) {
alert('내용을 입력해 주십시오.');
if (typeof oEditors !== 'undefined') {
oEditors.getById['content'].exec("FOCUS");
} else if (typeof ed_content !== 'undefined') {
ed_content.returnFalse();
} else {
f.content.focus();
}
return false;
}
return true;
}
@@ -0,0 +1,32 @@
document.addEventListener('DOMContentLoaded', function() {
const previewButtons = document.querySelectorAll('.preview-btn');
previewButtons.forEach(button => {
button.addEventListener('click', function() {
// data-content 속성에 base64로 인코딩된 HTML 컨텐츠를 가져옵니다.
const encodedContent = this.dataset.content;
console.log(encodedContent);
if (!encodedContent) {
alert('미리보기할 내용이 없습니다.');
return;
}
try {
// base64로 인코딩된 HTML 컨텐츠를 디코딩합니다.
// atob()는 base64 문자열을 디코딩하고, decodeURIComponent/escape 트릭으로 UTF-8 문자열을 올바르게 처리합니다.
const decodedContent = decodeURIComponent(atob(encodedContent).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
const popup = window.open('', '미리보기', 'width=800,height=600,scrollbars=yes');
// [수정] decodedContent는 이미 완성된 HTML 문서이므로, 추가적인 태그로 감싸지 않고 그대로 출력합니다.
// 이렇게 해야 폼 페이지의 미리보기와 동일하게 작동합니다.
popup.document.write(decodedContent);
popup.document.close();
} catch (e) {
console.error('미리보기 컨텐츠 디코딩 오류:', e);
alert('미리보기 내용을 여는 데 실패했습니다.');
}
});
});
});
+233
View File
@@ -0,0 +1,233 @@
<?php
if (!defined('_GNUBOARD_')) exit;
// PHPMailer 라이브러리 및 필요한 Manager 클래스들을 포함합니다.
require_once(G5_PHPMAILER_PATH . '/PHPMailerAutoload.php');
require_once(__DIR__ . '/SmtpConfigManager.php');
require_once(__DIR__ . '/TemplateManager.php');
require_once(__DIR__ . '/SendLogManager.php');
/**
* Class MailSender
* SMTP 설정과 템플릿을 사용하여 메일을 발송하고 로그를 기록하는 클래스
*/
class MailSender {
/**
* 지정된 템플릿으로 메일을 발송합니다.
* @param string $template_code 발송할 템플릿의 고유 코드
* @param string|array $to_email 수신자 이메일 주소 (문자열 또는 배열)
* @param array $vars 템플릿의 변수를 치환할 값 배열
* @param string|array $cc_email 참조 이메일 주소 (문자열 또는 배열)
* @param string|array $bcc_email 숨은 참조 이메일 주소 (문자열 또는 배열)
* @return bool 발송 성공 여부
*/
public function send($template_code, $to_email, $vars = [], $cc_email = [], $bcc_email = []) {
$logManager = new SendLogManager();
// 로그 기록을 위해 배열을 문자열로 변환
$to_email_log = is_array($to_email) ? implode(', ', $to_email) : $to_email;
$cc_email_log = is_array($cc_email) ? implode(', ', $cc_email) : $cc_email;
$bcc_email_log = is_array($bcc_email) ? implode(', ', $bcc_email) : $bcc_email;
$log_data = [
'to_email' => $to_email_log,
'cc_email' => $cc_email_log,
'bcc_email_log' => $bcc_email_log,
'subject' => '',
'body' => '',
'status' => 'fail', // 기본 상태를 '실패'로 설정
'error_msg' => '',
];
try {
// 1. 사용중인 SMTP 설정 가져오기
$smtpManager = new SmtpConfigManager();
$smtp_config = $smtpManager->getInUse();
if (!$smtp_config) {
throw new Exception('사용 가능한 SMTP 설정이 없습니다.');
}
// 2. 템플릿 정보 가져오기
$templateManager = new TemplateManager();
$template = $templateManager->getByCode($template_code);
if (!$template) {
throw new Exception("템플릿 코드 '{$template_code}'를 찾을 수 없습니다.");
}
if (!$template['is_use']) {
throw new Exception("템플릿 '{$template['title']}'은(는) 현재 사용 중지 상태입니다.");
}
// 3. 변수 치환
$subject = $template['title'];
$header = $template['header_html'];
$content = $template['content'];
$footer = $template['footer_html'];
// 템플릿에 저장된 기본값과 사용자 입력값을 병합 (사용자 입력값이 우선)
$default_vars = $templateManager->getVarsByTemplateId($template['id']);
$final_vars = array_merge($default_vars, $vars);
// 모든 변수를 제목, 헤더, 본문, 푸터에 적용
foreach ($final_vars as $key => $value) {
$search = '{' . $key . '}';
$subject = str_replace($search, $value, $subject);
$header = str_replace($search, $value, $header);
$content = str_replace($search, $value, $content);
$footer = str_replace($search, $value, $footer);
}
$full_html = $header . $content . $footer;
$log_data['subject'] = $subject;
$log_data['body'] = $full_html;
// 4. PHPMailer 설정 및 발송
$mail = new PHPMailer;
$mail->isSMTP();
$mail->CharSet = 'utf-8';
$mail->Host = $smtp_config['host'];
$mail->SMTPAuth = true;
$mail->Username = $smtp_config['username'];
$mail->Password = $smtp_config['password'];
$mail->SMTPSecure = $smtp_config['encryption'];
$mail->Port = (int)$smtp_config['port'];
$mail->setFrom($smtp_config['from_email'], get_text(htmlspecialchars_decode(stripslashes($smtp_config['from_name']))));
// 수신자 추가 (배열 또는 콤마 구분 문자열 지원)
$this->addAddresses($mail, $to_email, 'addAddress');
// 참조자 추가
$this->addAddresses($mail, $cc_email, 'addCC');
// 숨은 참조자 추가
$this->addAddresses($mail, $bcc_email, 'addBCC');
$mail->isHTML(true);
$mail->Subject = htmlspecialchars_decode(stripslashes(get_text($subject)));
$mail->Body = htmlspecialchars_decode(stripslashes($full_html));
if (!$mail->send()) {
throw new Exception('Mailer Error: ' . $mail->ErrorInfo);
}
// 성공 시 로그 데이터 업데이트
$log_data['status'] = 'success';
$log_data['error_msg'] = '';
} catch (Exception $e) {
// 실패 시 오류 메시지 기록
$log_data['error_msg'] = $e->getMessage();
} finally {
// 5. 성공/실패 여부에 관계없이 발송 로그 기록
$logManager->addLog($log_data);
}
return $log_data['status'] === 'success';
}
/**
* 템플릿을 사용하지 않고, 이미 완성된 제목과 본문으로 메일을 발송합니다. (재발송 기능용)
* @param string|array $to_email 수신자 이메일 주소
* @param string $subject 메일 제목
* @param string $body 메일 본문 (HTML)
* @param int|null $original_log_id 재발송의 대상이 되는 원본 로그 ID
* @param string|array $cc_email 참조 이메일
* @param string|array $bcc_email 숨은 참조 이메일
* @return bool 발송 성공 여부
*/
public function sendRaw($to_email, $subject, $body, $original_log_id = null, $cc_email = [], $bcc_email = []) {
$logManager = new SendLogManager();
$to_email_log = is_array($to_email) ? implode(', ', $to_email) : $to_email;
$cc_email_log = is_array($cc_email) ? implode(', ', $cc_email) : $cc_email;
$log_data = [
'to_email' => $to_email_log,
'cc_email' => $cc_email_log,
'subject' => $subject,
'body' => $body,
'status' => 'fail', // 기본 상태를 '실패'로 설정
'error_msg' => '',
'resend_of' => $original_log_id // 재발송 로그 ID 기록
];
try {
// 1. 사용중인 SMTP 설정 가져오기
$smtpManager = new SmtpConfigManager();
$smtp_config = $smtpManager->getInUse();
if (!$smtp_config) {
throw new Exception('사용 가능한 SMTP 설정이 없습니다.');
}
// 2. 변수 치환 과정은 생략
// 3. PHPMailer 설정 및 발송
$mail = new PHPMailer;
$mail->isSMTP();
$mail->CharSet = 'utf-8';
$mail->Host = $smtp_config['host'];
$mail->SMTPAuth = true;
$mail->Username = $smtp_config['username'];
$mail->Password = $smtp_config['password'];
$mail->SMTPSecure = $smtp_config['encryption'];
$mail->Port = (int)$smtp_config['port'];
// [보안/개선] 발신자 이름에 포함될 수 있는 HTML 태그를 제거합니다.
$from_name = get_text(htmlspecialchars_decode(stripslashes($smtp_config['from_name'])));
$mail->setFrom($smtp_config['from_email'], $from_name);
// 수신자, 참조, 숨은참조 추가
$this->addAddresses($mail, $to_email, 'addAddress');
$this->addAddresses($mail, $cc_email, 'addCC');
$this->addAddresses($mail, $bcc_email, 'addBCC');
$mail->isHTML(true);
// [보안/개선] 로그에 저장된 제목과 본문은 이미 완성된 상태이므로 불필요한 함수 적용을 최소화합니다.
$mail->Subject = htmlspecialchars_decode(stripslashes(get_text($subject)));
$mail->Body = htmlspecialchars_decode(stripslashes($body));
if (!$mail->send()) {
throw new Exception('Mailer Error: ' . $mail->ErrorInfo);
}
// 성공 시 로그 데이터 업데이트
$log_data['status'] = 'success';
$log_data['error_msg'] = '';
} catch (Exception $e) {
// 실패 시 오류 메시지 기록
$log_data['error_msg'] = $e->getMessage();
} finally {
// 4. 성공/실패 여부에 관계없이 발송 로그 기록
$logManager->addLog($log_data);
}
return $log_data['status'] === 'success';
}
/**
* 이메일 주소 목록을 PHPMailer 객체에 추가하는 헬퍼 메소드
* @param object $mail PHPMailer 객체
* @param string|array $addresses 이메일 주소 (문자열 또는 배열)
* @param string $method PHPMailer 메소드명 (addAddress, addCC, addBCC)
*/
private function addAddresses($mail, $addresses, $method) {
if (empty($addresses)) return;
if (!is_array($addresses)) {
// 콤마로 구분된 문자열인 경우 배열로 변환
$addresses = explode(',', $addresses);
}
foreach ($addresses as $email) {
$email = trim($email);
if (!empty($email)) {
// $mail->$method($email) 형태로 호출
$mail->$method($email);
}
}
}
}
@@ -0,0 +1,61 @@
<?php
if (!defined('_GNUBOARD_')) exit;
class SendLogManager {
protected $table = 'g5_mail_send_log';
public function getTotalCount() {
$row = sql_fetch("SELECT COUNT(*) as cnt FROM {$this->table}");
return $row['cnt'] ?? 0;
}
/**
* [추가] 페이징된 로그 목록을 가져옵니다.
* @param int $from_record 시작 레코드
* @param int $page_rows 페이지당 레코드 수
* @return array
*/
public function getPagedList($from_record, $page_rows) {
$from_record = (int)$from_record;
$page_rows = (int)$page_rows;
$sql = "SELECT * FROM {$this->table} ORDER BY id DESC LIMIT {$from_record}, {$page_rows}";
$result = sql_query($sql);
$list = [];
while($row = sql_fetch_array($result)) {
$list[] = $row;
}
return $list;
}
public function getById($id) {
$id = (int)$id;
return sql_fetch("SELECT * FROM {$this->table} WHERE id = '{$id}'");
}
// C:/project/other/saungjin/adm/mail_manage/classes/SendLogManager.php 파일
public function addLog($data) {
global $member;
// 넘어온 데이터를 안전하게 처리
$to_email = sql_real_escape_string($data['to_email']);
$cc_email = isset($data['cc_email']) ? sql_real_escape_string($data['cc_email']) : '';
$bcc_email = isset($data['bcc_email']) ? sql_real_escape_string($data['bcc_email']) : '';
$subject = sql_real_escape_string($data['subject']);
$body = sql_real_escape_string($data['body']);
$status = in_array($data['status'], ['success', 'fail']) ? $data['status'] : 'fail';
$error_msg = isset($data['error_msg']) ? sql_real_escape_string($data['error_msg']) : '';
$send_time = G5_TIME_YMDHIS;
$resend_of = isset($data['resend_of']) ? (int)$data['resend_of'] : 'NULL';
$created_by = $member['mb_id'] ?? 'guest'; // 로그인한 사용자 또는 guest
// [수정] install.php의 테이블 구조와 일치하도록 쿼리 수정
$sql = "INSERT INTO {$this->table}
(to_email, cc_email,bcc_email, subject, body, status, send_time, created_by, created_at, error_msg, resend_of)
VALUES
('{$to_email}', '{$cc_email}', '{$bcc_email}' ,'{$subject}', '{$body}', '{$status}', '{$send_time}', '{$created_by}', '{$send_time}', '{$error_msg}', {$resend_of})";
sql_query($sql);
}
}
@@ -0,0 +1,230 @@
<?php
if (!defined('_GNUBOARD_')) exit;
class SmtpConfigManager
{
private $table = 'g5_mail_smtp_config';
// [추가] 로그 테이블명 변수
private $log_table = 'g5_mail_smtp_change_log';
public function getAll()
{
$sql = "SELECT * FROM {$this->table} WHERE is_deleted = 0 ORDER BY id DESC";
$result = sql_query($sql);
$list = [];
while ($row = sql_fetch_array($result)) {
$list[] = $row;
}
return $list;
}
public function get($id)
{
$id = (int)$id;
return sql_fetch("SELECT * FROM {$this->table} WHERE id = '{$id}' AND is_deleted = 0");
}
public function getInUse()
{
// [추가] 사용중(is_use=1)인 첫번째 설정을 가져오는 메소드
return sql_fetch("SELECT * FROM {$this->table} WHERE is_use = 1 AND is_deleted = 0 LIMIT 1");
}
public function create($data)
{
global $member;
$now = G5_TIME_YMDHIS;
$set_sql = [];
$set_sql[] = " name = '" . sql_real_escape_string(trim($data['name'])) . "' ";
$set_sql[] = " host = '" . sql_real_escape_string(trim($data['host'])) . "' ";
$set_sql[] = " username = '" . sql_real_escape_string(trim($data['username'])) . "' ";
$set_sql[] = " password = '" . sql_real_escape_string(trim($data['password'])) . "' ";
$set_sql[] = " port = '" . (int)$data['port'] . "' ";
$set_sql[] = " encryption = '" . (in_array($data['encryption'], ['none', 'ssl', 'tls']) ? $data['encryption'] : 'ssl') . "' ";
$set_sql[] = " from_email = '" . sql_real_escape_string(trim($data['from_email'])) . "' ";
$set_sql[] = " from_name = '" . sql_real_escape_string(trim($data['from_name'])) . "' ";
$set_sql[] = " is_use = '" . (isset($data['is_use']) ? 1 : 0) . "' ";
$set_sql[] = " is_deleted = '0' ";
$set_sql[] = " created_by = '{$member['mb_id']}' ";
$set_sql[] = " updated_by = '{$member['mb_id']}' ";
$set_sql[] = " created_at = '{$now}' ";
$set_sql[] = " updated_at = '{$now}' ";
$sql = "INSERT INTO {$this->table} SET " . implode(', ', $set_sql);
sql_query($sql);
// [추가] 로그 기록
$id = sql_insert_id();
$details = "새로운 SMTP 설정 추가: " . sql_real_escape_string(trim($data['name']));
$this->insertLog($id, 'insert', $member['mb_id'], $details);
}
public function update($id, $data)
{
global $member;
$id = (int)$id;
$now = G5_TIME_YMDHIS;
$set_sql = [];
$set_sql[] = " name = '" . sql_real_escape_string(trim($data['name'])) . "' ";
$set_sql[] = " host = '" . sql_real_escape_string(trim($data['host'])) . "' ";
$set_sql[] = " username = '" . sql_real_escape_string(trim($data['username'])) . "' ";
$set_sql[] = " port = '" . (int)$data['port'] . "' ";
$set_sql[] = " encryption = '" . (in_array($data['encryption'], ['none', 'ssl', 'tls']) ? $data['encryption'] : 'ssl') . "' ";
$set_sql[] = " from_email = '" . sql_real_escape_string(trim($data['from_email'])) . "' ";
$set_sql[] = " from_name = '" . sql_real_escape_string(trim($data['from_name'])) . "' ";
$set_sql[] = " is_use = '" . (isset($data['is_use']) ? 1 : 0) . "' ";
$set_sql[] = " updated_by = '{$member['mb_id']}' ";
$set_sql[] = " updated_at = '{$now}' ";
if (!empty($data['password'])) {
$set_sql[] = " password = '" . sql_real_escape_string(trim($data['password'])) . "' ";
}
$sql = "UPDATE {$this->table} SET " . implode(', ', $set_sql) . " WHERE id = {$id}";
sql_query($sql);
// [추가] 로그 기록
$details = "SMTP 설정 수정 (ID: {$id})";
$this->insertLog($id, 'update', $member['mb_id'], $details);
}
public function delete($id)
{
global $member;
$id = (int)$id;
$now = G5_TIME_YMDHIS;
$mb_id = sql_real_escape_string($member['mb_id']);
$sql = "UPDATE {$this->table} SET
is_deleted = 1,
updated_by = '{$mb_id}',
updated_at = '{$now}'
WHERE id = {$id}";
sql_query($sql);
// [추가] 로그 기록
$details = "SMTP 설정 삭제 (ID: {$id})";
$this->insertLog($id, 'delete', $member['mb_id'], $details);
}
/**
* [추가] 변경 이력을 기록하는 private 메소드
*/
private function insertLog($smtp_config_id, $action, $changed_by, $details)
{
$smtp_config_id = (int)$smtp_config_id;
$action = sql_real_escape_string($action);
$changed_by = sql_real_escape_string($changed_by);
$details = sql_real_escape_string($details);
$change_date = G5_TIME_YMDHIS;
$sql = "INSERT INTO {$this->log_table}
(smtp_config_id, `action`, changed_by, change_date, change_details)
VALUES
('{$smtp_config_id}', '{$action}', '{$changed_by}', '{$change_date}', '{$details}')";
sql_query($sql);
}
}
/*if (!defined('_GNUBOARD_')) exit;
class SmtpConfigManager
{
private $table = 'g5_mail_smtp_config';
public function getAll()
{
$sql = "SELECT * FROM {$this->table} WHERE is_deleted = 0 ORDER BY id DESC";
$result = sql_query($sql);
$list = [];
while ($row = sql_fetch_array($result)) {
$list[] = $row;
}
return $list;
}
public function get($id)
{
$id = (int)$id;
return sql_fetch("SELECT * FROM {$this->table} WHERE id = '{$id}' AND is_deleted = 0");
}
public function getInUse()
{
// [추가] 사용중(is_use=1)인 첫번째 설정을 가져오는 메소드
return sql_fetch("SELECT * FROM {$this->table} WHERE is_use = 1 AND is_deleted = 0 LIMIT 1");
}
public function create($data)
{
global $member;
$now = G5_TIME_YMDHIS;
// [수정] sql_array_insert 함수 대신 직접 SQL 구문을 생성합니다.
// 이 방식은 SQL 인젝션에 더 안전하고 호환성 문제가 없습니다.
$set_sql = [];
$set_sql[] = " name = '".sql_real_escape_string(trim($data['name']))."' ";
$set_sql[] = " host = '".sql_real_escape_string(trim($data['host']))."' ";
$set_sql[] = " username = '".sql_real_escape_string(trim($data['username']))."' ";
$set_sql[] = " password = '".sql_real_escape_string(trim($data['password']))."' ";
$set_sql[] = " port = '".(int)$data['port']."' ";
$set_sql[] = " encryption = '".(in_array($data['encryption'], ['none','ssl','tls']) ? $data['encryption'] : 'ssl')."' ";
$set_sql[] = " from_email = '".sql_real_escape_string(trim($data['from_email']))."' ";
$set_sql[] = " from_name = '".sql_real_escape_string(trim($data['from_name']))."' ";
$set_sql[] = " is_use = '".(isset($data['is_use']) ? 1 : 0)."' ";
$set_sql[] = " is_deleted = '0' ";
$set_sql[] = " created_by = '{$member['mb_id']}' ";
$set_sql[] = " updated_by = '{$member['mb_id']}' ";
$set_sql[] = " created_at = '{$now}' ";
$set_sql[] = " updated_at = '{$now}' ";
$sql = "INSERT INTO {$this->table} SET " . implode(', ', $set_sql);
sql_query($sql);
}
public function update($id, $data)
{
global $member;
$id = (int)$id;
$now = G5_TIME_YMDHIS;
// [수정] sql_array_insert 함수 대신 직접 SQL 구문을 생성합니다.
$set_sql = [];
$set_sql[] = " name = '".sql_real_escape_string(trim($data['name']))."' ";
$set_sql[] = " host = '".sql_real_escape_string(trim($data['host']))."' ";
$set_sql[] = " username = '".sql_real_escape_string(trim($data['username']))."' ";
$set_sql[] = " port = '".(int)$data['port']."' ";
$set_sql[] = " encryption = '".(in_array($data['encryption'], ['none','ssl','tls']) ? $data['encryption'] : 'ssl')."' ";
$set_sql[] = " from_email = '".sql_real_escape_string(trim($data['from_email']))."' ";
$set_sql[] = " from_name = '".sql_real_escape_string(trim($data['from_name']))."' ";
$set_sql[] = " is_use = '".(isset($data['is_use']) ? 1 : 0)."' ";
$set_sql[] = " updated_by = '{$member['mb_id']}' ";
$set_sql[] = " updated_at = '{$now}' ";
// 비밀번호가 입력된 경우에만 업데이트
if (!empty($data['password'])) {
$set_sql[] = " password = '".sql_real_escape_string(trim($data['password']))."' ";
}
$sql = "UPDATE {$this->table} SET " . implode(', ', $set_sql) . " WHERE id = {$id}";
sql_query($sql);
}
public function delete($id)
{
global $member;
$id = (int)$id;
$now = G5_TIME_YMDHIS;
// [개선] sql_real_escape_string 을 사용하여 보안 강화
$mb_id = sql_real_escape_string($member['mb_id']);
$sql = "UPDATE {$this->table} SET
is_deleted = 1,
updated_by = '{$mb_id}',
updated_at = '{$now}'
WHERE id = {$id}";
sql_query($sql);
}
}*/
+159
View File
@@ -0,0 +1,159 @@
<?php
if (!defined('_GNUBOARD_')) exit;
class TemplateManager
{
protected $table = 'g5_mail_template';
private $log_table = 'g5_mail_template_change_log';
// [추가] 템플릿 변수 테이블명
private $vars_table = 'g5_mail_template_vars';
public function getAll()
{
// [수정] is_use -> is_deleted 로 조건 변경
$result = sql_query("SELECT * FROM {$this->table} WHERE is_deleted = 0 ORDER BY id DESC");
$list = [];
while ($row = sql_fetch_array($result)) {
$list[] = $row;
}
return $list;
}
public function getById($id)
{
$id = (int)$id;
return sql_fetch("SELECT * FROM {$this->table} WHERE id = '{$id}' AND is_deleted = 0 AND is_use = 1");
}
public function getByCode($code)
{
$code = sql_real_escape_string($code);
return sql_fetch("SELECT * FROM {$this->table} WHERE code = '{$code}' AND is_deleted = 0");
}
/**
* [추가] 특정 템플릿에 속한 모든 변수들을 가져옵니다.
* @param int $template_id
* @return array
*/
public function getVarsByTemplateId($template_id)
{
$template_id = (int)$template_id;
$sql = "SELECT var_name, default_value FROM {$this->vars_table} WHERE template_id = '{$template_id}'";
$result = sql_query($sql);
$vars = [];
while ($row = sql_fetch_array($result)) {
$vars[$row['var_name']] = $row['default_value'];
}
return $vars;
}
public function save($data)
{
global $member;
$id = isset($data['id']) ? (int)$data['id'] : 0;
$code = sql_real_escape_string($data['code']);
$title = sql_real_escape_string($data['title']);
$content = $data['content']; // [주의] SQL Escape는 syncTemplateVars에서 처리
// [추가] 헤더와 푸터 데이터를 받습니다.
$header_html = $data['header_html'] ?? '';
$footer_html = $data['footer_html'] ?? '';
$is_use = isset($data['is_use']) ? 1 : 0;
$now = G5_TIME_YMDHIS;
$mb_id = sql_real_escape_string($member['mb_id']);
if ($id > 0) {
// [수정] 업데이트 쿼리 수정
$sql = "UPDATE {$this->table} SET
code = '{$code}',
title = '{$title}',
content = '" . sql_real_escape_string($content) . "',
header_html = '" . sql_real_escape_string($header_html) . "',
footer_html = '" . sql_real_escape_string($footer_html) . "',
is_use = '{$is_use}',
updated_by = '{$mb_id}',
updated_at = '{$now}'
WHERE id = '{$id}'";
sql_query($sql);
// [추가] 업데이트 로그 기록
$details = "템플릿 수정 (ID: {$id}, 코드: {$code})";
$this->insertTemplateLog($id, 'update', $mb_id, $details);
} else {
// [수정] 생성 쿼리 수정
$sql = "INSERT INTO {$this->table}
(code, title, content, header_html, footer_html, is_use, is_deleted, created_by, updated_by, created_at, updated_at)
VALUES
('{$code}', '{$title}', '" . sql_real_escape_string($content) . "', '" . sql_real_escape_string($header_html) . "', '" . sql_real_escape_string($footer_html) . "', '{$is_use}', 0, '{$mb_id}', '{$mb_id}', '{$now}', '{$now}')";
sql_query($sql);
$id = sql_insert_id(); // 새로 생성된 ID를 가져옴
// [추가] 생성 로그 기록
$details = "새 템플릿 생성: " . $title;
$this->insertTemplateLog($id, 'insert', $mb_id, $details);
}
// [핵심 추가] 템플릿 변수 동기화
$this->syncTemplateVars($id, $content, $data['variables'] ?? []);
}
public function delete($id)
{
global $member;
$id = (int)$id;
$mb_id = sql_real_escape_string($member['mb_id']);
$now = G5_TIME_YMDHIS;
// [참고] is_deleted 필드는 사용하지 않으므로 실제 삭제 처리합니다.
// g5_mail_template_vars 테이블은 FOREIGN KEY (ON DELETE CASCADE) 설정으로 자동 삭제됩니다.
sql_query("DELETE FROM {$this->table} WHERE id = '{$id}'");
// [수정] 삭제 로그 기록
$details = "템플릿 삭제 (ID: {$id})";
$this->insertTemplateLog($id, 'delete', $mb_id, $details);
}
private function insertTemplateLog($template_id, $action, $changed_by, $details)
{
$template_id = (int)$template_id;
$action = sql_real_escape_string($action);
$changed_by = sql_real_escape_string($changed_by);
$details = sql_real_escape_string($details);
$change_date = G5_TIME_YMDHIS;
$sql = "INSERT INTO {$this->log_table}
(template_id, `action`, changed_by, change_date, change_details)
VALUES
('{$template_id}', '{$action}', '{$changed_by}', '{$change_date}', '{$details}')";
sql_query($sql);
}
/**
* [핵심 추가] 템플릿 내용의 변수와 DB를 동기화하는 메소드
*/
private function syncTemplateVars($template_id, $content, $submitted_vars)
{
// 1. 템플릿 내용에서 현재 사용중인 모든 변수 추출 (예: {name}, {order_id})
preg_match_all('/{(\w+)}/u', $content, $matches);
$vars_in_content = array_unique($matches[1]);
// 2. 이 템플릿과 관련된 기존 변수들을 모두 삭제 (가장 간단하고 확실한 동기화 방법)
sql_query("DELETE FROM {$this->vars_table} WHERE template_id = '{$template_id}'");
// 3. 내용에 존재하는 변수들만 다시 INSERT
if (!empty($vars_in_content)) {
$now = G5_TIME_YMDHIS;
foreach ($vars_in_content as $var_name) {
// 폼에서 전송된 기본값이 있으면 사용하고, 없으면 빈 문자열로 설정
$default_value = $submitted_vars[$var_name] ?? '';
$sql = "INSERT INTO {$this->vars_table}
(template_id, var_name, default_value, created_at, updated_at)
VALUES
('{$template_id}', '" . sql_real_escape_string($var_name) . "', '" . sql_real_escape_string($default_value) . "', '{$now}', '{$now}')";
sql_query($sql);
}
}
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
// 크론용 메일 발송 스크립트
include_once('../../common.php');
require_once(__DIR__.'/../classes/MailSender.php');
require_once(__DIR__.'/../classes/SendLogManager.php');
// 실제 예약메일 조회 및 발송 처리 구현 필요
$mailSender = new MailSender();
$logManager = new SendLogManager();
// 예시: 예약메일 배열 (실제 DB에서 예약메일을 가져와야 함)
$reservedMails = [
[
'to' => 'example@example.com',
'subject' => '예약 메일 테스트',
'body' => '<p>테스트 메일 내용</p>',
'smtp_id' => 1,
],
];
foreach ($reservedMails as $mail) {
$result = $mailSender->sendMail($mail['to'], $mail['subject'], $mail['body'], [], $mail['smtp_id']);
$logManager->addLog([
'to_email' => $mail['to'],
'subject' => $mail['subject'],
'body' => $mail['body'],
'status' => $result['success'] ? '성공' : '실패',
'error_msg' => $result['success'] ? '' : $result['message'],
]);
}
+338
View File
@@ -0,0 +1,338 @@
<?php
$sub_menu = '100000';
require_once './_common.php';
@require_once './safe_check.php';
if (function_exists('social_log_file_delete')) {
//소셜로그인 디버그 파일 24시간 지난것은 삭제
social_log_file_delete(86400);
}
$g5['title'] = '관리자메인';
require_once './admin.head.php';
$new_member_rows = 5;
$new_point_rows = 5;
$new_write_rows = 5;
$addtional_content_before = run_replace('adm_index_addtional_content_before', '', $is_admin, $auth, $member);
if ($addtional_content_before) {
echo $addtional_content_before;
}
if (!auth_check_menu($auth, '200100', 'r', true)) {
$sql_common = " from {$g5['member_table']} ";
$sql_search = " where (1) ";
if ($is_admin != 'super') {
$sql_search .= " and mb_level <= '{$member['mb_level']}' ";
}
if (!$sst) {
$sst = "mb_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'];
// 탈퇴회원수
$sql = " select count(*) as cnt {$sql_common} {$sql_search} and mb_leave_date <> '' {$sql_order} ";
$row = sql_fetch($sql);
$leave_count = $row['cnt'];
// 차단회원수
$sql = " SELECT count(*) as cnt {$sql_common} {$sql_search} and mb_intercept_date <> '' {$sql_order} ";
$row = sql_fetch($sql);
$intercept_count = $row['cnt'];
$sql = " SELECT * {$sql_common} {$sql_search} {$sql_order} limit {$new_member_rows} ";
$result = sql_query($sql);
$colspan = 12;
?>
<section>
<h2>신규가입회원 <?php echo $new_member_rows ?>건 목록</h2>
<div class="local_desc02 local_desc">
총회원수 <?php echo number_format($total_count) ?>명 중 차단 <?php echo number_format($intercept_count) ?>명, 탈퇴 : <?php echo number_format($leave_count) ?>명
</div>
<div class="tbl_head01 tbl_wrap">
<table>
<caption>신규가입회원</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>
<th scope="col">공개</th>
<th scope="col">인증</th>
<th scope="col">차단</th>
<th scope="col">그룹</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>';
}
if ($is_admin == 'group') {
$s_mod = '';
$s_del = '';
} else {
$s_mod = '<a href="./member_form.php?$qstr&amp;w=u&amp;mb_id=' . $row['mb_id'] . '">수정</a>';
$s_del = '<a href="./member_delete.php?' . $qstr . '&amp;w=d&amp;mb_id=' . $row['mb_id'] . '&amp;url=' . $_SERVER['SCRIPT_NAME'] . '" onclick="return delete_confirm(this);">삭제</a>';
}
$s_grp = '<a href="./boardgroupmember_form.php?mb_id=' . $row['mb_id'] . '">그룹</a>';
$leave_date = $row['mb_leave_date'] ? $row['mb_leave_date'] : date("Ymd", G5_SERVER_TIME);
$intercept_date = $row['mb_intercept_date'] ? $row['mb_intercept_date'] : date("Ymd", G5_SERVER_TIME);
$mb_nick = get_sideview($row['mb_id'], get_text($row['mb_nick']), $row['mb_email'], $row['mb_homepage']);
$mb_id = $row['mb_id'];
?>
<tr>
<td class="td_mbid"><?php echo $mb_id ?></td>
<td class="td_mbname"><?php echo get_text($row['mb_name']); ?></td>
<td class="td_mbname sv_use">
<div><?php echo $mb_nick ?></div>
</td>
<td class="td_num"><?php echo $row['mb_level'] ?></td>
<td><a href="./point_list.php?sfl=mb_id&amp;stx=<?php echo $row['mb_id'] ?>"><?php echo number_format($row['mb_point']) ?></a></td>
<td class="td_boolean"><?php echo $row['mb_mailling'] ? '예' : '아니오'; ?></td>
<td class="td_boolean"><?php echo $row['mb_open'] ? '예' : '아니오'; ?></td>
<td class="td_boolean"><?php echo preg_match('/[1-9]/', $row['mb_email_certify']) ? '예' : '아니오'; ?></td>
<td class="td_boolean"><?php echo $row['mb_intercept_date'] ? '예' : '아니오'; ?></td>
<td class="td_category"><?php echo $group ?></td>
</tr>
<?php
}
if ($i == 0) {
echo '<tr><td colspan="' . $colspan . '" class="empty_table">자료가 없습니다.</td></tr>';
}
?>
</tbody>
</table>
</div>
<div class="btn_list03 btn_list">
<a href="./member_list.php">회원 전체보기</a>
</div>
</section>
<?php
} //endif 최신 회원
if (!auth_check_menu($auth, '300100', 'r', true)) {
$sql_common = " from {$g5['board_new_table']} a, {$g5['board_table']} b, {$g5['group_table']} c where a.bo_table = b.bo_table and b.gr_id = c.gr_id ";
if ($gr_id) {
$sql_common .= " and b.gr_id = '{$gr_id}' ";
}
if (isset($view) && $view) {
if ($view == 'w') {
$sql_common .= " and a.wr_id = a.wr_parent ";
} elseif ($view == 'c') {
$sql_common .= " and a.wr_id <> a.wr_parent ";
}
}
$sql_order = " order by a.bn_id desc ";
$sql = " SELECT count(*) as cnt {$sql_common} ";
$row = sql_fetch($sql);
$total_count = $row['cnt'];
$colspan = 5;
?>
<section>
<h2>최근게시물</h2>
<div class="tbl_head01 tbl_wrap">
<table>
<caption>최근게시물</caption>
<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
$sql = " SELECT a.*, b.bo_subject, c.gr_subject, c.gr_id {$sql_common} {$sql_order} limit {$new_write_rows} ";
$result = sql_query($sql);
for ($i = 0; $row = sql_fetch_array($result); $i++) {
$tmp_write_table = $g5['write_prefix'] . $row['bo_table'];
if ($row['wr_id'] == $row['wr_parent']) {
// 원글
$comment = "";
$comment_link = "";
$row2 = sql_fetch(" SELECT * from {$tmp_write_table} where wr_id = '{$row['wr_id']}' ");
$name = get_sideview($row2['mb_id'], get_text(cut_str($row2['wr_name'], $config['cf_cut_name'])), $row2['wr_email'], $row2['wr_homepage']);
// 당일인 경우 시간으로 표시함
$datetime = substr($row2['wr_datetime'], 0, 10);
$datetime2 = $row2['wr_datetime'];
if ($datetime == G5_TIME_YMD) {
$datetime2 = substr($datetime2, 11, 5);
} else {
$datetime2 = substr($datetime2, 5, 5);
}
} else {
// 코멘트
$comment = '댓글. ';
$comment_link = '#c_' . $row['wr_id'];
$row2 = sql_fetch(" SELECT * from {$tmp_write_table} where wr_id = '{$row['wr_parent']}' ");
$row3 = sql_fetch(" SELECT mb_id, wr_name, wr_email, wr_homepage, wr_datetime from {$tmp_write_table} where wr_id = '{$row['wr_id']}' ");
$name = get_sideview($row3['mb_id'], get_text(cut_str($row3['wr_name'], $config['cf_cut_name'])), $row3['wr_email'], $row3['wr_homepage']);
// 당일인 경우 시간으로 표시함
$datetime = substr($row3['wr_datetime'], 0, 10);
$datetime2 = $row3['wr_datetime'];
if ($datetime == G5_TIME_YMD) {
$datetime2 = substr($datetime2, 11, 5);
} else {
$datetime2 = substr($datetime2, 5, 5);
}
}
?>
<tr>
<td class="td_category"><a href="<?php echo G5_BBS_URL ?>/new.php?gr_id=<?php echo $row['gr_id'] ?>"><?php echo cut_str($row['gr_subject'], 10) ?></a></td>
<td class="td_category"><a href="<?php echo get_pretty_url($row['bo_table']) ?>"><?php echo cut_str($row['bo_subject'], 20) ?></a></td>
<td><a href="<?php echo get_pretty_url($row['bo_table'], $row2['wr_id']); ?><?php echo $comment_link ?>"><?php echo $comment ?><?php echo conv_subject($row2['wr_subject'], 100) ?></a></td>
<td class="td_mbname">
<div><?php echo $name ?></div>
</td>
<td class="td_datetime"><?php echo $datetime ?></td>
</tr>
<?php
}
if ($i == 0) {
echo '<tr><td colspan="' . $colspan . '" class="empty_table">자료가 없습니다.</td></tr>';
}
?>
</tbody>
</table>
</div>
<div class="btn_list03 btn_list">
<a href="<?php echo G5_BBS_URL ?>/new.php">최근게시물 더보기</a>
</div>
</section>
<?php
} //endif 최근게시물
if (!auth_check_menu($auth, '200200', 'r', true)) {
$sql_common = " from {$g5['point_table']} ";
$sql_search = " where (1) ";
$sql_order = " order by po_id desc ";
$sql = " SELECT count(*) as cnt {$sql_common} {$sql_search} {$sql_order} ";
$row = sql_fetch($sql);
$total_count = $row['cnt'];
$sql = " SELECT * {$sql_common} {$sql_search} {$sql_order} limit {$new_point_rows} ";
$result = sql_query($sql);
$colspan = 7;
?>
<section>
<h2>최근 포인트 발생내역</h2>
<div class="local_desc02 local_desc">
전체 <?php echo number_format($total_count) ?> 건 중 <?php echo $new_point_rows ?>건 목록
</div>
<div class="tbl_head01 tbl_wrap">
<table>
<caption>최근 포인트 발생내역</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>
<th scope="col">포인트합</th>
</tr>
</thead>
<tbody>
<?php
$row2['mb_id'] = '';
for ($i = 0; $row = sql_fetch_array($result); $i++) {
if ($row2['mb_id'] != $row['mb_id']) {
$sql2 = " SELECT mb_id, mb_name, mb_nick, mb_email, mb_homepage, mb_point from {$g5['member_table']} where mb_id = '{$row['mb_id']}' ";
$row2 = sql_fetch($sql2);
}
$mb_nick = get_sideview($row['mb_id'], $row2['mb_nick'], $row2['mb_email'], $row2['mb_homepage']);
$link1 = $link2 = "";
if (!preg_match("/^\@/", $row['po_rel_table']) && $row['po_rel_table']) {
$link1 = '<a href="' . get_pretty_url($row['po_rel_table'], $row['po_rel_id']) . '" target="_blank">';
$link2 = '</a>';
}
?>
<tr>
<td class="td_mbid"><a href="./point_list.php?sfl=mb_id&amp;stx=<?php echo $row['mb_id'] ?>"><?php echo $row['mb_id'] ?></a></td>
<td class="td_mbname"><?php echo get_text($row2['mb_name']); ?></td>
<td class="td_name sv_use">
<div><?php echo $mb_nick ?></div>
</td>
<td class="td_datetime"><?php echo $row['po_datetime'] ?></td>
<td><?php echo $link1 . $row['po_content'] . $link2 ?></td>
<td class="td_numbig"><?php echo number_format($row['po_point']) ?></td>
<td class="td_numbig"><?php echo number_format($row['po_mb_point']) ?></td>
</tr>
<?php
}
if ($i == 0) {
echo '<tr><td colspan="' . $colspan . '" class="empty_table">자료가 없습니다.</td></tr>';
}
?>
</tbody>
</table>
</div>
<div class="btn_list03 btn_list">
<a href="./point_list.php">포인트내역 전체보기</a>
</div>
</section>
<?php
} //endif
$addtional_content_after = run_replace('adm_index_addtional_content_after', '', $is_admin, $auth, $member);
if ($addtional_content_after) {
echo $addtional_content_after;
}
require_once './admin.tail.php';
+167
View File
@@ -0,0 +1,167 @@
<?php
$sub_menu = '600900';
include_once('./_common.php');
include_once(__DIR__ . '/lib/SchemaManager.class.php');
if ($is_admin != 'super') {
alert('최고관리자만 접근 가능합니다.');
}
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();
$copy_results = [];
$solution_files = [
['source' => __DIR__ . '/admin.menu600.mail_manager.php', 'target' => G5_ADMIN_PATH . '/admin.menu600.mail_manager.php', 'desc' => '관리자 메뉴 파일']
];
foreach ($solution_files as $file) {
$key = basename($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 = ['copy' => $copy_results, 'db' => $db_results];
} 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.menu600.mail_manager.php';
if (file_exists($menu_file)) {
if (@unlink($menu_file)) {
$delete_result['menu'] = '메뉴 파일 삭제 성공';
} else {
$delete_result['menu'] = '메뉴 파일 삭제 실패 (권한 확인 필요)';
}
}
}
$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-envelope"></i> 메일 관리 솔루션</h1>
<p>SMTP 설정 및 메일 템플릿 관리 시스템</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="./smtp_config.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="./smtp_config.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('g5_mail_smtp_config' => 'SMTP 서버 설정', 'g5_mail_smtp_change_log' => 'SMTP 설정 변경 이력', 'g5_mail_template' => '메일 템플릿', 'g5_mail_template_vars' => '템플릿 변수', 'g5_mail_template_change_log' => '템플릿 변경 이력', 'g5_mail_send_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');
?>
+90
View File
@@ -0,0 +1,90 @@
-- 1. SMTP 서버 설정을 저장하는 테이블
CREATE TABLE IF NOT EXISTS `g5_mail_smtp_config` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '고유 ID',
`name` VARCHAR(255) NOT NULL COMMENT 'SMTP 설정 이름 (관리용)',
`host` VARCHAR(255) NOT NULL COMMENT 'SMTP 호스트 주소',
`username` VARCHAR(255) NOT NULL COMMENT 'SMTP 사용자 아이디',
`password` VARCHAR(255) NOT NULL COMMENT 'SMTP 사용자 비밀번호',
`port` INT NOT NULL DEFAULT 465 COMMENT '포트 번호',
`encryption` ENUM('none','ssl','tls') NOT NULL DEFAULT 'ssl' COMMENT '암호화 방식',
`from_email` VARCHAR(255) NOT NULL COMMENT '발신자 이메일',
`from_name` VARCHAR(255) NOT NULL COMMENT '발신자 이름',
`is_use` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '사용 여부 (1:사용, 0:미사용)',
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '삭제 여부 (1:삭제, 0:정상)',
`created_by` VARCHAR(20) NOT NULL COMMENT '생성자 아이디',
`updated_by` VARCHAR(20) NOT NULL COMMENT '수정자 아이디',
`created_at` DATETIME NOT NULL COMMENT '생성일시',
`updated_at` DATETIME NOT NULL COMMENT '수정일시'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='[솔루션] SMTP 서버 설정 정보';
-- 2. SMTP 설정 변경 이력을 기록하는 테이블
CREATE TABLE IF NOT EXISTS `g5_mail_smtp_change_log` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '고유 ID',
`smtp_config_id` INT UNSIGNED NOT NULL COMMENT 'SMTP 설정 ID',
`action` ENUM('insert','update','delete') NOT NULL COMMENT '작업 종류 (추가, 수정, 삭제)',
`changed_by` VARCHAR(20) NOT NULL COMMENT '변경자 아이디',
`change_date` DATETIME NOT NULL COMMENT '변경일시',
`change_details` TEXT NOT NULL COMMENT '변경 상세 내용',
KEY `smtp_config_id` (`smtp_config_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='[솔루션] SMTP 설정 변경 이력';
-- 3. 메일 템플릿의 본문, 헤더, 푸터 등을 저장하는 테이블
CREATE TABLE IF NOT EXISTS `g5_mail_template` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '고유 ID',
`code` VARCHAR(100) NOT NULL UNIQUE COMMENT '템플릿 고유 코드 (개발자용)',
`title` VARCHAR(255) NOT NULL COMMENT '메일 제목',
`content` LONGTEXT NOT NULL COMMENT '메일 본문 내용 (HTML)',
`header_html` TEXT NULL COMMENT '템플릿별 헤더 HTML',
`footer_html` TEXT NULL COMMENT '템플릿별 푸터 HTML',
`is_use` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '사용 여부 (1:사용, 0:미사용)',
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '삭제 여부 (1:삭제, 0:정상)',
`created_by` VARCHAR(20) NOT NULL COMMENT '생성자 아이디',
`updated_by` VARCHAR(20) NOT NULL COMMENT '수정자 아이디',
`created_at` DATETIME NOT NULL COMMENT '생성일시',
`updated_at` DATETIME NOT NULL COMMENT '수정일시'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='[솔루션] 메일 템플릿 정보';
-- 4. 각 템플릿에 사용되는 치환 변수({이름})와 기본값을 저장하는 테이블
CREATE TABLE IF NOT EXISTS `g5_mail_template_vars` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '고유 ID',
`template_id` INT UNSIGNED NOT NULL COMMENT '메일 템플릿 ID',
`var_name` VARCHAR(100) NOT NULL COMMENT '치환 변수명 (예: 이름)',
`default_value` TEXT NULL COMMENT '치환 변수의 기본값',
`created_at` DATETIME NOT NULL COMMENT '생성일시',
`updated_at` DATETIME NOT NULL COMMENT '수정일시',
KEY `template_id` (`template_id`),
CONSTRAINT `fk_template_vars_template_id` FOREIGN KEY (`template_id`) REFERENCES `g5_mail_template`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='[솔루션] 메일 템플릿의 치환 변수 정보';
-- 5. 메일 템플릿 변경 이력을 기록하는 테이블
CREATE TABLE IF NOT EXISTS `g5_mail_template_change_log` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '고유 ID',
`template_id` INT UNSIGNED NOT NULL COMMENT '메일 템플릿 ID',
`action` ENUM('insert','update','delete') NOT NULL COMMENT '작업 종류 (추가, 수정, 삭제)',
`changed_by` VARCHAR(20) NOT NULL COMMENT '변경자 아이디',
`change_date` DATETIME NOT NULL COMMENT '변경일시',
`change_details` TEXT NOT NULL COMMENT '변경 상세 내용',
KEY `template_id` (`template_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='[솔루션] 메일 템플릿 변경 이력';
-- 6. 메일 발송 성공/실패 이력을 모두 기록하는 테이블
CREATE TABLE IF NOT EXISTS `g5_mail_send_log` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '고유 ID',
`to_email` VARCHAR(255) NOT NULL COMMENT '수신자 이메일',
`cc_email` VARCHAR(255) DEFAULT NULL COMMENT '참조 이메일',
`bcc_email` VARCHAR(255) DEFAULT NULL COMMENT '숨은참조 이메일',
`subject` VARCHAR(255) NOT NULL COMMENT '메일 제목',
`body` LONGTEXT NOT NULL COMMENT '메일 본문 (HTML)',
`status` ENUM('success','fail') NOT NULL COMMENT '발송 상태 (성공, 실패)',
`error_msg` TEXT NULL COMMENT '발송 실패 시 상세 오류 메시지',
`try_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '재시도 횟수',
`max_try_count` INT UNSIGNED NOT NULL DEFAULT 3 COMMENT '최대 재시도 횟수',
`send_time` DATETIME DEFAULT NULL COMMENT '발송 완료 일시',
`resend_of` INT UNSIGNED DEFAULT NULL COMMENT '재발송의 원본 로그 ID',
`is_scheduled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '예약 발송 여부 (1:예약, 0:즉시)',
`scheduled_time` DATETIME DEFAULT NULL COMMENT '예약 발송 시간',
`created_by` VARCHAR(20) NOT NULL COMMENT '발송 요청자 아이디',
`created_at` DATETIME NOT NULL COMMENT '로그 생성 일시',
KEY `status` (`status`),
KEY `resend_of` (`resend_of`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='[솔루션] 메일 발송 이력';
@@ -0,0 +1,215 @@
<?php
/*ini_set('display_errors', 1);
error_reporting(E_ALL);*/
$sub_menu = "600000";
include_once("./_common.php");
auth_check_menu($auth, $sub_menu, 'r');
$g5['title'] = "email 솔루션 설치";
//$setup = (isset($_POST['setup']) && $_POST['setup']) ? 1 : 0;
include_once(G5_ADMIN_PATH.'/admin.head.php');
function copy_plugin_directory($source, $destination) {
if (!is_dir($source)) return false;
// 대상 디렉토리가 없으면 생성
if (!is_dir($destination)) {
mkdir($destination, 0755, true);
}
$dir = opendir($source);
while (false !== ($file = readdir($dir))) {
if ($file == '.' || $file == '..') continue;
$srcPath = $source . '/' . $file;
$dstPath = $destination . '/' . $file;
if (is_dir($srcPath)) {
copy_plugin_directory($srcPath, $dstPath); // 재귀 복사
} else {
copy($srcPath, $dstPath);
}
}
closedir($dir);
return true;
}
// 복사 실행 경로 설정
$plugin_target = G5_ADMIN_PATH . '/plugin/mail_manage'; // 예: /adm/plugin/mail_manage
$plugin_source = G5_PATH . '/plugin/mail_manage'; // 예: /plugin/mail_manage
if (!is_dir($plugin_target)) {
$success = copy_plugin_directory($plugin_source, $plugin_target);
if ($success) {
echo "<p>✅ /plugin/mail_manage 디렉토리가 자동으로 복사되었습니다.</p>";
} else {
echo "<p>❌ 플러그인 복사 실패. 수동 복사 필요.</p>";
}
}
// 4. 관리자 체크
if (!defined('_GNUBOARD_') || $is_admin != 'super') {
alert('최고 관리자만 설치할 수 있습니다.');
}
// ✅ 테이블 생성 함수
function create_plugin_tables() {
$tables = [];
// SMTP 설정 테이블
$tables[] = "
CREATE TABLE IF NOT EXISTS `g5_mail_smtp_config` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL,
`host` VARCHAR(255) NOT NULL,
`username` VARCHAR(255) NOT NULL,
`password` VARCHAR(255) NOT NULL,
`port` INT NOT NULL DEFAULT 465,
`encryption` ENUM('none','ssl','tls') NOT NULL DEFAULT 'ssl',
`from_email` VARCHAR(255) NOT NULL,
`from_name` VARCHAR(255) NOT NULL,
`is_use` TINYINT(1) NOT NULL DEFAULT 1,
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0,
`created_by` VARCHAR(20) NOT NULL,
`updated_by` VARCHAR(20) NOT NULL,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL
) DEFAULT CHARSET=utf8mb4";
// SMTP 변경 이력 테이블
$tables[] = "
CREATE TABLE IF NOT EXISTS `g5_mail_smtp_change_log` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`smtp_config_id` INT UNSIGNED NOT NULL,
`action` ENUM('insert','update','delete') NOT NULL,
`changed_by` VARCHAR(20) NOT NULL,
`change_date` DATETIME NOT NULL,
`change_details` TEXT NOT NULL,
KEY (`smtp_config_id`)
) DEFAULT CHARSET=utf8mb4";
// 메일 템플릿 테이블
$tables[] = "
CREATE TABLE IF NOT EXISTS `g5_mail_template` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`code` VARCHAR(100) NOT NULL UNIQUE,
`title` VARCHAR(255) NOT NULL,
`content` LONGTEXT NOT NULL,
`header_html` TEXT NULL,
`footer_html` TEXT NULL COMMENT '메일 공통 푸터',
`is_use` TINYINT(1) NOT NULL DEFAULT 1,
`is_deleted` TINYINT(1) NOT NULL DEFAULT 0,
`created_by` VARCHAR(20) NOT NULL,
`updated_by` VARCHAR(20) NOT NULL,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL
) DEFAULT CHARSET=utf8mb4";
// 템플릿 치환 변수 테이블
$tables[] = " CREATE TABLE IF NOT EXISTS `g5_mail_template_vars` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`template_id` INT UNSIGNED NOT NULL,
`var_name` VARCHAR(100) NOT NULL,
`default_value` TEXT DEFAULT '',
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
FOREIGN KEY (`template_id`) REFERENCES g5_mail_template(`id`) ON DELETE CASCADE
) DEFAULT CHARSET=utf8mb4";
// 템플릿 변경 이력 테이블
$tables[] = "
CREATE TABLE IF NOT EXISTS `g5_mail_template_change_log` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`template_id` INT UNSIGNED NOT NULL,
`action` ENUM('insert','update','delete') NOT NULL,
`changed_by` VARCHAR(20) NOT NULL,
`change_date` DATETIME NOT NULL,
`change_details` TEXT NOT NULL,
KEY (`template_id`)
) DEFAULT CHARSET=utf8mb4";
// 메일 발송 이력 테이블
$tables[] = "
CREATE TABLE IF NOT EXISTS `g5_mail_send_log` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`to_email` VARCHAR(255) NOT NULL,
`cc_email` VARCHAR(255) DEFAULT NULL,
`subject` VARCHAR(255) NOT NULL,
`body` LONGTEXT NOT NULL,
`status` ENUM('success','fail') NOT NULL,
`error_msg` TEXT, -- [추가] 발송 실패 시 상세 오류 메시지를 저장할 컬럼
`try_count` INT NOT NULL DEFAULT 0,
`max_try_count` INT NOT NULL DEFAULT 3,
`send_time` DATETIME DEFAULT NULL,
`resend_of` INT UNSIGNED DEFAULT NULL,
`is_scheduled` TINYINT(1) NOT NULL DEFAULT 0,
`scheduled_time` DATETIME DEFAULT NULL,
`created_by` VARCHAR(20) NOT NULL,
`created_at` DATETIME NOT NULL,
KEY (`resend_of`)
) DEFAULT CHARSET=utf8mb4;";
foreach ($tables as $sql) {
sql_query($sql);
}
}
// ✅ 관리자 메뉴 추가 함수
function register_plugin_admin_menu() {
global $g5;
$menu_table = $g5['menu_table'];
$parent_code = 'mail_manage';
$menu_name = '메일 관리';
$menu_link = G5_ADMIN_URL . '/plugin/mail_manage/smtp_config.php';
// 이미 등록되어 있으면 건너뜀
$exists = sql_fetch("SELECT * FROM {$menu_table} WHERE me_code = '{$parent_code}'");
if (!$exists) {
$max_order = sql_fetch("SELECT MAX(me_order) AS max_order FROM {$menu_table}");
$new_order = (int)($max_order['max_order'] ?? 0) + 1;
sql_query("
INSERT INTO {$menu_table} (me_code, me_name, me_link, me_target, me_order, me_use)
VALUES ('{$parent_code}', '{$menu_name}', '{$menu_link}', '', {$new_order}, 1)
");
$sub_menus = [
['smtp_config', 'SMTP 설정', './smtp_config.php'],
['template', '메일 템플릿 관리', './template.php'],
['send_log', '메일 발송 이력', './send_log.php'],
];
foreach ($sub_menus as $submenu) {
[$code, $name, $link] = $submenu;
$exist_sub = sql_fetch("SELECT * FROM {$menu_table} WHERE me_code = '{$code}' LIMIT 1");
if (!$exist_sub) {
$parent = sql_fetch("SELECT me_id FROM {$menu_table} WHERE me_code = 'mail_manage' LIMIT 1");
$parent_id = $parent['me_id'] ?? 0;
$max_order_sub = sql_fetch("SELECT MAX(me_order) as max_order FROM {$menu_table} WHERE me_parent = {$parent_id}");
$new_order_sub = (int)($max_order_sub['max_order'] ?? 0) + 1;
sql_query("
INSERT INTO {$menu_table} (me_code, me_name, me_link, me_target, me_order, me_use, me_auth, me_parent)
VALUES ('{$code}', '{$name}', '{$link}', '', {$new_order_sub}, 1, 'super', {$parent_id})
");
}
}
}
}
// ✅ 실행
create_plugin_tables();
register_plugin_admin_menu();
//copy_plugin_directory($plugin_source,$plugin_target);
// ✅ 성공 메시지 및 이동
//alert('메일 관리 플러그인 설치가 완료되었습니다.', G5_ADMIN_URL . '/plugin/mail_manage/smtp_config.php');
echo "<meta charset='utf-8'>";
echo "<h2>메일 관리 플러그인 설치 완료!</h2>";
echo "<p>테이블과 관리자 메뉴가 정상적으로 생성되었습니다.</p>";
echo "<p><a href='./smtp_config.php'>SMTP 설정으로 이동</a></p>";
include_once(G5_ADMIN_PATH.'/admin.tail.php');
+194
View File
@@ -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];
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
// [수정] 발송 이력 메뉴 코드를 설정하고, 그누보드 관리자 공통 파일을 포함합니다.
$sub_menu = "600300"; // install.php에서 설정한 메뉴 코드
include_once("./_common.php");
// 1. 권한 확인: 현재 관리자가 이 페이지를 볼 권한이 있는지 체크합니다.
auth_check_menu($auth, $sub_menu, "r");
// 2. 필요한 클래스 포함 및 페이지 제목 설정
require_once(__DIR__ . '/classes/SendLogManager.php');
$g5['title'] = '메일 발송 이력';
$logManager = new SendLogManager();
// 3. [추가] 페이지네이션 설정
$page = (isset($_GET['page']) && (int)$_GET['page'] > 0) ? (int)$_GET['page'] : 1;
$page_rows = 20; // 한 페이지에 보여줄 게시물 수
$total_count = $logManager->getTotalCount();
$total_page = ceil($total_count / $page_rows); // 전체 페이지 계산
$from_record = ($page - 1) * $page_rows; // 시작 레코드 구함
$list = $logManager->getPagedList($from_record, $page_rows);
$paging = get_paging(G5_IS_MOBILE ? $config['cf_mobile_pages'] : $config['cf_write_pages'], $page, $total_page, $_SERVER['SCRIPT_NAME'].'?'.$qstr.'&amp;page=');
// 4. 뷰 파일 포함
include_once(G5_ADMIN_PATH.'/admin.head.php');
include_once(__DIR__.'/templates/send_log_list.php');
include_once(G5_ADMIN_PATH.'/admin.tail.php');
+56
View File
@@ -0,0 +1,56 @@
<?php
$sub_menu = "600500"; // 관리자 메뉴 활성화를 위한 고유 코드
// [개선] GnuBoard의 표준 관리자 공통 파일을 사용하도록 경로를 수정합니다.
include_once(__DIR__."/_common.php");
// 1. 권한 확인
auth_check_menu($auth, $sub_menu, "r");
// 2. 필요한 클래스 포함
require_once(__DIR__ . '/classes/TemplateManager.php');
require_once(__DIR__ . '/classes/MailSender.php');
// 3. POST 요청 처리 (메일 발송)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['token'])) {
auth_check_menu($auth, $sub_menu, 'w');
check_admin_token();
$template_code = trim($_POST['template_code'] ?? '');
$to_email = trim($_POST['to_email'] ?? '');
// [확인] 동적으로 생성된 variables[변수명] 형태의 데이터를 배열로 받습니다.
$vars = $_POST['variables'] ?? [];
// 유효성 검사
if (!filter_var($to_email, FILTER_VALIDATE_EMAIL)) {
alert('올바른 이메일 주소를 입력해주세요.');
}
if (empty($template_code)) {
alert('사용할 템플릿을 선택해주세요.');
}
try {
$mailSender = new MailSender();
$success = $mailSender->send($template_code, $to_email, $vars);
if ($success) {
alert('테스트 메일이 성공적으로 발송되었습니다. 발송 이력 페이지에서 결과를 확인하세요.', './send_log.php');
} else {
alert('메일 발송에 실패했습니다. 발송 이력 페이지에서 오류 메시지를 확인하세요.', './send_log.php');
}
} catch (Exception $e) {
alert('메일 발송 중 시스템 오류가 발생했습니다: ' . $e->getMessage());
}
exit;
}
// 4. GET 요청 처리 (폼 표시)
$g5['title'] = '테스트 메일 발송';
// 폼에 필요한 템플릿 목록을 가져옴
$templateManager = new TemplateManager();
$templates = $templateManager->getAll();
// 뷰(View) 파일 포함
include_once(G5_ADMIN_PATH.'/admin.head.php');
include_once(__DIR__.'/templates/send_test_form.php');
include_once(G5_ADMIN_PATH.'/admin.tail.php');
+93
View File
@@ -0,0 +1,93 @@
<?php
// 1. 환경 설정 및 필수 파일 포함 (가장 먼저 실행)
//ini_set('display_errors', 1);
//error_reporting(E_ALL);
$sub_menu = "600100";
// _common.php는 그누보드 관리자 페이지의 모든 환경(DB, 세션, 권한 등)을 설정합니다.
include_once("./_common.php");
// SmtpConfigManager 클래스를 POST 처리 로직보다 먼저 포함해야 합니다.
include_once(__DIR__.'/classes/SmtpConfigManager.php');
// 2. 권한 확인
// 페이지에 대한 기본 읽기(r) 권한을 확인합니다.
auth_check_menu($auth, $sub_menu, "r");
// 3. 객체 생성 및 변수 초기화
$smtpManager = new SmtpConfigManager();
// GET과 POST 요청 모두에서 action과 id 값을 받을 수 있도록 $_REQUEST를 사용합니다.
$action = $_REQUEST['action'] ?? '';
$id = (int)($_REQUEST['id'] ?? 0);
// 4. POST 요청 처리 (데이터 생성, 수정, 삭제)
// action 값이 있을 때만 POST 로직을 실행하도록 조건을 추가합니다.
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action) {
// 데이터 변경 작업이므로 쓰기(w) 권한을 확인합니다.
auth_check_menu($auth, $sub_menu, 'w');
// CSRF 토큰을 체크하여 유효한 요청인지 확인합니다.
check_admin_token();
// 입력 데이터를 배열로 정리합니다.
$data = [
'name' => $_POST['name'] ?? '',
'host' => $_POST['host'] ?? '',
'username' => $_POST['username'] ?? '',
'password' => preg_replace('/\s+/', '', $_POST['password'] ?? ''), // 비밀번호 공백 제거
'port' => (int)($_POST['port'] ?? 0),
'encryption' => $_POST['encryption'] ?? 'ssl',
'from_email' => $_POST['from_email'] ?? '',
'from_name' => $_POST['from_name'] ?? '',
'is_use' => isset($_POST['is_use']) ? 1 : 0,
];
try {
if ($action === 'create') {
$smtpManager->create($data);
alert('SMTP 설정이 성공적으로 추가되었습니다.', './smtp_config.php');
} elseif ($action === 'update' && $id > 0) {
$smtpManager->update($id, $data);
alert('SMTP 설정이 성공적으로 수정되었습니다.', './smtp_config.php');
} elseif ($action === 'delete' && $id > 0) {
$smtpManager->delete($id);
alert('선택한 SMTP 설정이 삭제되었습니다.', './smtp_config.php');
}
} catch (Exception $e) {
// 데이터베이스 오류 등 예외 발생 시 처리
alert('작업 중 오류가 발생했습니다: '.$e->getMessage());
}
// 작업 완료 후 페이지 이동
goto_url('./smtp_config.php');
exit;
}
// 5. 페이지 제목 설정
$g5['title'] = 'SMTP 설정 관리';
// 6. 뷰(View) 처리 (폼 또는 목록 표시)
// 설정 추가 또는 수정 폼을 표시합니다.
if ($action === 'create_form' || $action === 'edit_form') {
$smtp = null;
if ($action === 'edit_form') {
if (!$id) {
alert('잘못된 접근입니다.');
}
$smtp = $smtpManager->get($id);
if (!$smtp) {
alert('존재하지 않는 SMTP 설정입니다.');
}
}
// smtp_config_form.php 파일 내에 admin.head.php와 admin.tail.php가 포함되어 있습니다.
include_once('./templates/smtp_config_form.php');
exit; // 폼 출력 후 스크립트 실행을 중단합니다.
}
// 기본 페이지: SMTP 설정 목록을 가져옵니다.
$smtp_list = $smtpManager->getAll();
// 목록 페이지의 상단 부분을 포함합니다.
include_once(G5_ADMIN_PATH.'/admin.head.php');
include(__DIR__.'/templates/smtp_config_list.php');
// 목록 페이지의 하단 부분을 포함합니다.
include_once(G5_ADMIN_PATH.'/admin.tail.php');
+99
View File
@@ -0,0 +1,99 @@
<?php
// 1. 환경 설정 및 필수 파일 포함 (가장 먼저 실행)
$sub_menu = "600200"; // 메뉴 활성화를 위한 코드
include_once("./_common.php"); // 그누보드 관리자 공통 파일
require_once(__DIR__ . '/classes/TemplateManager.php'); // 템플릿 관리 클래스
// 2. 권한 확인
auth_check_menu($auth, $sub_menu, "r");
// 3. 객체 생성 및 변수 초기화
$templateManager = new TemplateManager();
$action = $_REQUEST['action'] ?? ''; // GET 또는 POST로 받은 action
$id = (int)($_REQUEST['id'] ?? 0); // GET 또는 POST로 받은 id
// 4. 요청 처리 (POST, GET)
// 4-1. POST 요청 처리 (생성, 수정)
if ($action && $_SERVER['REQUEST_METHOD'] === 'POST') {
// 데이터 변경 작업이므로 쓰기(w) 권한을 확인합니다.
auth_check_menu($auth, $sub_menu, 'w');
// CSRF 토큰을 체크하여 유효한 요청인지 확인합니다.
check_admin_token();
try {
if ($action === 'create' || $action === 'update') {
// 템플릿 폼에서 넘어온 올바른 데이터를 받습니다.
$data = [
'id' => (int)($_POST['id'] ?? 0),
'code' => $_POST['code'] ?? '',
'title' => $_POST['title'] ?? '',
'content' => $_POST['content'] ?? '',
'header_html' => $_POST['header_html'] ?? '',
'footer_html' => $_POST['footer_html'] ?? '',
'is_use' => isset($_POST['is_use']) ? 1 : 0,
'variables' => $_POST['variables'] ?? [], // 변수 기본값 배열
];
// TemplateManager의 save() 메소드로 데이터를 저장합니다.
$templateManager->save($data);
alert('메일 템플릿이 성공적으로 저장되었습니다.', './template.php');
}
} catch (Exception $e) {
alert('작업 중 오류가 발생했습니다: ' . $e->getMessage());
}
goto_url('./template.php');
exit;
}
// 4-2. [개선] GET 요청 처리 (삭제)
if ($action === 'delete' && $id > 0) {
// 삭제(d) 권한을 확인하고, 토큰을 체크합니다. (CSRF 공격 방지)
auth_check_menu($auth, $sub_menu, 'd');
check_admin_token();
$templateManager->delete($id);
alert('선택한 템플릿을 삭제했습니다.', './template.php');
exit;
}
// 5. 뷰(View) 처리: action 값에 따라 다른 페이지를 보여줌
$g5['title'] = '메일 템플릿 관리';
// 5-1. 템플릿 생성 또는 수정 폼을 보여줘야 할 때
if ($action === 'create_form' || $action === 'edit_form') {
$template = null;
$template_vars = [];
if ($action === 'edit_form') {
if (!$id) {
alert('잘못된 접근입니다.');
}
$template = $templateManager->getById($id);
if (!$template) {
alert('존재하지 않는 템플릿입니다.');
}
// DB에서 이 템플릿의 변수와 기본값들을 가져옴
$template_vars = $templateManager->getVarsByTemplateId($id);
}
// [핵심 수정 1] DHTML 에디터 라이브러리 파일을 포함합니다.
// 이 파일에 editor_html(), get_editor_js() 함수가 정의되어 있습니다.
include_once(G5_EDITOR_LIB);
include_once(__DIR__ . '/templates/template_from.php');
exit;
}
// 5-2. 기본 동작: 템플릿 목록 페이지를 보여줌
include_once(G5_ADMIN_PATH . '/admin.head.php');
// [핵심 수정 2] getAll() 메소드는 이미 배열을 반환하므로, while 루프 없이 바로 변수에 할당합니다.
$template_list_data = $templateManager->getAll();
// template_list.php 파일을 불러와 목록을 표시합니다.
include(__DIR__ . '/templates/template_list.php');
include_once(G5_ADMIN_PATH . '/admin.tail.php');
@@ -0,0 +1,61 @@
<?php
if (!defined('_GNUBOARD_')) exit;
/**
* 이메일의 본문 내용을 받아 전체 레이아웃 HTML을 반환하는 함수
*
* @param string $content 본문 내용 HTML
* @return string 완성된 전체 이메일 HTML
*/
function get_mail_layout($content)
{
// HEREDOC 문법을 사용하여 HTML 구조를 정의합니다.
// EOT; 로 끝날 때까지의 모든 내용을 $html 변수에 담습니다.
$html = <<<EOT
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>메일 미리보기</title>
</head>
<body style="margin: 0; padding: 0; -webkit-text-size-adjust: 100%; background-color: #fff;">
<div style="width: 100%; max-width: 600px; margin: 40px auto; background-color: #ffffff; border: 1px solid #dee2e6; border-radius: 8px; overflow: hidden; font-family: 'Malgun Gothic', '맑은 고딕', sans-serif;">
<!-- Header: 모든 메일에 공통으로 적용되는 상단 부분 -->
<div style="padding: 30px; text-align: center; background-color: #f1f3f5; border-bottom: 1px solid #dee2e6;">
<span style="font-size: 14px; color: #868e96; letter-spacing: 1px;">Contact Us</span>
<h2 style="margin: 10px 0 0; font-size: 28px; color: #212529; font-weight: 600;">공간의 가능성을 열어보세요.</h2>
<p style="margin: 10px 0 0; font-size: 16px; color: #495057;">성진미도어의 전문가와 상담하고, 당신의 공간에 꼭 맞는 솔루션을 찾아보세요.</p>
</div>
<!-- Body: 이 부분이 템플릿 에디터의 내용으로 채워집니다. -->
<div style="padding: 30px;">
{$content}
</div>
<!-- Footer: 모든 메일에 공통으로 적용되는 하단 부분 -->
<div style="padding: 20px 30px; background-color: #f1f3f5; border-top: 1px solid #dee2e6; font-size: 14px; color: #868e96;">
<h4 style="margin: 0 0 15px; font-size: 16px; color: #495057;">Information</h4>
<p style="margin: 0 0 10px;">
<strong>본사 및 전시장:</strong> 충청남도 아산시 음봉면 월산로 128-130
</p>
<p style="margin: 0 0 10px;">
<strong>대표 연락처:</strong> T. 041-532-0555 / H. 010-5434-4126
</p>
<p style="margin: 0 0 10px;">
<strong>청주 전시장:</strong> T. 043-235-2352 / H. 010-2066-4126
</p>
<p style="margin: 0;">
<strong>운영시간:</strong> 평일 09:00 - 18:00 (주말 및 공휴일 휴무)
</p>
</div>
</div>
</body>
</html>
EOT;
return $html;
}
?>
@@ -0,0 +1,61 @@
<?php
if (!defined('_GNUBOARD_')) exit;
/**
* 이메일의 본문 내용을 받아 전체 레이아웃 HTML을 반환하는 함수
*
* @param string $content 본문 내용 HTML
* @return string 완성된 전체 이메일 HTML
*/
function get_mail_layout($content)
{
// HEREDOC 문법을 사용하여 HTML 구조를 정의합니다.
// EOT; 로 끝날 때까지의 모든 내용을 $html 변수에 담습니다.
$html = <<<EOT
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>메일 미리보기</title>
</head>
<body style="margin: 0; padding: 0; -webkit-text-size-adjust: 100%; background-color: #f8f9fa;">
<div style="width: 100%; max-width: 600px; margin: 40px auto; background-color: #ffffff; border: 1px solid #dee2e6; border-radius: 8px; overflow: hidden; font-family: 'Malgun Gothic', '맑은 고딕', sans-serif;">
<!-- Header: 모든 메일에 공통으로 적용되는 상단 부분 -->
<div style="padding: 30px; text-align: center; background-color: #f1f3f5; border-bottom: 1px solid #dee2e6;">
<span style="font-size: 14px; color: #868e96; letter-spacing: 1px;">Contact Us</span>
<h2 style="margin: 10px 0 0; font-size: 28px; color: #212529; font-weight: 600;">공간의 가능성을 열어보세요.</h2>
<p style="margin: 10px 0 0; font-size: 16px; color: #495057;">성진미도어의 전문가와 상담하고, 당신의 공간에 꼭 맞는 솔루션을 찾아보세요.</p>
</div>
<!-- Body: 이 부분이 템플릿 에디터의 내용으로 채워집니다. -->
<div style="padding: 30px;">
{$content}
</div>
<!-- Footer: 모든 메일에 공통으로 적용되는 하단 부분 -->
<div style="padding: 20px 30px; background-color: #f1f3f5; border-top: 1px solid #dee2e6; font-size: 14px; color: #868e96;">
<h4 style="margin: 0 0 15px; font-size: 16px; color: #495057;">Information</h4>
<p style="margin: 0 0 10px;">
<strong>본사 및 전시장:</strong> 충청남도 아산시 음봉면 월산로 128-130
</p>
<p style="margin: 0 0 10px;">
<strong>대표 연락처:</strong> T. 041-532-0555 / H. 010-5434-4126
</p>
<p style="margin: 0 0 10px;">
<strong>청주 전시장:</strong> T. 043-235-2352 / H. 010-2066-4126
</p>
<p style="margin: 0;">
<strong>운영시간:</strong> 평일 09:00 - 18:00 (주말 및 공휴일 휴무)
</p>
</div>
</div>
</body>
</html>
EOT;
return $html;
}
?>
@@ -0,0 +1,39 @@
<?php
if (!defined('_GNUBOARD_')) exit;
// 이 파일은 새 메일 템플릿 생성 시 기본으로 로드되는 헤더와 푸터 HTML을 정의합니다.
$default_header_html = <<<EOT
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{메일제목}</title>
</head>
<body style="margin: 0; padding: 0; -webkit-text-size-adjust: 100%; background-color: #f8f9fa;">
<div style="width: 100%; max-width: 600px; margin: 40px auto; background-color: #ffffff; border: 1px solid #dee2e6; border-radius: 8px; overflow: hidden; font-family: 'Malgun Gothic', '맑은 고딕', sans-serif;">
<!-- Header -->
<div style="padding: 30px; text-align: center; background-color: #f1f3f5; border-bottom: 1px solid #dee2e6;">
<h2 style="margin: 10px 0 0; font-size: 28px; color: #212529; font-weight: 600;">공간의 가능성을 열어보세요.</h2>
<p style="margin: 10px 0 0; font-size: 16px; color: #495057;">성진미도어의 전문가와 상담하고, 당신의 공간에 꼭 맞는 솔루션을 찾아보세요.</p>
</div>
EOT;
$default_footer_html = <<<EOT
<!-- Footer -->
<div style="padding: 20px 30px; background-color: #f1f3f5; border-top: 1px solid #dee2e6; font-size: 14px; color: #868e96;">
<h4 style="margin: 0 0 15px; font-size: 16px; color: #495057;">Information</h4>
<p style="margin: 0 0 10px;"><strong>본사 및 전시장:</strong> 충청남도 아산시 음봉면 월산로 128-130</p>
<p style="margin: 0 0 10px;"><strong>대표 연락처:</strong> T. 041-532-0555 / H. 010-5434-4126</p>
<p style="margin: 0 0 10px;"><strong>청주 전시장:</strong> T. 043-235-2352 / H. 010-2066-4126</p>
<p style="margin: 0;"><strong>운영시간:</strong> 평일 09:00 - 18:00 (주말 및 공휴일 휴무)</p>
</div>
</div>
</body>
</html>
EOT;
?>
@@ -0,0 +1,13 @@
<?php
if (!defined('_GNUBOARD_')) exit;
// 이 파일은 새 메일 템플릿 생성 시 기본으로 로드되는 헤더와 푸터 HTML을 정의합니다.
$base_mail_templates_php_path = __DIR__.'/base/mail_layout.php';
$mail_templates_php_path =__DIR__.'/'.$config['cf_theme'].'/mail_layout.php';
if(file_exists($mail_templates_php_path)){
include_once ($mail_templates_php_path);
}else {
include_once ($base_mail_templates_php_path);
}
?>
@@ -0,0 +1,37 @@
<?php
if (!defined('_GNUBOARD_')) exit;
// 이 파일은 새 메일 템플릿 생성 시 기본으로 로드되는 헤더와 푸터 HTML을 정의합니다.
$default_header_html = <<<EOT
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{메일제목}</title>
</head>
<body style="margin: 0; padding: 0; -webkit-text-size-adjust: 100%; background-color: #f8f9fa;">
<div style="width: 100%; max-width: 600px; margin: 40px auto; background-color: #ffffff; border: 1px solid #dee2e6; border-radius: 8px; overflow: hidden; font-family: 'Malgun Gothic', '맑은 고딕', sans-serif;">
<!-- Header -->
<div style="padding: 30px; text-align: center; background-color: #f1f3f5; border-bottom: 1px solid #dee2e6;">
<h2>귀사의 기술을 알리고 싶으신가요?</h2>
<p>월간 레이저 기술이 귀사의 비즈니스 파트너가 되어드립니다.</p>
</div>
EOT;
$default_footer_html = <<<EOT
<!-- Footer -->
<div style="padding: 20px 30px; background-color: #f1f3f5; border-top: 1px solid #dee2e6; font-size: 14px; color: #868e96;">
<h4 style="margin: 0 0 15px; font-size: 16px; color: #495057;">Information</h4>
<p><strong>(주)레이저월드 / (주)한국산업정보센타</strong></p>
<p><span>T. 02-868-0211</span></p>
<p></p>
<p>개인정보보호책임자: laser@laserworld.co.kr</p>
</div>
</body>
</html>
EOT;
?>
@@ -0,0 +1,96 @@
<?php
if (!defined('_GNUBOARD_')) exit;
?>
<div class="local_ov01 local_ov">
<strong><?= $g5['title'] ?></strong>
<p>시스템에서 발송된 모든 이메일의 성공 및 실패 기록을 확인할 수 있습니다.</p>
</div>
<form name="fmaillog" id="fmaillog" method="post">
<input type="hidden" name="token" value="<?php echo get_admin_token(); ?>">
<div class="tbl_head01 tbl_wrap">
<table>
<caption>메일 발송 이력 목록</caption>
<colgroup>
<col class="grid_1"> <!-- Checkbox -->
<col class="grid_1"> <!-- ID -->
<col class="grid_2"> <!-- 받는사람 -->
<col class="grid_2"> <!-- 참조 -->
<col class="grid_2"> <!-- 숨은참조 -->
<col> <!-- 제목 -->
<col class="grid_1"> <!-- 상태 -->
<col class="grid_3"> <!-- 관리 -->
<col class="grid_4"> <!-- 상세 정보 -->
<col class="grid_2"> <!-- 발송 시간 -->
</colgroup>
<thead>
<tr>
<th scope="col"><input type="checkbox" name="chkall" value="1" id="chkall"></th>
<th scope="col">ID</th>
<th scope="col">받는사람</th>
<th scope="col">참조</th>
<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
$is_empty = empty($list);
if (!$is_empty) {
foreach ($list as $row) {
// 상태에 따라 다른 CSS 클래스 적용 (성공: 파란색, 실패: 빨간색)
$status_class = $row['status'] === 'success' ? 'txt_true' : 'txt_done';
?>
<tr>
<td class="td_chk">
<input type="checkbox" name="chk[]" value="<?= $row['id'] ?>" id="chk_<?= $row['id'] ?>">
</td>
<td class="td_num"><?= $row['id'] ?></td>
<td class="td_left" title="<?= htmlspecialchars($row['to_email']) ?>"><?= htmlspecialchars($row['to_email']) ?></td>
<td class="td_left" title="<?= htmlspecialchars($row['cc_email']) ?>"><?= htmlspecialchars($row['cc_email']) ?></td>
<td class="td_left" title="<?= htmlspecialchars($row['bcc_email']) ?>"><?= htmlspecialchars($row['bcc_email']) ?></td>
<td class="td_left"><?= htmlspecialchars($row['subject']) ?></td>
<td class="td_state"><strong class="<?= $status_class ?>"><?= htmlspecialchars(ucfirst($row['status'])) ?></strong></td>
<td class="td_mng td_mng_s">
<button type="button" class="btn btn_03 preview-btn" data-content="<?php echo base64_encode(htmlspecialchars_decode(stripslashes($row['body']))); ?>">본문보기</button>
<button type="button" class="btn btn_01 resend-btn" data-id="<?= $row['id'] ?>">재발송</button>
</td>
<td class="td_left" style="color: #666;"><?= htmlspecialchars($row['error_msg']) ?: '-' ?></td>
<td class="td_datetime"><?= $row['send_time'] ?: 'N/A' ?></td>
</tr>
<?php
}
}
if ($is_empty) {
echo '<tr><td colspan="10" class="empty_table">발송된 이력이 없습니다.</td></tr>';
}
?>
</tbody>
</table>
</div>
<div class="btn_list01 btn_list">
<button type="button" id="bulk-resend-btn" class="btn btn_02">선택 재발송</button>
</div>
</form>
<?php echo $paging; ?>
<?php
// [추가] AJAX 재발송 요청을 위한 URL을 자바스크립트에 전달합니다.
?>
<script>
const ajax_resend_url = "<?php echo G5_ADMIN_URL; ?>/mail_manage/ajax_resend_mail.php";
</script>
<?php
// 이 페이지에서 사용할 자바스크립트 파일을 로드합니다.
add_javascript('<script src="'.G5_ADMIN_URL.'/mail_manage/assets/js/send_log_list.js?ver='.G5_JS_VER.'"></script>', 10);
?>
@@ -0,0 +1,63 @@
<?php
if (!defined('_GNUBOARD_')) exit;
?>
<div class="local_desc01 local_desc">
<p>
이 페이지에서 등록된 템플릿을 사용하여 테스트 메일을 발송할 수 있습니다.<br>
템플릿을 선택하면 해당 템플릿에 정의된 변수 입력란이 자동으로 나타납니다.
</p>
</div>
<form name="sendTestForm" id="sendTestForm" method="post" action="./send_test.php">
<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="to_email">받는 사람 이메일</label></th>
<td>
<input type="email" name="to_email" id="to_email" required class="required frm_input" size="40" placeholder="test@example.com">
</td>
</tr>
<tr>
<th scope="row"><label for="template_code">메일 템플릿</label></th>
<td>
<select name="template_code" id="template_code" required class="required">
<option value="">템플릿을 선택하세요</option>
<?php foreach ($templates as $tpl): ?>
<?php if ($tpl['is_use']): // 사용 중인 템플릿만 표시 ?>
<option value="<?php echo htmlspecialchars($tpl['code']); ?>">
<?php echo htmlspecialchars($tpl['title']); ?> (코드: <?php echo htmlspecialchars($tpl['code']); ?>)
</option>
<?php endif; ?>
<?php endforeach; ?>
</select>
</td>
</tr>
</tbody>
</table>
</div>
<!-- [수정] 템플릿 변수 입력 필드가 동적으로 추가될 영역 -->
<div id="variable_fields" class="tbl_frm01 tbl_wrap" style="display: none; margin-top:0;">
<!-- 템플릿을 선택하면 이곳에 변수 입력란이 자동으로 생성됩니다. -->
</div>
<div class="btn_confirm01 btn_confirm">
<input type="submit" value="테스트 발송하기" class="btn_submit btn" accesskey="s">
</div>
</form>
<script>
const ajax_template_vars_url = "<?php echo G5_ADMIN_URL; ?>/mail_manage/ajax_get_template_vars.php";
</script>
<?php
add_javascript('<script src="'.G5_ADMIN_URL.'/mail_manage/assets/js/send_test_form.js?ver='.G5_JS_VER.'"></script>', 10);
@@ -0,0 +1,92 @@
<?php
if (!defined('_GNUBOARD_')) exit;
$g5['title'] = $smtp ? 'SMTP 설정 수정' : 'SMTP 설정 추가';
include_once(G5_ADMIN_PATH.'/admin.head.php');
$row = $smtp ?? [
'id' => 0,
'name' => '',
'host' => '',
'username' => '',
'password' => '',
'port' => 465,
'encryption' => 'ssl',
'from_email' => '',
'from_name' => '',
'is_use' => 1,
];
?>
<form name="fsmtpconfig" id="fsmtpconfig" method="post" action="./smtp_config.php">
<input type="hidden" name="action" value="<?php echo $row['id'] ? 'update' : 'create'; ?>">
<input type="hidden" name="id" value="<?php echo (int)$row['id']; ?>">
<input type="hidden" name="token" value="<?php echo get_admin_token(); ?>">
<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="name">이름(용도)</label></th>
<td><input type="text" name="name" id="name" required class="required frm_input" size="40" value="<?php echo get_text($row['name']); ?>"></td>
</tr>
<tr>
<th scope="row"><label for="host">SMTP 호스트</label></th>
<td><input type="text" name="host" id="host" required class="required frm_input" size="40" value="<?php echo get_text($row['host']); ?>"></td>
</tr>
<tr>
<th scope="row"><label for="username">계정(아이디)</label></th>
<td><input type="text" name="username" id="username" required class="required frm_input" size="40" value="<?php echo get_text($row['username']); ?>"></td>
</tr>
<tr>
<th scope="row"><label for="password">비밀번호</label></th>
<td>
<input type="password" name="password" id="password" class="frm_input" size="40" <?php if (!$row['id']) echo 'required'; ?>>
<?php if ($row['id']) { ?>
<span class="frm_info">비밀번호를 변경할 경우에만 입력하세요.</span>
<?php } ?>
</td>
</tr>
<tr>
<th scope="row"><label for="port">포트</label></th>
<td><input type="number" name="port" id="port" required class="required frm_input" size="10" value="<?php echo get_text($row['port']); ?>"></td>
</tr>
<tr>
<th scope="row"><label for="encryption">암호화 방식</label></th>
<td>
<select name="encryption" id="encryption" required>
<option value="none" <?php echo ($row['encryption']=='none')?'selected':''; ?>>none</option>
<option value="ssl" <?php echo ($row['encryption']=='ssl')?'selected':''; ?>>SSL</option>
<option value="tls" <?php echo ($row['encryption']=='tls')?'selected':''; ?>>TLS</option>
</select>
</td>
</tr>
<tr>
<th scope="row"><label for="from_email">발신자 이메일</label></th>
<td><input type="email" name="from_email" id="from_email" required class="required frm_input" size="40" value="<?php echo get_text($row['from_email']); ?>"></td>
</tr>
<tr>
<th scope="row"><label for="from_name">발신자 이름</label></th>
<td><input type="text" name="from_name" id="from_name" required class="required frm_input" size="40" value="<?php echo get_text($row['from_name']); ?>"></td>
</tr>
<tr>
<th scope="row"><label for="is_use">사용 여부</label></th>
<td><input type="checkbox" name="is_use" id="is_use" value="1" <?php echo $row['is_use'] ? 'checked' : ''; ?>></td>
</tr>
</tbody>
</table>
</div>
<div class="btn_confirm01 btn_confirm">
<a href="./smtp_config.php" class="btn_cancel btn">목록으로</a>
<input type="submit" value="확인" class="btn_submit btn" accesskey="s">
</div>
</form>
<?php include_once(G5_ADMIN_PATH.'/admin.tail.php'); ?>
@@ -0,0 +1,60 @@
<?php
if (!defined('_GNUBOARD_')) exit;
?>
<div class="local_ov01 local_ov">
<strong>SMTP 설정 목록</strong>
<p>이곳에서 메일 발송에 사용할 여러 SMTP 서버를 등록하고 관리할 수 있습니다.</p>
</div>
<div class="btn_fixed_top">
<a href="?action=create_form" class="btn btn_01">SMTP 설정 추가</a>
</div>
<div class="tbl_head01 tbl_wrap">
<table>
<caption>SMTP 설정 목록</caption>
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">이름(용도)</th>
<th scope="col">호스트</th>
<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 ($smtp_list && count($smtp_list) > 0): ?>
<?php foreach ($smtp_list as $row): ?>
<tr>
<td class="td_num"><?php echo $row['id']; ?></td>
<td><?php echo get_text($row['name']); ?></td>
<td><?php echo get_text($row['host']); ?></td>
<td><?php echo get_text($row['username']); ?></td>
<td class="td_num"><?php echo $row['port']; ?></td>
<td class="td_num"><?php echo strtoupper($row['encryption']); ?></td>
<td><?php echo get_text($row['from_email']); ?></td>
<td class="td_num"><?php echo $row['is_use'] ? '<strong class="txt_true">사용</strong>' : '미사용'; ?></td>
<td class="td_mng">
<a href="?action=edit_form&amp;id=<?php echo $row['id']; ?>" class="btn btn_03">수정</a>
<form name="fdelete_<?php echo $row['id']; ?>" action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post" style="display:inline;">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="<?php echo $row['id']; ?>">
<!-- token 값은 admin.js 에서 자동으로 채워주므로 비워둬도 괜찮습니다. -->
<input type="hidden" name="token" value="">
<button type="submit" class="btn btn_02" onclick="return confirm('정말 삭제하시겠습니까?');">삭제</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="9" class="empty_table">등록된 SMTP 설정이 없습니다.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
+157
View File
@@ -0,0 +1,157 @@
<?php
if (!defined('_GNUBOARD_')) exit;
// $template 변수가 존재하면 '수정', 없으면 '추가'로 제목 설정
$g5['title'] = isset($template) && $template['id'] ? '메일 템플릿 수정' : '메일 템플릿 추가';
// [수정] DHTML 에디터를 사용하므로 admin.head.php를 먼저 include 합니다.
include_once(G5_ADMIN_PATH.'/admin.head.php');
// [수정] '추가' 모드일 때 기본 레이아웃을 로드하고, '수정' 모드일 때는 DB 데이터를 사용합니다.
if (isset($template) && $template['id']) {
// 수정 모드
$row = $template;
} else {
// 추가 모드: 기본 레이아웃 파일을 불러와 기본값을 설정합니다.
// 파일 경로는 사용자가 지정한 'mail_layout' 디렉토리를 사용합니다.
include_once(__DIR__.'/mail_layout/mail_layout.php');
$row = [
'id' => 0,
'code' => '',
'title' => '',
'content' => '<p>이곳에 메일 본문 내용을 입력하세요.</p>',
'header_html' => $default_header_html ?? '',
'footer_html' => $default_footer_html ?? '',
'is_use' => 1,
];
}
?>
<!-- [삭제] Summernote 관련 CSS, JS 링크를 모두 제거합니다. -->
<style>
/* 템플릿 폼 레이아웃을 위한 CSS */
.template-container { display: flex; gap: 20px; margin-top: 20px; }
.template-editor { flex: 3; } /* 에디터 영역을 더 넓게 */
.template-vars { flex: 1; background: #f9f9f9; padding: 15px; border: 1px solid #ddd; border-radius: 4px; }
.template-vars h3 { margin-top: 0; padding-bottom: 10px; border-bottom: 1px solid #eee; }
.template-vars label { display: block; margin-top: 10px; font-weight: bold; }
/* [추가] 헤더/푸터 textarea 스타일 */
.template-layout-editor { margin-bottom: 20px; }
.template-layout-editor h3 { margin: 0 0 5px; font-size: 1.1em; }
.template-layout-editor textarea { width: 100%; height: 150px; padding: 10px; border: 1px solid #ccc; border-radius: 4px; font-family: consolas, monospace; font-size: 1em; }
</style>
<div class="local_desc01 local_desc">
<p>
메일 내용에 <code>{변수명}</code> 형식으로 입력하면 오른쪽에 해당 변수에 대한 테스트 및 기본값 입력 창이 자동으로 생성됩니다.<br>
이곳에 입력된 값은 템플릿과 함께 저장되며, 실제 발송 시 다른 값으로 치환되지 않았을 때의 기본값으로 사용될 수 있습니다.
</p>
</div>
<!-- [수정] onsubmit 이벤트를 추가하여 에디터 내용을 전송前に textarea에 반영합니다. -->
<form name="templateForm" id="templateForm" method="post" action="./template.php" onsubmit="return form_check(this);">
<input type="hidden" name="action" value="<?php echo $row['id'] ? 'update' : 'create'; ?>">
<input type="hidden" name="id" value="<?php echo (int)$row['id']; ?>">
<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="code">템플릿 코드</label></th>
<td>
<input type="text" name="code" id="code" required class="required frm_input" size="40" value="<?php echo get_text($row['code']); ?>">
<?php if ($row['id']): ?>
<span class="frm_info">이 코드는 템플릿을 식별하는 고유 값이므로 변경에 주의하세요.</span>
<?php else: ?>
<span class="frm_info">영문, 숫자, 언더바(_)만 사용 가능하며, 다른 템플릿과 중복될 수 없습니다. (예: `member_join`)</span>
<?php endif; ?>
</td>
</tr>
<tr>
<th scope="row"><label for="title">이메일 제목</label></th>
<td>
<input type="text" name="title" id="title" required class="required frm_input" size="80" value="<?php echo get_text($row['title']); ?>" placeholder="예: {쇼핑몰명} - {username}님의 주문이 완료되었습니다.">
</td>
</tr>
<tr>
<th scope="row"><label for="is_use">사용 여부</label></th>
<td><input type="checkbox" name="is_use" id="is_use" value="1" <?php echo $row['is_use'] ? 'checked' : ''; ?>></td>
</tr>
</tbody>
</table>
<!-- [추가] 헤더 HTML 입력 영역 -->
<div class="template-layout-editor">
<h3>헤더 HTML</h3>
<p class="frm_info">메일 본문 내용 위에 위치할 상단 레이아웃입니다. (로고, 인사말 등) 변수 감지 기능은 지원하지 않습니다.</p>
<?php echo editor_html('header_html', get_text(stripslashes(htmlspecialchars_decode($row['header_html'], 1)))); ?>
</div>
<div class="template-container">
<div class="template-editor">
<h3>이메일 내용 (HTML)</h3>
<!-- [수정] GnuBoard DHTML 에디터(SmartEditor2)로 변경 -->
<!-- 메일의 핵심 본문 영역입니다. -->
<?php echo editor_html('content', get_text(stripslashes(htmlspecialchars_decode($row['content'], 1)))); ?>
</div>
<div class="template-vars">
<h3><strong>변수 기본값 설정</strong></h3>
<p class="frm_info">'이메일 내용' 영역에서 감지된 변수 목록입니다.</p>
<div id="variableInputs">
<!-- 변수 감지 시 이곳에 입력 필드가 자동으로 생성됩니다. -->
</div>
</div>
</div>
<div class="btn_confirm01 btn_confirm">
<!-- <a href="./template.php" class="btn_cancel btn">목록</a>-->
<!-- [개선] 변수 새로고침 버튼 추가 -->
<button type="button" onclick="refreshVariables()" class="btn">변수 새로고침</button>
<!-- <button type="button" onclick="previewTemplate()" class="btn">미리보기</button>-->
<!-- <input type="submit" value="저장하기" class="btn_submit btn" accesskey="s">-->
</div>
<!-- [추가] 푸터 HTML 입력 영역 -->
<div class="template-layout-editor" style="margin-top: 20px;">
<h3>푸터 HTML</h3>
<p class="frm_info">메일 본문 내용 아래에 위치할 하단 레이아웃입니다. (회사 정보, 저작권 등) 변수 감지 기능은 지원하지 않습니다.</p>
<?php echo editor_html('footer_html', get_text(stripslashes(htmlspecialchars_decode($row['footer_html'], 1)))); ?>
</div>
</div>
<div class="btn_confirm01 btn_confirm">
<a href="./template.php" class="btn_cancel btn">목록</a>
<!-- [개선] 변수 새로고침 버튼 추가 -->
<button type="button" onclick="refreshVariables()" class="btn">변수 새로고침</button>
<button type="button" onclick="previewTemplate()" class="btn">미리보기</button>
<input type="submit" value="저장하기" class="btn_submit btn" accesskey="s">
</div>
</form>
<!-- PHP의 템플릿 변수 데이터를 JavaScript에서 사용할 수 있도록 변환 -->
<script>
const serverVars = <?php echo json_encode($template_vars ?? []); ?>;
</script>
<?php
// [수정] template_form.js 파일을 GnuBoard의 add_javascript 함수를 통해 로드합니다.
// 이렇게 하면 jQuery 라이브러리가 먼저 로드된 후 이 스크립트가 실행되어 'jQuery is not defined' 오류를 해결합니다.
// URL은 웹 경로로 지정해야 합니다.
add_javascript('<script src="'.G5_ADMIN_URL.'/mail_manage/assets/js/template.js"></script>', 1);
add_javascript('<script src="'.G5_ADMIN_URL.'/mail_manage/assets/js/template_form.js"></script>', 1);
// [수정] GnuBoard DHTML 에디터의 JS 파일을 로드합니다.
// 이 함수는 에디터를 활성화하는 필수 스크립트를 생성하며, 반드시 호출해야 합니다.
//echo get_editor_js('header_html');
//echo get_editor_js('content');
//echo get_editor_js('footer_html');
include_once(G5_ADMIN_PATH.'/admin.tail.php');
?>
@@ -0,0 +1,77 @@
<?php
if (!defined('_GNUBOARD_')) exit;
?>
<div class="local_ov01 local_ov">
<strong>메일 템플릿 목록</strong>
<p>이곳에서 메일 발송에 사용할 여러 템플릿을 등록하고 관리할 수 있습니다.</p>
</div>
<div class="btn_fixed_top">
<a href="?action=create_form" class="btn btn_01">메일 템플릿 추가</a>
</div>
<div class="tbl_head01 tbl_wrap">
<table>
<caption>메일 템플릿 목록</caption>
<colgroup>
<col class="grid_1"> <!-- ID -->
<col class="grid_3"> <!-- 코드 -->
<col> <!-- 제목 -->
<col class="grid_2"> <!-- 미리보기 -->
<col class="grid_1"> <!-- 사용 -->
<col class="grid_3"> <!-- 관리 -->
</colgroup>
<thead>
<tr>
<th scope="col">ID</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 (!empty($template_list_data)):
foreach ($template_list_data as $row):
?>
<tr>
<td class="td_num"><?= $row['id'] ?></td>
<td class="td_left"><?= htmlspecialchars($row['code']) ?></td>
<td class="td_left"><?= htmlspecialchars($row['title']) ?></td>
<td>
<!-- [개선] 헤더, 본문, 푸터를 합친 전체 내용을 미리보기 하도록 수정합니다. -->
<?php $full_content = stripslashes(htmlspecialchars_decode($row['header_html'] ?? '') . ($row['content'] ?? '') . ($row['footer_html'] ?? '')); ?>
<button type="button" class="btn btn_03 preview-btn" data-content="<?php echo base64_encode($full_content); ?>">미리보기</button>
</td>
<td class="td_num"><?= $row['is_use'] ? '<strong class="txt_true">사용</strong>' : '미사용' ?></td>
<td class="td_mng td_mng_m">
<a href="./template.php?action=edit_form&id=<?= $row['id'] ?>" class="btn btn_03">수정</a>
<!-- 💡 [개선] 삭제 링크에서 token을 직접 생성하지 않고, delete_confirm() 함수가 CSRF 토큰을 자동으로 처리하도록 합니다. -->
<a href="./template.php?action=delete&id=<?= $row['id'] ?>" onclick="return delete_confirm(this);" class="btn btn_02">삭제</a>
</td>
</tr>
<?php
endforeach;
else:
?>
<tr>
<td colspan="6" class="empty_table">등록된 템플릿이 없습니다.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
// [개선] GnuBoard의 자바스크립트 로드 방식을 따라 add_javascript() 함수를 사용합니다.
add_javascript('<script src="'.G5_ADMIN_URL.'/mail_manage/assets/js/template_list.js"></script>', 1);
?>
<noscript>
<p>
귀하께서 사용하시는 브라우저는 현재 <strong>자바스크립트를 사용하지 않음</strong>으로 설정되어 있습니다.<br>
<strong>자바스크립트를 사용하지 않음</strong>으로 설정하신 경우는 수정이나 삭제시 별도의 경고창이 나오지 않으므로 이점 주의하시기 바랍니다.
</p>
</noscript>