input('year', now()->year); $month = (int) $request->input('month', now()->month); $tenantId = session('selected_tenant_id', 1); $firstDay = Carbon::create($year, $month, 1); $lastDay = $firstDay->copy()->endOfMonth(); $startOfWeek = $firstDay->copy()->startOfWeek(Carbon::SUNDAY); $endOfWeek = $lastDay->copy()->endOfWeek(Carbon::SATURDAY); $calendarData = Schedule::forTenant($tenantId) ->active() ->betweenDates($startOfWeek->toDateString(), $endOfWeek->toDateString()) ->withCount('files') ->orderBy('start_date') ->orderBy('start_time') ->get() ->groupBy(fn ($s) => $s->start_date->format('Y-m-d')); $holidayMap = $this->getHolidayMap($tenantId, $year, $month); return view('dashboard.partials.calendar', compact( 'year', 'month', 'calendarData', 'holidayMap' )); } /** * 일정 등록 */ public function store(Request $request): JsonResponse { $validated = $request->validate([ 'title' => 'required|string|max:255', 'description' => 'nullable|string|max:1000', 'start_date' => 'required|date', 'end_date' => 'nullable|date|after_or_equal:start_date', 'start_time' => 'nullable|date_format:H:i', 'end_time' => 'nullable|date_format:H:i', 'is_all_day' => 'boolean', 'type' => 'required|string|max:50', 'color' => 'nullable|string|max:20', ]); $tenantId = session('selected_tenant_id', 1); $validated['tenant_id'] = $tenantId; $validated['created_by'] = auth()->id(); $validated['is_all_day'] = $validated['is_all_day'] ?? true; if (empty($validated['end_date'])) { $validated['end_date'] = $validated['start_date']; } $schedule = Schedule::create($validated); return response()->json([ 'success' => true, 'message' => '일정이 등록되었습니다.', 'data' => $schedule, ]); } /** * 일정 상세 (JSON) */ public function show(int $id): JsonResponse { $tenantId = session('selected_tenant_id', 1); $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, ]); } /** * 일정 수정 */ public function update(Request $request, int $id): JsonResponse { $validated = $request->validate([ 'title' => 'required|string|max:255', 'description' => 'nullable|string|max:1000', 'start_date' => 'required|date', 'end_date' => 'nullable|date|after_or_equal:start_date', 'start_time' => 'nullable|date_format:H:i', 'end_time' => 'nullable|date_format:H:i', 'is_all_day' => 'boolean', 'type' => 'required|string|max:50', 'color' => 'nullable|string|max:20', ]); $tenantId = session('selected_tenant_id', 1); $schedule = Schedule::forTenant($tenantId)->findOrFail($id); $validated['updated_by'] = auth()->id(); $validated['is_all_day'] = $validated['is_all_day'] ?? true; if (empty($validated['end_date'])) { $validated['end_date'] = $validated['start_date']; } $schedule->update($validated); return response()->json([ 'success' => true, 'message' => '일정이 수정되었습니다.', 'data' => $schedule->fresh(), ]); } /** * 일정 삭제 */ public function destroy(int $id): JsonResponse { $tenantId = session('selected_tenant_id', 1); $schedule = Schedule::forTenant($tenantId)->findOrFail($id); $schedule->update(['deleted_by' => auth()->id()]); $schedule->delete(); return response()->json([ 'success' => true, 'message' => '일정이 삭제되었습니다.', ]); } /** * 파일 업로드 (다중) */ 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'; } /** * 해당 월의 휴일 맵 생성 (날짜 => 휴일명) */ private function getHolidayMap(int $tenantId, int $year, int $month): array { $startOfMonth = Carbon::create($year, $month, 1)->startOfWeek(Carbon::SUNDAY); $endOfMonth = Carbon::create($year, $month, 1)->endOfMonth()->endOfWeek(Carbon::SATURDAY); $holidays = Holiday::forTenant($tenantId) ->where('start_date', '<=', $endOfMonth->toDateString()) ->where('end_date', '>=', $startOfMonth->toDateString()) ->get(); $map = []; foreach ($holidays as $holiday) { $start = $holiday->start_date->copy(); $end = $holiday->end_date->copy(); for ($d = $start; $d->lte($end); $d->addDay()) { $map[$d->format('Y-m-d')] = $holiday->name; } } return $map; } }