# GLOBEAU.KR 파일 기반 마크다운 뷰어 시스템 통합 명세서

## 1. 개요 및 기술 스택
* **시스템 명칭:** 파일 기반 마크다운 뷰어 시스템 (File-based Markdown Viewer System)
* **목적:** 데이터베이스 없이 서버 저장소에 위치한 마크다운(`.md`) 파일을 직접 읽어와 웹에서 체계적으로 조회, 폴더 및 `01_readme.md` 기반 섹션 자동 분류, 아코디언 토글, 뷰어 상태 유지(localStorage), 맞춤형 타이틀 매핑 및 다중 키워드 검색을 수행하는 자체 문서 센터 구축
* **Tech Stack:**
  * **Backend:** Laravel 13, PHP 8.4, Apache Web Server, Laravel Storage Facade
  * **Frontend UI:** Bootstrap 5 (Collapse 아코디언), FontAwesome 6, GitHub Official Markdown CSS (`github-markdown-light.min.css`)
  * **Markdown & Highlighting Engine:** `marked.js` (클라이언트 마크다운 파서), `highlight.js` (소스코드 신택스 하이라이팅)

---

## 2. 파일 명명 규칙 및 디렉토리 구조 및 주요 파일 경로
* **파일명 표준:** 대소문자 혼동 및 URL 인코딩 오류를 방지하기 위해 **모두 소문자와 언더바(`_`)를 조합한 형태**를 원칙으로 채택합니다. (예: `architecture.md`, `database.md`, `deployment.md`)
* **저장소 구조:** `storage/app/public/documents/` 하위에 폴더별 디렉토리를 생성하여 관리합니다.
  * 각 폴더 내에 **`01_readme.md`** (또는 `readme.md`) 파일을 두어 해당 폴더의 섹션 메타데이터(`section_key`, `section_label`)를 정의합니다.
  * 나머지 문서 파일들은 파일명 오름차순(01234abc 순서)으로 정렬되며, 폴더가 없는 루트 파일들은 리스트의 맨 마지막에 배치됩니다.
* **리스트 제외 대상:** `01_readme.md` 및 `readme.md` 파일은 메타데이터 정의 용도로만 사용되며, 웹 문서 목록 리스트에는 노출되지 않습니다.

### 2.1. 주요 파일 및 경로 명세
* **라우트 파일:** `routes/web.php`
* **컨트롤러 파일:** `app/Http/Controllers/DocumentController.php`
* **문서 목록 및 검색 뷰:** `resources/views/documents/index.blade.php`
* **개별 문서 상세 뷰:** `resources/views/documents/show.blade.php`vk
* **마크다운 저장소:** `storage/app/public/documents/*.md`

### 2.2. 라우트 설정 (`routes/web.php`)
    use App\Http\Controllers\DocumentController;

    Route::get('/docs', [DocumentController::class, 'index']);  // 문서 목록, 폴더별 아코디언 그룹화 및 다중 키워드 검색
    Route::get('/docs/{page}', [DocumentController::class, 'show']);  // 개별 문서 상세 보기 (상단 YAML 영역 자동 제거)

---

## 3. 백엔드 핵심 콘트롤러 (`app/Http/Controllers/DocumentController.php`)

### 3.1. Front Matter (YAML) 동적 파싱
문서 파일 상단에 위치한 YAML 메타데이터 영역을 파싱하여 타이틀 및 섹션 정보를 동적으로 추출합니다.
#### [Front Matter 예시]
    ---
    title: 시스템 아키텍처 및 구조 설계
    section_key: chapter_1
    section_label: 제1장 - 시스템 및 데이터베이스 설계
    ---

### 3.2. 폴더 선행 배치 및 내림차순 정렬
* `documents` 하위 디렉토리 목록을 가져와 내림차순(`rsort`)으로 정렬하고, 폴더가 없는 루트 파일 그룹을 리스트 최하단에 배치합니다.
* 폴더 내 개별 마크다운 파일들은 파일명 기준 오름차순(01234abc 순서)으로 정렬합니다.

