미디어 카테고리 관리 추가

This commit is contained in:
2026-05-02 17:56:00 +09:00
parent 04b8a7006a
commit dd0a643d73
11 changed files with 242 additions and 19 deletions

View File

@@ -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 || '')
})

View File

@@ -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)
}