diff --git a/app/Http/Controllers/Api/V1/QualityDocumentController.php b/app/Http/Controllers/Api/V1/QualityDocumentController.php index 82cb42f..c0e9657 100644 --- a/app/Http/Controllers/Api/V1/QualityDocumentController.php +++ b/app/Http/Controllers/Api/V1/QualityDocumentController.php @@ -124,4 +124,24 @@ public function resultDocument(int $id) return $this->service->resultDocument($id); }, __('message.fetched')); } + + public function uploadFile(Request $request, int $id) + { + $request->validate([ + 'file' => ['required', 'file', 'max:51200'], // 50MB + ]); + + return ApiResponse::handle(function () use ($request, $id) { + return $this->service->uploadFile($id, $request->file('file')); + }, __('message.created')); + } + + public function deleteFile(int $id) + { + return ApiResponse::handle(function () use ($id) { + $this->service->deleteFile($id); + + return 'success'; + }, __('message.deleted')); + } } diff --git a/app/Models/Qualitys/QualityDocument.php b/app/Models/Qualitys/QualityDocument.php index bcbc104..f5ba578 100644 --- a/app/Models/Qualitys/QualityDocument.php +++ b/app/Models/Qualitys/QualityDocument.php @@ -2,6 +2,7 @@ namespace App\Models\Qualitys; +use App\Models\Commons\File; use App\Models\Members\User; use App\Models\Orders\Client; use App\Traits\Auditable; @@ -72,6 +73,12 @@ public function performanceReport() return $this->hasOne(PerformanceReport::class); } + public function file() + { + return $this->hasOne(File::class, 'document_id') + ->where('document_type', static::class); + } + // ===== 채번 ===== public static function generateDocNumber(int $tenantId): string diff --git a/app/Services/QualityDocumentService.php b/app/Services/QualityDocumentService.php index 2391b2f..e67a9ff 100644 --- a/app/Services/QualityDocumentService.php +++ b/app/Services/QualityDocumentService.php @@ -2,6 +2,7 @@ namespace App\Services; +use App\Models\Commons\File; use App\Models\Documents\Document; use App\Models\Documents\DocumentData; use App\Models\Documents\DocumentTemplate; @@ -13,7 +14,9 @@ use App\Models\Qualitys\QualityDocumentLocation; use App\Models\Qualitys\QualityDocumentOrder; use App\Services\Audit\AuditLogger; +use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Storage; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -1248,4 +1251,78 @@ private function formatInspectionPeriod(array $options): string return $start ?: $end ?: ''; } + + // ========================================================================= + // 파일 업로드/삭제 + // ========================================================================= + + /** + * 품질관리서 파일 업로드 (1건당 1파일, 기존 파일 있으면 교체) + */ + public function uploadFile(int $id, UploadedFile $uploadedFile): array + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + $doc = QualityDocument::where('tenant_id', $tenantId)->findOrFail($id); + + // 기존 파일이 있으면 물리 삭제 (교체) + $existingFile = $doc->file; + if ($existingFile) { + $existingFile->permanentDelete(); + } + + // 저장 경로: {tenant_id}/quality-documents/{year}/{month}/{stored_name} + $date = now(); + $storedName = bin2hex(random_bytes(16)).'.'.$uploadedFile->getClientOriginalExtension(); + $filePath = sprintf( + '%d/quality-documents/%s/%s/%s', + $tenantId, + $date->format('Y'), + $date->format('m'), + $storedName + ); + + // R2에 파일 저장 + Storage::disk('r2')->put($filePath, file_get_contents($uploadedFile->getPathname())); + + // DB 레코드 생성 + $file = File::create([ + 'tenant_id' => $tenantId, + 'document_type' => QualityDocument::class, + 'document_id' => $doc->id, + 'display_name' => $uploadedFile->getClientOriginalName(), + 'stored_name' => $storedName, + 'file_path' => $filePath, + 'file_size' => $uploadedFile->getSize(), + 'mime_type' => $uploadedFile->getClientMimeType(), + 'uploaded_by' => $userId, + 'created_by' => $userId, + ]); + + return [ + 'id' => $file->id, + 'display_name' => $file->display_name, + 'file_size' => $file->file_size, + 'mime_type' => $file->mime_type, + 'created_at' => $file->created_at?->toIso8601String(), + ]; + } + + /** + * 품질관리서 파일 삭제 + */ + public function deleteFile(int $id): void + { + $tenantId = $this->tenantId(); + + $doc = QualityDocument::where('tenant_id', $tenantId)->findOrFail($id); + + $file = $doc->file; + if (! $file) { + throw new NotFoundHttpException(__('error.not_found')); + } + + $file->softDeleteFile($this->apiUserId()); + } } diff --git a/routes/api/v1/quality.php b/routes/api/v1/quality.php index c09c5f1..0788b4e 100644 --- a/routes/api/v1/quality.php +++ b/routes/api/v1/quality.php @@ -29,6 +29,8 @@ Route::post('/{id}/locations/{locId}/inspect', [QualityDocumentController::class, 'inspectLocation'])->whereNumber('id')->whereNumber('locId')->name('v1.quality.documents.inspect-location'); Route::get('/{id}/request-document', [QualityDocumentController::class, 'requestDocument'])->whereNumber('id')->name('v1.quality.documents.request-document'); Route::get('/{id}/result-document', [QualityDocumentController::class, 'resultDocument'])->whereNumber('id')->name('v1.quality.documents.result-document'); + Route::post('/{id}/upload-file', [QualityDocumentController::class, 'uploadFile'])->whereNumber('id')->name('v1.quality.documents.upload-file'); + Route::delete('/{id}/file', [QualityDocumentController::class, 'deleteFile'])->whereNumber('id')->name('v1.quality.documents.delete-file'); }); // 실적신고