diff --git a/backend/src/db.js b/backend/src/db.js index d4f3a22..1f4dcf4 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -1083,6 +1083,8 @@ async function cleanupMissingUploadReferences() { const stats = { clearedAvatars: 0, clearedGameThumbnails: 0, + clearedTierListThumbnails: 0, + clearedTemplateRequestThumbnails: 0, deletedGameItems: 0, updatedTierLists: 0, updatedTemplateRequests: 0, @@ -1124,67 +1126,69 @@ async function cleanupMissingUploadReferences() { missingCustomSrcs.add(row.src) } - if (missingCustomItemIds.size || missingCustomSrcs.size) { - for (const row of tierListRows) { - const groups = parseJson(row.groups_json, []) - const pool = parseJson(row.pool_json, []) - let changed = false - let nextThumbnail = row.thumbnail_src || '' + for (const row of tierListRows) { + const groups = parseJson(row.groups_json, []) + const pool = parseJson(row.pool_json, []) + let changed = false + let nextThumbnail = row.thumbnail_src || '' - if (row.thumbnail_src && !(await fileExistsForUploadSrc(row.thumbnail_src))) { - nextThumbnail = '' - changed = true - } + if (row.thumbnail_src && !(await fileExistsForUploadSrc(row.thumbnail_src))) { + nextThumbnail = '' + changed = true + stats.clearedTierListThumbnails += 1 + } - const strippedPool = stripMissingItems(pool, missingCustomItemIds, missingCustomSrcs) - const strippedGroups = stripItemIdsFromGroups(groups, missingCustomItemIds) - if (strippedPool.changed || strippedGroups.changed) changed = true + const strippedPool = stripMissingItems(pool, missingCustomItemIds, missingCustomSrcs) + const strippedGroups = stripItemIdsFromGroups(groups, missingCustomItemIds) + if (strippedPool.changed || strippedGroups.changed) changed = true - if (changed) { - await query('UPDATE tierlists SET thumbnail_src = ?, groups_json = ?, pool_json = ?, updated_at = ? WHERE id = ?', [ + if (changed) { + await query('UPDATE tierlists SET thumbnail_src = ?, groups_json = ?, pool_json = ?, updated_at = ? WHERE id = ?', [ + nextThumbnail, + serializeJson(strippedGroups.groups), + serializeJson(strippedPool.items), + now(), + row.id, + ]) + stats.updatedTierLists += 1 + } + } + + for (const row of templateRequestRows) { + const groups = parseJson(row.groups_json, []) + const items = parseJson(row.items_json, []) + const boardItems = parseJson(row.board_items_json, []) + let changed = false + let nextThumbnail = row.thumbnail_src_snapshot || '' + + if (row.thumbnail_src_snapshot && !(await fileExistsForUploadSrc(row.thumbnail_src_snapshot))) { + nextThumbnail = '' + changed = true + stats.clearedTemplateRequestThumbnails += 1 + } + + const strippedItems = stripMissingItems(items, missingCustomItemIds, missingCustomSrcs) + const strippedBoardItems = stripMissingItems(boardItems, missingCustomItemIds, missingCustomSrcs) + const strippedGroups = stripItemIdsFromGroups(groups, missingCustomItemIds) + if (strippedItems.changed || strippedBoardItems.changed || strippedGroups.changed) changed = true + + if (changed) { + await query( + 'UPDATE template_requests SET thumbnail_src_snapshot = ?, groups_json = ?, items_json = ?, board_items_json = ?, updated_at = ? WHERE id = ?', + [ nextThumbnail, serializeJson(strippedGroups.groups), - serializeJson(strippedPool.items), + serializeJson(strippedItems.items), + serializeJson(strippedBoardItems.items), now(), row.id, - ]) - stats.updatedTierLists += 1 - } - } - - for (const row of templateRequestRows) { - const groups = parseJson(row.groups_json, []) - const items = parseJson(row.items_json, []) - const boardItems = parseJson(row.board_items_json, []) - let changed = false - let nextThumbnail = row.thumbnail_src_snapshot || '' - - if (row.thumbnail_src_snapshot && !(await fileExistsForUploadSrc(row.thumbnail_src_snapshot))) { - nextThumbnail = '' - changed = true - } - - const strippedItems = stripMissingItems(items, missingCustomItemIds, missingCustomSrcs) - const strippedBoardItems = stripMissingItems(boardItems, missingCustomItemIds, missingCustomSrcs) - const strippedGroups = stripItemIdsFromGroups(groups, missingCustomItemIds) - if (strippedItems.changed || strippedBoardItems.changed || strippedGroups.changed) changed = true - - if (changed) { - await query( - 'UPDATE template_requests SET thumbnail_src_snapshot = ?, groups_json = ?, items_json = ?, board_items_json = ?, updated_at = ? WHERE id = ?', - [ - nextThumbnail, - serializeJson(strippedGroups.groups), - serializeJson(strippedItems.items), - serializeJson(strippedBoardItems.items), - now(), - row.id, - ] - ) - stats.updatedTemplateRequests += 1 - } + ] + ) + stats.updatedTemplateRequests += 1 } + } + if (missingCustomItemIds.size) { await deleteCustomItems(Array.from(missingCustomItemIds)) stats.deletedCustomItems = missingCustomItemIds.size } diff --git a/docs/history.md b/docs/history.md index 9e5562c..6546e36 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,5 +1,8 @@ # 의사결정 이력 +## 2026-04-02 v1.3.66 +- 누락 참조 정리 도구는 커스텀 아이템 누락이 없어도 티어표/요청 썸네일 누락을 항상 따로 정리해야 하므로, 썸네일 정리를 커스텀 아이템 분기에 묶어두면 안 된다고 정리했다. + ## 2026-04-02 v1.3.65 - 누락 파일 수치가 계속 쌓이는 상태에서는 원인 분석만으로는 운영 부담이 줄지 않으므로, 실제 파일이 없는 참조만 골라 썸네일/아바타/게임 아이템/커스텀 아이템 참조를 정리하는 관리자 액션을 제공하는 편이 맞다고 정리했다. diff --git a/docs/update.md b/docs/update.md index 59a2a5d..dbf0f76 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,9 @@ # 업데이트 로그 +## 2026-04-02 v1.3.66 +- `누락 참조 정리`는 처음엔 누락 커스텀 아이템이 있을 때만 티어표/요청 썸네일까지 함께 보던 조건 때문에, 누락 썸네일만 남아 있으면 수치가 줄지 않던 문제가 있었으므로 분기를 풀어 티어표 썸네일과 요청 썸네일도 항상 실제 파일 존재 여부를 확인해 정리하도록 수정함. +- 정리 완료 메시지에도 `티어표 썸네일`, `요청 썸네일` 항목을 추가해 어떤 종류의 누락 참조가 실제로 정리됐는지 바로 알 수 있게 함. + ## 2026-04-02 v1.3.65 - 관리자 이미지 최적화 패널에 `누락 참조 정리` 액션을 추가해, 실제 파일이 없는 `/uploads/...` 참조만 대상으로 썸네일/아바타는 비우고 누락된 게임 아이템·커스텀 아이템은 관련 티어표/템플릿 요청 참조와 함께 정리할 수 있게 함. - 따라서 예전 수동 파일 정리나 레거시 데이터로 인해 쌓인 `누락 파일`은 단순 통계로만 남지 않고, 관리자 화면에서 실제로 줄일 수 있는 관리 도구를 갖게 됨. diff --git a/frontend/src/views/AdminView.vue b/frontend/src/views/AdminView.vue index 3edcba2..c83f2c5 100644 --- a/frontend/src/views/AdminView.vue +++ b/frontend/src/views/AdminView.vue @@ -677,7 +677,14 @@ async function cleanupMissingImageReferences() { const data = await api.cleanupAdminMissingImageReferences() await Promise.all([refreshImageDiagnostics(), refreshGames(), refreshCustomItems(), refreshTemplateRequests()]) const result = data.result || {} - success.value = `누락 참조를 정리했어요. 아바타 ${result.clearedAvatars || 0}건, 게임 썸네일 ${result.clearedGameThumbnails || 0}건, 게임 아이템 ${result.deletedGameItems || 0}건, 커스텀 아이템 ${result.deletedCustomItems || 0}건` + success.value = + `누락 참조를 정리했어요. ` + + `아바타 ${result.clearedAvatars || 0}건, ` + + `게임 썸네일 ${result.clearedGameThumbnails || 0}건, ` + + `티어표 썸네일 ${result.clearedTierListThumbnails || 0}건, ` + + `요청 썸네일 ${result.clearedTemplateRequestThumbnails || 0}건, ` + + `게임 아이템 ${result.deletedGameItems || 0}건, ` + + `커스텀 아이템 ${result.deletedCustomItems || 0}건` } catch (e) { error.value = '누락 이미지 참조 정리에 실패했어요.' } finally {