feat: 관리자 수동 이미지 대체 기능 추가
This commit is contained in:
@@ -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