From 489a491415fc3dce69668b6e87015aba3eaf9fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Tue, 10 Feb 2026 20:36:22 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EC=9D=BC=EC=A0=95=20=EC=B2=A8=EB=B6=80?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(=EB=8B=A4=EC=A4=91=20=EC=97=85=EB=A1=9C=EB=93=9C,=20=EB=93=9C?= =?UTF-8?q?=EB=9E=98=EA=B7=B8=EC=95=A4=EB=93=9C=EB=A1=AD,=20GCS)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DashboardCalendarController에 uploadFiles/deleteFile/downloadFile 추가 - 파일 업로드 라우트 3개 추가 (POST/DELETE/GET) - 모달에 드래그앤드롭 파일 업로드 영역 추가 - XHR 진행률 표시, 파일 목록 렌더링, 개별 삭제 - Google Cloud Storage 연동 (가용시 자동 업로드) - files 테이블 document_type='schedule' 활용 Co-Authored-By: Claude Opus 4.6 --- .../DashboardCalendarController.php | 135 ++++++ resources/views/dashboard/index.blade.php | 397 ++++++++++++++---- routes/web.php | 5 + 3 files changed, 450 insertions(+), 87 deletions(-) diff --git a/app/Http/Controllers/DashboardCalendarController.php b/app/Http/Controllers/DashboardCalendarController.php index 93ac94a9..be121e0e 100644 --- a/app/Http/Controllers/DashboardCalendarController.php +++ b/app/Http/Controllers/DashboardCalendarController.php @@ -2,12 +2,17 @@ namespace App\Http\Controllers; +use App\Models\Boards\File; use App\Models\System\Holiday; use App\Models\System\Schedule; +use App\Services\GoogleCloudStorageService; use Carbon\Carbon; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Contracts\View\View; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; class DashboardCalendarController extends Controller { @@ -85,9 +90,16 @@ public function show(int $id): JsonResponse $schedule = Schedule::forTenant($tenantId)->findOrFail($id); + // 첨부파일 목록 + $files = File::where('document_type', 'schedule') + ->where('document_id', $id) + ->whereNull('deleted_at') + ->get(['id', 'display_name', 'original_name', 'file_size', 'mime_type', 'created_at']); + return response()->json([ 'success' => true, 'data' => $schedule, + 'files' => $files, ]); } @@ -144,6 +156,129 @@ public function destroy(int $id): JsonResponse ]); } + /** + * 파일 업로드 (다중) + */ + public function uploadFiles(Request $request, int $scheduleId, GoogleCloudStorageService $gcs): JsonResponse + { + $request->validate([ + 'files' => 'required|array|min:1', + 'files.*' => 'file|max:20480', // 20MB + ]); + + $tenantId = session('selected_tenant_id', 1); + $schedule = Schedule::forTenant($tenantId)->findOrFail($scheduleId); + + $uploaded = []; + + foreach ($request->file('files') as $file) { + $originalName = $file->getClientOriginalName(); + $storedName = Str::random(40) . '.' . $file->getClientOriginalExtension(); + $storagePath = "schedules/{$tenantId}/{$schedule->id}/{$storedName}"; + + // 로컬(tenant 디스크) 저장 + Storage::disk('tenant')->put($storagePath, file_get_contents($file)); + + // GCS 업로드 (가능한 경우) + $gcsUri = null; + if ($gcs->isAvailable()) { + $gcsObjectName = "schedules/{$tenantId}/{$schedule->id}/{$storedName}"; + $gcsUri = $gcs->upload($file->getRealPath(), $gcsObjectName); + } + + // DB 레코드 + $fileRecord = File::create([ + 'tenant_id' => $tenantId, + 'document_type' => 'schedule', + 'document_id' => $schedule->id, + 'file_path' => $storagePath, + 'display_name' => $originalName, + 'stored_name' => $storedName, + 'original_name' => $originalName, + 'file_name' => $originalName, + 'file_size' => $file->getSize(), + 'mime_type' => $file->getMimeType(), + 'file_type' => $this->determineFileType($file->getMimeType()), + 'is_temp' => false, + 'uploaded_by' => auth()->id(), + 'created_by' => auth()->id(), + ]); + + $uploaded[] = [ + 'id' => $fileRecord->id, + 'name' => $originalName, + 'size' => $fileRecord->getFormattedSize(), + 'gcs' => $gcsUri ? true : false, + ]; + } + + return response()->json([ + 'success' => true, + 'message' => count($uploaded) . '개 파일이 업로드되었습니다.', + 'files' => $uploaded, + ]); + } + + /** + * 파일 삭제 + */ + public function deleteFile(int $scheduleId, int $fileId, GoogleCloudStorageService $gcs): JsonResponse + { + $tenantId = session('selected_tenant_id', 1); + Schedule::forTenant($tenantId)->findOrFail($scheduleId); + + $file = File::where('document_type', 'schedule') + ->where('document_id', $scheduleId) + ->where('id', $fileId) + ->firstOrFail(); + + // GCS 삭제 + if ($gcs->isAvailable() && $file->file_path) { + $gcs->delete($file->file_path); + } + + // 로컬 삭제 + if ($file->file_path && Storage::disk('tenant')->exists($file->file_path)) { + Storage::disk('tenant')->delete($file->file_path); + } + + $file->deleted_by = auth()->id(); + $file->save(); + $file->delete(); + + return response()->json([ + 'success' => true, + 'message' => '파일이 삭제되었습니다.', + ]); + } + + /** + * 파일 다운로드 + */ + public function downloadFile(int $scheduleId, int $fileId) + { + $tenantId = session('selected_tenant_id', 1); + Schedule::forTenant($tenantId)->findOrFail($scheduleId); + + $file = File::where('document_type', 'schedule') + ->where('document_id', $scheduleId) + ->where('id', $fileId) + ->firstOrFail(); + + return $file->download(); + } + + /** + * MIME 타입으로 파일 유형 결정 + */ + private function determineFileType(string $mimeType): string + { + if (str_starts_with($mimeType, 'image/')) return 'image'; + if (str_contains($mimeType, 'spreadsheet') || str_contains($mimeType, 'excel')) return 'excel'; + if (str_contains($mimeType, 'zip') || str_contains($mimeType, 'rar') || str_contains($mimeType, 'archive')) return 'archive'; + return 'document'; + } + /** * 해당 월의 휴일 맵 생성 (날짜 => 휴일명) */ diff --git a/resources/views/dashboard/index.blade.php b/resources/views/dashboard/index.blade.php index 8ec7eac9..619e4315 100644 --- a/resources/views/dashboard/index.blade.php +++ b/resources/views/dashboard/index.blade.php @@ -86,9 +86,9 @@