GNU/_PAGE/structure/directory/directory_tree.php
<?php
// directory_tree_file_view.php
// /home/www/ 파일 시스템 트리
// 디렉토리 토글 + 파일 코드보기 / 새창열기
// daemon_*, cron_* 파일은 새창열기 비활성
// + 즉시 검색 + 전체 디렉토리 열기

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

// --------------------
// 공통 함수
// --------------------
function h($s){
    return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}

// --------------------
// 설정
// --------------------
$ROOT_PATH = '/home/www';
$ROOT_PATH = rtrim(realpath($ROOT_PATH), '/');
if ($ROOT_PATH === false) {
    die('루트 경로 오류');
}

// --------------------
// ASCII 트리 출력 (?tree=)
// --------------------
if (isset($_GET['tree'])) {
    $targetPath = $_GET['tree'];
    
    if ($targetPath === '' || $targetPath === null) {
        $realTarget = $ROOT_PATH;
    } else {
        $realTarget = realpath($targetPath);
        if (!$realTarget || strpos($realTarget, $ROOT_PATH) !== 0) {
            http_response_code(403);
            exit('접근 불가');
        }
    }
    
    if (!is_dir($realTarget)) {
        http_response_code(403);
        exit('디렉토리가 아님');
    }
    
    function generateAsciiTree($path, $rootLimit, $prefix = '', $isLast = false) {
        $result = '';
        $name = basename($path);
        if ($name === '') {
            $name = $path;
        }
        
        $result .= $prefix . ($isLast ? '└─ ' : '├─ ') . $name . "\n";
        
        $items = [];
        $dh = @opendir($path);
        if ($dh) {
            while (($item = readdir($dh)) !== false) {
                if ($item === '.' || $item === '..') continue;
                $itemPath = $path . '/' . $item;
                $realItem = realpath($itemPath);
                if (!$realItem || strpos($realItem, $rootLimit) !== 0) continue;
                if (is_link($realItem)) continue;
                $items[] = ['name' => $item, 'path' => $realItem, 'isDir' => is_dir($realItem)];
            }
            closedir($dh);
        }
        
        usort($items, function($a, $b) {
            if ($a['isDir'] === $b['isDir']) {
                return strcmp($a['name'], $b['name']);
            }
            return $a['isDir'] ? -1 : 1;
        });
        
        $count = count($items);
        foreach ($items as $index => $item) {
            $isLastItem = ($index === $count - 1);
            $newPrefix = $prefix . ($isLast ? '   ' : '│  ');
            if ($item['isDir']) {
                $result .= generateAsciiTree($item['path'], $rootLimit, $newPrefix, $isLastItem);
            } else {
                $result .= $newPrefix . ($isLastItem ? '└─ ' : '├─ ') . $item['name'] . "\n";
            }
        }
        
        return $result;
    }
    
    $treeText = $realTarget . "\n";
    $treeText .= generateAsciiTree($realTarget, $ROOT_PATH, '', true);
    
    header('Content-Type: text/plain; charset=UTF-8');
    echo $treeText;
    exit;
}

// --------------------
// 파일 코드보기 / tail 보기 (?view=, ?mode=tail)
// --------------------
if (isset($_GET['view'])) {
    $path = $_GET['view'];
    $realFile = realpath($ROOT_PATH . '/' . ltrim($path, '/'));
    $mode = $_GET['mode'] ?? 'full';

    if (!$realFile || strpos($realFile, $ROOT_PATH) !== 0) {
        http_response_code(403);
        exit('접근 불가');
    }

    $allow = ['txt','php','log','md','json','css','js','html'];
    $ext = strtolower(pathinfo($realFile, PATHINFO_EXTENSION));
    if (!in_array($ext, $allow)) {
        http_response_code(403);
        exit('허용되지 않은 파일');
    }

    $content = @file_get_contents($realFile);
    if ($content === false) {
        http_response_code(500);
        exit('파일 읽기 실패');
    }
    
    if ($mode === 'tail' && $ext === 'log') {
        $lines = explode("\n", $content);
        $n = min(200, count($lines));
        $content = implode("\n", array_slice($lines, -$n));
    }
    ?>




<title><?=h($path)?></title>



<div class="header"><?=h($path)?><?php if($mode==='tail'):?> (마지막 200줄)<?php endif;?></div>
<pre><?=h($content)?></pre>


<?php
    exit;
}

// --------------------
// 디렉토리 스캔 (파일 개수 계산 포함)
// --------------------
function countFiles($items) {
    $count = 0;
    foreach ($items as $it) {
        if ($it['type'] === 'file') {
            $count++;
        } else {
            $count += countFiles($it['children']);
        }
    }
    return $count;
}

function scanDirTree(string $path, string $root, int $depth = 0): array {
    // 재귀 깊이 5로 제한
    if ($depth > 5) {
        return [];
    }
    
    $items = [];
    $dh = @opendir($path);
    if (!$dh) return $items;

    while (($name = readdir($dh)) !== false) {
        // 디렉토리 내 항목 1000개 초과 시 스캔 중단
        if (count($items) >= 1000) {
            break;
        }
        
        if ($name === '.' || $name === '..') continue;

        $real = realpath($path.'/'.$name);
        if (!$real || strpos($real, $root) !== 0) continue;

        if (is_dir($real)) {
            $children = scanDirTree($real, $root, $depth + 1);
            $items[] = [
                'type'=>'dir',
                'name'=>$name,
                'path'=>$real,
                'children'=>$children,
                'fileCount'=>countFiles($children),
                'depth'=>$depth
            ];
        } else {
            $items[] = [
                'type'=>'file',
                'name'=>$name,
                'path'=>$real,
                'size'=>@filesize($real),
                'mtime'=>@filemtime($real),
                'depth'=>$depth
            ];
        }
    }
    closedir($dh);

    usort($items,function($a,$b){
        if ($a['type']===$b['type']) return strcmp($a['name'],$b['name']);
        return $a['type']==='dir'?-1:1;
    });
    return $items;
}

