게시물 Export ZIP 생성 연결 v1.5.22

This commit is contained in:
2026-06-01 13:33:41 +09:00
parent 7c8245c4e9
commit f8621d49d8
14 changed files with 962 additions and 19 deletions

View File

@@ -0,0 +1,85 @@
import { createReadStream } from 'node:fs'
import { stat } from 'node:fs/promises'
import { join, relative } from 'node:path'
import {
createError,
getRouterParam,
sendStream,
setResponseHeader
} from 'h3'
import { requireAdminSession } from '../../../../../../utils/admin-auth'
import { getReadyPostExportFile } from '../../../../../../repositories/post-export-repository'
const uploadRoot = join(process.cwd(), 'public', 'uploads')
/**
* Export 파일 경로를 안전하게 해석한다.
* @param {string} filePath - DB에 저장된 상대 경로
* @returns {string|null} 디스크 경로
*/
const resolveExportFilePath = (filePath) => {
const absolutePath = join(uploadRoot, filePath || '')
const relativePath = relative(uploadRoot, absolutePath)
if (!relativePath || relativePath.startsWith('..')) {
return null
}
return absolutePath
}
/**
* 관리자 게시물 Export 파일 다운로드 API
* @param {import('h3').H3Event} event - 요청 이벤트
* @returns {Promise<unknown>} ZIP 파일 스트림
*/
export default defineEventHandler(async (event) => {
requireAdminSession(event)
const fileId = getRouterParam(event, 'fileId')
if (!fileId) {
throw createError({
statusCode: 400,
statusMessage: 'Export 파일 ID가 필요합니다.'
})
}
const exportFile = await getReadyPostExportFile(fileId)
if (!exportFile) {
throw createError({
statusCode: 404,
statusMessage: 'Export 파일을 찾을 수 없습니다.'
})
}
const absolutePath = resolveExportFilePath(exportFile.filePath)
if (!absolutePath) {
throw createError({
statusCode: 404,
statusMessage: 'Export 파일 경로가 올바르지 않습니다.'
})
}
const fileStat = await stat(absolutePath).catch(() => null)
if (!fileStat?.isFile()) {
throw createError({
statusCode: 404,
statusMessage: 'Export 파일이 서버에 없습니다.'
})
}
setResponseHeader(event, 'content-type', 'application/zip')
setResponseHeader(event, 'content-length', String(fileStat.size))
setResponseHeader(event, 'cache-control', 'private, no-store')
setResponseHeader(
event,
'content-disposition',
`attachment; filename*=UTF-8''${encodeURIComponent(exportFile.fileName)}`
)
return sendStream(event, createReadStream(absolutePath))
})