릴리스: v1.3.36 관리자 아이템 라이브러리 보강
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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' })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user