GNU/_PAGE/structure/mysql/SQL_tree.php
<?php
// db_tree_db_toggle.php
// DB -> TABLE -> COLUMN
// DB 클릭 : 테이블 전체 토글
// TABLE 클릭 : 컬럼 토글
// phpmyadmin DB 제외

date_default_timezone_set('Asia/Seoul');
header('Content-Type: text/html; charset=UTF-8');

require_once '/home/www/DB/db_upbit.php';
if (!isset($db_upbit)) die('DB 핸들러 없음');
$pdo = $db_upbit;

$sql = "
SELECT
    t.table_schema,
    t.table_name,
    t.table_comment,
    c.column_name,
    c.column_type,
    c.column_comment,
    c.ordinal_position
FROM information_schema.tables t
LEFT JOIN information_schema.columns c
    ON t.table_schema = c.table_schema
   AND t.table_name   = c.table_name
WHERE t.table_schema NOT IN (
    'information_schema',
    'mysql',
    'performance_schema',
    'sys',
    'phpmyadmin'
)
ORDER BY
    t.table_schema,
    t.table_name,
    c.ordinal_position
";

$rows = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);

// 트리 구성
$tree = [];
foreach ($rows as $r) {
    $db = $r['table_schema'];
    $tb = $r['table_name'];

    if (!isset($tree[$db])) $tree[$db] = [];
    if (!isset($tree[$db][$tb])) {
        $tree[$db][$tb] = [
            'comment' => $r['table_comment'],
            'columns' => []
        ];
    }
    if ($r['column_name']) {
        $tree[$db][$tb]['columns'][] = [
            'name'    => $r['column_name'],
            'type'    => $r['column_type'],
            'comment' => $r['column_comment'],
        ];
    }
}

// ========== [신규 기능 추가 시작] ==========

// PK/INDEX 정보 조회
$pkIndexSql = "
SELECT
    k.table_schema,
    k.table_name,
    k.column_name,
    k.constraint_name,
    k.ordinal_position
FROM information_schema.key_column_usage k
WHERE k.table_schema NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys', 'phpmyadmin')
ORDER BY k.table_schema, k.table_name, k.ordinal_position
";
$pkIndexRows = $pdo->query($pkIndexSql)->fetchAll(PDO::FETCH_ASSOC);
$pkIndexMap = [];
foreach ($pkIndexRows as $r) {
    $key = $r['table_schema'] . '.' . $r['table_name'] . '.' . $r['column_name'];
    if (!isset($pkIndexMap[$key])) $pkIndexMap[$key] = [];
    $pkIndexMap[$key][] = [
        'constraint' => $r['constraint_name'],
        'is_pk' => ($r['constraint_name'] === 'PRIMARY')
    ];
}

// 테이블 ROW 수/용량 조회
$tableStatsSql = "
SELECT
    t.table_schema,
    t.table_name,
    t.table_rows,
    ROUND((t.data_length + t.index_length) / 1024 / 1024, 2) AS size_mb,
    t.update_time,
    t.create_time
FROM information_schema.tables t
WHERE t.table_schema NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys', 'phpmyadmin')
AND t.table_type = 'BASE TABLE'
";
$tableStatsRows = $pdo->query($tableStatsSql)->fetchAll(PDO::FETCH_ASSOC);
$tableStatsMap = [];
foreach ($tableStatsRows as $r) {
    $key = $r['table_schema'] . '.' . $r['table_name'];
    $tableStatsMap[$key] = [
        'rows' => $r['table_rows'],
        'size_mb' => $r['size_mb'],
        'update_time' => $r['update_time'],
        'create_time' => $r['create_time']
    ];
}

// CREATE TABLE SQL 조회용
$createTableMap = [];
foreach ($tree as $dbName => $tables) {
    foreach ($tables as $tableName => $t) {
        try {
            $createSql = "SHOW CREATE TABLE `{$dbName}`.`{$tableName}`";
            $createRow = $pdo->query($createSql)->fetch(PDO::FETCH_ASSOC);
            if ($createRow && isset($createRow['Create Table'])) {
                $createTableMap[$dbName . '.' . $tableName] = $createRow['Create Table'];
            }
        } catch (Exception $e) {
            // 무시
        }
    }
}

// 데몬 사용 테이블 스캔
$daemonTables = [];
$daemonColumnMap = []; // 데몬별 사용 컬럼 매핑
$daemonPaths = [
    '/home/www/DATA/UPBIT/daemon',
    '/home/www/admin/_cron',
    '/home/www/admin/_daemon'
];
foreach ($daemonPaths as $path) {
    if (is_dir($path)) {
        $files = glob($path . '/daemon_*.php');
        foreach ($files as $file) {
            $daemonName = basename($file);
            $content = @file_get_contents($file);
            if ($content) {
                // 테이블명 패턴 찾기
                preg_match_all('/(?:INSERT\s+INTO|FROM|UPDATE|INTO)\s+[`"]?(\w+)[`"]?/i', $content, $matches);
                foreach ($matches[1] as $table) {
                    if (strpos($table, 'daemon') !== false || strlen($table) > 5) {
                        $daemonTables[$table] = true;
                        if (!isset($daemonColumnMap[$table])) $daemonColumnMap[$table] = [];
                        if (!isset($daemonColumnMap[$table][$daemonName])) $daemonColumnMap[$table][$daemonName] = [];
                        // 컬럼명 패턴 찾기
                        preg_match_all('/[`"]?(\w+)[`"]?\s*[,\)]/i', $content, $colMatches);
                        foreach ($colMatches[1] as $col) {
                            if (strlen($col) > 2 && !in_array(strtoupper($col), ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'FROM', 'INTO', 'SET', 'WHERE', 'VALUES'])) {
                                $daemonColumnMap[$table][$daemonName][] = $col;
                            }
                        }
                    }
                }
            }
        }
    }
}

// DB 접근 권한 조회
$dbPrivilegesSql = "
SELECT
    table_schema,
    COUNT(DISTINCT table_name) as table_count
FROM information_schema.tables
WHERE table_schema NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys', 'phpmyadmin')
GROUP BY table_schema
";
$dbPrivileges = $pdo->query($dbPrivilegesSql)->fetchAll(PDO::FETCH_ASSOC);
$dbPrivilegesMap = [];
foreach ($dbPrivileges as $r) {
    $dbPrivilegesMap[$r['table_schema']] = $r['table_count'];
}

// ========== [신규: 탐색·분석 기능 추가 시작] ==========

// 컬럼 사용 빈도 계산 (전체 DB에서 동일 컬럼명 출현 횟수)
$columnFrequencyMap = [];
foreach ($tree as $dbName => $tables) {
    foreach ($tables as $tableName => $t) {
        foreach ($t['columns'] as $col) {
            $colName = strtolower($col['name']);
            if (!isset($columnFrequencyMap[$colName])) {
                $columnFrequencyMap[$colName] = 0;
            }
            $columnFrequencyMap[$colName]++;
        }
    }
}

// 컬럼명 유사도 그룹핑 (Levenshtein 기반)
$columnSimilarityGroups = [];
$allColumnNames = array_keys($columnFrequencyMap);
foreach ($allColumnNames as $i => $col1) {
    $group = [$col1];
    foreach ($allColumnNames as $j => $col2) {
        if ($i < $j) {
            $similarity = 1 - (levenshtein($col1, $col2) / max(strlen($col1), strlen($col2)));
            if ($similarity > 0.7 && $similarity < 1.0) {
                $group[] = $col2;
            }
        }
    }
    if (count($group) > 1) {
        $columnSimilarityGroups[] = $group;
    }
}

// 테이블 간 참조 추정 (이름 기반)
$tableReferences = [];
foreach ($tree as $dbName => $tables) {
    foreach ($tables as $tableName => $t) {
        $refs = [];
        // 외래키 패턴: table_id, table_name 등
        foreach ($t['columns'] as $col) {
            $colName = strtolower($col['name']);
            if (preg_match('/(\w+)_id$/', $colName, $m)) {
                $refTable = $m[1];
                // 다른 테이블에 존재하는지 확인
                foreach ($tree as $db2 => $tables2) {
                    foreach ($tables2 as $tb2 => $t2) {
                        if (strtolower($tb2) === $refTable || strtolower($tb2) . 's' === $refTable) {
                            $refs[] = $db2 . '.' . $tb2;
                        }
                    }
                }
            }
        }
        if (!empty($refs)) {
            $tableReferences[$dbName . '.' . $tableName] = $refs;
        }
    }
}

// ========== [신규: 구조 이해 기능 추가 시작] ==========

// 컬럼 상세 정보 조회 (NULL, 기본값 등)
$columnDetailSql = "
SELECT
    table_schema,
    table_name,
    column_name,
    is_nullable,
    column_default,
    extra,
    column_key
