diff --git a/app/Http/Controllers/V1/Equipment/EquipmentPhotoController.php b/app/Http/Controllers/V1/Equipment/EquipmentPhotoController.php index 71ca832..35de41c 100644 --- a/app/Http/Controllers/V1/Equipment/EquipmentPhotoController.php +++ b/app/Http/Controllers/V1/Equipment/EquipmentPhotoController.php @@ -4,9 +4,9 @@ use App\Helpers\ApiResponse; use App\Http\Controllers\Controller; +use App\Http\Requests\Equipment\StoreEquipmentPhotoRequest; use App\Services\Equipment\EquipmentPhotoService; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; class EquipmentPhotoController extends Controller { @@ -20,10 +20,10 @@ public function index(int $id): JsonResponse ); } - public function store(Request $request, int $id): JsonResponse + public function store(StoreEquipmentPhotoRequest $request, int $id): JsonResponse { return ApiResponse::handle( - fn () => $this->service->store($id, $request->all()), + fn () => $this->service->store($id, $request->file('files')), __('message.equipment.photo_uploaded') ); } diff --git a/app/Http/Requests/Equipment/StoreEquipmentPhotoRequest.php b/app/Http/Requests/Equipment/StoreEquipmentPhotoRequest.php new file mode 100644 index 0000000..699859d --- /dev/null +++ b/app/Http/Requests/Equipment/StoreEquipmentPhotoRequest.php @@ -0,0 +1,53 @@ +route('id'); + $currentCount = File::where('document_id', $equipmentId) + ->where('document_type', 'equipment') + ->whereNull('deleted_at') + ->count(); + + $maxFiles = 10 - $currentCount; + + return [ + 'files' => ['required', 'array', 'min:1', "max:{$maxFiles}"], + 'files.*' => [ + 'required', + 'file', + 'mimes:jpg,jpeg,png,gif,bmp,webp', + 'max:10240', // 10MB + ], + ]; + } + + public function attributes(): array + { + return [ + 'files' => '사진 파일', + 'files.*' => '사진 파일', + ]; + } + + public function messages(): array + { + return [ + 'files.required' => __('error.file.required'), + 'files.max' => __('error.equipment.photo_limit_exceeded'), + 'files.*.mimes' => __('error.file.invalid_type'), + 'files.*.max' => __('error.file.size_exceeded'), + ]; + } +} diff --git a/app/Models/Equipment/Equipment.php b/app/Models/Equipment/Equipment.php index 3c7d996..dadf480 100644 --- a/app/Models/Equipment/Equipment.php +++ b/app/Models/Equipment/Equipment.php @@ -2,7 +2,7 @@ namespace App\Models\Equipment; -use App\Models\Boards\File; +use App\Models\Commons\File; use App\Traits\Auditable; use App\Traits\BelongsToTenant; use App\Traits\ModelTrait; diff --git a/app/Services/Equipment/EquipmentPhotoService.php b/app/Services/Equipment/EquipmentPhotoService.php index e06cccb..1d6bbe1 100644 --- a/app/Services/Equipment/EquipmentPhotoService.php +++ b/app/Services/Equipment/EquipmentPhotoService.php @@ -2,41 +2,86 @@ namespace App\Services\Equipment; -use App\Models\Boards\File; +use App\Models\Commons\File; use App\Models\Equipment\Equipment; use App\Services\Service; +use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\Storage; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class EquipmentPhotoService extends Service { - public function index(int $equipmentId): \Illuminate\Database\Eloquent\Collection + private const MAX_PHOTOS = 10; + + public function index(int $equipmentId): array { - $equipment = Equipment::find($equipmentId); + $equipment = $this->getEquipment($equipmentId); - if (! $equipment) { - throw new NotFoundHttpException(__('error.equipment.not_found')); - } - - return $equipment->photos; + return $equipment->photos->map(fn ($file) => $this->formatFileResponse($file))->values()->toArray(); } - public function store(int $equipmentId, array $fileData): File + /** + * @param UploadedFile[] $files + */ + public function store(int $equipmentId, array $files): array { - $equipment = Equipment::find($equipmentId); + $equipment = $this->getEquipment($equipmentId); + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); - if (! $equipment) { - throw new NotFoundHttpException(__('error.equipment.not_found')); + $currentCount = File::where('document_id', $equipmentId) + ->where('document_type', 'equipment') + ->whereNull('deleted_at') + ->count(); + + if ($currentCount + count($files) > self::MAX_PHOTOS) { + throw new \Exception(__('error.equipment.photo_limit_exceeded')); } - return File::create(array_merge($fileData, [ - 'document_id' => $equipmentId, - 'document_type' => 'equipment', - ])); + $uploaded = []; + + foreach ($files as $uploadedFile) { + $extension = $uploadedFile->getClientOriginalExtension(); + $storedName = bin2hex(random_bytes(8)).'.'.$extension; + $displayName = $uploadedFile->getClientOriginalName(); + + $year = date('Y'); + $month = date('m'); + $directory = sprintf('%d/equipment/%s/%s', $tenantId, $year, $month); + $filePath = $directory.'/'.$storedName; + + Storage::disk('r2')->putFileAs($directory, $uploadedFile, $storedName); + + $mimeType = $uploadedFile->getMimeType(); + + $file = File::create([ + 'tenant_id' => $tenantId, + 'display_name' => $displayName, + 'stored_name' => $storedName, + 'file_path' => $filePath, + 'file_size' => $uploadedFile->getSize(), + 'mime_type' => $mimeType, + 'file_type' => 'image', + 'document_id' => $equipmentId, + 'document_type' => 'equipment', + 'is_temp' => false, + 'uploaded_by' => $userId, + 'created_by' => $userId, + ]); + + $uploaded[] = $this->formatFileResponse($file); + } + + return $uploaded; } - public function destroy(int $equipmentId, int $fileId): bool + public function destroy(int $equipmentId, int $fileId): array { - $file = File::where('document_id', $equipmentId) + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + $file = File::where('tenant_id', $tenantId) + ->where('document_id', $equipmentId) ->where('document_type', 'equipment') ->where('id', $fileId) ->first(); @@ -45,6 +90,35 @@ public function destroy(int $equipmentId, int $fileId): bool throw new NotFoundHttpException(__('error.file.not_found')); } - return $file->delete(); + $file->softDeleteFile($userId); + + return [ + 'file_id' => $fileId, + 'deleted' => true, + ]; + } + + private function getEquipment(int $equipmentId): Equipment + { + $equipment = Equipment::find($equipmentId); + + if (! $equipment) { + throw new NotFoundHttpException(__('error.equipment.not_found')); + } + + return $equipment; + } + + private function formatFileResponse(File $file): array + { + return [ + 'id' => $file->id, + 'file_name' => $file->display_name, + 'file_path' => $file->file_path, + 'file_url' => url("/api/v1/files/{$file->id}/download"), + 'file_size' => $file->file_size, + 'mime_type' => $file->mime_type, + 'created_at' => $file->created_at?->format('Y-m-d H:i:s'), + ]; } }