릴리스: v1.4.32 내부 이름층 topic/template 정리 마감

This commit is contained in:
2026-04-02 21:50:36 +09:00
parent 60cc5a72c5
commit 85863b1b36
14 changed files with 125 additions and 114 deletions

View File

@@ -24,7 +24,7 @@ const allowedOrigins = (process.env.CORS_ORIGINS || '')
const FileStore = FileStoreFactory(session)
;['uploads/avatars', 'uploads/games', 'uploads/custom', 'uploads/tierlists', '.sessions'].forEach((relativePath) => {
;['uploads/avatars', 'uploads/topics', 'uploads/custom', 'uploads/tierlists', '.sessions'].forEach((relativePath) => {
fs.mkdirSync(path.join(__dirname, relativePath), { recursive: true })
})

View File

@@ -7,7 +7,7 @@ const {
} = require('../src/db')
const BACKEND_ROOT = path.join(__dirname, '..')
const TARGET_DIRS = ['avatars', 'custom', 'games', 'tierlists']
const TARGET_DIRS = ['avatars', 'custom', 'topics', 'tierlists']
async function main() {
await ensureData()

View File

@@ -35,7 +35,7 @@ function getOptimizationConfig(roles) {
if (roleSet.has('avatar')) {
return { directory: 'avatars', width: 512, height: 512, fit: 'cover', quality: 82 }
}
if (roleSet.has('game-thumbnail') || roleSet.has('tierlist-thumbnail') || roleSet.has('template-thumbnail')) {
if (roleSet.has('topic-thumbnail') || roleSet.has('tierlist-thumbnail') || roleSet.has('template-thumbnail')) {
return { directory: 'legacy-thumbnails', width: 1280, height: 1280, fit: 'inside', quality: 84 }
}
return { directory: 'legacy-items', width: 512, height: 512, fit: 'inside', quality: 84 }

View File

@@ -67,7 +67,7 @@ function mapUserRow(row) {
}
}
function mapGameRow(row) {
function mapTopicRow(row) {
if (!row) return null
return {
id: row.id,
@@ -81,7 +81,7 @@ function mapGameRow(row) {
}
}
function mapGameItemRow(row) {
function mapTopicItemRow(row) {
if (!row) return null
return {
id: row.id,
@@ -292,8 +292,8 @@ async function ensureSchema() {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
`)
const gameIsPublicColumns = await query("SHOW COLUMNS FROM topics LIKE 'is_public'")
if (!gameIsPublicColumns.length) {
const topicIsPublicColumns = await query("SHOW COLUMNS FROM topics LIKE 'is_public'")
if (!topicIsPublicColumns.length) {
await query('ALTER TABLE topics ADD COLUMN is_public TINYINT(1) NOT NULL DEFAULT 1 AFTER thumbnail_src')
await query('UPDATE topics SET is_public = 1 WHERE is_public IS NULL')
}
@@ -316,8 +316,8 @@ async function ensureSchema() {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
`)
const gameItemDisplayOrderColumns = await query("SHOW COLUMNS FROM topic_items LIKE 'display_order'")
if (!gameItemDisplayOrderColumns.length) {
const topicItemDisplayOrderColumns = await query("SHOW COLUMNS FROM topic_items LIKE 'display_order'")
if (!topicItemDisplayOrderColumns.length) {
await query('ALTER TABLE topic_items ADD COLUMN display_order INT NULL DEFAULT NULL AFTER label')
}
@@ -705,7 +705,7 @@ async function listTopics(currentUserId = '', options = {}) {
`,
[FREEFORM_TOPIC_ID]
)
const topics = rows.map(mapGameRow)
const topics = rows.map(mapTopicRow)
if (!currentUserId) return topics.map((topic) => ({ ...topic, isFavorited: false }))
const favoriteRows = await query('SELECT topic_id FROM favorite_topics WHERE user_id = ?', [currentUserId])
@@ -718,7 +718,7 @@ async function listTopics(currentUserId = '', options = {}) {
async function findTopicById(id) {
const rows = await query('SELECT id, name, thumbnail_src, is_public, display_rank, created_at FROM topics WHERE id = ? LIMIT 1', [id])
return mapGameRow(rows[0])
return mapTopicRow(rows[0])
}
async function listTopicItems(topicId) {
@@ -735,12 +735,12 @@ async function listTopicItems(topicId) {
`,
[topicId]
)
return rows.map(mapGameItemRow)
return rows.map(mapTopicItemRow)
}
async function findTopicItemById(itemId) {
const rows = await query('SELECT id, topic_id, src, label, display_order, created_at FROM topic_items WHERE id = ? LIMIT 1', [itemId])
return mapGameItemRow(rows[0])
return mapTopicItemRow(rows[0])
}
async function getTopicDetail(topicId) {
@@ -868,7 +868,7 @@ async function listUnusedImageAssets({ limit = 100, minAgeHours = 24 } = {}) {
const referencedSrcs = new Set()
const [userRows, gameRows, gameItemRows, customItemRows, tierListRows, templateRequestRows] = await Promise.all([
const [userRows, topicRows, topicItemRows, customItemRows, tierListRows, templateRequestRows] = await Promise.all([
query("SELECT avatar_src FROM users WHERE avatar_src <> ''"),
query("SELECT thumbnail_src FROM topics WHERE thumbnail_src <> ''"),
query("SELECT src FROM topic_items WHERE src <> ''"),
@@ -878,8 +878,8 @@ async function listUnusedImageAssets({ limit = 100, minAgeHours = 24 } = {}) {
])
for (const row of userRows) if (row.avatar_src) referencedSrcs.add(row.avatar_src)
for (const row of gameRows) if (row.thumbnail_src) referencedSrcs.add(row.thumbnail_src)
for (const row of gameItemRows) if (row.src) referencedSrcs.add(row.src)
for (const row of topicRows) if (row.thumbnail_src) referencedSrcs.add(row.thumbnail_src)
for (const row of topicItemRows) if (row.src) referencedSrcs.add(row.src)
for (const row of customItemRows) if (row.src) referencedSrcs.add(row.src)
for (const row of tierListRows) {
@@ -921,7 +921,7 @@ async function listReferencedUploadUsage() {
usageMap.get(src).add(role)
}
const [userRows, gameRows, gameItemRows, customItemRows, tierListRows, templateRequestRows] = await Promise.all([
const [userRows, topicRows, topicItemRows, customItemRows, tierListRows, templateRequestRows] = await Promise.all([
query("SELECT avatar_src FROM users WHERE avatar_src <> ''"),
query("SELECT thumbnail_src FROM topics WHERE thumbnail_src <> ''"),
query("SELECT src FROM topic_items WHERE src <> ''"),
@@ -931,8 +931,8 @@ async function listReferencedUploadUsage() {
])
for (const row of userRows) addUsage(row.avatar_src, 'avatar')
for (const row of gameRows) addUsage(row.thumbnail_src, 'topic-thumbnail')
for (const row of gameItemRows) addUsage(row.src, 'topic-item')
for (const row of topicRows) addUsage(row.thumbnail_src, 'topic-thumbnail')
for (const row of topicItemRows) addUsage(row.src, 'topic-item')
for (const row of customItemRows) addUsage(row.src, 'custom-item')
for (const row of tierListRows) {
@@ -964,14 +964,14 @@ function replaceItemSrc(items, fromSrc, toSrc) {
async function replaceUploadSourceReferences({ fromSrc, toSrc }) {
if (!fromSrc || !toSrc || fromSrc === toSrc) return { updatedRows: 0 }
const [userResult, gameResult, gameItemResult, customItemResult] = await Promise.all([
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]),
])
let updatedRows = Number(userResult.affectedRows || 0) + Number(gameResult.affectedRows || 0) + Number(gameItemResult.affectedRows || 0) + Number(customItemResult.affectedRows || 0)
let updatedRows = Number(userResult.affectedRows || 0) + Number(topicResult.affectedRows || 0) + Number(topicItemResult.affectedRows || 0) + Number(customItemResult.affectedRows || 0)
const tierListRows = await query('SELECT id, thumbnail_src, pool_json FROM tierlists')
for (const row of tierListRows) {
@@ -1120,16 +1120,16 @@ function stripMissingItems(items, missingItemIds, missingSrcs) {
async function cleanupMissingUploadReferences() {
const stats = {
clearedAvatars: 0,
clearedGameThumbnails: 0,
clearedTopicThumbnails: 0,
clearedTierListThumbnails: 0,
clearedTemplateRequestThumbnails: 0,
deletedGameItems: 0,
deletedTopicItems: 0,
updatedTierLists: 0,
updatedTemplateRequests: 0,
deletedCustomItems: 0,
}
const [userRows, gameRows, gameItemRows, customItemRows, tierListRows, templateRequestRows] = await Promise.all([
const [userRows, topicRows, topicItemRows, customItemRows, tierListRows, templateRequestRows] = await Promise.all([
query("SELECT id, avatar_src FROM users WHERE avatar_src <> ''"),
query("SELECT id, thumbnail_src FROM topics WHERE thumbnail_src <> ''"),
query("SELECT id, src FROM topic_items WHERE src <> ''"),
@@ -1144,16 +1144,16 @@ async function cleanupMissingUploadReferences() {
stats.clearedAvatars += 1
}
for (const row of gameRows) {
for (const row of topicRows) {
if (await fileExistsForUploadSrc(row.thumbnail_src)) continue
await query('UPDATE topics SET thumbnail_src = ? WHERE id = ?', ['', row.id])
stats.clearedGameThumbnails += 1
stats.clearedTopicThumbnails += 1
}
for (const row of gameItemRows) {
for (const row of topicItemRows) {
if (await fileExistsForUploadSrc(row.src)) continue
await deleteTopicItem(row.id)
stats.deletedGameItems += 1
stats.deletedTopicItems += 1
}
const missingCustomItemIds = new Set()
@@ -1313,13 +1313,13 @@ async function createTopicItem({ id, topicId, src, label }) {
createdAt,
])
const rows = await query('SELECT id, topic_id, src, label, display_order, created_at FROM topic_items WHERE id = ? LIMIT 1', [id])
return mapGameItemRow(rows[0])
return mapTopicItemRow(rows[0])
}
async function updateTopicItemLabel(itemId, label) {
await query('UPDATE topic_items SET label = ? WHERE id = ?', [label, itemId])
const rows = await query('SELECT id, topic_id, src, label, display_order, created_at FROM topic_items WHERE id = ? LIMIT 1', [itemId])
return mapGameItemRow(rows[0])
return mapTopicItemRow(rows[0])
}
async function updateTopicItemDisplayOrder(topicId, itemIds) {
@@ -1521,7 +1521,7 @@ async function listCustomItems({ queryText = '', page = 1, limit = 50, filterMod
const hasQuery = !!searchText
const search = `%${searchText}%`
const [customRows, gameItemRows, assetRows, usageMeta] = await Promise.all([
const [customRows, topicItemRows, assetRows, usageMeta] = await Promise.all([
query(
`
SELECT
@@ -1569,7 +1569,7 @@ async function listCustomItems({ queryText = '', page = 1, limit = 50, filterMod
])
const templateLinkedBySrc = new Map()
gameItemRows.forEach((row) => {
topicItemRows.forEach((row) => {
if (!row?.src) return
if (!templateLinkedBySrc.has(row.src)) templateLinkedBySrc.set(row.src, new Map())
templateLinkedBySrc.get(row.src).set(row.topic_id, {
@@ -1596,7 +1596,7 @@ async function listCustomItems({ queryText = '', page = 1, limit = 50, filterMod
}
})
const templateSrcSet = new Set(gameItemRows.map((row) => row.src).filter(Boolean))
const templateSrcSet = new Set(topicItemRows.map((row) => row.src).filter(Boolean))
const customSrcSet = new Set(customRows.map((row) => row.src).filter(Boolean))
const assetLibraryItems = assetRows
.filter((row) => row?.src && !templateSrcSet.has(row.src) && !customSrcSet.has(row.src))
@@ -1619,7 +1619,7 @@ async function listCustomItems({ queryText = '', page = 1, limit = 50, filterMod
isAssetLibraryItem: true,
}))
const templateItems = gameItemRows.map((row) => ({
const templateItems = topicItemRows.map((row) => ({
id: row.id,
ownerId: '',
src: row.src,
@@ -1995,12 +1995,12 @@ async function listAdminTierLists({ queryText = '', topicId = '', page = 1, limi
const normalizedPage = Math.max(Number(page) || 1, 1)
const hasQuery = !!(queryText || '').trim()
const resolvedTopicId = (topicId || '').trim()
const hasGameId = !!resolvedTopicId
const hasTopicId = !!resolvedTopicId
const search = `%${(queryText || '').trim()}%`
const whereParts = []
const params = []
if (hasGameId) {
if (hasTopicId) {
whereParts.push('t.topic_id = ?')
params.push(resolvedTopicId)
}
@@ -2080,12 +2080,12 @@ async function listAdminTierLists({ queryText = '', topicId = '', page = 1, limi
async function summarizeAdminTierLists({ queryText = '', topicId = '' } = {}) {
const hasQuery = !!(queryText || '').trim()
const resolvedTopicId = (topicId || '').trim()
const hasGameId = !!resolvedTopicId
const hasTopicId = !!resolvedTopicId
const search = `%${(queryText || '').trim()}%`
const whereParts = []
const params = []
if (hasGameId) {
if (hasTopicId) {
whereParts.push('t.topic_id = ?')
params.push(resolvedTopicId)
}

View File

@@ -128,7 +128,7 @@ router.post('/templates', requireAdmin, async (req, res) => {
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 })
if (parsed.data.thumbnailSrc) {
const copiedThumb = await copyUploadIntoGameAsset(parsed.data.thumbnailSrc)
const copiedThumb = await copyUploadIntoTopicAsset(parsed.data.thumbnailSrc)
await updateTopicThumbnail(template.id, copiedThumb)
}
const savedTemplate = await findTopicById(template.id)
@@ -469,7 +469,7 @@ async function promoteLibraryItemToTemplateItem({ item, templateId }) {
})
}
async function copyUploadIntoGameAsset(src) {
async function copyUploadIntoTopicAsset(src) {
if (typeof src !== 'string') return ''
const raw = src.trim()
if (!raw) return ''
@@ -507,7 +507,7 @@ async function promoteTierListItemsToTemplate({ tierList, templateId, itemIds =
const createdItems = []
for (const item of itemsToCopy) {
const copiedSrc = await copyUploadIntoGameAsset(item.src)
const copiedSrc = await copyUploadIntoTopicAsset(item.src)
createdItems.push(
await createTopicItem({
id: nanoid(),
@@ -531,7 +531,7 @@ async function promoteSnapshotItemsToTemplate({ items, templateId }) {
const createdItems = []
for (const item of items || []) {
const copiedSrc = await copyUploadIntoGameAsset(item.src)
const copiedSrc = await copyUploadIntoTopicAsset(item.src)
if (!copiedSrc || existingSrcs.has(copiedSrc)) continue
createdItems.push(
await createTopicItem({
@@ -576,13 +576,13 @@ function pickTemplateRequestItems(templateRequest, itemIds = [], itemLabels = {}
async function createTemplateFromTierList({ tierList, templateId, templateName }) {
await createTopic({ id: templateId, name: templateName, isPublic: false })
if (tierList.thumbnailSrc) {
const copiedThumb = await copyUploadIntoGameAsset(tierList.thumbnailSrc)
const copiedThumb = await copyUploadIntoTopicAsset(tierList.thumbnailSrc)
await updateTopicThumbnail(templateId, copiedThumb)
}
const createdItems = []
for (const item of uniqueTierListPoolItems(tierList)) {
const copiedSrc = await copyUploadIntoGameAsset(item.src)
const copiedSrc = await copyUploadIntoTopicAsset(item.src)
createdItems.push(
await createTopicItem({
id: nanoid(),
@@ -600,7 +600,7 @@ async function createTemplateFromRequest({ templateRequest, templateId, template
await createTopic({ id: templateId, name: templateName, isPublic: false })
if (templateRequest.thumbnailSrc) {
const copiedThumb = await copyUploadIntoGameAsset(templateRequest.thumbnailSrc)
const copiedThumb = await copyUploadIntoTopicAsset(templateRequest.thumbnailSrc)
await updateTopicThumbnail(templateId, copiedThumb)
}