게시물 Export ZIP 생성 연결 v1.5.22
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import { requireAdminSession } from '../../../../utils/admin-auth'
|
||||
import { listPostExportJobs } from '../../../../repositories/post-export-repository'
|
||||
import {
|
||||
listPostExportJobs,
|
||||
queuePendingPostExportJobs
|
||||
} from '../../../../repositories/post-export-repository'
|
||||
|
||||
/**
|
||||
* 관리자 게시물 Export 작업 목록 API
|
||||
@@ -8,6 +11,9 @@ import { listPostExportJobs } from '../../../../repositories/post-export-reposit
|
||||
*/
|
||||
export default defineEventHandler((event) => {
|
||||
requireAdminSession(event)
|
||||
queuePendingPostExportJobs().catch((error) => {
|
||||
console.error('대기 중인 게시물 Export 작업 실행 실패', error)
|
||||
})
|
||||
|
||||
return listPostExportJobs()
|
||||
})
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { readBody } from 'h3'
|
||||
import { z } from 'zod'
|
||||
import { requireAdminSession } from '../../../../utils/admin-auth'
|
||||
import { createPostExportJob } from '../../../../repositories/post-export-repository'
|
||||
import {
|
||||
createPostExportJob,
|
||||
queuePostExportJobRun
|
||||
} from '../../../../repositories/post-export-repository'
|
||||
|
||||
const postExportJobInputSchema = z.object({
|
||||
chunkSize: z.number().int().min(1).max(500).optional(),
|
||||
@@ -17,11 +20,17 @@ export default defineEventHandler(async (event) => {
|
||||
const adminSession = requireAdminSession(event)
|
||||
const input = postExportJobInputSchema.parse(await readBody(event))
|
||||
|
||||
return createPostExportJob({
|
||||
const job = await createPostExportJob({
|
||||
requestedBy: adminSession.userId,
|
||||
requestedEmail: adminSession.email,
|
||||
scope: 'all',
|
||||
chunkSize: input.chunkSize,
|
||||
retentionDays: input.retentionDays
|
||||
})
|
||||
|
||||
if (job.status === 'queued') {
|
||||
queuePostExportJobRun(job.id)
|
||||
}
|
||||
|
||||
return job
|
||||
})
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
Reference in New Issue
Block a user