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
+76
View File
@@ -0,0 +1,76 @@
<?php
define('G5_IS_ADMIN', true);
include_once('../../common.php');
if (!defined('_GNUBOARD_')) exit;
include_once(G5_ADMIN_PATH.'/admin.lib.php');
// 설문 관리 솔루션 버전
define('SURVEY_VERSION', '1.0.0');
// 설문 관리 솔루션 경로
define('SURVEY_ADMIN_PATH', G5_ADMIN_PATH.'/survey_manage');
define('SURVEY_ADMIN_URL', G5_ADMIN_URL.'/survey_manage');
define('SURVEY_DATA_PATH', G5_DATA_PATH.'/survey');
define('SURVEY_DATA_URL', G5_DATA_URL.'/survey');
// 데이터 디렉토리 생성
@mkdir(SURVEY_DATA_PATH, G5_DIR_PERMISSION, true);
@chmod(SURVEY_DATA_PATH, G5_DIR_PERMISSION);
// 설문 관리 라이브러리 함수들
include_once(SURVEY_ADMIN_PATH.'/lib/survey.lib.php');
// 설문 상태 상수
/*define('SURVEY_STATUS_DRAFT', 'draft');
define('SURVEY_STATUS_ACTIVE', 'active');
define('SURVEY_STATUS_CLOSED', 'closed');
define('SURVEY_STATUS_DELETED', 'deleted');*/
// 질문 유형 상수
define('QUESTION_TYPE_TEXT', 'text');
define('QUESTION_TYPE_TEXTAREA', 'textarea');
define('QUESTION_TYPE_RADIO', 'radio');
define('QUESTION_TYPE_CHECKBOX', 'checkbox');
define('QUESTION_TYPE_SELECT', 'select');
define('QUESTION_TYPE_RATING', 'rating');
define('QUESTION_TYPE_DATE', 'date');
/*// 응답 상태 상수
define('RESPONSE_STATUS_STARTED', 'started');
define('RESPONSE_STATUS_COMPLETED', 'completed');
define('RESPONSE_STATUS_ABANDONED', 'abandoned');*/
// 공통 CSS 및 JS 파일 추가
add_stylesheet('<link rel="stylesheet" href="'.SURVEY_ADMIN_URL.'/css/survey_admin.css?ver='.G5_SERVER_TIME.'">', 0);
add_javascript('<script src="'.SURVEY_ADMIN_URL.'/js/survey_admin.js?ver='.G5_SERVER_TIME.'"></script>', 100);
// 페이지별 CSS/JS 파일 자동 로드
$current_file = basename($_SERVER['PHP_SELF'], '.php');
$page_css_map = [
'survey_form' => 'survey_form.css',
'template_form' => 'template_form.css',
'template_list' => 'template_list.css',
'statistics' => 'statistics.css'
];
$page_js_map = [
'survey_form' => 'survey_form.js',
'template_form' => 'template_form.js',
'statistics' => 'statistics.js'
];
// 페이지별 CSS 로드
if (isset($page_css_map[$current_file])) {
$css_file = SURVEY_ADMIN_PATH.'/css/'.$page_css_map[$current_file];
if (file_exists($css_file)) {
add_stylesheet('<link rel="stylesheet" href="'.SURVEY_ADMIN_URL.'/css/'.$page_css_map[$current_file].'?ver='.filemtime($css_file).'">', 1);
}
}
// 페이지별 JS 로드
if (isset($page_js_map[$current_file])) {
$js_file = SURVEY_ADMIN_PATH.'/js/'.$page_js_map[$current_file];
if (file_exists($js_file)) {
add_javascript('<script src="'.SURVEY_ADMIN_URL.'/js/'.$page_js_map[$current_file].'?ver='.filemtime($js_file).'"></script>', 101);
}
}
@@ -0,0 +1,14 @@
<?php
if (!defined('_GNUBOARD_')) exit;
// 710번대 최상위 메뉴 '설문 관리'를 정의합니다.
$menu['menu710'][] = array('710000', '설문 관리', G5_ADMIN_URL.'/survey_manage/survey_list.php', 'survey_manage', 'fa-poll');
// '설문 관리'의 하위 메뉴들을 정의합니다.
$menu['menu710'][] = array('710100', '설문 목록', G5_ADMIN_URL.'/survey_manage/survey_list.php', 'survey_list');
$menu['menu710'][] = array('710200', '설문 작성', G5_ADMIN_URL.'/survey_manage/survey_form.php', 'survey_create');
$menu['menu710'][] = array('710300', '템플릿 관리', G5_ADMIN_URL.'/survey_manage/template_list.php', 'survey_template');
$menu['menu710'][] = array('710400', '응답 관리', G5_ADMIN_URL.'/survey_manage/response_list.php', 'survey_response');
$menu['menu710'][] = array('710500', '통계 분석', G5_ADMIN_URL.'/survey_manage/statistics_list.php', 'survey_statistics');
$menu['menu710'][] = array('710600', '엑셀 내보내기', G5_ADMIN_URL.'/survey_manage/export.php', 'survey_export');
$menu['menu710'][] = array('710700', '솔루션 설치', G5_ADMIN_URL.'/survey_manage/install.php', 'survey_solution_install');
+44
View File
@@ -0,0 +1,44 @@
<?php
include_once('./_common.php');
header('Content-Type: application/json');
$st_id = isset($_GET['st_id']) ? (int)$_GET['st_id'] : 0;
if (!$st_id) {
echo json_encode(['success' => false, 'message' => '템플릿 ID가 없습니다.']);
exit;
}
// 템플릿 정보 가져오기
$template = sql_fetch("SELECT * FROM survey_templates WHERE st_id = '$st_id'");
if (!$template) {
echo json_encode(['success' => false, 'message' => '존재하지 않는 템플릿입니다.']);
exit;
}
// 템플릿 질문들 가져오기
$questions = [];
$question_sql = "SELECT * FROM survey_template_questions WHERE st_id = '$st_id' ORDER BY stq_order ASC";
$question_result = sql_query($question_sql);
while ($question = sql_fetch_array($question_result)) {
$question_data = [
'stq_id' => $question['stq_id'],
'stq_order' => $question['stq_order'],
'stq_type' => $question['stq_type'],
'stq_title' => $question['stq_title'],
'stq_description' => $question['stq_description'],
'stq_required' => $question['stq_required'],
'stq_options' => $question['stq_options'] ? json_decode($question['stq_options'], true) : []
];
$questions[] = $question_data;
}
echo json_encode([
'success' => true,
'template' => $template,
'questions' => $questions
]);
?>
+391
View File
@@ -0,0 +1,391 @@
/* 통계 페이지 스타일 */
.statistics-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.stats-header {
background: linear-gradient(135deg, #AA20FF 0%, #8A1ACC 100%);
color: white;
padding: 30px;
border-radius: 15px;
margin-bottom: 30px;
text-align: center;
}
.stats-header h1 {
margin: 0 0 10px 0;
font-size: 2.2em;
}
.stats-header p {
margin: 0;
opacity: 0.9;
}
.stats-overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
text-align: center;
border-left: 4px solid #AA20FF;
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
}
.stat-number {
font-size: 2.5em;
font-weight: 700;
color: #AA20FF;
margin-bottom: 8px;
line-height: 1;
}
.stat-label {
color: #666;
font-size: 0.9em;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-change {
font-size: 0.8em;
margin-top: 5px;
}
.stat-change.positive {
color: #28a745;
}
.stat-change.negative {
color: #dc3545;
}
.chart-section {
background: white;
border-radius: 15px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e9ecef;
}
.chart-title {
font-size: 1.4em;
font-weight: 600;
color: #333;
margin: 0;
}
.chart-controls {
display: flex;
gap: 10px;
}
.chart-btn {
padding: 6px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 5px;
cursor: pointer;
font-size: 0.8em;
transition: all 0.3s ease;
}
.chart-btn.active,
.chart-btn:hover {
background: #AA20FF;
color: white;
border-color: #AA20FF;
}
.chart-container {
position: relative;
height: 400px;
margin-bottom: 20px;
}
.questions-stats {
display: grid;
gap: 30px;
}
.question-stat {
background: white;
border-radius: 15px;
padding: 30px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.question-header {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e9ecef;
}
.question-number {
display: inline-block;
width: 30px;
height: 30px;
background: #AA20FF;
color: white;
border-radius: 50%;
text-align: center;
line-height: 30px;
font-weight: bold;
font-size: 0.9em;
margin-right: 15px;
}
.question-title {
font-size: 1.2em;
font-weight: 600;
color: #333;
display: inline;
}
.question-type {
display: inline-block;
padding: 3px 8px;
background: #e9ecef;
color: #666;
border-radius: 12px;
font-size: 0.7em;
font-weight: 500;
text-transform: uppercase;
margin-left: 10px;
}
.question-stats-grid {
display: grid;
grid-template-columns: 1fr 300px;
gap: 30px;
align-items: start;
}
.answer-stats {
display: flex;
flex-direction: column;
gap: 15px;
}
.answer-item {
display: flex;
align-items: center;
gap: 15px;
padding: 15px;
background: #fff;
border-radius: 10px;
transition: all 0.3s ease;
}
.answer-item:hover {
background: #e9ecef;
}
.answer-bar {
flex: 1;
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
position: relative;
}
.answer-fill {
height: 100%;
background: linear-gradient(90deg, #AA20FF 0%, #8A1ACC 100%);
border-radius: 4px;
transition: width 0.5s ease;
}
.answer-label {
min-width: 150px;
font-weight: 500;
color: #333;
}
.answer-count {
min-width: 80px;
text-align: right;
font-weight: 600;
color: #666;
}
.answer-percentage {
min-width: 50px;
text-align: right;
font-size: 0.9em;
color: #AA20FF;
font-weight: 600;
}
.question-chart {
height: 250px;
}
.text-responses {
max-height: 300px;
overflow-y: auto;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
.text-response {
padding: 15px;
border-bottom: 1px solid #f0f0f0;
line-height: 1.5;
}
.text-response:last-child {
border-bottom: none;
}
.text-response-meta {
font-size: 0.8em;
color: #666;
margin-bottom: 8px;
}
.text-response-content {
color: #333;
}
.export-section {
background: white;
border-radius: 15px;
padding: 30px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
text-align: center;
}
.export-buttons {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.export-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
background: #AA20FF;
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: all 0.3s ease;
border: none;
cursor: pointer;
}
.export-btn:hover {
background: #8A1ACC;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(170, 32, 255, 0.3);
color: white;
}
.export-btn.secondary {
background: #6c757d;
}
.export-btn.secondary:hover {
background: #545b62;
}
/* 반응형 */
@media (max-width: 768px) {
.statistics-container {
padding: 10px;
}
.stats-overview {
grid-template-columns: 1fr;
}
.question-stats-grid {
grid-template-columns: 1fr;
}
.chart-header {
flex-direction: column;
gap: 15px;
align-items: flex-start;
}
.export-buttons {
flex-direction: column;
align-items: center;
}
.export-btn {
width: 100%;
max-width: 250px;
justify-content: center;
}
.answer-item {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.answer-label,
.answer-count,
.answer-percentage {
min-width: auto;
text-align: left;
}
}
/* 애니메이션 */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.stat-card,
.chart-section,
.question-stat {
animation: fadeInUp 0.6s ease forwards;
}
.stat-card:nth-child(even) {
animation-delay: 0.1s;
}
.question-stat:nth-child(even) {
animation-delay: 0.2s;
}
+220
View File
@@ -0,0 +1,220 @@
/* 설문 관리 관리자 스타일 */
.status-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 0.8em;
font-weight: 500;
text-transform: uppercase;
}
.status-draft {
background: #e9ecef;
color: #495057;
}
.status-active {
background: #d4edda;
color: #155724;
}
.status-closed {
background: #f8d7da;
color: #721c24;
}
.status-deleted {
background: #d1ecf1;
color: #0c5460;
}
/* 공통 버튼 스타일 */
.btn {
display: inline-block;
padding: 8px 16px;
border: none;
border-radius: 4px;
text-decoration: none;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary {
background: #AA20FF;
color: white;
}
.btn-primary:hover {
background: #8A1ACC;
color: white;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-success:hover {
background: #218838;
color: white;
}
.btn-warning {
background: #ffc107;
color: #212529;
}
.btn-warning:hover {
background: #e0a800;
color: #212529;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
color: white;
}
.btn-info {
background: #17a2b8;
color: white;
}
.btn-info:hover {
background: #138496;
color: white;
}
/* 알림 스타일 */
.alert {
padding: 15px;
margin: 20px 0;
border-radius: 5px;
}
.alert-success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.alert-danger {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.alert-warning {
background-color: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
}
.alert-info {
background-color: #d1ecf1;
border: 1px solid #bee5eb;
color: #0c5460;
}
/* 테이블 스타일 */
.table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.table th,
.table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #dee2e6;
}
.table th {
background-color: #fff;
font-weight: 600;
}
.table tbody tr:hover {
background-color: #f5f5f5;
}
/* 폼 스타일 */
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
font-weight: 600;
margin-bottom: 5px;
color: #333;
}
.form-control {
width: 100%;
padding: 10px 12px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 1em;
}
.form-control:focus {
outline: none;
border-color: #AA20FF;
box-shadow: 0 0 0 2px rgba(170, 32, 255, 0.25);
}
/* 셀렉트 박스 스타일 수정 */
select {
height: 43px !important;
line-height: 41px !important;
border: 1px solid #d5d5d5 !important;
padding: 8px 12px !important;
font-size: 1em !important;
background-color: #fff !important;
}
.form-select,
.question-type-select {
height: 43px !important;
line-height: 41px !important;
border: 1px solid #d5d5d5 !important;
padding: 8px 12px !important;
font-size: 1em !important;
background-color: #fff !important;
}
/* 설문 폼 내의 모든 셀렉트 박스 */
.survey-form-container select,
.template-form-container select {
height: 43px !important;
line-height: 41px !important;
border: 1px solid #d5d5d5 !important;
padding: 8px 12px !important;
font-size: 1em !important;
background-color: #fff !important;
vertical-align: middle !important;
}
/* 반응형 */
@media (max-width: 768px) {
.btn {
padding: 6px 12px;
font-size: 0.9em;
}
.table {
font-size: 0.9em;
}
.table th,
.table td {
padding: 8px;
}
}
+380
View File
@@ -0,0 +1,380 @@
/* 설문 작성/수정 폼 스타일 */
.survey-form-container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.form-header {
background: linear-gradient(135deg, #AA20FF 0%, #8A1ACC 100%);
color: white;
padding: 30px;
border-radius: 15px;
margin-bottom: 30px;
text-align: center;
}
.form-header h1 {
margin: 0 0 10px 0;
font-size: 2.2em;
}
.form-tabs {
display: flex;
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 20px;
overflow: hidden;
}
.form-tab {
flex: 1;
padding: 15px 20px;
background: #fff;
border: none;
cursor: pointer;
font-size: 1em;
font-weight: 600;
transition: all 0.3s ease;
position: relative;
}
.form-tab.active {
background: #AA20FF;
color: white;
}
.form-tab:not(.active):hover {
background: #e9ecef;
}
.tab-content {
display: none;
background: white;
border-radius: 15px;
padding: 30px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.tab-content.active {
display: block;
}
.form-group {
margin-bottom: 25px;
}
.form-label {
display: block;
font-weight: 600;
color: #333;
margin-bottom: 8px;
font-size: 1em;
}
.form-label.required::after {
content: ' *';
color: #e74c3c;
}
.form-input,
.form-textarea,
.form-select {
width: 100%;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1em;
transition: all 0.3s ease;
background: #fafafa;
}
.form-input:focus,
.form-textarea:focus,
.form-select:focus {
outline: none;
border-color: #AA20FF;
background: white;
box-shadow: 0 0 0 3px rgba(170, 32, 255, 0.1);
}
.form-textarea {
min-height: 100px;
resize: vertical;
}
.form-row {
display: flex;
gap: 20px;
}
.form-col {
flex: 1;
}
.checkbox-group {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox-item input[type="checkbox"] {
transform: scale(1.2);
accent-color: #AA20FF;
}
.color-picker {
width: 60px;
height: 40px;
border: none;
border-radius: 8px;
cursor: pointer;
}
/* 템플릿 선택 섹션 */
.template-section {
background: #fff;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
.template-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin-top: 15px;
}
.template-card {
background: white;
padding: 20px;
border-radius: 10px;
border: 2px solid #e0e0e0;
cursor: pointer;
transition: all 0.3s ease;
}
.template-card:hover {
border-color: #AA20FF;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.template-card.selected {
border-color: #AA20FF;
background: rgba(170, 32, 255, 0.05);
}
/* 질문 관리 섹션 */
.questions-container {
margin-top: 20px;
}
.question-item {
background: #fff;
border: 2px solid #e0e0e0;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
position: relative;
}
.question-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.question-number {
background: #AA20FF;
color: white;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 15px;
}
.question-controls {
display: flex;
gap: 10px;
}
.btn-sm {
padding: 5px 10px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 0.8em;
transition: all 0.3s ease;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-sm:hover {
transform: translateY(-1px);
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.question-type-select {
margin-bottom: 15px;
}
.options-container {
margin-top: 15px;
}
.option-item {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 10px;
}
.option-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 5px;
}
.add-option-btn {
background: #28a745;
color: white;
border: none;
padding: 8px 15px;
border-radius: 5px;
cursor: pointer;
font-size: 0.9em;
}
.add-question-btn {
width: 100%;
padding: 15px;
background: #AA20FF;
color: white;
border: none;
border-radius: 10px;
font-size: 1.1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 20px;
}
.add-question-btn:hover {
background: #8A1ACC;
transform: translateY(-2px);
}
/* 폼 액션 버튼 */
.form-actions {
display: flex;
gap: 15px;
justify-content: center;
padding: 20px;
background: white;
border-radius: 15px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.btn {
padding: 12px 30px;
border: none;
border-radius: 8px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
text-align: center;
}
.btn-primary {
background: #AA20FF;
color: white;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
.help-text {
font-size: 0.9em;
color: #666;
margin-top: 5px;
line-height: 1.4;
}
/* 셀렉트 박스 높이 수정 */
select,
.form-select,
.question-type-select {
height: 43px !important;
line-height: 41px !important;
padding: 8px 12px !important;
vertical-align: middle !important;
}
/* 반응형 */
@media (max-width: 768px) {
.survey-form-container {
padding: 10px;
}
.form-row {
flex-direction: column;
}
.form-tabs {
flex-direction: column;
}
.template-grid {
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column;
}
.question-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
}
+290
View File
@@ -0,0 +1,290 @@
/* 템플릿 작성/수정 폼 스타일 */
.template-form-container {
max-width: 1000px;
margin: 0 auto;
}
.form-header {
background: linear-gradient(135deg, #AA20FF 0%, #8A1ACC 100%);
color: white;
padding: 30px;
border-radius: 8px 8px 0 0;
text-align: center;
}
.form-content {
background: white;
padding: 30px;
border-radius: 0 0 8px 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.form-section {
margin-bottom: 40px;
padding-bottom: 30px;
border-bottom: 1px solid #e9ecef;
}
.form-section:last-child {
border-bottom: none;
margin-bottom: 0;
}
.section-title {
font-size: 1.3em;
font-weight: 600;
color: #333;
margin-bottom: 20px;
display: flex;
align-items: center;
}
.section-title i {
margin-right: 10px;
color: #AA20FF;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.form-label.required::after {
content: ' *';
color: #dc3545;
}
.form-control {
width: 100%;
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 1em;
transition: border-color 0.3s;
}
.form-control:focus {
outline: none;
border-color: #AA20FF;
box-shadow: 0 0 0 3px rgba(170, 32, 255, 0.1);
}
.form-select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
background-position: right 12px center;
background-repeat: no-repeat;
background-size: 16px 12px;
padding-right: 40px;
height: 43px !important;
line-height: 41px !important;
}
.questions-container {
border: 2px dashed #ddd;
border-radius: 8px;
padding: 20px;
min-height: 200px;
}
.question-item {
background: #fff;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
position: relative;
}
.question-header {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.question-number {
background: #AA20FF;
color: white;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
margin-right: 15px;
font-size: 0.9em;
}
.question-actions {
margin-left: auto;
display: flex;
gap: 10px;
}
.btn-sm {
padding: 6px 12px;
font-size: 0.85em;
border-radius: 4px;
border: none;
cursor: pointer;
transition: all 0.3s;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #545b62;
}
.form-row {
display: flex;
gap: 15px;
align-items: end;
}
.form-row .form-group {
flex: 1;
margin-bottom: 0;
}
.options-container {
margin-top: 15px;
}
.option-item {
display: flex;
align-items: center;
margin-bottom: 10px;
gap: 10px;
}
.option-input {
flex: 1;
}
.btn-add-option {
background: #28a745;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
}
.btn-add-option:hover {
background: #218838;
}
.btn-add-question {
width: 100%;
padding: 15px;
background: #AA20FF;
color: white;
border: none;
border-radius: 6px;
font-size: 1em;
font-weight: 500;
cursor: pointer;
transition: background 0.3s;
}
.btn-add-question:hover {
background: #8A1ACC;
}
.form-actions {
text-align: center;
padding-top: 30px;
border-top: 1px solid #e9ecef;
margin-top: 30px;
}
.btn-primary {
background: #AA20FF;
color: white;
padding: 12px 30px;
border: none;
border-radius: 6px;
font-size: 1em;
font-weight: 500;
cursor: pointer;
margin: 0 10px;
transition: background 0.3s;
}
.btn-primary:hover {
background: #8A1ACC;
}
.btn-outline {
background: transparent;
color: #6c757d;
border: 1px solid #6c757d;
}
.btn-outline:hover {
background: #6c757d;
color: white;
}
.empty-questions {
text-align: center;
color: #666;
padding: 40px 20px;
}
.empty-questions i {
font-size: 3em;
margin-bottom: 15px;
opacity: 0.3;
}
/* 반응형 */
@media (max-width: 768px) {
.template-form-container {
margin: 10px;
}
.form-content {
padding: 20px;
}
.form-row {
flex-direction: column;
gap: 0;
}
.form-row .form-group {
margin-bottom: 15px;
}
.question-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.question-actions {
margin-left: 0;
}
}
+231
View File
@@ -0,0 +1,231 @@
/* 템플릿 목록 페이지 스타일 */
.template-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 20px;
background: linear-gradient(135deg, #AA20FF 0%, #8A1ACC 100%);
color: white;
border-radius: 8px;
}
.template-stats {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.stat-card {
flex: 1;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
border-left: 4px solid #AA20FF;
}
.stat-number {
font-size: 2em;
font-weight: bold;
color: #AA20FF;
margin-bottom: 5px;
}
.stat-label {
color: #666;
font-size: 0.9em;
}
.category-filter {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.category-btn {
padding: 8px 16px;
border: 1px solid #ddd;
background: white;
color: #666;
text-decoration: none;
border-radius: 20px;
font-size: 0.9em;
transition: all 0.3s;
}
.category-btn:hover,
.category-btn.active {
background: #AA20FF;
color: white;
border-color: #AA20FF;
}
.template-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.template-card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transition: all 0.3s ease;
border: 2px solid transparent;
}
.template-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
border-color: #AA20FF;
}
.template-card-header {
margin-bottom: 15px;
}
.template-title {
font-size: 1.3em;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.template-category {
display: inline-block;
padding: 4px 12px;
background: #e9ecef;
color: #666;
border-radius: 12px;
font-size: 0.8em;
font-weight: 500;
}
.template-description {
color: #666;
line-height: 1.5;
margin-bottom: 15px;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.template-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9em;
color: #888;
margin-bottom: 15px;
}
.template-actions {
display: flex;
gap: 10px;
}
.btn-template {
flex: 1;
padding: 10px 15px;
border: none;
border-radius: 6px;
font-size: 0.9em;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
text-align: center;
}
.btn-use {
background: #AA20FF;
color: white;
}
.btn-use:hover {
background: #8A1ACC;
color: white;
}
.btn-preview {
background: #fff;
color: #666;
border: 1px solid #e0e0e0;
}
.btn-preview:hover {
background: #e9ecef;
color: #333;
}
.search-form {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.search-form .form-row {
display: flex;
gap: 10px;
align-items: center;
}
.search-form select,
.search-form input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
.create-btn {
position: fixed;
bottom: 30px;
right: 30px;
width: 60px;
height: 60px;
background: #AA20FF;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
font-size: 1.5em;
box-shadow: 0 4px 12px rgba(170, 32, 255, 0.3);
transition: all 0.3s;
z-index: 1000;
}
.create-btn:hover {
background: #8A1ACC;
transform: scale(1.1);
color: white;
}
/* 반응형 */
@media (max-width: 768px) {
.template-grid {
grid-template-columns: 1fr;
}
.template-stats {
flex-direction: column;
}
.category-filter {
justify-content: center;
}
.search-form .form-row {
flex-direction: column;
}
}
+392
View File
@@ -0,0 +1,392 @@
<?php
$sub_menu = '710600';
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "r");
// 엑셀 다운로드 처리
if (isset($_GET['sv_id']) && isset($_GET['format'])) {
$sv_id = (int)$_GET['sv_id'];
$format = $_GET['format'];
if ($format === 'excel' || $format === 'csv') {
$survey = get_survey($sv_id);
if (!$survey) {
alert('존재하지 않는 설문입니다.');
}
$export_data = get_survey_export_data($sv_id);
if (empty($export_data) || count($export_data) <= 1) { // 헤더만 있는 경우 포함
alert('내보낼 응답 데이터가 없습니다.');
}
if ($format === 'excel') {
// 💡 [수정] ZipArchive 클래스가 없는 치명적 오류를 방지하기 위해, 클래스 존재 여부를 먼저 확인합니다.
// if (!class_exists('ZipArchive')) {
// die('엑셀 파일 생성에 필요한 ZipArchive 클래스를 찾을 수 없습니다. 서버의 PHP 설정에서 "zip" 확장을 활성화해주세요.');
// }
include_once(G5_LIB_PATH . '/PHPExcel.php');
try {
// 💡 대용량 데이터 처리 시 발생할 수 있는 메모리 부족 및 시간 초과 문제를 방지합니다.
$objPHPExcel = new PHPExcel();
$objPHPExcel->setActiveSheetIndex(0)
->fromArray($export_data, null, 'A1');
// 컬럼 너비 자동 조정
$sheet = $objPHPExcel->getActiveSheet();
foreach (range('A', $sheet->getHighestDataColumn()) as $col) {
$sheet->getColumnDimension($col)->setAutoSize(true);
}
// 💡 기존에 생성된 모든 출력 버퍼를 강제로 비워서, 순수한 파일 데이터만 전송되도록 보장합니다.
while (ob_get_level()) {
ob_end_clean();
}
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment; filename="survey_' . $sv_id . '_' . date('Y-m-d') . '.xlsx"');
header('Cache-Control: max-age=0');
$objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007');
$objWriter->save('php://output');
exit;
} catch (Exception $e) {
// 💡 엑셀 생성 중 오류 발생 시, 깨진 파일을 보내는 대신 명확한 오류 메시지를 출력합니다.
die('엑셀 파일 생성 중 오류가 발생했습니다. 오류: ' . htmlspecialchars($e->getMessage()));
}
} else { // CSV
// CSV 다운로드
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="survey_' . $sv_id . '_' . date('Y-m-d') . '.csv"');
header('Cache-Control: max-age=0');
$output = fopen('php://output', 'w');
// UTF-8 BOM 추가 (엑셀에서 한글 깨짐 방지)
fprintf($output, chr(0xEF) . chr(0xBB) . chr(0xBF));
foreach ($export_data as $row) {
fputcsv($output, $row);
}
fclose($output);
exit;
}
}
}
$g5['title'] = '엑셀 내보내기';
include_once(G5_ADMIN_PATH . '/admin.head.php');
// 설문 목록 가져오기
$survey_list = array();
$survey_sql = "SELECT sv_id, sv_title, sv_created_at,
(SELECT COUNT(*) FROM survey_responses WHERE sv_id = sm.sv_id AND sr_status = 'completed') as response_count
FROM survey_master sm
ORDER BY sv_created_at DESC";
$survey_result = sql_query($survey_sql);
while ($survey_row = sql_fetch_array($survey_result)) {
$survey_list[] = $survey_row;
}
?>
<style>
.export-header {
display: flex;
justify-content: between;
align-items: center;
margin-bottom: 20px;
padding: 20px;
background: linear-gradient(135deg, #AA20FF 0%, #8A1ACC 100%);
color: white;
border-radius: 8px;
}
.export-guide {
background: #e7f3ff;
border: 1px solid #b3d9ff;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
}
.export-guide h3 {
color: #0066cc;
margin-bottom: 15px;
}
.export-guide ul {
margin: 0;
padding-left: 20px;
}
.export-guide li {
margin-bottom: 8px;
line-height: 1.5;
}
.survey-export-list {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.survey-export-list table {
width: 100%;
border-collapse: collapse;
}
.survey-export-list th {
background: #fff;
padding: 15px 10px;
text-align: left;
font-weight: 600;
border-bottom: 1px solid #dee2e6;
}
.survey-export-list td {
padding: 15px 10px;
border-bottom: 1px solid #f1f3f4;
vertical-align: middle;
}
.survey-export-list tr:hover {
background: #fff;
}
.survey-title {
font-weight: 600;
color: #333;
margin-bottom: 5px;
}
.survey-meta {
font-size: 0.9em;
color: #666;
}
.response-count {
font-size: 1.2em;
font-weight: bold;
color: #AA20FF;
}
.export-buttons {
display: flex;
gap: 10px;
}
.btn-export {
padding: 8px 15px;
border: none;
border-radius: 5px;
font-size: 0.9em;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 5px;
}
.btn-excel {
background: #28a745;
color: white;
}
.btn-excel:hover {
background: #218838;
color: white;
}
.btn-csv {
background: #17a2b8;
color: white;
}
.btn-csv:hover {
background: #138496;
color: white;
}
.btn-export:disabled {
background: #ccc;
cursor: not-allowed;
}
.empty-state {
text-align: center;
padding: 80px 20px;
color: #666;
}
.empty-state i {
font-size: 4em;
margin-bottom: 20px;
display: block;
opacity: 0.3;
}
.format-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.format-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-left: 4px solid #AA20FF;
}
.format-card h4 {
color: #AA20FF;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.format-card p {
color: #666;
line-height: 1.5;
margin: 0;
}
/* 반응형 */
@media (max-width: 768px) {
.format-info {
grid-template-columns: 1fr;
}
.export-buttons {
flex-direction: column;
}
.survey-export-list {
font-size: 0.9em;
}
.survey-export-list th,
.survey-export-list td {
padding: 10px 5px;
}
}
</style>
<div class="export-header">
<div>
<h1><i class="fa fa-download"></i> 엑셀 내보내기</h1>
<p>설문 응답 데이터를 엑셀 또는 CSV 형식으로 다운로드할 수 있습니다</p>
</div>
</div>
<div class="export-guide">
<h3><i class="fa fa-info-circle"></i> 내보내기 안내</h3>
<ul>
<li><strong>엑셀 형식 (.xls)</strong>: Microsoft Excel에서 바로 열 수 있으며, 한글이 깨지지 않습니다.</li>
<li><strong>CSV 형식 (.csv)</strong>: 다양한 프로그램에서 호환되며, 데이터 분석 도구에서 사용하기 좋습니다.</li>
<li>완료된 응답만 내보내기 됩니다. (진행중이거나 중단된 응답은 제외)</li>
<li>모든 질문과 답변이 포함되며, 응답자 정보도 함께 제공됩니다.</li>
</ul>
</div>
<div class="format-info">
<div class="format-card">
<h4><i class="fa fa-file-excel"></i> 엑셀 형식 (XLS)</h4>
<p>Microsoft Excel에서 바로 열 수 있는 형식입니다. 한글 깨짐 없이 데이터를 확인할 수 있으며, 차트나 피벗 테이블 생성에 적합합니다.</p>
</div>
<div class="format-card">
<h4><i class="fa fa-file-csv"></i> CSV 형식</h4>
<p>범용적인 데이터 형식으로 Google Sheets, R, Python 등 다양한 분석 도구에서 사용할 수 있습니다. 대용량 데이터 처리에 적합합니다.</p>
</div>
</div>
<?php if (!empty($survey_list)): ?>
<div class="survey-export-list">
<table>
<thead>
<tr>
<th width="60">ID</th>
<th>설문 제목</th>
<th width="100">응답 수</th>
<th width="120">생성일</th>
<th width="200">내보내기</th>
</tr>
</thead>
<tbody>
<?php foreach ($survey_list as $survey): ?>
<tr>
<td><?php echo $survey['sv_id']; ?></td>
<td>
<div class="survey-title"><?php echo htmlspecialchars($survey['sv_title']); ?></div>
<div class="survey-meta">ID: <?php echo $survey['sv_id']; ?></div>
</td>
<td>
<span class="response-count"><?php echo number_format($survey['response_count']); ?></span>
</td>
<td>
<small><?php echo date('Y-m-d', strtotime($survey['sv_created_at'])); ?></small>
</td>
<td>
<div class="export-buttons">
<?php if ($survey['response_count'] > 0): ?>
<a href="?sv_id=<?php echo $survey['sv_id']; ?>&format=excel"
class="btn-export btn-excel">
<i class="fa fa-file-excel"></i> 엑셀
</a>
<a href="?sv_id=<?php echo $survey['sv_id']; ?>&format=csv" class="btn-export btn-csv">
<i class="fa fa-file-csv"></i> CSV
</a>
<?php else: ?>
<button class="btn-export" disabled>
<i class="fa fa-ban"></i> 응답 없음
</button>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="empty-state">
<i class="fa fa-poll"></i>
<h3>등록된 설문이 없습니다</h3>
<p>설문을 먼저 생성해주세요.</p>
<a href="survey_form.php" style="color: #AA20FF; margin-top: 15px; display: inline-block;">
<i class="fa fa-plus"></i> 새 설문 만들기
</a>
</div>
<?php endif; ?>
<script>
// 다운로드 진행 상태 표시
document.querySelectorAll('.btn-export:not([disabled])').forEach(btn => {
btn.addEventListener('click', function (e) {
const originalText = this.innerHTML;
this.innerHTML = '<i class="fa fa-spinner fa-spin"></i> 준비중...';
this.style.pointerEvents = 'none';
// 3초 후 원래 상태로 복원
setTimeout(() => {
this.innerHTML = originalText;
this.style.pointerEvents = 'auto';
}, 3000);
});
});
</script>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
?>
+351
View File
@@ -0,0 +1,351 @@
<?php
$sub_menu = '700306';
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "r");
// 엑셀 다운로드 처리
if (isset($_GET['sv_id']) && isset($_GET['format']) && isset($_GET['method'])) {
$sv_id = (int)$_GET['sv_id'];
$format = $_GET['format'];
$method = $_GET['method'];
if ($format === 'excel') {
$survey = get_survey($sv_id);
if (!$survey) {
alert('존재하지 않는 설문입니다.');
}
$export_data = get_survey_export_data($sv_id);
if (empty($export_data) || count($export_data) <= 1) {
alert('내보낼 응답 데이터가 없습니다.');
}
// 방법 1: 간단한 HTML 테이블 방식 (현재 수정된 방식)
if ($method === 'html') {
while (ob_get_level()) {
ob_end_clean();
}
try {
$filename = 'survey_' . $sv_id . '_' . date('Y-m-d') . '_html.xls';
header('Content-Type: application/vnd.ms-excel');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Cache-Control: max-age=0');
header('Pragma: public');
echo '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">';
echo '<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head>';
echo '<body><table border="1">';
foreach ($export_data as $row) {
echo '<tr>';
foreach ($row as $cell) {
echo '<td>' . htmlspecialchars($cell) . '</td>';
}
echo '</tr>';
}
echo '</table></body></html>';
exit;
} catch (Exception $e) {
die('엑셀 파일 생성 중 오류가 발생했습니다. 오류: ' . htmlspecialchars($e->getMessage()));
}
}
// 방법 2: CSV를 XLS로 저장하는 방식
elseif ($method === 'csv_as_xls') {
while (ob_get_level()) {
ob_end_clean();
}
try {
$filename = 'survey_' . $sv_id . '_' . date('Y-m-d') . '_csv.xls';
header('Content-Type: application/vnd.ms-excel');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Cache-Control: max-age=0');
foreach ($export_data as $row) {
echo implode("\t", $row) . "\n";
}
exit;
} catch (Exception $e) {
die('엑셀 파일 생성 중 오류가 발생했습니다. 오류: ' . htmlspecialchars($e->getMessage()));
}
}
// 방법 3: 기존 PHPExcel 방식 (문제가 있는 방식)
elseif ($method === 'phpexcel') {
if (!file_exists(G5_LIB_PATH . '/PHPExcel.php')) {
die('PHPExcel 라이브러리가 없습니다.');
}
include_once(G5_LIB_PATH . '/PHPExcel.php');
try {
while (ob_get_level()) {
ob_end_clean();
}
@ini_set('memory_limit', '256M');
@set_time_limit(300);
$objPHPExcel = new PHPExcel();
$objPHPExcel->setActiveSheetIndex(0)->fromArray($export_data, null, 'A1');
$sheet = $objPHPExcel->getActiveSheet();
foreach (range('A', $sheet->getHighestDataColumn()) as $col) {
$sheet->getColumnDimension($col)->setAutoSize(true);
}
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment; filename="survey_' . $sv_id . '_' . date('Y-m-d') . '_phpexcel.xlsx"');
header('Cache-Control: max-age=0');
$objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007');
$objWriter->save('php://output');
exit;
} catch (Exception $e) {
die('PHPExcel 오류: ' . htmlspecialchars($e->getMessage()));
}
}
// 방법 4: 순수 CSV 다운로드
elseif ($method === 'csv') {
while (ob_get_level()) {
ob_end_clean();
}
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="survey_' . $sv_id . '_' . date('Y-m-d') . '.csv"');
header('Cache-Control: max-age=0');
$output = fopen('php://output', 'w');
fprintf($output, chr(0xEF) . chr(0xBB) . chr(0xBF)); // UTF-8 BOM
foreach ($export_data as $row) {
fputcsv($output, $row);
}
fclose($output);
exit;
}
}
}
$g5['title'] = '엑셀 내보내기 테스트';
include_once(G5_ADMIN_PATH . '/admin.head.php');
// 설문 목록 가져오기
$survey_list = array();
$survey_sql = "SELECT sv_id, sv_title, sv_created_at,
(SELECT COUNT(*) FROM survey_responses WHERE sv_id = sm.sv_id AND sr_status = 'completed') as response_count
FROM survey_master sm
ORDER BY sv_created_at DESC";
$survey_result = sql_query($survey_sql);
while ($survey_row = sql_fetch_array($survey_result)) {
$survey_list[] = $survey_row;
}
?>
<style>
.test-header {
background: linear-gradient(135deg, #FF6B35 0%, #F7931E 100%);
color: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.test-methods {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.method-card {
background: white;
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 15px;
transition: all 0.3s ease;
}
.method-card:hover {
border-color: #FF6B35;
box-shadow: 0 4px 12px rgba(255, 107, 53, 0.15);
}
.method-title {
font-weight: bold;
color: #FF6B35;
margin-bottom: 8px;
}
.method-desc {
font-size: 0.9em;
color: #666;
margin-bottom: 10px;
}
.test-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.btn-test {
padding: 6px 12px;
border: none;
border-radius: 4px;
font-size: 0.85em;
cursor: pointer;
text-decoration: none;
display: inline-block;
transition: all 0.3s ease;
}
.btn-html { background: #28a745; color: white; }
.btn-html:hover { background: #218838; color: white; }
.btn-csv-xls { background: #17a2b8; color: white; }
.btn-csv-xls:hover { background: #138496; color: white; }
.btn-phpexcel { background: #6f42c1; color: white; }
.btn-phpexcel:hover { background: #5a32a3; color: white; }
.btn-csv { background: #fd7e14; color: white; }
.btn-csv:hover { background: #e8690b; color: white; }
.survey-list {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.survey-list table {
width: 100%;
border-collapse: collapse;
}
.survey-list th {
background: #fff;
padding: 12px 10px;
text-align: left;
font-weight: 600;
border-bottom: 1px solid #dee2e6;
}
.survey-list td {
padding: 12px 10px;
border-bottom: 1px solid #f1f3f4;
vertical-align: middle;
}
.survey-list tr:hover {
background: #fff;
}
</style>
<div class="test-header">
<h1><i class="fa fa-flask"></i> 엑셀 내보내기 테스트</h1>
<p>다양한 방법으로 엑셀 파일을 생성해서 어떤 방법이 문제없이 작동하는지 테스트합니다</p>
</div>
<div class="test-methods">
<div class="method-card">
<div class="method-title">방법 1: HTML 테이블</div>
<div class="method-desc">HTML 테이블을 엑셀 형식으로 출력하는 방식 (현재 수정된 방식)</div>
</div>
<div class="method-card">
<div class="method-title">방법 2: CSV를 XLS로</div>
<div class="method-desc">CSV 데이터를 XLS 확장자로 저장하는 방식</div>
</div>
<div class="method-card">
<div class="method-title">방법 3: PHPExcel</div>
<div class="method-desc">기존 PHPExcel 라이브러리를 사용하는 방식 (문제가 있는 방식)</div>
</div>
<div class="method-card">
<div class="method-title">방법 4: 순수 CSV</div>
<div class="method-desc">표준 CSV 형식으로 다운로드</div>
</div>
</div>
<?php if (!empty($survey_list)): ?>
<div class="survey-list">
<table>
<thead>
<tr>
<th width="60">ID</th>
<th>설문 제목</th>
<th width="80">응답 수</th>
<th width="100">생성일</th>
<th width="300">테스트 다운로드</th>
</tr>
</thead>
<tbody>
<?php foreach ($survey_list as $survey): ?>
<tr>
<td><?php echo $survey['sv_id']; ?></td>
<td>
<div style="font-weight: 600;"><?php echo htmlspecialchars($survey['sv_title']); ?></div>
</td>
<td>
<span style="font-weight: bold; color: #FF6B35;"><?php echo number_format($survey['response_count']); ?></span>
</td>
<td>
<small><?php echo date('Y-m-d', strtotime($survey['sv_created_at'])); ?></small>
</td>
<td>
<div class="test-buttons">
<?php if ($survey['response_count'] > 0): ?>
<a href="?sv_id=<?php echo $survey['sv_id']; ?>&format=excel&method=html"
class="btn-test btn-html">HTML</a>
<a href="?sv_id=<?php echo $survey['sv_id']; ?>&format=excel&method=csv_as_xls"
class="btn-test btn-csv-xls">CSV→XLS</a>
<a href="?sv_id=<?php echo $survey['sv_id']; ?>&format=excel&method=phpexcel"
class="btn-test btn-phpexcel">PHPExcel</a>
<a href="?sv_id=<?php echo $survey['sv_id']; ?>&format=excel&method=csv"
class="btn-test btn-csv">CSV</a>
<?php else: ?>
<span style="color: #999;">응답 없음</span>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="text-align: center; padding: 60px; color: #666;">
<i class="fa fa-poll" style="font-size: 3em; margin-bottom: 15px; opacity: 0.3;"></i>
<h3>등록된 설문이 없습니다</h3>
<p>설문을 먼저 생성해주세요.</p>
</div>
<?php endif; ?>
<div style="margin-top: 30px; padding: 20px; background: #fff; border-radius: 8px;">
<h4><i class="fa fa-info-circle"></i> 테스트 방법 설명</h4>
<ul>
<li><strong>HTML</strong>: HTML 테이블을 엑셀로 인식시키는 방식 - 가장 안정적</li>
<li><strong>CSV→XLS</strong>: CSV 데이터를 XLS 확장자로 저장 - 호환성 좋음</li>
<li><strong>PHPExcel</strong>: 기존 라이브러리 방식 - 문제가 있을 수 있음</li>
<li><strong>CSV</strong>: 표준 CSV 형식 - 가장 확실함</li>
</ul>
<p><strong>권장:</strong> 각 방법을 테스트해보고 정상적으로 다운로드되고 열리는 방법을 확인하세요.</p>
</div>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
?>
+200
View File
@@ -0,0 +1,200 @@
<?php
$sub_menu = '710700';
include_once('./_common.php');
include_once(__DIR__ . '/lib/SchemaManager.class.php');
auth_check_menu($auth, $sub_menu, "w");
$g5['title'] = '설문 관리 솔루션 설치';
include_once(G5_ADMIN_PATH.'/admin.head.php');
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;
}
function create_admin_menu_file() {
$source_file = __DIR__ . '/admin.menu710.survey.php';
$target_file = G5_ADMIN_PATH . '/admin.menu710.survey.php';
if (!file_exists($source_file)) return "실패 (메뉴 원본 파일 없음)";
if (file_exists($target_file)) return "성공 (이미 존재함)";
if (@copy($source_file, $target_file)) return "성공";
return "실패 (파일 복사 오류)";
}
$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();
$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(); }
$menu_msg = create_admin_menu_file();
$install_result = ['db' => $db_results, 'menu' => $menu_msg];
} 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.menu710.survey.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) > 0 && 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: #f8f9fa; 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-poll"></i> 설문 관리 솔루션</h1>
<p>그누보드5 기반의 전문적인 설문조사 시스템</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="<?php echo SURVEY_ADMIN_URL; ?>/survey_list.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="<?php echo SURVEY_ADMIN_URL; ?>/survey_list.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-star"></i> 주요 기능</h3>
<div class="feature-grid">
<div class="feature-card">
<i class="fa fa-edit"></i>
<h4>설문 작성</h4>
<p>다양한 질문 유형으로<br>전문적인 설문 작성</p>
</div>
<div class="feature-card">
<i class="fa fa-users"></i>
<h4>응답 관리</h4>
<p>실시간 응답 현황<br>모니터링</p>
</div>
<div class="feature-card">
<i class="fa fa-chart-bar"></i>
<h4>통계 분석</h4>
<p>시각적 차트와<br>상세 통계 제공</p>
</div>
<div class="feature-card">
<i class="fa fa-file-excel"></i>
<h4>엑셀 내보내기</h4>
<p>응답 데이터를<br>엑셀로 다운로드</p>
</div>
<div class="feature-card">
<i class="fa fa-mobile-alt"></i>
<h4>반응형 디자인</h4>
<p>PC/모바일<br>최적화 지원</p>
</div>
<div class="feature-card">
<i class="fa fa-palette"></i>
<h4>테마 커스터마이징</h4>
<p>rd.lwd 테마 기반<br>세련된 디자인</p>
</div>
</div>
<h3><i class="fa fa-database"></i> 설치 상태</h3>
<table class="status-table">
<thead><tr><th>테이블명</th><th>상태</th></tr></thead>
<tbody>
<?php foreach ($tables_to_check as $table): ?>
<tr>
<td><code><?php echo $table; ?></code></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');
?>
+155
View File
@@ -0,0 +1,155 @@
-- 설문 관리 솔루션 데이터베이스 스키마
-- Survey Management Solution Database Schema
-- 설문지 마스터 테이블
CREATE TABLE IF NOT EXISTS `survey_master` (
`sv_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '설문지 ID',
`sv_title` varchar(255) NOT NULL COMMENT '설문지 제목',
`sv_description` text COMMENT '설문지 설명',
`sv_start_date` datetime NOT NULL COMMENT '설문 시작일',
`sv_end_date` datetime NOT NULL COMMENT '설문 종료일',
`sv_status` enum('draft','active','closed','deleted') NOT NULL DEFAULT 'draft' COMMENT '설문 상태',
`sv_allow_anonymous` tinyint(1) NOT NULL DEFAULT 1 COMMENT '익명 참여 허용',
`sv_allow_multiple` tinyint(1) NOT NULL DEFAULT 0 COMMENT '중복 참여 허용',
`sv_max_responses` int(11) DEFAULT NULL COMMENT '최대 응답 수',
`sv_theme_color` varchar(7) DEFAULT '#AA20FF' COMMENT '테마 색상',
`sv_thank_message` text COMMENT '완료 메시지',
`sv_created_by` varchar(20) NOT NULL COMMENT '생성자',
`sv_created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
`sv_updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일시',
PRIMARY KEY (`sv_id`),
KEY `idx_status` (`sv_status`),
KEY `idx_dates` (`sv_start_date`, `sv_end_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='설문지 마스터';
-- 설문 질문 테이블
CREATE TABLE IF NOT EXISTS `survey_questions` (
`sq_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '질문 ID',
`sv_id` int(11) NOT NULL COMMENT '설문지 ID',
`sq_order` int(11) NOT NULL DEFAULT 1 COMMENT '질문 순서',
`sq_type` enum('text','textarea','radio','checkbox','select','rating','date','email','number') NOT NULL COMMENT '질문 유형',
`sq_title` varchar(500) NOT NULL COMMENT '질문 제목',
`sq_description` text COMMENT '질문 설명',
`sq_required` tinyint(1) NOT NULL DEFAULT 0 COMMENT '필수 여부',
`sq_options` text COMMENT '선택지 옵션 (JSON)',
`sq_validation` text COMMENT '유효성 검사 규칙 (JSON)',
`sq_created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
PRIMARY KEY (`sq_id`),
KEY `fk_survey_questions_master` (`sv_id`),
KEY `idx_order` (`sv_id`, `sq_order`),
CONSTRAINT `fk_survey_questions_master` FOREIGN KEY (`sv_id`) REFERENCES `survey_master` (`sv_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='설문 질문';
-- 설문 응답자 테이블
CREATE TABLE IF NOT EXISTS `survey_responses` (
`sr_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '응답 ID',
`sv_id` int(11) NOT NULL COMMENT '설문지 ID',
`sr_mb_id` varchar(20) DEFAULT NULL COMMENT '회원 ID (익명시 NULL)',
`sr_ip` varchar(45) NOT NULL COMMENT '응답자 IP',
`sr_user_agent` text COMMENT '사용자 에이전트',
`sr_session_id` varchar(128) COMMENT '세션 ID',
`sr_started_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '시작 시간',
`sr_completed_at` datetime DEFAULT NULL COMMENT '완료 시간',
`sr_status` enum('started','completed','abandoned') NOT NULL DEFAULT 'started' COMMENT '응답 상태',
PRIMARY KEY (`sr_id`),
KEY `fk_survey_responses_master` (`sv_id`),
KEY `idx_member` (`sr_mb_id`),
KEY `idx_ip` (`sr_ip`),
KEY `idx_status` (`sr_status`),
CONSTRAINT `fk_survey_responses_master` FOREIGN KEY (`sv_id`) REFERENCES `survey_master` (`sv_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='설문 응답자';
-- 설문 답변 테이블
CREATE TABLE IF NOT EXISTS `survey_answers` (
`sa_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '답변 ID',
`sr_id` int(11) NOT NULL COMMENT '응답 ID',
`sq_id` int(11) NOT NULL COMMENT '질문 ID',
`sa_value` text COMMENT '답변 값',
`sa_text` text COMMENT '기타 텍스트 답변',
`sa_created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '답변 시간',
PRIMARY KEY (`sa_id`),
KEY `fk_survey_answers_response` (`sr_id`),
KEY `fk_survey_answers_question` (`sq_id`),
KEY `idx_response_question` (`sr_id`, `sq_id`),
CONSTRAINT `fk_survey_answers_response` FOREIGN KEY (`sr_id`) REFERENCES `survey_responses` (`sr_id`) ON DELETE CASCADE,
CONSTRAINT `fk_survey_answers_question` FOREIGN KEY (`sq_id`) REFERENCES `survey_questions` (`sq_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='설문 답변';
-- 설문 통계 캐시 테이블 (성능 최적화용)
CREATE TABLE IF NOT EXISTS `survey_statistics` (
`ss_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '통계 ID',
`sv_id` int(11) NOT NULL COMMENT '설문지 ID',
`sq_id` int(11) NOT NULL COMMENT '질문 ID',
`ss_option_value` varchar(500) COMMENT '선택지 값',
`ss_count` int(11) NOT NULL DEFAULT 0 COMMENT '응답 수',
`ss_percentage` decimal(5,2) NOT NULL DEFAULT 0.00 COMMENT '비율',
`ss_updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '업데이트 시간',
PRIMARY KEY (`ss_id`),
UNIQUE KEY `uk_survey_stats` (`sv_id`, `sq_id`, `ss_option_value`),
KEY `fk_survey_statistics_master` (`sv_id`),
KEY `fk_survey_statistics_question` (`sq_id`),
CONSTRAINT `fk_survey_statistics_master` FOREIGN KEY (`sv_id`) REFERENCES `survey_master` (`sv_id`) ON DELETE CASCADE,
CONSTRAINT `fk_survey_statistics_question` FOREIGN KEY (`sq_id`) REFERENCES `survey_questions` (`sq_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='설문 통계 캐시';
-- 설문 템플릿 마스터 테이블
CREATE TABLE IF NOT EXISTS `survey_templates` (
`st_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '템플릿 ID',
`st_name` varchar(255) NOT NULL COMMENT '템플릿 이름',
`st_description` text COMMENT '템플릿 설명',
`st_category` varchar(50) NOT NULL DEFAULT '기타' COMMENT '템플릿 카테고리',
`st_is_public` tinyint(1) NOT NULL DEFAULT 1 COMMENT '공개 여부',
`st_created_by` varchar(20) NOT NULL COMMENT '생성자',
`st_created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
`st_updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일시',
PRIMARY KEY (`st_id`),
KEY `idx_category` (`st_category`),
KEY `idx_public` (`st_is_public`),
KEY `idx_created_by` (`st_created_by`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='설문 템플릿 마스터';
-- 설문 템플릿 질문 테이블
CREATE TABLE IF NOT EXISTS `survey_template_questions` (
`stq_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '템플릿 질문 ID',
`st_id` int(11) NOT NULL COMMENT '템플릿 ID',
`stq_order` int(11) NOT NULL DEFAULT 1 COMMENT '질문 순서',
`stq_type` enum('text','textarea','radio','checkbox','select','rating','date','email','number') NOT NULL COMMENT '질문 유형',
`stq_title` varchar(500) NOT NULL COMMENT '질문 제목',
`stq_description` text COMMENT '질문 설명',
`stq_required` tinyint(1) NOT NULL DEFAULT 0 COMMENT '필수 여부',
`stq_options` text COMMENT '선택지 옵션 (JSON)',
`stq_created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시',
PRIMARY KEY (`stq_id`),
KEY `fk_survey_template_questions_master` (`st_id`),
KEY `idx_order` (`st_id`, `stq_order`),
CONSTRAINT `fk_survey_template_questions_master` FOREIGN KEY (`st_id`) REFERENCES `survey_templates` (`st_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='설문 템플릿 질문';
-- 기본 템플릿 데이터 삽입
INSERT INTO `survey_templates` (`st_name`, `st_description`, `st_category`, `st_is_public`, `st_created_by`) VALUES
('고객 만족도 조사', '제품이나 서비스에 대한 고객 만족도를 측정하는 기본 템플릿입니다.', '고객서비스', 1, 'admin'),
('제품 피드백 설문', '새로운 제품에 대한 사용자 피드백을 수집하는 템플릿입니다.', '제품개발', 1, 'admin'),
('마케팅 캠페인 효과 측정', '마케팅 캠페인의 효과를 측정하고 개선점을 찾는 템플릿입니다.', '마케팅', 1, 'admin');
-- 고객 만족도 조사 템플릿 질문들
INSERT INTO `survey_template_questions` (`st_id`, `stq_order`, `stq_type`, `stq_title`, `stq_description`, `stq_required`, `stq_options`) VALUES
(1, 1, 'rating', '전반적인 서비스 만족도를 평가해주세요.', '1점(매우 불만족)부터 5점(매우 만족)까지 평가해주세요.', 1, NULL),
(1, 2, 'radio', '우리 서비스를 다른 사람에게 추천하시겠습니까?', '', 1, '["매우 추천", "추천", "보통", "추천하지 않음", "절대 추천하지 않음"]'),
(1, 3, 'checkbox', '어떤 부분이 가장 만족스러우셨나요? (복수선택 가능)', '', 0, '["제품 품질", "고객 서비스", "배송 속도", "가격", "사용 편의성", "기타"]'),
(1, 4, 'textarea', '개선이 필요한 부분이나 추가 의견이 있으시면 자유롭게 작성해주세요.', '', 0, NULL);
-- 제품 피드백 설문 템플릿 질문들
INSERT INTO `survey_template_questions` (`st_id`, `stq_order`, `stq_type`, `stq_title`, `stq_description`, `stq_required`, `stq_options`) VALUES
(2, 1, 'radio', '이 제품을 어떻게 알게 되셨나요?', '', 1, '["검색엔진", "소셜미디어", "친구/지인 추천", "광고", "기타"]'),
(2, 2, 'rating', '제품의 사용 편의성은 어떠셨나요?', '1점(매우 어려움)부터 5점(매우 쉬움)까지 평가해주세요.', 1, NULL),
(2, 3, 'radio', '제품의 가격은 적정하다고 생각하시나요?', '', 1, '["매우 비쌈", "비쌈", "적정함", "저렴함", "매우 저렴함"]'),
(2, 4, 'checkbox', '어떤 기능을 가장 자주 사용하시나요?', '', 0, '["기본 기능", "고급 기능", "설정 기능", "공유 기능", "분석 기능"]'),
(2, 5, 'textarea', '추가하고 싶은 기능이나 개선사항이 있다면 알려주세요.', '', 0, NULL);
-- 마케팅 캠페인 효과 측정 템플릿 질문들
INSERT INTO `survey_template_questions` (`st_id`, `stq_order`, `stq_type`, `stq_title`, `stq_description`, `stq_required`, `stq_options`) VALUES
(3, 1, 'radio', '최근 우리 브랜드의 광고를 본 적이 있나요?', '', 1, '["예, 여러 번 봤습니다", "예, 한두 번 봤습니다", "아니오, 본 적 없습니다"]'),
(3, 2, 'checkbox', '어떤 채널에서 우리 광고를 보셨나요? (복수선택 가능)', '', 0, '["TV", "온라인 광고", "소셜미디어", "옥외광고", "라디오", "기타"]'),
(3, 3, 'rating', '광고가 제품에 대한 관심을 높이는데 도움이 되었나요?', '1점(전혀 도움 안됨)부터 5점(매우 도움됨)까지 평가해주세요.', 1, NULL),
(3, 4, 'radio', '광고를 본 후 실제로 제품을 구매하셨나요?', '', 1, '["예, 구매했습니다", "아니오, 하지만 구매를 고려중입니다", "아니오, 구매할 계획이 없습니다"]'),
(3, 5, 'textarea', '광고에 대한 전반적인 의견이나 개선사항을 알려주세요.', '', 0, NULL);
+355
View File
@@ -0,0 +1,355 @@
<?php
$sub_menu = '710700';
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "w");
$g5['title'] = '템플릿 데이터 설치';
include_once(G5_ADMIN_PATH.'/admin.head.php');
if (isset($_POST['install_templates']) && $_POST['install_templates'] == '1') {
check_admin_token();
// 기존 템플릿 데이터 삭제 (선택사항)
if (isset($_POST['clear_existing'])) {
sql_query("DELETE FROM survey_template_questions");
sql_query("DELETE FROM survey_templates");
sql_query("ALTER TABLE survey_templates AUTO_INCREMENT = 1");
sql_query("ALTER TABLE survey_template_questions AUTO_INCREMENT = 1");
}
$success_count = 0;
$error_count = 0;
$errors = [];
// 기본 템플릿 데이터 삽입
$templates = [
[
'name' => '고객 만족도 조사',
'description' => '제품이나 서비스에 대한 고객 만족도를 측정하는 기본 템플릿입니다.',
'category' => '고객서비스',
'questions' => [
[
'order' => 1,
'type' => 'rating',
'title' => '전반적인 서비스 만족도를 평가해주세요.',
'description' => '1점(매우 불만족)부터 5점(매우 만족)까지 평가해주세요.',
'required' => 1,
'options' => null
],
[
'order' => 2,
'type' => 'radio',
'title' => '우리 서비스를 다른 사람에게 추천하시겠습니까?',
'description' => '',
'required' => 1,
'options' => '["매우 추천", "추천", "보통", "추천하지 않음", "절대 추천하지 않음"]'
],
[
'order' => 3,
'type' => 'checkbox',
'title' => '어떤 부분이 가장 만족스러우셨나요? (복수선택 가능)',
'description' => '',
'required' => 0,
'options' => '["제품 품질", "고객 서비스", "배송 속도", "가격", "사용 편의성", "기타"]'
],
[
'order' => 4,
'type' => 'textarea',
'title' => '개선이 필요한 부분이나 추가 의견이 있으시면 자유롭게 작성해주세요.',
'description' => '',
'required' => 0,
'options' => null
]
]
],
[
'name' => '제품 피드백 설문',
'description' => '새로운 제품에 대한 사용자 피드백을 수집하는 템플릿입니다.',
'category' => '제품개발',
'questions' => [
[
'order' => 1,
'type' => 'radio',
'title' => '이 제품을 어떻게 알게 되셨나요?',
'description' => '',
'required' => 1,
'options' => '["검색엔진", "소셜미디어", "친구/지인 추천", "광고", "기타"]'
],
[
'order' => 2,
'type' => 'rating',
'title' => '제품의 사용 편의성은 어떠셨나요?',
'description' => '1점(매우 어려움)부터 5점(매우 쉬움)까지 평가해주세요.',
'required' => 1,
'options' => null
],
[
'order' => 3,
'type' => 'radio',
'title' => '제품의 가격은 적정하다고 생각하시나요?',
'description' => '',
'required' => 1,
'options' => '["매우 비쌈", "비쌈", "적정함", "저렴함", "매우 저렴함"]'
],
[
'order' => 4,
'type' => 'checkbox',
'title' => '어떤 기능을 가장 자주 사용하시나요?',
'description' => '',
'required' => 0,
'options' => '["기본 기능", "고급 기능", "설정 기능", "공유 기능", "분석 기능"]'
],
[
'order' => 5,
'type' => 'textarea',
'title' => '추가하고 싶은 기능이나 개선사항이 있다면 알려주세요.',
'description' => '',
'required' => 0,
'options' => null
]
]
],
[
'name' => '마케팅 캠페인 효과 측정',
'description' => '마케팅 캠페인의 효과를 측정하고 개선점을 찾는 템플릿입니다.',
'category' => '마케팅',
'questions' => [
[
'order' => 1,
'type' => 'radio',
'title' => '최근 우리 브랜드의 광고를 본 적이 있나요?',
'description' => '',
'required' => 1,
'options' => '["예, 여러 번 봤습니다", "예, 한두 번 봤습니다", "아니오, 본 적 없습니다"]'
],
[
'order' => 2,
'type' => 'checkbox',
'title' => '어떤 채널에서 우리 광고를 보셨나요? (복수선택 가능)',
'description' => '',
'required' => 0,
'options' => '["TV", "온라인 광고", "소셜미디어", "옥외광고", "라디오", "기타"]'
],
[
'order' => 3,
'type' => 'rating',
'title' => '광고가 제품에 대한 관심을 높이는데 도움이 되었나요?',
'description' => '1점(전혀 도움 안됨)부터 5점(매우 도움됨)까지 평가해주세요.',
'required' => 1,
'options' => null
],
[
'order' => 4,
'type' => 'radio',
'title' => '광고를 본 후 실제로 제품을 구매하셨나요?',
'description' => '',
'required' => 1,
'options' => '["예, 구매했습니다", "아니오, 하지만 구매를 고려중입니다", "아니오, 구매할 계획이 없습니다"]'
],
[
'order' => 5,
'type' => 'textarea',
'title' => '광고에 대한 전반적인 의견이나 개선사항을 알려주세요.',
'description' => '',
'required' => 0,
'options' => null
]
]
]
];
foreach ($templates as $template_data) {
// 템플릿 삽입
$sql = "INSERT INTO survey_templates
(st_name, st_description, st_category, st_is_public, st_created_by, st_created_at)
VALUES
('".sql_real_escape_string($template_data['name'])."',
'".sql_real_escape_string($template_data['description'])."',
'".sql_real_escape_string($template_data['category'])."',
1,
'admin',
NOW())";
if (sql_query($sql)) {
$st_id = sql_insert_id();
$success_count++;
// 템플릿 질문들 삽입
foreach ($template_data['questions'] as $question) {
$options_value = $question['options'] ? "'".sql_real_escape_string($question['options'])."'" : 'NULL';
$question_sql = "INSERT INTO survey_template_questions
(st_id, stq_order, stq_type, stq_title, stq_description, stq_required, stq_options, stq_created_at)
VALUES
('$st_id',
'{$question['order']}',
'".sql_real_escape_string($question['type'])."',
'".sql_real_escape_string($question['title'])."',
'".sql_real_escape_string($question['description'])."',
'{$question['required']}',
$options_value,
NOW())";
if (sql_query($question_sql)) {
$success_count++;
} else {
$error_count++;
$errors[] = "질문 삽입 실패: " . $question['title'];
}
}
} else {
$error_count++;
$errors[] = "템플릿 삽입 실패: " . $template_data['name'];
}
}
echo '<div class="alert alert-success">';
echo '<h4>템플릿 데이터 설치 완료!</h4>';
echo '<p>성공: '.$success_count.'개, 실패: '.$error_count.'개</p>';
if (!empty($errors)) {
echo '<details><summary>오류 상세</summary>';
foreach ($errors as $error) {
echo '<p style="color: red;">'.$error.'</p>';
}
echo '</details>';
}
echo '<p><a href="template_list.php" class="btn btn-primary">템플릿 목록 확인하기</a></p>';
echo '</div>';
} else {
// 현재 템플릿 상태 확인
$template_count = sql_fetch("SELECT COUNT(*) as cnt FROM survey_templates")['cnt'];
$question_count = sql_fetch("SELECT COUNT(*) as cnt FROM survey_template_questions")['cnt'];
?>
<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);
}
.status-info {
background: #fff;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 4px solid #17a2b8;
}
.install-form {
background: #fff;
padding: 20px;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
.checkbox-item {
margin: 15px 0;
}
.checkbox-item input[type="checkbox"] {
margin-right: 8px;
transform: scale(1.2);
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
text-decoration: none;
display: inline-block;
}
.btn-primary {
background: #AA20FF;
color: white;
}
.btn-warning {
background: #ffc107;
color: #212529;
}
.btn:hover {
opacity: 0.9;
}
.alert {
padding: 15px;
margin: 20px 0;
border-radius: 5px;
}
.alert-success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.alert-warning {
background-color: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
}
</style>
<div class="install-container">
<h1><i class="fa fa-magic"></i> 템플릿 데이터 설치</h1>
<div class="status-info">
<h3>현재 상태</h3>
<p><strong>템플릿 수:</strong> <?php echo number_format($template_count); ?>개</p>
<p><strong>템플릿 질문 수:</strong> <?php echo number_format($question_count); ?>개</p>
<?php if ($template_count == 0 || $question_count == 0): ?>
<div class="alert alert-warning">
<strong>주의:</strong> 템플릿 데이터가 부족합니다. 아래 버튼을 클릭하여 기본 템플릿을 설치하세요.
</div>
<?php endif; ?>
</div>
<div class="install-form">
<h3>기본 템플릿 설치</h3>
<p>다음 3개의 기본 템플릿과 질문들이 설치됩니다:</p>
<ul>
<li><strong>고객 만족도 조사</strong> - 4개 질문 (평점, 객관식, 체크박스, 주관식)</li>
<li><strong>제품 피드백 설문</strong> - 5개 질문 (객관식, 평점, 체크박스, 주관식)</li>
<li><strong>마케팅 캠페인 효과 측정</strong> - 5개 질문 (객관식, 체크박스, 평점, 주관식)</li>
</ul>
<form method="post" onsubmit="return confirm('템플릿 데이터를 설치하시겠습니까?');">
<input type="hidden" name="install_templates" value="1">
<input type="hidden" name="token" value="<?php echo get_token(); ?>">
<?php if ($template_count > 0): ?>
<div class="checkbox-item">
<input type="checkbox" name="clear_existing" id="clearExisting">
<label for="clearExisting">기존 템플릿 데이터를 모두 삭제하고 새로 설치</label>
<small style="display: block; color: #666; margin-top: 5px;">
체크하지 않으면 기존 데이터에 추가로 설치됩니다.
</small>
</div>
<?php endif; ?>
<button type="submit" class="btn btn-primary">
<i class="fa fa-download"></i> 기본 템플릿 설치하기
</button>
<a href="template_list.php" class="btn btn-warning">
<i class="fa fa-list"></i> 템플릿 목록 보기
</a>
</form>
</div>
</div>
<?php
}
include_once(G5_ADMIN_PATH.'/admin.tail.php');
?>
+296
View File
@@ -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;
}
};
+214
View File
@@ -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);
+394
View File
@@ -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;
}
});
}
+280
View File
@@ -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;
}
});
}
@@ -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];
}
}
+365
View File
@@ -0,0 +1,365 @@
<?php
if (!defined('_GNUBOARD_')) exit;
// 💡 [핵심 수정] 라이브러리가 어디서든 독립적으로 실행될 수 있도록 필요한 상수를 정의합니다.
// AJAX 등 다른 경로로 라이브러리만 직접 호출될 때, 상수가 정의되지 않는 문제를 해결합니다.
// if(!defined(...))로 감싸서 _common.php 등에서 이미 정의된 경우 충돌을 방지합니다.
if (!defined('SURVEY_STATUS_DRAFT')) define('SURVEY_STATUS_DRAFT', 'draft');
if (!defined('SURVEY_STATUS_ACTIVE')) define('SURVEY_STATUS_ACTIVE', 'active');
if (!defined('SURVEY_STATUS_CLOSED')) define('SURVEY_STATUS_CLOSED', 'closed');
if (!defined('SURVEY_STATUS_DELETED')) define('SURVEY_STATUS_DELETED', 'deleted');
if (!defined('RESPONSE_STATUS_STARTED')) define('RESPONSE_STATUS_STARTED', 'started');
if (!defined('RESPONSE_STATUS_COMPLETED')) define('RESPONSE_STATUS_COMPLETED', 'completed');
if (!defined('RESPONSE_STATUS_ABANDONED')) define('RESPONSE_STATUS_ABANDONED', 'abandoned');
/**
* 설문 관리 라이브러리 함수들
*/
/**
* 설문지 정보 가져오기
*/
function get_survey($sv_id) {
global $g5;
$sql = "SELECT * FROM survey_master WHERE sv_id = '$sv_id'";
return sql_fetch($sql);
}
/**
* 설문 질문 목록 가져오기
*/
function get_survey_questions($sv_id) {
global $g5;
$sql = "SELECT * FROM survey_questions WHERE sv_id = '$sv_id' ORDER BY sq_order ASC";
$result = sql_query($sql);
$questions = array();
while ($row = sql_fetch_array($result)) {
if ($row['sq_options']) {
$row['sq_options'] = json_decode($row['sq_options'], true);
}
if ($row['sq_validation']) {
$row['sq_validation'] = json_decode($row['sq_validation'], true);
}
$questions[] = $row;
}
return $questions;
}
/**
* 설문 응답 수 가져오기
*/
function get_survey_response_count($sv_id, $status = 'completed') {
global $g5;
$where = "sv_id = '$sv_id'";
if ($status) {
$where .= " AND sr_status = '$status'";
}
$sql = "SELECT COUNT(*) as cnt FROM survey_responses WHERE $where";
$row = sql_fetch($sql);
return $row['cnt'];
}
/**
* 설문 상태 업데이트
*/
function update_survey_status($sv_id, $status) {
global $g5;
$sql = "UPDATE survey_master SET sv_status = '$status', sv_updated_at = NOW() WHERE sv_id = '$sv_id'";
return sql_query($sql);
}
/**
* 설문 응답 시작
*/
function start_survey_response($sv_id, $mb_id = null, $ip = '', $user_agent = '', $session_id = '') {
global $g5;
$sql = "INSERT INTO survey_responses
(sv_id, sr_mb_id, sr_ip, sr_user_agent, sr_session_id, sr_started_at, sr_status)
VALUES
('$sv_id', ".($mb_id ? "'$mb_id'" : 'NULL').", '$ip', '$user_agent', '$session_id', NOW(), '".RESPONSE_STATUS_STARTED."')";
sql_query($sql);
return sql_insert_id();
}
/**
* 설문 응답 완료
*/
function complete_survey_response($sr_id) {
global $g5;
$sql = "UPDATE survey_responses
SET sr_status = '".RESPONSE_STATUS_COMPLETED."', sr_completed_at = NOW()
WHERE sr_id = '$sr_id'";
return sql_query($sql);
}
/**
* 설문 답변 저장
*/
function save_survey_answer($sr_id, $sq_id, $value, $text = '') {
global $g5;
// 기존 답변 삭제
$sql = "DELETE FROM survey_answers WHERE sr_id = '$sr_id' AND sq_id = '$sq_id'";
sql_query($sql);
// 새 답변 저장
if (is_array($value)) {
foreach ($value as $v) {
$sql = "INSERT INTO survey_answers (sr_id, sq_id, sa_value, sa_text, sa_created_at)
VALUES ('$sr_id', '$sq_id', '$v', '$text', NOW())";
sql_query($sql);
}
} else {
$sql = "INSERT INTO survey_answers (sr_id, sq_id, sa_value, sa_text, sa_created_at)
VALUES ('$sr_id', '$sq_id', '$value', '$text', NOW())";
sql_query($sql);
}
return true;
}
/**
* 설문 통계 업데이트
*/
function update_survey_statistics($sv_id) {
global $g5;
// 기존 통계 삭제
$sql = "DELETE FROM survey_statistics WHERE sv_id = '$sv_id'";
sql_query($sql);
// 질문별 통계 생성
$questions = get_survey_questions($sv_id);
$total_responses = get_survey_response_count($sv_id, 'completed');
foreach ($questions as $question) {
$sq_id = $question['sq_id'];
if (in_array($question['sq_type'], ['radio', 'checkbox', 'select'])) {
// 선택형 질문 통계
$sql = "SELECT sa_value, COUNT(*) as cnt
FROM survey_answers sa
JOIN survey_responses sr ON sa.sr_id = sr.sr_id
WHERE sr.sv_id = '$sv_id' AND sa.sq_id = '$sq_id' AND sr.sr_status = 'completed'
GROUP BY sa_value";
$result = sql_query($sql);
while ($row = sql_fetch_array($result)) {
$percentage = $total_responses > 0 ? round(($row['cnt'] / $total_responses) * 100, 2) : 0;
$insert_sql = "INSERT INTO survey_statistics
(sv_id, sq_id, ss_option_value, ss_count, ss_percentage)
VALUES
('$sv_id', '$sq_id', '{$row['sa_value']}', '{$row['cnt']}', '$percentage')";
sql_query($insert_sql);
}
} elseif ($question['sq_type'] == 'rating') {
// 평점 질문 통계
$sql = "SELECT sa_value, COUNT(*) as cnt
FROM survey_answers sa
JOIN survey_responses sr ON sa.sr_id = sr.sr_id
WHERE sr.sv_id = '$sv_id' AND sa.sq_id = '$sq_id' AND sr.sr_status = 'completed'
GROUP BY sa_value
ORDER BY CAST(sa_value AS UNSIGNED)";
$result = sql_query($sql);
while ($row = sql_fetch_array($result)) {
$percentage = $total_responses > 0 ? round(($row['cnt'] / $total_responses) * 100, 2) : 0;
$insert_sql = "INSERT INTO survey_statistics
(sv_id, sq_id, ss_option_value, ss_count, ss_percentage)
VALUES
('$sv_id', '$sq_id', '{$row['sa_value']}', '{$row['cnt']}', '$percentage')";
sql_query($insert_sql);
}
}
}
return true;
}
/**
* 설문 템플릿 목록 가져오기
*/
function get_survey_templates($category = '') {
global $g5;
$where = "st_is_public = 1";
if ($category) {
$where .= " AND st_category = '$category'";
}
$sql = "SELECT * FROM survey_templates WHERE $where ORDER BY st_created_at DESC";
$result = sql_query($sql);
$templates = array();
while ($row = sql_fetch_array($result)) {
$templates[] = $row;
}
return $templates;
}
/**
* 설문 템플릿으로 설문 생성
*/
function create_survey_from_template($template_id, $title, $created_by) {
global $g5;
$template = sql_fetch("SELECT * FROM survey_templates WHERE st_id = '$template_id'");
if (!$template) {
return false;
}
$template_data = json_decode($template['st_data'], true);
// 설문지 생성
$sql = "INSERT INTO survey_master
(sv_title, sv_description, sv_start_date, sv_end_date, sv_status, sv_created_by)
VALUES
('$title', '{$template_data['description']}', NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY), '".SURVEY_STATUS_DRAFT."', '$created_by')";
sql_query($sql);
$sv_id = sql_insert_id();
// 질문들 생성
foreach ($template_data['questions'] as $index => $question) {
$sq_order = $index + 1;
$sq_type = $question['type'];
$sq_title = addslashes($question['title']);
$sq_required = isset($question['required']) && $question['required'] ? 1 : 0;
$sq_options = isset($question['options']) ? addslashes(json_encode($question['options'])) : '';
$sq_validation = isset($question['validation']) ? addslashes(json_encode($question['validation'])) : '';
$sql = "INSERT INTO survey_questions
(sv_id, sq_order, sq_type, sq_title, sq_required, sq_options, sq_validation)
VALUES
('$sv_id', '$sq_order', '$sq_type', '$sq_title', '$sq_required', '$sq_options', '$sq_validation')";
sql_query($sql);
}
return $sv_id;
}
/**
* 설문 데이터 엑셀 내보내기용 배열 생성
*/
function get_survey_export_data($sv_id) {
global $g5;
$survey = get_survey($sv_id);
$questions = get_survey_questions($sv_id);
// 헤더 생성
$headers = ['응답ID', '응답자', 'IP주소', '시작시간', '완료시간'];
foreach ($questions as $question) {
$headers[] = strip_tags($question['sq_title']);
}
// 데이터 생성
$data = array();
$data[] = $headers;
$sql = "SELECT * FROM survey_responses
WHERE sv_id = '$sv_id' AND sr_status = 'completed'
ORDER BY sr_completed_at DESC";
$result = sql_query($sql);
while ($response = sql_fetch_array($result)) {
$row = array();
$row[] = $response['sr_id'];
$row[] = $response['sr_mb_id'] ?: '익명';
$row[] = $response['sr_ip'];
$row[] = $response['sr_started_at'];
$row[] = $response['sr_completed_at'];
// 각 질문별 답변
foreach ($questions as $question) {
$answer_sql = "SELECT sa_value, sa_text FROM survey_answers
WHERE sr_id = '{$response['sr_id']}' AND sq_id = '{$question['sq_id']}'";
$answer_result = sql_query($answer_sql);
$answers = array();
while ($answer = sql_fetch_array($answer_result)) {
if ($answer['sa_text']) {
$answers[] = $answer['sa_value'] . ' (' . $answer['sa_text'] . ')';
} else {
$answers[] = $answer['sa_value'];
}
}
$row[] = implode(', ', $answers);
}
$data[] = $row;
}
return $data;
}
/**
* 설문 유효성 검사
*/
function validate_survey_access($sv_id, $mb_id = null, $ip = '') {
global $g5;
$survey = get_survey($sv_id);
if (!$survey) {
return array('success' => false, 'message' => '존재하지 않는 설문입니다.');
}
// 상태 확인
if ($survey['sv_status'] != SURVEY_STATUS_ACTIVE) {
return array('success' => false, 'message' => '현재 참여할 수 없는 설문입니다.');
}
// 기간 확인
$now = date('Y-m-d H:i:s');
if ($now < $survey['sv_start_date']) {
return array('success' => false, 'message' => '아직 시작되지 않은 설문입니다.');
}
if ($now > $survey['sv_end_date']) {
return array('success' => false, 'message' => '종료된 설문입니다.');
}
// 중복 참여 확인
if (!$survey['sv_allow_multiple']) {
$where = "sv_id = '$sv_id' AND sr_status = 'completed'";
if ($mb_id) {
$where .= " AND sr_mb_id = '$mb_id'";
} else {
$where .= " AND sr_ip = '$ip'";
}
$existing = sql_fetch("SELECT COUNT(*) as cnt FROM survey_responses WHERE $where");
if ($existing['cnt'] > 0) {
return array('success' => false, 'message' => '이미 참여하신 설문입니다.');
}
}
// 최대 응답 수 확인
if ($survey['sv_max_responses']) {
$total_responses = get_survey_response_count($sv_id, 'completed');
if ($total_responses >= $survey['sv_max_responses']) {
return array('success' => false, 'message' => '설문 참여 인원이 마감되었습니다.');
}
}
return array('success' => true, 'survey' => $survey);
}
+452
View File
@@ -0,0 +1,452 @@
<?php
$sub_menu = '710304';
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "r");
$g5['title'] = '응답 관리';
include_once(G5_ADMIN_PATH.'/admin.head.php');
// 설문 선택
$sv_id = isset($_GET['sv_id']) ? (int)$_GET['sv_id'] : 0;
// 설문 목록 가져오기
$survey_list = array();
$survey_sql = "SELECT sv_id, sv_title FROM survey_master ORDER BY sv_created_at DESC";
$survey_result = sql_query($survey_sql);
while ($survey_row = sql_fetch_array($survey_result)) {
$survey_list[] = $survey_row;
}
if ($sv_id) {
$survey = get_survey($sv_id);
if (!$survey) {
alert('존재하지 않는 설문입니다.', 'response_list.php');
}
}
// 변수 초기화
$sfl = isset($_GET['sfl']) ? clean_xss_tags($_GET['sfl']) : 'sr_mb_id';
$stx = isset($_GET['stx']) ? clean_xss_tags($_GET['stx']) : '';
$status = isset($_GET['status']) ? clean_xss_tags($_GET['status']) : '';
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
// 검색 조건
$where = " WHERE 1=1 ";
$sql_search = "";
if ($sv_id) {
$where .= " AND sr.sv_id = '".sql_real_escape_string($sv_id)."' ";
$sql_search .= "&sv_id=$sv_id";
}
if ($stx) {
if ($sfl == 'sr_mb_id') {
$where .= " AND sr.sr_mb_id LIKE '%".sql_real_escape_string($stx)."%' ";
} elseif ($sfl == 'sr_ip') {
$where .= " AND sr.sr_ip LIKE '%".sql_real_escape_string($stx)."%' ";
}
$sql_search .= "&sfl=$sfl&stx=".urlencode($stx);
}
if ($status) {
$where .= " AND sr.sr_status = '".sql_real_escape_string($status)."' ";
$sql_search .= "&status=".urlencode($status);
}
// 페이징
$sql_common = " FROM survey_responses sr
LEFT JOIN survey_master sm ON sr.sv_id = sm.sv_id
$where ";
$sql = " SELECT COUNT(*) as cnt $sql_common ";
$row = sql_fetch($sql);
$total_count = $row['cnt'];
$rows = 20;
$total_page = ceil($total_count / $rows);
if ($page < 1) $page = 1;
$from_record = ($page - 1) * $rows;
$sql = " SELECT sr.*, sm.sv_title
$sql_common
ORDER BY sr.sr_started_at DESC
LIMIT $from_record, $rows ";
$result = sql_query($sql);
// 상태별 카운트
$status_counts = array();
$status_where = $sv_id ? " WHERE sv_id = '$sv_id' " : " WHERE 1=1 ";
$status_sql = "SELECT sr_status, COUNT(*) as cnt FROM survey_responses $status_where GROUP BY sr_status";
$status_result = sql_query($status_sql);
while ($status_row = sql_fetch_array($status_result)) {
$status_counts[$status_row['sr_status']] = $status_row['cnt'];
}
?>
<style>
.response-header {
display: flex;
justify-content: between;
align-items: center;
margin-bottom: 20px;
padding: 20px;
background: linear-gradient(135deg, #AA20FF 0%, #8A1ACC 100%);
color: white;
border-radius: 8px;
}
.response-stats {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.stat-card {
flex: 1;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
border-left: 4px solid #AA20FF;
}
.stat-number {
font-size: 2em;
font-weight: bold;
color: #AA20FF;
margin-bottom: 5px;
}
.stat-label {
color: #666;
font-size: 0.9em;
}
.survey-selector {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.survey-selector select {
width: 100%;
max-width: 400px;
padding: 10px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1em;
}
.status-filter {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.status-btn {
padding: 8px 16px;
border: 1px solid #ddd;
background: white;
color: #666;
text-decoration: none;
border-radius: 20px;
font-size: 0.9em;
transition: all 0.3s;
}
.status-btn:hover,
.status-btn.active {
background: #AA20FF;
color: white;
border-color: #AA20FF;
}
.response-table {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.response-table table {
width: 100%;
border-collapse: collapse;
}
.response-table th {
background: #fff;
padding: 15px 10px;
text-align: left;
font-weight: 600;
border-bottom: 1px solid #dee2e6;
}
.response-table td {
padding: 15px 10px;
border-bottom: 1px solid #f1f3f4;
vertical-align: middle;
}
.response-table tr:hover {
background: #fff;
}
.status-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 0.8em;
font-weight: 500;
text-transform: uppercase;
}
.status-started { background: #fff3cd; color: #856404; }
.status-completed { background: #d4edda; color: #155724; }
.status-abandoned { background: #f8d7da; color: #721c24; }
.action-buttons {
display: flex;
gap: 5px;
}
.btn-sm {
padding: 5px 10px;
font-size: 0.8em;
border-radius: 4px;
text-decoration: none;
border: none;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary { background: #007bff; color: white; }
.btn-info { background: #17a2b8; color: white; }
.btn-danger { background: #dc3545; color: white; }
.btn-sm:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.search-form {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.search-form .form-row {
display: flex;
gap: 10px;
align-items: center;
}
.search-form select,
.search-form input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
/* 반응형 */
@media (max-width: 768px) {
.response-stats {
flex-direction: column;
}
.status-filter {
justify-content: center;
}
.search-form .form-row {
flex-direction: column;
}
.response-table {
font-size: 0.9em;
}
.response-table th,
.response-table td {
padding: 10px 5px;
}
.action-buttons {
flex-direction: column;
}
}
</style>
<div class="response-header">
<div>
<h1><i class="fa fa-users"></i> 응답 관리</h1>
<p>설문 응답을 관리하고 분석할 수 있습니다</p>
</div>
</div>
<div class="response-stats">
<div class="stat-card">
<div class="stat-number"><?php echo number_format($status_counts['completed'] ?? 0); ?></div>
<div class="stat-label">완료된 응답</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo number_format($status_counts['started'] ?? 0); ?></div>
<div class="stat-label">진행중인 응답</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo number_format($status_counts['abandoned'] ?? 0); ?></div>
<div class="stat-label">중단된 응답</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo number_format($total_count); ?></div>
<div class="stat-label">전체 응답</div>
</div>
</div>
<div class="survey-selector">
<label for="survey_select"><strong>설문 선택:</strong></label>
<select id="survey_select" onchange="location.href='?sv_id='+this.value">
<option value="">전체 설문</option>
<?php foreach ($survey_list as $survey_item): ?>
<option value="<?php echo $survey_item['sv_id']; ?>" <?php echo $sv_id == $survey_item['sv_id'] ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($survey_item['sv_title']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="search-form">
<form method="get" class="form-row">
<?php if ($sv_id): ?>
<input type="hidden" name="sv_id" value="<?php echo $sv_id; ?>">
<?php endif; ?>
<select name="sfl">
<option value="sr_mb_id"<?php echo $sfl == 'sr_mb_id' ? ' selected' : ''; ?>>회원ID</option>
<option value="sr_ip"<?php echo $sfl == 'sr_ip' ? ' selected' : ''; ?>>IP주소</option>
</select>
<input type="text" name="stx" value="<?php echo $stx; ?>" placeholder="검색어를 입력하세요">
<button type="submit" class="btn-sm btn-primary">
<i class="fa fa-search"></i> 검색
</button>
<?php if ($stx): ?>
<a href="?<?php echo $sv_id ? 'sv_id='.$sv_id : ''; ?>" class="btn-sm btn-warning">
<i class="fa fa-times"></i> 초기화
</a>
<?php endif; ?>
</form>
</div>
<div class="status-filter">
<a href="?<?php echo $sv_id ? 'sv_id='.$sv_id : ''; ?>" class="status-btn <?php echo !isset($_GET['status']) ? 'active' : ''; ?>">
<i class="fa fa-list"></i> 전체
</a>
<a href="?<?php echo $sv_id ? 'sv_id='.$sv_id.'&' : ''; ?>status=completed" class="status-btn <?php echo $status == 'completed' ? 'active' : ''; ?>">
<i class="fa fa-check"></i> 완료 (<?php echo $status_counts['completed'] ?? 0; ?>)
</a>
<a href="?<?php echo $sv_id ? 'sv_id='.$sv_id.'&' : ''; ?>status=started" class="status-btn <?php echo $status == 'started' ? 'active' : ''; ?>">
<i class="fa fa-play"></i> 진행중 (<?php echo $status_counts['started'] ?? 0; ?>)
</a>
<a href="?<?php echo $sv_id ? 'sv_id='.$sv_id.'&' : ''; ?>status=abandoned" class="status-btn <?php echo $status == 'abandoned' ? 'active' : ''; ?>">
<i class="fa fa-times"></i> 중단 (<?php echo $status_counts['abandoned'] ?? 0; ?>)
</a>
</div>
<div class="response-table">
<table>
<thead>
<tr>
<th width="60">ID</th>
<?php if (!$sv_id): ?>
<th>설문 제목</th>
<?php endif; ?>
<th width="100">응답자</th>
<th width="120">IP주소</th>
<th width="80">상태</th>
<th width="120">시작시간</th>
<th width="120">완료시간</th>
<th width="150">관리</th>
</tr>
</thead>
<tbody>
<?php
if (sql_num_rows($result) > 0) {
while ($row = sql_fetch_array($result)) {
// 상태별 클래스
$status_class = 'status-' . $row['sr_status'];
$status_text = array(
'started' => '진행중',
'completed' => '완료',
'abandoned' => '중단'
);
?>
<tr>
<td><?php echo $row['sr_id']; ?></td>
<?php if (!$sv_id): ?>
<td>
<strong><?php echo htmlspecialchars($row['sv_title']); ?></strong>
</td>
<?php endif; ?>
<td><?php echo $row['sr_mb_id'] ?: '익명'; ?></td>
<td><?php echo $row['sr_ip']; ?></td>
<td>
<span class="status-badge <?php echo $status_class; ?>">
<?php echo $status_text[$row['sr_status']]; ?>
</span>
</td>
<td>
<small><?php echo date('Y-m-d H:i', strtotime($row['sr_started_at'])); ?></small>
</td>
<td>
<?php if ($row['sr_completed_at']): ?>
<small><?php echo date('Y-m-d H:i', strtotime($row['sr_completed_at'])); ?></small>
<?php else: ?>
<span style="color: #999;">-</span>
<?php endif; ?>
</td>
<td>
<div class="action-buttons">
<?php if ($row['sr_status'] == 'completed'): ?>
<a href="response_view.php?sr_id=<?php echo $row['sr_id']; ?>" class="btn-sm btn-primary" title="응답 보기">
<i class="fa fa-eye"></i>
</a>
<a href="statistics.php?sv_id=<?php echo $row['sv_id']; ?>" class="btn-sm btn-info" title="통계">
<i class="fa fa-chart-bar"></i>
</a>
<?php endif; ?>
<a href="response_delete.php?sr_id=<?php echo $row['sr_id']; ?>" class="btn-sm btn-danger" title="삭제" onclick="return confirm('정말 삭제하시겠습니까?')">
<i class="fa fa-trash"></i>
</a>
</div>
</td>
</tr>
<?php
}
} else {
?>
<tr>
<td colspan="<?php echo $sv_id ? '7' : '8'; ?>" style="text-align: center; padding: 50px; color: #999;">
<i class="fa fa-inbox" style="font-size: 3em; margin-bottom: 20px; display: block;"></i>
등록된 응답이 없습니다.
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php
// 페이징
if ($total_page > 1) {
$paging = get_paging(10, $page, $total_page, "?$sql_search&page=");
echo '<div style="margin-top: 20px;">' . $paging . '</div>';
}
?>
<?php
include_once(G5_ADMIN_PATH.'/admin.tail.php');
?>
+413
View File
@@ -0,0 +1,413 @@
<?php
$sub_menu = '710310';
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "r");
$sr_id = isset($_GET['sr_id']) ? (int)$_GET['sr_id'] : 0;
if (!$sr_id) {
alert('응답을 선택해주세요.', 'response_list.php');
}
// 응답 정보 가져오기
$response = sql_fetch("
SELECT sr.*, sm.sv_title, sm.sv_description
FROM survey_responses sr
LEFT JOIN survey_master sm ON sr.sv_id = sm.sv_id
WHERE sr.sr_id = '$sr_id'
");
if (!$response) {
alert('존재하지 않는 응답입니다.', 'response_list.php');
}
$survey = get_survey($response['sv_id']);
$questions = get_survey_questions($response['sv_id']);
// 답변 데이터 가져오기
$answers = array();
$answer_sql = "SELECT * FROM survey_answers WHERE sr_id = '$sr_id' ORDER BY sq_id";
$answer_result = sql_query($answer_sql);
while ($answer_row = sql_fetch_array($answer_result)) {
if (!isset($answers[$answer_row['sq_id']])) {
$answers[$answer_row['sq_id']] = array();
}
$answers[$answer_row['sq_id']][] = $answer_row;
}
$g5['title'] = '응답 상세보기 - ' . $response['sv_title'];
include_once(G5_ADMIN_PATH.'/admin.head.php');
// 응답 시간 계산
$response_time = 0;
if ($response['sr_completed_at'] && $response['sr_started_at']) {
$start = strtotime($response['sr_started_at']);
$end = strtotime($response['sr_completed_at']);
$response_time = round(($end - $start) / 60, 1); // 분 단위
}
?>
<style>
.response-view-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.response-header {
background: linear-gradient(135deg, #AA20FF 0%, #8A1ACC 100%);
color: white;
padding: 30px;
border-radius: 15px;
margin-bottom: 30px;
text-align: center;
}
.response-header h1 {
margin: 0 0 10px 0;
font-size: 2em;
}
.response-info {
background: white;
border-radius: 12px;
padding: 25px;
margin-bottom: 30px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.info-item {
text-align: center;
padding: 15px;
background: #fff;
border-radius: 8px;
}
.info-label {
font-size: 0.9em;
color: #666;
margin-bottom: 5px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.info-value {
font-size: 1.2em;
font-weight: 600;
color: #333;
}
.status-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 0.8em;
font-weight: 500;
text-transform: uppercase;
}
.status-completed { background: #d4edda; color: #155724; }
.status-started { background: #fff3cd; color: #856404; }
.status-abandoned { background: #f8d7da; color: #721c24; }
.answers-container {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.answer-item {
margin-bottom: 30px;
padding-bottom: 25px;
border-bottom: 1px solid #f0f0f0;
}
.answer-item:last-child {
border-bottom: none;
margin-bottom: 0;
}
.question-header {
margin-bottom: 15px;
}
.question-number {
display: inline-block;
width: 25px;
height: 25px;
background: #AA20FF;
color: white;
border-radius: 50%;
text-align: center;
line-height: 25px;
font-weight: bold;
font-size: 0.8em;
margin-right: 10px;
}
.question-title {
font-size: 1.1em;
font-weight: 600;
color: #333;
display: inline;
}
.question-type {
display: inline-block;
padding: 2px 8px;
background: #e9ecef;
color: #666;
border-radius: 10px;
font-size: 0.7em;
font-weight: 500;
text-transform: uppercase;
margin-left: 10px;
}
.answer-content {
margin-left: 35px;
padding: 15px 20px;
background: #fff;
border-radius: 8px;
border-left: 4px solid #AA20FF;
}
.answer-value {
color: #333;
line-height: 1.5;
font-weight: 500;
}
.answer-text {
color: #666;
font-style: italic;
margin-top: 5px;
}
.multiple-answers {
display: flex;
flex-direction: column;
gap: 8px;
}
.answer-chip {
display: inline-block;
padding: 5px 12px;
background: #AA20FF;
color: white;
border-radius: 15px;
font-size: 0.9em;
margin-right: 8px;
margin-bottom: 5px;
}
.no-answer {
color: #999;
font-style: italic;
}
.action-buttons {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #f0f0f0;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: #AA20FF;
color: white;
}
.btn-primary:hover {
background: #8A1ACC;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #545b62;
color: white;
}
.btn-info {
background: #17a2b8;
color: white;
}
.btn-info:hover {
background: #138496;
color: white;
}
/* 반응형 */
@media (max-width: 768px) {
.response-view-container {
padding: 10px;
}
.info-grid {
grid-template-columns: 1fr;
}
.action-buttons {
flex-direction: column;
align-items: center;
}
.btn {
width: 100%;
max-width: 250px;
justify-content: center;
}
.answer-content {
margin-left: 0;
}
}
</style>
<div class="response-view-container">
<div class="response-header">
<h1><i class="fa fa-eye"></i> 응답 상세보기</h1>
<p><?php echo htmlspecialchars($response['sv_title']); ?></p>
</div>
<div class="response-info">
<div class="info-grid">
<div class="info-item">
<div class="info-label">응답 ID</div>
<div class="info-value"><?php echo $response['sr_id']; ?></div>
</div>
<div class="info-item">
<div class="info-label">응답자</div>
<div class="info-value"><?php echo $response['sr_mb_id'] ?: '익명'; ?></div>
</div>
<div class="info-item">
<div class="info-label">IP 주소</div>
<div class="info-value"><?php echo $response['sr_ip']; ?></div>
</div>
<div class="info-item">
<div class="info-label">상태</div>
<div class="info-value">
<span class="status-badge status-<?php echo $response['sr_status']; ?>">
<?php
$status_text = array(
'started' => '진행중',
'completed' => '완료',
'abandoned' => '중단'
);
echo $status_text[$response['sr_status']];
?>
</span>
</div>
</div>
<div class="info-item">
<div class="info-label">시작 시간</div>
<div class="info-value">
<small><?php echo date('Y-m-d H:i:s', strtotime($response['sr_started_at'])); ?></small>
</div>
</div>
<div class="info-item">
<div class="info-label">완료 시간</div>
<div class="info-value">
<?php if ($response['sr_completed_at']): ?>
<small><?php echo date('Y-m-d H:i:s', strtotime($response['sr_completed_at'])); ?></small>
<?php else: ?>
<span class="no-answer">미완료</span>
<?php endif; ?>
</div>
</div>
<?php if ($response_time > 0): ?>
<div class="info-item">
<div class="info-label">응답 시간</div>
<div class="info-value"><?php echo $response_time; ?>분</div>
</div>
<?php endif; ?>
</div>
</div>
<div class="answers-container">
<h2><i class="fa fa-list-alt"></i> 응답 내용</h2>
<?php foreach ($questions as $index => $question): ?>
<div class="answer-item">
<div class="question-header">
<span class="question-number"><?php echo $index + 1; ?></span>
<h3 class="question-title"><?php echo htmlspecialchars($question['sq_title']); ?></h3>
<span class="question-type"><?php echo $question['sq_type']; ?></span>
</div>
<div class="answer-content">
<?php if (isset($answers[$question['sq_id']]) && !empty($answers[$question['sq_id']])): ?>
<?php if (count($answers[$question['sq_id']]) > 1): ?>
<!-- 다중 답변 (체크박스) -->
<div class="multiple-answers">
<?php foreach ($answers[$question['sq_id']] as $answer): ?>
<span class="answer-chip"><?php echo htmlspecialchars($answer['sa_value']); ?></span>
<?php if ($answer['sa_text']): ?>
<div class="answer-text"><?php echo htmlspecialchars($answer['sa_text']); ?></div>
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php else: ?>
<!-- 단일 답변 -->
<div class="answer-value">
<?php echo nl2br(htmlspecialchars($answers[$question['sq_id']][0]['sa_value'])); ?>
</div>
<?php if ($answers[$question['sq_id']][0]['sa_text']): ?>
<div class="answer-text">
<?php echo nl2br(htmlspecialchars($answers[$question['sq_id']][0]['sa_text'])); ?>
</div>
<?php endif; ?>
<?php endif; ?>
<?php else: ?>
<div class="no-answer">답변 없음</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="action-buttons">
<a href="response_list.php?sv_id=<?php echo $response['sv_id']; ?>" class="btn btn-secondary">
<i class="fa fa-arrow-left"></i> 목록으로
</a>
<a href="statistics.php?sv_id=<?php echo $response['sv_id']; ?>" class="btn btn-info">
<i class="fa fa-chart-bar"></i> 통계 보기
</a>
<a href="export.php?sv_id=<?php echo $response['sv_id']; ?>&format=excel" class="btn btn-primary">
<i class="fa fa-file-excel"></i> 엑셀 다운로드
</a>
</div>
</div>
<?php
include_once(G5_ADMIN_PATH.'/admin.tail.php');
?>
+276
View File
@@ -0,0 +1,276 @@
<?php
$sub_menu = '710304';
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "r");
$sv_id = isset($_GET['sv_id']) ? (int)$_GET['sv_id'] : 0;
if (!$sv_id) {
alert('설문을 선택해주세요.', 'survey_list.php');
}
$survey = get_survey($sv_id);
if (!$survey) {
alert('존재하지 않는 설문입니다.', 'survey_list.php');
}
$questions = get_survey_questions($sv_id);
$total_responses = get_survey_response_count($sv_id, 'completed');
// 통계 업데이트
update_survey_statistics($sv_id);
$g5['title'] = '설문 통계 - ' . $survey['sv_title'];
include_once(G5_ADMIN_PATH.'/admin.head.php');
// 응답 현황 통계
$response_stats = sql_fetch("
SELECT
COUNT(CASE WHEN sr_status = 'completed' THEN 1 END) as completed,
COUNT(CASE WHEN sr_status = 'started' THEN 1 END) as started,
COUNT(CASE WHEN sr_status = 'abandoned' THEN 1 END) as abandoned,
COUNT(*) as total
FROM survey_responses
WHERE sv_id = '$sv_id'
");
// 일별 응답 통계 (최근 30일)
$daily_stats = [];
$daily_sql = "
SELECT
DATE(sr_completed_at) as date,
COUNT(*) as count
FROM survey_responses
WHERE sv_id = '$sv_id'
AND sr_status = 'completed'
AND sr_completed_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY DATE(sr_completed_at)
ORDER BY date ASC
";
$daily_result = sql_query($daily_sql);
while ($row = sql_fetch_array($daily_result)) {
$daily_stats[$row['date']] = $row['count'];
}
// 시간대별 응답 통계
$hourly_stats = [];
$hourly_sql = "
SELECT
HOUR(sr_completed_at) as hour,
COUNT(*) as count
FROM survey_responses
WHERE sv_id = '$sv_id'
AND sr_status = 'completed'
GROUP BY HOUR(sr_completed_at)
ORDER BY hour ASC
";
$hourly_result = sql_query($hourly_sql);
while ($row = sql_fetch_array($hourly_result)) {
$hourly_stats[$row['hour']] = $row['count'];
}
// 완료율 계산
$completion_rate = $response_stats['total'] > 0 ?
round(($response_stats['completed'] / $response_stats['total']) * 100, 1) : 0;
// 평균 응답 시간 계산
$avg_time_sql = "
SELECT AVG(TIMESTAMPDIFF(MINUTE, sr_started_at, sr_completed_at)) as avg_minutes
FROM survey_responses
WHERE sv_id = '$sv_id'
AND sr_status = 'completed'
AND sr_completed_at IS NOT NULL
";
$avg_time_result = sql_fetch($avg_time_sql);
$avg_response_time = round($avg_time_result['avg_minutes'] ?? 0, 1);
?>
<!-- CSS는 statistics.css 파일에서 자동 로드됩니다 -->
<div class="statistics-container">
<!-- 통계 헤더 -->
<div class="stats-header">
<h1><i class="fa fa-chart-bar"></i> 설문 통계</h1>
<p><?php echo htmlspecialchars($survey['sv_title']); ?></p>
</div>
<!-- 통계 개요 -->
<div class="stats-overview">
<div class="stat-card">
<div class="stat-number"><?php echo number_format($total_responses); ?></div>
<div class="stat-label">완료된 응답</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo $completion_rate; ?>%</div>
<div class="stat-label">완료율</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo $avg_response_time; ?>분</div>
<div class="stat-label">평균 응답시간</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo number_format($response_stats['started']); ?></div>
<div class="stat-label">진행중인 응답</div>
</div>
</div>
<!-- 응답 현황 차트 -->
<div class="chart-section">
<div class="chart-header">
<h3 class="chart-title">응답 현황</h3>
<div class="chart-controls">
<button class="chart-btn active" data-chart="daily">일별</button>
<button class="chart-btn" data-chart="hourly">시간대별</button>
</div>
</div>
<div class="chart-container">
<canvas id="responseChart"></canvas>
</div>
</div>
<!-- 질문별 통계 -->
<div class="questions-stats">
<?php foreach ($questions as $index => $question): ?>
<div class="question-stat">
<div class="question-header">
<span class="question-number"><?php echo $index + 1; ?></span>
<h4 class="question-title"><?php echo htmlspecialchars($question['sq_title']); ?></h4>
<span class="question-type"><?php echo $question['sq_type']; ?></span>
</div>
<?php
// 질문별 통계 데이터 가져오기
$question_stats_sql = "
SELECT ss_option_value, ss_count, ss_percentage
FROM survey_statistics
WHERE sv_id = '$sv_id' AND sq_id = '{$question['sq_id']}'
ORDER BY ss_count DESC
";
$question_stats_result = sql_query($question_stats_sql);
$has_stats = sql_num_rows($question_stats_result) > 0;
?>
<?php if ($has_stats): ?>
<div class="question-stats-grid">
<div class="answer-stats">
<?php while ($stat = sql_fetch_array($question_stats_result)): ?>
<div class="answer-item">
<div class="answer-label"><?php echo htmlspecialchars($stat['ss_option_value']); ?></div>
<div class="answer-bar">
<div class="answer-fill" data-width="<?php echo $stat['ss_percentage']; ?>"></div>
</div>
<div class="answer-count"><?php echo number_format($stat['ss_count']); ?>명</div>
<div class="answer-percentage"><?php echo $stat['ss_percentage']; ?>%</div>
</div>
<?php endwhile; ?>
</div>
<div class="question-chart">
<canvas id="questionChart<?php echo $question['sq_id']; ?>"></canvas>
</div>
</div>
<?php elseif (in_array($question['sq_type'], ['text', 'textarea'])): ?>
<!-- 텍스트 응답 표시 -->
<div class="text-responses">
<?php
$text_responses_sql = "
SELECT sa.sa_value, sr.sr_completed_at, sr.sr_mb_id
FROM survey_answers sa
JOIN survey_responses sr ON sa.sr_id = sr.sr_id
WHERE sa.sq_id = '{$question['sq_id']}'
AND sr.sr_status = 'completed'
AND sa.sa_value != ''
ORDER BY sr.sr_completed_at DESC
LIMIT 20
";
$text_responses_result = sql_query($text_responses_sql);
if (sql_num_rows($text_responses_result) > 0):
while ($text_response = sql_fetch_array($text_responses_result)):
?>
<div class="text-response">
<div class="text-response-meta">
<?php echo $text_response['sr_mb_id'] ?: '익명'; ?> •
<?php echo date('Y-m-d H:i', strtotime($text_response['sr_completed_at'])); ?>
</div>
<div class="text-response-content">
<?php echo nl2br(htmlspecialchars($text_response['sa_value'])); ?>
</div>
</div>
<?php
endwhile;
else:
?>
<div class="text-response">
<div class="text-response-content no-responses">
아직 응답이 없습니다.
</div>
</div>
<?php endif; ?>
</div>
<?php else: ?>
<div class="no-data">
<i class="fa fa-chart-bar"></i>
<p>아직 응답 데이터가 없습니다.</p>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<!-- 내보내기 섹션 -->
<div class="export-section">
<h3><i class="fa fa-download"></i> 데이터 내보내기</h3>
<p>설문 결과를 다양한 형식으로 내보낼 수 있습니다.</p>
<div class="export-buttons">
<a href="export.php?sv_id=<?php echo $sv_id; ?>&format=excel" class="export-btn">
<i class="fa fa-file-excel"></i> 엑셀 다운로드
</a>
<a href="export.php?sv_id=<?php echo $sv_id; ?>&format=csv" class="export-btn secondary">
<i class="fa fa-file-csv"></i> CSV 다운로드
</a>
<button onclick="printStatistics()" class="export-btn secondary">
<i class="fa fa-print"></i> 인쇄하기
</button>
</div>
</div>
</div>
<!-- Chart.js 라이브러리 -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- JavaScript는 statistics.js 파일에서 자동 로드됩니다 -->
<script>
// 페이지별 초기 데이터 설정
// 💡 [수정] JavaScript에서 사용하는 데이터 구조(survey, stats, chartData)에 맞게 객체를 재구성합니다.
// 이전에 발생했던 'daily' 또는 'title' 속성을 찾을 수 없다는 오류를 해결합니다.
window.statisticsData = {
survey: {
id: <?php echo $sv_id; ?>,
title: '<?php echo addslashes(htmlspecialchars($survey['sv_title'])); ?>'
},
stats: {
totalResponses: '<?php echo number_format($total_responses); ?>',
completionRate: '<?php echo $completion_rate; ?>',
avgResponseTime: '<?php echo $avg_response_time; ?>',
startedResponses: '<?php echo number_format($response_stats['started']); ?>'
},
chartData: {
daily: <?php echo json_encode($daily_stats); ?>,
hourly: <?php echo json_encode($hourly_stats); ?>
},
questions: <?php echo json_encode($questions); ?>
};
</script>
<?php
include_once(G5_ADMIN_PATH.'/admin.tail.php');
?>
+292
View File
@@ -0,0 +1,292 @@
<?php
$sub_menu = '710500';
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "r");
$g5['title'] = '통계 분석';
include_once(G5_ADMIN_PATH.'/admin.head.php');
// 통계 분석 가능한 설문 목록 (완료된 응답이 있는 설문들)
$sql = "SELECT sm.*,
COUNT(sr.sr_id) as total_responses,
COUNT(CASE WHEN sr.sr_status = 'completed' THEN 1 END) as completed_responses
FROM survey_master sm
LEFT JOIN survey_responses sr ON sm.sv_id = sr.sv_id
WHERE sm.sv_status != 'deleted'
GROUP BY sm.sv_id
HAVING completed_responses > 0
ORDER BY sm.sv_created_at DESC";
$result = sql_query($sql);
?>
<style>
.statistics-header {
display: flex;
justify-content: between;
align-items: center;
margin-bottom: 20px;
padding: 20px;
background: linear-gradient(135deg, #AA20FF 0%, #8A1ACC 100%);
color: white;
border-radius: 8px;
}
.survey-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.survey-stats-card {
background: white;
border-radius: 12px;
padding: 25px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transition: all 0.3s ease;
border-left: 4px solid #AA20FF;
}
.survey-stats-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
}
.survey-card-header {
margin-bottom: 20px;
}
.survey-title {
font-size: 1.3em;
font-weight: 600;
color: #333;
margin-bottom: 8px;
line-height: 1.3;
}
.survey-meta {
display: flex;
gap: 15px;
font-size: 0.9em;
color: #666;
margin-bottom: 15px;
}
.survey-stats-info {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
margin-bottom: 20px;
}
.stat-item {
text-align: center;
padding: 15px;
background: #fff;
border-radius: 8px;
}
.stat-number {
font-size: 1.8em;
font-weight: 700;
color: #AA20FF;
margin-bottom: 5px;
}
.stat-label {
font-size: 0.8em;
color: #666;
font-weight: 500;
text-transform: uppercase;
}
.survey-actions {
display: flex;
gap: 10px;
justify-content: center;
}
.btn-stats {
flex: 1;
padding: 10px 15px;
background: #AA20FF;
color: white;
text-decoration: none;
border-radius: 6px;
text-align: center;
font-weight: 500;
transition: all 0.3s ease;
border: none;
cursor: pointer;
}
.btn-stats:hover {
background: #8A1ACC;
color: white;
transform: translateY(-1px);
}
.btn-export {
background: #28a745;
}
.btn-export:hover {
background: #218838;
}
.empty-state {
text-align: center;
padding: 80px 20px;
color: #666;
}
.empty-state i {
font-size: 4em;
margin-bottom: 20px;
opacity: 0.3;
}
.completion-rate {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
}
.progress-bar {
flex: 1;
height: 6px;
background: #e0e0e0;
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #AA20FF 0%, #8A1ACC 100%);
border-radius: 3px;
transition: width 0.5s ease;
}
.progress-text {
font-size: 0.9em;
font-weight: 600;
color: #AA20FF;
min-width: 40px;
}
/* 반응형 */
@media (max-width: 768px) {
.survey-stats-grid {
grid-template-columns: 1fr;
}
.survey-stats-info {
grid-template-columns: 1fr;
}
.survey-actions {
flex-direction: column;
}
.survey-meta {
flex-direction: column;
gap: 5px;
}
}
</style>
<div class="statistics-header">
<div>
<h1><i class="fa fa-chart-bar"></i> 통계 분석</h1>
<p>설문별 응답 현황과 통계를 확인할 수 있습니다</p>
</div>
</div>
<?php if (sql_num_rows($result) > 0): ?>
<div class="survey-stats-grid">
<?php while ($survey = sql_fetch_array($result)): ?>
<?php
$completion_rate = $survey['total_responses'] > 0 ?
round(($survey['completed_responses'] / $survey['total_responses']) * 100, 1) : 0;
// 평균 응답 시간 계산
$avg_time_sql = "
SELECT AVG(TIMESTAMPDIFF(MINUTE, sr_started_at, sr_completed_at)) as avg_minutes
FROM survey_responses
WHERE sv_id = '{$survey['sv_id']}'
AND sr_status = 'completed'
AND sr_completed_at IS NOT NULL
";
$avg_time_result = sql_fetch($avg_time_sql);
$avg_response_time = round($avg_time_result['avg_minutes'] ?? 0, 1);
?>
<div class="survey-stats-card">
<div class="survey-card-header">
<h3 class="survey-title"><?php echo htmlspecialchars($survey['sv_title']); ?></h3>
<div class="survey-meta">
<span><i class="fa fa-user"></i> <?php echo $survey['sv_created_by']; ?></span>
<span><i class="fa fa-calendar"></i> <?php echo date('Y-m-d', strtotime($survey['sv_created_at'])); ?></span>
<span class="status-badge status-<?php echo $survey['sv_status']; ?>">
<?php
$status_text = [
'draft' => '작성중',
'active' => '진행중',
'closed' => '종료',
'deleted' => '삭제됨'
];
echo $status_text[$survey['sv_status']];
?>
</span>
</div>
</div>
<div class="survey-stats-info">
<div class="stat-item">
<div class="stat-number"><?php echo number_format($survey['completed_responses']); ?></div>
<div class="stat-label">완료된 응답</div>
</div>
<div class="stat-item">
<div class="stat-number"><?php echo $avg_response_time; ?>분</div>
<div class="stat-label">평균 응답시간</div>
</div>
</div>
<div class="completion-rate">
<div class="progress-bar">
<div class="progress-fill" style="width: <?php echo $completion_rate; ?>%"></div>
</div>
<div class="progress-text"><?php echo $completion_rate; ?>%</div>
</div>
<div class="survey-actions">
<a href="statistics.php?sv_id=<?php echo $survey['sv_id']; ?>" class="btn-stats">
<i class="fa fa-chart-bar"></i> 통계 보기
</a>
<a href="export.php?sv_id=<?php echo $survey['sv_id']; ?>" class="btn-stats btn-export">
<i class="fa fa-file-excel"></i> 내보내기
</a>
</div>
</div>
<?php endwhile; ?>
</div>
<?php else: ?>
<div class="empty-state">
<i class="fa fa-chart-bar"></i>
<h3>통계 분석 가능한 설문이 없습니다</h3>
<p>완료된 응답이 있는 설문만 통계 분석이 가능합니다.</p>
<a href="survey_list.php" style="color: #AA20FF; margin-top: 15px; display: inline-block;">
설문 목록으로 이동
</a>
</div>
<?php endif; ?>
<?php
include_once(G5_ADMIN_PATH.'/admin.tail.php');
?>
</content>
</invoke>
+998
View File
@@ -0,0 +1,998 @@
<?php
$sub_menu = '710304';
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "r");
$sv_id = isset($_GET['sv_id']) ? (int) $_GET['sv_id'] : 0;
if (!$sv_id) {
alert('설문을 선택해주세요.', 'survey_list.php');
}
$survey = get_survey($sv_id);
if (!$survey) {
alert('존재하지 않는 설문입니다.', 'survey_list.php');
}
$questions = get_survey_questions($sv_id);
$total_responses = get_survey_response_count($sv_id, 'completed');
// 통계 업데이트
update_survey_statistics($sv_id);
$g5['title'] = '설문 통계 - ' . $survey['sv_title'];
include_once(G5_ADMIN_PATH . '/admin.head.php');
// 응답 현황 통계
$response_stats = sql_fetch("
SELECT
COUNT(CASE WHEN sr_status = 'completed' THEN 1 END) as completed,
COUNT(CASE WHEN sr_status = 'started' THEN 1 END) as started,
COUNT(CASE WHEN sr_status = 'abandoned' THEN 1 END) as abandoned,
COUNT(*) as total
FROM survey_responses
WHERE sv_id = '$sv_id'
");
// 일별 응답 통계 (최근 30일)
$daily_stats = array();
$daily_sql = "
SELECT
DATE(sr_completed_at) as date,
COUNT(*) as count
FROM survey_responses
WHERE sv_id = '$sv_id'
AND sr_status = 'completed'
AND sr_completed_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY DATE(sr_completed_at)
ORDER BY date ASC
";
$daily_result = sql_query($daily_sql);
while ($row = sql_fetch_array($daily_result)) {
$daily_stats[$row['date']] = $row['count'];
}
// 시간대별 응답 통계
$hourly_stats = array();
$hourly_sql = "
SELECT
HOUR(sr_completed_at) as hour,
COUNT(*) as count
FROM survey_responses
WHERE sv_id = '$sv_id'
AND sr_status = 'completed'
GROUP BY HOUR(sr_completed_at)
ORDER BY hour ASC
";
$hourly_result = sql_query($hourly_sql);
while ($row = sql_fetch_array($hourly_result)) {
$hourly_stats[$row['hour']] = $row['count'];
}
// 완료율 계산
$completion_rate = $response_stats['total'] > 0 ?
round(($response_stats['completed'] / $response_stats['total']) * 100, 1) : 0;
// 평균 응답 시간 계산
$avg_time_sql = "
SELECT AVG(TIMESTAMPDIFF(MINUTE, sr_started_at, sr_completed_at)) as avg_minutes
FROM survey_responses
WHERE sv_id = '$sv_id'
AND sr_status = 'completed'
AND sr_completed_at IS NOT NULL
";
$avg_time_result = sql_fetch($avg_time_sql);
$avg_response_time = round($avg_time_result['avg_minutes'] ?? 0, 1);
?>
<style>
.statistics-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.stats-header {
background: linear-gradient(135deg, #AA20FF 0%, #8A1ACC 100%);
color: white;
padding: 30px;
border-radius: 15px;
margin-bottom: 30px;
text-align: center;
}
.stats-header h1 {
margin: 0 0 10px 0;
font-size: 2.2em;
}
.stats-header p {
margin: 0;
opacity: 0.9;
}
.stats-overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
text-align: center;
border-left: 4px solid #AA20FF;
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.stat-number {
font-size: 2.5em;
font-weight: 700;
color: #AA20FF;
margin-bottom: 8px;
line-height: 1;
}
.stat-label {
color: #666;
font-size: 0.9em;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-change {
font-size: 0.8em;
margin-top: 5px;
}
.stat-change.positive {
color: #28a745;
}
.stat-change.negative {
color: #dc3545;
}
.chart-section {
background: white;
border-radius: 15px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.chart-header {
display: flex;
justify-content: between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e9ecef;
}
.chart-title {
font-size: 1.4em;
font-weight: 600;
color: #333;
margin: 0;
}
.chart-controls {
display: flex;
gap: 10px;
}
.chart-btn {
padding: 6px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 5px;
cursor: pointer;
font-size: 0.8em;
transition: all 0.3s ease;
}
.chart-btn.active,
.chart-btn:hover {
background: #AA20FF;
color: white;
border-color: #AA20FF;
}
.chart-container {
position: relative;
height: 400px;
margin-bottom: 20px;
}
.questions-stats {
display: grid;
gap: 30px;
}
.question-stat {
background: white;
border-radius: 15px;
padding: 30px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.question-header {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e9ecef;
}
.question-number {
display: inline-block;
width: 30px;
height: 30px;
background: #AA20FF;
color: white;
border-radius: 50%;
text-align: center;
line-height: 30px;
font-weight: bold;
font-size: 0.9em;
margin-right: 15px;
}
.question-title {
font-size: 1.2em;
font-weight: 600;
color: #333;
display: inline;
}
.question-type {
display: inline-block;
padding: 3px 8px;
background: #e9ecef;
color: #666;
border-radius: 12px;
font-size: 0.7em;
font-weight: 500;
text-transform: uppercase;
margin-left: 10px;
}
.question-stats-grid {
display: grid;
grid-template-columns: 1fr 300px;
gap: 30px;
align-items: start;
}
.answer-stats {
display: flex;
flex-direction: column;
gap: 15px;
}
.answer-item {
display: flex;
align-items: center;
gap: 15px;
padding: 15px;
background: #fff;
border-radius: 10px;
transition: all 0.3s ease;
}
.answer-item:hover {
background: #e9ecef;
}
.answer-bar {
flex: 1;
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
position: relative;
}
.answer-fill {
height: 100%;
background: linear-gradient(90deg, #AA20FF 0%, #8A1ACC 100%);
border-radius: 4px;
transition: width 0.5s ease;
}
.answer-label {
min-width: 150px;
font-weight: 500;
color: #333;
}
.answer-count {
min-width: 80px;
text-align: right;
font-weight: 600;
color: #666;
}
.answer-percentage {
min-width: 50px;
text-align: right;
font-size: 0.9em;
color: #AA20FF;
font-weight: 600;
}
.question-chart {
height: 250px;
}
.text-responses {
max-height: 300px;
overflow-y: auto;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
.text-response {
padding: 15px;
border-bottom: 1px solid #f0f0f0;
line-height: 1.5;
}
.text-response:last-child {
border-bottom: none;
}
.text-response-meta {
font-size: 0.8em;
color: #666;
margin-bottom: 8px;
}
.text-response-content {
color: #333;
}
.export-section {
background: white;
border-radius: 15px;
padding: 30px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
text-align: center;
}
.export-buttons {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.export-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
background: #AA20FF;
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: all 0.3s ease;
border: none;
cursor: pointer;
}
.export-btn:hover {
background: #8A1ACC;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(170, 32, 255, 0.3);
color: white;
}
.export-btn.secondary {
background: #6c757d;
}
.export-btn.secondary:hover {
background: #545b62;
}
/* 반응형 */
@media (max-width: 768px) {
.statistics-container {
padding: 10px;
}
.stats-overview {
grid-template-columns: 1fr;
}
.question-stats-grid {
grid-template-columns: 1fr;
}
.chart-header {
flex-direction: column;
gap: 15px;
align-items: flex-start;
}
.export-buttons {
flex-direction: column;
align-items: center;
}
.export-btn {
width: 100%;
max-width: 250px;
justify-content: center;
}
.answer-item {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.answer-label,
.answer-count,
.answer-percentage {
min-width: auto;
text-align: left;
}
}
/* 애니메이션 */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.stat-card,
.chart-section,
.question-stat {
animation: fadeInUp 0.6s ease forwards;
}
.stat-card:nth-child(even) {
animation-delay: 0.1s;
}
.question-stat:nth-child(even) {
animation-delay: 0.2s;
}
</style>
<div class="statistics-container">
<!-- 통계 헤더 -->
<div class="stats-header">
<h1><i class="fa fa-chart-bar"></i> 설문 통계</h1>
<p><?php echo htmlspecialchars($survey['sv_title']); ?></p>
</div>
<!-- 통계 개요 -->
<div class="stats-overview">
<div class="stat-card">
<div class="stat-number"><?php echo number_format($total_responses); ?></div>
<div class="stat-label">완료된 응답</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo $completion_rate; ?>%</div>
<div class="stat-label">완료율</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo $avg_response_time; ?>분</div>
<div class="stat-label">평균 응답시간</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo number_format($response_stats['started']); ?></div>
<div class="stat-label">진행중인 응답</div>
</div>
</div>
<!-- 응답 현황 차트 -->
<div class="chart-section">
<div class="chart-header">
<h3 class="chart-title">응답 현황</h3>
<div class="chart-controls">
<button class="chart-btn active" data-chart="daily">일별</button>
<button class="chart-btn" data-chart="hourly">시간대별</button>
</div>
</div>
<div class="chart-container">
<canvas id="responseChart"></canvas>
</div>
</div>
<!-- 질문별 통계 -->
<div class="questions-stats">
<?php foreach ($questions as $index => $question): ?>
<div class="question-stat">
<div class="question-header">
<span class="question-number"><?php echo $index + 1; ?></span>
<h4 class="question-title"><?php echo htmlspecialchars($question['sq_title']); ?></h4>
<span class="question-type"><?php echo $question['sq_type']; ?></span>
</div>
<?php
// 질문별 통계 데이터 가져오기
$question_stats_sql = "
SELECT ss_option_value, ss_count, ss_percentage
FROM survey_statistics
WHERE sv_id = '$sv_id' AND sq_id = '{$question['sq_id']}'
ORDER BY ss_count DESC
";
$question_stats_result = sql_query($question_stats_sql);
$has_stats = sql_num_rows($question_stats_result) > 0;
?>
<?php if ($has_stats): ?>
<div class="question-stats-grid">
<div class="answer-stats">
<?php while ($stat = sql_fetch_array($question_stats_result)): ?>
<div class="answer-item">
<div class="answer-label"><?php echo htmlspecialchars($stat['ss_option_value']); ?></div>
<div class="answer-bar">
<div class="answer-fill" style="width: <?php echo $stat['ss_percentage']; ?>%"></div>
</div>
<div class="answer-count"><?php echo number_format($stat['ss_count']); ?>명</div>
<div class="answer-percentage"><?php echo $stat['ss_percentage']; ?>%</div>
</div>
<?php endwhile; ?>
</div>
<div class="question-chart">
<canvas id="questionChart<?php echo $question['sq_id']; ?>"></canvas>
</div>
</div>
<?php elseif (in_array($question['sq_type'], ['text', 'textarea'])): ?>
<!-- 텍스트 응답 표시 -->
<div class="text-responses">
<?php
$text_responses_sql = "
SELECT sa.sa_value, sr.sr_completed_at, sr.sr_mb_id
FROM survey_answers sa
JOIN survey_responses sr ON sa.sr_id = sr.sr_id
WHERE sa.sq_id = '{$question['sq_id']}'
AND sr.sr_status = 'completed'
AND sa.sa_value != ''
ORDER BY sr.sr_completed_at DESC
LIMIT 20
";
$text_responses_result = sql_query($text_responses_sql);
if (sql_num_rows($text_responses_result) > 0):
while ($text_response = sql_fetch_array($text_responses_result)):
?>
<div class="text-response">
<div class="text-response-meta">
<?php echo $text_response['sr_mb_id'] ?: '익명'; ?> •
<?php echo date('Y-m-d H:i', strtotime($text_response['sr_completed_at'])); ?>
</div>
<div class="text-response-content">
<?php echo nl2br(htmlspecialchars($text_response['sa_value'])); ?>
</div>
</div>
<?php
endwhile;
else:
?>
<div class="text-response">
<div class="text-response-content" style="text-align: center; color: #666;">
아직 응답이 없습니다.
</div>
</div>
<?php endif; ?>
</div>
<?php else: ?>
<div style="text-align: center; padding: 40px; color: #666;">
<i class="fa fa-chart-bar"
style="font-size: 3em; margin-bottom: 15px; display: block; opacity: 0.3;"></i>
<p>아직 응답 데이터가 없습니다.</p>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<!-- 내보내기 섹션 -->
<div class="export-section">
<h3><i class="fa fa-download"></i> 데이터 내보내기</h3>
<p>설문 결과를 다양한 형식으로 내보낼 수 있습니다.</p>
<div class="export-buttons">
<a href="export.php?sv_id=<?php echo $sv_id; ?>&format=excel" class="export-btn">
<i class="fa fa-file-excel"></i> 엑셀 다운로드
</a>
<a href="export.php?sv_id=<?php echo $sv_id; ?>&format=csv" class="export-btn secondary">
<i class="fa fa-file-csv"></i> CSV 다운로드
</a>
<button onclick="printStatistics()" class="export-btn secondary">
<i class="fa fa-print"></i> 인쇄하기
</button>
</div>
</div>
</div>
<!-- Chart.js 라이브러리 -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// 통계 데이터를 JavaScript로 전달
window.statisticsData = {
survey: {
title: '<?php echo addslashes(htmlspecialchars($survey['sv_title'])); ?>',
id: <?php echo $sv_id; ?>
},
stats: {
totalResponses: '<?php echo number_format($total_responses); ?>',
completionRate: '<?php echo $completion_rate; ?>',
avgResponseTime: '<?php echo $avg_response_time; ?>',
startedResponses: '<?php echo number_format($response_stats['started']); ?>'
},
chartData: {
daily: <?php echo json_encode($daily_stats); ?>,
hourly: <?php echo json_encode($hourly_stats); ?>
},
questions: <?php echo json_encode($questions); ?>
};
// 응답 현황 차트
let responseChart;
function initResponseChart() {
const ctx = document.getElementById('responseChart').getContext('2d');
// 일별 데이터 준비 (최근 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) return;
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 foreach ($questions as $question): ?>
<?php if (in_array($question['sq_type'], ['radio', 'checkbox', 'select', 'rating'])): ?>
{
const ctx<?php echo $question['sq_id']; ?> = document.getElementById('questionChart<?php echo $question['sq_id']; ?>');
if (ctx<?php echo $question['sq_id']; ?>) {
<?php
// 질문별 차트 데이터 준비
$chart_data = array();
$chart_labels = array();
$chart_colors = array();
$chart_sql = "
SELECT ss_option_value, ss_count
FROM survey_statistics
WHERE sv_id = '$sv_id' AND sq_id = '{$question['sq_id']}'
ORDER BY ss_count DESC
";
$chart_result = sql_query($chart_sql);
$color_palette = ['#AA20FF', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8'];
$color_index = 0;
while ($chart_row = sql_fetch_array($chart_result)) {
$chart_labels[] = $chart_row['ss_option_value'];
$chart_data[] = $chart_row['ss_count'];
$chart_colors[] = $color_palette[$color_index % count($color_palette)];
$color_index++;
}
?>
new Chart(ctx<?php echo $question['sq_id']; ?>, {
type: '<?php echo $question['sq_type'] === 'rating' ? 'bar' : 'doughnut'; ?>',
data: {
labels: <?php echo json_encode($chart_labels); ?>,
datasets: [{
data: <?php echo json_encode($chart_data); ?>,
backgroundColor: <?php echo json_encode($chart_colors); ?>,
borderWidth: 2,
borderColor: '#fff'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
padding: 15,
usePointStyle: true
}
}
}
<?php if ($question['sq_type'] === 'rating'): ?>
, scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
}
<?php endif; ?>
}
});
}
}
<?php endif; ?>
<?php endforeach; ?>
}
// 차트 타입 변경 이벤트
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 printStatistics() {
const printWindow = window.open('', '_blank');
// PHP 데이터를 JavaScript 변수로 전달
const surveyTitle = '<?php echo addslashes(htmlspecialchars($survey['sv_title'])); ?>';
const totalResponses = '<?php echo number_format($total_responses); ?>';
const completionRate = '<?php echo $completion_rate; ?>';
const avgResponseTime = '<?php echo $avg_response_time; ?>';
const startedResponses = '<?php echo number_format($response_stats['started']); ?>';
const printContent = `
<html>
<head>
<title>설문 통계 - ${surveyTitle}</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; }
.question-stat { margin-bottom: 30px; padding: 20px; border: 1px solid #ddd; }
.question-header { margin-bottom: 15px; }
.answer-stats { margin-top: 15px; }
@media print { .no-print { display: none; } }
</style>
</head>
<body>
<div class="header">
<h1>설문 통계</h1>
<h2>${surveyTitle}</h2>
<p>생성일: ${new Date().toLocaleDateString('ko-KR')}</p>
</div>
<div class="stat-grid">
<div class="stat-item">
<div class="stat-number">${totalResponses}</div>
<div>완료된 응답</div>
</div>
<div class="stat-item">
<div class="stat-number">${completionRate}%</div>
<div>완료율</div>
</div>
<div class="stat-item">
<div class="stat-number">${avgResponseTime}분</div>
<div>평균 응답시간</div>
</div>
<div class="stat-item">
<div class="stat-number">${startedResponses}</div>
<div>진행중인 응답</div>
</div>
</div>
<div class="questions-section">
${getQuestionsForPrint()}
</div>
</body>
</html>
`;
printWindow.document.write(printContent);
printWindow.document.close();
printWindow.print();
}
// 질문 통계를 인쇄용으로 변환하는 함수
function getQuestionsForPrint() {
const questionsStats = document.querySelector('.questions-stats');
if (!questionsStats) return '';
let printHtml = '';
const questionItems = questionsStats.querySelectorAll('.question-stat');
questionItems.forEach((question, index) => {
const questionTitle = question.querySelector('.question-title');
const questionType = question.querySelector('.question-type');
const answerItems = question.querySelectorAll('.answer-item');
const textResponses = question.querySelectorAll('.text-response');
if (questionTitle) {
printHtml += `
<div class="question-stat">
<div class="question-header">
<h3>${index + 1}. ${questionTitle.textContent}</h3>
${questionType ? `<span style="color: #666; font-size: 0.9em;">[${questionType.textContent}]</span>` : ''}
</div>
`;
if (answerItems.length > 0) {
printHtml += '<div class="answer-stats">';
answerItems.forEach(item => {
const label = item.querySelector('.answer-label');
const count = item.querySelector('.answer-count');
const percentage = item.querySelector('.answer-percentage');
if (label && count && percentage) {
printHtml += `
<div class="answer-item">
<span>${label.textContent}</span>
<span>${count.textContent} (${percentage.textContent})</span>
</div>
`;
}
});
printHtml += '</div>';
}
if (textResponses.length > 0) {
printHtml += '<div class="text-responses" style="margin-top: 15px;">';
printHtml += '<h4>주요 응답:</h4>';
textResponses.forEach((response, idx) => {
if (idx < 5) { // 최대 5개만 인쇄
const content = response.querySelector('.text-response-content');
if (content) {
printHtml += `<p style="margin: 10px 0; padding: 10px; background: #f9f9f9; border-left: 3px solid #AA20FF;">${content.textContent}</p>`;
}
}
});
printHtml += '</div>';
}
printHtml += '</div>';
}
});
return printHtml;
}
// 페이지 로드 시 차트 초기화
document.addEventListener('DOMContentLoaded', function () {
initResponseChart();
initQuestionCharts();
// 애니메이션 효과
const 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);
});
};
</script>
<?php
include_once(G5_ADMIN_PATH . '/admin.tail.php');
?>
+32
View File
@@ -0,0 +1,32 @@
<?php
$sub_menu = '710100';
include_once('./_common.php');
$sv_id = isset($_GET['sv_id']) ? (int)$_GET['sv_id'] : 0;
if (!$sv_id) {
alert('설문 ID가 없습니다.', './survey_list.php');
}
$survey = get_survey($sv_id);
if (!$survey) {
alert('존재하지 않는 설문입니다.', './survey_list.php');
}
if ($survey['sv_status'] != 'draft') {
alert('임시저장 상태의 설문만 활성화할 수 있습니다.', './survey_list.php');
}
// 질문이 있는지 확인
$questions = get_survey_questions($sv_id);
if (empty($questions)) {
alert('질문이 없는 설문은 활성화할 수 없습니다.', './survey_form.php?sv_id='.$sv_id);
}
// 설문 활성화
if (update_survey_status($sv_id, 'active')) {
alert('설문이 활성화되었습니다.', './survey_list.php');
} else {
alert('설문 활성화에 실패했습니다.', './survey_list.php');
}
?>
+34
View File
@@ -0,0 +1,34 @@
<?php
$sub_menu = '710100';
include_once('./_common.php');
$sv_id = isset($_GET['sv_id']) ? (int) $_GET['sv_id'] : 0;
if (!$sv_id) {
alert('설문 ID가 없습니다.', './survey_list.php');
}
$survey = get_survey($sv_id);
if (!$survey) {
alert('존재하지 않는 설문입니다.', './survey_list.php');
}
// 응답이 있는 설문은 삭제 대신 상태를 'deleted'로 변경
$response_count = get_survey_response_count($sv_id, '');
if ($response_count > 0) {
// 응답이 있는 경우 소프트 삭제
if (update_survey_status($sv_id, 'deleted')) {
alert('설문이 삭제 처리되었습니다.', './survey_list.php');
} else {
alert('설문 삭제에 실패했습니다.', './survey_list.php');
}
} else {
// 응답이 없는 경우 완전 삭제
$sql = "DELETE FROM survey_master WHERE sv_id = '$sv_id'";
if (sql_query($sql)) {
alert('설문이 완전히 삭제되었습니다.', './survey_list.php');
} else {
alert('설문 삭제에 실패했습니다.', './survey_list.php');
}
}
?>
+208
View File
@@ -0,0 +1,208 @@
<?php
$sub_menu = '710200';
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "w");
$sv_id = isset($_GET['sv_id']) ? (int)$_GET['sv_id'] : 0;
$template_id = isset($_GET['template_id']) ? (int)$_GET['template_id'] : 0;
$is_edit = $sv_id > 0;
$survey = [
'sv_title' => '',
'sv_description' => '',
'sv_start_date' => date('Y-m-d H:i'),
'sv_end_date' => date('Y-m-d H:i', strtotime('+30 days')),
'sv_allow_anonymous' => 1,
'sv_allow_multiple' => 0,
'sv_max_responses' => '',
'sv_theme_color' => '#AA20FF',
'sv_thank_message' => ''
];
$questions = [];
if ($is_edit) {
$survey = get_survey($sv_id);
if (!$survey) {
alert('존재하지 않는 설문입니다.', 'survey_list.php');
}
$questions = get_survey_questions($sv_id);
$g5['title'] = '설문 수정';
} else {
$g5['title'] = '설문 작성';
}
include_once(G5_ADMIN_PATH.'/admin.head.php');
// 템플릿 목록
$templates = get_survey_templates();
?>
<!-- CSS는 survey_form.css 파일에서 자동 로드됩니다 -->
<div class="survey-form-container">
<div class="form-header">
<h1><?php echo $is_edit ? '설문 수정' : '새 설문 만들기'; ?></h1>
<p><?php echo $is_edit ? '설문 내용을 수정할 수 있습니다' : '전문적인 설문조사를 쉽게 만들어보세요'; ?></p>
</div>
<form id="surveyForm" method="post" action="survey_form_update.php">
<input type="hidden" name="sv_id" value="<?php echo $sv_id; ?>">
<!-- 탭 메뉴 -->
<div class="form-tabs">
<button type="button" class="form-tab active" data-tab="basic">
<i class="fa fa-info-circle"></i> 기본 정보
</button>
<button type="button" class="form-tab" data-tab="questions">
<i class="fa fa-question-circle"></i> 질문 설정
</button>
<button type="button" class="form-tab" data-tab="settings">
<i class="fa fa-cog"></i> 고급 설정
</button>
</div>
<!-- 기본 정보 탭 -->
<div class="tab-content active" id="basic">
<?php if (!$is_edit): ?>
<!-- 템플릿 선택 -->
<div class="template-section">
<h3><i class="fa fa-magic"></i> 템플릿으로 빠르게 시작하기</h3>
<p class="help-text">미리 만들어진 템플릿을 선택하면 질문이 자동으로 추가됩니다.</p>
<div class="template-grid">
<div class="template-card" data-template="0">
<h4><i class="fa fa-plus"></i> 직접 작성</h4>
<p>처음부터 직접 설문을 만들어보세요</p>
</div>
<?php foreach ($templates as $template): ?>
<div class="template-card" data-template="<?php echo $template['st_id']; ?>">
<h4><?php echo htmlspecialchars($template['st_name']); ?></h4>
<p><?php echo htmlspecialchars($template['st_description']); ?></p>
<small class="template-category"><?php echo $template['st_category']; ?></small>
</div>
<?php endforeach; ?>
</div>
<input type="hidden" name="template_id" id="templateId" value="<?php echo $template_id; ?>">
</div>
<?php endif; ?>
<div class="form-group">
<label class="form-label required">설문 제목</label>
<input type="text" name="sv_title" class="form-input"
value="<?php echo htmlspecialchars($survey['sv_title']); ?>"
placeholder="설문조사의 제목을 입력하세요" required>
<div class="help-text">명확하고 흥미로운 제목을 작성하면 더 많은 참여를 유도할 수 있습니다.</div>
</div>
<div class="form-group">
<label class="form-label">설문 설명</label>
<textarea name="sv_description" class="form-textarea"
placeholder="설문의 목적과 내용을 간단히 설명해주세요"><?php echo htmlspecialchars($survey['sv_description']); ?></textarea>
<div class="help-text">설문의 목적과 예상 소요시간을 안내하면 참여율이 높아집니다.</div>
</div>
<div class="form-row">
<div class="form-col">
<label class="form-label required">시작일시</label>
<input type="datetime-local" name="sv_start_date" class="form-input"
value="<?php echo date('Y-m-d\TH:i', strtotime($survey['sv_start_date'])); ?>" required>
</div>
<div class="form-col">
<label class="form-label required">종료일시</label>
<input type="datetime-local" name="sv_end_date" class="form-input"
value="<?php echo date('Y-m-d\TH:i', strtotime($survey['sv_end_date'])); ?>" required>
</div>
</div>
<div class="form-group">
<label class="form-label">테마 색상</label>
<input type="color" name="sv_theme_color" class="color-picker"
value="<?php echo $survey['sv_theme_color']; ?>">
<div class="help-text">설문 페이지의 메인 색상을 선택하세요.</div>
</div>
<div class="form-group">
<label class="form-label">완료 메시지</label>
<textarea name="sv_thank_message" class="form-textarea"
placeholder="설문 완료 후 표시될 감사 메시지를 입력하세요"><?php echo htmlspecialchars($survey['sv_thank_message']); ?></textarea>
<div class="help-text">참여자에게 감사 인사와 함께 추가 안내사항을 전달할 수 있습니다.</div>
</div>
</div>
<!-- 질문 설정 탭 -->
<div class="tab-content" id="questions">
<div class="questions-container">
<div id="questionsList">
<?php if (!empty($questions)): ?>
<?php foreach ($questions as $index => $question): ?>
<div class="question-item" data-question-id="<?php echo $question['sq_id']; ?>">
<!-- 기존 질문 렌더링 로직은 JavaScript에서 처리 -->
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<button type="button" class="add-question-btn" onclick="addQuestion()">
<i class="fa fa-plus"></i> 질문 추가하기
</button>
</div>
</div>
<!-- 고급 설정 탭 -->
<div class="tab-content" id="settings">
<div class="form-group">
<label class="form-label">참여 설정</label>
<div class="checkbox-group">
<div class="checkbox-item">
<input type="checkbox" name="sv_allow_anonymous" value="1"
<?php echo $survey['sv_allow_anonymous'] ? 'checked' : ''; ?>>
<label>익명 참여 허용</label>
</div>
<div class="checkbox-item">
<input type="checkbox" name="sv_allow_multiple" value="1"
<?php echo $survey['sv_allow_multiple'] ? 'checked' : ''; ?>>
<label>중복 참여 허용</label>
</div>
</div>
<div class="help-text">익명 참여를 허용하면 로그인하지 않은 사용자도 참여할 수 있습니다.</div>
</div>
<div class="form-group">
<label class="form-label">최대 응답 수</label>
<input type="number" name="sv_max_responses" class="form-input"
value="<?php echo $survey['sv_max_responses']; ?>"
placeholder="제한 없음" min="1">
<div class="help-text">응답 수를 제한하려면 숫자를 입력하세요. 비워두면 제한이 없습니다.</div>
</div>
</div>
<!-- 폼 액션 -->
<div class="form-actions">
<button type="submit" name="action" value="save_draft" class="btn btn-secondary">
<i class="fa fa-save"></i> 임시저장
</button>
<button type="submit" name="action" value="save_and_activate" class="btn btn-success">
<i class="fa fa-play"></i> 저장 후 활성화
</button>
<a href="survey_list.php" class="btn btn-secondary">
<i class="fa fa-times"></i> 취소
</a>
</div>
</form>
</div>
<!-- JavaScript는 survey_form.js 파일에서 자동 로드됩니다 -->
<script>
// 페이지별 초기 데이터 설정
window.surveyFormData = {
questionCount: <?php echo count($questions); ?>,
templateId: <?php echo $template_id; ?>,
isEdit: <?php echo $is_edit ? 'true' : 'false'; ?>
};
</script>
<?php
include_once(G5_ADMIN_PATH.'/admin.tail.php');
?>
+944
View File
@@ -0,0 +1,944 @@
<?php
$sub_menu = '710200';
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "w");
$sv_id = isset($_GET['sv_id']) ? (int)$_GET['sv_id'] : 0;
$template_id = isset($_GET['template_id']) ? (int)$_GET['template_id'] : 0;
$is_edit = $sv_id > 0;
$survey = array(
'sv_title' => '',
'sv_description' => '',
'sv_start_date' => date('Y-m-d H:i'),
'sv_end_date' => date('Y-m-d H:i', strtotime('+30 days')),
'sv_allow_anonymous' => 1,
'sv_allow_multiple' => 0,
'sv_max_responses' => '',
'sv_theme_color' => '#AA20FF',
'sv_thank_message' => ''
);
$questions = array();
if ($is_edit) {
$survey = get_survey($sv_id);
if (!$survey) {
alert('존재하지 않는 설문입니다.', 'survey_list.php');
}
$questions = get_survey_questions($sv_id);
$g5['title'] = '설문 수정';
} else {
$g5['title'] = '설문 작성';
}
include_once(G5_ADMIN_PATH.'/admin.head.php');
// 템플릿 목록
$templates = get_survey_templates();
?>
<style>
.survey-form-container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.form-header {
background: linear-gradient(135deg, #AA20FF 0%, #8A1ACC 100%);
color: white;
padding: 30px;
border-radius: 15px;
margin-bottom: 30px;
text-align: center;
}
.form-header h1 {
margin: 0 0 10px 0;
font-size: 2.2em;
}
.form-tabs {
display: flex;
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 20px;
overflow: hidden;
}
.form-tab {
flex: 1;
padding: 15px 20px;
background: #fff;
border: none;
cursor: pointer;
font-size: 1em;
font-weight: 600;
transition: all 0.3s ease;
position: relative;
}
.form-tab.active {
background: #AA20FF;
color: white;
}
.form-tab:not(.active):hover {
background: #e9ecef;
}
.tab-content {
display: none;
background: white;
border-radius: 15px;
padding: 30px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.tab-content.active {
display: block;
}
.form-group {
margin-bottom: 25px;
}
.form-label {
display: block;
font-weight: 600;
color: #333;
margin-bottom: 8px;
font-size: 1em;
}
.form-label.required::after {
content: ' *';
color: #e74c3c;
}
.form-input,
.form-textarea,
.form-select {
width: 100%;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1em;
transition: all 0.3s ease;
background: #fafafa;
}
/* 셀렉트 박스 높이 수정 */
select,
.form-select,
.question-type-select {
height: 43px !important;
line-height: 41px !important;
padding: 8px 12px !important;
vertical-align: middle !important;
}
.form-input:focus,
.form-textarea:focus,
.form-select:focus {
outline: none;
border-color: #AA20FF;
background: white;
box-shadow: 0 0 0 3px rgba(170, 32, 255, 0.1);
}
.form-textarea {
min-height: 100px;
resize: vertical;
}
.form-row {
display: flex;
gap: 20px;
}
.form-col {
flex: 1;
}
.checkbox-group {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox-item input[type="checkbox"] {
transform: scale(1.2);
accent-color: #AA20FF;
}
.color-picker {
width: 60px;
height: 40px;
border: none;
border-radius: 8px;
cursor: pointer;
}
.template-section {
background: #fff;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
.template-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin-top: 15px;
}
.template-card {
background: white;
padding: 20px;
border-radius: 10px;
border: 2px solid #e0e0e0;
cursor: pointer;
transition: all 0.3s ease;
}
.template-card:hover {
border-color: #AA20FF;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.template-card.selected {
border-color: #AA20FF;
background: rgba(170, 32, 255, 0.05);
}
.questions-container {
margin-top: 20px;
}
.question-item {
background: #fff;
border: 2px solid #e0e0e0;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
position: relative;
}
.question-header {
display: flex;
justify-content: between;
align-items: center;
margin-bottom: 15px;
}
.question-number {
background: #AA20FF;
color: white;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 15px;
}
.question-controls {
display: flex;
gap: 10px;
}
.btn-sm {
padding: 5px 10px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 0.8em;
transition: all 0.3s ease;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-sm:hover {
transform: translateY(-1px);
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.question-type-select {
margin-bottom: 15px;
}
.options-container {
margin-top: 15px;
}
.option-item {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 10px;
}
.option-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 5px;
}
.add-option-btn {
background: #28a745;
color: white;
border: none;
padding: 8px 15px;
border-radius: 5px;
cursor: pointer;
font-size: 0.9em;
}
.add-question-btn {
width: 100%;
padding: 15px;
background: #AA20FF;
color: white;
border: none;
border-radius: 10px;
font-size: 1.1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 20px;
}
.add-question-btn:hover {
background: #8A1ACC;
transform: translateY(-2px);
}
.form-actions {
display: flex;
gap: 15px;
justify-content: center;
padding: 20px;
background: white;
border-radius: 15px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.btn {
padding: 12px 30px;
border: none;
border-radius: 8px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
text-align: center;
}
.btn-primary {
background: #AA20FF;
color: white;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
}
.help-text {
font-size: 0.9em;
color: #666;
margin-top: 5px;
line-height: 1.4;
}
/* 반응형 */
@media (max-width: 768px) {
.survey-form-container {
padding: 10px;
}
.form-row {
flex-direction: column;
}
.form-tabs {
flex-direction: column;
}
.template-grid {
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column;
}
.question-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
}
</style>
<div class="survey-form-container">
<div class="form-header">
<h1><?php echo $is_edit ? '설문 수정' : '새 설문 만들기'; ?></h1>
<p><?php echo $is_edit ? '설문 내용을 수정할 수 있습니다' : '전문적인 설문조사를 쉽게 만들어보세요'; ?></p>
</div>
<form id="surveyForm" method="post" action="survey_form_update.php">
<input type="hidden" name="sv_id" value="<?php echo $sv_id; ?>">
<!-- 탭 메뉴 -->
<div class="form-tabs">
<button type="button" class="form-tab active" data-tab="basic">
<i class="fa fa-info-circle"></i> 기본 정보
</button>
<button type="button" class="form-tab" data-tab="questions">
<i class="fa fa-question-circle"></i> 질문 설정
</button>
<button type="button" class="form-tab" data-tab="settings">
<i class="fa fa-cog"></i> 고급 설정
</button>
</div>
<!-- 기본 정보 탭 -->
<div class="tab-content active" id="basic">
<?php if (!$is_edit): ?>
<!-- 템플릿 선택 -->
<div class="template-section">
<h3><i class="fa fa-magic"></i> 템플릿으로 빠르게 시작하기</h3>
<p class="help-text">미리 만들어진 템플릿을 선택하면 질문이 자동으로 추가됩니다.</p>
<div class="template-grid">
<div class="template-card" data-template="0">
<h4><i class="fa fa-plus"></i> 직접 작성</h4>
<p>처음부터 직접 설문을 만들어보세요</p>
</div>
<?php foreach ($templates as $template): ?>
<div class="template-card" data-template="<?php echo $template['st_id']; ?>">
<h4><?php echo htmlspecialchars($template['st_name']); ?></h4>
<p><?php echo htmlspecialchars($template['st_description']); ?></p>
<small style="color: #666;"><?php echo $template['st_category']; ?></small>
</div>
<?php endforeach; ?>
</div>
<input type="hidden" name="template_id" id="templateId" value="<?php echo $template_id; ?>">
</div>
<?php endif; ?>
<div class="form-group">
<label class="form-label required">설문 제목</label>
<input type="text" name="sv_title" class="form-input"
value="<?php echo htmlspecialchars($survey['sv_title']); ?>"
placeholder="설문조사의 제목을 입력하세요" required>
<div class="help-text">명확하고 흥미로운 제목을 작성하면 더 많은 참여를 유도할 수 있습니다.</div>
</div>
<div class="form-group">
<label class="form-label">설문 설명</label>
<textarea name="sv_description" class="form-textarea"
placeholder="설문의 목적과 내용을 간단히 설명해주세요"><?php echo htmlspecialchars($survey['sv_description']); ?></textarea>
<div class="help-text">설문의 목적과 예상 소요시간을 안내하면 참여율이 높아집니다.</div>
</div>
<div class="form-row">
<div class="form-col">
<label class="form-label required">시작일시</label>
<input type="datetime-local" name="sv_start_date" class="form-input"
value="<?php echo date('Y-m-d\TH:i', strtotime($survey['sv_start_date'])); ?>" required>
</div>
<div class="form-col">
<label class="form-label required">종료일시</label>
<input type="datetime-local" name="sv_end_date" class="form-input"
value="<?php echo date('Y-m-d\TH:i', strtotime($survey['sv_end_date'])); ?>" required>
</div>
</div>
<div class="form-group">
<label class="form-label">테마 색상</label>
<input type="color" name="sv_theme_color" class="color-picker"
value="<?php echo $survey['sv_theme_color']; ?>">
<div class="help-text">설문 페이지의 메인 색상을 선택하세요.</div>
</div>
<div class="form-group">
<label class="form-label">완료 메시지</label>
<textarea name="sv_thank_message" class="form-textarea"
placeholder="설문 완료 후 표시될 감사 메시지를 입력하세요"><?php echo htmlspecialchars($survey['sv_thank_message']); ?></textarea>
<div class="help-text">참여자에게 감사 인사와 함께 추가 안내사항을 전달할 수 있습니다.</div>
</div>
</div>
<!-- 질문 설정 탭 -->
<div class="tab-content" id="questions">
<div class="questions-container">
<div id="questionsList">
<?php if (!empty($questions)): ?>
<?php foreach ($questions as $index => $question): ?>
<div class="question-item" data-question-id="<?php echo $question['sq_id']; ?>">
<!-- 질문 내용 렌더링 -->
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<button type="button" class="add-question-btn" onclick="addQuestion()">
<i class="fa fa-plus"></i> 질문 추가하기
</button>
</div>
</div>
<!-- 고급 설정 탭 -->
<div class="tab-content" id="settings">
<div class="form-group">
<label class="form-label">참여 설정</label>
<div class="checkbox-group">
<div class="checkbox-item">
<input type="checkbox" name="sv_allow_anonymous" value="1"
<?php echo $survey['sv_allow_anonymous'] ? 'checked' : ''; ?>>
<label>익명 참여 허용</label>
</div>
<div class="checkbox-item">
<input type="checkbox" name="sv_allow_multiple" value="1"
<?php echo $survey['sv_allow_multiple'] ? 'checked' : ''; ?>>
<label>중복 참여 허용</label>
</div>
</div>
<div class="help-text">익명 참여를 허용하면 로그인하지 않은 사용자도 참여할 수 있습니다.</div>
</div>
<div class="form-group">
<label class="form-label">최대 응답 수</label>
<input type="number" name="sv_max_responses" class="form-input"
value="<?php echo $survey['sv_max_responses']; ?>"
placeholder="제한 없음" min="1">
<div class="help-text">응답 수를 제한하려면 숫자를 입력하세요. 비워두면 제한이 없습니다.</div>
</div>
</div>
<!-- 폼 액션 -->
<div class="form-actions">
<button type="submit" name="action" value="save_draft" class="btn btn-secondary">
<i class="fa fa-save"></i> 임시저장
</button>
<button type="submit" name="action" value="save_and_activate" class="btn btn-success">
<i class="fa fa-play"></i> 저장 후 활성화
</button>
<a href="survey_list.php" class="btn btn-secondary">
<i class="fa fa-times"></i> 취소
</a>
</div>
</form>
</div>
<script>
let questionCount = <?php echo count($questions); ?>;
const questionTypes = {
'text': '단답형',
'textarea': '장문형',
'radio': '단일선택',
'checkbox': '다중선택',
'select': '드롭다운',
'rating': '평점',
'date': '날짜'
};
// 탭 전환
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');
});
});
// 템플릿 선택
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;
}
// 폼 제출 전 검증
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;
}
});
// 페이지 로드 시 기존 질문들의 타입에 따라 옵션 표시
document.addEventListener('DOMContentLoaded', function() {
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(); // 템플릿 카드 클릭하여 자동 선택
}
}
});
</script>
<?php
include_once(G5_ADMIN_PATH.'/admin.tail.php');
?>
+178
View File
@@ -0,0 +1,178 @@
<?php
$sub_menu = '710200';
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "w");
$sv_id = isset($_POST['sv_id']) ? (int)$_POST['sv_id'] : 0;
$action = isset($_POST['action']) ? $_POST['action'] : 'save_draft';
$template_id = isset($_POST['template_id']) ? (int)$_POST['template_id'] : 0;
// 기본 정보
$sv_title = isset($_POST['sv_title']) ? trim($_POST['sv_title']) : '';
$sv_description = isset($_POST['sv_description']) ? trim($_POST['sv_description']) : '';
$sv_start_date = isset($_POST['sv_start_date']) ? $_POST['sv_start_date'] : '';
$sv_end_date = isset($_POST['sv_end_date']) ? $_POST['sv_end_date'] : '';
$sv_allow_anonymous = isset($_POST['sv_allow_anonymous']) ? 1 : 0;
$sv_allow_multiple = isset($_POST['sv_allow_multiple']) ? 1 : 0;
$sv_max_responses = isset($_POST['sv_max_responses']) ? (int)$_POST['sv_max_responses'] : null;
$sv_theme_color = isset($_POST['sv_theme_color']) ? $_POST['sv_theme_color'] : '#AA20FF';
$sv_thank_message = isset($_POST['sv_thank_message']) ? trim($_POST['sv_thank_message']) : '';
// 유효성 검사
if (!$sv_title) {
alert('설문 제목을 입력해주세요.');
}
if (!$sv_start_date || !$sv_end_date) {
alert('설문 시작일과 종료일을 입력해주세요.');
}
if (strtotime($sv_start_date) >= strtotime($sv_end_date)) {
alert('종료일시는 시작일시보다 늦어야 합니다.');
}
// 상태 결정
$sv_status = ($action === 'save_and_activate') ? 'active' : 'draft';
// 질문 데이터
$questions = isset($_POST['questions']) ? $_POST['questions'] : [];
if ($sv_id > 0) {
// 기존 설문 수정
$sql = "UPDATE survey_master SET
sv_title = '".sql_real_escape_string($sv_title)."',
sv_description = '".sql_real_escape_string($sv_description)."',
sv_start_date = '".sql_real_escape_string($sv_start_date)."',
sv_end_date = '".sql_real_escape_string($sv_end_date)."',
sv_status = '".sql_real_escape_string($sv_status)."',
sv_allow_anonymous = '$sv_allow_anonymous',
sv_allow_multiple = '$sv_allow_multiple',
sv_max_responses = ".($sv_max_responses ? "'$sv_max_responses'" : 'NULL').",
sv_theme_color = '".sql_real_escape_string($sv_theme_color)."',
sv_thank_message = '".sql_real_escape_string($sv_thank_message)."',
sv_updated_at = NOW()
WHERE sv_id = '$sv_id'";
sql_query($sql);
// 기존 질문들 삭제
sql_query("DELETE FROM survey_questions WHERE sv_id = '$sv_id'");
} else {
// 새 설문 생성
if ($template_id > 0) {
// 템플릿으로부터 설문 생성
$new_sv_id = create_survey_from_template($template_id, $sv_title, $member['mb_id']);
if ($new_sv_id) {
// 템플릿으로 생성된 설문의 기본 정보 업데이트
$sql = "UPDATE survey_master SET
sv_description = '".sql_real_escape_string($sv_description)."',
sv_start_date = '".sql_real_escape_string($sv_start_date)."',
sv_end_date = '".sql_real_escape_string($sv_end_date)."',
sv_status = '".sql_real_escape_string($sv_status)."',
sv_allow_anonymous = '$sv_allow_anonymous',
sv_allow_multiple = '$sv_allow_multiple',
sv_max_responses = ".($sv_max_responses ? "'$sv_max_responses'" : 'NULL').",
sv_theme_color = '".sql_real_escape_string($sv_theme_color)."',
sv_thank_message = '".sql_real_escape_string($sv_thank_message)."'
WHERE sv_id = '$new_sv_id'";
sql_query($sql);
$sv_id = $new_sv_id;
// 템플릿 질문들을 사용자가 수정했다면 업데이트
if (!empty($questions)) {
sql_query("DELETE FROM survey_questions WHERE sv_id = '$sv_id'");
}
} else {
alert('템플릿으로부터 설문 생성에 실패했습니다.');
}
} else {
// 직접 생성
$sql = "INSERT INTO survey_master
(sv_title, sv_description, sv_start_date, sv_end_date, sv_status,
sv_allow_anonymous, sv_allow_multiple, sv_max_responses, sv_theme_color,
sv_thank_message, sv_created_by, sv_created_at)
VALUES
('".sql_real_escape_string($sv_title)."',
'".sql_real_escape_string($sv_description)."',
'".sql_real_escape_string($sv_start_date)."',
'".sql_real_escape_string($sv_end_date)."',
'".sql_real_escape_string($sv_status)."',
'$sv_allow_anonymous',
'$sv_allow_multiple',
".($sv_max_responses ? "'$sv_max_responses'" : 'NULL').",
'".sql_real_escape_string($sv_theme_color)."',
'".sql_real_escape_string($sv_thank_message)."',
'{$member['mb_id']}',
NOW())";
sql_query($sql);
$sv_id = sql_insert_id();
if (!$sv_id) {
alert('설문 생성에 실패했습니다.');
}
}
}
// 질문들 저장 (템플릿 사용 시에도 사용자가 수정했다면 저장)
if (!empty($questions)) {
foreach ($questions as $order => $question) {
$sq_type = isset($question['type']) ? trim($question['type']) : 'text';
$sq_title = isset($question['title']) ? trim($question['title']) : '';
$sq_description = isset($question['description']) ? trim($question['description']) : '';
$sq_required = isset($question['required']) ? 1 : 0;
$sq_options = '';
if (!$sq_title) {
continue; // 제목이 없는 질문은 건너뛰기
}
// 옵션 처리 (객관식 질문인 경우)
if (in_array($sq_type, ['radio', 'checkbox', 'select']) && isset($question['options'])) {
$options = array_filter($question['options'], function($option) {
return trim($option) !== '';
});
if (!empty($options)) {
$sq_options = json_encode(array_values($options), JSON_UNESCAPED_UNICODE);
}
}
$sql = "INSERT INTO survey_questions
(sv_id, sq_order, sq_type, sq_title, sq_description, sq_required, sq_options, sq_created_at)
VALUES
('$sv_id',
'".($order + 1)."',
'".sql_real_escape_string($sq_type)."',
'".sql_real_escape_string($sq_title)."',
'".sql_real_escape_string($sq_description)."',
'$sq_required',
'".sql_real_escape_string($sq_options)."',
NOW())";
sql_query($sql);
}
}
// 활성화된 설문인 경우 질문이 있는지 확인
if ($sv_status === 'active') {
$question_count = sql_fetch("SELECT COUNT(*) as cnt FROM survey_questions WHERE sv_id = '$sv_id'")['cnt'];
if ($question_count == 0) {
// 질문이 없으면 임시저장 상태로 변경
sql_query("UPDATE survey_master SET sv_status = 'draft' WHERE sv_id = '$sv_id'");
alert('질문이 없어서 임시저장 상태로 저장되었습니다. 질문을 추가한 후 활성화해주세요.', 'survey_form.php?sv_id='.$sv_id);
}
}
$message = '';
if ($action === 'save_and_activate') {
$message = '설문이 저장되고 활성화되었습니다.';
} else {
$message = '설문이 임시저장되었습니다.';
}
alert($message, 'survey_list.php');
?>
+411
View File
@@ -0,0 +1,411 @@
<?php
$sub_menu = '710100';
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "r");
$g5['title'] = '설문 목록';
include_once(G5_ADMIN_PATH.'/admin.head.php');
// 변수 초기화
$sfl = isset($_GET['sfl']) ? clean_xss_tags($_GET['sfl']) : 'sv_title';
$stx = isset($_GET['stx']) ? clean_xss_tags($_GET['stx']) : '';
$status = isset($_GET['status']) ? clean_xss_tags($_GET['status']) : '';
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
// 검색 조건
$where = " WHERE 1=1 ";
$sql_search = "";
if ($stx) {
$where .= " AND $sfl LIKE '%".sql_real_escape_string($stx)."%' ";
$sql_search .= "&sfl=$sfl&stx=".urlencode($stx);
}
if ($status) {
$where .= " AND sv_status = '".sql_real_escape_string($status)."' ";
$sql_search .= "&status=".urlencode($status);
}
// 페이징
$sql_common = " FROM survey_master $where ";
$sql = " SELECT COUNT(*) as cnt $sql_common ";
$row = sql_fetch($sql);
$total_count = $row['cnt'];
$rows = 20;
$total_page = ceil($total_count / $rows);
if ($page < 1) $page = 1;
$from_record = ($page - 1) * $rows;
$sql = " SELECT * $sql_common ORDER BY sv_id DESC LIMIT $from_record, $rows ";
$result = sql_query($sql);
// 상태별 카운트
$status_counts = array();
$status_sql = "SELECT sv_status, COUNT(*) as cnt FROM survey_master GROUP BY sv_status";
$status_result = sql_query($status_sql);
while ($status_row = sql_fetch_array($status_result)) {
$status_counts[$status_row['sv_status']] = $status_row['cnt'];
}
?>
<style>
.survey-header {
display: flex;
justify-content: between;
align-items: center;
margin-bottom: 20px;
padding: 20px;
background: linear-gradient(135deg, #AA20FF 0%, #8A1ACC 100%);
color: white;
border-radius: 8px;
}
.survey-stats {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.stat-card {
flex: 1;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
border-left: 4px solid #AA20FF;
}
.stat-number {
font-size: 2em;
font-weight: bold;
color: #AA20FF;
margin-bottom: 5px;
}
.stat-label {
color: #666;
font-size: 0.9em;
}
.status-filter {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.status-btn {
padding: 8px 16px;
border: 1px solid #ddd;
background: white;
color: #666;
text-decoration: none;
border-radius: 20px;
font-size: 0.9em;
transition: all 0.3s;
}
.status-btn:hover,
.status-btn.active {
background: #AA20FF;
color: white;
border-color: #AA20FF;
}
.survey-table {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.survey-table table {
width: 100%;
border-collapse: collapse;
}
.survey-table th {
background: #fff;
padding: 15px 10px;
text-align: left;
font-weight: 600;
border-bottom: 1px solid #dee2e6;
}
.survey-table td {
padding: 15px 10px;
border-bottom: 1px solid #f1f3f4;
vertical-align: middle;
}
.survey-table tr:hover {
background: #fff;
}
.status-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 0.8em;
font-weight: 500;
text-transform: uppercase;
}
.status-draft { background: #e9ecef; color: #495057; }
.status-active { background: #d4edda; color: #155724; }
.status-closed { background: #f8d7da; color: #721c24; }
.status-deleted { background: #d1ecf1; color: #0c5460; }
.action-buttons {
display: flex;
gap: 5px;
}
.btn-sm {
padding: 5px 10px;
font-size: 0.8em;
border-radius: 4px;
text-decoration: none;
border: none;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary { background: #007bff; color: white; }
.btn-success { background: #28a745; color: white; }
.btn-warning { background: #ffc107; color: #212529; }
.btn-danger { background: #dc3545; color: white; }
.btn-info { background: #17a2b8; color: white; }
.btn-sm:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.search-form {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.search-form .form-row {
display: flex;
gap: 10px;
align-items: center;
}
.search-form select,
.search-form input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
.create-btn {
position: fixed;
bottom: 30px;
right: 30px;
width: 60px;
height: 60px;
background: #AA20FF;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
font-size: 1.5em;
box-shadow: 0 4px 12px rgba(170, 32, 255, 0.3);
transition: all 0.3s;
z-index: 1000;
}
.create-btn:hover {
background: #8A1ACC;
transform: scale(1.1);
color: white;
}
</style>
<div class="survey-header">
<div>
<h1><i class="fa fa-poll"></i> 설문 관리</h1>
<p>설문조사를 생성하고 관리할 수 있습니다</p>
</div>
</div>
<div class="survey-stats">
<div class="stat-card">
<div class="stat-number"><?php echo number_format($status_counts['active'] ?? 0); ?></div>
<div class="stat-label">진행중인 설문</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo number_format($status_counts['draft'] ?? 0); ?></div>
<div class="stat-label">작성중인 설문</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo number_format($status_counts['closed'] ?? 0); ?></div>
<div class="stat-label">종료된 설문</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo number_format($total_count); ?></div>
<div class="stat-label">전체 설문</div>
</div>
</div>
<div class="search-form">
<form method="get" class="form-row">
<select name="sfl">
<option value="sv_title"<?php echo $sfl == 'sv_title' ? ' selected' : ''; ?>>제목</option>
<option value="sv_description"<?php echo $sfl == 'sv_description' ? ' selected' : ''; ?>>설명</option>
<option value="sv_created_by"<?php echo $sfl == 'sv_created_by' ? ' selected' : ''; ?>>작성자</option>
</select>
<input type="text" name="stx" value="<?php echo $stx; ?>" placeholder="검색어를 입력하세요">
<button type="submit" class="btn-sm btn-primary">
<i class="fa fa-search"></i> 검색
</button>
<?php if ($stx): ?>
<a href="?" class="btn-sm btn-warning">
<i class="fa fa-times"></i> 초기화
</a>
<?php endif; ?>
</form>
</div>
<div class="status-filter">
<a href="?" class="status-btn <?php echo !isset($_GET['status']) ? 'active' : ''; ?>">
<i class="fa fa-list"></i> 전체
</a>
<a href="?status=draft" class="status-btn <?php echo $status == 'draft' ? 'active' : ''; ?>">
<i class="fa fa-edit"></i> 작성중 (<?php echo $status_counts['draft'] ?? 0; ?>)
</a>
<a href="?status=active" class="status-btn <?php echo $status == 'active' ? 'active' : ''; ?>">
<i class="fa fa-play"></i> 진행중 (<?php echo $status_counts['active'] ?? 0; ?>)
</a>
<a href="?status=closed" class="status-btn <?php echo $status == 'closed' ? 'active' : ''; ?>">
<i class="fa fa-stop"></i> 종료 (<?php echo $status_counts['closed'] ?? 0; ?>)
</a>
</div>
<div class="survey-table">
<table>
<thead>
<tr>
<th width="60">ID</th>
<th>설문 제목</th>
<th width="100">상태</th>
<th width="120">응답 수</th>
<th width="150">설문 기간</th>
<th width="100">작성자</th>
<th width="120">생성일</th>
<th width="200">관리</th>
</tr>
</thead>
<tbody>
<?php
if (sql_num_rows($result) > 0) {
while ($row = sql_fetch_array($result)) {
$response_count = get_survey_response_count($row['sv_id'], 'completed');
$total_responses = get_survey_response_count($row['sv_id'], '');
// 상태별 클래스
$status_class = 'status-' . $row['sv_status'];
$status_text = array(
'draft' => '작성중',
'active' => '진행중',
'closed' => '종료',
'deleted' => '삭제됨'
);
?>
<tr>
<td><?php echo $row['sv_id']; ?></td>
<td>
<strong><?php echo htmlspecialchars($row['sv_title']); ?></strong>
<?php if ($row['sv_description']): ?>
<br><small style="color: #666;"><?php
if (function_exists('mb_substr')) {
echo htmlspecialchars(mb_substr($row['sv_description'], 0, 50)) . '...';
} else {
echo htmlspecialchars(substr($row['sv_description'], 0, 150)) . '...'; // mbstring이 없을 경우, 바이트 단위로 자름
}
?></small>
<?php endif; ?>
</td>
<td>
<span class="status-badge <?php echo $status_class; ?>">
<?php echo $status_text[$row['sv_status']]; ?>
</span>
</td>
<td>
<strong><?php echo number_format($response_count); ?></strong>
<?php if ($total_responses > $response_count): ?>
<br><small style="color: #666;">진행중: <?php echo $total_responses - $response_count; ?></small>
<?php endif; ?>
</td>
<td>
<small>
<?php echo date('m/d', strtotime($row['sv_start_date'])); ?> ~<br>
<?php echo date('m/d', strtotime($row['sv_end_date'])); ?>
</small>
</td>
<td><?php echo $row['sv_created_by']; ?></td>
<td>
<small><?php echo date('Y-m-d', strtotime($row['sv_created_at'])); ?></small>
</td>
<td>
<div class="action-buttons">
<a href="survey_form.php?sv_id=<?php echo $row['sv_id']; ?>" class="btn-sm btn-primary" title="수정">
<i class="fa fa-edit"></i>
</a>
<?php if ($row['sv_status'] == 'draft'): ?>
<a href="survey_activate.php?sv_id=<?php echo $row['sv_id']; ?>" class="btn-sm btn-success" title="활성화" onclick="return confirm('설문을 활성화하시겠습니까?')">
<i class="fa fa-play"></i>
</a>
<?php endif; ?>
<?php if ($response_count > 0): ?>
<a href="statistics.php?sv_id=<?php echo $row['sv_id']; ?>" class="btn-sm btn-info" title="통계">
<i class="fa fa-chart-bar"></i>
</a>
<a href="export.php?sv_id=<?php echo $row['sv_id']; ?>" class="btn-sm btn-warning" title="엑셀 다운로드">
<i class="fa fa-file-excel"></i>
</a>
<?php endif; ?>
<a href="survey_delete.php?sv_id=<?php echo $row['sv_id']; ?>" class="btn-sm btn-danger" title="삭제" onclick="return confirm('정말 삭제하시겠습니까?')">
<i class="fa fa-trash"></i>
</a>
</div>
</td>
</tr>
<?php
}
} else {
?>
<tr>
<td colspan="8" style="text-align: center; padding: 50px; color: #999;">
<i class="fa fa-inbox" style="font-size: 3em; margin-bottom: 20px; display: block;"></i>
등록된 설문이 없습니다.<br>
<a href="survey_form.php" style="color: #AA20FF; margin-top: 10px; display: inline-block;">첫 설문을 만들어보세요</a>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php
// 페이징
if ($total_page > 1) {
$paging = get_paging(10, $page, $total_page, "?$sql_search&page=");
echo '<div style="margin-top: 20px;">' . $paging . '</div>';
}
?>
<a href="survey_form.php" class="create-btn" title="새 설문 만들기">
<i class="fa fa-plus"></i>
</a>
<?php
include_once(G5_ADMIN_PATH.'/admin.tail.php');
?>
+230
View File
@@ -0,0 +1,230 @@
<?php
$sub_menu = '710300';
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "w");
$st_id = isset($_GET['st_id']) ? (int)$_GET['st_id'] : 0;
$is_edit = $st_id > 0;
$template = [
'st_name' => '',
'st_description' => '',
'st_category' => '고객서비스',
'st_is_public' => 1
];
$questions = [];
if ($is_edit) {
$template = sql_fetch("SELECT * FROM survey_templates WHERE st_id = '$st_id'");
if (!$template) {
alert('존재하지 않는 템플릿입니다.', 'template_list.php');
}
// 템플릿 질문들 가져오기
$question_sql = "SELECT * FROM survey_template_questions WHERE st_id = '$st_id' ORDER BY stq_order ASC";
$question_result = sql_query($question_sql);
while ($question = sql_fetch_array($question_result)) {
$questions[] = $question;
}
$g5['title'] = '템플릿 수정';
} else {
$g5['title'] = '새 템플릿 만들기';
}
include_once(G5_ADMIN_PATH.'/admin.head.php');
?>
<!-- CSS는 template_form.css 파일에서 자동 로드됩니다 -->
<div class="template-form-container">
<div class="form-header">
<h1><i class="fa fa-magic"></i> <?php echo $is_edit ? '템플릿 수정' : '새 템플릿 만들기'; ?></h1>
<p>설문 템플릿을 만들어 다른 사용자들과 공유하세요</p>
</div>
<div class="form-content">
<form name="templateForm" method="post" action="template_form_update.php">
<?php if ($is_edit): ?>
<input type="hidden" name="st_id" value="<?php echo $st_id; ?>">
<input type="hidden" name="mode" value="update">
<?php else: ?>
<input type="hidden" name="mode" value="insert">
<?php endif; ?>
<div class="form-section">
<h2 class="section-title">
<i class="fa fa-info-circle"></i> 기본 정보
</h2>
<div class="form-group">
<label class="form-label required">템플릿 이름</label>
<input type="text" name="st_name" class="form-control"
value="<?php echo htmlspecialchars($template['st_name']); ?>"
placeholder="템플릿 이름을 입력하세요" required>
</div>
<div class="form-group">
<label class="form-label">템플릿 설명</label>
<textarea name="st_description" class="form-control" rows="3"
placeholder="템플릿에 대한 설명을 입력하세요"><?php echo htmlspecialchars($template['st_description']); ?></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label required">카테고리</label>
<select name="st_category" class="form-control form-select" required>
<option value="고객서비스"<?php echo $template['st_category'] == '고객서비스' ? ' selected' : ''; ?>>고객서비스</option>
<option value="마케팅"<?php echo $template['st_category'] == '마케팅' ? ' selected' : ''; ?>>마케팅</option>
<option value="제품개발"<?php echo $template['st_category'] == '제품개발' ? ' selected' : ''; ?>>제품개발</option>
<option value="인사관리"<?php echo $template['st_category'] == '인사관리' ? ' selected' : ''; ?>>인사관리</option>
<option value="기타"<?php echo $template['st_category'] == '기타' ? ' selected' : ''; ?>>기타</option>
</select>
</div>
<div class="form-group">
<label class="form-label">공개 설정</label>
<select name="st_is_public" class="form-control form-select">
<option value="1"<?php echo $template['st_is_public'] == 1 ? ' selected' : ''; ?>>공개</option>
<option value="0"<?php echo $template['st_is_public'] == 0 ? ' selected' : ''; ?>>비공개</option>
</select>
</div>
</div>
</div>
<div class="form-section">
<h2 class="section-title">
<i class="fa fa-question-circle"></i> 질문 구성
</h2>
<div class="questions-container" id="questionsContainer">
<?php if (count($questions) > 0): ?>
<?php foreach ($questions as $index => $question): ?>
<div class="question-item" data-index="<?php echo $index; ?>">
<div class="question-header">
<div class="question-number"><?php echo $index + 1; ?></div>
<div class="question-title-text">질문 <?php echo $index + 1; ?></div>
<div class="question-actions">
<button type="button" class="btn-sm btn-secondary" onclick="moveQuestion(<?php echo $index; ?>, 'up')">
<i class="fa fa-arrow-up"></i>
</button>
<button type="button" class="btn-sm btn-secondary" onclick="moveQuestion(<?php echo $index; ?>, 'down')">
<i class="fa fa-arrow-down"></i>
</button>
<button type="button" class="btn-sm btn-danger" onclick="removeQuestion(<?php echo $index; ?>)">
<i class="fa fa-trash"></i>
</button>
</div>
</div>
<input type="hidden" name="questions[<?php echo $index; ?>][stq_id]" value="<?php echo $question['stq_id']; ?>">
<div class="form-row">
<div class="form-group">
<label class="form-label">질문 제목</label>
<input type="text" name="questions[<?php echo $index; ?>][stq_title]" class="form-control"
value="<?php echo htmlspecialchars($question['stq_title']); ?>"
placeholder="질문을 입력하세요" required>
</div>
<div class="form-group question-type-group">
<label class="form-label">질문 유형</label>
<select name="questions[<?php echo $index; ?>][stq_type]" class="form-control form-select question-type-select"
onchange="toggleOptions(<?php echo $index; ?>)">
<option value="text"<?php echo $question['stq_type'] == 'text' ? ' selected' : ''; ?>>단답형</option>
<option value="textarea"<?php echo $question['stq_type'] == 'textarea' ? ' selected' : ''; ?>>장문형</option>
<option value="radio"<?php echo $question['stq_type'] == 'radio' ? ' selected' : ''; ?>>객관식(단일)</option>
<option value="checkbox"<?php echo $question['stq_type'] == 'checkbox' ? ' selected' : ''; ?>>객관식(다중)</option>
<option value="select"<?php echo $question['stq_type'] == 'select' ? ' selected' : ''; ?>>드롭다운</option>
<option value="rating"<?php echo $question['stq_type'] == 'rating' ? ' selected' : ''; ?>>평점</option>
<option value="date"<?php echo $question['stq_type'] == 'date' ? ' selected' : ''; ?>>날짜</option>
<option value="email"<?php echo $question['stq_type'] == 'email' ? ' selected' : ''; ?>>이메일</option>
<option value="number"<?php echo $question['stq_type'] == 'number' ? ' selected' : ''; ?>>숫자</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">질문 설명 (선택사항)</label>
<textarea name="questions[<?php echo $index; ?>][stq_description]" class="form-control" rows="2"
placeholder="질문에 대한 추가 설명"><?php echo htmlspecialchars($question['stq_description']); ?></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label>
<input type="checkbox" name="questions[<?php echo $index; ?>][stq_required]" value="1"
<?php echo $question['stq_required'] ? 'checked' : ''; ?>>
필수 질문
</label>
</div>
</div>
<?php if (in_array($question['stq_type'], ['radio', 'checkbox', 'select'])): ?>
<div class="options-container" id="optionsContainer<?php echo $index; ?>">
<label class="form-label">선택 옵션</label>
<?php
$options = json_decode($question['stq_options'], true);
if (is_array($options)):
foreach ($options as $opt_index => $option):
?>
<div class="option-item">
<input type="text" name="questions[<?php echo $index; ?>][options][]"
class="form-control option-input"
value="<?php echo htmlspecialchars($option); ?>"
placeholder="옵션을 입력하세요">
<button type="button" class="btn-sm btn-danger" onclick="removeOption(this)">
<i class="fa fa-times"></i>
</button>
</div>
<?php
endforeach;
endif;
?>
<button type="button" class="btn-add-option" onclick="addOption(<?php echo $index; ?>)">
<i class="fa fa-plus"></i> 옵션 추가
</button>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php else: ?>
<div class="empty-questions" id="emptyQuestions">
<i class="fa fa-question-circle"></i>
<h3>질문을 추가해주세요</h3>
<p>아래 버튼을 클릭하여 첫 번째 질문을 만들어보세요.</p>
</div>
<?php endif; ?>
</div>
<button type="button" class="btn-add-question" onclick="addQuestion()">
<i class="fa fa-plus"></i> 질문 추가
</button>
</div>
<div class="form-actions">
<button type="submit" class="btn-primary">
<i class="fa fa-save"></i> <?php echo $is_edit ? '템플릿 수정' : '템플릿 저장'; ?>
</button>
<a href="template_list.php" class="btn-primary btn-outline">
<i class="fa fa-times"></i> 취소
</a>
</div>
</form>
</div>
</div>
<!-- JavaScript는 template_form.js 파일에서 자동 로드됩니다 -->
<script>
// 페이지별 초기 데이터 설정
window.templateFormData = {
questionIndex: <?php echo count($questions); ?>,
isEdit: <?php echo $is_edit ? 'true' : 'false'; ?>
};
</script>
<?php
include_once(G5_ADMIN_PATH.'/admin.tail.php');
?>
+720
View File
@@ -0,0 +1,720 @@
<?php
$sub_menu = '710300';
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "w");
$st_id = isset($_GET['st_id']) ? (int)$_GET['st_id'] : 0;
$is_edit = $st_id > 0;
$template = array(
'st_name' => '',
'st_description' => '',
'st_category' => '고객서비스',
'st_is_public' => 1
);
$questions = array();
if ($is_edit) {
$template = sql_fetch("SELECT * FROM survey_templates WHERE st_id = '$st_id'");
if (!$template) {
alert('존재하지 않는 템플릿입니다.', 'template_list.php');
}
// 템플릿 질문들 가져오기
$question_sql = "SELECT * FROM survey_template_questions WHERE st_id = '$st_id' ORDER BY stq_order ASC";
$question_result = sql_query($question_sql);
while ($question = sql_fetch_array($question_result)) {
$questions[] = $question;
}
$g5['title'] = '템플릿 수정';
} else {
$g5['title'] = '새 템플릿 만들기';
}
include_once(G5_ADMIN_PATH.'/admin.head.php');
?>
<style>
.template-form-container {
max-width: 1000px;
margin: 0 auto;
}
.form-header {
background: linear-gradient(135deg, #AA20FF 0%, #8A1ACC 100%);
color: white;
padding: 30px;
border-radius: 8px 8px 0 0;
text-align: center;
}
.form-content {
background: white;
padding: 30px;
border-radius: 0 0 8px 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.form-section {
margin-bottom: 40px;
padding-bottom: 30px;
border-bottom: 1px solid #e9ecef;
}
.form-section:last-child {
border-bottom: none;
margin-bottom: 0;
}
.section-title {
font-size: 1.3em;
font-weight: 600;
color: #333;
margin-bottom: 20px;
display: flex;
align-items: center;
}
.section-title i {
margin-right: 10px;
color: #AA20FF;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.form-label.required::after {
content: ' *';
color: #dc3545;
}
.form-control {
width: 100%;
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 1em;
transition: border-color 0.3s;
}
.form-control:focus {
outline: none;
border-color: #AA20FF;
box-shadow: 0 0 0 3px rgba(170, 32, 255, 0.1);
}
.form-select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
background-position: right 12px center;
background-repeat: no-repeat;
background-size: 16px 12px;
padding-right: 40px;
height: 43px !important;
line-height: 41px !important;
}
.questions-container {
border: 2px dashed #ddd;
border-radius: 8px;
padding: 20px;
min-height: 200px;
}
.question-item {
background: #fff;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
position: relative;
}
.question-header {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.question-number {
background: #AA20FF;
color: white;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
margin-right: 15px;
font-size: 0.9em;
}
.question-actions {
margin-left: auto;
display: flex;
gap: 10px;
}
.btn-sm {
padding: 6px 12px;
font-size: 0.85em;
border-radius: 4px;
border: none;
cursor: pointer;
transition: all 0.3s;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #545b62;
}
.form-row {
display: flex;
gap: 15px;
align-items: end;
}
.form-row .form-group {
flex: 1;
margin-bottom: 0;
}
.options-container {
margin-top: 15px;
}
.option-item {
display: flex;
align-items: center;
margin-bottom: 10px;
gap: 10px;
}
.option-input {
flex: 1;
}
.btn-add-option {
background: #28a745;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
}
.btn-add-option:hover {
background: #218838;
}
.btn-add-question {
width: 100%;
padding: 15px;
background: #AA20FF;
color: white;
border: none;
border-radius: 6px;
font-size: 1em;
font-weight: 500;
cursor: pointer;
transition: background 0.3s;
}
.btn-add-question:hover {
background: #8A1ACC;
}
.form-actions {
text-align: center;
padding-top: 30px;
border-top: 1px solid #e9ecef;
margin-top: 30px;
}
.btn-primary {
background: #AA20FF;
color: white;
padding: 12px 30px;
border: none;
border-radius: 6px;
font-size: 1em;
font-weight: 500;
cursor: pointer;
margin: 0 10px;
transition: background 0.3s;
}
.btn-primary:hover {
background: #8A1ACC;
}
.btn-outline {
background: transparent;
color: #6c757d;
border: 1px solid #6c757d;
}
.btn-outline:hover {
background: #6c757d;
color: white;
}
.empty-questions {
text-align: center;
color: #666;
padding: 40px 20px;
}
.empty-questions i {
font-size: 3em;
margin-bottom: 15px;
opacity: 0.3;
}
/* 반응형 */
@media (max-width: 768px) {
.template-form-container {
margin: 10px;
}
.form-content {
padding: 20px;
}
.form-row {
flex-direction: column;
gap: 0;
}
.form-row .form-group {
margin-bottom: 15px;
}
.question-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.question-actions {
margin-left: 0;
}
}
</style>
<div class="template-form-container">
<div class="form-header">
<h1><i class="fa fa-magic"></i> <?php echo $is_edit ? '템플릿 수정' : '새 템플릿 만들기'; ?></h1>
<p>설문 템플릿을 만들어 다른 사용자들과 공유하세요</p>
</div>
<div class="form-content">
<form name="templateForm" method="post" action="template_form_update.php">
<?php if ($is_edit): ?>
<input type="hidden" name="st_id" value="<?php echo $st_id; ?>">
<input type="hidden" name="mode" value="update">
<?php else: ?>
<input type="hidden" name="mode" value="insert">
<?php endif; ?>
<div class="form-section">
<h2 class="section-title">
<i class="fa fa-info-circle"></i> 기본 정보
</h2>
<div class="form-group">
<label class="form-label required">템플릿 이름</label>
<input type="text" name="st_name" class="form-control"
value="<?php echo htmlspecialchars($template['st_name']); ?>"
placeholder="템플릿 이름을 입력하세요" required>
</div>
<div class="form-group">
<label class="form-label">템플릿 설명</label>
<textarea name="st_description" class="form-control" rows="3"
placeholder="템플릿에 대한 설명을 입력하세요"><?php echo htmlspecialchars($template['st_description']); ?></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label required">카테고리</label>
<select name="st_category" class="form-control form-select" required>
<option value="고객서비스"<?php echo $template['st_category'] == '고객서비스' ? ' selected' : ''; ?>>고객서비스</option>
<option value="마케팅"<?php echo $template['st_category'] == '마케팅' ? ' selected' : ''; ?>>마케팅</option>
<option value="제품개발"<?php echo $template['st_category'] == '제품개발' ? ' selected' : ''; ?>>제품개발</option>
<option value="인사관리"<?php echo $template['st_category'] == '인사관리' ? ' selected' : ''; ?>>인사관리</option>
<option value="기타"<?php echo $template['st_category'] == '기타' ? ' selected' : ''; ?>>기타</option>
</select>
</div>
<div class="form-group">
<label class="form-label">공개 설정</label>
<select name="st_is_public" class="form-control form-select">
<option value="1"<?php echo $template['st_is_public'] == 1 ? ' selected' : ''; ?>>공개</option>
<option value="0"<?php echo $template['st_is_public'] == 0 ? ' selected' : ''; ?>>비공개</option>
</select>
</div>
</div>
</div>
<div class="form-section">
<h2 class="section-title">
<i class="fa fa-question-circle"></i> 질문 구성
</h2>
<div class="questions-container" id="questionsContainer">
<?php if (count($questions) > 0): ?>
<?php foreach ($questions as $index => $question): ?>
<div class="question-item" data-index="<?php echo $index; ?>">
<div class="question-header">
<div class="question-number"><?php echo $index + 1; ?></div>
<div style="flex: 1;">질문 <?php echo $index + 1; ?></div>
<div class="question-actions">
<button type="button" class="btn-sm btn-secondary" onclick="moveQuestion(<?php echo $index; ?>, 'up')">
<i class="fa fa-arrow-up"></i>
</button>
<button type="button" class="btn-sm btn-secondary" onclick="moveQuestion(<?php echo $index; ?>, 'down')">
<i class="fa fa-arrow-down"></i>
</button>
<button type="button" class="btn-sm btn-danger" onclick="removeQuestion(<?php echo $index; ?>)">
<i class="fa fa-trash"></i>
</button>
</div>
</div>
<input type="hidden" name="questions[<?php echo $index; ?>][stq_id]" value="<?php echo $question['stq_id']; ?>">
<div class="form-row">
<div class="form-group">
<label class="form-label">질문 제목</label>
<input type="text" name="questions[<?php echo $index; ?>][stq_title]" class="form-control"
value="<?php echo htmlspecialchars($question['stq_title']); ?>"
placeholder="질문을 입력하세요" required>
</div>
<div class="form-group" style="min-width: 150px;">
<label class="form-label">질문 유형</label>
<select name="questions[<?php echo $index; ?>][stq_type]" class="form-control form-select question-type-select"
onchange="toggleOptions(<?php echo $index; ?>)">
<option value="text"<?php echo $question['stq_type'] == 'text' ? ' selected' : ''; ?>>단답형</option>
<option value="textarea"<?php echo $question['stq_type'] == 'textarea' ? ' selected' : ''; ?>>장문형</option>
<option value="radio"<?php echo $question['stq_type'] == 'radio' ? ' selected' : ''; ?>>객관식(단일)</option>
<option value="checkbox"<?php echo $question['stq_type'] == 'checkbox' ? ' selected' : ''; ?>>객관식(다중)</option>
<option value="select"<?php echo $question['stq_type'] == 'select' ? ' selected' : ''; ?>>드롭다운</option>
<option value="rating"<?php echo $question['stq_type'] == 'rating' ? ' selected' : ''; ?>>평점</option>
<option value="date"<?php echo $question['stq_type'] == 'date' ? ' selected' : ''; ?>>날짜</option>
<option value="email"<?php echo $question['stq_type'] == 'email' ? ' selected' : ''; ?>>이메일</option>
<option value="number"<?php echo $question['stq_type'] == 'number' ? ' selected' : ''; ?>>숫자</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">질문 설명 (선택사항)</label>
<textarea name="questions[<?php echo $index; ?>][stq_description]" class="form-control" rows="2"
placeholder="질문에 대한 추가 설명"><?php echo htmlspecialchars($question['stq_description']); ?></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label>
<input type="checkbox" name="questions[<?php echo $index; ?>][stq_required]" value="1"
<?php echo $question['stq_required'] ? 'checked' : ''; ?>>
필수 질문
</label>
</div>
</div>
<?php if (in_array($question['stq_type'], ['radio', 'checkbox', 'select'])): ?>
<div class="options-container" id="optionsContainer<?php echo $index; ?>">
<label class="form-label">선택 옵션</label>
<?php
$options = json_decode($question['stq_options'], true);
if (is_array($options)):
foreach ($options as $opt_index => $option):
?>
<div class="option-item">
<input type="text" name="questions[<?php echo $index; ?>][options][]"
class="form-control option-input"
value="<?php echo htmlspecialchars($option); ?>"
placeholder="옵션을 입력하세요">
<button type="button" class="btn-sm btn-danger" onclick="removeOption(this)">
<i class="fa fa-times"></i>
</button>
</div>
<?php
endforeach;
endif;
?>
<button type="button" class="btn-add-option" onclick="addOption(<?php echo $index; ?>)">
<i class="fa fa-plus"></i> 옵션 추가
</button>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php else: ?>
<div class="empty-questions" id="emptyQuestions">
<i class="fa fa-question-circle"></i>
<h3>질문을 추가해주세요</h3>
<p>아래 버튼을 클릭하여 첫 번째 질문을 만들어보세요.</p>
</div>
<?php endif; ?>
</div>
<button type="button" class="btn-add-question" onclick="addQuestion()">
<i class="fa fa-plus"></i> 질문 추가
</button>
</div>
<div class="form-actions">
<button type="submit" class="btn-primary">
<i class="fa fa-save"></i> <?php echo $is_edit ? '템플릿 수정' : '템플릿 저장'; ?>
</button>
<a href="template_list.php" class="btn-primary btn-outline">
<i class="fa fa-times"></i> 취소
</a>
</div>
</form>
</div>
</div>
<script>
let questionIndex = <?php echo count($questions); ?>;
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 style="flex: 1;">질문 ${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" style="min-width: 150px;">
<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-header > div:nth-child(2)');
numberElement.textContent = index + 1;
titleElement.textContent = `질문 ${index + 1}`;
});
}
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) {
button.parentElement.remove();
}
// 폼 제출 전 검증
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;
}
});
</script>
<?php
include_once(G5_ADMIN_PATH.'/admin.tail.php');
?>
+110
View File
@@ -0,0 +1,110 @@
<?php
$sub_menu = '710300';
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "w");
$mode = isset($_POST['mode']) ? $_POST['mode'] : '';
$st_id = isset($_POST['st_id']) ? (int)$_POST['st_id'] : 0;
if (!$mode) {
alert('잘못된 접근입니다.');
}
// 기본 정보
$st_name = isset($_POST['st_name']) ? trim($_POST['st_name']) : '';
$st_description = isset($_POST['st_description']) ? trim($_POST['st_description']) : '';
$st_category = isset($_POST['st_category']) ? trim($_POST['st_category']) : '기타';
$st_is_public = isset($_POST['st_is_public']) ? (int)$_POST['st_is_public'] : 1;
if (!$st_name) {
alert('템플릿 이름을 입력해주세요.');
}
// 질문 데이터
$questions = isset($_POST['questions']) ? $_POST['questions'] : array();
if (empty($questions)) {
alert('최소 1개 이상의 질문을 추가해주세요.');
}
if ($mode == 'insert') {
// 새 템플릿 생성
$sql = "INSERT INTO survey_templates
(st_name, st_description, st_category, st_is_public, st_created_by, st_created_at)
VALUES
('".sql_real_escape_string($st_name)."',
'".sql_real_escape_string($st_description)."',
'".sql_real_escape_string($st_category)."',
'$st_is_public',
'{$member['mb_id']}',
NOW())";
sql_query($sql);
$st_id = sql_insert_id();
if (!$st_id) {
alert('템플릿 생성에 실패했습니다.');
}
} else if ($mode == 'update') {
// 기존 템플릿 수정
if (!$st_id) {
alert('템플릿 ID가 없습니다.');
}
$sql = "UPDATE survey_templates SET
st_name = '".sql_real_escape_string($st_name)."',
st_description = '".sql_real_escape_string($st_description)."',
st_category = '".sql_real_escape_string($st_category)."',
st_is_public = '$st_is_public',
st_updated_at = NOW()
WHERE st_id = '$st_id'";
sql_query($sql);
// 기존 질문들 삭제
sql_query("DELETE FROM survey_template_questions WHERE st_id = '$st_id'");
}
// 질문들 저장
foreach ($questions as $order => $question) {
$stq_title = isset($question['stq_title']) ? trim($question['stq_title']) : '';
$stq_description = isset($question['stq_description']) ? trim($question['stq_description']) : '';
$stq_type = isset($question['stq_type']) ? trim($question['stq_type']) : 'text';
$stq_required = isset($question['stq_required']) ? 1 : 0;
$stq_options = '';
if (!$stq_title) {
continue; // 제목이 없는 질문은 건너뛰기
}
// 옵션 처리 (객관식 질문인 경우)
if (in_array($stq_type, ['radio', 'checkbox', 'select']) && isset($question['options'])) {
$options = array_filter($question['options'], function($option) {
return trim($option) !== '';
});
if (!empty($options)) {
$stq_options = json_encode(array_values($options), JSON_UNESCAPED_UNICODE);
}
}
$sql = "INSERT INTO survey_template_questions
(st_id, stq_order, stq_type, stq_title, stq_description, stq_required, stq_options, stq_created_at)
VALUES
('$st_id',
'".($order + 1)."',
'".sql_real_escape_string($stq_type)."',
'".sql_real_escape_string($stq_title)."',
'".sql_real_escape_string($stq_description)."',
'$stq_required',
'".sql_real_escape_string($stq_options)."',
NOW())";
sql_query($sql);
}
$message = $mode == 'insert' ? '템플릿이 생성되었습니다.' : '템플릿이 수정되었습니다.';
alert($message, 'template_list.php');
?>
+395
View File
@@ -0,0 +1,395 @@
<?php
$sub_menu = '710300';
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "r");
$g5['title'] = '템플릿 관리';
include_once(G5_ADMIN_PATH.'/admin.head.php');
// 변수 초기화
$sfl = isset($_GET['sfl']) ? clean_xss_tags($_GET['sfl']) : 'st_name';
$stx = isset($_GET['stx']) ? clean_xss_tags($_GET['stx']) : '';
$category = isset($_GET['category']) ? clean_xss_tags($_GET['category']) : '';
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
// 검색 조건
$where = " WHERE st_is_public = 1 ";
$sql_search = "";
if ($stx) {
$where .= " AND $sfl LIKE '%".sql_real_escape_string($stx)."%' ";
$sql_search .= "&sfl=$sfl&stx=".urlencode($stx);
}
if ($category) {
$where .= " AND st_category = '".sql_real_escape_string($category)."' ";
$sql_search .= "&category=".urlencode($category);
}
// 페이징
$sql_common = " FROM survey_templates $where ";
$sql = " SELECT COUNT(*) as cnt $sql_common ";
$row = sql_fetch($sql);
$total_count = $row['cnt'];
$rows = 20;
$total_page = ceil($total_count / $rows);
if ($page < 1) $page = 1;
$from_record = ($page - 1) * $rows;
$sql = " SELECT * $sql_common ORDER BY st_created_at DESC LIMIT $from_record, $rows ";
$result = sql_query($sql);
// 카테고리별 카운트
$category_counts = array();
$category_sql = "SELECT st_category, COUNT(*) as cnt FROM survey_templates WHERE st_is_public = 1 GROUP BY st_category";
$category_result = sql_query($category_sql);
while ($category_row = sql_fetch_array($category_result)) {
$category_counts[$category_row['st_category']] = $category_row['cnt'];
}
?>
<style>
.template-header {
display: flex;
justify-content: between;
align-items: center;
margin-bottom: 20px;
padding: 20px;
background: linear-gradient(135deg, #AA20FF 0%, #8A1ACC 100%);
color: white;
border-radius: 8px;
}
.template-stats {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.stat-card {
flex: 1;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
border-left: 4px solid #AA20FF;
}
.stat-number {
font-size: 2em;
font-weight: bold;
color: #AA20FF;
margin-bottom: 5px;
}
.stat-label {
color: #666;
font-size: 0.9em;
}
.category-filter {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.category-btn {
padding: 8px 16px;
border: 1px solid #ddd;
background: white;
color: #666;
text-decoration: none;
border-radius: 20px;
font-size: 0.9em;
transition: all 0.3s;
}
.category-btn:hover,
.category-btn.active {
background: #AA20FF;
color: white;
border-color: #AA20FF;
}
.template-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.template-card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transition: all 0.3s ease;
border: 2px solid transparent;
}
.template-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
border-color: #AA20FF;
}
.template-card-header {
margin-bottom: 15px;
}
.template-title {
font-size: 1.3em;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.template-category {
display: inline-block;
padding: 4px 12px;
background: #e9ecef;
color: #666;
border-radius: 12px;
font-size: 0.8em;
font-weight: 500;
}
.template-description {
color: #666;
line-height: 1.5;
margin-bottom: 15px;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.template-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9em;
color: #888;
margin-bottom: 15px;
}
.template-actions {
display: flex;
gap: 10px;
}
.btn-template {
flex: 1;
padding: 10px 15px;
border: none;
border-radius: 6px;
font-size: 0.9em;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
text-align: center;
}
.btn-use {
background: #AA20FF;
color: white;
}
.btn-use:hover {
background: #8A1ACC;
color: white;
}
.btn-preview {
background: #fff;
color: #666;
border: 1px solid #e0e0e0;
}
.btn-preview:hover {
background: #e9ecef;
color: #333;
}
.search-form {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.search-form .form-row {
display: flex;
gap: 10px;
align-items: center;
}
.search-form select,
.search-form input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
.create-btn {
position: fixed;
bottom: 30px;
right: 30px;
width: 60px;
height: 60px;
background: #AA20FF;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
font-size: 1.5em;
box-shadow: 0 4px 12px rgba(170, 32, 255, 0.3);
transition: all 0.3s;
z-index: 1000;
}
.create-btn:hover {
background: #8A1ACC;
transform: scale(1.1);
color: white;
}
/* 반응형 */
@media (max-width: 768px) {
.template-grid {
grid-template-columns: 1fr;
}
.template-stats {
flex-direction: column;
}
.category-filter {
justify-content: center;
}
.search-form .form-row {
flex-direction: column;
}
}
</style>
<div class="template-header">
<div>
<h1><i class="fa fa-magic"></i> 템플릿 관리</h1>
<p>미리 만들어진 설문 템플릿을 관리하고 새로운 템플릿을 만들 수 있습니다</p>
</div>
</div>
<div class="template-stats">
<div class="stat-card">
<div class="stat-number"><?php echo number_format($category_counts['고객서비스'] ?? 0); ?></div>
<div class="stat-label">고객서비스</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo number_format($category_counts['마케팅'] ?? 0); ?></div>
<div class="stat-label">마케팅</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo number_format($category_counts['제품개발'] ?? 0); ?></div>
<div class="stat-label">제품개발</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo number_format($total_count); ?></div>
<div class="stat-label">전체 템플릿</div>
</div>
</div>
<div class="search-form">
<form method="get" class="form-row">
<select name="sfl">
<option value="st_name"<?php echo $sfl == 'st_name' ? ' selected' : ''; ?>>템플릿명</option>
<option value="st_description"<?php echo $sfl == 'st_description' ? ' selected' : ''; ?>>설명</option>
<option value="st_category"<?php echo $sfl == 'st_category' ? ' selected' : ''; ?>>카테고리</option>
</select>
<input type="text" name="stx" value="<?php echo $stx; ?>" placeholder="검색어를 입력하세요">
<button type="submit" class="btn-sm btn-primary">
<i class="fa fa-search"></i> 검색
</button>
<?php if ($stx): ?>
<a href="?" class="btn-sm btn-warning">
<i class="fa fa-times"></i> 초기화
</a>
<?php endif; ?>
</form>
</div>
<div class="category-filter">
<a href="?" class="category-btn <?php echo !isset($_GET['category']) ? 'active' : ''; ?>">
<i class="fa fa-list"></i> 전체
</a>
<a href="?category=고객서비스" class="category-btn <?php echo $category == '고객서비스' ? 'active' : ''; ?>">
<i class="fa fa-users"></i> 고객서비스 (<?php echo $category_counts['고객서비스'] ?? 0; ?>)
</a>
<a href="?category=마케팅" class="category-btn <?php echo $category == '마케팅' ? 'active' : ''; ?>">
<i class="fa fa-bullhorn"></i> 마케팅 (<?php echo $category_counts['마케팅'] ?? 0; ?>)
</a>
<a href="?category=제품개발" class="category-btn <?php echo $category == '제품개발' ? 'active' : ''; ?>">
<i class="fa fa-cogs"></i> 제품개발 (<?php echo $category_counts['제품개발'] ?? 0; ?>)
</a>
</div>
<?php if (sql_num_rows($result) > 0): ?>
<div class="template-grid">
<?php while ($template = sql_fetch_array($result)): ?>
<div class="template-card">
<div class="template-card-header">
<h3 class="template-title"><?php echo htmlspecialchars($template['st_name']); ?></h3>
<span class="template-category"><?php echo htmlspecialchars($template['st_category']); ?></span>
</div>
<p class="template-description"><?php echo htmlspecialchars($template['st_description']); ?></p>
<div class="template-meta">
<span><i class="fa fa-user"></i> <?php echo $template['st_created_by']; ?></span>
<span><i class="fa fa-calendar"></i> <?php echo date('Y-m-d', strtotime($template['st_created_at'])); ?></span>
</div>
<div class="template-actions">
<a href="survey_form.php?template_id=<?php echo $template['st_id']; ?>" class="btn-template btn-use">
<i class="fa fa-plus"></i> 사용하기
</a>
<a href="template_preview.php?st_id=<?php echo $template['st_id']; ?>" class="btn-template btn-preview">
<i class="fa fa-eye"></i> 미리보기
</a>
</div>
</div>
<?php endwhile; ?>
</div>
<?php
// 페이징
if ($total_page > 1) {
$paging = get_paging(10, $page, $total_page, "?$sql_search&page=");
echo '<div style="margin-top: 20px;">' . $paging . '</div>';
}
?>
<?php else: ?>
<div style="text-align: center; padding: 80px 20px; color: #666;">
<i class="fa fa-magic" style="font-size: 4em; margin-bottom: 20px; display: block; opacity: 0.3;"></i>
<h3>등록된 템플릿이 없습니다</h3>
<p>새로운 템플릿을 만들어보세요.</p>
</div>
<?php endif; ?>
<a href="template_form.php" class="create-btn" title="새 템플릿 만들기">
<i class="fa fa-plus"></i>
</a>
<?php
include_once(G5_ADMIN_PATH.'/admin.tail.php');
?>
+165
View File
@@ -0,0 +1,165 @@
<?php
$sub_menu = '710300';
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "r");
$g5['title'] = '템플릿 관리';
include_once(G5_ADMIN_PATH.'/admin.head.php');
// 변수 초기화
$sfl = isset($_GET['sfl']) ? clean_xss_tags($_GET['sfl']) : 'st_name';
$stx = isset($_GET['stx']) ? clean_xss_tags($_GET['stx']) : '';
$category = isset($_GET['category']) ? clean_xss_tags($_GET['category']) : '';
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
// 검색 조건
$where = " WHERE st_is_public = 1 ";
$sql_search = "";
if ($stx) {
$where .= " AND $sfl LIKE '%".sql_real_escape_string($stx)."%' ";
$sql_search .= "&sfl=$sfl&stx=".urlencode($stx);
}
if ($category) {
$where .= " AND st_category = '".sql_real_escape_string($category)."' ";
$sql_search .= "&category=".urlencode($category);
}
// 페이징
$sql_common = " FROM survey_templates $where ";
$sql = " SELECT COUNT(*) as cnt $sql_common ";
$row = sql_fetch($sql);
$total_count = $row['cnt'];
$rows = 20;
$total_page = ceil($total_count / $rows);
if ($page < 1) $page = 1;
$from_record = ($page - 1) * $rows;
$sql = " SELECT * $sql_common ORDER BY st_created_at DESC LIMIT $from_record, $rows ";
$result = sql_query($sql);
// 카테고리별 카운트
$category_counts = [];
$category_sql = "SELECT st_category, COUNT(*) as cnt FROM survey_templates WHERE st_is_public = 1 GROUP BY st_category";
$category_result = sql_query($category_sql);
while ($category_row = sql_fetch_array($category_result)) {
$category_counts[$category_row['st_category']] = $category_row['cnt'];
}
?>
<!-- CSS는 template_list.css 파일에서 자동 로드됩니다 -->
<div class="template-header">
<div>
<h1><i class="fa fa-magic"></i> 템플릿 관리</h1>
<p>미리 만들어진 설문 템플릿을 관리하고 새로운 템플릿을 만들 수 있습니다</p>
</div>
</div>
<div class="template-stats">
<div class="stat-card">
<div class="stat-number"><?php echo number_format($category_counts['고객서비스'] ?? 0); ?></div>
<div class="stat-label">고객서비스</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo number_format($category_counts['마케팅'] ?? 0); ?></div>
<div class="stat-label">마케팅</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo number_format($category_counts['제품개발'] ?? 0); ?></div>
<div class="stat-label">제품개발</div>
</div>
<div class="stat-card">
<div class="stat-number"><?php echo number_format($total_count); ?></div>
<div class="stat-label">전체 템플릿</div>
</div>
</div>
<div class="search-form">
<form method="get" class="form-row">
<select name="sfl">
<option value="st_name"<?php echo $sfl == 'st_name' ? ' selected' : ''; ?>>템플릿명</option>
<option value="st_description"<?php echo $sfl == 'st_description' ? ' selected' : ''; ?>>설명</option>
<option value="st_category"<?php echo $sfl == 'st_category' ? ' selected' : ''; ?>>카테고리</option>
</select>
<input type="text" name="stx" value="<?php echo $stx; ?>" placeholder="검색어를 입력하세요">
<button type="submit" class="btn-sm btn-primary">
<i class="fa fa-search"></i> 검색
</button>
<?php if ($stx): ?>
<a href="?" class="btn-sm btn-warning">
<i class="fa fa-times"></i> 초기화
</a>
<?php endif; ?>
</form>
</div>
<div class="category-filter">
<a href="?" class="category-btn <?php echo !isset($_GET['category']) ? 'active' : ''; ?>">
<i class="fa fa-list"></i> 전체
</a>
<a href="?category=고객서비스" class="category-btn <?php echo $category == '고객서비스' ? 'active' : ''; ?>">
<i class="fa fa-users"></i> 고객서비스 (<?php echo $category_counts['고객서비스'] ?? 0; ?>)
</a>
<a href="?category=마케팅" class="category-btn <?php echo $category == '마케팅' ? 'active' : ''; ?>">
<i class="fa fa-bullhorn"></i> 마케팅 (<?php echo $category_counts['마케팅'] ?? 0; ?>)
</a>
<a href="?category=제품개발" class="category-btn <?php echo $category == '제품개발' ? 'active' : ''; ?>">
<i class="fa fa-cogs"></i> 제품개발 (<?php echo $category_counts['제품개발'] ?? 0; ?>)
</a>
</div>
<?php if (sql_num_rows($result) > 0): ?>
<div class="template-grid">
<?php while ($template = sql_fetch_array($result)): ?>
<div class="template-card">
<div class="template-card-header">
<h3 class="template-title"><?php echo htmlspecialchars($template['st_name']); ?></h3>
<span class="template-category"><?php echo htmlspecialchars($template['st_category']); ?></span>
</div>
<p class="template-description"><?php echo htmlspecialchars($template['st_description']); ?></p>
<div class="template-meta">
<span><i class="fa fa-user"></i> <?php echo $template['st_created_by']; ?></span>
<span><i class="fa fa-calendar"></i> <?php echo date('Y-m-d', strtotime($template['st_created_at'])); ?></span>
</div>
<div class="template-actions">
<a href="survey_form.php?template_id=<?php echo $template['st_id']; ?>" class="btn-template btn-use">
<i class="fa fa-plus"></i> 사용하기
</a>
<a href="template_preview.php?st_id=<?php echo $template['st_id']; ?>" class="btn-template btn-preview">
<i class="fa fa-eye"></i> 미리보기
</a>
</div>
</div>
<?php endwhile; ?>
</div>
<?php
// 페이징
if ($total_page > 1) {
$paging = get_paging(10, $page, $total_page, "?$sql_search&page=");
echo '<div class="pagination-wrapper">' . $paging . '</div>';
}
?>
<?php else: ?>
<div class="empty-state">
<i class="fa fa-magic"></i>
<h3>등록된 템플릿이 없습니다</h3>
<p>새로운 템플릿을 만들어보세요.</p>
</div>
<?php endif; ?>
<a href="template_form.php" class="create-btn" title="새 템플릿 만들기">
<i class="fa fa-plus"></i>
</a>
<?php
include_once(G5_ADMIN_PATH.'/admin.tail.php');
?>
+370
View File
@@ -0,0 +1,370 @@
<?php
$sub_menu = '710300';
include_once('./_common.php');
auth_check_menu($auth, $sub_menu, "r");
$st_id = isset($_GET['st_id']) ? (int)$_GET['st_id'] : 0;
if (!$st_id) {
alert('템플릿을 선택해주세요.', 'template_list.php');
}
// 템플릿 정보 가져오기
$template = sql_fetch("SELECT * FROM survey_templates WHERE st_id = '$st_id' AND st_is_public = 1");
if (!$template) {
alert('존재하지 않는 템플릿입니다.', 'template_list.php');
}
// 템플릿 질문들 가져오기
$questions = array();
$question_sql = "SELECT * FROM survey_template_questions WHERE st_id = '$st_id' ORDER BY stq_order ASC";
$question_result = sql_query($question_sql);
while ($question = sql_fetch_array($question_result)) {
$questions[] = $question;
}
$g5['title'] = '템플릿 미리보기 - ' . $template['st_name'];
include_once(G5_ADMIN_PATH.'/admin.head.php');
?>
<style>
.preview-container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
overflow: hidden;
}
.preview-header {
background: linear-gradient(135deg, #AA20FF 0%, #8A1ACC 100%);
color: white;
padding: 30px;
text-align: center;
}
.preview-title {
font-size: 2em;
font-weight: 600;
margin-bottom: 10px;
}
.preview-description {
font-size: 1.1em;
opacity: 0.9;
line-height: 1.5;
}
.preview-meta {
background: #fff;
padding: 20px 30px;
border-bottom: 1px solid #e9ecef;
}
.meta-row {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.meta-row:last-child {
margin-bottom: 0;
}
.meta-label {
font-weight: 600;
color: #666;
}
.meta-value {
color: #333;
}
.preview-content {
padding: 30px;
}
.question-item {
margin-bottom: 30px;
padding: 25px;
background: #fff;
border-radius: 8px;
border-left: 4px solid #AA20FF;
}
.question-header {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.question-number {
background: #AA20FF;
color: white;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
margin-right: 15px;
font-size: 0.9em;
}
.question-title {
font-size: 1.2em;
font-weight: 600;
color: #333;
flex: 1;
}
.question-required {
color: #dc3545;
font-size: 0.9em;
margin-left: 10px;
}
.question-description {
color: #666;
margin-bottom: 15px;
line-height: 1.5;
}
.question-type {
display: inline-block;
background: #e9ecef;
color: #666;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.8em;
font-weight: 500;
margin-bottom: 15px;
}
.question-options {
margin-top: 15px;
}
.option-item {
display: flex;
align-items: center;
margin-bottom: 8px;
padding: 8px 12px;
background: white;
border-radius: 4px;
border: 1px solid #e0e0e0;
}
.option-input {
margin-right: 10px;
}
.option-text {
color: #333;
}
.preview-actions {
padding: 30px;
background: #fff;
text-align: center;
border-top: 1px solid #e9ecef;
}
.btn-action {
display: inline-block;
padding: 12px 30px;
margin: 0 10px;
border-radius: 6px;
text-decoration: none;
font-weight: 500;
transition: all 0.3s;
}
.btn-primary {
background: #AA20FF;
color: white;
}
.btn-primary:hover {
background: #8A1ACC;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #545b62;
color: white;
}
.empty-state {
text-align: center;
padding: 60px 30px;
color: #666;
}
.empty-state i {
font-size: 3em;
margin-bottom: 20px;
opacity: 0.3;
}
/* 반응형 */
@media (max-width: 768px) {
.preview-container {
margin: 10px;
border-radius: 8px;
}
.preview-header {
padding: 20px;
}
.preview-title {
font-size: 1.5em;
}
.preview-content {
padding: 20px;
}
.question-item {
padding: 20px;
}
.meta-row {
flex-direction: column;
gap: 5px;
}
}
</style>
<div class="preview-container">
<div class="preview-header">
<h1 class="preview-title"><?php echo htmlspecialchars($template['st_name']); ?></h1>
<?php if ($template['st_description']): ?>
<p class="preview-description"><?php echo nl2br(htmlspecialchars($template['st_description'])); ?></p>
<?php endif; ?>
</div>
<div class="preview-meta">
<div class="meta-row">
<span class="meta-label">카테고리</span>
<span class="meta-value"><?php echo htmlspecialchars($template['st_category']); ?></span>
</div>
<div class="meta-row">
<span class="meta-label">작성자</span>
<span class="meta-value"><?php echo htmlspecialchars($template['st_created_by']); ?></span>
</div>
<div class="meta-row">
<span class="meta-label">생성일</span>
<span class="meta-value"><?php echo date('Y년 m월 d일', strtotime($template['st_created_at'])); ?></span>
</div>
<div class="meta-row">
<span class="meta-label">질문 수</span>
<span class="meta-value"><?php echo count($questions); ?>개</span>
</div>
</div>
<div class="preview-content">
<?php if (count($questions) > 0): ?>
<?php foreach ($questions as $index => $question): ?>
<div class="question-item">
<div class="question-header">
<div class="question-number"><?php echo $index + 1; ?></div>
<div class="question-title">
<?php echo htmlspecialchars($question['stq_title']); ?>
<?php if ($question['stq_required']): ?>
<span class="question-required">*</span>
<?php endif; ?>
</div>
</div>
<?php
$type_names = array(
'text' => '단답형',
'textarea' => '장문형',
'radio' => '객관식 (단일선택)',
'checkbox' => '객관식 (다중선택)',
'select' => '드롭다운',
'rating' => '평점',
'date' => '날짜',
'email' => '이메일',
'number' => '숫자'
);
?>
<div class="question-type">
<i class="fa fa-tag"></i> <?php echo $type_names[$question['stq_type']] ?? $question['stq_type']; ?>
</div>
<?php if ($question['stq_description']): ?>
<div class="question-description">
<?php echo nl2br(htmlspecialchars($question['stq_description'])); ?>
</div>
<?php endif; ?>
<?php if (in_array($question['stq_type'], ['radio', 'checkbox', 'select']) && $question['stq_options']): ?>
<div class="question-options">
<?php
$options = json_decode($question['stq_options'], true);
if (is_array($options)):
foreach ($options as $option):
?>
<div class="option-item">
<input type="<?php echo $question['stq_type'] == 'checkbox' ? 'checkbox' : 'radio'; ?>"
class="option-input" disabled>
<span class="option-text"><?php echo htmlspecialchars($option); ?></span>
</div>
<?php
endforeach;
endif;
?>
</div>
<?php elseif ($question['stq_type'] == 'rating'): ?>
<div class="question-options">
<div class="option-item">
<?php for ($i = 1; $i <= 5; $i++): ?>
<i class="fa fa-star-o" style="color: #ddd; margin-right: 5px;"></i>
<?php endfor; ?>
<span class="option-text">1-5점 평점</span>
</div>
</div>
<?php elseif ($question['stq_type'] == 'text'): ?>
<div class="question-options">
<input type="text" class="form-control" placeholder="답변을 입력하세요" disabled style="background: #fff;">
</div>
<?php elseif ($question['stq_type'] == 'textarea'): ?>
<div class="question-options">
<textarea class="form-control" rows="3" placeholder="답변을 입력하세요" disabled style="background: #fff;"></textarea>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php else: ?>
<div class="empty-state">
<i class="fa fa-question-circle"></i>
<h3>질문이 없습니다</h3>
<p>이 템플릿에는 아직 질문이 등록되지 않았습니다.</p>
</div>
<?php endif; ?>
</div>
<div class="preview-actions">
<a href="survey_form.php?template_id=<?php echo $template['st_id']; ?>" class="btn-action btn-primary">
<i class="fa fa-plus"></i> 이 템플릿으로 설문 만들기
</a>
<a href="template_list.php" class="btn-action btn-secondary">
<i class="fa fa-list"></i> 템플릿 목록으로
</a>
</div>
</div>
<?php
include_once(G5_ADMIN_PATH.'/admin.tail.php');
?>