import { rm } from 'node:fs/promises' import { createError } from 'h3' import { requireAdminSession } from '../../../../utils/admin-auth' import { createPostImportJob, createPostImportJobZipPath, queuePostImportJobRun } from '../../../../repositories/post-import-repository' import { saveMultipartFileToDisk } from '../../../../utils/multipart-file-upload' const MAX_IMPORT_ZIP_BYTES = 300 * 1024 * 1024 /** * 관리자 게시물 Import API * @param {import('h3').H3Event} event - 요청 이벤트 * @returns {Promise} 생성된 Import 작업 */ export default defineEventHandler(async (event) => { const adminSession = requireAdminSession(event) const zipFilePath = await createPostImportJobZipPath('upload.zip') let shouldRemoveZip = true try { const uploaded = await saveMultipartFileToDisk(event, { fieldName: 'file', filePath: zipFilePath, maxBytes: MAX_IMPORT_ZIP_BYTES }) if (!String(uploaded.filename || '').toLowerCase().endsWith('.zip')) { throw createError({ statusCode: 400, message: 'ZIP 파일만 Import할 수 있습니다.' }) } if (!uploaded.size) { throw createError({ statusCode: 400, message: '비어 있는 ZIP 파일은 Import할 수 없습니다.' }) } const job = await createPostImportJob({ zipFilePath, requestedBy: adminSession.userId, requestedEmail: adminSession.email, originalFileName: uploaded.filename || 'upload.zip' }) shouldRemoveZip = false queuePostImportJobRun(job.id) return job } catch (error) { if (error?.statusCode) { throw error } if (error?.code === 'MULTIPART_FILE_TOO_LARGE') { throw createError({ statusCode: 413, message: 'Import ZIP 파일은 최대 300MB까지 처리할 수 있습니다.' }) } if (['MULTIPART_FILE_NOT_FOUND', 'INVALID_MULTIPART_REQUEST'].includes(error?.code)) { throw createError({ statusCode: 400, message: 'Import할 ZIP 파일을 선택해 주세요.' }) } if (error?.message === 'IMPORT_MARKDOWN_NOT_FOUND') { throw createError({ statusCode: 400, message: 'ZIP 안에서 Import할 Markdown 게시물을 찾지 못했습니다.' }) } if (error?.message === 'IMPORT_POST_LIMIT_EXCEEDED') { throw createError({ statusCode: 400, message: '한 번에 Import할 수 있는 게시물 수를 초과했습니다.' }) } if ([ 'ZIP_ENTRY_COUNT_LIMIT_EXCEEDED', 'ZIP_ENTRY_SIZE_LIMIT_EXCEEDED', 'ZIP_TOTAL_SIZE_LIMIT_EXCEEDED', 'ZIP_COMPRESSION_RATIO_LIMIT_EXCEEDED', 'IMPORT_MARKDOWN_SIZE_LIMIT_EXCEEDED' ].includes(error?.message)) { throw createError({ statusCode: 413, message: 'ZIP 내부 파일의 개수·압축률 또는 압축 해제 크기가 허용 범위를 초과했습니다.' }) } if ([ 'INVALID_ZIP_FILE', 'UNSAFE_ZIP_ENTRY_PATH', 'DUPLICATE_ZIP_ENTRY_PATH' ].includes(error?.message)) { throw createError({ statusCode: 400, message: '안전하게 처리할 수 없는 ZIP 파일입니다.' }) } if (error?.code === '23505') { throw createError({ statusCode: 409, message: '게시물 Import 중 중복 데이터가 감지되었습니다. 다시 시도해 주세요.' }) } throw createError({ statusCode: 500, message: '게시물 Import 작업을 시작하지 못했습니다.' }) } finally { if (shouldRemoveZip) { await rm(zipFilePath, { force: true }) } } })