diff --git a/backend/src/db.js b/backend/src/db.js
index 482aba2..c6fe31f 100644
--- a/backend/src/db.js
+++ b/backend/src/db.js
@@ -2026,6 +2026,50 @@ async function listAdminTierLists({ queryText = '', page = 1, limit = 50, curren
}
}
+async function summarizeAdminTierLists({ queryText = '', gameId = '' } = {}) {
+ const hasQuery = !!(queryText || '').trim()
+ const hasGameId = !!(gameId || '').trim()
+ const search = `%${(queryText || '').trim()}%`
+ const whereParts = []
+ const params = []
+
+ if (hasGameId) {
+ whereParts.push('t.game_id = ?')
+ params.push((gameId || '').trim())
+ }
+
+ if (hasQuery) {
+ whereParts.push(`(
+ t.title LIKE ?
+ OR g.name LIKE ?
+ OR g.id LIKE ?
+ OR u.email LIKE ?
+ OR u.nickname LIKE ?
+ )`)
+ params.push(search, search, search, search, search)
+ }
+
+ const whereClause = whereParts.length ? `WHERE ${whereParts.join(' AND ')}` : ''
+ const rows = await query(
+ `
+ SELECT t.is_public
+ FROM tierlists t
+ INNER JOIN users u ON u.id = t.author_id
+ INNER JOIN games g ON g.id = t.game_id
+ ${whereClause}
+ `,
+ params
+ )
+
+ const total = rows.length
+ const publicCount = rows.filter((row) => Number(row.is_public) === 1).length
+ return {
+ total,
+ publicCount,
+ privateCount: Math.max(0, total - publicCount),
+ }
+}
+
async function findTierListById(id, currentUserId = '') {
const rows = await query(
`
@@ -2408,6 +2452,7 @@ module.exports = {
listFavoriteTierLists,
listUserTierLists,
listAdminTierLists,
+ summarizeAdminTierLists,
findTierListById,
favoriteTierList,
unfavoriteTierList,
diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js
index ae0eeb7..df3d295 100644
--- a/backend/src/routes/admin.js
+++ b/backend/src/routes/admin.js
@@ -31,6 +31,7 @@ const {
listUsers,
findPrimaryAdminUser,
listAdminTierLists,
+ summarizeAdminTierLists,
findTierListById,
listAdminTemplateRequests,
findTemplateRequestById,
@@ -310,6 +311,21 @@ router.get('/tierlists', requireAdmin, async (req, res) => {
res.json(result)
})
+router.get('/tierlists/stats', requireAdmin, async (req, res) => {
+ const schema = z.object({
+ q: z.string().trim().max(120).optional().default(''),
+ gameId: z.string().trim().max(120).optional().default(''),
+ })
+ const parsed = schema.safeParse(req.query)
+ if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
+
+ const result = await summarizeAdminTierLists({
+ queryText: parsed.data.q,
+ gameId: parsed.data.gameId,
+ })
+ res.json(result)
+})
+
router.get('/template-requests', requireAdmin, async (req, res) => {
const requests = await listAdminTemplateRequests({ statuses: ['pending', 'reviewing'] })
res.json({ requests })
diff --git a/docs/history.md b/docs/history.md
index c1f0df2..7e06e17 100644
--- a/docs/history.md
+++ b/docs/history.md
@@ -1,5 +1,10 @@
# 의사결정 이력
+## 2026-04-02 v1.3.69
+- 관리자 아이템 라이브러리의 참조 수는 저장 구조 설명에는 도움이 되지만 실제 운영에서는 대부분 의미가 약하므로, 카드와 모달에서 계속 전면에 두기보다 다시 숨기고 필요한 경우 내부 데이터로만 남기는 편이 맞다고 정리했다.
+- 관리자 상단 요약 수치는 `활성/대기` 같은 상태 문구보다 실제 운영 판단에 바로 쓰이는 `공개/비공개 개수`가 더 중요하므로, 게임 관리와 티어표 관리 모두 공개 상태 집계를 먼저 보여주는 편이 낫다고 판단했다.
+- 공개/비공개 수치는 현재 페이지 일부를 세면 오해가 생기기 쉬우므로, 검색/게임 기준 전체 결과를 집계하는 별도 관리자 API로 계산하는 편이 맞다고 정리했다.
+
## 2026-04-02 v1.3.68
- 관리자 아이템 상세는 새 모달을 겹쳐 올리는 방식보다 기존 모달 안에서 `왼쪽 선택 대상 / 오른쪽 작업과 참조 정보` 역할만 분명히 나누는 편이 더 안정적이라고 정리했다.
- 같은 이미지를 두 위치에서 반복 노출하면 “모달이 두 개 겹친 것처럼” 느껴질 수 있으므로, 선택 썸네일은 한 곳에만 두고 양쪽 패널은 각자 스크롤되는 구조로 정리하는 편이 맞다고 판단했다.
diff --git a/docs/todo.md b/docs/todo.md
index 419c0a3..8714f92 100644
--- a/docs/todo.md
+++ b/docs/todo.md
@@ -1,6 +1,7 @@
# 할 일 및 이슈
## 단기 확인
+- 관리자 게임 관리 상단의 `전체 / 공개 / 비공개` 티어표 수치와 전체 티어표 관리 상단 집계가 실제 운영 데이터와 맞는지, 공개 전환 직후 숫자가 자연스럽게 갱신되는지 한 번 더 QA한다.
- 관리자 아이템 상세 모달은 왼쪽 선택 카드와 오른쪽 상세 본문으로 다시 정리했으므로, 데스크톱/모바일에서 긴 게임 목록과 긴 참조 목록이 각각 자연스럽게 스크롤되는지 한 번 더 QA한다.
- 관리자 계정으로 `/admin/...`을 직접 새로고침했을 때 홈으로 튕기지 않고 그대로 유지되는지, 실제 세션이 살아 있는 브라우저와 만료된 브라우저 각각에서 한 번 더 QA한다.
- 티어표 만들기 화면의 보드 드롭존은 점선/높이/중앙 정렬로 존재감이 커졌으므로, 데스크톱과 모바일에서 파일 선택 버튼과 안내 문구 밀도를 한 번 더 QA한다.
diff --git a/docs/update.md b/docs/update.md
index d00b077..b773386 100644
--- a/docs/update.md
+++ b/docs/update.md
@@ -1,5 +1,10 @@
# 업데이트 로그
+## 2026-04-02 v1.3.69
+- 관리자 아이템 라이브러리는 참조 수/공유 기록 UI가 실제 운영 판단에 비해 노이즈가 커 보여 카드 수치, 상세 모달의 같은 이미지 참조 섹션, 삭제 확인 문구의 참조 강조를 걷어내고 다시 항목 자체 관리 흐름 위주로 정리함.
+- 관리자 게임 관리 상단 요약은 더 이상 `선택 상태`처럼 추상적인 문구를 보여주지 않고, 선택된 게임 기준으로 만들어진 티어표의 `전체 / 공개 / 비공개` 개수를 바로 보여주도록 바꿈.
+- 전체 티어표 관리 상단에도 검색 결과 기준 `전체 / 공개 / 비공개` 수치를 함께 노출하고, 이를 위해 관리자 티어표 집계 API를 별도로 추가해 페이지 단위가 아니라 실제 전체 결과 기준 숫자를 안정적으로 표시함.
+
## 2026-04-02 v1.3.68
- 관리자 아이템 상세 모달은 같은 이미지를 왼쪽 선택 카드와 오른쪽 본문에서 두 번 보여주던 중복 미리보기를 제거해, 한 모달 안에서 정보가 겹쳐 보이던 문제를 정리함.
- 왼쪽 게임 선택 패널과 오른쪽 상세 정보 패널은 각각 독립 스크롤이 되도록 바꾸고, 스크롤바도 다시 보이게 조정해 긴 목록이나 긴 참조 정보가 있어도 레이아웃이 깨지지 않고 탐색할 수 있게 함.
diff --git a/frontend/src/components/admin/AdminItemsSection.vue b/frontend/src/components/admin/AdminItemsSection.vue
index 518081c..ae9ae47 100644
--- a/frontend/src/components/admin/AdminItemsSection.vue
+++ b/frontend/src/components/admin/AdminItemsSection.vue
@@ -19,10 +19,6 @@ const props = defineProps({
{{ item.sourceLabel }}