feat: 관리자 수동 이미지 대체 기능 추가
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user