추천 티어표 분리와 관리자 추천 지정 기능 추가
This commit is contained in:
@@ -140,6 +140,9 @@ function mapTierListRow(row) {
|
||||
thumbnailSrc: row.thumbnail_src || '',
|
||||
description: row.description || '',
|
||||
isPublic: !!row.is_public,
|
||||
isFeatured: !!row.is_featured,
|
||||
featuredAt: Number(row.featured_at || 0),
|
||||
featuredBy: row.featured_by || '',
|
||||
showCharacterNames: !!row.show_character_names,
|
||||
iconSize: Number(row.icon_size || 80),
|
||||
sourceTierListId: row.source_tierlist_id || '',
|
||||
@@ -378,6 +381,9 @@ async function ensureSchema() {
|
||||
thumbnail_src VARCHAR(255) NOT NULL DEFAULT '',
|
||||
description TEXT NOT NULL,
|
||||
is_public TINYINT(1) NOT NULL DEFAULT 0,
|
||||
is_featured TINYINT(1) NOT NULL DEFAULT 0,
|
||||
featured_at BIGINT NOT NULL DEFAULT 0,
|
||||
featured_by VARCHAR(64) NOT NULL DEFAULT '',
|
||||
show_character_names TINYINT(1) NOT NULL DEFAULT 0,
|
||||
icon_size INT NOT NULL DEFAULT 80,
|
||||
source_tierlist_id VARCHAR(64) NULL DEFAULT NULL,
|
||||
@@ -390,6 +396,7 @@ async function ensureSchema() {
|
||||
INDEX idx_tierlists_author_id (author_id),
|
||||
INDEX idx_tierlists_topic_id (topic_id),
|
||||
INDEX idx_tierlists_public_topic_updated (is_public, topic_id, updated_at),
|
||||
INDEX idx_tierlists_featured_topic (is_public, is_featured, topic_id, featured_at),
|
||||
CONSTRAINT fk_tierlists_author FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_tierlists_topic FOREIGN KEY (topic_id) REFERENCES topics(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
@@ -525,6 +532,18 @@ async function ensureSchema() {
|
||||
if (!tierListShowNamesColumns.length) {
|
||||
await query("ALTER TABLE tierlists ADD COLUMN show_character_names TINYINT(1) NOT NULL DEFAULT 0 AFTER is_public")
|
||||
}
|
||||
const tierListFeaturedColumns = await query("SHOW COLUMNS FROM tierlists LIKE 'is_featured'")
|
||||
if (!tierListFeaturedColumns.length) {
|
||||
await query("ALTER TABLE tierlists ADD COLUMN is_featured TINYINT(1) NOT NULL DEFAULT 0 AFTER is_public")
|
||||
}
|
||||
const tierListFeaturedAtColumns = await query("SHOW COLUMNS FROM tierlists LIKE 'featured_at'")
|
||||
if (!tierListFeaturedAtColumns.length) {
|
||||
await query("ALTER TABLE tierlists ADD COLUMN featured_at BIGINT NOT NULL DEFAULT 0 AFTER is_featured")
|
||||
}
|
||||
const tierListFeaturedByColumns = await query("SHOW COLUMNS FROM tierlists LIKE 'featured_by'")
|
||||
if (!tierListFeaturedByColumns.length) {
|
||||
await query("ALTER TABLE tierlists ADD COLUMN featured_by VARCHAR(64) NOT NULL DEFAULT '' AFTER featured_at")
|
||||
}
|
||||
const tierListIconSizeColumns = await query("SHOW COLUMNS FROM tierlists LIKE 'icon_size'")
|
||||
if (!tierListIconSizeColumns.length) {
|
||||
await query("ALTER TABLE tierlists ADD COLUMN icon_size INT NOT NULL DEFAULT 80 AFTER show_character_names")
|
||||
@@ -1997,6 +2016,9 @@ async function listPublicTierLists(topicId, currentUserId = '', queryText = '')
|
||||
t.topic_id,
|
||||
t.title,
|
||||
t.thumbnail_src,
|
||||
t.is_featured,
|
||||
t.featured_at,
|
||||
t.featured_by,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
t.author_id,
|
||||
@@ -2006,8 +2028,8 @@ async function listPublicTierLists(topicId, currentUserId = '', queryText = '')
|
||||
FROM tierlists t
|
||||
INNER JOIN users u ON u.id = t.author_id
|
||||
${whereClause}
|
||||
ORDER BY t.updated_at DESC
|
||||
LIMIT 50
|
||||
ORDER BY t.is_featured DESC, t.featured_at DESC, t.updated_at DESC
|
||||
LIMIT 200
|
||||
`,
|
||||
params
|
||||
)
|
||||
@@ -2017,6 +2039,8 @@ async function listPublicTierLists(topicId, currentUserId = '', queryText = '')
|
||||
topicId: row.topic_id,
|
||||
title: row.title,
|
||||
thumbnailSrc: row.thumbnail_src || '',
|
||||
isFeatured: !!row.is_featured,
|
||||
featuredAt: Number(row.featured_at || 0),
|
||||
createdAt: Number(row.created_at),
|
||||
updatedAt: Number(row.updated_at),
|
||||
authorId: row.author_id,
|
||||
@@ -2029,7 +2053,22 @@ async function listPublicTierLists(topicId, currentUserId = '', queryText = '')
|
||||
tierLists.map((tierList) => tierList.id),
|
||||
currentUserId
|
||||
)
|
||||
return applyFavoriteMetaToTierLists(tierLists, favoriteStats)
|
||||
const mergedTierLists = applyFavoriteMetaToTierLists(tierLists, favoriteStats)
|
||||
const featuredTierLists = mergedTierLists
|
||||
.filter((tierList) => tierList.isFeatured)
|
||||
.slice()
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Number(b.featuredAt || 0) - Number(a.featuredAt || 0) ||
|
||||
Number(b.favoriteCount || 0) - Number(a.favoriteCount || 0) ||
|
||||
Number(b.updatedAt || 0) - Number(a.updatedAt || 0)
|
||||
)
|
||||
.slice(0, 16)
|
||||
|
||||
return {
|
||||
featuredTierLists,
|
||||
tierLists: mergedTierLists.filter((tierList) => !tierList.isFeatured),
|
||||
}
|
||||
}
|
||||
|
||||
async function listFavoriteTierLists(userId, { queryText = '', sort = 'favorited' } = {}) {
|
||||
@@ -2062,6 +2101,9 @@ async function listFavoriteTierLists(userId, { queryText = '', sort = 'favorited
|
||||
t.thumbnail_src,
|
||||
t.description,
|
||||
t.is_public,
|
||||
t.is_featured,
|
||||
t.featured_at,
|
||||
t.featured_by,
|
||||
t.show_character_names,
|
||||
t.icon_size,
|
||||
t.source_tierlist_id,
|
||||
@@ -2207,6 +2249,9 @@ async function listAdminTierLists({ queryText = '', topicId = '', page = 1, limi
|
||||
t.thumbnail_src,
|
||||
t.description,
|
||||
t.is_public,
|
||||
t.is_featured,
|
||||
t.featured_at,
|
||||
t.featured_by,
|
||||
t.show_character_names,
|
||||
t.icon_size,
|
||||
t.source_tierlist_id,
|
||||
@@ -2282,7 +2327,7 @@ async function summarizeAdminTierLists({ queryText = '', topicId = '' } = {}) {
|
||||
const whereClause = whereParts.length ? `WHERE ${whereParts.join(' AND ')}` : ''
|
||||
const rows = await query(
|
||||
`
|
||||
SELECT t.is_public
|
||||
SELECT t.is_public, t.is_featured
|
||||
FROM tierlists t
|
||||
INNER JOIN users u ON u.id = t.author_id
|
||||
INNER JOIN topics tp ON tp.id = t.topic_id
|
||||
@@ -2293,10 +2338,12 @@ async function summarizeAdminTierLists({ queryText = '', topicId = '' } = {}) {
|
||||
|
||||
const total = rows.length
|
||||
const publicCount = rows.filter((row) => Number(row.is_public) === 1).length
|
||||
const featuredCount = rows.filter((row) => Number(row.is_featured) === 1).length
|
||||
return {
|
||||
total,
|
||||
publicCount,
|
||||
privateCount: Math.max(0, total - publicCount),
|
||||
featuredCount,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2312,6 +2359,9 @@ async function findTierListById(id, currentUserId = '') {
|
||||
t.thumbnail_src,
|
||||
t.description,
|
||||
t.is_public,
|
||||
t.is_featured,
|
||||
t.featured_at,
|
||||
t.featured_by,
|
||||
t.show_character_names,
|
||||
t.icon_size,
|
||||
t.source_tierlist_id,
|
||||
@@ -2520,13 +2570,38 @@ async function deleteTierList(id) {
|
||||
}
|
||||
|
||||
async function updateAdminTierListMeta({ id, title, description = '', isPublic }) {
|
||||
const nextUpdatedAt = now()
|
||||
if (!isPublic) {
|
||||
await query(
|
||||
`
|
||||
UPDATE tierlists
|
||||
SET title = ?, description = ?, is_public = 0, is_featured = 0, featured_at = 0, featured_by = '', updated_at = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
[title, description || '', nextUpdatedAt, id]
|
||||
)
|
||||
return findTierListById(id)
|
||||
}
|
||||
|
||||
await query(
|
||||
`
|
||||
UPDATE tierlists
|
||||
SET title = ?, description = ?, is_public = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
[title, description || '', isPublic ? 1 : 0, now(), id]
|
||||
[title, description || '', 1, nextUpdatedAt, id]
|
||||
)
|
||||
return findTierListById(id)
|
||||
}
|
||||
|
||||
async function updateTierListFeaturedStatus({ id, isFeatured, adminUserId }) {
|
||||
await query(
|
||||
`
|
||||
UPDATE tierlists
|
||||
SET is_featured = ?, featured_at = ?, featured_by = ?
|
||||
WHERE id = ?
|
||||
`,
|
||||
[isFeatured ? 1 : 0, isFeatured ? now() : 0, isFeatured ? adminUserId || '' : '', id]
|
||||
)
|
||||
return findTierListById(id)
|
||||
}
|
||||
@@ -2710,6 +2785,7 @@ module.exports = {
|
||||
summarizeAdminTierLists,
|
||||
findTierListById,
|
||||
updateAdminTierListMeta,
|
||||
updateTierListFeaturedStatus,
|
||||
favoriteTopic,
|
||||
unfavoriteTopic,
|
||||
favoriteTierList,
|
||||
|
||||
@@ -38,6 +38,7 @@ const {
|
||||
summarizeAdminTierLists,
|
||||
findTierListById,
|
||||
updateAdminTierListMeta,
|
||||
updateTierListFeaturedStatus,
|
||||
listAdminTemplateRequests,
|
||||
findTemplateRequestById,
|
||||
updateTemplateRequestStatus,
|
||||
@@ -380,6 +381,25 @@ router.get('/tierlists/stats', requireAdmin, async (req, res) => {
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
router.patch('/tierlists/:tierListId/featured', requireAdmin, async (req, res) => {
|
||||
const schema = z.object({
|
||||
isFeatured: z.boolean(),
|
||||
})
|
||||
const parsed = schema.safeParse(req.body)
|
||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||
|
||||
const tierList = await findTierListById(req.params.tierListId, req.session?.userId || '')
|
||||
if (!tierList) return res.status(404).json({ error: 'not_found' })
|
||||
if (parsed.data.isFeatured && !tierList.isPublic) return res.status(400).json({ error: 'public_tierlist_required' })
|
||||
|
||||
const updated = await updateTierListFeaturedStatus({
|
||||
id: tierList.id,
|
||||
isFeatured: parsed.data.isFeatured,
|
||||
adminUserId: req.session.userId,
|
||||
})
|
||||
res.json({ tierList: updated })
|
||||
})
|
||||
|
||||
router.get('/template-requests', requireAdmin, async (req, res) => {
|
||||
const requests = await listAdminTemplateRequests({ statuses: ['pending', 'reviewing'] })
|
||||
res.json({ requests })
|
||||
|
||||
@@ -124,8 +124,8 @@ const tierListUpsertSchema = z.object({
|
||||
router.get('/public', async (req, res) => {
|
||||
const topicId = typeof req.query.topicId === 'string' ? req.query.topicId : ''
|
||||
const queryText = typeof req.query.q === 'string' ? req.query.q : ''
|
||||
const lists = await listPublicTierLists(topicId, req.session?.userId || '', queryText)
|
||||
res.json({ tierLists: lists })
|
||||
const result = await listPublicTierLists(topicId, req.session?.userId || '', queryText)
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
router.get('/me', requireAuth, async (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user