미디어 카테고리 관리 추가
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { readBody } from 'h3'
|
||||
import { requireAdminSession } from '../../../utils/admin-auth'
|
||||
import { renameMediaItem } from '../../../utils/media-library'
|
||||
import { renameMediaItem, updateMediaCategory } from '../../../utils/media-library'
|
||||
|
||||
/**
|
||||
* 관리자 미디어 파일명 변경 API
|
||||
@@ -12,5 +12,9 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const body = await readBody(event)
|
||||
|
||||
if (body?.category !== undefined) {
|
||||
return updateMediaCategory(body?.url, body?.category)
|
||||
}
|
||||
|
||||
return renameMediaItem(body?.url, body?.name || '')
|
||||
})
|
||||
|
||||
@@ -2,9 +2,49 @@ import { readdir, rename, rm, stat } from 'node:fs/promises'
|
||||
import { basename, dirname, extname, join, relative } from 'node:path'
|
||||
import { createError } from 'h3'
|
||||
import { listAdminPosts, listPages } from '../repositories/content-repository'
|
||||
import { getPostgresClient } from '../repositories/postgres-client'
|
||||
|
||||
const uploadRoot = join(process.cwd(), 'public', 'uploads')
|
||||
|
||||
/**
|
||||
* 기본 미디어 카테고리 이름 반환
|
||||
* @param {string} relativePath - 업로드 루트 기준 상대 경로
|
||||
* @returns {string} 기본 카테고리
|
||||
*/
|
||||
const getDefaultMediaCategory = (relativePath) => relativePath.split('/')[0] || '미분류'
|
||||
|
||||
/**
|
||||
* 미디어 메타데이터 목록을 URL 기준 객체로 조회
|
||||
* @returns {Promise<Object>} URL별 미디어 메타데이터
|
||||
*/
|
||||
const getMediaMetadataMap = async () => {
|
||||
const sql = getPostgresClient()
|
||||
|
||||
if (!sql) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const rows = await sql`
|
||||
SELECT *
|
||||
FROM media_metadata
|
||||
`
|
||||
|
||||
return Object.fromEntries(rows.map((row) => [row.url, {
|
||||
category: row.category,
|
||||
updatedAt: row.updated_at.toISOString()
|
||||
}]))
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 카테고리 정리
|
||||
* @param {string} category - 입력 카테고리
|
||||
* @returns {string} 정리된 카테고리
|
||||
*/
|
||||
const normalizeMediaCategory = (category) => String(category || '')
|
||||
.trim()
|
||||
.replace(/\s+/g, ' ')
|
||||
|| '미분류'
|
||||
|
||||
/**
|
||||
* 미디어 파일명 조각을 안전하게 정리
|
||||
* @param {string} value - 원본 파일명
|
||||
@@ -58,7 +98,7 @@ const createMediaItem = async (filePath) => {
|
||||
title: basename(filePath, extname(filePath)),
|
||||
size: fileStat.size,
|
||||
updatedAt: fileStat.mtime.toISOString(),
|
||||
category: relativePath.split('/')[0] || 'uploads'
|
||||
category: getDefaultMediaCategory(relativePath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,18 +198,98 @@ const getMediaUsage = (url, posts, pages) => {
|
||||
*/
|
||||
export const listMediaItems = async () => {
|
||||
const items = await readMediaDirectory(uploadRoot)
|
||||
const metadataMap = await getMediaMetadataMap()
|
||||
const [posts, pages] = await Promise.all([
|
||||
listAdminPosts(),
|
||||
listPages()
|
||||
])
|
||||
const itemsWithUsage = items.map((item) => ({
|
||||
...item,
|
||||
category: metadataMap[item.url]?.category || item.category,
|
||||
usage: getMediaUsage(item.url, posts, pages)
|
||||
}))
|
||||
|
||||
return itemsWithUsage.sort((left, right) => new Date(right.updatedAt) - new Date(left.updatedAt))
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 메타데이터 삭제
|
||||
* @param {string} url - 미디어 URL
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const deleteMediaMetadata = async (url) => {
|
||||
const sql = getPostgresClient()
|
||||
|
||||
if (!sql) {
|
||||
return
|
||||
}
|
||||
|
||||
await sql`
|
||||
DELETE FROM media_metadata
|
||||
WHERE url = ${url}
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 메타데이터 URL 변경
|
||||
* @param {string} currentUrl - 기존 미디어 URL
|
||||
* @param {string} nextUrl - 새 미디어 URL
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const moveMediaMetadata = async (currentUrl, nextUrl) => {
|
||||
const sql = getPostgresClient()
|
||||
|
||||
if (!sql) {
|
||||
return
|
||||
}
|
||||
|
||||
await sql`
|
||||
UPDATE media_metadata
|
||||
SET
|
||||
url = ${nextUrl},
|
||||
updated_at = now()
|
||||
WHERE url = ${currentUrl}
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 카테고리 저장
|
||||
* @param {string} url - 미디어 URL
|
||||
* @param {string} category - 미디어 카테고리
|
||||
* @returns {Promise<Object>} 수정된 미디어 항목
|
||||
*/
|
||||
export const updateMediaCategory = async (url, category) => {
|
||||
const sql = getPostgresClient()
|
||||
const mediaPath = resolveMediaPath(url)
|
||||
|
||||
if (!sql) {
|
||||
throw new Error('DATABASE_REQUIRED')
|
||||
}
|
||||
|
||||
await sql`
|
||||
INSERT INTO media_metadata (
|
||||
url,
|
||||
category
|
||||
)
|
||||
VALUES (
|
||||
${url},
|
||||
${normalizeMediaCategory(category)}
|
||||
)
|
||||
ON CONFLICT (url) DO UPDATE
|
||||
SET
|
||||
category = EXCLUDED.category,
|
||||
updated_at = now()
|
||||
`
|
||||
|
||||
const item = await createMediaItem(mediaPath)
|
||||
|
||||
return {
|
||||
...item,
|
||||
category: normalizeMediaCategory(category),
|
||||
usage: []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 파일 삭제
|
||||
* @param {string} url - 삭제할 미디어 URL
|
||||
@@ -190,6 +310,7 @@ export const deleteMediaItem = async (url) => {
|
||||
}
|
||||
|
||||
await rm(resolveMediaPath(url))
|
||||
await deleteMediaMetadata(url)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,5 +348,8 @@ export const renameMediaItem = async (url, name) => {
|
||||
|
||||
await rename(currentPath, nextPath)
|
||||
|
||||
const renamedItem = await createMediaItem(nextPath)
|
||||
await moveMediaMetadata(url, renamedItem.url)
|
||||
|
||||
return createMediaItem(nextPath)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user