Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9aa714501 |
@@ -472,6 +472,28 @@ async function createCustomItem({ id, ownerId, src, label }) {
|
|||||||
return { id, ownerId, src, label, origin: 'custom', createdAt }
|
return { id, ownerId, src, label, origin: 'custom', createdAt }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function findCustomItemById(id) {
|
||||||
|
const rows = await query(
|
||||||
|
`
|
||||||
|
SELECT id, owner_id, src, label, created_at
|
||||||
|
FROM custom_items
|
||||||
|
WHERE id = ?
|
||||||
|
LIMIT 1
|
||||||
|
`,
|
||||||
|
[id]
|
||||||
|
)
|
||||||
|
|
||||||
|
const row = rows[0]
|
||||||
|
if (!row) return null
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
ownerId: row.owner_id,
|
||||||
|
src: row.src,
|
||||||
|
label: row.label,
|
||||||
|
createdAt: Number(row.created_at),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function getCustomItemUsageMap() {
|
async function getCustomItemUsageMap() {
|
||||||
const rows = await query('SELECT groups_json, pool_json FROM tierlists')
|
const rows = await query('SELECT groups_json, pool_json FROM tierlists')
|
||||||
const usageMap = new Map()
|
const usageMap = new Map()
|
||||||
@@ -778,6 +800,7 @@ module.exports = {
|
|||||||
deleteGame,
|
deleteGame,
|
||||||
updateGameDisplayOrder,
|
updateGameDisplayOrder,
|
||||||
createCustomItem,
|
createCustomItem,
|
||||||
|
findCustomItemById,
|
||||||
listCustomItems,
|
listCustomItems,
|
||||||
findUnusedCustomItems,
|
findUnusedCustomItems,
|
||||||
listPublicTierLists,
|
listPublicTierLists,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const {
|
|||||||
deleteGame,
|
deleteGame,
|
||||||
updateGameDisplayOrder,
|
updateGameDisplayOrder,
|
||||||
listCustomItems,
|
listCustomItems,
|
||||||
|
findCustomItemById,
|
||||||
findUnusedCustomItems,
|
findUnusedCustomItems,
|
||||||
findCustomItemsByIds,
|
findCustomItemsByIds,
|
||||||
deleteCustomItems,
|
deleteCustomItems,
|
||||||
@@ -174,6 +175,23 @@ async function removeCustomItemFiles(items) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function promoteCustomItemToGameItem({ customItem, gameId }) {
|
||||||
|
const originalName = path.basename(customItem.src || '')
|
||||||
|
const nextFilename = buildUploadFilename({ originalname: originalName })
|
||||||
|
const sourcePath = path.join(__dirname, '..', '..', customItem.src.replace(/^\//, ''))
|
||||||
|
const targetRelativePath = path.join('uploads', 'games', nextFilename)
|
||||||
|
const targetPath = path.join(__dirname, '..', '..', targetRelativePath)
|
||||||
|
|
||||||
|
await fs.copyFile(sourcePath, targetPath)
|
||||||
|
|
||||||
|
return createGameItem({
|
||||||
|
id: nanoid(),
|
||||||
|
gameId,
|
||||||
|
src: `/${targetRelativePath.replace(/\\/g, '/')}`,
|
||||||
|
label: customItem.label,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
router.delete('/custom-items/:itemId', requireAdmin, async (req, res) => {
|
router.delete('/custom-items/:itemId', requireAdmin, async (req, res) => {
|
||||||
const result = await listCustomItems({ page: 1, limit: 200, orphanOnly: false })
|
const result = await listCustomItems({ page: 1, limit: 200, orphanOnly: false })
|
||||||
const target = result.items.find((item) => item.id === req.params.itemId)
|
const target = result.items.find((item) => item.id === req.params.itemId)
|
||||||
@@ -186,6 +204,23 @@ router.delete('/custom-items/:itemId', requireAdmin, async (req, res) => {
|
|||||||
res.json({ ok: true })
|
res.json({ ok: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.post('/custom-items/:itemId/promote', requireAdmin, async (req, res) => {
|
||||||
|
const schema = z.object({
|
||||||
|
gameId: z.string().min(1),
|
||||||
|
})
|
||||||
|
const parsed = schema.safeParse(req.body)
|
||||||
|
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||||
|
|
||||||
|
const game = await findGameById(parsed.data.gameId)
|
||||||
|
if (!game) return res.status(404).json({ error: 'game_not_found' })
|
||||||
|
|
||||||
|
const customItem = await findCustomItemById(req.params.itemId)
|
||||||
|
if (!customItem) return res.status(404).json({ error: 'not_found' })
|
||||||
|
|
||||||
|
const item = await promoteCustomItemToGameItem({ customItem, gameId: game.id })
|
||||||
|
res.json({ item })
|
||||||
|
})
|
||||||
|
|
||||||
router.delete('/custom-items', requireAdmin, async (req, res) => {
|
router.delete('/custom-items', requireAdmin, async (req, res) => {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
q: z.string().trim().max(120).optional().default(''),
|
q: z.string().trim().max(120).optional().default(''),
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# 의사결정 이력
|
# 의사결정 이력
|
||||||
|
|
||||||
|
## 2026-03-26 v0.1.41
|
||||||
|
- 관리자 커스텀 아이템 승격은 버튼만 보이는 상태로 끝나면 안 되므로, 프런트 API와 백엔드 라우트가 실제로 함께 연결되어야 기능이 완결된다고 정리했다.
|
||||||
|
|
||||||
## 2026-03-26 v0.1.40
|
## 2026-03-26 v0.1.40
|
||||||
- 관리자 기본 아이템 이름 저장은 눌러도 변화가 없으면 혼란스러우므로, 실제 변경이 있을 때만 버튼이 활성화되는 편이 더 명확하다고 판단했다.
|
- 관리자 기본 아이템 이름 저장은 눌러도 변화가 없으면 혼란스러우므로, 실제 변경이 있을 때만 버튼이 활성화되는 편이 더 명확하다고 판단했다.
|
||||||
- 사용자 커스텀 이미지는 관리자 검토 후 특정 게임의 기본 템플릿으로 복제해 가져올 수 있어야 운영 효율이 높아지므로, 게임 선택 기반 승격 흐름을 추가하기로 결정했다.
|
- 사용자 커스텀 이미지는 관리자 검토 후 특정 게임의 기본 템플릿으로 복제해 가져올 수 있어야 운영 효율이 높아지므로, 게임 선택 기반 승격 흐름을 추가하기로 결정했다.
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# 업데이트 로그
|
# 업데이트 로그
|
||||||
|
|
||||||
|
## 2026-03-26 v0.1.41
|
||||||
|
- **커스텀 아이템 승격 연결 수정**: 관리자 아이템 관리의 `기본 템플릿에 추가` 버튼이 실제 API와 백엔드 승격 라우트로 연결되도록 누락된 프런트/백엔드 구현을 보완
|
||||||
|
|
||||||
## 2026-03-26 v0.1.40
|
## 2026-03-26 v0.1.40
|
||||||
- **기본 아이템 저장 UX 보강**: 관리자 게임 관리에서 아이템 이름이 실제로 바뀐 경우에만 `이름 저장` 버튼이 활성화되도록 조정하고, 저장 중 상태를 버튼에 표시
|
- **기본 아이템 저장 UX 보강**: 관리자 게임 관리에서 아이템 이름이 실제로 바뀐 경우에만 `이름 저장` 버튼이 활성화되도록 조정하고, 저장 중 상태를 버튼에 표시
|
||||||
- **커스텀 아이템 승격 추가**: 관리자 아이템 관리에서 사용자 커스텀 이미지를 선택한 게임의 기본 템플릿으로 복제해 가져올 수 있도록 API와 UI를 추가
|
- **커스텀 아이템 승격 추가**: 관리자 아이템 관리에서 사용자 커스텀 이미지를 선택한 게임의 기본 템플릿으로 복제해 가져올 수 있도록 API와 UI를 추가
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ export const api = {
|
|||||||
request(
|
request(
|
||||||
`/api/admin/custom-items?q=${encodeURIComponent(q)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}&orphanOnly=${encodeURIComponent(orphanOnly)}`
|
`/api/admin/custom-items?q=${encodeURIComponent(q)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}&orphanOnly=${encodeURIComponent(orphanOnly)}`
|
||||||
),
|
),
|
||||||
|
promoteAdminCustomItem: (itemId, payload) =>
|
||||||
|
request(`/api/admin/custom-items/${encodeURIComponent(itemId)}/promote`, { method: 'POST', body: payload }),
|
||||||
listAdminUsers: () => request('/api/admin/users'),
|
listAdminUsers: () => request('/api/admin/users'),
|
||||||
updateAdminUser: (userId, payload) =>
|
updateAdminUser: (userId, payload) =>
|
||||||
request(`/api/admin/users/${encodeURIComponent(userId)}`, { method: 'PATCH', body: payload }),
|
request(`/api/admin/users/${encodeURIComponent(userId)}`, { method: 'PATCH', body: payload }),
|
||||||
|
|||||||
Reference in New Issue
Block a user