관리자 기능과 계정 즐겨찾기 개선
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user