관리자 기능과 계정 즐겨찾기 개선

This commit is contained in:
2026-06-23 14:32:43 +09:00
parent 00e05dbe3f
commit 93e81ce3ea
26 changed files with 804 additions and 914 deletions

View File

@@ -3,7 +3,12 @@ import bcrypt from 'bcryptjs';
import { createRequireAdmin } from '../middleware/auth.js';
import { contentHash, validateCards, nowIso } from '../utils.js';
import { resolveDeckCards } from '../services/cardResolver.js';
import { getPreviewUrl } from '../services/previewImage.js';
import {
getPreviewUrl,
deletePreviewImage,
listOrphanPreviewImages,
deleteOrphanPreviewImages,
} from '../services/previewImage.js';
import { getAvatarUrl, saveAvatarImage, deleteAvatarImage } from '../services/avatarImage.js';
/**
@@ -237,13 +242,29 @@ export function createAdminRouter(db) {
});
});
router.get('/previews/orphans', (_req, res) => {
const referencedCodes = db.prepare('SELECT share_code FROM decks').all().map((row) => row.share_code);
const files = listOrphanPreviewImages(referencedCodes);
res.json({
files,
totalCount: files.length,
totalBytes: files.reduce((sum, file) => sum + file.size, 0),
});
});
router.delete('/previews/orphans', (_req, res) => {
const referencedCodes = db.prepare('SELECT share_code FROM decks').all().map((row) => row.share_code);
res.json({ ok: true, ...deleteOrphanPreviewImages(referencedCodes) });
});
router.delete('/decks/:shareCode', (req, res) => {
const row = db.prepare('SELECT id FROM decks WHERE share_code = ?').get(req.params.shareCode);
const row = db.prepare('SELECT id, share_code FROM decks WHERE share_code = ?').get(req.params.shareCode);
if (!row) {
return res.status(404).json({ error: '덱을 찾을 수 없습니다.' });
}
db.prepare('DELETE FROM decks WHERE id = ?').run(row.id);
res.json({ ok: true });
const previewDeleted = deletePreviewImage(row.share_code);
res.json({ ok: true, previewDeleted });
});
return router;

View File

@@ -3,7 +3,12 @@ import path from 'path';
import { requireAuth, optionalAuth } from '../middleware/auth.js';
import { resolveDeckCards } from '../services/cardResolver.js';
import { generateShareCode, validateCards, nowIso } from '../utils.js';
import { savePreviewImage, getPreviewFilePath, getPreviewUrl } from '../services/previewImage.js';
import {
savePreviewImage,
getPreviewFilePath,
getPreviewUrl,
deletePreviewImage,
} from '../services/previewImage.js';
/**
* 덱 행을 API 응답 형식으로 변환
@@ -57,6 +62,84 @@ function getAuthor(db, userId) {
export function createDecksRouter(db) {
const router = Router();
/**
* 현재 사용자가 접근할 수 없는 즐겨찾기 정리
* @param {number} userId
*/
function cleanupUnavailableFavorites(userId) {
db.prepare(`
DELETE FROM deck_favorites
WHERE user_id = ?
AND share_code IN (
SELECT f.share_code
FROM deck_favorites f
JOIN decks d ON d.share_code = f.share_code
WHERE f.user_id = ?
AND (
(d.expires_at IS NOT NULL AND d.expires_at <= datetime('now'))
OR (d.is_public = 0 AND (d.user_id IS NULL OR d.user_id != ?))
)
)
`).run(userId, userId, userId);
}
router.get('/favorites', requireAuth, (req, res) => {
cleanupUnavailableFavorites(req.user.id);
const favorites = db.prepare(`
SELECT share_code
FROM deck_favorites
WHERE user_id = ?
ORDER BY created_at DESC
`).all(req.user.id);
res.json({ favorites: favorites.map((row) => row.share_code) });
});
router.post('/favorites', requireAuth, (req, res) => {
const shareCodes = Array.isArray(req.body?.shareCodes)
? [...new Set(req.body.shareCodes.filter((code) => typeof code === 'string' && code.length <= 64))].slice(0, 500)
: [];
const insert = db.prepare(`
INSERT OR IGNORE INTO deck_favorites (user_id, share_code)
SELECT ?, share_code
FROM decks
WHERE share_code = ?
AND (expires_at IS NULL OR expires_at > datetime('now'))
AND (is_public = 1 OR user_id = ?)
`);
const sync = db.transaction((codes) => {
codes.forEach((code) => insert.run(req.user.id, code, req.user.id));
});
sync(shareCodes);
cleanupUnavailableFavorites(req.user.id);
const favorites = db.prepare(`
SELECT share_code FROM deck_favorites WHERE user_id = ? ORDER BY created_at DESC
`).all(req.user.id);
res.json({ favorites: favorites.map((row) => row.share_code) });
});
router.put('/favorites/:shareCode', requireAuth, (req, res) => {
const deck = db.prepare(`
SELECT share_code
FROM decks
WHERE share_code = ?
AND (expires_at IS NULL OR expires_at > datetime('now'))
AND (is_public = 1 OR user_id = ?)
`).get(req.params.shareCode, req.user.id);
if (!deck) {
return res.status(404).json({ error: '즐겨찾기에 추가할 수 있는 덱을 찾을 수 없습니다.' });
}
db.prepare(`
INSERT OR IGNORE INTO deck_favorites (user_id, share_code) VALUES (?, ?)
`).run(req.user.id, deck.share_code);
res.json({ ok: true, favorite: true });
});
router.delete('/favorites/:shareCode', requireAuth, (req, res) => {
db.prepare('DELETE FROM deck_favorites WHERE user_id = ? AND share_code = ?')
.run(req.user.id, req.params.shareCode);
res.json({ ok: true, favorite: false });
});
router.get('/presets', async (_req, res, next) => {
try {
const rows = db.prepare(`
@@ -283,7 +366,8 @@ export function createDecksRouter(db) {
}
db.prepare('DELETE FROM decks WHERE id = ?').run(row.id);
res.json({ ok: true });
const previewDeleted = deletePreviewImage(row.share_code);
res.json({ ok: true, previewDeleted });
});
return router;