$scanStartTime = microtime(true);
$tree = scanDirTree($ROOT_PATH,$ROOT_PATH);
$scanEndTime = microtime(true);
$scanTime = date('Y-m-d H:i:s');

// 변경 감지를 위한 경로 목록 생성
function collectPaths($items) {
    $paths = [];
    foreach ($items as $it) {
        $paths[] = $it['path'];
        if ($it['type'] === 'dir' && !empty($it['children'])) {
            $paths = array_merge($paths, collectPaths($it['children']));
        }
    }
    return $paths;
}
$currentPaths = collectPaths($tree);
require_once '/home/www/GNU/_PAGE/head.php';
?>

<title>FILE TREE</title>
<style>
:root {
    --bg-primary: #0a0e27;
    --bg-secondary: #151932;
    --bg-tertiary: #1e2742;
    --bg-card: #1a1f3a;
    --bg-hover: #252b4a;
    --bg-border: #2a3458;
    --text-primary: #e2e8f0;
    --text-secondary: #94a3b8;
    --text-muted: #64748b;
    --accent-primary: #3b82f6;
    --accent-secondary: #8b5cf6;
    --success: #10b981;
    --danger: #ef4444;
    --warning: #f59e0b;
    --border-color: #2a3458;
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Malgun Gothic', 'Roboto', sans-serif;
    background: var(--bg-primary);
    color: var(--text-primary);
    line-height: 1.6;
}
.directory_tree { padding: 50px; }

h2 {
    color: #f59e0b !important;
    font-size: 24px;
    font-weight: 700;
    margin-bottom: 20px;
    color: var(--text-primary);
}

h2  i { color: #21b3d8 !important; padding-left: 10px; }

h3 {
    font-size: 18px;
    font-weight: 600;
    margin-bottom: 12px;
    color: var(--text-primary);
}

.scan-info {
    color: var(--text-muted);
    font-size: 13px;
    margin-bottom: 16px;
    padding: 8px 12px;
    background: var(--bg-tertiary);
    border-radius: 6px;
    border: 1px solid var(--border-color);
    display: inline-block;
}

.search-box {
    margin-bottom: 24px;
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
}

#searchInput {
    flex: 1;
    min-width: 300px;
    padding: 10px 16px;
    background: var(--bg-tertiary);
    border: 1px solid var(--border-color);
    border-radius: 8px;
    color: var(--text-primary);
    font-size: 14px;
    transition: all 0.2s ease;
}

#searchInput:focus {
    outline: none;
    border-color: var(--accent-primary);
    background: var(--bg-card);
}

#searchInput::placeholder {
    color: var(--text-muted);
}

.btn {
    padding: 8px 16px;
    background: var(--bg-tertiary);
    border: 1px solid var(--border-color);
    border-radius: 6px;
    color: var(--text-primary);
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.2s ease;
    text-decoration: none;
    display: inline-block;
}

.btn:hover {
    background: var(--bg-hover);
    border-color: var(--accent-primary);
    color: var(--accent-primary);
}

.status-info {
    color: var(--text-secondary);
    font-size: 13px;
    padding: 8px 12px;
    background: var(--bg-tertiary);
    border-radius: 6px;
    border: 1px solid var(--border-color);
}

.search-result {
    color: var(--accent-primary);
    font-size: 13px;
    font-weight: 600;
    padding: 8px 12px;
}

.pinned-section {
    margin-bottom: 24px;
    padding: 16px;
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: 8px;
}

.pinned-items {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 12px;
}

.pinned-item {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    background: var(--bg-tertiary);
    border: 1px solid var(--border-color);
    border-radius: 6px;
    font-size: 13px;
    transition: all 0.2s ease;
}

.pinned-item:hover {
    background: var(--bg-hover);
    border-color: var(--accent-primary);
}

.pinned-item-remove {
    cursor: pointer;
    color: var(--text-muted);
    font-weight: bold;
    padding: 0 4px;
}

.pinned-item-remove:hover {
    color: var(--danger);
}

.node {
    margin-left: 0;
}

.dir-head {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 12px;
    margin: 2px 0;
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: 6px;
    cursor: pointer;
    transition: all 0.2s ease;
    position: relative;
}

.dir-head:hover {
    background: var(--bg-hover);
    border-color: var(--accent-primary);
}

.dir-head.pinned {
    border-color: var(--warning);
    background: rgba(245, 158, 11, 0.1);
}

.dir-icon {
    font-size: 16px;
    flex-shrink: 0;
}

.dir-toggle {
    font-size: 12px;
    color: var(--text-secondary);
    width: 16px;
    text-align: center;
    flex-shrink: 0;
}

.dir-name {
    flex: 1;
    font-weight: 500;
    color: var(--text-primary);
}

.dir-count {
    color: var(--text-muted);
    font-size: 12px;
    padding: 2px 8px;
    background: var(--bg-tertiary);
    border-radius: 4px;
}

.dir-actions {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
}

.dir-pin {
    cursor: pointer;
    font-size: 14px;
    color: var(--text-muted);
    transition: color 0.2s;
    flex-shrink: 0;
}

.dir-pin:hover {
    color: var(--warning);
}

