id(); $product = $this->getProductById($id); $validated = $request->validated(); $fileType = $validated['type']; $uploadedFile = $validated['file']; // 파일명 생성 (64bit 난수) $extension = $uploadedFile->getClientOriginalExtension(); $storedName = bin2hex(random_bytes(8)) . '.' . $extension; $displayName = $uploadedFile->getClientOriginalName(); // 경로 생성: tenants/{tenant_id}/items/{year}/{month}/{stored_name} $year = date('Y'); $month = date('m'); $directory = sprintf('%d/items/%s/%s', $tenantId, $year, $month); $filePath = $directory . '/' . $storedName; // 파일 저장 (tenant 디스크) Storage::disk('tenant')->putFileAs($directory, $uploadedFile, $storedName); // files 테이블에 저장 $file = File::create([ 'tenant_id' => $tenantId, 'display_name' => $displayName, 'stored_name' => $storedName, 'file_path' => $filePath, 'file_size' => $uploadedFile->getSize(), 'mime_type' => $uploadedFile->getMimeType(), 'file_type' => $this->getFileTypeCategory($uploadedFile->getMimeType()), 'document_id' => $product->id, 'document_type' => 'product_' . $fileType, // product_bending_diagram, product_specification, product_certification 'is_temp' => false, 'uploaded_by' => $userId, 'created_by' => $userId, ]); // Product 모델 업데이트 (file_id 참조 방식) $updateData = $this->buildUpdateData($fileType, $file->id, $validated); $product->update($updateData); // 파일 URL 생성 $fileUrl = $this->getFileUrl($filePath); return [ 'file_id' => $file->id, 'file_type' => $fileType, 'file_url' => $fileUrl, 'file_path' => $filePath, 'file_name' => $displayName, 'file_size' => $file->file_size, 'mime_type' => $file->mime_type, 'product' => $product->fresh(), ]; }, __('message.file.uploaded')); } /** * 파일 삭제 * * DELETE /api/v1/items/{id}/files/{type} */ public function delete(int $id, string $type, Request $request) { return ApiResponse::handle(function () use ($id, $type) { $userId = auth()->id(); $product = $this->getProductById($id); // 파일 타입 검증 if (! in_array($type, ['bending_diagram', 'specification', 'certification'])) { throw new \InvalidArgumentException(__('error.file.invalid_file_type')); } // 파일 ID 가져오기 $fileId = $this->getFileId($product, $type); $deleted = false; if ($fileId) { $file = File::find($fileId); if ($file) { // Soft delete (files 테이블) $file->softDeleteFile($userId); $deleted = true; } } // DB 필드 null 처리 (Product) $updateData = $this->buildDeleteData($type); $product->update($updateData); return [ 'file_type' => $type, 'deleted' => $deleted, 'product' => $product->fresh(), ]; }, __('message.file.deleted')); } /** * ID로 Product 조회 */ private function getProductById(int $id): Product { $tenantId = app('tenant_id'); $product = Product::query() ->where('tenant_id', $tenantId) ->find($id); if (! $product) { throw new NotFoundHttpException(__('error.not_found')); } return $product; } /** * MIME 타입에서 파일 타입 카테고리 추출 */ private function getFileTypeCategory(string $mimeType): string { if (str_starts_with($mimeType, 'image/')) { return 'image'; } if (in_array($mimeType, [ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel', 'text/csv', ])) { return 'excel'; } if (in_array($mimeType, ['application/zip', 'application/x-rar-compressed'])) { return 'archive'; } return 'document'; } /** * 파일 URL 생성 (tenant 디스크는 private이므로 API 경유) */ private function getFileUrl(string $filePath): string { // Public URL이 아닌 API 다운로드 경로 반환 return url('/api/v1/files/download/' . base64_encode($filePath)); } /** * 파일 타입별 업데이트 데이터 구성 (file_id 참조) */ private function buildUpdateData(string $fileType, int $fileId, array $validated): array { return match ($fileType) { 'bending_diagram' => [ 'bending_diagram' => $fileId, // file_id 저장 'bending_details' => $validated['bending_details'] ?? null, ], 'specification' => [ 'specification_file' => $fileId, // file_id 저장 ], 'certification' => [ 'certification_file' => $fileId, // file_id 저장 'certification_number' => $validated['certification_number'] ?? null, 'certification_start_date' => $validated['certification_start_date'] ?? null, 'certification_end_date' => $validated['certification_end_date'] ?? null, ], default => throw new \InvalidArgumentException(__('error.file.invalid_file_type')), }; } /** * 파일 타입별 삭제 데이터 구성 */ private function buildDeleteData(string $fileType): array { return match ($fileType) { 'bending_diagram' => [ 'bending_diagram' => null, 'bending_details' => null, ], 'specification' => [ 'specification_file' => null, ], 'certification' => [ 'certification_file' => null, 'certification_number' => null, 'certification_start_date' => null, 'certification_end_date' => null, ], default => throw new \InvalidArgumentException(__('error.file.invalid_file_type')), }; } /** * Product에서 파일 ID 가져오기 */ private function getFileId(Product $product, string $fileType): ?int { $value = match ($fileType) { 'bending_diagram' => $product->bending_diagram, 'specification' => $product->specification_file, 'certification' => $product->certification_file, default => null, }; return $value ? (int) $value : null; } }