feat(backend): 활동 로그 유틸리티 및 API 엔드포인트 추가
- logActivity() fire-and-forget 유틸리티 함수 - GET /api/admin/logs 엔드포인트 (필터/페이지네이션) - 라우트 등록 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c6332c4f96
commit
357fd7fc88
3 changed files with 159 additions and 0 deletions
129
backend/src/routes/admin/logs.js
Normal file
129
backend/src/routes/admin/logs.js
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import { errorResponse } from '../../schemas/index.js';
|
||||
import { serverError } from '../../utils/error.js';
|
||||
|
||||
/**
|
||||
* 활동 로그 관리자 라우트
|
||||
*/
|
||||
export default async function logsRoutes(fastify) {
|
||||
const { db } = fastify;
|
||||
|
||||
/**
|
||||
* GET /api/admin/logs
|
||||
* 활동 로그 목록 조회
|
||||
*/
|
||||
fastify.get('/', {
|
||||
schema: {
|
||||
tags: ['admin/logs'],
|
||||
summary: '활동 로그 목록 조회',
|
||||
security: [{ bearerAuth: [] }],
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page: { type: 'integer', minimum: 1, default: 1 },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 50 },
|
||||
category: { type: 'string', description: '카테고리 필터 (콤마 구분)' },
|
||||
actor: { type: 'string', description: '행위자 필터 (admin 또는 bot)' },
|
||||
search: { type: 'string', description: 'summary 검색' },
|
||||
from: { type: 'string', description: '시작 날짜 (YYYY-MM-DD)' },
|
||||
to: { type: 'string', description: '종료 날짜 (YYYY-MM-DD)' },
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
logs: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'integer' },
|
||||
actor: { type: 'string' },
|
||||
action: { type: 'string' },
|
||||
category: { type: 'string' },
|
||||
target_type: { type: 'string', nullable: true },
|
||||
target_id: { type: 'integer', nullable: true },
|
||||
summary: { type: 'string' },
|
||||
details: { type: 'object', nullable: true },
|
||||
created_at: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
total: { type: 'integer' },
|
||||
page: { type: 'integer' },
|
||||
limit: { type: 'integer' },
|
||||
totalPages: { type: 'integer' },
|
||||
},
|
||||
},
|
||||
500: errorResponse,
|
||||
},
|
||||
},
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const { page = 1, limit = 50, category, actor, search, from, to } = request.query;
|
||||
|
||||
try {
|
||||
const conditions = [];
|
||||
const params = [];
|
||||
|
||||
// 카테고리 필터
|
||||
if (category) {
|
||||
const categories = category.split(',').map(c => c.trim()).filter(Boolean);
|
||||
if (categories.length > 0) {
|
||||
conditions.push(`category IN (${categories.map(() => '?').join(',')})`);
|
||||
params.push(...categories);
|
||||
}
|
||||
}
|
||||
|
||||
// 행위자 필터
|
||||
if (actor === 'admin') {
|
||||
conditions.push("actor = 'admin'");
|
||||
} else if (actor === 'bot') {
|
||||
conditions.push("actor != 'admin'");
|
||||
}
|
||||
|
||||
// 텍스트 검색
|
||||
if (search) {
|
||||
conditions.push('summary LIKE ?');
|
||||
params.push(`%${search}%`);
|
||||
}
|
||||
|
||||
// 날짜 필터
|
||||
if (from) {
|
||||
conditions.push('created_at >= ?');
|
||||
params.push(`${from} 00:00:00`);
|
||||
}
|
||||
if (to) {
|
||||
conditions.push('created_at <= ?');
|
||||
params.push(`${to} 23:59:59`);
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// 총 개수 조회
|
||||
const [countResult] = await db.query(
|
||||
`SELECT COUNT(*) as total FROM logs ${whereClause}`,
|
||||
params
|
||||
);
|
||||
const total = countResult[0].total;
|
||||
|
||||
// 로그 조회
|
||||
const [logs] = await db.query(
|
||||
`SELECT * FROM logs ${whereClause} ORDER BY created_at DESC LIMIT ? OFFSET ?`,
|
||||
[...params, limit, offset]
|
||||
);
|
||||
|
||||
return {
|
||||
logs,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
} catch (err) {
|
||||
fastify.log.error(`활동 로그 조회 오류: ${err.message}`);
|
||||
return serverError(reply, err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import youtubeAdminRoutes from './admin/youtube.js';
|
|||
import xAdminRoutes from './admin/x.js';
|
||||
import concertAdminRoutes from './admin/concert.js';
|
||||
import placesAdminRoutes from './admin/places.js';
|
||||
import logsAdminRoutes from './admin/logs.js';
|
||||
|
||||
/**
|
||||
* 라우트 통합
|
||||
|
|
@ -51,4 +52,7 @@ export default async function routes(fastify) {
|
|||
|
||||
// 관리자 - 장소 검색 라우트
|
||||
fastify.register(placesAdminRoutes, { prefix: '/admin' });
|
||||
|
||||
// 관리자 - 활동 로그 라우트
|
||||
fastify.register(logsAdminRoutes, { prefix: '/admin/logs' });
|
||||
}
|
||||
|
|
|
|||
26
backend/src/utils/log.js
Normal file
26
backend/src/utils/log.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* 활동 로그 유틸리티
|
||||
* fire-and-forget: 로그 실패가 비즈니스 로직에 영향 주지 않도록 처리
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {object} db - DB 커넥션
|
||||
* @param {object} params
|
||||
* @param {string} params.actor - 행위자 ("admin", "youtube-3", "x-1" 등)
|
||||
* @param {string} params.action - 행동 (create, update, delete, upload, start, stop, sync_complete, error)
|
||||
* @param {string} params.category - 대분류 (album, schedule, member, bot, category, dict, concert, sync)
|
||||
* @param {string} [params.targetType] - 대상 타입 (youtube_schedule, x_schedule, album, photo, member 등)
|
||||
* @param {number} [params.targetId] - 대상 DB ID
|
||||
* @param {string} params.summary - 한 줄 요약
|
||||
* @param {object} [params.details] - 추가 상세 정보 (JSON)
|
||||
*/
|
||||
export async function logActivity(db, { actor, action, category, targetType, targetId, summary, details }) {
|
||||
try {
|
||||
await db.query(
|
||||
'INSERT INTO logs (actor, action, category, target_type, target_id, summary, details) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
[actor, action, category, targetType || null, targetId || null, summary, details ? JSON.stringify(details) : null]
|
||||
);
|
||||
} catch (err) {
|
||||
// 로그 실패는 무시 — 비즈니스 로직에 영향 주지 않음
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue