first commit 2

This commit is contained in:
hmw1001
2026-06-11 18:47:38 +09:00
parent c768729ce6
commit 6f534e33a6
11095 changed files with 1595758 additions and 0 deletions
@@ -0,0 +1,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);
}