Files
sam-manage/app/Http/Controllers/Additional/DocxController.php

178 lines
5.4 KiB
PHP
Raw Normal View History

<?php
namespace App\Http\Controllers\Additional;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* 추가기능 > DOCX 관리 컨트롤러
* 파일시스템 스캔 기반으로 DOCX 파일 목록 조회 다운로드
*/
class DocxController extends Controller
{
private array $scanPaths = [
'/var/www/docs/contracts/docx' => ['label' => '계약서', 'source' => 'docs'],
'/var/www/docs' => ['label' => '문서', 'source' => 'docs'],
'/var/www/mng/docs' => ['label' => '교육/문서', 'source' => 'mng'],
'/var/www/mng/public/docs' => ['label' => '공개 문서', 'source' => 'mng'],
'/var/www/sales-docs' => ['label' => '영업', 'source' => 'sales'],
];
/**
* DOCX 목록 페이지
*/
public function index(Request $request): View|Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('additional.docx.index'));
}
$files = $this->scanDocxFiles();
$categories = collect($files)->pluck('category')->unique()->sort()->values()->all();
$totalSize = collect($files)->sum('size_bytes');
return view('additional.docx.index', [
'files' => $files,
'categories' => $categories,
'totalCount' => count($files),
'totalSize' => $this->formatFileSize($totalSize),
]);
}
/**
* DOCX 파일 다운로드
*/
public function download(Request $request): BinaryFileResponse|Response
{
$filePath = $request->query('file');
if (! $filePath) {
abort(400, '파일 경로가 지정되지 않았습니다.');
}
// 경로 트래버설 방지
if (str_contains($filePath, '..')) {
abort(403, '잘못된 파일 경로입니다.');
}
// .docx 확장자만 허용
if (strtolower(pathinfo($filePath, PATHINFO_EXTENSION)) !== 'docx') {
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.wordprocessingml.document',
]);
}
/**
* 모든 스캔 경로에서 DOCX 파일 수집
*/
private function scanDocxFiles(): array
{
$files = [];
$seenPaths = [];
foreach ($this->scanPaths as $basePath => $meta) {
if (! is_dir($basePath)) {
continue;
}
$this->findDocxInDirectory($basePath, $basePath, $meta, $files, $seenPaths);
}
// 수정일 내림차순 정렬
usort($files, fn ($a, $b) => $b['modified_at'] <=> $a['modified_at']);
return $files;
}
/**
* 디렉토리에서 DOCX 파일 재귀 검색
*/
private function findDocxInDirectory(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)) {
if (isset($this->scanPaths[$fullPath])) {
continue;
}
$this->findDocxInDirectory($fullPath, $basePath, $meta, $files, $seenPaths);
continue;
}
if (strtolower(pathinfo($item, PATHINFO_EXTENSION)) !== 'docx') {
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';
}
}