관리자 인기 지표와 회원 핵심 지표를 보강한다
This commit is contained in:
@@ -64,6 +64,8 @@ function mapUserRow(row) {
|
||||
avatarSrc: row.avatar_src || '',
|
||||
createdAt: Number(row.created_at),
|
||||
tierListCount: Number(row.tierlist_count || 0),
|
||||
followerCount: Number(row.follower_count || 0),
|
||||
receivedFavoriteCount: Number(row.received_favorite_count || 0),
|
||||
recentActivityAt: Number(row.recent_activity_at || row.created_at || 0),
|
||||
}
|
||||
}
|
||||
@@ -839,6 +841,14 @@ async function listUsers({ queryText = '', sort = 'recent', direction = 'desc' }
|
||||
? isAsc
|
||||
? 'tierlist_count ASC, recent_activity_at ASC, u.email ASC'
|
||||
: 'tierlist_count DESC, recent_activity_at DESC, u.email ASC'
|
||||
: sort === 'followers'
|
||||
? isAsc
|
||||
? 'follower_count ASC, recent_activity_at ASC, u.email ASC'
|
||||
: 'follower_count DESC, recent_activity_at DESC, u.email ASC'
|
||||
: sort === 'favorites'
|
||||
? isAsc
|
||||
? 'received_favorite_count ASC, recent_activity_at ASC, u.email ASC'
|
||||
: 'received_favorite_count DESC, recent_activity_at DESC, u.email ASC'
|
||||
: isAsc
|
||||
? 'recent_activity_at ASC, u.created_at ASC, u.email ASC'
|
||||
: 'recent_activity_at DESC, u.created_at ASC, u.email ASC'
|
||||
@@ -853,13 +863,17 @@ async function listUsers({ queryText = '', sort = 'recent', direction = 'desc' }
|
||||
u.is_admin,
|
||||
u.avatar_src,
|
||||
u.created_at,
|
||||
COUNT(t.id) AS tierlist_count,
|
||||
COUNT(DISTINCT t.id) AS tierlist_count,
|
||||
COUNT(DISTINCT uf.follower_id) AS follower_count,
|
||||
COUNT(DISTINCT ft.user_id, ft.tierlist_id) AS received_favorite_count,
|
||||
GREATEST(
|
||||
u.created_at,
|
||||
COALESCE(MAX(t.updated_at), 0)
|
||||
) AS recent_activity_at
|
||||
FROM users u
|
||||
LEFT JOIN tierlists t ON t.author_id = u.id
|
||||
LEFT JOIN user_follows uf ON uf.following_id = u.id
|
||||
LEFT JOIN favorite_tierlists ft ON ft.tierlist_id = t.id
|
||||
${where.length ? `WHERE ${where.join(' AND ')}` : ''}
|
||||
GROUP BY u.id, u.email, u.nickname, u.email_verified, u.is_admin, u.avatar_src, u.created_at
|
||||
ORDER BY ${orderBy}
|
||||
@@ -2414,9 +2428,18 @@ function getAutoThumbnailSrc(groups = [], pool = []) {
|
||||
return fallbackItem?.src || ''
|
||||
}
|
||||
|
||||
async function listAdminTierLists({ queryText = '', topicId = '', page = 1, limit = 50, currentUserId = '' } = {}) {
|
||||
async function listAdminTierLists({
|
||||
queryText = '',
|
||||
topicId = '',
|
||||
page = 1,
|
||||
limit = 50,
|
||||
sort = 'recent',
|
||||
minFavorites = 0,
|
||||
currentUserId = '',
|
||||
} = {}) {
|
||||
const normalizedLimit = Math.min(Math.max(Number(limit) || 50, 1), 200)
|
||||
const normalizedPage = Math.max(Number(page) || 1, 1)
|
||||
const normalizedMinFavorites = Math.max(Number(minFavorites) || 0, 0)
|
||||
const hasQuery = !!(queryText || '').trim()
|
||||
const resolvedTopicId = (topicId || '').trim()
|
||||
const hasTopicId = !!resolvedTopicId
|
||||
@@ -2477,7 +2500,7 @@ async function listAdminTierLists({ queryText = '', topicId = '', page = 1, limi
|
||||
params
|
||||
)
|
||||
|
||||
const allItems = rows.map((row) => {
|
||||
const baseItems = rows.map((row) => {
|
||||
const tierList = mapTierListRow(row)
|
||||
const poolItems = uniqueTierListItems(tierList.pool)
|
||||
const extraItems = poolItems.filter((item) => item.origin === 'custom')
|
||||
@@ -2488,23 +2511,47 @@ async function listAdminTierLists({ queryText = '', topicId = '', page = 1, limi
|
||||
extraItems,
|
||||
}
|
||||
})
|
||||
|
||||
const total = allItems.length
|
||||
const offset = (normalizedPage - 1) * normalizedLimit
|
||||
const pagedTierLists = allItems.slice(offset, offset + normalizedLimit)
|
||||
const favoriteStats = await getFavoriteStatsForTierListIds(
|
||||
pagedTierLists.map((tierList) => tierList.id),
|
||||
baseItems.map((tierList) => tierList.id),
|
||||
currentUserId
|
||||
)
|
||||
const filteredItems = applyFavoriteMetaToTierLists(baseItems, favoriteStats)
|
||||
.filter((tierList) => Number(tierList.favoriteCount || 0) >= normalizedMinFavorites)
|
||||
.sort((a, b) => {
|
||||
if (sort === 'favorites') {
|
||||
return (
|
||||
Number(b.favoriteCount || 0) - Number(a.favoriteCount || 0) ||
|
||||
Number(b.updatedAt || 0) - Number(a.updatedAt || 0) ||
|
||||
Number(b.createdAt || 0) - Number(a.createdAt || 0)
|
||||
)
|
||||
}
|
||||
if (sort === 'created') {
|
||||
return (
|
||||
Number(b.createdAt || 0) - Number(a.createdAt || 0) ||
|
||||
Number(b.updatedAt || 0) - Number(a.updatedAt || 0) ||
|
||||
String(a.title || '').localeCompare(String(b.title || ''))
|
||||
)
|
||||
}
|
||||
return (
|
||||
Number(b.updatedAt || 0) - Number(a.updatedAt || 0) ||
|
||||
Number(b.createdAt || 0) - Number(a.createdAt || 0) ||
|
||||
String(a.title || '').localeCompare(String(b.title || ''))
|
||||
)
|
||||
})
|
||||
|
||||
const total = filteredItems.length
|
||||
const offset = (normalizedPage - 1) * normalizedLimit
|
||||
const pagedTierLists = filteredItems.slice(offset, offset + normalizedLimit)
|
||||
return {
|
||||
tierLists: applyFavoriteMetaToTierLists(pagedTierLists, favoriteStats),
|
||||
tierLists: pagedTierLists,
|
||||
total,
|
||||
page: normalizedPage,
|
||||
limit: normalizedLimit,
|
||||
}
|
||||
}
|
||||
|
||||
async function summarizeAdminTierLists({ queryText = '', topicId = '' } = {}) {
|
||||
async function summarizeAdminTierLists({ queryText = '', topicId = '', minFavorites = 0 } = {}) {
|
||||
const normalizedMinFavorites = Math.max(Number(minFavorites) || 0, 0)
|
||||
const hasQuery = !!(queryText || '').trim()
|
||||
const resolvedTopicId = (topicId || '').trim()
|
||||
const hasTopicId = !!resolvedTopicId
|
||||
@@ -2532,6 +2579,7 @@ async function summarizeAdminTierLists({ queryText = '', topicId = '' } = {}) {
|
||||
const rows = await query(
|
||||
`
|
||||
SELECT t.is_public, t.is_featured
|
||||
, t.id
|
||||
FROM tierlists t
|
||||
INNER JOIN users u ON u.id = t.author_id
|
||||
INNER JOIN topics tp ON tp.id = t.topic_id
|
||||
@@ -2540,9 +2588,16 @@ async function summarizeAdminTierLists({ queryText = '', topicId = '' } = {}) {
|
||||
params
|
||||
)
|
||||
|
||||
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
|
||||
const favoriteStats = normalizedMinFavorites > 0
|
||||
? await getFavoriteStatsForTierListIds(rows.map((row) => row.id), '')
|
||||
: { countMap: new Map(), favoritedSet: new Set() }
|
||||
const scopedRows = normalizedMinFavorites > 0
|
||||
? rows.filter((row) => Number(favoriteStats.countMap.get(row.id) || 0) >= normalizedMinFavorites)
|
||||
: rows
|
||||
|
||||
const total = scopedRows.length
|
||||
const publicCount = scopedRows.filter((row) => Number(row.is_public) === 1).length
|
||||
const featuredCount = scopedRows.filter((row) => Number(row.is_featured) === 1).length
|
||||
return {
|
||||
total,
|
||||
publicCount,
|
||||
|
||||
@@ -350,6 +350,8 @@ router.get('/tierlists', requireAdmin, async (req, res) => {
|
||||
const schema = z.object({
|
||||
q: z.string().trim().max(120).optional().default(''),
|
||||
topicId: z.string().trim().max(120).optional().default(''),
|
||||
sort: z.enum(['recent', 'created', 'favorites']).optional().default('recent'),
|
||||
minFavorites: z.coerce.number().int().min(0).max(1000000).optional().default(0),
|
||||
page: z.coerce.number().int().min(1).optional().default(1),
|
||||
limit: z.coerce.number().int().min(1).max(200).optional().default(50),
|
||||
})
|
||||
@@ -359,6 +361,8 @@ router.get('/tierlists', requireAdmin, async (req, res) => {
|
||||
const result = await listAdminTierLists({
|
||||
queryText: parsed.data.q,
|
||||
topicId: parsed.data.topicId,
|
||||
sort: parsed.data.sort,
|
||||
minFavorites: parsed.data.minFavorites,
|
||||
page: parsed.data.page,
|
||||
limit: parsed.data.limit,
|
||||
currentUserId: req.session?.userId || '',
|
||||
@@ -370,6 +374,7 @@ router.get('/tierlists/stats', requireAdmin, async (req, res) => {
|
||||
const schema = z.object({
|
||||
q: z.string().trim().max(120).optional().default(''),
|
||||
topicId: z.string().trim().max(120).optional().default(''),
|
||||
minFavorites: z.coerce.number().int().min(0).max(1000000).optional().default(0),
|
||||
})
|
||||
const parsed = schema.safeParse(req.query)
|
||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||
@@ -377,6 +382,7 @@ router.get('/tierlists/stats', requireAdmin, async (req, res) => {
|
||||
const result = await summarizeAdminTierLists({
|
||||
queryText: parsed.data.q,
|
||||
topicId: parsed.data.topicId,
|
||||
minFavorites: parsed.data.minFavorites,
|
||||
})
|
||||
res.json(result)
|
||||
})
|
||||
@@ -970,7 +976,7 @@ router.delete('/custom-items', requireAdmin, async (req, res) => {
|
||||
router.get('/users', requireAdmin, async (req, res) => {
|
||||
const schema = z.object({
|
||||
q: z.string().trim().max(120).optional().default(''),
|
||||
sort: z.enum(['recent', 'created', 'tierlists']).optional().default('recent'),
|
||||
sort: z.enum(['recent', 'created', 'tierlists', 'followers', 'favorites']).optional().default('recent'),
|
||||
direction: z.enum(['asc', 'desc']).optional().default('desc'),
|
||||
})
|
||||
const parsed = schema.safeParse(req.query)
|
||||
|
||||
Reference in New Issue
Block a user