feat:일정 첨부파일 기능 추가 (다중 업로드, 드래그앤드롭, GCS)
- DashboardCalendarController에 uploadFiles/deleteFile/downloadFile 추가 - 파일 업로드 라우트 3개 추가 (POST/DELETE/GET) - 모달에 드래그앤드롭 파일 업로드 영역 추가 - XHR 진행률 표시, 파일 목록 렌더링, 개별 삭제 - Google Cloud Storage 연동 (가용시 자동 업로드) - files 테이블 document_type='schedule' 활용 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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';
|
||||
}
|
||||
|
||||
/**
|
||||
* 해당 월의 휴일 맵 생성 (날짜 => 휴일명)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user