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 @@
.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('미리보기 내용을 여는 데 실패했습니다.');
}
});
});
});