### 3.3. 다중 키워드 검색 및 파일당 매칭 제한
* 사용자가 입력한 검색어(`q`)를 분석하여 원본 구문, 공백 제거 형태, 2글자 이상의 개별 단어 조합으로 파싱합니다.
* 한 문서 내에서 검색 결과가 난립하지 않도록 **파일당 최대 5개 매칭 제한** 및 중복 위치 방지 로직을 적용하고, 주변 전후 텍스트를 발췌한 `snippet`과 고유 순번(`match_index`)을 부여합니다.

### 3.4. 상세 보기 시 YAML 자동 제거 (`show`)
* 개별 문서를 조회할 때 정규식을 이용해 파일 상단의 YAML Front Matter 영역(`--- ... ---`)을 자동으로 제거하여, 본문 내용만 깔끔하게 렌더링되도록 전달합니다.

---

## 4. 프론트엔드 뷰 파이프라인

### 4.1. 문서 목록 및 아코디언 뷰 (`resources/views/documents/index.blade.php`)
* 컨트롤러에서 가공된 섹션별 데이터를 바탕으로 Bootstrap Collapse 아코디언 UI를 구성합니다.
* 각 섹션 헤더를 클릭하여 개별적으로 열고 닫을 수 있으며, 브라우저 `localStorage`를 연동하여 문서를 읽고 뒤로 가기를 눌렀을 때도 **이전에 열어두었던 아코디언 상태와 스크롤 위치가 완벽히 복원**됩니다.
* 검색 중일 때는 일치하는 문서를 바로 확인할 수 있도록 검색 결과가 포함된 섹션들이 자동으로 펼쳐집니다.

### ② 개별 상세 뷰 및 자동 하이라이팅 (`resources/views/documents/show.blade.php`)
* `marked.js`를 통해 마크다운 문자열을 GitHub 스타일의 HTML로 안전하게 렌더링합니다.
* URL 파라미터에 검색어와 순번이 포함되어 있는 경우, 브라우저 내장 탐색 로직을 활용하여 지정된 N번째 위치를 정확히 탐색합니다.

---

## 5. 부록: 전체 사용 파일 소스 코드 및 상세 코멘트

### A. 라우트 파일 (`routes/web.php`)
    <?php

    use Illuminate\Support\Facades\Route;
    use App\Http\Controllers\DocumentController;

    // 문서 센터 메인 목록 및 다중 키워드 검색 라우트
    Route::get('/docs', [DocumentController::class, 'index']);

    // 개별 마크다운 문서 상세 조회 라우트 (slug 기반)
    Route::get('/docs/{page}', [DocumentController::class, 'show']);

