feat: 템플릿 태그와 병합 가져오기 및 에디터 제거 추가
This commit is contained in:
@@ -20,9 +20,11 @@ const {
|
||||
updateTopicThumbnail,
|
||||
createTopicItem,
|
||||
updateTopicItemLabel,
|
||||
updateTopicItemMeta,
|
||||
updateTopicItemDisplayOrder,
|
||||
countTierListsUsingTopicItem,
|
||||
updateCustomItemLabel,
|
||||
updateCustomItemMeta,
|
||||
updateImageAssetLabel,
|
||||
deleteTopicItem,
|
||||
deleteTopic,
|
||||
@@ -99,6 +101,17 @@ function buildItemLabelFromSrc(src) {
|
||||
const upload = createMemoryUpload(multer, { fileSize: 20 * 1024 * 1024, maxCount: 100 })
|
||||
const avatarUpload = createMemoryUpload(multer, { fileSize: 4 * 1024 * 1024 })
|
||||
|
||||
function normalizeRouteTags(tags) {
|
||||
const values = Array.isArray(tags) ? tags : typeof tags === 'string' ? tags.split(',') : []
|
||||
return Array.from(
|
||||
new Set(
|
||||
values
|
||||
.map((tag) => String(tag || '').trim().replace(/^#/, '').slice(0, 40))
|
||||
.filter(Boolean)
|
||||
)
|
||||
).slice(0, 30)
|
||||
}
|
||||
|
||||
function decorateAdminUser(user, primaryAdmin) {
|
||||
if (!user) return null
|
||||
const isPrimaryAdmin = !!user.isAdmin && primaryAdmin?.id === user.id
|
||||
@@ -127,6 +140,7 @@ router.post('/templates', requireAdmin, async (req, res) => {
|
||||
const schema = z.object({
|
||||
slug: z.string().trim().min(1).max(120).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
|
||||
name: z.string().min(1).max(60),
|
||||
tags: z.array(z.string().trim().min(1).max(40)).max(30).optional().default([]),
|
||||
isPublic: z.boolean().optional().default(false),
|
||||
thumbnailSrc: z.string().max(255).optional().default(''),
|
||||
})
|
||||
@@ -136,7 +150,12 @@ router.post('/templates', requireAdmin, async (req, res) => {
|
||||
if (exists) return res.status(409).json({ error: 'topic_slug_taken' })
|
||||
let template
|
||||
try {
|
||||
template = await createTopic({ slug: parsed.data.slug, name: parsed.data.name, isPublic: parsed.data.isPublic })
|
||||
template = await createTopic({
|
||||
slug: parsed.data.slug,
|
||||
name: parsed.data.name,
|
||||
tags: normalizeRouteTags(parsed.data.tags),
|
||||
isPublic: parsed.data.isPublic,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error?.code === 'TOPIC_SLUG_INVALID') return res.status(400).json({ error: 'topic_slug_invalid' })
|
||||
if (error?.code === 'ER_DUP_ENTRY') return res.status(409).json({ error: 'topic_slug_taken' })
|
||||
@@ -154,6 +173,7 @@ router.patch('/templates/:templateId', requireAdmin, async (req, res) => {
|
||||
const schema = z.object({
|
||||
slug: z.string().trim().min(1).max(120).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).optional(),
|
||||
name: z.string().trim().min(1).max(60).optional(),
|
||||
tags: z.array(z.string().trim().min(1).max(40)).max(30).optional(),
|
||||
isPublic: z.boolean().optional(),
|
||||
})
|
||||
const parsed = schema.safeParse(req.body)
|
||||
@@ -169,6 +189,7 @@ router.patch('/templates/:templateId', requireAdmin, async (req, res) => {
|
||||
? await updateTopicMeta(template.id, {
|
||||
slug: parsed.data.slug || template.slug,
|
||||
name: parsed.data.name || template.name,
|
||||
tags: typeof parsed.data.tags !== 'undefined' ? normalizeRouteTags(parsed.data.tags) : template.tags || [],
|
||||
isPublic: typeof parsed.data.isPublic === 'boolean' ? parsed.data.isPublic : template.isPublic,
|
||||
})
|
||||
: await findTopicById(template.id)
|
||||
@@ -309,14 +330,20 @@ router.get('/templates/:templateId/items/:itemId/usage', requireAdmin, async (re
|
||||
})
|
||||
|
||||
router.patch('/templates/:templateId/items/:itemId', requireAdmin, async (req, res) => {
|
||||
const schema = z.object({ label: z.string().trim().min(1).max(60) })
|
||||
const schema = z.object({
|
||||
label: z.string().trim().min(1).max(60),
|
||||
tags: z.array(z.string().trim().min(1).max(40)).max(30).optional().default([]),
|
||||
})
|
||||
const parsed = schema.safeParse(req.body)
|
||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||
|
||||
const template = await findTopicById(getTemplateIdFromParams(req))
|
||||
if (!template) return res.status(404).json({ error: 'not_found' })
|
||||
|
||||
const updated = await updateTopicItemLabel(req.params.itemId, parsed.data.label)
|
||||
const updated = await updateTopicItemMeta(req.params.itemId, {
|
||||
label: parsed.data.label,
|
||||
tags: normalizeRouteTags(parsed.data.tags),
|
||||
})
|
||||
if (!updated || updated.topicId !== template.id) return res.status(404).json({ error: 'not_found' })
|
||||
res.json({ item: updated })
|
||||
})
|
||||
@@ -332,6 +359,7 @@ router.delete('/templates/:templateId', requireAdmin, async (req, res) => {
|
||||
router.patch('/custom-items/:itemId/label', requireAdmin, async (req, res) => {
|
||||
const schema = z.object({
|
||||
label: z.string().trim().min(1).max(60),
|
||||
tags: z.array(z.string().trim().min(1).max(40)).max(30).optional().default([]),
|
||||
sourceType: z.enum(['template', 'user', 'asset']).optional().default('user'),
|
||||
})
|
||||
const parsed = schema.safeParse(req.body)
|
||||
@@ -345,12 +373,18 @@ router.patch('/custom-items/:itemId/label', requireAdmin, async (req, res) => {
|
||||
}
|
||||
|
||||
if (parsed.data.sourceType === 'template') {
|
||||
const updated = await updateTopicItemLabel(itemId, parsed.data.label)
|
||||
const updated = await updateTopicItemMeta(itemId, {
|
||||
label: parsed.data.label,
|
||||
tags: normalizeRouteTags(parsed.data.tags),
|
||||
})
|
||||
if (!updated) return res.status(404).json({ error: 'not_found' })
|
||||
return res.json({ item: updated })
|
||||
}
|
||||
|
||||
const updated = await updateCustomItemLabel(itemId, parsed.data.label)
|
||||
const updated = await updateCustomItemMeta(itemId, {
|
||||
label: parsed.data.label,
|
||||
tags: normalizeRouteTags(parsed.data.tags),
|
||||
})
|
||||
if (!updated) return res.status(404).json({ error: 'not_found' })
|
||||
return res.json({ item: updated })
|
||||
})
|
||||
@@ -559,6 +593,7 @@ async function promoteLibraryItemToTemplateItem({ item, templateId }) {
|
||||
topicId: templateId,
|
||||
src: item.src || '',
|
||||
label: item.label,
|
||||
tags: item.tags || [],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -576,6 +611,7 @@ async function findLibraryItemForReplacement(itemId, sourceType = '') {
|
||||
sourceType: 'asset',
|
||||
src: asset.src || '',
|
||||
label: asset.labelOverride || buildItemLabelFromSrc(asset.src),
|
||||
tags: [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,6 +623,7 @@ async function findLibraryItemForReplacement(itemId, sourceType = '') {
|
||||
sourceType: 'template',
|
||||
src: item.src || '',
|
||||
label: item.label || buildItemLabelFromSrc(item.src),
|
||||
tags: item.tags || [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,6 +634,7 @@ async function findLibraryItemForReplacement(itemId, sourceType = '') {
|
||||
sourceType: 'user',
|
||||
src: customItem.src || '',
|
||||
label: customItem.label || buildItemLabelFromSrc(customItem.src),
|
||||
tags: customItem.tags || [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,6 +645,7 @@ async function findLibraryItemForReplacement(itemId, sourceType = '') {
|
||||
sourceType: 'template',
|
||||
src: templateItem.src || '',
|
||||
label: templateItem.label || buildItemLabelFromSrc(templateItem.src),
|
||||
tags: templateItem.tags || [],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user