- Products 테이블에 9개 파일 관련 필드 추가
- bending_diagram, bending_details (JSON)
- specification_file, specification_file_name
- certification_file, certification_file_name
- certification_number, certification_start_date, certification_end_date
- ItemsFileController 구현 (Code-based API)
- POST /items/{code}/files - 파일 업로드
- DELETE /items/{code}/files/{type} - 파일 삭제
- 파일 타입: bending_diagram, specification, certification
- ItemsFileUploadRequest 검증
- 파일 타입별 MIME 검증 (이미지/문서)
- 파일 크기 제한 (10MB/20MB)
- 인증 정보 및 절곡 상세 정보 검증
- Swagger 문서 작성 (ItemsFileApi.php)
- 업로드/삭제 API 스펙
- 스키마: ItemFileUploadResponse, ItemFileDeleteResponse
175 lines
5.7 KiB
PHP
175 lines
5.7 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\V1;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Requests\ItemsFileUploadRequest;
|
|
use App\Http\Responses\ApiResponse;
|
|
use App\Models\Products\Product;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
|
|
/**
|
|
* 품목 파일 관리 컨트롤러
|
|
*
|
|
* Code-based 파일 업로드/삭제 API
|
|
* - 절곡도 (bending_diagram)
|
|
* - 시방서 (specification)
|
|
* - 인정서 (certification)
|
|
*/
|
|
class ItemsFileController extends Controller
|
|
{
|
|
/**
|
|
* 파일 업로드
|
|
*
|
|
* POST /api/v1/items/{code}/files
|
|
*/
|
|
public function upload(string $code, ItemsFileUploadRequest $request)
|
|
{
|
|
return ApiResponse::handle(function () use ($code, $request) {
|
|
$product = $this->getProductByCode($code);
|
|
$validated = $request->validated();
|
|
$fileType = $request->route('type') ?? $validated['type'];
|
|
$file = $validated['file'];
|
|
|
|
// 파일 저장 경로: items/{code}/{type}/{filename}
|
|
$directory = sprintf('items/%s/%s', $code, $fileType);
|
|
$filePath = Storage::disk('public')->putFile($directory, $file);
|
|
$fileUrl = Storage::disk('public')->url($filePath);
|
|
$originalName = $file->getClientOriginalName();
|
|
|
|
// Product 모델 업데이트
|
|
$updateData = $this->buildUpdateData($fileType, $filePath, $originalName, $validated);
|
|
$product->update($updateData);
|
|
|
|
return [
|
|
'file_type' => $fileType,
|
|
'file_url' => $fileUrl,
|
|
'file_path' => $filePath,
|
|
'file_name' => $originalName,
|
|
'product' => $product->fresh(),
|
|
];
|
|
}, __('message.file.uploaded'));
|
|
}
|
|
|
|
/**
|
|
* 파일 삭제
|
|
*
|
|
* DELETE /api/v1/items/{code}/files/{type}
|
|
*/
|
|
public function delete(string $code, string $type, Request $request)
|
|
{
|
|
return ApiResponse::handle(function () use ($code, $type) {
|
|
$product = $this->getProductByCode($code);
|
|
|
|
// 파일 타입 검증
|
|
if (! in_array($type, ['bending_diagram', 'specification', 'certification'])) {
|
|
throw new \InvalidArgumentException(__('error.file.invalid_file_type'));
|
|
}
|
|
|
|
// 파일 경로 가져오기
|
|
$filePath = $this->getFilePath($product, $type);
|
|
|
|
if ($filePath) {
|
|
// 물리적 파일 삭제
|
|
Storage::disk('public')->delete($filePath);
|
|
}
|
|
|
|
// DB 필드 null 처리
|
|
$updateData = $this->buildDeleteData($type);
|
|
$product->update($updateData);
|
|
|
|
return [
|
|
'file_type' => $type,
|
|
'deleted' => (bool) $filePath,
|
|
'product' => $product->fresh(),
|
|
];
|
|
}, __('message.file.deleted'));
|
|
}
|
|
|
|
/**
|
|
* 코드로 Product 조회
|
|
*/
|
|
private function getProductByCode(string $code): Product
|
|
{
|
|
$tenantId = app('tenant_id');
|
|
$product = Product::query()
|
|
->where('tenant_id', $tenantId)
|
|
->where('code', $code)
|
|
->first();
|
|
|
|
if (! $product) {
|
|
throw new NotFoundHttpException(__('error.not_found'));
|
|
}
|
|
|
|
return $product;
|
|
}
|
|
|
|
/**
|
|
* 파일 타입별 업데이트 데이터 구성
|
|
*/
|
|
private function buildUpdateData(string $fileType, string $filePath, string $originalName, array $validated): array
|
|
{
|
|
$updateData = match ($fileType) {
|
|
'bending_diagram' => [
|
|
'bending_diagram' => $filePath,
|
|
'bending_details' => $validated['bending_details'] ?? null,
|
|
],
|
|
'specification' => [
|
|
'specification_file' => $filePath,
|
|
'specification_file_name' => $originalName,
|
|
],
|
|
'certification' => [
|
|
'certification_file' => $filePath,
|
|
'certification_file_name' => $originalName,
|
|
'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')),
|
|
};
|
|
|
|
return $updateData;
|
|
}
|
|
|
|
/**
|
|
* 파일 타입별 삭제 데이터 구성
|
|
*/
|
|
private function buildDeleteData(string $fileType): array
|
|
{
|
|
return match ($fileType) {
|
|
'bending_diagram' => [
|
|
'bending_diagram' => null,
|
|
'bending_details' => null,
|
|
],
|
|
'specification' => [
|
|
'specification_file' => null,
|
|
'specification_file_name' => null,
|
|
],
|
|
'certification' => [
|
|
'certification_file' => null,
|
|
'certification_file_name' => null,
|
|
'certification_number' => null,
|
|
'certification_start_date' => null,
|
|
'certification_end_date' => null,
|
|
],
|
|
default => throw new \InvalidArgumentException(__('error.file.invalid_file_type')),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Product에서 파일 경로 가져오기
|
|
*/
|
|
private function getFilePath(Product $product, string $fileType): ?string
|
|
{
|
|
return match ($fileType) {
|
|
'bending_diagram' => $product->bending_diagram,
|
|
'specification' => $product->specification_file,
|
|
'certification' => $product->certification_file,
|
|
default => null,
|
|
};
|
|
}
|
|
}
|