FROM information_schema.columns
WHERE table_schema NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys', 'phpmyadmin')
";
$columnDetailRows = $pdo->query($columnDetailSql)->fetchAll(PDO::FETCH_ASSOC);
$columnDetailMap = [];
foreach ($columnDetailRows as $r) {
    $key = $r['table_schema'] . '.' . $r['table_name'] . '.' . $r['column_name'];
    $columnDetailMap[$key] = [
        'is_nullable' => $r['is_nullable'],
        'column_default' => $r['column_default'],
        'extra' => $r['extra'],
        'column_key' => $r['column_key']
    ];
}

// 테이블 역할 태그 자동 분류
$tableRoleTags = [];
foreach ($tree as $dbName => $tables) {
    foreach ($tables as $tableName => $t) {
        $tags = [];
        $tbLower = strtolower($tableName);
        $commentLower = strtolower($t['comment']);
        
        // 로그 테이블
        if (strpos($tbLower, 'log') !== false || strpos($tbLower, '_log') !== false || 
            strpos($commentLower, '로그') !== false || strpos($commentLower, 'log') !== false) {
            $tags[] = 'LOG';
        }
        // 데몬 테이블
        if (strpos($tbLower, 'daemon') !== false || isset($daemonTables[$tableName])) {
            $tags[] = 'DAEMON';
        }
        // 정산 테이블
        if (strpos($tbLower, 'settle') !== false || strpos($tbLower, 'calc') !== false ||
            strpos($commentLower, '정산') !== false) {
            $tags[] = 'SETTLE';
        }
        // 임시 테이블
        if (strpos($tbLower, 'temp') !== false || strpos($tbLower, 'tmp') !== false ||
            strpos($commentLower, '임시') !== false || strpos($commentLower, 'temp') !== false) {
            $tags[] = 'TEMP';
        }
        
        if (!empty($tags)) {
            $tableRoleTags[$dbName . '.' . $tableName] = $tags;
        }
    }
}

// 네이밍 규칙 위반 감지
$namingViolations = [];
$namingPatterns = [
    'snake_case' => '/^[a-z][a-z0-9_]*$/',
    'camelCase' => '/^[a-z][a-zA-Z0-9]*$/',
    'PascalCase' => '/^[A-Z][a-zA-Z0-9]*$/'
];
foreach ($tree as $dbName => $tables) {
    foreach ($tables as $tableName => $t) {
        $violations = [];
        // 테이블명 체크
        if (!preg_match($namingPatterns['snake_case'], strtolower($tableName))) {
            $violations[] = 'TABLE_NAME';
        }
        // 컬럼명 체크
        foreach ($t['columns'] as $col) {
            if (!preg_match($namingPatterns['snake_case'], strtolower($col['name']))) {
                $violations[] = 'COLUMN_' . $col['name'];
            }
        }
        if (!empty($violations)) {
            $namingViolations[$dbName . '.' . $tableName] = $violations;
        }
    }
}

// 타입 이상치 감지
$typeAnomalies = [];
foreach ($tree as $dbName => $tables) {
    foreach ($tables as $tableName => $t) {
        $anomalies = [];
        foreach ($t['columns'] as $col) {
            $typeLower = strtolower($col['type']);
            // varchar 남용 (255 초과)
            if (preg_match('/varchar\((\d+)\)/', $typeLower, $m)) {
                $len = intval($m[1]);
                if ($len > 255) {
                    $anomalies[] = ['type' => 'VARCHAR_OVERUSE', 'column' => $col['name'], 'value' => $len];
                }
            }
            // text 타입이 아닌데 큰 길이
            if (preg_match('/varchar\((\d+)\)/', $typeLower, $m)) {
                $len = intval($m[1]);
                if ($len > 500) {
                    $anomalies[] = ['type' => 'SHOULD_BE_TEXT', 'column' => $col['name'], 'value' => $len];
                }
            }
        }
        if (!empty($anomalies)) {
            $typeAnomalies[$dbName . '.' . $tableName] = $anomalies;
        }
    }
}

// 실시간 증가 테이블 감지 (최근 업데이트 + 큰 ROW 수)
$growingTables = [];
foreach ($tableStatsMap as $key => $stats) {
    if ($stats['rows'] > 10000 && $stats['update_time']) {
        $updateTime = strtotime($stats['update_time']);
        if ((time() - $updateTime) < 3600) { // 1시간 이내 업데이트
            $growingTables[$key] = $stats;
        }
    }
}

// 삭제 예정 테이블 마킹 (deprecated, old, backup 등 포함)
$deprecatedTables = [];
foreach ($tree as $dbName => $tables) {
    foreach ($tables as $tableName => $t) {
        $tbLower = strtolower($tableName);
        $commentLower = strtolower($t['comment']);
        if (strpos($tbLower, 'deprecated') !== false || strpos($tbLower, '_old') !== false ||
            strpos($tbLower, '_backup') !== false || strpos($commentLower, '삭제') !== false ||
            strpos($commentLower, 'deprecated') !== false) {
            $deprecatedTables[$dbName . '.' . $tableName] = true;
        }
    }
}

// 백업 제외 테이블 (임시, 로그 등)
$backupExcludedTables = [];
foreach ($tableRoleTags as $key => $tags) {
    if (in_array('TEMP', $tags) || in_array('LOG', $tags)) {
        $backupExcludedTables[$key] = true;
    }
}

// ========== [신규: 안정성 체크 추가 시작] ==========

// 인덱스 누락 경고 (자주 검색될 만한 컬럼에 인덱스 없음)
$missingIndexWarnings = [];
foreach ($tree as $dbName => $tables) {
    foreach ($tables as $tableName => $t) {
        $warnings = [];
        foreach ($t['columns'] as $col) {
            $colName = strtolower($col['name']);
            $colKey = $dbName . '.' . $tableName . '.' . $col['name'];
            // _id, _name, _date 등 패턴에 인덱스 없으면 경고
            if ((strpos($colName, '_id') !== false || strpos($colName, '_name') !== false || 
                 strpos($colName, '_date') !== false || strpos($colName, 'timestamp') !== false) &&
                !isset($pkIndexMap[$colKey])) {
                $warnings[] = $col['name'];
            }
        }
        if (!empty($warnings)) {
            $missingIndexWarnings[$dbName . '.' . $tableName] = $warnings;
        }
    }
}

// bigint/int 오버플로우 위험
$overflowRisks = [];
foreach ($tree as $dbName => $tables) {
    foreach ($tables as $tableName => $t) {
        $risks = [];
        foreach ($t['columns'] as $col) {
            $typeLower = strtolower($col['type']);
            // int 타입인데 큰 값이 들어갈 가능성
            if (strpos($typeLower, 'int') !== false && strpos($typeLower, 'bigint') === false) {
                $colName = strtolower($col['name']);
                if (strpos($colName, 'count') !== false || strpos($colName, 'total') !== false ||
                    strpos($colName, 'amount') !== false) {
                    $risks[] = $col['name'];
                }
            }
        }
        if (!empty($risks)) {
            $overflowRisks[$dbName . '.' . $tableName] = $risks;
        }
    }
}

// timestamp 표준 위반 (datetime 대신 timestamp 미사용 등)
$timestampViolations = [];
foreach ($tree as $dbName => $tables) {
    foreach ($tables as $tableName => $t) {
        $violations = [];
        foreach ($t['columns'] as $col) {
            $typeLower = strtolower($col['type']);
            $colName = strtolower($col['name']);
            // created_at, updated_at 등은 timestamp 권장
            if ((strpos($colName, 'created_at') !== false || strpos($colName, 'updated_at') !== false ||
                 strpos($colName, 'timestamp') !== false) && strpos($typeLower, 'timestamp') === false) {
                $violations[] = $col['name'];
            }
        }
        if (!empty($violations)) {
            $timestampViolations[$dbName . '.' . $tableName] = $violations;
        }
    }
}

// 중복 의미 컬럼 경고 (유사한 이름의 컬럼)
$duplicateMeaningColumns = [];
foreach ($tree as $dbName => $tables) {
    foreach ($tables as $tableName => $t) {
        $duplicates = [];
        $colNames = array_map(function($c) { return strtolower($c['name']); }, $t['columns']);
        foreach ($colNames as $i => $name1) {
            foreach ($colNames as $j => $name2) {
                if ($i < $j) {
                    $similarity = 1 - (levenshtein($name1, $name2) / max(strlen($name1), strlen($name2)));
                    if ($similarity > 0.8 && $similarity < 1.0) {
                        $duplicates[] = [$t['columns'][$i]['name'], $t['columns'][$j]['name']];
                    }
                }
            }
        }
        if (!empty($duplicates)) {
            $duplicateMeaningColumns[$dbName . '.' . $tableName] = $duplicates;
        }
    }
}

// ========== [신규 기능 추가 끝] ==========

require_once '/home/www/GNU/_PAGE/head.php';
?>


