관리자 미디어 라이브러리 기본 기능 추가
This commit is contained in:
137
server/utils/media-library.js
Normal file
137
server/utils/media-library.js
Normal file
@@ -0,0 +1,137 @@
|
||||
import { readdir, rename, rm, stat } from 'node:fs/promises'
|
||||
import { basename, dirname, extname, join, relative } from 'node:path'
|
||||
import { createError } from 'h3'
|
||||
|
||||
const uploadRoot = join(process.cwd(), 'public', 'uploads')
|
||||
|
||||
/**
|
||||
* 미디어 파일명 조각을 안전하게 정리
|
||||
* @param {string} value - 원본 파일명
|
||||
* @returns {string} 정리된 파일명
|
||||
*/
|
||||
export const sanitizeMediaName = (value) => value
|
||||
.replace(/[^a-zA-Z0-9가-힣._-]/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
|
||||
/**
|
||||
* 업로드 URL을 실제 파일 경로로 변환
|
||||
* @param {string} url - 업로드 URL
|
||||
* @returns {string} 파일 경로
|
||||
*/
|
||||
export const resolveMediaPath = (url) => {
|
||||
if (!url?.startsWith('/uploads/')) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '미디어 URL 형식이 올바르지 않습니다.'
|
||||
})
|
||||
}
|
||||
|
||||
const decodedUrl = decodeURIComponent(url)
|
||||
const filePath = join(process.cwd(), 'public', decodedUrl)
|
||||
const relativePath = relative(uploadRoot, filePath)
|
||||
|
||||
if (relativePath.startsWith('..') || relativePath === '') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '미디어 경로가 올바르지 않습니다.'
|
||||
})
|
||||
}
|
||||
|
||||
return filePath
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 경로를 미디어 항목으로 변환
|
||||
* @param {string} filePath - 파일 경로
|
||||
* @returns {Promise<Object>} 미디어 항목
|
||||
*/
|
||||
const createMediaItem = async (filePath) => {
|
||||
const fileStat = await stat(filePath)
|
||||
const relativePath = relative(uploadRoot, filePath)
|
||||
const url = `/uploads/${relativePath.split('/').join('/')}`
|
||||
|
||||
return {
|
||||
url,
|
||||
name: basename(filePath),
|
||||
title: basename(filePath, extname(filePath)),
|
||||
size: fileStat.size,
|
||||
updatedAt: fileStat.mtime.toISOString(),
|
||||
category: relativePath.split('/')[0] || 'uploads'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 업로드 디렉토리의 이미지 파일을 재귀적으로 조회
|
||||
* @param {string} directoryPath - 조회할 디렉토리
|
||||
* @returns {Promise<Array<Object>>} 미디어 항목 목록
|
||||
*/
|
||||
const readMediaDirectory = async (directoryPath) => {
|
||||
let entries = []
|
||||
|
||||
try {
|
||||
entries = await readdir(directoryPath, { withFileTypes: true })
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
|
||||
const items = await Promise.all(entries.map(async (entry) => {
|
||||
const entryPath = join(directoryPath, entry.name)
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
return readMediaDirectory(entryPath)
|
||||
}
|
||||
|
||||
if (!/\.(jpe?g|png|webp|gif)$/i.test(entry.name)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [await createMediaItem(entryPath)]
|
||||
}))
|
||||
|
||||
return items.flat()
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 목록 조회
|
||||
* @returns {Promise<Array<Object>>} 미디어 항목 목록
|
||||
*/
|
||||
export const listMediaItems = async () => {
|
||||
const items = await readMediaDirectory(uploadRoot)
|
||||
|
||||
return items.sort((left, right) => new Date(right.updatedAt) - new Date(left.updatedAt))
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 파일 삭제
|
||||
* @param {string} url - 삭제할 미디어 URL
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const deleteMediaItem = async (url) => {
|
||||
await rm(resolveMediaPath(url))
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 파일명 변경
|
||||
* @param {string} url - 기존 미디어 URL
|
||||
* @param {string} name - 새 파일명
|
||||
* @returns {Promise<Object>} 변경된 미디어 항목
|
||||
*/
|
||||
export const renameMediaItem = async (url, name) => {
|
||||
const currentPath = resolveMediaPath(url)
|
||||
const currentExtension = extname(currentPath)
|
||||
const cleanName = sanitizeMediaName(name.replace(/\.[^.]+$/g, ''))
|
||||
|
||||
if (!cleanName) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: '파일명을 입력해 주세요.'
|
||||
})
|
||||
}
|
||||
|
||||
const nextPath = join(dirname(currentPath), `${cleanName}${currentExtension}`)
|
||||
|
||||
await rename(currentPath, nextPath)
|
||||
|
||||
return createMediaItem(nextPath)
|
||||
}
|
||||
Reference in New Issue
Block a user