first commit 2
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* 통계 페이지 JavaScript
|
||||
*/
|
||||
|
||||
// 전역 변수
|
||||
let responseChart;
|
||||
|
||||
// 페이지 로드 시 초기화
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 데이터가 로드될 때까지 대기
|
||||
if (typeof window.statisticsData !== 'undefined') {
|
||||
initStatistics();
|
||||
} else {
|
||||
// 데이터 로드 대기
|
||||
setTimeout(() => {
|
||||
if (typeof window.statisticsData !== 'undefined') {
|
||||
initStatistics();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
function initStatistics() {
|
||||
// 차트 초기화
|
||||
initResponseChart();
|
||||
initQuestionCharts();
|
||||
|
||||
// 차트 타입 변경 이벤트
|
||||
initChartControls();
|
||||
|
||||
// 숫자 애니메이션 실행
|
||||
setTimeout(animateNumbers, 500);
|
||||
}
|
||||
|
||||
// 응답 현황 차트 초기화
|
||||
function initResponseChart() {
|
||||
const ctx = document.getElementById('responseChart');
|
||||
if (!ctx || !window.statisticsData) return;
|
||||
|
||||
const dailyData = window.statisticsData.chartData.daily;
|
||||
|
||||
// 일별 데이터 준비 (최근 30일)
|
||||
const dailyLabels = [];
|
||||
const dailyValues = [];
|
||||
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
const label = date.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' });
|
||||
|
||||
dailyLabels.push(label);
|
||||
dailyValues.push(dailyData[dateStr] || 0);
|
||||
}
|
||||
|
||||
responseChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: dailyLabels,
|
||||
datasets: [{
|
||||
label: '일별 응답 수',
|
||||
data: dailyValues,
|
||||
borderColor: '#AA20FF',
|
||||
backgroundColor: 'rgba(170, 32, 255, 0.1)',
|
||||
borderWidth: 3,
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
stepSize: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 응답 차트 업데이트
|
||||
function updateResponseChart(type) {
|
||||
if (!responseChart || !window.statisticsData) return;
|
||||
|
||||
const dailyData = window.statisticsData.chartData.daily;
|
||||
const hourlyData = window.statisticsData.chartData.hourly;
|
||||
|
||||
if (type === 'hourly') {
|
||||
// 시간대별 데이터 준비
|
||||
const hourlyLabels = [];
|
||||
const hourlyValues = [];
|
||||
|
||||
for (let i = 0; i < 24; i++) {
|
||||
hourlyLabels.push(i + '시');
|
||||
hourlyValues.push(hourlyData[i] || 0);
|
||||
}
|
||||
|
||||
responseChart.data.labels = hourlyLabels;
|
||||
responseChart.data.datasets[0].data = hourlyValues;
|
||||
responseChart.data.datasets[0].label = '시간대별 응답 수';
|
||||
responseChart.options.scales.x.title = { display: true, text: '시간' };
|
||||
} else {
|
||||
// 일별 데이터로 복원
|
||||
const dailyLabels = [];
|
||||
const dailyValues = [];
|
||||
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
const label = date.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' });
|
||||
|
||||
dailyLabels.push(label);
|
||||
dailyValues.push(dailyData[dateStr] || 0);
|
||||
}
|
||||
|
||||
responseChart.data.labels = dailyLabels;
|
||||
responseChart.data.datasets[0].data = dailyValues;
|
||||
responseChart.data.datasets[0].label = '일별 응답 수';
|
||||
responseChart.options.scales.x.title = { display: true, text: '날짜' };
|
||||
}
|
||||
|
||||
responseChart.update();
|
||||
}
|
||||
|
||||
// 질문별 차트 초기화
|
||||
function initQuestionCharts() {
|
||||
// PHP에서 전달된 질문 데이터를 기반으로 차트 생성
|
||||
// 이 부분은 PHP에서 JavaScript로 데이터를 전달받아 처리
|
||||
}
|
||||
|
||||
// 차트 컨트롤 이벤트
|
||||
function initChartControls() {
|
||||
document.querySelectorAll('.chart-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
document.querySelectorAll('.chart-btn').forEach(b => b.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
const chartType = this.dataset.chart;
|
||||
updateResponseChart(chartType);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 숫자 애니메이션
|
||||
function animateNumbers() {
|
||||
document.querySelectorAll('.stat-number').forEach(element => {
|
||||
const target = parseInt(element.textContent.replace(/,/g, ''));
|
||||
let current = 0;
|
||||
const increment = target / 50;
|
||||
const timer = setInterval(() => {
|
||||
current += increment;
|
||||
if (current >= target) {
|
||||
current = target;
|
||||
clearInterval(timer);
|
||||
}
|
||||
element.textContent = Math.floor(current).toLocaleString('ko-KR');
|
||||
}, 20);
|
||||
});
|
||||
}
|
||||
|
||||
// 인쇄 기능
|
||||
function printStatistics() {
|
||||
if (!window.statisticsData) return;
|
||||
|
||||
const printWindow = window.open('', '_blank');
|
||||
const data = window.statisticsData;
|
||||
|
||||
const printContent = `
|
||||
<html>
|
||||
<head>
|
||||
<title>설문 통계 - ${data.survey.title}</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.header { text-align: center; margin-bottom: 30px; }
|
||||
.stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 30px; }
|
||||
.stat-item { text-align: center; padding: 15px; border: 1px solid #ddd; }
|
||||
.stat-number { font-size: 2em; font-weight: bold; color: #AA20FF; }
|
||||
.question { margin-bottom: 30px; page-break-inside: avoid; }
|
||||
.question-title { font-size: 1.2em; font-weight: bold; margin-bottom: 15px; }
|
||||
.answer-item { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
|
||||
@media print { .no-print { display: none; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>설문 통계</h1>
|
||||
<h2>${data.survey.title}</h2>
|
||||
<p>생성일: ${new Date().toLocaleDateString('ko-KR')}</p>
|
||||
</div>
|
||||
|
||||
<div class="stat-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">${data.stats.totalResponses}</div>
|
||||
<div>완료된 응답</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">${data.stats.completionRate}%</div>
|
||||
<div>완료율</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">${data.stats.avgResponseTime}분</div>
|
||||
<div>평균 응답시간</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">${data.stats.startedResponses}</div>
|
||||
<div>진행중인 응답</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${document.querySelector('.questions-stats') ? document.querySelector('.questions-stats').innerHTML : ''}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
printWindow.document.write(printContent);
|
||||
printWindow.document.close();
|
||||
printWindow.print();
|
||||
}
|
||||
|
||||
// 차트 생성 헬퍼 함수
|
||||
function createQuestionChart(canvasId, chartData, chartType = 'doughnut') {
|
||||
const ctx = document.getElementById(canvasId);
|
||||
if (!ctx) return;
|
||||
|
||||
const colorPalette = ['#AA20FF', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8'];
|
||||
|
||||
new Chart(ctx, {
|
||||
type: chartType,
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
datasets: [{
|
||||
data: chartData.data,
|
||||
backgroundColor: colorPalette.slice(0, chartData.data.length),
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
padding: 15,
|
||||
usePointStyle: true
|
||||
}
|
||||
}
|
||||
},
|
||||
...(chartType === 'bar' ? {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
stepSize: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} : {})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 데이터 내보내기 함수들
|
||||
function exportToExcel(surveyId) {
|
||||
window.location.href = `export.php?sv_id=${surveyId}&format=excel`;
|
||||
}
|
||||
|
||||
function exportToCSV(surveyId) {
|
||||
window.location.href = `export.php?sv_id=${surveyId}&format=csv`;
|
||||
}
|
||||
|
||||
// 유틸리티 함수들
|
||||
const StatisticsUtils = {
|
||||
formatNumber: function(num) {
|
||||
return new Intl.NumberFormat('ko-KR').format(num);
|
||||
},
|
||||
|
||||
formatPercentage: function(num) {
|
||||
return num.toFixed(1) + '%';
|
||||
},
|
||||
|
||||
calculatePercentage: function(value, total) {
|
||||
return total > 0 ? (value / total) * 100 : 0;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,214 @@
|
||||
// 설문 관리 관리자 JavaScript
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 확인 대화상자
|
||||
const confirmButtons = document.querySelectorAll('[onclick*="confirm"]');
|
||||
confirmButtons.forEach(button => {
|
||||
button.addEventListener('click', function(e) {
|
||||
const message = this.getAttribute('onclick').match(/confirm\('([^']+)'\)/);
|
||||
if (message && !confirm(message[1])) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 툴팁 초기화
|
||||
const tooltips = document.querySelectorAll('[title]');
|
||||
tooltips.forEach(element => {
|
||||
element.addEventListener('mouseenter', function() {
|
||||
const title = this.getAttribute('title');
|
||||
if (title) {
|
||||
this.setAttribute('data-original-title', title);
|
||||
this.removeAttribute('title');
|
||||
|
||||
const tooltip = document.createElement('div');
|
||||
tooltip.className = 'tooltip';
|
||||
tooltip.textContent = title;
|
||||
tooltip.style.cssText = `
|
||||
position: absolute;
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
document.body.appendChild(tooltip);
|
||||
|
||||
const rect = this.getBoundingClientRect();
|
||||
tooltip.style.left = rect.left + (rect.width / 2) - (tooltip.offsetWidth / 2) + 'px';
|
||||
tooltip.style.top = rect.top - tooltip.offsetHeight - 5 + 'px';
|
||||
|
||||
this._tooltip = tooltip;
|
||||
}
|
||||
});
|
||||
|
||||
element.addEventListener('mouseleave', function() {
|
||||
if (this._tooltip) {
|
||||
document.body.removeChild(this._tooltip);
|
||||
this._tooltip = null;
|
||||
}
|
||||
|
||||
const originalTitle = this.getAttribute('data-original-title');
|
||||
if (originalTitle) {
|
||||
this.setAttribute('title', originalTitle);
|
||||
this.removeAttribute('data-original-title');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 자동 저장 기능 (폼이 있는 경우)
|
||||
const forms = document.querySelectorAll('form');
|
||||
forms.forEach(form => {
|
||||
const inputs = form.querySelectorAll('input, textarea, select');
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener('change', function() {
|
||||
// 자동 저장 로직 (필요시 구현)
|
||||
console.log('Form data changed:', this.name, this.value);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 유틸리티 함수들
|
||||
window.SurveyAdmin = window.SurveyAdmin || {
|
||||
// 숫자 포맷팅
|
||||
formatNumber: function(num) {
|
||||
return new Intl.NumberFormat('ko-KR').format(num);
|
||||
},
|
||||
|
||||
// 날짜 포맷팅
|
||||
formatDate: function(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('ko-KR');
|
||||
},
|
||||
|
||||
// 시간 포맷팅
|
||||
formatDateTime: function(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('ko-KR');
|
||||
},
|
||||
|
||||
// AJAX 요청
|
||||
ajax: function(url, data, callback) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
callback(null, response);
|
||||
} catch (e) {
|
||||
callback(null, xhr.responseText);
|
||||
}
|
||||
} else {
|
||||
callback(new Error('Request failed: ' + xhr.status));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const params = new URLSearchParams(data).toString();
|
||||
xhr.send(params);
|
||||
},
|
||||
|
||||
// 알림 표시
|
||||
showAlert: function(message, type = 'info') {
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.textContent = message;
|
||||
alert.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
min-width: 300px;
|
||||
animation: slideInRight 0.3s ease;
|
||||
`;
|
||||
|
||||
document.body.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
alert.style.animation = 'slideOutRight 0.3s ease';
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 300);
|
||||
}, 3000);
|
||||
},
|
||||
|
||||
// 로딩 표시
|
||||
showLoading: function(show = true) {
|
||||
let loading = document.getElementById('loading-overlay');
|
||||
|
||||
if (show) {
|
||||
if (!loading) {
|
||||
loading = document.createElement('div');
|
||||
loading.id = 'loading-overlay';
|
||||
loading.innerHTML = `
|
||||
<div style="
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
">
|
||||
<div style="
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
">
|
||||
<div style="
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #AA20FF;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 10px;
|
||||
"></div>
|
||||
<div>처리 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(loading);
|
||||
}
|
||||
} else {
|
||||
if (loading) {
|
||||
loading.parentNode.removeChild(loading);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// CSS 애니메이션 추가
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideInRight {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideOutRight {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(100%); opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
@@ -0,0 +1,394 @@
|
||||
/**
|
||||
* 설문 작성/수정 폼 JavaScript
|
||||
*/
|
||||
|
||||
let questionCount = 0;
|
||||
const questionTypes = {
|
||||
'text': '단답형',
|
||||
'textarea': '장문형',
|
||||
'radio': '단일선택',
|
||||
'checkbox': '다중선택',
|
||||
'select': '드롭다운',
|
||||
'rating': '평점',
|
||||
'date': '날짜'
|
||||
};
|
||||
|
||||
// 페이지 로드 시 초기화
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initSurveyForm();
|
||||
});
|
||||
|
||||
function initSurveyForm() {
|
||||
// 탭 전환 이벤트
|
||||
initTabs();
|
||||
|
||||
// 템플릿 선택 이벤트
|
||||
initTemplateSelection();
|
||||
|
||||
// 폼 검증 이벤트
|
||||
initFormValidation();
|
||||
|
||||
// 기존 질문들의 타입에 따라 옵션 표시
|
||||
document.querySelectorAll('.question-type-select').forEach(select => {
|
||||
updateQuestionType(select);
|
||||
});
|
||||
|
||||
// URL에서 template_id가 있으면 해당 템플릿 자동 선택
|
||||
const templateId = document.getElementById('templateId')?.value;
|
||||
if (templateId > 0) {
|
||||
const templateCard = document.querySelector(`[data-template="${templateId}"]`);
|
||||
if (templateCard) {
|
||||
templateCard.click();
|
||||
}
|
||||
}
|
||||
|
||||
// 질문 개수 초기화
|
||||
questionCount = document.querySelectorAll('.question-item').length;
|
||||
}
|
||||
|
||||
// 탭 전환 기능
|
||||
function initTabs() {
|
||||
document.querySelectorAll('.form-tab').forEach(tab => {
|
||||
tab.addEventListener('click', function() {
|
||||
const targetTab = this.dataset.tab;
|
||||
|
||||
// 탭 활성화
|
||||
document.querySelectorAll('.form-tab').forEach(t => t.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
// 콘텐츠 표시
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
document.getElementById(targetTab).classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 템플릿 선택 기능
|
||||
function initTemplateSelection() {
|
||||
document.querySelectorAll('.template-card').forEach(card => {
|
||||
card.addEventListener('click', function() {
|
||||
document.querySelectorAll('.template-card').forEach(c => c.classList.remove('selected'));
|
||||
this.classList.add('selected');
|
||||
|
||||
const templateId = this.dataset.template;
|
||||
document.getElementById('templateId').value = templateId;
|
||||
|
||||
// 템플릿 질문 로드
|
||||
if (templateId > 0) {
|
||||
loadTemplateQuestions(templateId);
|
||||
} else {
|
||||
// 직접 작성 선택 시 기존 질문들 초기화
|
||||
document.getElementById('questionsList').innerHTML = '';
|
||||
questionCount = 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 템플릿 질문 로드
|
||||
function loadTemplateQuestions(templateId) {
|
||||
fetch(`ajax_get_template.php?st_id=${templateId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// 기존 질문들 초기화
|
||||
document.getElementById('questionsList').innerHTML = '';
|
||||
questionCount = 0;
|
||||
|
||||
// 템플릿 질문들 추가
|
||||
data.questions.forEach((question, index) => {
|
||||
addTemplateQuestion(question, index + 1);
|
||||
});
|
||||
|
||||
// 기본 정보도 템플릿에서 가져오기
|
||||
if (data.template.st_name) {
|
||||
document.querySelector('input[name="sv_title"]').value = data.template.st_name;
|
||||
}
|
||||
if (data.template.st_description) {
|
||||
document.querySelector('textarea[name="sv_description"]').value = data.template.st_description;
|
||||
}
|
||||
|
||||
alert('템플릿이 적용되었습니다. 질문 설정 탭에서 확인해보세요.');
|
||||
} else {
|
||||
alert('템플릿 로드 실패: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('템플릿 로드 중 오류가 발생했습니다.');
|
||||
});
|
||||
}
|
||||
|
||||
// 템플릿 질문 추가
|
||||
function addTemplateQuestion(questionData, questionNumber) {
|
||||
questionCount = questionNumber;
|
||||
|
||||
const optionsHtml = ['radio', 'checkbox', 'select'].includes(questionData.stq_type) && questionData.stq_options.length > 0
|
||||
? `<div class="options-container" style="display: block;">
|
||||
<label class="form-label">선택지</label>
|
||||
<div class="options-list">
|
||||
${questionData.stq_options.map((option, index) => `
|
||||
<div class="option-item">
|
||||
<input type="text" name="questions[${questionNumber}][options][]" class="option-input" value="${option}" placeholder="선택지 ${index + 1}">
|
||||
<button type="button" class="btn-sm btn-danger" onclick="removeOption(this)">삭제</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<button type="button" class="add-option-btn" onclick="addOption(this)">선택지 추가</button>
|
||||
</div>`
|
||||
: `<div class="options-container" style="display: none;">
|
||||
<label class="form-label">선택지</label>
|
||||
<div class="options-list">
|
||||
<div class="option-item">
|
||||
<input type="text" name="questions[${questionNumber}][options][]" class="option-input" placeholder="선택지 1">
|
||||
<button type="button" class="btn-sm btn-danger" onclick="removeOption(this)">삭제</button>
|
||||
</div>
|
||||
<div class="option-item">
|
||||
<input type="text" name="questions[${questionNumber}][options][]" class="option-input" placeholder="선택지 2">
|
||||
<button type="button" class="btn-sm btn-danger" onclick="removeOption(this)">삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="add-option-btn" onclick="addOption(this)">선택지 추가</button>
|
||||
</div>`;
|
||||
|
||||
const questionHtml = `
|
||||
<div class="question-item" data-question-index="${questionNumber}">
|
||||
<div class="question-header">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div class="question-number">${questionNumber}</div>
|
||||
<div style="flex: 1;">
|
||||
<select name="questions[${questionNumber}][type]" class="question-type-select form-select" onchange="updateQuestionType(this)">
|
||||
<option value="text" ${questionData.stq_type === 'text' ? 'selected' : ''}>단답형</option>
|
||||
<option value="textarea" ${questionData.stq_type === 'textarea' ? 'selected' : ''}>장문형</option>
|
||||
<option value="radio" ${questionData.stq_type === 'radio' ? 'selected' : ''}>단일선택</option>
|
||||
<option value="checkbox" ${questionData.stq_type === 'checkbox' ? 'selected' : ''}>다중선택</option>
|
||||
<option value="select" ${questionData.stq_type === 'select' ? 'selected' : ''}>드롭다운</option>
|
||||
<option value="rating" ${questionData.stq_type === 'rating' ? 'selected' : ''}>평점</option>
|
||||
<option value="date" ${questionData.stq_type === 'date' ? 'selected' : ''}>날짜</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="question-controls">
|
||||
<button type="button" class="btn-sm btn-secondary" onclick="moveQuestion(this, 'up')">↑</button>
|
||||
<button type="button" class="btn-sm btn-secondary" onclick="moveQuestion(this, 'down')">↓</button>
|
||||
<button type="button" class="btn-sm btn-danger" onclick="removeQuestion(this)">삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">질문 제목</label>
|
||||
<input type="text" name="questions[${questionNumber}][title]" class="form-input"
|
||||
value="${questionData.stq_title}" placeholder="질문을 입력하세요" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">질문 설명 (선택사항)</label>
|
||||
<textarea name="questions[${questionNumber}][description]" class="form-textarea"
|
||||
placeholder="질문에 대한 추가 설명">${questionData.stq_description || ''}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox-item">
|
||||
<input type="checkbox" name="questions[${questionNumber}][required]" value="1" ${questionData.stq_required ? 'checked' : ''}>
|
||||
<label>필수 질문</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${optionsHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('questionsList').insertAdjacentHTML('beforeend', questionHtml);
|
||||
}
|
||||
|
||||
// 질문 추가
|
||||
function addQuestion() {
|
||||
questionCount++;
|
||||
const questionHtml = `
|
||||
<div class="question-item" data-question-index="${questionCount}">
|
||||
<div class="question-header">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div class="question-number">${questionCount}</div>
|
||||
<div style="flex: 1;">
|
||||
<select name="questions[${questionCount}][type]" class="question-type-select form-select" onchange="updateQuestionType(this)">
|
||||
<option value="text">단답형</option>
|
||||
<option value="textarea">장문형</option>
|
||||
<option value="radio">단일선택</option>
|
||||
<option value="checkbox">다중선택</option>
|
||||
<option value="select">드롭다운</option>
|
||||
<option value="rating">평점</option>
|
||||
<option value="date">날짜</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="question-controls">
|
||||
<button type="button" class="btn-sm btn-secondary" onclick="moveQuestion(this, 'up')">↑</button>
|
||||
<button type="button" class="btn-sm btn-secondary" onclick="moveQuestion(this, 'down')">↓</button>
|
||||
<button type="button" class="btn-sm btn-danger" onclick="removeQuestion(this)">삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">질문 제목</label>
|
||||
<input type="text" name="questions[${questionCount}][title]" class="form-input" placeholder="질문을 입력하세요" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">질문 설명 (선택사항)</label>
|
||||
<textarea name="questions[${questionCount}][description]" class="form-textarea" placeholder="질문에 대한 추가 설명"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox-item">
|
||||
<input type="checkbox" name="questions[${questionCount}][required]" value="1">
|
||||
<label>필수 질문</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="options-container" style="display: none;">
|
||||
<label class="form-label">선택지</label>
|
||||
<div class="options-list">
|
||||
<div class="option-item">
|
||||
<input type="text" name="questions[${questionCount}][options][]" class="option-input" placeholder="선택지 1">
|
||||
<button type="button" class="btn-sm btn-danger" onclick="removeOption(this)">삭제</button>
|
||||
</div>
|
||||
<div class="option-item">
|
||||
<input type="text" name="questions[${questionCount}][options][]" class="option-input" placeholder="선택지 2">
|
||||
<button type="button" class="btn-sm btn-danger" onclick="removeOption(this)">삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="add-option-btn" onclick="addOption(this)">선택지 추가</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('questionsList').insertAdjacentHTML('beforeend', questionHtml);
|
||||
updateQuestionNumbers();
|
||||
}
|
||||
|
||||
// 질문 타입 변경
|
||||
function updateQuestionType(select) {
|
||||
const questionItem = select.closest('.question-item');
|
||||
const optionsContainer = questionItem.querySelector('.options-container');
|
||||
const questionType = select.value;
|
||||
|
||||
if (['radio', 'checkbox', 'select'].includes(questionType)) {
|
||||
optionsContainer.style.display = 'block';
|
||||
} else {
|
||||
optionsContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 질문 삭제
|
||||
function removeQuestion(button) {
|
||||
if (confirm('이 질문을 삭제하시겠습니까?')) {
|
||||
button.closest('.question-item').remove();
|
||||
updateQuestionNumbers();
|
||||
}
|
||||
}
|
||||
|
||||
// 질문 순서 변경
|
||||
function moveQuestion(button, direction) {
|
||||
const questionItem = button.closest('.question-item');
|
||||
const sibling = direction === 'up' ? questionItem.previousElementSibling : questionItem.nextElementSibling;
|
||||
|
||||
if (sibling) {
|
||||
if (direction === 'up') {
|
||||
questionItem.parentNode.insertBefore(questionItem, sibling);
|
||||
} else {
|
||||
questionItem.parentNode.insertBefore(sibling, questionItem);
|
||||
}
|
||||
updateQuestionNumbers();
|
||||
}
|
||||
}
|
||||
|
||||
// 선택지 추가
|
||||
function addOption(button) {
|
||||
const optionsList = button.previousElementSibling;
|
||||
const questionIndex = button.closest('.question-item').dataset.questionIndex;
|
||||
const optionCount = optionsList.children.length + 1;
|
||||
|
||||
const optionHtml = `
|
||||
<div class="option-item">
|
||||
<input type="text" name="questions[${questionIndex}][options][]" class="option-input" placeholder="선택지 ${optionCount}">
|
||||
<button type="button" class="btn-sm btn-danger" onclick="removeOption(this)">삭제</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
optionsList.insertAdjacentHTML('beforeend', optionHtml);
|
||||
}
|
||||
|
||||
// 선택지 삭제
|
||||
function removeOption(button) {
|
||||
const optionsList = button.closest('.options-list');
|
||||
if (optionsList.children.length > 2) {
|
||||
button.closest('.option-item').remove();
|
||||
} else {
|
||||
alert('최소 2개의 선택지가 필요합니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 질문 번호 업데이트
|
||||
function updateQuestionNumbers() {
|
||||
const questions = document.querySelectorAll('.question-item');
|
||||
questions.forEach((question, index) => {
|
||||
const number = index + 1;
|
||||
question.querySelector('.question-number').textContent = number;
|
||||
question.dataset.questionIndex = number;
|
||||
|
||||
// input name 속성 업데이트
|
||||
const inputs = question.querySelectorAll('input, textarea, select');
|
||||
inputs.forEach(input => {
|
||||
if (input.name && input.name.includes('questions[')) {
|
||||
input.name = input.name.replace(/questions\[\d+\]/, `questions[${number}]`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
questionCount = questions.length;
|
||||
}
|
||||
|
||||
// 폼 검증
|
||||
function initFormValidation() {
|
||||
document.getElementById('surveyForm').addEventListener('submit', function(e) {
|
||||
const title = document.querySelector('input[name="sv_title"]').value.trim();
|
||||
const startDate = new Date(document.querySelector('input[name="sv_start_date"]').value);
|
||||
const endDate = new Date(document.querySelector('input[name="sv_end_date"]').value);
|
||||
|
||||
if (!title) {
|
||||
alert('설문 제목을 입력해주세요.');
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (startDate >= endDate) {
|
||||
alert('종료일시는 시작일시보다 늦어야 합니다.');
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const questions = document.querySelectorAll('.question-item');
|
||||
if (questions.length === 0) {
|
||||
alert('최소 1개의 질문을 추가해주세요.');
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// 질문 제목 검증
|
||||
let hasEmptyQuestion = false;
|
||||
questions.forEach(question => {
|
||||
const titleInput = question.querySelector('input[name*="[title]"]');
|
||||
if (!titleInput.value.trim()) {
|
||||
hasEmptyQuestion = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasEmptyQuestion) {
|
||||
alert('모든 질문의 제목을 입력해주세요.');
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* 템플릿 작성/수정 폼 JavaScript
|
||||
*/
|
||||
|
||||
let questionIndex = 0;
|
||||
|
||||
// 페이지 로드 시 초기화
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initTemplateForm();
|
||||
});
|
||||
|
||||
function initTemplateForm() {
|
||||
// 초기 데이터 설정
|
||||
if (window.templateFormData) {
|
||||
questionIndex = window.templateFormData.questionIndex;
|
||||
}
|
||||
|
||||
// 폼 검증 이벤트
|
||||
initFormValidation();
|
||||
|
||||
// 기존 질문들의 타입에 따라 옵션 표시
|
||||
document.querySelectorAll('.question-type-select').forEach(select => {
|
||||
toggleOptions(select.closest('.question-item').dataset.index);
|
||||
});
|
||||
}
|
||||
|
||||
function addQuestion() {
|
||||
const container = document.getElementById('questionsContainer');
|
||||
const emptyState = document.getElementById('emptyQuestions');
|
||||
|
||||
if (emptyState) {
|
||||
emptyState.remove();
|
||||
}
|
||||
|
||||
const questionHtml = `
|
||||
<div class="question-item" data-index="${questionIndex}">
|
||||
<div class="question-header">
|
||||
<div class="question-number">${questionIndex + 1}</div>
|
||||
<div class="question-title-text">질문 ${questionIndex + 1}</div>
|
||||
<div class="question-actions">
|
||||
<button type="button" class="btn-sm btn-secondary" onclick="moveQuestion(${questionIndex}, 'up')">
|
||||
<i class="fa fa-arrow-up"></i>
|
||||
</button>
|
||||
<button type="button" class="btn-sm btn-secondary" onclick="moveQuestion(${questionIndex}, 'down')">
|
||||
<i class="fa fa-arrow-down"></i>
|
||||
</button>
|
||||
<button type="button" class="btn-sm btn-danger" onclick="removeQuestion(${questionIndex})">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">질문 제목</label>
|
||||
<input type="text" name="questions[${questionIndex}][stq_title]" class="form-control"
|
||||
placeholder="질문을 입력하세요" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group question-type-group">
|
||||
<label class="form-label">질문 유형</label>
|
||||
<select name="questions[${questionIndex}][stq_type]" class="form-control form-select question-type-select"
|
||||
onchange="toggleOptions(${questionIndex})">
|
||||
<option value="text">단답형</option>
|
||||
<option value="textarea">장문형</option>
|
||||
<option value="radio">객관식(단일)</option>
|
||||
<option value="checkbox">객관식(다중)</option>
|
||||
<option value="select">드롭다운</option>
|
||||
<option value="rating">평점</option>
|
||||
<option value="date">날짜</option>
|
||||
<option value="email">이메일</option>
|
||||
<option value="number">숫자</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">질문 설명 (선택사항)</label>
|
||||
<textarea name="questions[${questionIndex}][stq_description]" class="form-control" rows="2"
|
||||
placeholder="질문에 대한 추가 설명"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" name="questions[${questionIndex}][stq_required]" value="1">
|
||||
필수 질문
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.insertAdjacentHTML('beforeend', questionHtml);
|
||||
questionIndex++;
|
||||
updateQuestionNumbers();
|
||||
}
|
||||
|
||||
function removeQuestion(index) {
|
||||
if (confirm('이 질문을 삭제하시겠습니까?')) {
|
||||
const questionItem = document.querySelector(`[data-index="${index}"]`);
|
||||
questionItem.remove();
|
||||
|
||||
const remainingQuestions = document.querySelectorAll('.question-item');
|
||||
if (remainingQuestions.length === 0) {
|
||||
const container = document.getElementById('questionsContainer');
|
||||
container.innerHTML = `
|
||||
<div class="empty-questions" id="emptyQuestions">
|
||||
<i class="fa fa-question-circle"></i>
|
||||
<h3>질문을 추가해주세요</h3>
|
||||
<p>아래 버튼을 클릭하여 첫 번째 질문을 만들어보세요.</p>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
updateQuestionNumbers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function moveQuestion(index, direction) {
|
||||
const questionItem = document.querySelector(`[data-index="${index}"]`);
|
||||
const container = document.getElementById('questionsContainer');
|
||||
|
||||
if (direction === 'up' && questionItem.previousElementSibling) {
|
||||
container.insertBefore(questionItem, questionItem.previousElementSibling);
|
||||
} else if (direction === 'down' && questionItem.nextElementSibling) {
|
||||
container.insertBefore(questionItem.nextElementSibling, questionItem);
|
||||
}
|
||||
|
||||
updateQuestionNumbers();
|
||||
}
|
||||
|
||||
function updateQuestionNumbers() {
|
||||
const questions = document.querySelectorAll('.question-item');
|
||||
questions.forEach((question, index) => {
|
||||
const numberElement = question.querySelector('.question-number');
|
||||
const titleElement = question.querySelector('.question-title-text');
|
||||
|
||||
numberElement.textContent = index + 1;
|
||||
titleElement.textContent = `질문 ${index + 1}`;
|
||||
|
||||
// data-index 업데이트
|
||||
question.dataset.index = index;
|
||||
|
||||
// input name 속성 업데이트
|
||||
const inputs = question.querySelectorAll('input, textarea, select');
|
||||
inputs.forEach(input => {
|
||||
if (input.name && input.name.includes('questions[')) {
|
||||
input.name = input.name.replace(/questions\[\d+\]/, `questions[${index}]`);
|
||||
}
|
||||
});
|
||||
|
||||
// 옵션 컨테이너 ID 업데이트
|
||||
const optionsContainer = question.querySelector('.options-container');
|
||||
if (optionsContainer) {
|
||||
optionsContainer.id = `optionsContainer${index}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleOptions(index) {
|
||||
const questionItem = document.querySelector(`[data-index="${index}"]`);
|
||||
const typeSelect = questionItem.querySelector('.question-type-select');
|
||||
const selectedType = typeSelect.value;
|
||||
|
||||
// 기존 옵션 컨테이너 제거
|
||||
const existingOptions = questionItem.querySelector('.options-container');
|
||||
if (existingOptions) {
|
||||
existingOptions.remove();
|
||||
}
|
||||
|
||||
// 객관식 질문인 경우 옵션 컨테이너 추가
|
||||
if (['radio', 'checkbox', 'select'].includes(selectedType)) {
|
||||
const optionsHtml = `
|
||||
<div class="options-container" id="optionsContainer${index}">
|
||||
<label class="form-label">선택 옵션</label>
|
||||
<div class="option-item">
|
||||
<input type="text" name="questions[${index}][options][]"
|
||||
class="form-control option-input"
|
||||
placeholder="옵션을 입력하세요">
|
||||
<button type="button" class="btn-sm btn-danger" onclick="removeOption(this)">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn-add-option" onclick="addOption(${index})">
|
||||
<i class="fa fa-plus"></i> 옵션 추가
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
questionItem.insertAdjacentHTML('beforeend', optionsHtml);
|
||||
}
|
||||
}
|
||||
|
||||
function addOption(questionIndex) {
|
||||
const container = document.getElementById(`optionsContainer${questionIndex}`);
|
||||
const addButton = container.querySelector('.btn-add-option');
|
||||
|
||||
const optionHtml = `
|
||||
<div class="option-item">
|
||||
<input type="text" name="questions[${questionIndex}][options][]"
|
||||
class="form-control option-input"
|
||||
placeholder="옵션을 입력하세요">
|
||||
<button type="button" class="btn-sm btn-danger" onclick="removeOption(this)">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
addButton.insertAdjacentHTML('beforebegin', optionHtml);
|
||||
}
|
||||
|
||||
function removeOption(button) {
|
||||
const optionsContainer = button.closest('.options-container');
|
||||
const optionItems = optionsContainer.querySelectorAll('.option-item');
|
||||
|
||||
if (optionItems.length > 1) {
|
||||
button.parentElement.remove();
|
||||
} else {
|
||||
alert('최소 1개의 옵션이 필요합니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 폼 검증
|
||||
function initFormValidation() {
|
||||
document.querySelector('form[name="templateForm"]').addEventListener('submit', function(e) {
|
||||
const templateName = document.querySelector('input[name="st_name"]').value.trim();
|
||||
if (!templateName) {
|
||||
alert('템플릿 이름을 입력해주세요.');
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const questions = document.querySelectorAll('.question-item');
|
||||
if (questions.length === 0) {
|
||||
alert('최소 1개 이상의 질문을 추가해주세요.');
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// 각 질문의 제목 검증
|
||||
let hasEmptyTitle = false;
|
||||
questions.forEach((question, index) => {
|
||||
const titleInput = question.querySelector('input[name*="[stq_title]"]');
|
||||
if (!titleInput.value.trim()) {
|
||||
alert(`질문 ${index + 1}의 제목을 입력해주세요.`);
|
||||
hasEmptyTitle = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasEmptyTitle) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// 객관식 질문의 옵션 검증
|
||||
let hasEmptyOptions = false;
|
||||
questions.forEach((question, index) => {
|
||||
const typeSelect = question.querySelector('.question-type-select');
|
||||
const selectedType = typeSelect.value;
|
||||
|
||||
if (['radio', 'checkbox', 'select'].includes(selectedType)) {
|
||||
const optionInputs = question.querySelectorAll('.option-input');
|
||||
const filledOptions = Array.from(optionInputs).filter(input => input.value.trim());
|
||||
|
||||
if (filledOptions.length < 2) {
|
||||
alert(`질문 ${index + 1}은 최소 2개의 옵션이 필요합니다.`);
|
||||
hasEmptyOptions = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (hasEmptyOptions) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user