Files
sam-api/app/Http/Controllers/Api/V1/ItemsFileController.php
권혁성 5448f0e57d deploy: 2026-03-12 배포
- feat: [barobill] 바로빌 카드/은행/홈택스 REST API 구현
- feat: [equipment] 설비관리 API 백엔드 구현
- feat: [payroll] 급여관리 계산 엔진 및 일괄 처리 API
- feat: [QMS] 점검표 템플릿 관리 + 로트심사 개선
- feat: [생산/출하] 수주 단위 출하 자동생성 + 상태 흐름 개선
- feat: [receiving] 입고 성적서 파일 연결
- feat: [견적] 제어기 타입 체계 변경
- feat: [email] 테넌트 메일 설정 마이그레이션 및 모델
- feat: [pmis] 시공관리 테이블 마이그레이션
- feat: [R2] 파일 업로드 커맨드 + filesystems 설정
- feat: [배포] Jenkinsfile 롤백 기능 추가
- fix: [approval] SAM API 규칙 준수 코드 개선
- fix: [account-codes] 계정과목 중복 데이터 정리
- fix: [payroll] 일괄 생성 시 삭제된 사용자 건너뛰기
- fix: [db] codebridge DB 분리 후 깨진 FK 제약조건 제거
- refactor: [barobill] 바로빌 연동 코드 전면 개선
2026-03-12 15:20:20 +09:00

266 lines
8.2 KiB
PHP

<?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) {
$existingFile->permanentDelete(); // 파일 교체 시 완전 삭제
$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('r2')->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';
}
}