릴리스: v1.3.36 관리자 아이템 라이브러리 보강

This commit is contained in:
2026-04-01 16:30:58 +09:00
parent 7e80320e9f
commit 5f6f01942e
7 changed files with 104 additions and 43 deletions

View File

@@ -936,6 +936,14 @@ async function listImageAssets() {
return rows.map(mapImageAssetRow)
}
async function findImageAssetById(id) {
const rows = await query(
'SELECT id, content_hash, src, mime_type, byte_size, original_byte_size, width, height, created_at FROM image_assets WHERE id = ? LIMIT 1',
[id]
)
return mapImageAssetRow(rows[0])
}
async function getReferencedUploadFootprint() {
const [referencedSrcs, assets] = await Promise.all([listReferencedUploadSources(), listImageAssets()])
const assetMap = new Map(assets.map((asset) => [asset.src, asset]))
@@ -1217,7 +1225,7 @@ async function listCustomItems({ queryText = '', page = 1, limit = 50, orphanOnl
const hasQuery = !!searchText
const search = `%${searchText}%`
const [customRows, gameItemRows, usageMeta] = await Promise.all([
const [customRows, gameItemRows, assetRows, usageMeta] = await Promise.all([
query(
`
SELECT
@@ -1251,6 +1259,16 @@ async function listCustomItems({ queryText = '', page = 1, limit = 50, orphanOnl
`,
hasQuery ? [search, search, search, search] : []
),
query(
`
SELECT ia.id, ia.src, ia.created_at
FROM image_assets ia
WHERE ia.src LIKE '/uploads/assets/%'
${hasQuery ? 'AND ia.src LIKE ?' : ''}
ORDER BY ia.created_at DESC
`,
hasQuery ? [search] : []
),
getCustomItemUsageMeta(),
])
@@ -1282,6 +1300,29 @@ async function listCustomItems({ queryText = '', page = 1, limit = 50, orphanOnl
}
})
const templateSrcSet = new Set(gameItemRows.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))
.map((row) => ({
id: `asset:${row.id}`,
assetId: row.id,
ownerId: '',
src: row.src,
label: (row.src.split('/').pop() || '').replace(/\.[^.]+$/, '') || '이름 없음',
createdAt: Number(row.created_at || 0),
ownerName: '관리자 보관 자산',
ownerEmail: '',
usageCount: 0,
linkedGames: [],
sourceType: 'template',
sourceLabel: '관리자 템플릿',
canDelete: true,
sourceGameId: '',
sourceGameName: '',
isAssetLibraryItem: true,
}))
const templateItems = gameItemRows.map((row) => ({
id: row.id,
ownerId: '',
@@ -1299,7 +1340,7 @@ async function listCustomItems({ queryText = '', page = 1, limit = 50, orphanOnl
sourceGameName: row.game_name || row.game_id,
}))
const allItems = [...customItems, ...templateItems]
const allItems = [...customItems, ...templateItems, ...assetLibraryItems]
.filter((item) => {
if (!orphanOnly) return true
return item.sourceType === 'user' && item.usageCount === 0 && item.linkedGames.length === 0
@@ -2001,6 +2042,7 @@ module.exports = {
updateGameThumbnail,
findImageAssetByHash,
findImageAssetBySrc,
findImageAssetById,
createImageAsset,
createImageOptimizationJob,
findImageOptimizationJobById,

View File

@@ -9,6 +9,7 @@ const {
findUserById,
findGameById,
findGameItemById,
findImageAssetById,
createGame,
listGames,
updateGameThumbnail,
@@ -309,6 +310,20 @@ router.post('/image-assets/stats/reset', requireAdmin, async (req, res) => {
res.json({ deletedCount })
})
async function removeUploadFiles(srcs) {
await Promise.all(
(srcs || []).map(async (src) => {
if (!src || !src.startsWith('/uploads/')) return
const absolutePath = path.join(__dirname, '..', '..', src.replace(/^\//, ''))
try {
await fs.unlink(absolutePath)
} catch (e) {
if (e?.code !== 'ENOENT') throw e
}
})
)
}
async function removeCustomItemFiles(items) {
await Promise.all(
items.map(async (item) => {
@@ -426,10 +441,19 @@ async function createGameTemplateFromRequest({ templateRequest, gameId, gameName
}
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: 10000, orphanOnly: false })
const target = result.items.find((item) => item.id === req.params.itemId)
if (!target) return res.status(404).json({ error: 'not_found' })
if (target.sourceType === 'template') {
if (String(target.id || '').startsWith('asset:')) {
const assetId = String(target.id).slice('asset:'.length)
const asset = await findImageAssetById(assetId)
if (!asset) return res.status(404).json({ error: 'not_found' })
await deleteImageAssets([assetId])
await removeUploadFiles([asset.src])
return res.json({ ok: true, sourceType: 'template-asset' })
}
await deleteGameItem(target.id)
return res.json({ ok: true, sourceType: 'template' })
}