### B. 컨트롤러 파일 (`app/Http/Controllers/DocumentController.php`)
    <?php

    namespace App\Http\Controllers;

    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Storage;

    class DocumentController extends Controller
    {
        /**
         * 문서 목록 및 다중 키워드 검색 처리
         * - 폴더 디렉토리 내림차순 정렬 및 루트 파일 후행 배치
         * - 01_readme.md 기반 섹션 동적 오버라이드 및 readme 파일 리스트 제외
         */
        public function index(Request $request)
        {
            $disk = Storage::disk('public');
            
            // documents 디렉토리 내 하위 폴더 목록 조회 후 내림차순 정렬
            $directories =$disk->directories('documents');
            rsort($directories);

            // 폴더가 없는 루트 파일을 마지막에 처리하기 위해 빈 문자열('') 추가 병합
            $allFolders = array_merge($directories, ['']);$searchQuery = trim($request->input('q', ''));$documents = [];

            // 검색어 파싱 및 다중 키워드 조합 생성 (원본, 공백 제거, 2글자 이상 단어들)
            $searchKeywords = [];
            if (!empty($searchQuery)) {
                $lowerQuery = mb_strtolower($searchQuery);
                $searchKeywords[] =$lowerQuery;

                $splitted = preg_split('/\s+/',$searchQuery);
                if (count($splitted) > 1) {
                    $searchKeywords[] = mb_strtolower(implode('',$splitted));
                    
                    foreach ($splitted as$word) {
                        $word = trim($word);
                        if (mb_strlen($word) >= 2) {
                            $searchKeywords[] = mb_strtolower($word);
                        }
                    }
                }
                $searchKeywords = array_unique($searchKeywords);
            }

            foreach ($allFolders as $folder) {$folderPath = $folder === '' ? 'documents' :$folder;
                $files = $disk->files($folderPath);

                // 1. 해당 폴더의 섹션 정보 기본값 설정
                $section = [
                    'key' => $folder === '' ? 'general' : basename($folder),
                    'label' => $folder === '' ? '기타 문서' : ucfirst(basename($folder))
                ];

                // 2. 폴더 내 01_readme.md 또는 readme.md 확인하여 섹션 정보 오버라이드
                foreach ($files as$file) {
                    $fileName = strtolower(basename($file));
                    if (in_array($fileName, ['01_readme.md', 'readme.md'])) {
                        $readmeContent =$disk->get($file);$readmeInfo = $this->parseFrontMatter($readmeContent);
                        if (!empty($readmeInfo['section']['key']) &&$readmeInfo['section']['key'] !== 'appendix') {
                            $section =$readmeInfo['section'];
                        }
                        break;
                    }
                }

                // 폴더 내 파일명 기준 오름차순 정렬 (01234abc 순서)
                sort($files);

                foreach ($files as$file) {
                    if (in_array(pathinfo($file, PATHINFO_EXTENSION), ['md', 'markdown'])) {
                        $fileName = strtolower(basename($file));
                        
                        // 3. readme 파일은 리스트 목록에서 제외
                        if (in_array($fileName, ['01_readme.md', 'readme.md'])) {
                            continue;
                        }

                        $slug = pathinfo($file, PATHINFO_FILENAME);
                        $content =$disk->get($file);$updatedAt = date('Y-m-d H:i:s', $disk->lastModified($file));
                        
                        // 파일 상단 Front Matter에서 타이틀 추출
                        $docInfo =$this->parseFrontMatter($content);$title = $docInfo['title'] ?? ucfirst(str_replace('_', ' ', $slug));

                        // 검색어가 없을 경우 전체 기본 목록 구성
                        if (empty($searchQuery)) {$documents[] = [
                                'slug' => $slug,
                                'title' => $title,
                                'snippet' => '',
                                'updated_at' => $updatedAt,
                                'match_index' => 1,
                                'keyword' => '',
                                'section_key' => $section['key'],
                                'section_label' => $section['label'],
                                'folder' => $folder
                            ];
                            continue;
                        }

                        $lowerContent = mb_strtolower($content);$lowerTitle = mb_strtolower($title);$fileMatchCount = 0;    
                        $matchedPositions = []; 

                        foreach ($searchKeywords as$kw) {
                            // 제목 매칭 검사 (파일당 최대 5개 제한 적용)
                            if (str_contains($lowerTitle,$kw)) {
                                $hash = 'title_' .$kw;
                                if (!isset($matchedPositions[$hash])) {
                                    if ($fileMatchCount >= 5) break; 
                                    
                                    $matchedPositions[$hash] = true;
                                    $fileMatchCount++;

                                    $documents[] = [
                                        'slug' => $slug,
                                        'title' => $title,
                                        'snippet' => "제목에서 '{$kw}' 항목을 찾았습니다.",
                                        'updated_at' => $updatedAt,
                                        'match_index' => $fileMatchCount,
                                        'keyword' => $kw,
                                        'section_key' => $section['key'],
                                        'section_label' => $section['label'],
                                        'folder' => $folder
                                    ];
                                }
                            }

                            // 본문 매칭 검사 및 주변 텍스트 snippet 발췌
                            $offset = 0;
                            while (($pos = mb_strpos($lowerContent, $kw,$offset)) !== false) {
                                if ($fileMatchCount >= 5) break 2; 

                                $hash = $pos . '_' .$kw;
                                if (!isset($matchedPositions[$hash])) {
                                    $matchedPositions[$hash] = true;
                                    $fileMatchCount++;

                                    $start = max(0,$pos - 40);
                                    $length = mb_strlen($kw) + 80;
                                    $snippet = '...' . trim(mb_substr($content, $start,$length)) . '...';

                                    $documents[] = [
                                        'slug' => $slug,
                                        'title' => $title,
                                        'snippet' => $snippet,
                                        'updated_at' => $updatedAt,
                                        'match_index' => $fileMatchCount,
                                        'keyword' => $kw,
                                        'section_key' => $section['key'],
                                        'section_label' => $section['label'],
                                        'folder' => $folder
                                    ];
                                }

                                $offset = $pos + mb_strlen($kw);
                            }
                        }
                    }
                }
            }

            // 블레이드 컴파일러 루프 파싱 충돌을 방지하기 위해 섹션별 연관 배열 구조로 안전하게 변환
            $sectionsArray = [];
            foreach (collect($documents)->groupBy('section_key') as$sectionKey => $docs) {$firstDoc = $docs->first();$sectionsArray[] = [
                    'key' => $sectionKey,
                    'label' => is_array($firstDoc) ? ($firstDoc['section_label'] ?? '기타 문서') : '기타 문서',
                    'docs' => $docs->toArray()
                ];
            }

            return view('documents.index', [
                'sections' => $sectionsArray,
                'searchQuery' => $searchQuery
            ]);
        }

        /**
         * 마크다운 파일 상단의 YAML Front Matter 파서
         */
        private function parseFrontMatter($content)
        {
            $title = null;
            $section = [
                'key' => 'appendix',
                'label' => '부록'
            ];

            if (preg_match('/---\s*\n(.*?)\n---/s', $content,$matches)) {
                $yaml =$matches[1];
                
                if (preg_match('/^title:\s*(.*)$/m', $yaml,$tMatch)) {
                    $title = trim($tMatch[1]);
                }
                
                preg_match('/^section_key:\s*(.*)$/m', $yaml,$keyMatch);
                preg_match('/^section_label:\s*(.*)$/m', $yaml,$labelMatch);
                
                if (!empty($keyMatch[1]) && !empty($labelMatch[1])) {$section = [
                        'key' => trim($keyMatch[1]),                         'label' => trim($labelMatch[1])
                    ];
                }
            }

            return [
                'title' => $title,
                'section' => $section
            ];
        }

        /**
         * 개별 마크다운 문서 상세 보기 조회
         * - 상단 YAML Front Matter 영역 자동 제거 후 뷰 전달
         */
        public function show($page = 'architecture')
        {
            $disk = Storage::disk('public');
            $path = "documents/{$page}.md";
            
            if (!$disk->exists($path)) {$allFiles = $disk->allFiles('documents');$foundPath = null;
                foreach ($allFiles as$f) {
                    if (pathinfo($f, PATHINFO_FILENAME) ===$page) {
                        $foundPath =$f;
                        break;
                    }
                }

                if (!$foundPath || !$disk->exists($foundPath)) {
                    abort(404);
                }
                $path =$foundPath;
            }

            $rawContent = $disk->get($path);

            // 본문 출력 시 상단 YAML 메타데이터 영역(--- ... ---) 제거
            $markdownContent = preg_replace('/^---\s*\n.*?\n---\s*\n/s', '',$rawContent);

            return view('documents.show', [
                'markdownContent' => $markdownContent,
                'page' => $page
            ]);
        }
    }

