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'], '/var/www/sales-docs/pptx-output' => ['label' => '영업 산출물', 'source' => 'sales'], '/var/www/sales-docs/plan/pptx' => ['label' => '영업 기획', 'source' => 'sales'], '/var/www/sales-docs/plan' => ['label' => '영업 기획', 'source' => 'sales'], ]; /** * 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'; } }