v4.0.9: 관리자 회원 목록 2열·덱 공개/전체 통계 추가
관리자가 회원별 저장 덱 이용 현황을 한눈에 볼 수 있도록 API 집계와 UI를 보강한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -11,6 +11,41 @@ import {
|
||||
} from '../services/previewImage.js';
|
||||
import { getAvatarUrl, saveAvatarImage, deleteAvatarImage } from '../services/avatarImage.js';
|
||||
|
||||
/**
|
||||
* 회원 목록에 저장 덱 수(공개/전체) 부여
|
||||
* @param {import('better-sqlite3').Database} db
|
||||
* @param {Array<{ id: number }>} users
|
||||
* @returns {Array<object>}
|
||||
*/
|
||||
function attachUserDeckCounts(db, users) {
|
||||
if (!users.length) return users;
|
||||
|
||||
const placeholders = users.map(() => '?').join(',');
|
||||
const ids = users.map((u) => u.id);
|
||||
const counts = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT user_id AS userId,
|
||||
COUNT(*) AS deckTotal,
|
||||
SUM(CASE WHEN is_public = 1 THEN 1 ELSE 0 END) AS deckPublic
|
||||
FROM decks
|
||||
WHERE share_type = 'user' AND user_id IN (${placeholders})
|
||||
GROUP BY user_id
|
||||
`,
|
||||
)
|
||||
.all(...ids);
|
||||
const countByUser = new Map(counts.map((row) => [row.userId, row]));
|
||||
|
||||
return users.map((user) => {
|
||||
const stats = countByUser.get(user.id);
|
||||
return {
|
||||
...user,
|
||||
deckPublic: stats?.deckPublic ?? 0,
|
||||
deckTotal: stats?.deckTotal ?? 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 라우트
|
||||
* @param {import('better-sqlite3').Database} db
|
||||
@@ -49,10 +84,13 @@ export function createAdminRouter(db) {
|
||||
}
|
||||
|
||||
res.json({
|
||||
users: rows.map((u) => ({
|
||||
...u,
|
||||
avatarUrl: getAvatarUrl(u.id) || u.avatarUrl,
|
||||
})),
|
||||
users: attachUserDeckCounts(
|
||||
db,
|
||||
rows.map((u) => ({
|
||||
...u,
|
||||
avatarUrl: getAvatarUrl(u.id) || u.avatarUrl,
|
||||
})),
|
||||
),
|
||||
total,
|
||||
page: Number(page),
|
||||
limit: Number(limit),
|
||||
@@ -70,9 +108,14 @@ export function createAdminRouter(db) {
|
||||
return res.status(404).json({ error: '사용자를 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
const deckCount = db.prepare('SELECT COUNT(*) AS c FROM decks WHERE user_id = ?').get(user.id).c;
|
||||
const deckTotal = db.prepare(
|
||||
"SELECT COUNT(*) AS c FROM decks WHERE user_id = ? AND share_type = 'user'",
|
||||
).get(user.id).c;
|
||||
const deckPublic = db.prepare(
|
||||
"SELECT COUNT(*) AS c FROM decks WHERE user_id = ? AND share_type = 'user' AND is_public = 1",
|
||||
).get(user.id).c;
|
||||
user.avatarUrl = getAvatarUrl(user.id) || user.avatarUrl;
|
||||
res.json({ user, deckCount });
|
||||
res.json({ user, deckCount: deckTotal, deckPublic, deckTotal });
|
||||
});
|
||||
|
||||
router.put('/users/:id', (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user