<title>DB TREE TOGGLE</title>
<style>
body{
    margin:0;
    background:#0b0f16;
    color:#e6edf6;
    font-family:system-ui,-apple-system,"Noto Sans KR",sans-serif;
    font-size:14px;
}
.MySQL { padding:50px; }
.Db_title { font-size: 30px; padding: 20px; color: #fb7185; }
.Db_title i { color: #5fe266; }
.db{margin-bottom:22px;}
.db-head{
    display:flex;
    gap:8px;
    font-size:16px;
    font-weight:800;
    color:#9fc5ff;
    cursor:pointer;
    user-select:none;
}
.db-toggle{width:14px;color:#89a0bb;}
.tables{
    margin-top:8px;
    padding-left:14px;
    display:none; /* DB 기본 접힘 */
}
.table{margin-bottom:10px;}
.table-head{
    display:flex;
    gap:10px;
    cursor:pointer;
    user-select:none;
}
.table-toggle{width:14px;color:#89a0bb;}
.table-name{
    min-width:260px;
    font-weight:700;
    color:#ffffff;
}
.table-comment{color:#9aa9bc;}
.no-comment{color:#ff9b9b;}
.columns{
    display:none; /* 컬럼 기본 숨김 */
    padding-left:18px;
    margin-top:6px;
}
.col-row{
    display:flex;
    gap:10px;
    line-height:1.6;
}
.col-name{min-width:220px;color:#d7e6ff;}
.col-type{min-width:180px;color:#89a0bb;}
.col-comment{color:#9aa9bc;}

/* ========== [신규 스타일 추가 시작] ========== */

/* 검색/필터 UI */
.search-panel {
    background: #1a1f2e;
    padding: 16px;
    margin-bottom: 20px;
    border-radius: 8px;
    border: 1px solid #2a3441;
}
.search-row {
    display: flex;
    gap: 12px;
    align-items: center;
    margin-bottom: 12px;
    flex-wrap: wrap;
}
.search-input {
    flex: 1;
    min-width: 300px;
    padding: 8px 12px;
    background: #0b0f16;
    border: 1px solid #2a3441;
    color: #e6edf6;
    border-radius: 4px;
    font-size: 14px;
}
.search-input:focus {
    outline: none;
    border-color: #9fc5ff;
}
.search-options {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
}
.search-option {
    display: flex;
    align-items: center;
    gap: 6px;
}
.search-option input[type="checkbox"],
.search-option input[type="radio"] {
    cursor: pointer;
}
.search-option label {
    cursor: pointer;
    user-select: none;
    font-size: 13px;
}
.search-btn {
    padding: 8px 16px;
    background: #2563eb;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
}
.search-btn:hover {
    background: #1d4ed8;
}
.control-panel {
    display: flex;
    gap: 8px;
    margin-bottom: 16px;
    flex-wrap: wrap;
    position: sticky;
    top: 0;
    background: #0b0f16;
    padding: 12px;
    z-index: 100;
    border-bottom: 2px solid #2a3441;
}
.control-btn {
    padding: 6px 12px;
    background: #1a1f2e;
    color: #e6edf6;
    border: 1px solid #2a3441;
    border-radius: 4px;
    cursor: pointer;
    font-size: 13px;
}
.control-btn:hover {
    background: #2a3441;
}

/* 하이라이트 */
.highlight {
    background: #fbbf24;
    color: #000;
    padding: 2px 4px;
    border-radius: 2px;
    font-weight: bold;
}

/* 컬럼 타입별 색상 */
.col-type-int { color: #60a5fa; }
.col-type-varchar { color: #34d399; }
.col-type-text { color: #a78bfa; }
.col-type-datetime { color: #fbbf24; }
.col-type-decimal { color: #fb7185; }
.col-type-enum { color: #f472b6; }
.col-type-other { color: #89a0bb; }

/* PK/INDEX 강조 */
.col-pk {
    font-weight: bold;
    color: #fbbf24 !important;
    position: relative;
}
.col-pk::before {
    content: "🔑 ";
}
.col-index {
    color: #60a5fa !important;
    position: relative;
}
.col-index::before {
    content: "📌 ";
}

/* 설명 없는 항목 흐림 */
.no-comment {
    opacity: 0.5;
    filter: blur(0.5px);
}

/* 테이블 정보 배지 */
.table-badge {
    display: inline-block;
    padding: 2px 8px;
    background: #1a1f2e;
    border: 1px solid #2a3441;
    border-radius: 4px;
    font-size: 11px;
    margin-left: 8px;
    color: #9aa9bc;
}
.table-badge.daemon {
    background: #dc2626;
    color: white;
    border-color: #991b1b;
}
.table-badge.recent {
    background: #059669;
    color: white;
    border-color: #047857;
}
.table-badge.col-count {
    background: #2563eb;
    color: white;
    border-color: #1e40af;
}
.table-badge.growing {
    background: #f59e0b;
    color: white;
    border-color: #d97706;
}
.table-badge.deprecated {
    background: #6b7280;
    color: white;
    border-color: #4b5563;
}
.table-badge.role-tag {
    background: #8b5cf6;
    color: white;
    border-color: #7c3aed;
}
.table-badge.warning {
    background: #ef4444;
    color: white;
    border-color: #dc2626;
}

/* 즐겨찾기 */
.favorite {
    color: #fbbf24 !important;
}
.favorite::before {
    content: "⭐ ";
}

/* 다크/라이트 모드 */
body.light-mode {
    background: #ffffff;
    color: #1a1f2e;
}
body.light-mode .search-panel {
    background: #f3f4f6;
    border-color: #d1d5db;
}
body.light-mode .search-input {
    background: #ffffff;
    border-color: #d1d5db;
    color: #1a1f2e;
}
body.light-mode .control-btn {
    background: #f3f4f6;
    border-color: #d1d5db;
    color: #1a1f2e;
}
body.light-mode .db-head {
    color: #2563eb;
}
body.light-mode .table-name {
    color: #111827;
}
body.light-mode .col-name {
    color: #1e40af;
}

/* 숨김 처리 (검색 필터) */
.hidden {
    display: none !important;
}

/* 팝업 */
.popup {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background: #1a1f2e;
    border: 2px solid #2a3441;
    border-radius: 8px;
    padding: 20px;
    max-width: 90%;
    max-height: 90vh;
    overflow: auto;
    z-index: 10000;
    box-shadow: 0 10px 40px rgba(0,0,0,0.5);
}
.popup-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 16px;
    padding-bottom: 12px;
    border-bottom: 1px solid #2a3441;
}
.popup-title {
    font-size: 18px;
    font-weight: bold;
    color: #9fc5ff;
}
.popup-close {
    background: #dc2626;
    color: white;
    border: none;
    padding: 6px 12px;
    border-radius: 4px;
    cursor: pointer;
}
.popup-content {
    font-family: 'Courier New', monospace;
    font-size: 12px;
    white-space: pre-wrap;
    word-break: break-all;
    background: #0b0f16;
    padding: 12px;
    border-radius: 4px;
    max-height: 70vh;
    overflow: auto;
}
.popup-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0,0,0,0.7);
    z-index: 9999;
}

/* 비교 모드 */
.compare-mode .table {
    border-left: 3px solid transparent;
}
.compare-mode .table.selected {
    border-left-color: #2563eb;
    background: rgba(37, 99, 235, 0.1);
}

/* 컬럼 클릭 복사 피드백 */
.copied {
    background: #059669 !important;
    color: white !important;
    transition: all 0.3s;
}

/* ========== [신규: 툴팁 스타일 추가] ========== */
.tooltip {
    position: relative;
    cursor: help;
}
.tooltip:hover::after {
    content: attr(data-tooltip);
    position: absolute;
    left: 100%;
    top: 0;
    background: #1a1f2e;
    color: #e6edf6;
    padding: 8px 12px;
    border-radius: 4px;
    border: 1px solid #2a3441;
    white-space: nowrap;
    z-index: 1000;
    font-size: 12px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.3);
    margin-left: 8px;
}

/* ========== [신규: 컬럼 접기/펼치기] ========== */
.col-row.collapsed {
    opacity: 0.3;
    max-height: 20px;
    overflow: hidden;
}
.col-toggle-btn {
    cursor: pointer;
    color: #89a0bb;
    margin-right: 4px;
    user-select: none;
}

/* ========== [신규: 드래그 정렬] ========== */
.col-row.dragging {
    opacity: 0.5;
}
.col-row[draggable="true"] {
    cursor: move;
}

/* ========== [신규: 모바일 모드] ========== */
@media (max-width: 768px) {
    .table-head {
        flex-direction: column;
        align-items: flex-start;
    }
    .table-badge {
        font-size: 10px;
        padding: 1px 4px;
    }
    .col-row {
        flex-direction: column;
        gap: 4px;
    }
    .search-panel {
        padding: 12px;
    }
    .control-panel {
        flex-direction: column;
    }
}

/*---------| BODY SCROLLBAR |---------*/
 body::-webkit-scrollbar                                            { width:10px; }
 body::-webkit-scrollbar-thumb                                      { background-color:#333; border:1px solid #222; }
 body::-webkit-scrollbar-thumb:hover                                { background-color:#555; cursor:default; }
 body::-webkit-scrollbar-track                                      { background-color:#0b0e11; }
/* ========== [신규 스타일 추가 끝] ========== */
</style>

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">

<div class="MySQL">

<h2 class="Db_title"><i class="fa-solid fa-database"></i> DB → TABLE → COLUMN (DB / TABLE 토글)</h2>

<!-- ========== [신규 UI 추가 시작] ========== -->

<div class="search-panel">
    <div class="search-row">
        <input type="text" id="searchInput" class="search-input" placeholder="검색어 입력 (다중 키워드는 공백으로 구분, 정규식 지원)">
        <button class="search-btn" onclick="doSearch()">검색</button>
        <button class="search-btn" onclick="clearSearch()">초기화</button>
    </div>
    <div class="search-row">
        <div class="search-options">
            <div class="search-option">
                <input type="radio" name="searchTarget" id="searchAll" value="all" checked>
                <label for="searchAll">전체</label>
            </div>
            <div class="search-option">
                <input type="radio" name="searchTarget" id="searchColumn" value="column">
                <label for="searchColumn">컬럼명만</label>
            </div>
            <div class="search-option">
                <input type="radio" name="searchTarget" id="searchComment" value="comment">
                <label for="searchComment">설명만</label>
            </div>
            <div class="search-option">
                <input type="checkbox" id="includeType">
                <label for="includeType">타입 포함</label>
            </div>
            <div class="search-option">
                <input type="radio" name="searchMode" id="searchAnd" value="and" checked>
                <label for="searchAnd">AND</label>
            </div>
            <div class="search-option">
                <input type="radio" name="searchMode" id="searchOr" value="or">
                <label for="searchOr">OR</label>
            </div>
            <div class="search-option">
                <input type="checkbox" id="useRegex">
                <label for="useRegex">정규식</label>
            </div>
        </div>
    </div>
</div>

<div class="control-panel">
    <button class="control-btn" onclick="expandAll()">전체 펼치기</button>
    <button class="control-btn" onclick="collapseAll()">전체 접기</button>
    <button class="control-btn" onclick="toggleTheme()">다크/라이트</button>
    <button class="control-btn" onclick="exportJSON()">JSON 내보내기</button>
    <button class="control-btn" onclick="exportCSV()">CSV 내보내기</button>
    <button class="control-btn" onclick="location.reload()">리로드</button>
    <button class="control-btn" onclick="showCompareMode()">테이블 비교</button>
    <button class="control-btn" onclick="showColumnFrequency()">컬럼 빈도</button>
    <button class="control-btn" onclick="showSimilarityGroups()">유사도 그룹</button>
    <button class="control-btn" onclick="showSafetyChecks()">안정성 체크</button>
    <button class="control-btn" onclick="alert('자동 생성: 테이블의 생성 버튼을 클릭하세요')">자동 생성</button>
</div>

<!-- ========== [신규 UI 추가 끝] ========== -->

<?php foreach ($tree as $dbName => $tables): ?>
<div class="db" data-db="<?=htmlspecialchars($dbName)?>">
    <div class="db-head" onclick="toggleDb(this)">
        <div class="db-toggle">▶</div>
        <div><?=htmlspecialchars($dbName)?></div>
        <!-- ========== [신규: DB 접근 권한 표시] ========== -->
        <?php if (isset($dbPrivilegesMap[$dbName])): ?>
        <span class="table-badge"><?=$dbPrivilegesMap[$dbName]?> tables</span>
        <?php endif; ?>
        <!-- ========== [신규 끝] ========== -->
        <span class="table-badge" onclick="event.stopPropagation(); toggleFavorite('db', '<?=htmlspecialchars($dbName)?>')" style="cursor:pointer;">⭐</span>
    </div>

    <div class="tables">
        <?php foreach ($tables as $tableName => $t): ?>
        <?php
        $tableKey = $dbName . '.' . $tableName;
        $colCount = count($t['columns']);
        $isDaemon = isset($daemonTables[$tableName]);
        $stats = isset($tableStatsMap[$tableKey]) ? $tableStatsMap[$tableKey] : null;
        $isRecent = false;
        if ($stats && $stats['update_time']) {
            $updateTime = strtotime($stats['update_time']);
            $isRecent = (time() - $updateTime) < 86400 * 7; // 7일 이내
        }
        $isGrowing = isset($growingTables[$tableKey]);
        $isDeprecated = isset($deprecatedTables[$tableKey]);
        $roleTags = isset($tableRoleTags[$tableKey]) ? $tableRoleTags[$tableKey] : [];
        $hasWarnings = isset($missingIndexWarnings[$tableKey]) || isset($overflowRisks[$tableKey]) || 
                       isset($timestampViolations[$tableKey]) || isset($typeAnomalies[$tableKey]);
        ?>
        <div class="table" data-db="<?=htmlspecialchars($dbName)?>" data-table="<?=htmlspecialchars($tableName)?>">
            <div class="table-head" onclick="toggleTable(this)">
                <div class="table-toggle">▶</div>
                <div class="table-name"><?=htmlspecialchars($tableName)?></div>
                <div class="table-comment <?=($t['comment']===''?'no-comment':'')?>">
                    <?= $t['comment'] !== '' ? htmlspecialchars($t['comment']) : '(설명 없음)' ?>
                </div>
                <!-- ========== [신규: 테이블 정보 배지] ========== -->
                <span class="table-badge col-count"><?=$colCount?> cols</span>
                <?php if ($stats): ?>
                <span class="table-badge"><?=number_format($stats['rows'])?> rows</span>
                <span class="table-badge"><?=$stats['size_mb']?> MB</span>
                <?php if ($stats['create_time']): ?>
                <span class="table-badge" title="생성: <?=$stats['create_time']?>">생성: <?=date('Y-m-d', strtotime($stats['create_time']))?></span>
                <?php endif; ?>
                <?php if ($stats['update_time']): ?>
                <span class="table-badge" title="수정: <?=$stats['update_time']?>">수정: <?=date('Y-m-d H:i', strtotime($stats['update_time']))?></span>
                <?php endif; ?>
                <?php endif; ?>
                <?php if ($isDaemon): ?>
                <span class="table-badge daemon">DAEMON</span>
                <?php endif; ?>
                <?php if ($isRecent): ?>
                <span class="table-badge recent">RECENT</span>
                <?php endif; ?>
                <?php if ($isGrowing): ?>
                <span class="table-badge growing">GROWING</span>
                <?php endif; ?>
                <?php if ($isDeprecated): ?>
                <span class="table-badge deprecated">DEPRECATED</span>
                <?php endif; ?>
                <?php foreach ($roleTags as $tag): ?>
                <span class="table-badge role-tag"><?=$tag?></span>
                <?php endforeach; ?>
                <?php if ($hasWarnings): ?>
                <span class="table-badge warning" onclick="event.stopPropagation(); showTableWarnings('<?=htmlspecialchars($dbName)?>', '<?=htmlspecialchars($tableName)?>')" style="cursor:pointer;">⚠️</span>
                <?php endif; ?>
                <!-- ========== [신규 끝] ========== -->
                <span class="table-badge" onclick="event.stopPropagation(); showCreateTable('<?=htmlspecialchars($dbName)?>', '<?=htmlspecialchars($tableName)?>')" style="cursor:pointer;background:#7c3aed;">SQL</span>
                <span class="table-badge" onclick="event.stopPropagation(); toggleFavorite('table', '<?=htmlspecialchars($dbName)?>', '<?=htmlspecialchars($tableName)?>')" style="cursor:pointer;">⭐</span>
                <span class="table-badge" onclick="event.stopPropagation(); selectForCompare('<?=htmlspecialchars($dbName)?>', '<?=htmlspecialchars($tableName)?>')" style="cursor:pointer;background:#059669;">비교</span>
                <span class="table-badge" onclick="event.stopPropagation(); showTableRefs('<?=htmlspecialchars($dbName)?>', '<?=htmlspecialchars($tableName)?>')" style="cursor:pointer;background:#0891b2;">참조</span>
                <span class="table-badge" onclick="event.stopPropagation(); showAutoGenMenu('<?=htmlspecialchars($dbName)?>', '<?=htmlspecialchars($tableName)?>')" style="cursor:pointer;background:#be185d;">생성</span>
            </div>

            <div class="columns">
                <?php foreach ($t['columns'] as $idx => $c): ?>
                <?php
                $colKey = $tableKey . '.' . $c['name'];
                $isPk = false;
                $isIndex = false;
                if (isset($pkIndexMap[$colKey])) {
                    foreach ($pkIndexMap[$colKey] as $idxInfo) {
                        if ($idxInfo['is_pk']) $isPk = true;
                        else $isIndex = true;
                    }
                }
                $typeClass = 'col-type-other';
                $typeLower = strtolower($c['type']);
                if (strpos($typeLower, 'int') !== false) $typeClass = 'col-type-int';
                elseif (strpos($typeLower, 'varchar') !== false || strpos($typeLower, 'char') !== false) $typeClass = 'col-type-varchar';
                elseif (strpos($typeLower, 'text') !== false) $typeClass = 'col-type-text';
                elseif (strpos($typeLower, 'datetime') !== false || strpos($typeLower, 'timestamp') !== false || strpos($typeLower, 'date') !== false) $typeClass = 'col-type-datetime';
                elseif (strpos($typeLower, 'decimal') !== false || strpos($typeLower, 'float') !== false || strpos($typeLower, 'double') !== false) $typeClass = 'col-type-decimal';
                elseif (strpos($typeLower, 'enum') !== false) $typeClass = 'col-type-enum';
                
                $colDetail = isset($columnDetailMap[$colKey]) ? $columnDetailMap[$colKey] : null;
                $frequency = isset($columnFrequencyMap[strtolower($c['name'])]) ? $columnFrequencyMap[strtolower($c['name'])] : 0;
                $tooltipText = "컬럼명: {$c['name']}\n타입: {$c['type']}\n설명: " . ($c['comment'] ?: '없음');
                if ($colDetail) {
                    $tooltipText .= "\nNULL: " . ($colDetail['is_nullable'] === 'YES' ? '허용' : '불가');
                    if ($colDetail['column_default'] !== null) {
                        $tooltipText .= "\n기본값: " . $colDetail['column_default'];
                    }
                }
                $tooltipText .= "\n사용 빈도: {$frequency}회";
                ?>
                <div class="col-row" 
                     data-col-name="<?=htmlspecialchars($c['name'])?>"
                     data-col-type="<?=htmlspecialchars($c['type'])?>"
                     data-col-comment="<?=htmlspecialchars($c['comment'])?>"
                     draggable="true"
                     ondragstart="handleDragStart(event)"
                     ondragover="handleDragOver(event)"
                     ondrop="handleDrop(event)">
                    <span class="col-toggle-btn" onclick="event.stopPropagation(); toggleColumnRow(this)" title="접기/펼치기">▾</span>
                    <div class="col-name <?=$isPk ? 'col-pk' : ($isIndex ? 'col-index' : '')?> tooltip" 
                         data-tooltip="<?=htmlspecialchars($tooltipText)?>"
                         onclick="copyColumnName(this, '<?=htmlspecialchars($c['name'])?>')" 
                         style="cursor:pointer;" 
                         title="클릭하여 복사">
                        <?=htmlspecialchars($c['name'])?>
                        <?php if ($frequency > 5): ?>
                        <span style="color:#fbbf24;font-size:10px;">(빈도:<?=$frequency?>)</span>
                        <?php endif; ?>
                    </div>
                    <div class="col-type <?=$typeClass?>"><?=htmlspecialchars($c['type'])?>
                        <?php if ($colDetail && $colDetail['column_default'] !== null): ?>
                        <span style="color:#34d399;font-size:11px;">[기본:<?=htmlspecialchars($colDetail['column_default'])?>]</span>
                        <?php endif; ?>
                        <?php if ($colDetail && $colDetail['is_nullable'] === 'YES'): ?>
                        <span style="color:#fbbf24;font-size:11px;">[NULL]</span>
                        <?php endif; ?>
                    </div>
                    <div class="col-comment <?=($c['comment']===''?'no-comment':'')?>">
                        <?= $c['comment'] !== '' ? htmlspecialchars($c['comment']) : '(설명 없음)' ?>
                    </div>
                </div>
                <?php endforeach; ?>
            </div>
        </div>
        <?php endforeach; ?>
    </div>
</div>
<?php endforeach; ?>

<script>
// ========== [기존 함수 유지] ==========
function toggleDb(head){
    const tables = head.nextElementSibling;
    const icon = head.querySelector('.db-toggle');
    if (!tables) return;

    const open = tables.style.display === 'block';
    tables.style.display = open ? 'none' : 'block';
    icon.textContent = open ? '▶' : '▼';
    
    // ========== [신규: localStorage 저장] ==========
    const dbName = head.closest('.db').dataset.db;
    saveExpandState();
    // ========== [신규 끝] ==========
}

function toggleTable(head){
    const cols = head.nextElementSibling;
    const icon = head.querySelector('.table-toggle');
    if (!cols) return;

    const open = cols.style.display === 'block';
    cols.style.display = open ? 'none' : 'block';
    icon.textContent = open ? '▶' : '▼';
    
    // ========== [신규: localStorage 저장] ==========
    saveExpandState();
    // ========== [신규 끝] ==========
}

// ========== [신규 함수 추가 시작] ==========

// 검색 기능
let currentSearchTerms = [];
let currentSearchMode = 'and';
let currentSearchTarget = 'all';
let currentIncludeType = false;
let currentUseRegex = false;

function doSearch() {
    const input = document.getElementById('searchInput').value.trim();
    if (!input) {
        clearSearch();
        return;
    }
    
    currentSearchTerms = input.split(/\s+/).filter(t => t);
    currentSearchMode = document.querySelector('input[name="searchMode"]:checked').value;
    currentSearchTarget = document.querySelector('input[name="searchTarget"]:checked').value;
    currentIncludeType = document.getElementById('includeType').checked;
    currentUseRegex = document.getElementById('useRegex').checked;
    
    applySearch();
}

function applySearch() {
    const allRows = document.querySelectorAll('.col-row, .table, .db');
    
    allRows.forEach(row => {
        let match = false;
        let text = '';
        
        if (row.classList.contains('col-row')) {
            const name = row.dataset.colName || '';
            const type = row.dataset.colType || '';
            const comment = row.dataset.colComment || '';
            
            if (currentSearchTarget === 'column') {
                text = name;
            } else if (currentSearchTarget === 'comment') {
                text = comment;
            } else {
                text = name + ' ' + comment;
                if (currentIncludeType) text += ' ' + type;
            }
        } else if (row.classList.contains('table')) {
            const tableName = row.querySelector('.table-name')?.textContent || '';
            const comment = row.querySelector('.table-comment')?.textContent || '';
            text = tableName + ' ' + comment;
        } else if (row.classList.contains('db')) {
            const dbName = row.querySelector('.db-head > div:nth-child(2)')?.textContent || '';
            text = dbName;
        }
        
        if (currentSearchTerms.length > 0) {
            if (currentSearchMode === 'and') {
                match = currentSearchTerms.every(term => testMatch(text, term));
            } else {
                match = currentSearchTerms.some(term => testMatch(text, term));
            }
        }
        
        if (match) {
            row.classList.remove('hidden');
            highlightText(row, currentSearchTerms);
            // 부모 요소들도 표시
            let parent = row.parentElement;
            while (parent) {
                if (parent.classList.contains('tables')) {
                    parent.style.display = 'block';
                    const dbHead = parent.previousElementSibling;
                    if (dbHead) {
                        dbHead.querySelector('.db-toggle').textContent = '▼';
                    }
                } else if (parent.classList.contains('columns')) {
                    parent.style.display = 'block';
                    const tableHead = parent.previousElementSibling;
                    if (tableHead) {
                        tableHead.querySelector('.table-toggle').textContent = '▼';
                    }
                }
                parent = parent.parentElement;
            }
        } else {
            if (row.classList.contains('col-row')) {
                row.classList.add('hidden');
            } else if (row.classList.contains('table')) {
                const hasVisibleCols = Array.from(row.querySelectorAll('.col-row')).some(r => !r.classList.contains('hidden'));
                if (!hasVisibleCols) {
                    row.classList.add('hidden');
                } else {
                    row.classList.remove('hidden');
                }
            } else if (row.classList.contains('db')) {
                const hasVisibleTables = Array.from(row.querySelectorAll('.table')).some(t => !t.classList.contains('hidden'));
                if (!hasVisibleTables) {
                    row.classList.add('hidden');
                } else {
                    row.classList.remove('hidden');
                }
            }
        }
    });
}

function testMatch(text, term) {
    if (currentUseRegex) {
        try {
            const regex = new RegExp(term, 'i');
            return regex.test(text);
        } catch (e) {
            return text.toLowerCase().includes(term.toLowerCase());
        }
    } else {
        return text.toLowerCase().includes(term.toLowerCase());
    }
}

function highlightText(element, terms) {
    if (!element || element.children.length === 0) return;
    
    const walker = document.createTreeWalker(
        element,
        NodeFilter.SHOW_TEXT,
        null,
        false
    );
    
    const textNodes = [];
    let node;
    while (node = walker.nextNode()) {
        if (node.parentElement && !node.parentElement.classList.contains('highlight')) {
            textNodes.push(node);
        }
    }
    
    textNodes.forEach(textNode => {
        let text = textNode.textContent;
        let newHTML = text;
        
        terms.forEach(term => {
            if (currentUseRegex) {
                try {
                    const regex = new RegExp('(' + term + ')', 'gi');
                    newHTML = newHTML.replace(regex, '<span class="highlight">$1</span>');
                } catch (e) {
                    const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                    newHTML = newHTML.replace(new RegExp('(' + escaped + ')', 'gi'), '<span class="highlight">$1</span>');
                }
            } else {
                const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                newHTML = newHTML.replace(new RegExp('(' + escaped + ')', 'gi'), '<span class="highlight">$1</span>');
            }
        });
        
        if (newHTML !== text) {
            const wrapper = document.createElement('span');
            wrapper.innerHTML = newHTML;
            textNode.parentNode.replaceChild(wrapper, textNode);
        }
    });
}

function clearSearch() {
    document.getElementById('searchInput').value = '';
    currentSearchTerms = [];
    document.querySelectorAll('.hidden').forEach(el => el.classList.remove('hidden'));
    document.querySelectorAll('.highlight').forEach(el => {
        const parent = el.parentNode;
        parent.replaceChild(document.createTextNode(el.textContent), el);
        parent.normalize();
    });
}

// 전체 펼치기/접기
function expandAll() {
    document.querySelectorAll('.tables').forEach(el => {
        el.style.display = 'block';
        el.previousElementSibling.querySelector('.db-toggle').textContent = '▼';
    });
    document.querySelectorAll('.columns').forEach(el => {
        el.style.display = 'block';
        el.previousElementSibling.querySelector('.table-toggle').textContent = '▼';
    });
    saveExpandState();
}

function collapseAll() {
    document.querySelectorAll('.tables').forEach(el => {
        el.style.display = 'none';
        el.previousElementSibling.querySelector('.db-toggle').textContent = '▶';
    });
    document.querySelectorAll('.columns').forEach(el => {
        el.style.display = 'none';
        el.previousElementSibling.querySelector('.table-toggle').textContent = '▶';
    });
    saveExpandState();
}

// localStorage 저장/복원
function saveExpandState() {
    const state = {
        dbs: {},
        tables: {}
    };
    
    document.querySelectorAll('.db').forEach(db => {
        const dbName = db.dataset.db;
        const tablesEl = db.querySelector('.tables');
        state.dbs[dbName] = tablesEl.style.display === 'block';
        
        db.querySelectorAll('.table').forEach(table => {
            const tableKey = dbName + '.' + table.dataset.table;
            const colsEl = table.querySelector('.columns');
            state.tables[tableKey] = colsEl.style.display === 'block';
        });
    });
    
    localStorage.setItem('dbTreeExpandState', JSON.stringify(state));
}

function restoreExpandState() {
    try {
        const saved = localStorage.getItem('dbTreeExpandState');
        if (!saved) return;
        
        const state = JSON.parse(saved);
        
        document.querySelectorAll('.db').forEach(db => {
            const dbName = db.dataset.db;
            if (state.dbs[dbName]) {
                const tablesEl = db.querySelector('.tables');
                tablesEl.style.display = 'block';
                db.querySelector('.db-toggle').textContent = '▼';
            }
            
            db.querySelectorAll('.table').forEach(table => {
                const tableKey = dbName + '.' + table.dataset.table;
                if (state.tables[tableKey]) {
                    const colsEl = table.querySelector('.columns');
                    colsEl.style.display = 'block';
                    table.querySelector('.table-toggle').textContent = '▼';
                }
            });
        });
    } catch (e) {
        console.error('복원 실패:', e);
    }
}

// 즐겨찾기
function toggleFavorite(type, dbName, tableName) {
    const key = type === 'db' ? 'fav_db_' + dbName : 'fav_table_' + dbName + '.' + tableName;
    const current = localStorage.getItem(key);
    
    if (current === '1') {
        localStorage.removeItem(key);
        updateFavoriteUI(type, dbName, tableName, false);
    } else {
        localStorage.setItem(key, '1');
        updateFavoriteUI(type, dbName, tableName, true);
    }
}

function updateFavoriteUI(type, dbName, tableName, isFav) {
    if (type === 'db') {
        const db = document.querySelector(`.db[data-db="${dbName}"]`);
        if (db) {
            const head = db.querySelector('.db-head');
            if (isFav) {
                head.classList.add('favorite');
            } else {
                head.classList.remove('favorite');
            }
        }
    } else {
        const table = document.querySelector(`.table[data-db="${dbName}"][data-table="${tableName}"]`);
        if (table) {
            const head = table.querySelector('.table-head');
            if (isFav) {
                head.classList.add('favorite');
            } else {
                head.classList.remove('favorite');
            }
        }
    }
}

function loadFavorites() {
    document.querySelectorAll('.db').forEach(db => {
        const dbName = db.dataset.db;
        if (localStorage.getItem('fav_db_' + dbName) === '1') {
            updateFavoriteUI('db', dbName, null, true);
        }
        
        db.querySelectorAll('.table').forEach(table => {
            const tableName = table.dataset.table;
            const key = 'fav_table_' + dbName + '.' + tableName;
            if (localStorage.getItem(key) === '1') {
                updateFavoriteUI('table', dbName, tableName, true);
            }
        });
    });
}

// 컬럼명 복사
function copyColumnName(el, name) {
    if (navigator.clipboard) {
        navigator.clipboard.writeText(name).then(() => {
            el.classList.add('copied');
            setTimeout(() => el.classList.remove('copied'), 1000);
        });
    } else {
        const textarea = document.createElement('textarea');
        textarea.value = name;
        document.body.appendChild(textarea);
        textarea.select();
        document.execCommand('copy');
        document.body.removeChild(textarea);
        el.classList.add('copied');
        setTimeout(() => el.classList.remove('copied'), 1000);
    }
}

// CREATE TABLE SQL 팝업
const createTableData = <?=json_encode($createTableMap, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)?>;

function showCreateTable(dbName, tableName) {
    const key = dbName + '.' + tableName;
    const sql = createTableData[key] || 'CREATE TABLE SQL을 가져올 수 없습니다.';
    
    const overlay = document.createElement('div');
    overlay.className = 'popup-overlay';
    overlay.onclick = () => document.body.removeChild(overlay);
    
    const popup = document.createElement('div');
    popup.className = 'popup';
    popup.onclick = (e) => e.stopPropagation();
    
    popup.innerHTML = `
        <div class="popup-header">
            <div class="popup-title">CREATE TABLE: ${dbName}.${tableName}</div>
            <button class="popup-close" onclick="document.body.removeChild(this.closest('.popup-overlay'))">닫기</button>
        </div>
        <div class="popup-content">${escapeHtml(sql)}</div>
    `;
    
    overlay.appendChild(popup);
    document.body.appendChild(overlay);
}

function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

// 테이블 비교
let compareSelected = [];

function selectForCompare(dbName, tableName) {
    const key = dbName + '.' + tableName;
    const index = compareSelected.indexOf(key);
    
    if (index > -1) {
        compareSelected.splice(index, 1);
        updateCompareUI();
    } else {
        if (compareSelected.length >= 2) {
            compareSelected.shift();
        }
        compareSelected.push(key);
        updateCompareUI();
    }
    
    if (compareSelected.length === 2) {
        showCompareResult();
    }
}

function updateCompareUI() {
    document.querySelectorAll('.table').forEach(table => {
        const key = table.dataset.db + '.' + table.dataset.table;
        if (compareSelected.includes(key)) {
            table.classList.add('selected');
        } else {
            table.classList.remove('selected');
        }
    });
}

function showCompareResult() {
    if (compareSelected.length !== 2) return;
    
    const [key1, key2] = compareSelected;
    const [db1, tb1] = key1.split('.');
    const [db2, tb2] = key2.split('.');
    
    const table1 = document.querySelector(`.table[data-db="${db1}"][data-table="${tb1}"]`);
    const table2 = document.querySelector(`.table[data-db="${db2}"][data-table="${tb2}"]`);
    
    if (!table1 || !table2) return;
    
    const cols1 = Array.from(table1.querySelectorAll('.col-row')).map(r => ({
        name: r.dataset.colName,
        type: r.dataset.colType,
        comment: r.dataset.colComment
    }));
    
    const cols2 = Array.from(table2.querySelectorAll('.col-row')).map(r => ({
        name: r.dataset.colName,
        type: r.dataset.colType,
        comment: r.dataset.colComment
    }));
    
    const only1 = cols1.filter(c1 => !cols2.some(c2 => c2.name === c1.name));
    const only2 = cols2.filter(c2 => !cols1.some(c1 => c1.name === c2.name));
    const common = cols1.filter(c1 => cols2.some(c2 => c2.name === c1.name));
    const diff = common.filter(c1 => {
        const c2 = cols2.find(c => c.name === c1.name);
        return c1.type !== c2.type || c1.comment !== c2.comment;
    });
    
    let html = `
        <h3>테이블 비교: ${key1} vs ${key2}</h3>
        <h4>공통 컬럼 (${common.length})</h4>
        <ul>
    `;
    common.forEach(c => {
        const c2 = cols2.find(col => col.name === c.name);
        const typeDiff = c.type !== c2.type ? ` [타입 다름: ${c.type} vs ${c2.type}]` : '';
        const commentDiff = c.comment !== c2.comment ? ` [설명 다름]` : '';
        html += `<li>${c.name}${typeDiff}${commentDiff}</li>`;
    });
    html += `</ul>`;
    
    if (only1.length > 0) {
        html += `<h4>${key1}에만 있는 컬럼 (${only1.length})</h4><ul>`;
        only1.forEach(c => html += `<li>${c.name} (${c.type})</li>`);
        html += `</ul>`;
    }
    
    if (only2.length > 0) {
        html += `<h4>${key2}에만 있는 컬럼 (${only2.length})</h4><ul>`;
        only2.forEach(c => html += `<li>${c.name} (${c.type})</li>`);
        html += `</ul>`;
    }
    
    if (diff.length > 0) {
        html += `<h4>타입/설명이 다른 컬럼 (${diff.length})</h4><ul>`;
        diff.forEach(c => {
            const c2 = cols2.find(col => col.name === c.name);
            html += `<li>${c.name}: 타입(${c.type} vs ${c2.type}), 설명(${c.comment || '없음'} vs ${c2.comment || '없음'})</li>`;
        });
        html += `</ul>`;
    }
    
    const overlay = document.createElement('div');
    overlay.className = 'popup-overlay';
    overlay.onclick = () => {
        document.body.removeChild(overlay);
        compareSelected = [];
        updateCompareUI();
    };
    
    const popup = document.createElement('div');
    popup.className = 'popup';
    popup.onclick = (e) => e.stopPropagation();
    popup.innerHTML = `
        <div class="popup-header">
            <div class="popup-title">테이블 비교 결과</div>
            <button class="popup-close" onclick="document.body.removeChild(this.closest('.popup-overlay')); compareSelected = []; updateCompareUI();">닫기</button>
        </div>
        <div class="popup-content" style="white-space:normal;">${html}</div>
    `;
    
    overlay.appendChild(popup);
    document.body.appendChild(overlay);
}

function showCompareMode() {
    alert('비교 모드: 비교할 테이블의 "비교" 버튼을 클릭하세요. (최대 2개)');
    document.body.classList.add('compare-mode');
}

// 다크/라이트 모드
function toggleTheme() {
    document.body.classList.toggle('light-mode');
    localStorage.setItem('dbTreeTheme', document.body.classList.contains('light-mode') ? 'light' : 'dark');
}

function loadTheme() {
    const theme = localStorage.getItem('dbTreeTheme');
    if (theme === 'light') {
        document.body.classList.add('light-mode');
    }
}

// 내보내기
const treeData = <?=json_encode($tree, JSON_UNESCAPED_UNICODE)?>;
const columnFrequencyData = <?=json_encode($columnFrequencyMap, JSON_UNESCAPED_UNICODE)?>;
const tableReferencesData = <?=json_encode($tableReferences, JSON_UNESCAPED_UNICODE)?>;
const tableRoleTagsData = <?=json_encode($tableRoleTags, JSON_UNESCAPED_UNICODE)?>;
const missingIndexWarningsData = <?=json_encode($missingIndexWarnings, JSON_UNESCAPED_UNICODE)?>;
const overflowRisksData = <?=json_encode($overflowRisks, JSON_UNESCAPED_UNICODE)?>;
const timestampViolationsData = <?=json_encode($timestampViolations, JSON_UNESCAPED_UNICODE)?>;
const typeAnomaliesData = <?=json_encode($typeAnomalies, JSON_UNESCAPED_UNICODE)?>;
const daemonColumnMapData = <?=json_encode($daemonColumnMap, JSON_UNESCAPED_UNICODE)?>;

function exportJSON() {
    const dataStr = JSON.stringify(treeData, null, 2);
    const blob = new Blob([dataStr], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'db_tree_' + new Date().toISOString().split('T')[0] + '.json';
    a.click();
    URL.revokeObjectURL(url);
}

function exportCSV() {
    let csv = 'DB,Table,Column,Type,Comment\n';
    
    Object.keys(treeData).forEach(dbName => {
        Object.keys(treeData[dbName]).forEach(tableName => {
            const table = treeData[dbName][tableName];
            table.columns.forEach(col => {
                csv += `"${dbName}","${tableName}","${col.name}","${col.type}","${col.comment || ''}"\n`;
            });
        });
    });
    
    const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'db_tree_' + new Date().toISOString().split('T')[0] + '.csv';
    a.click();
    URL.revokeObjectURL(url);
}

// ========== [신규: 탐색·분석 기능] ==========

function showColumnFrequency() {
    const sorted = Object.entries(columnFrequencyData).sort((a, b) => b[1] - a[1]);
    let html = '<h3>컬럼 사용 빈도 (상위 50개)</h3><ul>';
    sorted.slice(0, 50).forEach(([name, freq]) => {
        html += `<li>${name}: ${freq}회</li>`;
    });
    html += '</ul>';
    
    showPopup('컬럼 사용 빈도', html);
}

function showSimilarityGroups() {
    const groups = <?=json_encode($columnSimilarityGroups, JSON_UNESCAPED_UNICODE)?>;
    let html = '<h3>유사 컬럼명 그룹</h3><ul>';
    groups.forEach(group => {
        html += `<li>${group.join(', ')}</li>`;
    });
    html += '</ul>';
    
    showPopup('유사도 그룹', html);
}

function showTableRefs(dbName, tableName) {
    const key = dbName + '.' + tableName;
    const refs = tableReferencesData[key] || [];
    let html = `<h3>${key} 참조 추정</h3>`;
    if (refs.length > 0) {
        html += '<ul>';
        refs.forEach(ref => html += `<li>${ref}</li>`);
        html += '</ul>';
    } else {
        html += '<p>참조 관계를 찾을 수 없습니다.</p>';
    }
    
    showPopup('테이블 참조', html);
}

// ========== [신규: 안정성 체크] ==========

function showSafetyChecks() {
    let html = '<h3>안정성 체크 결과</h3>';
    
    html += '<h4>인덱스 누락 경고</h4><ul>';
    Object.entries(missingIndexWarningsData).forEach(([table, cols]) => {
        html += `<li>${table}: ${cols.join(', ')}</li>`;
    });
    html += '</ul>';
    
    html += '<h4>오버플로우 위험</h4><ul>';
    Object.entries(overflowRisksData).forEach(([table, cols]) => {
        html += `<li>${table}: ${cols.join(', ')}</li>`;
    });
    html += '</ul>';
    
    html += '<h4>Timestamp 표준 위반</h4><ul>';
    Object.entries(timestampViolationsData).forEach(([table, cols]) => {
        html += `<li>${table}: ${cols.join(', ')}</li>`;
    });
    html += '</ul>';
    
    html += '<h4>타입 이상치</h4><ul>';
    Object.entries(typeAnomaliesData).forEach(([table, anomalies]) => {
        anomalies.forEach(a => {
            html += `<li>${table}.${a.column}: ${a.type} (값: ${a.value})</li>`;
        });
    });
    html += '</ul>';
    
    showPopup('안정성 체크', html);
}

function showTableWarnings(dbName, tableName) {
    const key = dbName + '.' + tableName;
    let html = `<h3>${key} 경고 사항</h3><ul>`;
    
    if (missingIndexWarningsData[key]) {
        html += `<li>인덱스 누락: ${missingIndexWarningsData[key].join(', ')}</li>`;
    }
    if (overflowRisksData[key]) {
        html += `<li>오버플로우 위험: ${overflowRisksData[key].join(', ')}</li>`;
    }
    if (timestampViolationsData[key]) {
        html += `<li>Timestamp 표준 위반: ${timestampViolationsData[key].join(', ')}</li>`;
    }
    if (typeAnomaliesData[key]) {
        typeAnomaliesData[key].forEach(a => {
            html += `<li>타입 이상치: ${a.column} - ${a.type}</li>`;
        });
    }
    
    html += '</ul>';
    showPopup('테이블 경고', html);
}

// ========== [신규: 자동 생성 기능] ==========

function showAutoGenMenu(dbName, tableName) {
    const table = document.querySelector(`.table[data-db="${dbName}"][data-table="${tableName}"]`);
    if (!table) return;
    
    const cols = Array.from(table.querySelectorAll('.col-row')).map(r => ({
        name: r.dataset.colName,
        type: r.dataset.colType,
        comment: r.dataset.colComment
    }));
    
    let html = `<h3>${dbName}.${tableName} 자동 생성</h3>`;
    html += '<div style="display:flex;flex-direction:column;gap:8px;">';
    html += '<button onclick="generateCRUD(\'' + dbName + '\', \'' + tableName + '\')" style="padding:8px;">CRUD 템플릿</button>';
    html += '<button onclick="generatePHPArray(\'' + dbName + '\', \'' + tableName + '\')" style="padding:8px;">PHP 배열</button>';
    html += '<button onclick="generateDTO(\'' + dbName + '\', \'' + tableName + '\')" style="padding:8px;">DTO 클래스</button>';
    html += '<button onclick="generateInsertSample(\'' + dbName + '\', \'' + tableName + '\')" style="padding:8px;">INSERT 샘플</button>';
    html += '<button onclick="generateAlterPreview(\'' + dbName + '\', \'' + tableName + '\')" style="padding:8px;">ALTER 미리보기</button>';
    html += '</div>';
    
    showPopup('자동 생성 메뉴', html);
}

function generateCRUD(dbName, tableName) {
    const table = document.querySelector(`.table[data-db="${dbName}"][data-table="${tableName}"]`);
    const cols = Array.from(table.querySelectorAll('.col-row')).map(r => ({
        name: r.dataset.colName,
        type: r.dataset.colType
    }));
    
    let php = ['<?php', '// ' + tableName + ' CRUD', '', 
        '// CREATE', 'function create_' + tableName + '(\\$data) {', '    global \\$pdo;', 
        '    \\$sql = "INSERT INTO ' + tableName + ' (`' + cols.map(c => c.name).join('`, `') + '`) VALUES (' + 
        cols.map(() => '?').join(', ') + ')";', '    \\$stmt = \\$pdo->prepare(\\$sql);', 
        '    return \\$stmt->execute([' + cols.map(c => '\\$data[\\\'' + c.name + '\\\']').join(', ') + ']);', '}', '',
        '// READ', 'function get_' + tableName + '(\\$id) {', '    global \\$pdo;', 
        '    \\$sql = "SELECT * FROM ' + tableName + ' WHERE id = ?";', '    \\$stmt = \\$pdo->prepare(\\$sql);', 
        '    \\$stmt->execute([\\$id]);', '    return \\$stmt->fetch(PDO::FETCH_ASSOC);', '}', '',
        '// UPDATE', 'function update_' + tableName + '(\\$id, \\$data) {', '    global \\$pdo;', '    \\$set = [];', 
        '    foreach (\\$data as \\$k => \\$v) {', '        \\$set[] = "\\$k = ?";', '    }', 
        '    \\$sql = "UPDATE ' + tableName + ' SET " . implode(\\\', \\\', \\$set) . " WHERE id = ?";', 
        '    \\$stmt = \\$pdo->prepare(\\$sql);', '    return \\$stmt->execute(array_merge(array_values(\\$data), [\\$id]));', '}', '',
        '// DELETE', 'function delete_' + tableName + '(\\$id) {', '    global \\$pdo;', 
        '    \\$sql = "DELETE FROM ' + tableName + ' WHERE id = ?";', '    \\$stmt = \\$pdo->prepare(\\$sql);', 
        '    return \\$stmt->execute([\\$id]);', '}'].join('\n');
    
    showPopup('CRUD 템플릿', '<pre>' + escapeHtml(php) + '</pre>');
}

function generatePHPArray(dbName, tableName) {
    const table = document.querySelector(`.table[data-db="${dbName}"][data-table="${tableName}"]`);
    const cols = Array.from(table.querySelectorAll('.col-row')).map(r => r.dataset.colName);
    
    let php = ['<?php', '\\$' + tableName + ' = ['];
    cols.forEach(col => {
        php.push('    \\\'' + col + '\\\' => \\\'\\\',');
    });
    php.push('];');
    php = php.join('\n');
    
    showPopup('PHP 배열', '<pre>' + escapeHtml(php) + '</pre>');
}

function generateDTO(dbName, tableName) {
    const table = document.querySelector(`.table[data-db="${dbName}"][data-table="${tableName}"]`);
    const cols = Array.from(table.querySelectorAll('.col-row')).map(r => ({
        name: r.dataset.colName,
        type: r.dataset.colType
    }));
    
    let php = ['<?php', 'class ' + tableName + 'DTO {'];
    cols.forEach(col => {
        php.push('    public \\$' + col.name + ';');
    });
    php.push('', '    public function __construct(\\$data = []) {');
    cols.forEach(col => {
        php.push('        \\$this->' + col.name + ' = \\$data[\\\'' + col.name + '\\\'] ?? null;');
    });
    php.push('    }', '}');
    php = php.join('\n');
    
    showPopup('DTO 클래스', '<pre>' + escapeHtml(php) + '</pre>');
}

function generateInsertSample(dbName, tableName) {
    const table = document.querySelector(`.table[data-db="${dbName}"][data-table="${tableName}"]`);
    const cols = Array.from(table.querySelectorAll('.col-row')).map(r => r.dataset.colName);
    
    let sql = `INSERT INTO ${tableName} (`;
    sql += cols.join(', ') + ') VALUES (';
    sql += cols.map(() => '?').join(', ') + ');';
    
    showPopup('INSERT 샘플', '<pre>' + escapeHtml(sql) + '</pre>');
}

function generateAlterPreview(dbName, tableName) {
    const table = document.querySelector(`.table[data-db="${dbName}"][data-table="${tableName}"]`);
    const cols = Array.from(table.querySelectorAll('.col-row')).map(r => ({
        name: r.dataset.colName,
        type: r.dataset.colType
    }));
    
    let sql = ['-- ALTER TABLE ' + tableName + ' 미리보기', '', '-- 컬럼 추가 예시:', 
        'ALTER TABLE ' + tableName + ' ADD COLUMN new_column VARCHAR(255) NULL;', ''];
    if (cols.length > 0) {
        sql.push('-- 컬럼 수정 예시:', 'ALTER TABLE ' + tableName + ' MODIFY COLUMN ' + cols[0].name + ' ' + cols[0].type + ';', '');
    }
    if (cols.length > 0) {
        sql.push('-- 컬럼 삭제 예시:', 'ALTER TABLE ' + tableName + ' DROP COLUMN ' + cols[0].name + ';');
    }
    sql = sql.join('\n');
    
    showPopup('ALTER 미리보기', '<pre>' + escapeHtml(sql) + '</pre>');
}

// ========== [신규: UX 디테일] ==========

function toggleColumnRow(btn) {
    const row = btn.closest('.col-row');
    row.classList.toggle('collapsed');
    btn.textContent = row.classList.contains('collapsed') ? '▸' : '▾';
}

let draggedElement = null;

function handleDragStart(e) {
    draggedElement = e.target.closest('.col-row');
    e.target.closest('.col-row').classList.add('dragging');
}

function handleDragOver(e) {
    e.preventDefault();
    const afterElement = getDragAfterElement(e.target.closest('.columns'), e.clientY);
    const dragging = document.querySelector('.dragging');
    if (afterElement == null) {
        e.target.closest('.columns').appendChild(dragging);
    } else {
        e.target.closest('.columns').insertBefore(dragging, afterElement);
    }
}

function handleDrop(e) {
    e.preventDefault();
    if (draggedElement) {
        draggedElement.classList.remove('dragging');
        draggedElement = null;
    }
}

function getDragAfterElement(container, y) {
    const draggableElements = [...container.querySelectorAll('.col-row:not(.dragging)')];
    return draggableElements.reduce((closest, child) => {
        const box = child.getBoundingClientRect();
        const offset = y - box.top - box.height / 2;
        if (offset < 0 && offset > closest.offset) {
            return { offset: offset, element: child };
        } else {
            return closest;
        }
    }, { offset: Number.NEGATIVE_INFINITY }).element;
}

function showPopup(title, content) {
    const overlay = document.createElement('div');
    overlay.className = 'popup-overlay';
    overlay.onclick = () => document.body.removeChild(overlay);
    
    const popup = document.createElement('div');
    popup.className = 'popup';
    popup.onclick = (e) => e.stopPropagation();
    popup.innerHTML = `
        <div class="popup-header">
            <div class="popup-title">${title}</div>
            <button class="popup-close" onclick="document.body.removeChild(this.closest('.popup-overlay'))">닫기</button>
        </div>
        <div class="popup-content" style="white-space:normal;">${content}</div>
    `;
    
    overlay.appendChild(popup);
    document.body.appendChild(overlay);
}

// 키보드 단축키
document.addEventListener('keydown', (e) => {
    // Ctrl+F: 검색 포커스
    if (e.ctrlKey && e.key === 'f') {
        e.preventDefault();
        document.getElementById('searchInput').focus();
    }
    // Ctrl+E: 전체 펼치기
    if (e.ctrlKey && e.key === 'e') {
        e.preventDefault();
        expandAll();
    }
    // Ctrl+C: 전체 접기
    if (e.ctrlKey && e.key === 'c' && !e.shiftKey) {
        e.preventDefault();
        collapseAll();
    }
    // Esc: 검색 초기화
    if (e.key === 'Escape') {
        clearSearch();
        document.getElementById('searchInput').blur();
    }
    // Enter: 검색 실행
    if (e.key === 'Enter' && document.activeElement === document.getElementById('searchInput')) {
        doSearch();
    }
});

// 초기화
window.addEventListener('DOMContentLoaded', () => {
    restoreExpandState();
    loadFavorites();
    loadTheme();
});

// ========== [신규 함수 추가 끝] ==========
</script>

</div>
<?php require_once '/home/www/GNU/_PAGE/tail.php'; ?>