orderBy('work_date', 'desc') ->orderBy('id', 'desc'); if (! empty($params['search'])) { $search = $params['search']; $query->where(function ($q) use ($search) { $q->where('site_name', 'like', "%{$search}%") ->orWhere('description', 'like', "%{$search}%"); }); } if (! empty($params['date_from'])) { $query->where('work_date', '>=', $params['date_from']); } if (! empty($params['date_to'])) { $query->where('work_date', '<=', $params['date_to']); } $perPage = (int) ($params['per_page'] ?? 12); return $query->paginate($perPage); } public function create(array $data): ConstructionSitePhoto { $photo = ConstructionSitePhoto::create([ 'tenant_id' => session('selected_tenant_id'), 'user_id' => Auth::id(), 'site_name' => $data['site_name'], 'work_date' => $data['work_date'], 'description' => $data['description'] ?? null, ]); // 기본 빈 행 1개 자동 생성 $photo->rows()->create(['sort_order' => 0]); return $photo->load('rows'); } public function uploadPhoto(ConstructionSitePhotoRow $row, $file, string $type): bool { if (! in_array($type, ['before', 'during', 'after'])) { return false; } $photo = $row->constructionSitePhoto; $extension = $file->getClientOriginalExtension(); $timestamp = now()->format('Ymd_His'); $objectName = "construction-site-photos/{$photo->tenant_id}/{$photo->id}/{$row->id}_{$timestamp}_{$type}.{$extension}"; // 기존 사진이 있으면 GCS에서 삭제 $oldPath = $row->{$type.'_photo_path'}; if ($oldPath) { $this->googleCloudService->deleteFromStorage($oldPath); } // 임시 파일로 저장 후 GCS 업로드 $tempPath = $file->getRealPath(); $result = $this->googleCloudService->uploadToStorage($tempPath, $objectName); if (! $result) { Log::error('ConstructionSitePhoto: GCS 업로드 실패', [ 'photo_id' => $photo->id, 'row_id' => $row->id, 'type' => $type, ]); return false; } $row->update([ $type.'_photo_path' => $objectName, $type.'_photo_gcs_uri' => $result['uri'], $type.'_photo_size' => $result['size'], ]); AiTokenHelper::saveGcsStorageUsage('공사현장사진대지-GCS저장', $result['size']); return true; } public function update(ConstructionSitePhoto $photo, array $data): ConstructionSitePhoto { $photo->update([ 'site_name' => $data['site_name'], 'work_date' => $data['work_date'], 'description' => $data['description'] ?? null, ]); return $photo->fresh()->load('rows'); } public function delete(ConstructionSitePhoto $photo): bool { // rows 순회하여 모든 GCS 파일 삭제 foreach ($photo->rows as $row) { foreach (['before', 'during', 'after'] as $type) { $path = $row->{$type.'_photo_path'}; if ($path) { $this->googleCloudService->deleteFromStorage($path); } } } return $photo->delete(); } public function deletePhotoByType(ConstructionSitePhotoRow $row, string $type): bool { if (! in_array($type, ['before', 'during', 'after'])) { return false; } $path = $row->{$type.'_photo_path'}; if ($path) { $this->googleCloudService->deleteFromStorage($path); } $row->update([ $type.'_photo_path' => null, $type.'_photo_gcs_uri' => null, $type.'_photo_size' => null, ]); return true; } public function addRow(ConstructionSitePhoto $photo): ConstructionSitePhotoRow { $nextOrder = ($photo->rows()->max('sort_order') ?? -1) + 1; return $photo->rows()->create(['sort_order' => $nextOrder]); } public function deleteRow(ConstructionSitePhotoRow $row): bool { // 행의 GCS 파일 삭제 foreach (['before', 'during', 'after'] as $type) { $path = $row->{$type.'_photo_path'}; if ($path) { $this->googleCloudService->deleteFromStorage($path); } } $photoId = $row->construction_site_photo_id; $row->delete(); // 나머지 행 순서 재정렬 $remainingRows = ConstructionSitePhotoRow::where('construction_site_photo_id', $photoId) ->orderBy('sort_order') ->get(); foreach ($remainingRows as $i => $r) { if ($r->sort_order !== $i) { $r->update(['sort_order' => $i]); } } return true; } }