507 lines
19 KiB
JavaScript
507 lines
19 KiB
JavaScript
function initSurveyFormModule(moduleId) {
|
|
const moduleElement = document.getElementById(moduleId);
|
|
if (!moduleElement || moduleElement.classList.contains('initialized')) return;
|
|
|
|
// 설문 데이터 가져오기
|
|
const surveyData = JSON.parse(moduleElement.dataset.survey || '{}');
|
|
const { survey, questions, theme_color } = surveyData;
|
|
|
|
// DOM 요소들
|
|
const questionsContainer = moduleElement.querySelector('#questionsContainer');
|
|
const progressFill = moduleElement.querySelector('#progressFill');
|
|
const progressText = moduleElement.querySelector('#progressText');
|
|
const currentQuestionSpan = moduleElement.querySelector('#currentQuestion');
|
|
const submitBtn = moduleElement.querySelector('#submitBtn');
|
|
const surveyForm = moduleElement.querySelector('#surveyForm');
|
|
const confirmModal = moduleElement.querySelector('.confirm-modal');
|
|
const modalOverlay = confirmModal.querySelector('.modal-overlay');
|
|
const modalClose = confirmModal.querySelector('.modal-close');
|
|
const btnCancel = confirmModal.querySelector('.btn-cancel');
|
|
const btnConfirm = confirmModal.querySelector('.btn-confirm');
|
|
|
|
// CSS 변수 설정
|
|
if (theme_color) {
|
|
moduleElement.style.setProperty('--survey-primary', theme_color);
|
|
moduleElement.style.setProperty('--survey-primary-light', theme_color + '20');
|
|
moduleElement.style.setProperty('--survey-primary-dark', theme_color + 'CC');
|
|
}
|
|
|
|
// 질문 렌더링
|
|
function renderQuestions() {
|
|
if (!questions || !questions.length) return;
|
|
|
|
const questionsHTML = questions.map((question, index) => {
|
|
const fieldName = `answer_${question.sq_id}`;
|
|
const required = question.sq_required ? 'required' : '';
|
|
const requiredMark = question.sq_required ? '<span class="question-required">*</span>' : '';
|
|
|
|
let inputHTML = '';
|
|
|
|
switch (question.sq_type) {
|
|
case 'text':
|
|
inputHTML = `
|
|
<input type="text"
|
|
name="${fieldName}"
|
|
class="form-input"
|
|
placeholder="답변을 입력해주세요"
|
|
${required}>
|
|
`;
|
|
break;
|
|
|
|
case 'textarea':
|
|
inputHTML = `
|
|
<textarea name="${fieldName}"
|
|
class="form-input form-textarea"
|
|
placeholder="자세한 답변을 입력해주세요"
|
|
${required}></textarea>
|
|
`;
|
|
break;
|
|
|
|
case 'radio':
|
|
if (question.sq_options && Array.isArray(question.sq_options)) {
|
|
inputHTML = `
|
|
<div class="radio-group">
|
|
${question.sq_options.map((option, optionIndex) => `
|
|
<div class="radio-item">
|
|
<input type="radio"
|
|
name="${fieldName}"
|
|
value="${escapeHtml(option)}"
|
|
id="${fieldName}_${optionIndex}"
|
|
${required}>
|
|
<label for="${fieldName}_${optionIndex}">
|
|
${escapeHtml(option)}
|
|
</label>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
}
|
|
break;
|
|
|
|
case 'checkbox':
|
|
if (question.sq_options && Array.isArray(question.sq_options)) {
|
|
inputHTML = `
|
|
<div class="checkbox-group">
|
|
${question.sq_options.map((option, optionIndex) => `
|
|
<div class="checkbox-item">
|
|
<input type="checkbox"
|
|
name="${fieldName}[]"
|
|
value="${escapeHtml(option)}"
|
|
id="${fieldName}_${optionIndex}">
|
|
<label for="${fieldName}_${optionIndex}">
|
|
${escapeHtml(option)}
|
|
</label>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
}
|
|
break;
|
|
|
|
case 'select':
|
|
if (question.sq_options && Array.isArray(question.sq_options)) {
|
|
inputHTML = `
|
|
<select name="${fieldName}" class="form-select" ${required}>
|
|
<option value="">선택해주세요</option>
|
|
${question.sq_options.map(option => `
|
|
<option value="${escapeHtml(option)}">
|
|
${escapeHtml(option)}
|
|
</option>
|
|
`).join('')}
|
|
</select>
|
|
`;
|
|
}
|
|
break;
|
|
|
|
case 'rating':
|
|
const scale = question.sq_options?.scale || 5;
|
|
const labels = question.sq_options?.labels || [];
|
|
inputHTML = `
|
|
<div class="rating-container">
|
|
${Array.from({ length: scale }, (_, i) => i + 1).map(value => `
|
|
<div class="rating-item" data-value="${value}">
|
|
<input type="radio"
|
|
name="${fieldName}"
|
|
value="${value}"
|
|
id="${fieldName}_${value}"
|
|
style="display: none;"
|
|
${required}>
|
|
<div class="rating-number">${value}</div>
|
|
${labels[value - 1] ? `<div class="rating-label">${escapeHtml(labels[value - 1])}</div>` : ''}
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
break;
|
|
|
|
case 'date':
|
|
inputHTML = `
|
|
<input type="date"
|
|
name="${fieldName}"
|
|
class="form-date"
|
|
${required}>
|
|
`;
|
|
break;
|
|
}
|
|
|
|
return `
|
|
<div class="question-container" data-question="${index + 1}">
|
|
<div class="question-header">
|
|
<span class="question-number">${index + 1}</span>
|
|
<h3 class="question-title">
|
|
${escapeHtml(question.sq_title)}
|
|
${requiredMark}
|
|
</h3>
|
|
|
|
${question.sq_description ? `
|
|
<p class="question-description">${escapeHtml(question.sq_description)}</p>
|
|
` : ''}
|
|
</div>
|
|
|
|
<div class="question-input">
|
|
${inputHTML}
|
|
<div class="error-message" id="error_${question.sq_id}"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
questionsContainer.innerHTML = questionsHTML;
|
|
}
|
|
|
|
// HTML 이스케이프 함수
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// 진행률 업데이트
|
|
function updateProgress() {
|
|
let answeredQuestions = 0;
|
|
const totalQuestions = questions.length;
|
|
|
|
questions.forEach((question, index) => {
|
|
const questionContainer = questionsContainer.children[index];
|
|
if (!questionContainer) return;
|
|
|
|
const inputs = questionContainer.querySelectorAll('input, textarea, select');
|
|
let hasAnswer = false;
|
|
|
|
inputs.forEach(input => {
|
|
if (input.type === 'radio' || input.type === 'checkbox') {
|
|
if (input.checked) hasAnswer = true;
|
|
} else if (input.value.trim() !== '') {
|
|
hasAnswer = true;
|
|
}
|
|
});
|
|
|
|
if (hasAnswer) {
|
|
answeredQuestions++;
|
|
}
|
|
});
|
|
|
|
const progress = Math.round((answeredQuestions / totalQuestions) * 100);
|
|
progressFill.style.width = progress + '%';
|
|
progressText.textContent = progress + '%';
|
|
currentQuestionSpan.textContent = answeredQuestions;
|
|
|
|
// 모든 필수 질문이 답변되었는지 확인
|
|
const requiredQuestions = questionsContainer.querySelectorAll('input[required], textarea[required], select[required]');
|
|
let allRequiredAnswered = true;
|
|
|
|
requiredQuestions.forEach(input => {
|
|
if (input.type === 'radio') {
|
|
const radioGroup = questionsContainer.querySelectorAll(`input[name="${input.name}"]`);
|
|
let radioChecked = false;
|
|
radioGroup.forEach(radio => {
|
|
if (radio.checked) radioChecked = true;
|
|
});
|
|
if (!radioChecked) allRequiredAnswered = false;
|
|
} else if (input.type === 'checkbox') {
|
|
const checkboxGroup = questionsContainer.querySelectorAll(`input[name="${input.name}"]`);
|
|
let checkboxChecked = false;
|
|
checkboxGroup.forEach(checkbox => {
|
|
if (checkbox.checked) checkboxChecked = true;
|
|
});
|
|
if (!checkboxChecked) allRequiredAnswered = false;
|
|
} else if (input.value.trim() === '') {
|
|
allRequiredAnswered = false;
|
|
}
|
|
});
|
|
|
|
submitBtn.disabled = !allRequiredAnswered;
|
|
}
|
|
|
|
// 이벤트 리스너 바인딩
|
|
function bindEvents() {
|
|
// 라디오/체크박스 스타일링
|
|
questionsContainer.addEventListener('click', (e) => {
|
|
const radioItem = e.target.closest('.radio-item');
|
|
const checkboxItem = e.target.closest('.checkbox-item');
|
|
|
|
if (radioItem) {
|
|
const input = radioItem.querySelector('input');
|
|
const groupName = input.name;
|
|
|
|
// 라디오 버튼 그룹의 다른 선택 해제
|
|
questionsContainer.querySelectorAll(`input[name="${groupName}"]`).forEach(radio => {
|
|
radio.closest('.radio-item').classList.remove('selected');
|
|
});
|
|
|
|
input.checked = true;
|
|
radioItem.classList.add('selected');
|
|
updateProgress();
|
|
}
|
|
|
|
if (checkboxItem) {
|
|
const input = checkboxItem.querySelector('input');
|
|
input.checked = !input.checked;
|
|
checkboxItem.classList.toggle('selected', input.checked);
|
|
updateProgress();
|
|
}
|
|
});
|
|
|
|
// 평점 클릭 이벤트
|
|
questionsContainer.addEventListener('click', (e) => {
|
|
const ratingItem = e.target.closest('.rating-item');
|
|
if (ratingItem) {
|
|
const value = ratingItem.dataset.value;
|
|
const input = ratingItem.querySelector('input');
|
|
const container = ratingItem.closest('.rating-container');
|
|
|
|
// 같은 그룹의 다른 평점 해제
|
|
container.querySelectorAll('.rating-item').forEach(item => {
|
|
item.classList.remove('selected');
|
|
});
|
|
|
|
// 현재 평점 선택
|
|
ratingItem.classList.add('selected');
|
|
input.checked = true;
|
|
|
|
updateProgress();
|
|
}
|
|
});
|
|
|
|
// 일반 입력 필드 이벤트
|
|
questionsContainer.addEventListener('input', updateProgress);
|
|
questionsContainer.addEventListener('change', updateProgress);
|
|
|
|
// 폼 제출 이벤트
|
|
surveyForm.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
|
|
if (submitBtn.disabled) {
|
|
showNotification('모든 필수 항목을 입력해주세요.', 'error');
|
|
return;
|
|
}
|
|
|
|
// 확인 모달 표시
|
|
showConfirmModal();
|
|
});
|
|
|
|
// 모달 이벤트
|
|
modalClose.addEventListener('click', hideConfirmModal);
|
|
btnCancel.addEventListener('click', hideConfirmModal);
|
|
modalOverlay.addEventListener('click', hideConfirmModal);
|
|
|
|
btnConfirm.addEventListener('click', () => {
|
|
hideConfirmModal();
|
|
submitSurvey();
|
|
});
|
|
|
|
// ESC 키로 모달 닫기
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape' && confirmModal.classList.contains('show')) {
|
|
hideConfirmModal();
|
|
}
|
|
});
|
|
}
|
|
|
|
// 확인 모달 표시
|
|
function showConfirmModal() {
|
|
confirmModal.classList.add('show');
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
// 확인 모달 숨기기
|
|
function hideConfirmModal() {
|
|
confirmModal.classList.remove('show');
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
// 설문 제출
|
|
function submitSurvey() {
|
|
submitBtn.classList.add('loading');
|
|
submitBtn.innerHTML = '<div class="loading-spinner"></div> 제출 중...';
|
|
submitBtn.disabled = true;
|
|
|
|
const formData = new FormData(surveyForm);
|
|
formData.append('ajax', '1');
|
|
|
|
fetch(surveyForm.action, {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showNotification('설문이 성공적으로 제출되었습니다!', 'success');
|
|
setTimeout(() => {
|
|
window.location.href = data.redirect_url;
|
|
}, 1500);
|
|
} else {
|
|
showNotification(data.errors ? data.errors.join('\n') : '제출 중 오류가 발생했습니다.', 'error');
|
|
submitBtn.classList.remove('loading');
|
|
submitBtn.innerHTML = '<i class="fa fa-paper-plane"></i> 설문 제출하기';
|
|
submitBtn.disabled = false;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
showNotification('제출 중 오류가 발생했습니다.', 'error');
|
|
submitBtn.classList.remove('loading');
|
|
submitBtn.innerHTML = '<i class="fa fa-paper-plane"></i> 설문 제출하기';
|
|
submitBtn.disabled = false;
|
|
});
|
|
}
|
|
|
|
// 알림 표시
|
|
function showNotification(message, type = 'info') {
|
|
const notification = document.createElement('div');
|
|
notification.className = `survey-notification survey-notification-${type}`;
|
|
notification.innerHTML = `
|
|
<div class="notification-content">
|
|
<i class="fa fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
|
|
<span>${message}</span>
|
|
</div>
|
|
`;
|
|
|
|
// 스타일 추가
|
|
notification.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: ${type === 'success' ? '#28a745' : type === 'error' ? '#dc3545' : '#17a2b8'};
|
|
color: white;
|
|
padding: 15px 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
|
z-index: 1060;
|
|
animation: slideInRight 0.3s ease;
|
|
max-width: 300px;
|
|
word-wrap: break-word;
|
|
`;
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
setTimeout(() => {
|
|
notification.style.animation = 'slideOutRight 0.3s ease';
|
|
setTimeout(() => {
|
|
notification.remove();
|
|
}, 300);
|
|
}, 5000);
|
|
}
|
|
|
|
// 스크롤 시 질문 하이라이트
|
|
function initScrollHighlight() {
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
// 모든 하이라이트 제거
|
|
questionsContainer.querySelectorAll('.question-container').forEach(q => {
|
|
q.classList.remove('highlight');
|
|
});
|
|
// 현재 질문 하이라이트
|
|
entry.target.classList.add('highlight');
|
|
}
|
|
});
|
|
}, {
|
|
threshold: 0.5,
|
|
rootMargin: '-20% 0px -20% 0px'
|
|
});
|
|
|
|
questionsContainer.querySelectorAll('.question-container').forEach(question => {
|
|
observer.observe(question);
|
|
});
|
|
}
|
|
|
|
// 질문 애니메이션 초기화
|
|
function initQuestionAnimations() {
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.style.opacity = '1';
|
|
entry.target.style.animationPlayState = 'running';
|
|
}
|
|
});
|
|
}, {
|
|
threshold: 0.1,
|
|
rootMargin: '0px 0px -50px 0px'
|
|
});
|
|
|
|
questionsContainer.querySelectorAll('.question-container').forEach(question => {
|
|
question.style.opacity = '0';
|
|
question.style.animationPlayState = 'paused';
|
|
observer.observe(question);
|
|
});
|
|
}
|
|
|
|
// 초기화
|
|
function init() {
|
|
renderQuestions();
|
|
bindEvents();
|
|
updateProgress();
|
|
initScrollHighlight();
|
|
initQuestionAnimations();
|
|
|
|
moduleElement.classList.add('initialized');
|
|
}
|
|
|
|
// 모듈 초기화 실행
|
|
init();
|
|
}
|
|
|
|
// 추가 CSS 애니메이션 (JavaScript로 동적 추가)
|
|
const additionalStyles = `
|
|
@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;
|
|
}
|
|
}
|
|
|
|
.survey-notification .notification-content {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.survey-notification i {
|
|
font-size: 1.2em;
|
|
flex-shrink: 0;
|
|
}
|
|
`;
|
|
|
|
// 스타일 추가 (한 번만)
|
|
if (!document.getElementById('survey-form-module-styles')) {
|
|
const styleSheet = document.createElement('style');
|
|
styleSheet.id = 'survey-form-module-styles';
|
|
styleSheet.textContent = additionalStyles;
|
|
document.head.appendChild(styleSheet);
|
|
} |