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

266 lines
8.1 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\Item\ItemFileUploadRequest;
use App\Models\Commons\File;
use App\Models\Items\Item;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* 품목 파일 관리 컨트롤러
*
* group_id 기반 파일 관리, field_key 동적 지원
* - document_type: group_id (품목 그룹)
* - document_id: item_id (품목 ID)
* - file_type: field_key (동적)
*/
class ItemsFileController extends Controller
{
/**
* 품목 그룹 ID (고정값, 추후 설정으로 변경 가능)
*/
private const ITEM_GROUP_ID = '1';
/**
* 파일 목록 조회
*
* GET /api/v1/items/{id}/files
*/
public function index(int $id, Request $request)
{
return ApiResponse::handle(function () use ($id, $request) {
$tenantId = app('tenant_id');
$fieldKey = $request->input('field_key');
// 품목 존재 확인
$this->getItemById($id, $tenantId);
// 파일 조회
$query = File::query()
->where('tenant_id', $tenantId)
->where('document_type', self::ITEM_GROUP_ID)
->where('document_id', $id);
// 특정 field_key만 조회
if ($fieldKey) {
$query->where('field_key', $fieldKey);
}
$files = $query->orderBy('created_at', 'desc')->get();
// field_key별 그룹핑
$grouped = $files->groupBy('field_key')->map(function ($group) {
return $group->map(fn ($file) => $this->formatFileResponse($file))->values();
});
return $grouped;
}, __('message.fetched'));
}
/**
* 파일 업로드
*
* POST /api/v1/items/{id}/files
*/
public function upload(int $id, ItemFileUploadRequest $request)
{
return ApiResponse::handle(function () use ($id, $request) {
$tenantId = app('tenant_id');
$userId = auth()->id() ?? app('api_user');
$validated = $request->validated();
$fieldKey = $validated['field_key'];
$uploadedFile = $validated['file'];
$existingFileId = $validated['file_id'] ?? null;
// 품목 존재 확인
$this->getItemById($id, $tenantId);
$replaced = false;
// 기존 파일 교체 (file_id가 있는 경우)
if ($existingFileId) {
$existingFile = File::query()
->where('tenant_id', $tenantId)
->where('document_type', self::ITEM_GROUP_ID)
->where('document_id', $id)
->where('id', $existingFileId)
->first();
if ($existingFile) {
$this->deleteFile($existingFile);
$replaced = true;
}
}
// 파일명 생성 (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);
// file_type 자동 분류 (MIME 타입 기반)
$mimeType = $uploadedFile->getMimeType();
$fileType = $this->detectFileType($mimeType);
// files 테이블에 저장
$file = File::create([
'tenant_id' => $tenantId,
'display_name' => $displayName,
'stored_name' => $storedName,
'file_path' => $filePath,
'file_size' => $uploadedFile->getSize(),
'mime_type' => $mimeType,
'file_type' => $fileType, // 파일 형식 (image, document, excel, archive)
'field_key' => $fieldKey, // 비즈니스 용도 (drawing, certificate 등)
'document_id' => $id,
'document_type' => self::ITEM_GROUP_ID, // group_id
'is_temp' => false,
'uploaded_by' => $userId,
'created_by' => $userId,
]);
return [
'file_id' => $file->id,
'field_key' => $fieldKey,
'file_url' => $this->getFileUrl($file->id),
'file_path' => $filePath,
'file_name' => $displayName,
'file_size' => $file->file_size,
'mime_type' => $file->mime_type,
'replaced' => $replaced,
];
}, __('message.file.uploaded'));
}
/**
* 파일 삭제
*
* DELETE /api/v1/items/{id}/files/{fileId}
*/
public function delete(int $id, mixed $fileId, Request $request)
{
$fileId = (int) $fileId;
return ApiResponse::handle(function () use ($id, $fileId) {
$tenantId = app('tenant_id');
// 품목 존재 확인
$this->getItemById($id, $tenantId);
// 파일 조회
$file = File::query()
->where('tenant_id', $tenantId)
->where('document_type', self::ITEM_GROUP_ID)
->where('document_id', $id)
->where('id', $fileId)
->first();
if (! $file) {
throw new NotFoundHttpException(__('error.file.not_found'));
}
// Soft delete
$this->deleteFile($file);
return [
'file_id' => $fileId,
'deleted' => true,
];
}, __('message.file.deleted'));
}
/**
* ID로 품목 조회 (통합 items 테이블)
*/
private function getItemById(int $id, int $tenantId): Item
{
$item = Item::query()
->where('tenant_id', $tenantId)
->find($id);
if (! $item) {
throw new NotFoundHttpException(__('error.not_found'));
}
return $item;
}
/**
* 파일 삭제 (일원화된 삭제 로직)
*/
private function deleteFile(File $file): void
{
$userId = (int) (auth()->id() ?? app('api_user'));
$file->softDeleteFile($userId);
}
/**
* 파일 응답 포맷
*/
private function formatFileResponse(File $file): array
{
return [
'id' => $file->id,
'file_name' => $file->display_name,
'file_path' => $file->file_path,
'file_url' => $this->getFileUrl($file->id),
'file_size' => $file->file_size,
'mime_type' => $file->mime_type,
'file_type' => $file->file_type,
'field_key' => $file->field_key,
'created_at' => $file->created_at?->format('Y-m-d H:i:s'),
];
}
/**
* 파일 URL 생성
*/
private function getFileUrl(int $fileId): string
{
return url("/api/v1/files/{$fileId}/download");
}
/**
* MIME 타입 기반 파일 형식 분류
*/
private function detectFileType(string $mimeType): string
{
if (str_starts_with($mimeType, 'image/')) {
return 'image';
}
if (in_array($mimeType, [
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv',
])) {
return 'excel';
}
if (in_array($mimeType, [
'application/zip',
'application/x-rar-compressed',
'application/x-7z-compressed',
'application/gzip',
])) {
return 'archive';
}
// 기본값: document (pdf, doc, hwp 등)
return 'document';
}
}