diff --git a/app/Http/Controllers/Additional/PptxController.php b/app/Http/Controllers/Additional/PptxController.php new file mode 100644 index 00000000..1f9e82a0 --- /dev/null +++ b/app/Http/Controllers/Additional/PptxController.php @@ -0,0 +1,181 @@ + PPTX 관리 컨트롤러 + * 파일시스템 스캔 기반으로 PPTX 파일 목록 조회 및 다운로드 + */ +class PptxController extends Controller +{ + private array $scanPaths = [ + '/var/www/docs/pptx-output' => ['label' => '산출물', 'source' => 'docs'], + '/var/www/docs/rules' => ['label' => '정책/규칙', 'source' => 'docs'], + '/var/www/docs/guides' => ['label' => '가이드', 'source' => 'docs'], + '/var/www/docs/projects' => ['label' => '프로젝트', 'source' => 'docs'], + '/var/www/docs/plans' => ['label' => '계획', 'source' => 'docs'], + '/var/www/mng/docs/pptx-output' => ['label' => '산출물', 'source' => 'mng'], + '/var/www/mng/docs' => ['label' => '교육/문서', 'source' => 'mng'], + '/var/www/mng/public/docs' => ['label' => '공개 문서', 'source' => 'mng'], + ]; + + /** + * PPTX 목록 페이지 + */ + public function index(Request $request): View|Response + { + if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('additional.pptx.index')); + } + + $files = $this->scanPptxFiles(); + $categories = collect($files)->pluck('category')->unique()->sort()->values()->all(); + $totalSize = collect($files)->sum('size_bytes'); + + return view('additional.pptx.index', [ + 'files' => $files, + 'categories' => $categories, + 'totalCount' => count($files), + 'totalSize' => $this->formatFileSize($totalSize), + ]); + } + + /** + * PPTX 파일 다운로드 + */ + public function download(Request $request): BinaryFileResponse|Response + { + $filePath = $request->query('file'); + + if (! $filePath) { + abort(400, '파일 경로가 지정되지 않았습니다.'); + } + + // 경로 트래버설 방지 + if (str_contains($filePath, '..')) { + abort(403, '잘못된 파일 경로입니다.'); + } + + // .pptx 확장자만 허용 + if (strtolower(pathinfo($filePath, PATHINFO_EXTENSION)) !== 'pptx') { + abort(403, '허용되지 않는 파일 형식입니다.'); + } + + // 허용된 base path에 속하는지 검증 + $allowed = false; + foreach (array_keys($this->scanPaths) as $basePath) { + if (str_starts_with($filePath, $basePath.'/')) { + $allowed = true; + break; + } + } + + if (! $allowed) { + abort(403, '허용되지 않는 경로입니다.'); + } + + if (! file_exists($filePath)) { + abort(404, '파일을 찾을 수 없습니다.'); + } + + return response()->download($filePath, basename($filePath), [ + 'Content-Type' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + ]); + } + + /** + * 모든 스캔 경로에서 PPTX 파일 수집 + */ + private function scanPptxFiles(): array + { + $files = []; + $seenPaths = []; + + foreach ($this->scanPaths as $basePath => $meta) { + if (! is_dir($basePath)) { + continue; + } + + $this->findPptxInDirectory($basePath, $basePath, $meta, $files, $seenPaths); + } + + // 수정일 내림차순 정렬 + usort($files, fn ($a, $b) => $b['modified_at'] <=> $a['modified_at']); + + return $files; + } + + /** + * 디렉토리에서 PPTX 파일 재귀 검색 + */ + private function findPptxInDirectory(string $dir, string $basePath, array $meta, array &$files, array &$seenPaths): void + { + $items = scandir($dir); + if ($items === false) { + return; + } + + foreach ($items as $item) { + if ($item === '.' || $item === '..') { + continue; + } + + $fullPath = $dir.'/'.$item; + + if (is_dir($fullPath)) { + // mng/docs 경로에서 하위 디렉토리 중 다른 scanPath에 해당하는 것은 건너뜀 + if (isset($this->scanPaths[$fullPath])) { + continue; + } + $this->findPptxInDirectory($fullPath, $basePath, $meta, $files, $seenPaths); + + continue; + } + + if (strtolower(pathinfo($item, PATHINFO_EXTENSION)) !== 'pptx') { + continue; + } + + // 중복 방지 (realpath 기반) + $realPath = realpath($fullPath); + if ($realPath && isset($seenPaths[$realPath])) { + continue; + } + if ($realPath) { + $seenPaths[$realPath] = true; + } + + $relativePath = str_replace($basePath.'/', '', $fullPath); + $sizeBytes = filesize($fullPath); + $modifiedAt = filemtime($fullPath); + + $files[] = [ + 'name' => $item, + 'path' => $fullPath, + 'relative_path' => $relativePath, + 'category' => $meta['label'], + 'source' => $meta['source'], + 'source_dir' => str_replace('/var/www/', '', $basePath), + 'size_bytes' => $sizeBytes, + 'size' => $this->formatFileSize($sizeBytes), + 'modified_at' => $modifiedAt, + 'modified_date' => date('Y-m-d H:i', $modifiedAt), + ]; + } + } + + private function formatFileSize(int $bytes): string + { + if ($bytes >= 1048576) { + return number_format($bytes / 1048576, 1).' MB'; + } + + return number_format($bytes / 1024, 0).' KB'; + } +} diff --git a/resources/views/additional/pptx/index.blade.php b/resources/views/additional/pptx/index.blade.php new file mode 100644 index 00000000..b896eae2 --- /dev/null +++ b/resources/views/additional/pptx/index.blade.php @@ -0,0 +1,202 @@ +@extends('layouts.app') + +@section('title', 'PPTX 관리') + +@push('styles') + +@endpush + +@section('content') +
회사 프레젠테이션 자료를 한 곳에서 관리합니다
+PPTX 파일이 없습니다
+