const router = require('express').Router(); const { body, validationResult } = require('express-validator'); const prisma = require('../config/db'); const { requireAuth, requireRole } = require('../middleware/auth'); const { writeAuditLog } = require('../middleware/audit'); // 모든 admin 라우트는 admin 역할 필요 router.use(requireAuth, requireRole('admin')); // GET /api/admin/projects/pending router.get('/projects/pending', async (_req, res) => { try { const projects = await prisma.project.findMany({ where: { status: 'pending' }, orderBy: { updatedAt: 'asc' }, include: { user: { select: { id: true, email: true, nickname: true } }, files: { where: { fileType: 'image' }, take: 1 }, _count: { select: { files: true } }, }, }); res.json(projects); } catch (err) { res.status(500).json({ error: '서버 오류' }); } }); // GET /api/admin/projects — 전체 목록 (필터) router.get('/projects', async (req, res) => { try { const page = Math.max(1, parseInt(req.query.page) || 1); const limit = Math.min(100, parseInt(req.query.limit) || 30); const skip = (page - 1) * limit; const where = {}; if (req.query.status) where.status = req.query.status; const [projects, total] = await Promise.all([ prisma.project.findMany({ where, skip, take: limit, orderBy: { updatedAt: 'desc' }, include: { user: { select: { id: true, email: true, nickname: true } } }, }), prisma.project.count({ where }), ]); res.json({ projects, total, page, pages: Math.ceil(total / limit) }); } catch (err) { res.status(500).json({ error: '서버 오류' }); } }); // POST /api/admin/projects/:id/approve router.post('/projects/:id/approve', [ body('commissionRate').optional().isFloat({ min: 0, max: 1 }), body('price').isInt({ min: 100 }), ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) return res.status(400).json({ error: errors.array()[0].msg }); const { commissionRate = 0.1, price } = req.body; try { const project = await prisma.project.findUnique({ where: { id: req.params.id } }); if (!project) return res.status(404).json({ error: '프로젝트를 찾을 수 없습니다' }); if (project.status !== 'pending') return res.status(400).json({ error: '검토 대기 상태가 아닙니다' }); // 트랜잭션: 프로젝트 승인 + 상품 자동 생성 const [updated] = await prisma.$transaction([ prisma.project.update({ where: { id: req.params.id }, data: { status: 'approved', commissionRate: parseFloat(commissionRate), adminNote: null }, }), prisma.product.upsert({ where: { projectId: req.params.id }, update: { price: parseInt(price), isOnSale: true }, create: { projectId: req.params.id, price: parseInt(price) }, }), ]); await writeAuditLog({ userId: req.user.id, action: 'ADMIN_APPROVE', targetType: 'Project', targetId: req.params.id, req, metadata: { commissionRate, price }, responseStatus: 200, }); res.json(updated); } catch (err) { console.error(err); res.status(500).json({ error: '서버 오류' }); } }); // POST /api/admin/projects/:id/reject router.post('/projects/:id/reject', [ body('adminNote').trim().isLength({ min: 5 }), ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) return res.status(400).json({ error: errors.array()[0].msg }); try { const project = await prisma.project.findUnique({ where: { id: req.params.id } }); if (!project) return res.status(404).json({ error: '프로젝트를 찾을 수 없습니다' }); const updated = await prisma.project.update({ where: { id: req.params.id }, data: { status: 'rejected', adminNote: req.body.adminNote }, }); await writeAuditLog({ userId: req.user.id, action: 'ADMIN_REJECT', targetType: 'Project', targetId: req.params.id, req, responseStatus: 200, }); res.json(updated); } catch (err) { res.status(500).json({ error: '서버 오류' }); } }); // GET /api/admin/users router.get('/users', async (req, res) => { try { const page = Math.max(1, parseInt(req.query.page) || 1); const limit = Math.min(100, parseInt(req.query.limit) || 30); const skip = (page - 1) * limit; const where = {}; if (req.query.role) where.role = req.query.role; if (req.query.q) { where.OR = [ { email: { contains: req.query.q, mode: 'insensitive' } }, { nickname: { contains: req.query.q, mode: 'insensitive' } }, ]; } const [users, total] = await Promise.all([ prisma.user.findMany({ where, skip, take: limit, orderBy: { createdAt: 'desc' }, select: { id: true, email: true, nickname: true, role: true, isActive: true, createdAt: true, lastLoginAt: true }, }), prisma.user.count({ where }), ]); res.json({ users, total, page, pages: Math.ceil(total / limit) }); } catch (err) { res.status(500).json({ error: '서버 오류' }); } }); // PUT /api/admin/users/:id/toggle — 활성/비활성 router.put('/users/:id/toggle', async (req, res) => { try { const user = await prisma.user.findUnique({ where: { id: req.params.id } }); if (!user) return res.status(404).json({ error: '사용자를 찾을 수 없습니다' }); if (user.role === 'admin') return res.status(400).json({ error: '관리자 계정은 비활성화할 수 없습니다' }); const updated = await prisma.user.update({ where: { id: req.params.id }, data: { isActive: !user.isActive }, select: { id: true, email: true, isActive: true }, }); await writeAuditLog({ userId: req.user.id, action: updated.isActive ? 'ADMIN_USER_ACTIVATE' : 'ADMIN_USER_DEACTIVATE', targetType: 'User', targetId: req.params.id, req, responseStatus: 200, }); res.json(updated); } catch (err) { res.status(500).json({ error: '서버 오류' }); } }); // PUT /api/admin/users/:id/role — 역할 변경 router.put('/users/:id/role', [ body('role').isIn(['admin', 'seller', 'buyer']), ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) return res.status(400).json({ error: errors.array()[0].msg }); try { const updated = await prisma.user.update({ where: { id: req.params.id }, data: { role: req.body.role }, select: { id: true, email: true, role: true }, }); await writeAuditLog({ userId: req.user.id, action: 'ADMIN_ROLE_CHANGE', targetType: 'User', targetId: req.params.id, req, metadata: { newRole: req.body.role }, responseStatus: 200, }); res.json(updated); } catch (err) { res.status(500).json({ error: '서버 오류' }); } }); // GET /api/admin/logs router.get('/logs', async (req, res) => { try { const page = Math.max(1, parseInt(req.query.page) || 1); const limit = Math.min(200, parseInt(req.query.limit) || 50); const skip = (page - 1) * limit; const where = {}; if (req.query.action) where.action = req.query.action; if (req.query.userId) where.userId = req.query.userId; if (req.query.from || req.query.to) { where.createdAt = {}; if (req.query.from) where.createdAt.gte = new Date(req.query.from); if (req.query.to) where.createdAt.lte = new Date(req.query.to); } const [logs, total] = await Promise.all([ prisma.auditLog.findMany({ where, skip, take: limit, orderBy: { createdAt: 'desc' }, include: { user: { select: { email: true, nickname: true } } }, }), prisma.auditLog.count({ where }), ]); res.json({ logs, total, page, pages: Math.ceil(total / limit) }); } catch (err) { res.status(500).json({ error: '서버 오류' }); } }); // GET /api/admin/stats router.get('/stats', async (_req, res) => { try { const [ totalUsers, totalProjects, pendingProjects, totalOrders, paidOrders, ] = await Promise.all([ prisma.user.count(), prisma.project.count(), prisma.project.count({ where: { status: 'pending' } }), prisma.order.count(), prisma.order.aggregate({ where: { status: 'paid' }, _sum: { amount: true }, _count: true }), ]); res.json({ users: totalUsers, projects: { total: totalProjects, pending: pendingProjects }, revenue: { total: paidOrders._sum.amount || 0, orders: paidOrders._count }, }); } catch (err) { res.status(500).json({ error: '서버 오류' }); } }); module.exports = router;