feat: 품목 파일 업로드 API 구현 (절곡도, 시방서, 인정서)
- 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
This commit is contained in:
174
app/Http/Controllers/Api/V1/ItemsFileController.php
Normal file
174
app/Http/Controllers/Api/V1/ItemsFileController.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user