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
@@ -0,0 +1,507 @@
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);
}