feat: [quality] 품질관리서 파일 업로드/삭제 API

- POST /{id}/upload-file: 1건당 1파일, 기존 파일 교체
- DELETE /{id}/file: 파일 soft delete
- QualityDocument.file() relation 추가
- R2 저장 경로: {tenant_id}/quality-documents/{year}/{month}

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 10:14:00 +09:00
parent ef591074c7
commit 597aecb5e8
4 changed files with 106 additions and 0 deletions

View File

@@ -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'));
}
}

View File

@@ -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

View File

@@ -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());
}
}

View File

@@ -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');
});
// 실적신고