Files
sam-api/app/Http/Controllers/Api/V1/ItemsFileController.php

244 lines
8.2 KiB
PHP
Raw Normal View History

<?php
namespace App\Http\Controllers\Api\V1;
use App\Helpers\ApiResponse;
use App\Http\Controllers\Controller;
use App\Http\Requests\ItemsFileUploadRequest;
use App\Models\Commons\File;
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;
/**
* 품목 파일 관리 컨트롤러
*
* ID-based 파일 업로드/삭제 API (files 테이블 기반)
* - 절곡도 (bending_diagram)
* - 시방서 (specification)
* - 인정서 (certification)
*/
class ItemsFileController extends Controller
{
/**
* 파일 업로드
*
* POST /api/v1/items/{id}/files
*
* 파일 저장 구조 (file-storage-guide.md 참고):
* - 물리 경로: storage/app/tenants/{tenant_id}/items/{year}/{month}/{stored_name}
* - DB: files 테이블에 저장
* - Product: file_id 참조
*/
public function upload(int $id, ItemsFileUploadRequest $request)
{
return ApiResponse::handle(function () use ($id, $request) {
$tenantId = app('tenant_id');
$userId = auth()->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;
}
}