input('item_type', 'FG')); $fieldKey = $request->input('field_key'); // 품목 존재 확인 $this->getItemById($id, $itemType, $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(); $validated = $request->validated(); $itemType = strtoupper($validated['item_type'] ?? 'FG'); $fieldKey = $validated['field_key']; $uploadedFile = $validated['file']; $existingFileId = $validated['file_id'] ?? null; // 품목 존재 확인 $this->getItemById($id, $itemType, $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->softDeleteFile($userId); $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($filePath), '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, int $fileId, Request $request) { return ApiResponse::handle(function () use ($id, $fileId, $request) { $tenantId = app('tenant_id'); $userId = auth()->id(); $itemType = strtoupper($request->input('item_type', 'FG')); // 품목 존재 확인 $this->getItemById($id, $itemType, $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 $file->softDeleteFile($userId); return [ 'file_id' => $fileId, 'deleted' => true, ]; }, __('message.file.deleted')); } /** * ID로 품목 조회 (Product 또는 Material) */ private function getItemById(int $id, string $itemType, int $tenantId): Product|Material { if (ItemTypeHelper::isMaterial($itemType, $tenantId)) { $item = Material::query() ->where('tenant_id', $tenantId) ->find($id); } else { $item = Product::query() ->where('tenant_id', $tenantId) ->find($id); } if (! $item) { throw new NotFoundHttpException(__('error.not_found')); } return $item; } /** * 파일 응답 포맷 */ 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->file_path), '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(string $filePath): string { return url('/api/v1/files/download/'.base64_encode($filePath)); } /** * 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'; } }