.dir-pin.active {
    color: var(--warning);
}

.dir-children {
    display: none;
    margin-left: 24px;
    border-left: 1px solid var(--border-color);
    padding-left: 12px;
}

.dir-children.open {
    display: block;
}

.file {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 12px;
    margin: 2px 0;
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: 6px;
    transition: all 0.2s ease;
    position: relative;
}

.file:hover {
    background: var(--bg-hover);
    border-color: var(--accent-primary);
}

.file.hidden {
    display: none;
}

.file.pinned {
    border-color: var(--warning);
    background: rgba(245, 158, 11, 0.1);
}

.file.recent-clicked {
    border-color: var(--accent-primary);
    background: rgba(59, 130, 246, 0.1);
}

.file-recent-strong {
    background: rgba(16, 185, 129, 0.15) !important;
    border-color: var(--success) !important;
}

.file-recent-weak {
    background: rgba(16, 185, 129, 0.08) !important;
}

.file-error {
    border-left: 3px solid var(--danger) !important;
}

.file-warn {
    border-left: 3px solid var(--warning) !important;
}

.file-fail {
    border-left: 3px solid var(--danger) !important;
}

.file.new-item {
    animation: pulse-new 2s ease-out;
}

@keyframes pulse-new {
    0% { background-color: rgba(59, 130, 246, 0.3); }
    100% { background-color: transparent; }
}

.file-icon {
    font-size: 16px;
    flex-shrink: 0;
}

.file-name {
    flex: 1;
    color: var(--text-primary);
    font-weight: 400;
}

.file-name-highlight {
    background: var(--warning);
    color: var(--bg-primary);
    padding: 2px 4px;
    border-radius: 3px;
    font-weight: 600;
}

.file-badge {
    display: inline-block;
    padding: 2px 6px;
    margin-left: 8px;
    font-size: 11px;
    font-weight: 600;
    border-radius: 4px;
    background: var(--bg-tertiary);
    color: var(--text-secondary);
}

