릴리스: v0.1.47 템플릿 요청과 관리자 승인 흐름 추가

This commit is contained in:
2026-03-27 11:10:45 +09:00
parent e0eeaa01cd
commit 3b314381a0
11 changed files with 725 additions and 15 deletions

View File

@@ -85,6 +85,30 @@ function mapTierListRow(row) {
}
}
function mapTemplateRequestRow(row) {
if (!row) return null
return {
id: row.id,
type: row.request_type,
requesterId: row.requester_id,
requesterName: getUserDisplayName(row),
requesterAccountName: getUserAccountName(row),
requesterAvatarSrc: row.requester_avatar_src || '',
sourceTierListId: row.source_tierlist_id,
sourceGameId: row.source_game_id,
sourceGameName: row.source_game_name || '',
sourceTierListTitle: row.title_snapshot || '',
sourceDescription: row.description_snapshot || '',
thumbnailSrc: row.thumbnail_src_snapshot || '',
targetGameId: row.target_game_id || '',
targetGameName: row.target_game_name || '',
status: row.status,
items: parseJson(row.items_json, []),
createdAt: Number(row.created_at),
updatedAt: Number(row.updated_at),
}
}
function getUserDisplayName(row) {
if (!row) return ''
const nickname = (row.nickname || '').trim()
@@ -226,6 +250,29 @@ async function ensureSchema() {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
`)
await query(`
CREATE TABLE IF NOT EXISTS template_requests (
id VARCHAR(64) PRIMARY KEY,
request_type VARCHAR(20) NOT NULL,
requester_id VARCHAR(64) NOT NULL,
source_tierlist_id VARCHAR(64) NOT NULL,
source_game_id VARCHAR(120) NOT NULL,
target_game_id VARCHAR(120) NOT NULL DEFAULT '',
status VARCHAR(20) NOT NULL DEFAULT 'pending',
title_snapshot VARCHAR(120) NOT NULL,
description_snapshot TEXT NOT NULL,
thumbnail_src_snapshot VARCHAR(255) NOT NULL DEFAULT '',
items_json LONGTEXT NOT NULL,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
INDEX idx_template_requests_status_created (status, created_at),
INDEX idx_template_requests_source_tierlist (source_tierlist_id),
INDEX idx_template_requests_requester (requester_id),
CONSTRAINT fk_template_requests_requester FOREIGN KEY (requester_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_template_requests_source_tierlist FOREIGN KEY (source_tierlist_id) REFERENCES tierlists(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
`)
const tierListThumbnailColumns = await query("SHOW COLUMNS FROM tierlists LIKE 'thumbnail_src'")
if (!tierListThumbnailColumns.length) {
await query("ALTER TABLE tierlists ADD COLUMN thumbnail_src VARCHAR(255) NOT NULL DEFAULT '' AFTER title")
@@ -502,6 +549,24 @@ async function createCustomItem({ id, ownerId, src, label }) {
return { id, ownerId, src, label, origin: 'custom', createdAt }
}
async function syncOwnedCustomItemLabels({ ownerId, items }) {
const customItems = Array.from(
new Map(
(items || [])
.filter((item) => item?.origin === 'custom' && item?.id && typeof item.label === 'string')
.map((item) => [item.id, item])
).values()
)
if (!customItems.length) return
await Promise.all(
customItems.map((item) =>
query('UPDATE custom_items SET label = ? WHERE id = ? AND owner_id = ?', [item.label.trim().slice(0, 60), item.id, ownerId])
)
)
}
async function findCustomItemById(id) {
const rows = await query(
`
@@ -959,6 +1024,151 @@ async function findTierListById(id, currentUserId = '') {
return applyFavoriteMetaToTierLists([tierList], favoriteStats)[0]
}
async function findPendingTemplateRequestByTierList({ sourceTierListId, type }) {
const rows = await query(
`
SELECT id, request_type, status
FROM template_requests
WHERE source_tierlist_id = ? AND request_type = ? AND status = 'pending'
LIMIT 1
`,
[sourceTierListId, type]
)
return rows[0] || null
}
async function createTemplateRequest({
id,
type,
requesterId,
sourceTierListId,
sourceGameId,
targetGameId = '',
title,
description = '',
thumbnailSrc = '',
items = [],
}) {
const existing = await findPendingTemplateRequestByTierList({ sourceTierListId, type })
if (existing) {
const err = new Error('template_request_exists')
err.code = 'TEMPLATE_REQUEST_EXISTS'
throw err
}
const createdAt = now()
await query(
`
INSERT INTO template_requests (
id,
request_type,
requester_id,
source_tierlist_id,
source_game_id,
target_game_id,
status,
title_snapshot,
description_snapshot,
thumbnail_src_snapshot,
items_json,
created_at,
updated_at
)
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?, ?)
`,
[
id,
type,
requesterId,
sourceTierListId,
sourceGameId,
targetGameId,
title,
description,
thumbnailSrc,
serializeJson(items),
createdAt,
createdAt,
]
)
return findTemplateRequestById(id)
}
async function findTemplateRequestById(id) {
const rows = await query(
`
SELECT
tr.id,
tr.request_type,
tr.requester_id,
tr.source_tierlist_id,
tr.source_game_id,
tr.target_game_id,
tr.status,
tr.title_snapshot,
tr.description_snapshot,
tr.thumbnail_src_snapshot,
tr.items_json,
tr.created_at,
tr.updated_at,
u.nickname,
u.email,
u.avatar_src AS requester_avatar_src,
sg.name AS source_game_name,
tg.name AS target_game_name
FROM template_requests tr
INNER JOIN users u ON u.id = tr.requester_id
LEFT JOIN games sg ON sg.id = tr.source_game_id
LEFT JOIN games tg ON tg.id = tr.target_game_id
WHERE tr.id = ?
LIMIT 1
`,
[id]
)
return mapTemplateRequestRow(rows[0])
}
async function listAdminTemplateRequests({ status = 'pending' } = {}) {
const rows = await query(
`
SELECT
tr.id,
tr.request_type,
tr.requester_id,
tr.source_tierlist_id,
tr.source_game_id,
tr.target_game_id,
tr.status,
tr.title_snapshot,
tr.description_snapshot,
tr.thumbnail_src_snapshot,
tr.items_json,
tr.created_at,
tr.updated_at,
u.nickname,
u.email,
u.avatar_src AS requester_avatar_src,
sg.name AS source_game_name,
tg.name AS target_game_name
FROM template_requests tr
INNER JOIN users u ON u.id = tr.requester_id
LEFT JOIN games sg ON sg.id = tr.source_game_id
LEFT JOIN games tg ON tg.id = tr.target_game_id
WHERE tr.status = ?
ORDER BY tr.created_at DESC
`,
[status]
)
return rows.map(mapTemplateRequestRow)
}
async function updateTemplateRequestStatus({ id, status }) {
await query('UPDATE template_requests SET status = ?, updated_at = ? WHERE id = ?', [status, now(), id])
return findTemplateRequestById(id)
}
async function deleteTierList(id) {
await query('DELETE FROM tierlists WHERE id = ?', [id])
}
@@ -992,6 +1202,7 @@ async function deleteCustomItems(ids) {
async function saveTierList({ id, authorId, gameId, title, thumbnailSrc = '', description, isPublic, groups, pool }) {
const existing = id ? await findTierListById(id, authorId) : null
await syncOwnedCustomItemLabels({ ownerId: authorId, items: pool })
if (existing) {
await query(
@@ -1064,4 +1275,8 @@ module.exports = {
findCustomItemsByIds,
deleteCustomItems,
saveTierList,
createTemplateRequest,
findTemplateRequestById,
listAdminTemplateRequests,
updateTemplateRequestStatus,
}