feat: 관리자 수동 이미지 대체 기능 추가

This commit is contained in:
2026-04-06 10:41:05 +09:00
parent dddc29fd4b
commit 7164d32ae8
7 changed files with 269 additions and 15 deletions

View File

@@ -1114,24 +1114,33 @@ async function listReferencedUploadUsage() {
.sort((a, b) => a.src.localeCompare(b.src))
}
function replaceItemSrc(items, fromSrc, toSrc) {
function replaceItemSrc(items, fromSrc, toSrc, toLabel = '') {
let changed = false
const nextItems = (items || []).map((item) => {
if (item?.src !== fromSrc) return item
changed = true
return { ...item, src: toSrc }
return {
...item,
src: toSrc,
...(typeof toLabel === 'string' && toLabel.trim() ? { label: toLabel.trim().slice(0, 60) } : {}),
}
})
return { changed, items: nextItems }
}
async function replaceUploadSourceReferences({ fromSrc, toSrc }) {
async function replaceUploadSourceReferences({ fromSrc, toSrc, toLabel = '' }) {
if (!fromSrc || !toSrc || fromSrc === toSrc) return { updatedRows: 0 }
const normalizedLabel = typeof toLabel === 'string' ? toLabel.trim().slice(0, 60) : ''
const [userResult, topicResult, topicItemResult, customItemResult] = await Promise.all([
query('UPDATE users SET avatar_src = ? WHERE avatar_src = ?', [toSrc, fromSrc]),
query('UPDATE topics SET thumbnail_src = ? WHERE thumbnail_src = ?', [toSrc, fromSrc]),
query('UPDATE topic_items SET src = ? WHERE src = ?', [toSrc, fromSrc]),
query('UPDATE custom_items SET src = ? WHERE src = ?', [toSrc, fromSrc]),
normalizedLabel
? query('UPDATE topic_items SET src = ?, label = ? WHERE src = ?', [toSrc, normalizedLabel, fromSrc])
: query('UPDATE topic_items SET src = ? WHERE src = ?', [toSrc, fromSrc]),
normalizedLabel
? query('UPDATE custom_items SET src = ?, label = ? WHERE src = ?', [toSrc, normalizedLabel, fromSrc])
: query('UPDATE custom_items SET src = ? WHERE src = ?', [toSrc, fromSrc]),
])
let updatedRows = Number(userResult.affectedRows || 0) + Number(topicResult.affectedRows || 0) + Number(topicItemResult.affectedRows || 0) + Number(customItemResult.affectedRows || 0)
@@ -1145,7 +1154,7 @@ async function replaceUploadSourceReferences({ fromSrc, toSrc }) {
changed = true
}
const replacedPool = replaceItemSrc(parseJson(row.pool_json, []), fromSrc, toSrc)
const replacedPool = replaceItemSrc(parseJson(row.pool_json, []), fromSrc, toSrc, normalizedLabel)
if (replacedPool.changed) changed = true
if (changed) {
@@ -1168,8 +1177,8 @@ async function replaceUploadSourceReferences({ fromSrc, toSrc }) {
changed = true
}
const replacedItems = replaceItemSrc(parseJson(row.items_json, []), fromSrc, toSrc)
const replacedBoardItems = replaceItemSrc(parseJson(row.board_items_json, []), fromSrc, toSrc)
const replacedItems = replaceItemSrc(parseJson(row.items_json, []), fromSrc, toSrc, normalizedLabel)
const replacedBoardItems = replaceItemSrc(parseJson(row.board_items_json, []), fromSrc, toSrc, normalizedLabel)
if (replacedItems.changed || replacedBoardItems.changed) changed = true
if (changed) {

View File

@@ -53,6 +53,7 @@ const {
listRecentImageOptimizationJobs,
clearImageOptimizationJobs,
cleanupMissingUploadReferences,
replaceUploadSourceReferences,
} = require('../db')
const { requireAdmin } = require('../middleware/auth')
const { createMemoryUpload, writeOptimizedImage, getImageOptimizationQueueState } = require('../lib/image-storage')
@@ -551,6 +552,57 @@ async function promoteLibraryItemToTemplateItem({ item, templateId }) {
})
}
async function findLibraryItemForReplacement(itemId, sourceType = '') {
const normalizedId = String(itemId || '').trim()
const normalizedSourceType = String(sourceType || '').trim()
if (!normalizedId) return null
if (normalizedId.startsWith('asset:') || normalizedSourceType === 'asset') {
const assetId = normalizedId.startsWith('asset:') ? normalizedId.slice(6) : normalizedId
const asset = await findImageAssetById(assetId)
if (!asset) return null
return {
id: `asset:${asset.id}`,
sourceType: 'asset',
src: asset.src || '',
label: asset.labelOverride || buildItemLabelFromSrc(asset.src),
}
}
if (normalizedSourceType === 'template') {
const item = await findTopicItemById(normalizedId)
if (!item) return null
return {
id: item.id,
sourceType: 'template',
src: item.src || '',
label: item.label || buildItemLabelFromSrc(item.src),
}
}
const customItem = await findCustomItemById(normalizedId)
if (customItem) {
return {
id: customItem.id,
sourceType: 'user',
src: customItem.src || '',
label: customItem.label || buildItemLabelFromSrc(customItem.src),
}
}
const templateItem = await findTopicItemById(normalizedId)
if (templateItem) {
return {
id: templateItem.id,
sourceType: 'template',
src: templateItem.src || '',
label: templateItem.label || buildItemLabelFromSrc(templateItem.src),
}
}
return null
}
async function copyUploadIntoTopicAsset(src) {
if (typeof src !== 'string') return ''
const raw = src.trim()
@@ -760,6 +812,37 @@ router.post('/custom-items/:itemId/promote', requireAdmin, async (req, res) => {
res.json({ item })
})
router.post('/custom-items/:itemId/replace', requireAdmin, async (req, res) => {
const schema = z.object({
targetItemId: z.string().trim().min(1),
targetSourceType: z.enum(['template', 'user', 'asset']).optional().default('user'),
})
const parsed = schema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
const sourceItem = await findLibraryItemForReplacement(req.params.itemId)
if (!sourceItem?.src) return res.status(404).json({ error: 'source_not_found' })
const targetItem = await findLibraryItemForReplacement(parsed.data.targetItemId, parsed.data.targetSourceType)
if (!targetItem?.src) return res.status(404).json({ error: 'target_not_found' })
if (sourceItem.src === targetItem.src && (sourceItem.label || '') === (targetItem.label || '')) {
return res.status(409).json({ error: 'same_target' })
}
const result = await replaceUploadSourceReferences({
fromSrc: sourceItem.src,
toSrc: targetItem.src,
toLabel: targetItem.label || '',
})
res.json({
ok: true,
updatedRows: result.updatedRows || 0,
sourceItem,
targetItem,
})
})
router.post('/tierlists/:tierListId/promote-items', requireAdmin, async (req, res) => {
const schema = z.object({
topicId: z.string().min(1),