### C. 문서 목록 및 검색 뷰 (`resources/views/documents/index.blade.php`)
    <!DOCTYPE html>
    <html lang="ko">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>GLOBEAU.KR Docs - 문서 검색 센터</title>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
        <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
        <style>
            body { background-color: #f0f2f5; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
            .doc-container { max-width: 950px; margin: 50px auto; padding: 0 20px; }
            .doc-card { background: #fff; border: 1px solid #e1e4e8; border-radius: 10px; padding: 30px; box-shadow: 0 4px 15px rgba(0,0,0,0.04); }
            .doc-item { padding: 18px 20px; border-bottom: 1px solid #f1f5f9; transition: background 0.2s; display: block; text-decoration: none; color: #334155; }
            .doc-item:hover { background-color: #f8fafc; color: #0284c7; }
            .doc-item:last-child { border-bottom: none; }
            .snippet { font-size: 0.85rem; color: #64748b; background: #f8fafc; padding: 6px 10px; border-radius: 4px; margin-top: 6px; font-family: monospace; }
            .section-header { cursor: pointer; user-select: none; }
            .section-header:hover { background-color: #e2e8f0 !important; }
            .section-header .fa-chevron-down { transition: transform 0.3s; }
            .section-header.collapsed .fa-chevron-down { transform: rotate(-90deg); }
        </style>
    </head>
    <body>
        <div class="doc-container">
            <div class="mb-4">
                <h2 class="fw-bold text-dark mb-1"><i class="fas fa-book-open text-primary me-2"></i> GLOBEAU.KR 기술 문서 센터</h2>
                <p class="text-muted mb-0">전체 마크다운 문서의 본문과 제목을 대상으로 키워드를 검색합니다.</p>
            </div>

            <div class="doc-card">
                <!-- 검색 폼: 현재 검색어(?q=...)를 유지하여 상세 보기로 넘어갈 수 있도록 준비 -->
                <form action="{{ url('/docs') }}" method="GET" class="input-group mb-4">
                    <span class="input-group-text bg-white text-muted"><i class="fas fa-search"></i></span>
                    <input type="text" name="q" value="{{ $searchQuery ?? '' }}" class="form-control" placeholder="DB 컬럼명, 함수명, 키워드 등을 입력하세요...">
                    <button class="btn btn-primary px-4" type="submit">검색</button>
                    @if(!empty($searchQuery))
                        <a href="{{ url('/docs') }}" class="btn btn-outline-secondary">초기화</a>
                    @endif
                </form>

                @if(!empty($searchQuery))
                    <div class="mb-3 text-muted small">
                        <i class="fas fa-info-circle"></i> "<strong>{{ $searchQuery }}</strong>" 검색 결과가 조회되었습니다.
                    </div>
                @endif

                <div class="d-flex flex-column gap-3">
                    @php
                        $hasSections = !empty($sections) && is_array($sections) && count($sections) > 0;
                    @endphp

                    @if($hasSections)
                        @php
                            foreach ($sections as$section) {
                                $sectionKey =$section['key'] ?? '';
                                $sectionLabel =$section['label'] ?? '기타 문서';
                                $docs =$section['docs'] ?? [];
                                $isExpanded = !empty($searchQuery); // 검색 중일 때 true
                                $docCount = count($docs);
                                $collapsedClass =$isExpanded ? '' : 'collapsed';
                                $showClass =$isExpanded ? 'show' : '';
                                $expandedAttr =$isExpanded ? 'true' : 'false';
                        @endphp

                            <div class="border rounded overflow-hidden bg-white mb-3">
                                <div class="px-3 py-3 bg-light text-secondary fw-bold border-bottom d-flex justify-content-between align-items-center section-header {{ $collapsedClass }}"
                                    data-bs-toggle="collapse" 
                                    data-bs-target="#section-{{ $sectionKey }}" 
                                    aria-expanded="{{ $expandedAttr }}" 
                                    aria-controls="section-{{ $sectionKey }}"
                                    style="padding: 12px 16px;">
                                    <div>
                                        <i class="fas fa-bookmark me-1 text-primary"></i> {{ $sectionLabel }} 
                                        <span class="badge bg-secondary ms-2" style="font-size: 0.75rem;">{{ $docCount }}</span>
                                    </div>
                                    <div>
                                        <i class="fas fa-chevron-down small text-muted"></i>
                                    </div>
                                </div>

                                <div class="collapse {{ $showClass }}" id="section-{{ $sectionKey }}">
                                    <div class="list-group list-group-flush">
                                        @php
                                            foreach ($docs as$doc) {
                                                $slug =$doc['slug'] ?? '';
                                                $title =$doc['title'] ?? '';
                                                $updatedAt =$doc['updated_at'] ?? '';
                                                $keyword =$doc['keyword'] ?? '';
                                                $matchIndex =$doc['match_index'] ?? 1;
                                                $snippet =$doc['snippet'] ?? '';
                                                
                                                // 🌟 핵심: 문서 링크를 생성할 때 현재 검색 중인 키워드(?q=...)가 있다면 함께 전달합니다.
                                                $queryParams = [];
                                                if (!empty($keyword)) {
                                                    $queryParams['highlight'] =$keyword;
                                                    $queryParams['index'] =$matchIndex;
                                                }
                                                if (!empty($searchQuery)) {
                                                    $queryParams['q'] =$searchQuery; // 검색어 유지용 파라미터 추가
                                                }
                                                
                                                $url = url('/docs/' .$slug . (!empty($queryParams) ? '?' . http_build_query($queryParams) : ''));
                                        @endphp
                                                <a href="{{ $url }}" class="doc-item">
                                                    <div class="d-flex justify-content-between align-items-center">
                                                        <div>
                                                            <i class="fas fa-file-alt text-secondary me-2"></i>
                                                            <span class="fw-semibold fs-5">{{ $title }}</span>
                                                            <span class="badge bg-light text-muted ms-2 font-monospace small">{{ $slug }}.md</span>
                                                        </div>
                                                        <div class="text-muted small">
                                                            <i class="far fa-clock me-1"></i> {{ $updatedAt }}
                                                            <i class="fas fa-chevron-right ms-2 small"></i>
                                                        </div>
                                                    </div>
                                                    @if(!empty($snippet))
                                                        <div class="snippet text-truncate">
                                                            <i class="fas fa-code text-primary me-1"></i> {{ $snippet }}
                                                        </div>
                                                    @endif
                                                </a>
                                        @php
                                            }
                                        @endphp
                                    </div>
                                </div>
                            </div>

                        @php
                            }
                        @endphp
                    @else
                        <div class="text-center py-5 text-muted">
                            <i class="fas fa-search fa-2x mb-2 text-secondary opacity-50"></i>
                            <p class="mb-0">일치하는 문서 내용을 찾을 수 없습니다.</p>
                        </div>
                    @endif
                </div>
            </div>
        </div>

        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
        <script>
            if ('scrollRestoration' in history) {
                history.scrollRestoration = 'auto';
            }

            document.addEventListener("DOMContentLoaded", function () {
                const isSearching = @json(!empty($searchQuery));
                
                // 🌟 핵심: 평소(일반 탐색 모드)일 때만 localStorage에서 아코디언 상태를 읽고 씁니다.
                // 검색 중일 때는 아코디언이 모두 펼쳐져 있으므로 상태 저장을 하지 않습니다.
                if (!isSearching) {
                    document.querySelectorAll('.collapse').forEach(function (collapseElement) {
                        const sectionId = collapseElement.id;
                        const savedState = localStorage.getItem('doc_section_' + sectionId);
                        const header = document.querySelector(`[data-bs-target="#${sectionId}"]`);
                        
                        if (savedState === 'show') {
                            collapseElement.classList.add('show');
                            if (header) {
                                header.classList.remove('collapsed');
                                header.setAttribute('aria-expanded', 'true');
                            }
                        } else if (savedState === 'hide') {
                            collapseElement.classList.remove('show');
                            if (header) {
                                header.classList.add('collapsed');
                                header.setAttribute('aria-expanded', 'false');
                            }
                        }
                    });

                    // 평소 탐색할 때만 토글 상태를 localStorage에 저장
                    document.querySelectorAll('.collapse').forEach(function (collapseElement) {
                        collapseElement.addEventListener('shown.bs.collapse', function () {
                            localStorage.setItem('doc_section_' + this.id, 'show');
                        });
                        collapseElement.addEventListener('hidden.bs.collapse', function () {
                            localStorage.setItem('doc_section_' + this.id, 'hide');
                        });
                    });
                }
            });
        </script>
    </body>
    </html>

### D. 개별 문서 상세 뷰 (`resources/views/documents/show.blade.php`)
    <!DOCTYPE html>
    <html lang="ko">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>GLOBEAU.KR Docs - 상세 문서</title>
        <!-- 부트스트랩 5 CSS -->
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
        <!-- 폰트어썸 -->
        <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
        
        <!-- GitHub 공식 마크다운 스타일 -->
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.1/github-markdown-light.min.css">
        
        <!-- 코드 블록 신택스 하이라이팅 테마 -->
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/stackoverflow-dark.min.css">
        
        <style>
            body {
                background-color: #f0f2f5;
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
            }

            .doc-container {
                max-width: 1100px;
                margin: 40px auto;
                padding: 0 20px;
            }

            .markdown-body {
                box-sizing: border-box;
                padding: 60px;
                background-color: #ffffff;
                border: 1px solid #e1e4e8;
                border-radius: 12px;
                box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
            }

            /* 테이블 영역 스타일 */
            .markdown-body .table-responsive {
                width: 100%;
                overflow-x: auto;
                -webkit-overflow-scrolling: touch;
                margin-bottom: 1.5rem;
                background-color: #ffffff;
                border: 1px solid #e1e4e8;
                border-radius: 8px;
            }

            .markdown-body table {
                width: 100% !important;
                margin-bottom: 0 !important;
                background-color: #ffffff;
            }
            
            .markdown-body th, 
            .markdown-body td {
                white-space: nowrap;
                padding: 12px 16px !important;
            }

            .markdown-body .table-responsive::-webkit-scrollbar {
                height: 5px;
            }
            .markdown-body .table-responsive::-webkit-scrollbar-track {
                background: #f8f9fa;
                border-radius: 0 0 8px 8px;
            }
            .markdown-body .table-responsive::-webkit-scrollbar-thumb {
                background: #cbd5e1;
                border-radius: 4px;
            }

            .markdown-body blockquote {
                padding: 14px 20px;
                color: #24292e;
                background-color: #f6f8fa;
                border-left: 4px solid #0366d6;
                border-radius: 0 6px 6px 0;
                margin: 1.5rem 0;
            }

            /* 🌟 소스코드 영역: 텍스트 색상을 선명한 흰색으로 통일 */
            .markdown-body pre {
                background-color: #0b0f19 !important;
                border-radius: 8px !important;
                padding: 20px !important;
                border: 1px solid #30363d;
                box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.8);
                position: relative;
                overflow-x: auto;
            }

            .markdown-body pre code {
                background-color: transparent !important;
                color: #ffffff !important; /* 전체 텍스트 흰색 통일 */
                padding: 0 !important;
                border: none !important;
                font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace;
                font-size: 0.95rem;
                line-height: 1.6;
            }

            /* 하이라이트된 내부 요소들도 흰색으로 통일되도록 강제 지정 */
            .markdown-body pre code span {
                color: #ffffff !important;
            }

            .markdown-body code:not(pre code) {
                background-color: rgba(175, 184, 193, 0.2);
                color: #cf222e;
                padding: 0.2em 0.4em;
                font-size: 85%;
                border-radius: 6px;
                font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace;
            }

            /* 코드 블록 슬림 스크롤바 */
            .markdown-body pre::-webkit-scrollbar {
                height: 5px;
            }
            .markdown-body pre::-webkit-scrollbar-track {
                background: #0b0f19;
                border-radius: 0 0 8px 8px;
            }
            .markdown-body pre::-webkit-scrollbar-thumb {
                background: #484f58;
                border-radius: 4px;
            }
            .markdown-body pre::-webkit-scrollbar-thumb:hover {
                background: #6e7681;
            }

            @media (max-width: 767px) {
                .markdown-body {
                    padding: 20px;
                }
                .doc-container {
                    margin: 15px auto;
                    padding: 0 10px;
                }
            }
        </style>
    </head>
    <body>
        <div class="doc-container">
            <!-- 상단 네비게이션 (목록으로 이동) -->
            <div class="top-bar mb-2">
                @php
                    // URL에 검색어(q)가 포함되어 있었다면 목록으로 돌아갈 때 그 검색어를 유지합니다.
                    $backUrl = url('/docs') . (request()->has('q') ? '?q=' . urlencode(request()->get('q')) : '');
                @endphp
                <a href="{{ $backUrl }}" class="btn btn-outline-secondary btn-sm">
                    <i class="fas fa-arrow-left me-1"></i> {{ request()->has('q') ? '검색 결과로 돌아가기' : '문서 목록으로' }}
                </a>
            </div>

            <!-- 마크다운 렌더링 영역 -->
            <article class="markdown-body" id="doc-content">
                <div class="text-center py-5 text-muted">
                    <i class="fas fa-spinner fa-spin fa-2x"></i>
                    <p class="mt-2">문서를 불러오는 중입니다...</p>
                </div>
            </article>
        </div>

        <!-- 브라우저용 마크다운 파서 및 하이라이터 라이브러리 -->
        <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
        
        <script>
            const rawMarkdown = @json($markdownContent);

            marked.setOptions({
                highlight: function(code, lang) {
                    if (lang && hljs.getLanguage(lang)) {
                        return hljs.highlight(code, { language: lang }).value;
                    }
                    return hljs.highlightAuto(code).value;
                },
                gfm: true,
                breaks: true
            });

            // 1. 마크다운 렌더링 실행
            let parsedHtml = marked.parse(rawMarkdown);
            
            // 4칸 들여쓰기 코드 블록에 언어 클래스 자동 부여
            parsedHtml = parsedHtml.replace(/<pre><code>/g, '<pre><code class="language-php">');
            document.getElementById('doc-content').innerHTML = parsedHtml;

            // 하이라이트 강제 실행
            document.querySelectorAll('pre code').forEach((block) => {
                hljs.highlightElement(block);
            });

            // 본문 내 모든 테이블을 가로 스크롤 래퍼로 자동 감싸기
            document.querySelectorAll('.markdown-body table').forEach((table) => {
                const wrapper = document.createElement('div');
                wrapper.className = 'table-responsive';
                table.parentNode.insertBefore(wrapper, table);
                wrapper.appendChild(table);
            });

            // 2. URL 파라미터 확인 (검색어 및 매칭 순번)
            const urlParams = new URLSearchParams(window.location.search);
            const highlightQuery = urlParams.get('highlight');
            const targetIndex = parseInt(urlParams.get('index') || '1', 10);

            if (highlightQuery) {
                setTimeout(() => {
                    scrollToTargetOccurrence(highlightQuery, targetIndex);
                }, 300);
            }

            // 3. 브라우저 내장 검색 기능을 활용해 정확한 N번째 위치로 이동하고 하이라이트 배경 적용
            function scrollToTargetOccurrence(keyword, targetIdx) {
                window.getSelection().removeAllRanges();
                window.scrollTo({ top: 0, behavior: 'instant' });

                let foundCount = 0;
                let lastSuccessRange = null;

                for (let i = 0; i < targetIdx; i++) {
                    const found = window.find(keyword, false, false, true, false, false, false);
                    if (!found) break;
                    foundCount++;
                    const selection = window.getSelection();
                    if (selection.rangeCount > 0) {
                        lastSuccessRange = selection.getRangeAt(0);
                    }
                }

                if (foundCount === targetIdx && lastSuccessRange) {
                    const selection = window.getSelection();
                    selection.removeAllRanges();
                    selection.addRange(lastSuccessRange);

                    try {
                        const span = document.createElement('mark');
                        span.style.backgroundColor = '#f59e0b';
                        span.style.color = '#ffffff';
                        span.style.padding = '2px 6px';
                        span.style.borderRadius = '4px';
                        span.style.fontWeight = 'bold';
                        span.style.boxShadow = '0 0 0 2px rgba(245, 158, 11, 0.4)';
                        
                        lastSuccessRange.surroundContents(span);
                        span.scrollIntoView({ behavior: 'smooth', block: 'center' });
                    } catch (e) {
                        const selectedNode = lastSuccessRange.commonAncestorContainer;
                        const targetElement = selectedNode.nodeType === 1 ? selectedNode : selectedNode.parentNode;
                        if (targetElement) {
                            targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
                        }
                    }
                }
            }
        </script>
    </body>
    </html>