.file-badge.php { background: rgba(119, 123, 192, 0.2); color: #777bc0; }
.file-badge.js { background: rgba(247, 223, 30, 0.2); color: #f7df1e; }
.file-badge.css { background: rgba(86, 61, 124, 0.2); color: #563d7c; }
.file-badge.json { background: rgba(255, 255, 255, 0.2); color: #fff; }
.file-badge.log { background: rgba(158, 158, 158, 0.2); color: #9e9e9e; }
.file-badge.md { background: rgba(0, 0, 0, 0.2); color: #fff; }
.file-badge.txt { background: rgba(158, 158, 158, 0.2); color: #9e9e9e; }
.file-badge.html { background: rgba(227, 79, 38, 0.2); color: #e34f26; }
.file-badge.daemon { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
.file-badge.cron { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }

.file-actions {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
}

.file-pin {
    cursor: pointer;
    font-size: 14px;
    color: var(--text-muted);
    transition: color 0.2s;
    flex-shrink: 0;
}

.file-pin:hover {
    color: var(--warning);
}

.file-pin.active {
    color: var(--warning);
}

.file-size {
    color: var(--text-muted);
    font-size: 12px;
    flex-shrink: 0;
}

.file-tooltip {
    position: fixed;
    background: var(--bg-tertiary);
    border: 1px solid var(--border-color);
    border-radius: 6px;
    padding: 8px 12px;
    font-size: 12px;
    color: var(--text-primary);
    white-space: pre-line;
    z-index: 10000;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.2s;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}

.file-tooltip.show {
    opacity: 1;
}

.tree-modal {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.7);
    z-index: 10000;
    align-items: center;
    justify-content: center;
}

.tree-modal.show {
    display: flex;
}

.tree-modal-content {
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: 12px;
    width: 90%;
    max-width: 800px;
    max-height: 90vh;
    display: flex;
    flex-direction: column;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.8);
}

.tree-modal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 16px 20px;
    border-bottom: 1px solid var(--border-color);
}

.tree-modal-title {
    font-size: 18px;
    font-weight: 600;
    color: var(--text-primary);
}

.tree-modal-close {
    background: none;
    border: none;
    color: var(--text-secondary);
    font-size: 24px;
    cursor: pointer;
    padding: 0;
    width: 32px;
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 4px;
    transition: all 0.2s;
}

.tree-modal-close:hover {
    background: var(--bg-hover);
    color: var(--text-primary);
}

.tree-modal-body {
    padding: 20px;
    overflow: auto;
    flex: 1;
}

.tree-modal-body pre {
    margin: 0;
    padding: 0;
    background: transparent;
    color: var(--text-primary);
    font-family: monospace;
    font-size: 13px;
    line-height: 1.6;
    white-space: pre;
    overflow-x: auto;
}

.tree-modal-body::-webkit-scrollbar {
    width: 10px;
    height: 10px;
}

.tree-modal-body::-webkit-scrollbar-track {
    background: var(--bg-secondary);
    border-radius: 10px;
}

.tree-modal-body::-webkit-scrollbar-thumb {
    background: var(--bg-tertiary);
    border-radius: 10px;
    border: 2px solid var(--bg-secondary);
}

.tree-modal-body::-webkit-scrollbar-thumb:hover {
    background: var(--bg-hover);
}

@media (max-width: 768px) {
    body {
        padding: 12px;
    }
    
    .search-box {
        flex-direction: column;
        align-items: stretch;
    }
    
    #searchInput {
        min-width: 100%;
    }
    
    .dir-children {
        margin-left: 16px;
    }
    
    .dir-actions,
    .file-actions {
        flex-direction: column;
        width: 100%;
    }
    
    .btn {
        width: 100%;
        text-align: center;
    }
}
/*---------| 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="directory_tree">

<h2><i class="fa-solid fa-folder"></i> 파일 트리 (/home/www)</h2>

<div style="margin-bottom:12px;">
  <button class="btn" onclick="showTreeModal('')" style="font-size:13px;padding:6px 12px;">디렉토리 구도 보기</button>
</div>

<div class="scan-info">스캔 기준: <?=h($scanTime)?></div>

<div id="pinnedSection" class="pinned-section" style="display:none;">
  <h3>⭐ 고정된 항목</h3>
  <div id="pinnedItems" class="pinned-items"></div>
</div>

<div class="search-box">
  <input id="searchInput" placeholder="파일명 검색 (즉시) - /: 포커스, ESC: 초기화, *: 전체열기, R: 상태초기화">
  <button id="toggleAllBtn" class="status-info">전체 디렉토리 열기</button>
  <button id="closeAllBtn" class="status-info">모든 디렉토리 닫기</button>
  <button id="resetStateBtn" class="status-info">상태 초기화</button>
  <span id="openCount" class="status-info">열림: 0</span>
  <span id="searchResult" class="search-result"></span>
</div>

<div class="node">
  <div class="dir-head" onclick="toggleDir(this)" data-path="/home/www" data-rel-path="" data-depth="0">
    <span class="dir-icon">📁</span>
    <div class="dir-toggle">▶</div>
    <div class="dir-name">/home/www</div>
    <div class="dir-actions">
      <span class="btn" onclick="event.stopPropagation();showTreeModal('')">디렉토리 구도</span>
      <span class="btn" onclick="event.stopPropagation();copyDirPath('/home/www')">경로복사</span>
      <span class="btn" onclick="event.stopPropagation();collapseDir('/home/www')">이 디렉토리 접기</span>
    </div>
    <span class="dir-pin" onclick="event.stopPropagation();togglePin(this, '/home/www', 'dir')">⭐</span>
  </div>
  <div class="dir-children">
<?php
function renderTree($items,$root){
    foreach($items as $it){
        if($it['type']==='dir'){
            $rel = ltrim(str_replace($root,'',$it['path']),'/');
            
            echo '<div class="node">';
            echo '<div class="dir-head" onclick="toggleDir(this)" data-path="'.h($it['path']).'" data-rel-path="'.h($rel).'" data-depth="'.h($it['depth'] ?? 0).'"';
            echo '>';
            echo '<span class="dir-icon">📁</span>';
            echo '<div class="dir-toggle">▶</div>';
            echo '<div class="dir-name">'.h($it['name']).'</div>';
            if ($it['fileCount'] > 0) {
                echo '<span class="dir-count">('.$it['fileCount'].')</span>';
            }
            echo '<div class="dir-actions">';
            echo '<span class="btn" onclick="event.stopPropagation();showTreeModalByPath(\''.h($it['path']).'\')">디렉토리 구도</span>';
            echo '<span class="btn" onclick="event.stopPropagation();copyDirPath(\''.h($it['path']).'\')">경로복사</span>';
            echo '<span class="btn" onclick="event.stopPropagation();collapseDir(\''.h($it['path']).'\')">이 디렉토리 접기</span>';
            echo '</div>';
            echo '<span class="dir-pin" onclick="event.stopPropagation();togglePin(this, \''.h($it['path']).'\', \'dir\')">⭐</span>';
            echo '</div><div class="dir-children">';
            renderTree($it['children'],$root);
            echo '</div></div>';
        } else {
            $rel = ltrim(str_replace($root,'',$it['path']),'/');
            $fname = $it['name'];
            $ext = strtolower(pathinfo($fname, PATHINFO_EXTENSION));
            // str_starts_with() 대신 substr() 사용 (PHP 7.x 호환)
            $isDaemon = (strlen($fname) >= 7 && substr($fname, 0, 7) === 'daemon_');
            $isCron = (strlen($fname) >= 5 && substr($fname, 0, 5) === 'cron_');
            $isDaemonCron = $isDaemon || $isCron;
            $mtime = $it['mtime'] ?? 0;
            $now = time();
            $diff = $now - $mtime;
            $recentClass = '';
            if ($diff < 600) {
                $recentClass = 'file-recent-strong';
            } elseif ($diff < 3600) {
                $recentClass = 'file-recent-weak';
            }
            
            $errorClass = '';
            $lowerName = strtolower($fname);
            if (strpos($lowerName, 'error') !== false) {
                $errorClass = 'file-error';
            } elseif (strpos($lowerName, 'warn') !== false) {
                $errorClass = 'file-warn';
            } elseif (strpos($lowerName, 'fail') !== false) {
                $errorClass = 'file-fail';
            }
            
            $tooltipText = "경로: /$rel\n";
            $tooltipText .= "확장자: " . ($ext ? '.' . $ext : '없음') . "\n";
            $tooltipText .= "크기: " . number_format($it['size']) . " bytes\n";
            if ($mtime > 0) {
                $tooltipText .= "수정: " . date('Y-m-d H:i:s', $mtime);
            }

            echo '<div class="file" data-name="'.h(strtolower($fname)).'" data-tooltip="'.h($tooltipText).'" data-path="'.h($rel).'" data-full-path="'.h($it['path']).'" data-depth="'.h($it['depth'] ?? 0).'"';
            $classes = [];
            if ($recentClass) $classes[] = $recentClass;
            if ($errorClass) $classes[] = $errorClass;
            if (!empty($classes)) {
                echo ' class="'.implode(' ', $classes).'"';
            }
            echo '>';
            echo '<span class="file-icon">📄</span>';
            echo '<div class="file-name">'.h($fname);
            if ($ext && in_array($ext, ['php','js','css','json','log','md','txt','html'])) {
                echo '<span class="file-badge '.h($ext).'">.'.h($ext).'</span>';
            }
            if ($isDaemonCron) {
                echo '<span class="file-badge '.h($isDaemon?'daemon':'cron').'">'.h($isDaemon?'DAEMON':'CRON').'</span>';
            }
            echo '</div>';
            echo '<div class="file-actions">';

            if (!$isDaemonCron && !empty($rel)) {
                echo '<a class="btn" target="_blank" href="/'.h($rel).'">새창열기</a>';
            }
            echo '<a class="btn" target="_blank" href="?view='.urlencode($rel).'">코드보기</a>';
            if ($ext === 'log') {
                echo '<a class="btn" target="_blank" href="?view='.urlencode($rel).'&mode=tail">tail 보기</a>';
            }
            echo '<span class="btn" onclick="copyPath(\''.h($rel).'\')">경로복사</span>';

            echo '</div>';
            echo '<span class="file-pin" onclick="event.stopPropagation();togglePin(this, \''.h($rel).'\', \'file\')">⭐</span>';
            echo '<div class="file-size">('.number_format($it['size']).' bytes)</div>';
            echo '</div>';
        }
    }
}
renderTree($tree,$ROOT_PATH);
?>
  </div>
</div>

<div id="treeModal" class="tree-modal">
  <div class="tree-modal-content">
    <div class="tree-modal-header">
      <div class="tree-modal-title">디렉토리 구도</div>
      <button class="tree-modal-close" onclick="closeTreeModal()">✕</button>
    </div>
    <div class="tree-modal-body">
      <pre id="treeModalContent"></pre>
    </div>
  </div>
</div>

<script>
let allDirsOpen = false;
let openBatch = [];
let fileTooltip = null;

// 검색 관련 변수 (이벤트 리스너에서 사용되므로 먼저 선언)
const searchInput = document.getElementById('searchInput');
const searchResult = document.getElementById('searchResult');
let searchActive = false;

// localStorage 키
const STORAGE_OPEN_DIRS = 'dirTree_openDirs';
const STORAGE_PINNED = 'dirTree_pinned';
const STORAGE_RECENT_CLICK = 'dirTree_recentClick';
const STORAGE_PREV_PATHS = 'dirTree_prevPaths';

// 현재 열린 디렉토리 개수 업데이트
function updateOpenCount() {
    let count = 0;
    document.querySelectorAll('.dir-children').forEach(el => {
        if (el.classList.contains('open')) {
            count++;
        }
    });
    document.getElementById('openCount').textContent = '열림: ' + count;
}

// 변경 감지 표시
function markNewItems() {
    try {
        const prevPaths = JSON.parse(localStorage.getItem(STORAGE_PREV_PATHS) || '[]');
        const currentPaths = <?=json_encode($currentPaths)?>;
        
        currentPaths.forEach(path => {
            if (!prevPaths.includes(path)) {
                const dirHead = document.querySelector(`.dir-head[data-path="${path}"]`);
                const file = document.querySelector(`.file[data-full-path="${path}"]`);
                if (dirHead) {
                    dirHead.classList.add('new-item');
                }
                if (file) {
                    file.classList.add('new-item');
                }
            }
        });
        
        localStorage.setItem(STORAGE_PREV_PATHS, JSON.stringify(currentPaths));
    } catch(e) {}
}

// ASCII 트리 모달 - 전체 디렉토리용
function showTreeModal(relPath) {
    const modal = document.getElementById('treeModal');
    const content = document.getElementById('treeModalContent');
    const title = modal.querySelector('.tree-modal-title');
    
    content.textContent = '로딩 중...';
    modal.classList.add('show');
    
    const url = '?tree=' + encodeURIComponent(relPath || '');
    fetch(url)
        .then(response => {
            if (!response.ok) throw new Error('로딩 실패');
            return response.text();
        })
        .then(text => {
            content.textContent = text;
            title.textContent = '디렉토리 구도: ' + (relPath || '/home/www');
        })
        .catch(err => {
            content.textContent = '로딩 실패: ' + err.message;
        });
}

// ASCII 트리 모달 - 특정 디렉토리용
function showTreeModalByPath(fullPath) {
    const modal = document.getElementById('treeModal');
    const content = document.getElementById('treeModalContent');
    const title = modal.querySelector('.tree-modal-title');
    
    content.textContent = '로딩 중...';
    modal.classList.add('show');
    
    if (!fullPath) {
        content.textContent = '경로 오류';
        return;
    }
    
    const url = '?tree=' + encodeURIComponent(fullPath);
    fetch(url)
        .then(response => {
            if (!response.ok) throw new Error('로딩 실패');
            return response.text();
        })
        .then(text => {
            content.textContent = text;
            title.textContent = '디렉토리 구도: ' + fullPath;
        })
        .catch(err => {
            content.textContent = '로딩 실패: ' + err.message;
        });
}

function closeTreeModal() {
    document.getElementById('treeModal').classList.remove('show');
}

// 모든 디렉토리 닫기
function closeAllDirs() {
    document.querySelectorAll('.dir-children').forEach(el => {
        el.classList.remove('open');
    });
    document.querySelectorAll('.dir-toggle').forEach(el => {
        el.textContent = '▶';
    });
    localStorage.removeItem(STORAGE_OPEN_DIRS);
    allDirsOpen = false;
    document.getElementById('toggleAllBtn').textContent = '전체 디렉토리 열기';
    updateOpenCount();
}

// 상태 초기화
function resetState() {
    closeAllDirs();
    document.getElementById('searchInput').value = '';
    updateSearch();
    document.querySelectorAll('.file').forEach(f => f.classList.remove('hidden'));
    localStorage.removeItem(STORAGE_PINNED);
    localStorage.removeItem(STORAGE_RECENT_CLICK);
    document.querySelectorAll('.file.recent-clicked').forEach(f => {
        f.classList.remove('recent-clicked');
    });
    document.querySelectorAll('.dir-head.pinned, .file.pinned').forEach(el => {
        el.classList.remove('pinned');
    });
    document.querySelectorAll('.dir-pin.active, .file-pin.active').forEach(el => {
        el.classList.remove('active');
    });
    document.getElementById('pinnedSection').style.display = 'none';
    updatePinnedSection();
}

// 디렉토리 접기
function collapseDir(path) {
    const dirHead = document.querySelector(`.dir-head[data-path="${path}"]`);
    if (!dirHead) return;
    
    const children = dirHead.nextElementSibling;
    if (children && children.classList.contains('dir-children')) {
        children.classList.remove('open');
        const toggle = dirHead.querySelector('.dir-toggle');
        if (toggle) toggle.textContent = '▶';
        
        // 하위 모든 디렉토리도 닫기
        children.querySelectorAll('.dir-children').forEach(subChild => {
            subChild.classList.remove('open');
        });
        children.querySelectorAll('.dir-toggle').forEach(subToggle => {
            subToggle.textContent = '▶';
        });
    }
    
    saveOpenState();
    updateOpenCount();
}

// 공통 디렉토리 열기 함수 (개별/전체/검색 모두 사용)
function openDir(dirHead) {
    const children = dirHead.nextElementSibling;
    if (!children || !children.classList.contains('dir-children')) return;
    
    // 이미 열려있으면 스킵
    if (children.classList.contains('open')) {
        return;
    }
    
    // 부모가 닫혀있으면 먼저 열기 (재귀적으로)
    const parentNode = dirHead.closest('.node');
    if (parentNode && parentNode.parentElement) {
        const parentContainer = parentNode.parentElement;
        if (parentContainer.classList.contains('dir-children')) {
            const grandParentNode = parentContainer.parentElement;
            if (grandParentNode) {
                const parentHead = grandParentNode.querySelector('.dir-head');
                if (parentHead && parentHead !== dirHead) {
                    const parentChildren = parentHead.nextElementSibling;
                    if (parentChildren && parentChildren.classList.contains('dir-children')) {
                        if (!parentChildren.classList.contains('open')) {
                            openDir(parentHead);
                        }
                    }
                }
            }
        }
    }
    
    // open 클래스만 추가 (CSS가 display를 관리)
    children.classList.add('open');
    
    const toggle = dirHead.querySelector('.dir-toggle');
    if (toggle) toggle.textContent = '▼';
}

// 공통 디렉토리 닫기 함수
function closeDir(dirHead) {
    const children = dirHead.nextElementSibling;
    if (!children || !children.classList.contains('dir-children')) return;
    
    // open 클래스만 제거 (CSS가 display를 관리)
    children.classList.remove('open');
    
    const toggle = dirHead.querySelector('.dir-toggle');
    if (toggle) toggle.textContent = '▶';
}

// ESC 키로 모달 닫기
document.addEventListener('keydown', function(e){
    if (e.target.tagName === 'INPUT' && e.target !== searchInput) return;
    
    if (e.key === '/' && e.target !== searchInput) {
        e.preventDefault();
        searchInput.focus();
    } else if (e.key === 'Escape') {
        const modal = document.getElementById('treeModal');
        if (modal.classList.contains('show')) {
            closeTreeModal();
            return;
        }
        searchInput.value = '';
        updateSearch();
        document.querySelectorAll('.file').forEach(f => f.classList.remove('hidden'));
    } else if (e.key === '*') {
        e.preventDefault();
        openAllDirs();
    } else if (e.key === 'r' || e.key === 'R') {
        if (e.target.tagName !== 'INPUT') {
            e.preventDefault();
            resetState();
        }
    }
});

// 열림 상태 저장/복원
function saveOpenState() {
    const openDirs = [];
    document.querySelectorAll('.dir-children').forEach(el => {
        if (el.classList.contains('open')) {
            const dirHead = el.previousElementSibling;
            if (dirHead && dirHead.classList.contains('dir-head') && dirHead.dataset.path) {
                openDirs.push(dirHead.dataset.path);
            }
        }
    });
    localStorage.setItem(STORAGE_OPEN_DIRS, JSON.stringify(openDirs));
    updateOpenCount();
}

function restoreOpenState() {
    try {
        const saved = localStorage.getItem(STORAGE_OPEN_DIRS);
        if (!saved) return;
        const openDirs = JSON.parse(saved);
        
        // 깊이 순서대로 정렬 (부모를 먼저 열기 위해)
        const dirHeads = [];
        openDirs.forEach(path => {
            const dirHead = document.querySelector(`.dir-head[data-path="${path}"]`);
            if (dirHead) {
                dirHeads.push(dirHead);
            }
        });
        
        dirHeads.sort((a, b) => {
            const depthA = parseInt(a.dataset.depth || '0');
            const depthB = parseInt(b.dataset.depth || '0');
            return depthA - depthB;
        });
        
        dirHeads.forEach(dirHead => {
            openDir(dirHead);
        });
        
        updateOpenCount();
    } catch(e) {}
}

// 핀 기능
function getPinnedItems() {
    try {
        const saved = localStorage.getItem(STORAGE_PINNED);
        return saved ? JSON.parse(saved) : [];
    } catch(e) {
        return [];
    }
}

function savePinnedItems(items) {
    localStorage.setItem(STORAGE_PINNED, JSON.stringify(items));
}

function togglePin(el, path, type) {
    const pinned = getPinnedItems();
    const index = pinned.findIndex(p => p.path === path);
    
    if (index >= 0) {
        pinned.splice(index, 1);
        el.classList.remove('active');
        if (type === 'dir') {
            el.closest('.dir-head').classList.remove('pinned');
        } else {
            el.closest('.file').classList.remove('pinned');
        }
    } else {
        pinned.push({path: path, type: type});
        el.classList.add('active');
        if (type === 'dir') {
            el.closest('.dir-head').classList.add('pinned');
        } else {
            el.closest('.file').classList.add('pinned');
        }
    }
    
    savePinnedItems(pinned);
    updatePinnedSection();
}

function updatePinnedSection() {
    const pinned = getPinnedItems();
    const section = document.getElementById('pinnedSection');
    const items = document.getElementById('pinnedItems');
    
    if (pinned.length === 0) {
        section.style.display = 'none';
        return;
    }
    
    section.style.display = 'block';
    items.innerHTML = '';
    
    pinned.forEach(p => {
        const item = document.createElement('div');
        item.className = 'pinned-item';
        const name = p.path.split('/').pop() || p.path;
        item.innerHTML = `
            <span>${p.type === 'dir' ? '📁' : '📄'}</span>
            <span>${name}</span>
            <span class="pinned-item-remove" onclick="removePin('${p.path}')">✕</span>
        `;
        item.style.cursor = 'pointer';
        item.onclick = function() {
            if (p.type === 'dir') {
                const dirHead = document.querySelector(`.dir-head[data-path="${p.path}"]`);
                if (dirHead) {
                    toggleDir(dirHead);
                    dirHead.scrollIntoView({behavior:'smooth', block:'center'});
                }
            } else {
                const file = document.querySelector(`.file[data-path="${p.path}"]`);
                if (file) {
                    file.scrollIntoView({behavior:'smooth', block:'center'});
                }
            }
        };
        items.appendChild(item);
    });
    
    // 핀 표시 업데이트
    document.querySelectorAll('.dir-pin, .file-pin').forEach(el => {
        el.classList.remove('active');
    });
    document.querySelectorAll('.dir-head, .file').forEach(el => {
        el.classList.remove('pinned');
    });
    
    pinned.forEach(p => {
        if (p.type === 'dir') {
            const dirHead = document.querySelector(`.dir-head[data-path="${p.path}"]`);
            if (dirHead) {
                dirHead.classList.add('pinned');
                const pin = dirHead.querySelector('.dir-pin');
                if (pin) pin.classList.add('active');
            }
        } else {
            const file = document.querySelector(`.file[data-path="${p.path}"]`);
            if (file) {
                file.classList.add('pinned');
                const pin = file.querySelector('.file-pin');
                if (pin) pin.classList.add('active');
            }
        }
    });
}

function removePin(path) {
    const pinned = getPinnedItems();
    const filtered = pinned.filter(p => p.path !== path);
    savePinnedItems(filtered);
    updatePinnedSection();
}

// 디렉토리 경로 복사
function copyDirPath(path) {
    if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard.writeText(path).then(() => {
            // 복사 성공 (조용히)
        }).catch(() => {});
    } else {
        const textarea = document.createElement('textarea');
        textarea.value = path;
        textarea.style.position = 'fixed';
        textarea.style.opacity = '0';
        document.body.appendChild(textarea);
        textarea.select();
        try {
            document.execCommand('copy');
        } catch(e) {}
        document.body.removeChild(textarea);
    }
}

// 파일 경로 복사
function copyPath(path) {
    const fullPath = '/' + path;
    if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard.writeText(fullPath).then(() => {
            // 복사 성공 (조용히)
        }).catch(() => {});
    } else {
        const textarea = document.createElement('textarea');
        textarea.value = fullPath;
        textarea.style.position = 'fixed';
        textarea.style.opacity = '0';
        document.body.appendChild(textarea);
        textarea.select();
        try {
            document.execCommand('copy');
        } catch(e) {}
        document.body.removeChild(textarea);
    }
}

// 최근 클릭 파일 표시
function markRecentClick(path) {
    try {
        const recent = JSON.parse(localStorage.getItem(STORAGE_RECENT_CLICK) || '[]');
        const index = recent.indexOf(path);
        if (index >= 0) recent.splice(index, 1);
        recent.unshift(path);
        if (recent.length > 5) recent.pop();
        localStorage.setItem(STORAGE_RECENT_CLICK, JSON.stringify(recent));
        
        document.querySelectorAll('.file.recent-clicked').forEach(f => {
            f.classList.remove('recent-clicked');
        });
        
        recent.forEach(p => {
            const file = document.querySelector(`.file[data-path="${p}"]`);
            if (file) file.classList.add('recent-clicked');
        });
    } catch(e) {}
}

function toggleDir(h){
  const c = h.nextElementSibling;
  if (!c || !c.classList.contains('dir-children')) return;
  
  // 열림 상태 체크
  const isOpen = c.classList.contains('open');
  
  if (isOpen) {
    closeDir(h);
  } else {
    openDir(h);
  }
  saveOpenState();
}

function openAllDirs(forceOpen = false){
  if (forceOpen || !allDirsOpen) {
    allDirsOpen = true;
    document.getElementById('toggleAllBtn').textContent = '전체 디렉토리 닫기';
    
    // 모든 디렉토리 열기 (깊이 순서대로 부모부터)
    const allDirHeads = document.querySelectorAll('.dir-head');
    const dirHeadsArray = Array.from(allDirHeads);
    
    // 깊이 순서대로 정렬 (부모를 먼저 열기 위해)
    dirHeadsArray.sort((a, b) => {
      const depthA = parseInt(a.dataset.depth || '0');
      const depthB = parseInt(b.dataset.depth || '0');
      return depthA - depthB;
    });
    
    // 각 디렉토리를 openDir 함수로 열기 (부모 자동 처리)
    dirHeadsArray.forEach(dirHead => {
      const children = dirHead.nextElementSibling;
      if (children && children.classList.contains('dir-children')) {
        if (!children.classList.contains('open')) {
          openDir(dirHead);
        }
      }
    });
    
    saveOpenState();
  } else {
    const allDirHeads = document.querySelectorAll('.dir-head');
    allDirHeads.forEach(dirHead => {
      closeDir(dirHead);
    });
    allDirsOpen = false;
    document.getElementById('toggleAllBtn').textContent = '전체 디렉토리 열기';
    saveOpenState();
  }
}

document.getElementById('toggleAllBtn').addEventListener('click', function(){
  openAllDirs();
});

document.getElementById('closeAllBtn').addEventListener('click', function(){
  closeAllDirs();
});

document.getElementById('resetStateBtn').addEventListener('click', function(){
  resetState();
});

// 검색 하이라이트 함수
function highlightText(text, query) {
    if (!query) return text;
    const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
    return text.replace(regex, '<span class="file-name-highlight">$1</span>');
}

// 즉시 파일명 검색
function updateSearch() {
  const q = searchInput.value.toLowerCase().trim();
  searchActive = q.length > 0;
  
  const files = document.querySelectorAll('.file');
  let visibleCount = 0;
  
  files.forEach(f => {
    const name = f.dataset.name;
    const nameEl = f.querySelector('.file-name');
    if (q && !name.includes(q)) {
      f.classList.add('hidden');
    } else {
      f.classList.remove('hidden');
      if (!f.classList.contains('hidden')) {
        visibleCount++;
        if (q && nameEl) {
          // 원본 HTML 저장 (처음 한 번만)
          if (!nameEl.dataset.originalHtml) {
            nameEl.dataset.originalHtml = nameEl.innerHTML;
          }
          
          // 파일명만 추출 (배지 제외)
          let fileName = '';
          let badges = '';
          const children = nameEl.childNodes;
          
          children.forEach(child => {
            if (child.nodeType === 3) { // 텍스트 노드
              fileName += child.textContent;
            } else if (child.nodeType === 1 && child.classList.contains('file-badge')) { // 배지 요소
              badges += child.outerHTML;
            }
          });
          
          // 파일명만 하이라이트
          const highlightedName = highlightText(fileName.trim(), q);
          nameEl.innerHTML = highlightedName + badges;
        }
      }
    }
  });
  
  if (searchActive && searchResult) {
    searchResult.textContent = '결과: ' + visibleCount;
    // 모든 디렉토리 열기
    document.querySelectorAll('.dir-head').forEach(dirHead => {
      const children = dirHead.nextElementSibling;
      if (children && children.classList.contains('dir-children')) {
        if (!children.classList.contains('open')) {
          openDir(dirHead);
        }
      }
    });
    allDirsOpen = true;
    document.getElementById('toggleAllBtn').textContent = '전체 디렉토리 닫기';
  } else {
    if (searchResult) {
      searchResult.textContent = '';
    }
    // 하이라이트 제거 - 원본 HTML 복원
    document.querySelectorAll('.file-name').forEach(el => {
      if (el.dataset.originalHtml) {
        el.innerHTML = el.dataset.originalHtml;
        delete el.dataset.originalHtml;
      }
    });
  }
}

searchInput.addEventListener('input', updateSearch);

// 파일 클릭 시 최근 클릭 표시
document.addEventListener('click', function(e){
  const file = e.target.closest('.file');
  if (file && file.dataset.path) {
    markRecentClick(file.dataset.path);
  }
});

// 파일 hover tooltip - 완전 분리
document.querySelectorAll('.file').forEach(file => {
  file.addEventListener('mouseenter', function(e){
    const tooltipText = this.dataset.tooltip;
    if (!tooltipText) return;
    
    if (!fileTooltip) {
      fileTooltip = document.createElement('div');
      fileTooltip.className = 'file-tooltip';
      document.body.appendChild(fileTooltip);
    }
    
    const rect = this.getBoundingClientRect();
    fileTooltip.textContent = tooltipText;
    fileTooltip.style.left = rect.left + 'px';
    fileTooltip.style.top = (rect.top - 10) + 'px';
    fileTooltip.style.transform = 'translateY(-100%)';
    fileTooltip.classList.add('show');
  });
  
  file.addEventListener('mouseleave', function(e){
    if (fileTooltip) {
      fileTooltip.classList.remove('show');
    }
  });
});

// 초기화
document.addEventListener('DOMContentLoaded', function(){
  restoreOpenState();
  updatePinnedSection();
  markNewItems();
  
  try {
    const recent = JSON.parse(localStorage.getItem(STORAGE_RECENT_CLICK) || '[]');
    recent.forEach(path => {
      const file = document.querySelector(`.file[data-path="${path}"]`);
      if (file) file.classList.add('recent-clicked');
    });
  } catch(e) {}
});
</script>

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