템플릿 slug 구조와 빈 DB 초기화를 정리

This commit is contained in:
2026-04-03 14:36:52 +09:00
parent 30ec2e55b0
commit f506e31549
20 changed files with 422 additions and 290 deletions

View File

@@ -10,13 +10,14 @@ const {
findUserByEmail,
findUserByNickname,
findTopicById,
findTopicBySlug,
findTopicItemById,
listTopicItems,
findImageAssetById,
createTopic,
listTopics,
updateTopicMeta,
updateTopicThumbnail,
updateTopicVisibility,
createTopicItem,
updateTopicItemLabel,
updateTopicItemDisplayOrder,
@@ -119,16 +120,23 @@ function canManageAdminRole(actingUser, primaryAdmin) {
router.post('/templates', requireAdmin, async (req, res) => {
const schema = z.object({
id: z.string().min(1),
slug: z.string().trim().min(1).max(120).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
name: z.string().min(1).max(60),
isPublic: z.boolean().optional().default(false),
thumbnailSrc: z.string().max(255).optional().default(''),
})
const parsed = schema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
const exists = await findTopicById(parsed.data.id)
if (exists) return res.status(409).json({ error: 'topic_id_taken' })
const template = await createTopic({ id: parsed.data.id, name: parsed.data.name, isPublic: parsed.data.isPublic })
const exists = await findTopicBySlug(parsed.data.slug)
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 })
} 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' })
throw error
}
if (parsed.data.thumbnailSrc) {
const copiedThumb = await copyUploadIntoTopicAsset(parsed.data.thumbnailSrc)
await updateTopicThumbnail(template.id, copiedThumb)
@@ -139,7 +147,9 @@ router.post('/templates', requireAdmin, async (req, res) => {
router.patch('/templates/:templateId', requireAdmin, async (req, res) => {
const schema = z.object({
isPublic: z.boolean(),
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(),
isPublic: z.boolean().optional(),
})
const parsed = schema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
@@ -148,8 +158,21 @@ router.patch('/templates/:templateId', requireAdmin, async (req, res) => {
const template = await findTopicById(templateId)
if (!template) return res.status(404).json({ error: 'not_found' })
const updated = await updateTopicVisibility(template.id, parsed.data.isPublic)
res.json({ template: updated })
try {
const updated =
typeof parsed.data.name === 'string' || typeof parsed.data.slug === 'string' || typeof parsed.data.isPublic === 'boolean'
? await updateTopicMeta(template.id, {
slug: parsed.data.slug || template.slug,
name: parsed.data.name || template.name,
isPublic: typeof parsed.data.isPublic === 'boolean' ? parsed.data.isPublic : template.isPublic,
})
: await findTopicById(template.id)
return res.json({ template: updated })
} 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' })
throw error
}
})
router.patch('/templates/display-order', requireAdmin, async (req, res) => {
@@ -632,11 +655,11 @@ function pickTemplateRequestItems(templateRequest, itemIds = [], itemLabels = {}
})
}
async function createTemplateFromTierList({ tierList, templateId, templateName }) {
await createTopic({ id: templateId, name: templateName, isPublic: false })
async function createTemplateFromTierList({ tierList, templateSlug, templateName }) {
const template = await createTopic({ slug: templateSlug, name: templateName, isPublic: false })
if (tierList.thumbnailSrc) {
const copiedThumb = await copyUploadIntoTopicAsset(tierList.thumbnailSrc)
await updateTopicThumbnail(templateId, copiedThumb)
await updateTopicThumbnail(template.id, copiedThumb)
}
const createdItems = []
@@ -645,30 +668,30 @@ async function createTemplateFromTierList({ tierList, templateId, templateName }
createdItems.push(
await createTopicItem({
id: nanoid(),
topicId: templateId,
topicId: template.id,
src: copiedSrc,
label: item.label,
})
)
}
return { template: await findTopicById(templateId), items: createdItems }
return { template: await findTopicById(template.id), items: createdItems }
}
async function createTemplateFromRequest({ templateRequest, templateId, templateName }) {
await createTopic({ id: templateId, name: templateName, isPublic: false })
async function createTemplateFromRequest({ templateRequest, templateSlug, templateName }) {
const template = await createTopic({ slug: templateSlug, name: templateName, isPublic: false })
if (templateRequest.thumbnailSrc) {
const copiedThumb = await copyUploadIntoTopicAsset(templateRequest.thumbnailSrc)
await updateTopicThumbnail(templateId, copiedThumb)
await updateTopicThumbnail(template.id, copiedThumb)
}
const items = await promoteSnapshotItemsToTemplate({
items: templateRequest.items || [],
templateId,
templateId: template.id,
})
return { template: await findTopicById(templateId), items }
return { template: await findTopicById(template.id), items }
}
router.delete('/custom-items/:itemId', requireAdmin, async (req, res) => {
@@ -761,28 +784,34 @@ router.post('/tierlists/:tierListId/promote-items', requireAdmin, async (req, re
router.post('/tierlists/:tierListId/create-template', requireAdmin, async (req, res) => {
const schema = z.object({
topicId: z.string().trim().min(1).max(120),
slug: z.string().trim().min(1).max(120).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
name: z.string().trim().min(1).max(120),
itemIds: z.array(z.string().min(1)).optional().default([]),
})
const parsed = schema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
const exists = await findTopicById(parsed.data.topicId)
if (exists) return res.status(409).json({ error: 'topic_id_taken' })
const exists = await findTopicBySlug(parsed.data.slug)
if (exists) return res.status(409).json({ error: 'topic_slug_taken' })
const tierList = await findTierListById(req.params.tierListId)
if (!tierList) return res.status(404).json({ error: 'not_found' })
const result = await createTemplateFromTierList({
tierList: {
...tierList,
pool: parsed.data.itemIds.length ? (tierList.pool || []).filter((item) => parsed.data.itemIds.includes(item.id)) : tierList.pool,
},
templateId: parsed.data.topicId,
templateName: parsed.data.name,
})
res.json(result)
try {
const result = await createTemplateFromTierList({
tierList: {
...tierList,
pool: parsed.data.itemIds.length ? (tierList.pool || []).filter((item) => parsed.data.itemIds.includes(item.id)) : tierList.pool,
},
templateSlug: parsed.data.slug,
templateName: parsed.data.name,
})
return res.json(result)
} 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' })
throw error
}
})
router.patch('/tierlists/:tierListId', requireAdmin, async (req, res) => {
@@ -832,20 +861,27 @@ router.post('/template-requests/:requestId/approve', requireAdmin, async (req, r
}
const schema = z.object({
topicId: z.string().trim().min(1).max(120),
slug: z.string().trim().min(1).max(120).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
name: z.string().trim().min(1).max(120),
})
const parsed = schema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
const exists = await findTopicById(parsed.data.topicId)
if (exists) return res.status(409).json({ error: 'topic_id_taken' })
const exists = await findTopicBySlug(parsed.data.slug)
if (exists) return res.status(409).json({ error: 'topic_slug_taken' })
const result = await createTemplateFromRequest({
templateRequest,
templateId: parsed.data.topicId,
templateName: parsed.data.name,
})
let result
try {
result = await createTemplateFromRequest({
templateRequest,
templateSlug: parsed.data.slug,
templateName: parsed.data.name,
})
} 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' })
throw error
}
const request = await updateTemplateRequestStatus({ id: templateRequest.id, status: 'approved' })
res.json({ request, ...result })
})