v1.10.88 품질·보안 개선과 미디어 카탈로그 database 전환을 반영한다.
세션·로그인 제한, ZIP/업로드 스트리밍, CSP 최소 강제와 Report-Only, Playwright 격리 검증을 추가하고 미디어 목록을 reconcile 후 database 모드로 고정한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
import { requireAdminSession } from '../../../utils/admin-auth'
|
||||
import { listMediaItems } from '../../../utils/media-library'
|
||||
import { getMediaCatalogMode } from '../../../utils/media-catalog-mode'
|
||||
import {
|
||||
getMediaCatalogReadiness,
|
||||
listMediaCatalogItems,
|
||||
listPagedMediaCatalogItems
|
||||
} from '../../../repositories/media-catalog-repository'
|
||||
import {
|
||||
isPageCoverThumbnailUrl,
|
||||
isPostBodyThumbnailUrl,
|
||||
@@ -172,7 +178,7 @@ const countMediaKinds = (items) => items.reduce((counts, item) => {
|
||||
* @param {Record<string, unknown>} query - 요청 쿼리
|
||||
* @returns {Object} 페이지 미디어 응답
|
||||
*/
|
||||
const createPagedMediaResponse = (items, query) => {
|
||||
export const createPagedMediaResponse = (items, query) => {
|
||||
const tab = normalizeMediaTab(query.tab)
|
||||
const pageSize = normalizeMediaPageSize(query.pageSize)
|
||||
const requestedPage = Math.max(1, Number(query.page) || 1)
|
||||
@@ -220,6 +226,17 @@ const createPagedMediaResponse = (items, query) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 목록의 민감정보 없는 비교 수치를 만든다.
|
||||
* @param {Array<Object>} items - 미디어 항목 목록
|
||||
* @returns {{ count: number, bytes: number, kinds: Record<string, number> }} 비교 수치
|
||||
*/
|
||||
const createMediaComparisonSummary = (items) => ({
|
||||
count: items.length,
|
||||
bytes: items.reduce((total, item) => total + Number(item.size || 0), 0),
|
||||
kinds: countMediaKinds(items)
|
||||
})
|
||||
|
||||
/**
|
||||
* 관리자 미디어 목록 API
|
||||
* @param {import('h3').H3Event} event - 요청 이벤트
|
||||
@@ -228,10 +245,53 @@ const createPagedMediaResponse = (items, query) => {
|
||||
export default defineEventHandler(async (event) => {
|
||||
requireAdminSession(event)
|
||||
|
||||
const items = await listMediaItems()
|
||||
const mode = getMediaCatalogMode()
|
||||
const query = getQuery(event)
|
||||
const paged = query.paged === '1' || query.paged === 'true'
|
||||
let items
|
||||
|
||||
if (query.paged === '1' || query.paged === 'true') {
|
||||
if (mode === 'database') {
|
||||
try {
|
||||
const readiness = await getMediaCatalogReadiness()
|
||||
|
||||
if (!readiness.ready) {
|
||||
console.warn('[media-catalog] DB 카탈로그 준비 미완료, 파일시스템으로 대체', {
|
||||
reason: readiness.reason
|
||||
})
|
||||
throw new Error('MEDIA_CATALOG_NOT_READY')
|
||||
}
|
||||
|
||||
if (paged) {
|
||||
return await listPagedMediaCatalogItems(query)
|
||||
}
|
||||
|
||||
items = await listMediaCatalogItems()
|
||||
} catch {
|
||||
console.warn('[media-catalog] DB 목록 조회 실패, 파일시스템으로 대체')
|
||||
items = await listMediaItems()
|
||||
}
|
||||
} else {
|
||||
items = await listMediaItems()
|
||||
|
||||
if (mode === 'shadow') {
|
||||
listMediaCatalogItems().then((catalogItems) => {
|
||||
const filesystem = createMediaComparisonSummary(items)
|
||||
const catalog = createMediaComparisonSummary(catalogItems)
|
||||
console.info('[media-catalog] shadow 차이', {
|
||||
count: catalog.count - filesystem.count,
|
||||
bytes: catalog.bytes - filesystem.bytes,
|
||||
kinds: Object.fromEntries(Object.keys(filesystem.kinds).map((kind) => [
|
||||
kind,
|
||||
Number(catalog.kinds[kind] || 0) - Number(filesystem.kinds[kind] || 0)
|
||||
]))
|
||||
})
|
||||
}).catch(() => {
|
||||
console.warn('[media-catalog] shadow 비교 실패')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (paged) {
|
||||
return createPagedMediaResponse(items, query)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user