feat(backend): 관리자/봇 라우트에 logActivity 호출 추가
12개 관리자 라우트와 3개 봇 서비스 파일에 활동 로그 기록 추가. 관리자 작업(일정/앨범/멤버/봇 CRUD)과 봇 동기화(완료/에러)를 logs 테이블에 fire-and-forget으로 기록. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
357fd7fc88
commit
1f1d6987d1
15 changed files with 88 additions and 1 deletions
|
|
@ -3,6 +3,7 @@ import cron from 'node-cron';
|
|||
import staticBots from '../config/bots.js';
|
||||
import { syncAllSchedules } from '../services/meilisearch/index.js';
|
||||
import { nowKST } from '../utils/date.js';
|
||||
import { logActivity } from '../utils/log.js';
|
||||
|
||||
const REDIS_PREFIX = 'bot:status:';
|
||||
const TIMEZONE = 'Asia/Seoul';
|
||||
|
|
@ -203,6 +204,15 @@ async function schedulerPlugin(fastify, opts) {
|
|||
const result = await syncFn(bot);
|
||||
const addedCount = await handleSyncResult(botId, result, { setRunningStatus: true });
|
||||
fastify.log.info(`[${botId}] 동기화 완료: ${addedCount}개 추가`);
|
||||
if (addedCount > 0) {
|
||||
logActivity(fastify.db, {
|
||||
actor: botId,
|
||||
action: 'sync_complete',
|
||||
category: 'sync',
|
||||
summary: `${botId} 동기화 완료: ${addedCount}개 추가`,
|
||||
details: { addedCount },
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
await updateStatus(botId, {
|
||||
status: 'error',
|
||||
|
|
@ -210,6 +220,13 @@ async function schedulerPlugin(fastify, opts) {
|
|||
errorMessage: err.message,
|
||||
});
|
||||
fastify.log.error(`[${botId}] 동기화 오류: ${err.message}`);
|
||||
logActivity(fastify.db, {
|
||||
actor: botId,
|
||||
action: 'error',
|
||||
category: 'sync',
|
||||
summary: `${botId} 동기화 오류: ${err.message}`,
|
||||
details: { error: err.message },
|
||||
});
|
||||
}
|
||||
}, { timezone: TIMEZONE });
|
||||
|
||||
|
|
@ -223,8 +240,24 @@ async function schedulerPlugin(fastify, opts) {
|
|||
const result = await syncFn(bot);
|
||||
const addedCount = await handleSyncResult(botId, result);
|
||||
fastify.log.info(`[${botId}] 초기 동기화 완료: ${addedCount}개 추가`);
|
||||
if (addedCount > 0) {
|
||||
logActivity(fastify.db, {
|
||||
actor: botId,
|
||||
action: 'sync_complete',
|
||||
category: 'sync',
|
||||
summary: `${botId} 초기 동기화 완료: ${addedCount}개 추가`,
|
||||
details: { addedCount },
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
fastify.log.error(`[${botId}] 초기 동기화 오류: ${err.message}`);
|
||||
logActivity(fastify.db, {
|
||||
actor: botId,
|
||||
action: 'error',
|
||||
category: 'sync',
|
||||
summary: `${botId} 초기 동기화 오류: ${err.message}`,
|
||||
details: { error: err.message },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { errorResponse } from '../../schemas/index.js';
|
|||
import { syncAllSchedules } from '../../services/meilisearch/index.js';
|
||||
import { badRequest, notFound, serverError } from '../../utils/error.js';
|
||||
import { nowKST } from '../../utils/date.js';
|
||||
import { logActivity } from '../../utils/log.js';
|
||||
|
||||
// 봇 관련 스키마
|
||||
const botResponse = {
|
||||
|
|
@ -50,7 +51,7 @@ const botIdParam = {
|
|||
* 인증 필요
|
||||
*/
|
||||
export default async function botsRoutes(fastify) {
|
||||
const { scheduler, redis } = fastify;
|
||||
const { scheduler, redis, db } = fastify;
|
||||
const QUOTA_WARNING_KEY = 'youtube:quota_warning';
|
||||
|
||||
/**
|
||||
|
|
@ -161,6 +162,7 @@ export default async function botsRoutes(fastify) {
|
|||
|
||||
try {
|
||||
await scheduler.startBot(id);
|
||||
logActivity(db, { actor: 'admin', action: 'start', category: 'bot', targetType: null, targetId: null, summary: `봇 시작: ${id}` });
|
||||
return { success: true, message: '봇이 시작되었습니다.' };
|
||||
} catch (err) {
|
||||
return badRequest(reply, err.message);
|
||||
|
|
@ -195,6 +197,7 @@ export default async function botsRoutes(fastify) {
|
|||
|
||||
try {
|
||||
await scheduler.stopBot(id);
|
||||
logActivity(db, { actor: 'admin', action: 'stop', category: 'bot', targetType: null, targetId: null, summary: `봇 정지: ${id}` });
|
||||
return { success: true, message: '봇이 정지되었습니다.' };
|
||||
} catch (err) {
|
||||
return badRequest(reply, err.message);
|
||||
|
|
@ -263,6 +266,7 @@ export default async function botsRoutes(fastify) {
|
|||
updatedAt: nowKST(),
|
||||
}));
|
||||
|
||||
logActivity(db, { actor: 'admin', action: 'sync_complete', category: 'sync', targetType: null, targetId: null, summary: `전체 동기화: ${id} (${result.addedCount}개 추가)` });
|
||||
return {
|
||||
success: true,
|
||||
addedCount: result.addedCount,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { uploadConcertPoster, uploadConcertMerchandise } from '../../services/im
|
|||
import { CATEGORY_IDS } from '../../config/index.js';
|
||||
import { withTransaction } from '../../utils/transaction.js';
|
||||
import { badRequest, serverError } from '../../utils/error.js';
|
||||
import { logActivity } from '../../utils/log.js';
|
||||
|
||||
const CONCERT_CATEGORY_ID = CATEGORY_IDS.CONCERT;
|
||||
|
||||
|
|
@ -201,6 +202,7 @@ export default async function concertRoutes(fastify) {
|
|||
}
|
||||
}
|
||||
|
||||
logActivity(db, { actor: 'admin', action: 'create', category: 'concert', targetType: 'concert', targetId: result.seriesId, summary: `콘서트 일정 생성: ${title}` });
|
||||
return { success: true, seriesId: result.seriesId };
|
||||
} catch (err) {
|
||||
fastify.log.error(`콘서트 일정 저장 오류: ${err.message}`);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { errorResponse } from '../../schemas/index.js';
|
||||
import { badRequest, notFound, serverError } from '../../utils/error.js';
|
||||
import { fetchProfile } from '../../services/x/scraper.js';
|
||||
import { logActivity } from '../../utils/log.js';
|
||||
|
||||
/**
|
||||
* X 봇 스키마
|
||||
|
|
@ -241,6 +242,7 @@ export default async function xBotsRoutes(fastify) {
|
|||
|
||||
const [newBot] = await db.query('SELECT * FROM bot_x WHERE id = ?', [result.insertId]);
|
||||
reply.code(201);
|
||||
logActivity(db, { actor: 'admin', action: 'create', category: 'bot', targetType: 'x_bot', targetId: result.insertId, summary: `X 봇 생성: ${username}` });
|
||||
return formatBotResponse(newBot[0]);
|
||||
});
|
||||
|
||||
|
|
@ -341,6 +343,7 @@ export default async function xBotsRoutes(fastify) {
|
|||
}
|
||||
|
||||
const [updatedBot] = await db.query('SELECT * FROM bot_x WHERE id = ?', [id]);
|
||||
logActivity(db, { actor: 'admin', action: 'update', category: 'bot', targetType: 'x_bot', targetId: parseInt(id), summary: `X 봇 수정: ${existing[0].username}` });
|
||||
return formatBotResponse(updatedBot[0]);
|
||||
});
|
||||
|
||||
|
|
@ -388,6 +391,7 @@ export default async function xBotsRoutes(fastify) {
|
|||
// 스케줄러 캐시 무효화
|
||||
scheduler.invalidateCache();
|
||||
|
||||
logActivity(db, { actor: 'admin', action: 'delete', category: 'bot', targetType: 'x_bot', targetId: parseInt(id), summary: `X 봇 삭제: ${existing[0].username}` });
|
||||
return { success: true };
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
xScheduleCreate,
|
||||
} from '../../schemas/index.js';
|
||||
import { badRequest, conflict, serverError } from '../../utils/error.js';
|
||||
import { logActivity } from '../../utils/log.js';
|
||||
|
||||
const X_CATEGORY_ID = CATEGORY_IDS.X;
|
||||
const NITTER_URL = config.nitter?.url || process.env.NITTER_URL || 'http://nitter:8080';
|
||||
|
|
@ -153,6 +154,7 @@ export default async function xRoutes(fastify) {
|
|||
source_name: '',
|
||||
});
|
||||
|
||||
logActivity(db, { actor: 'admin', action: 'create', category: 'schedule', targetType: 'x_schedule', targetId: scheduleId, summary: `X 일정 생성: ${title}` });
|
||||
return { success: true, scheduleId };
|
||||
} catch (err) {
|
||||
fastify.log.error(`X 일정 저장 오류: ${err.message}`);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { errorResponse } from '../../schemas/index.js';
|
||||
import { badRequest, notFound, serverError } from '../../utils/error.js';
|
||||
import { getChannelByHandle } from '../../services/youtube/api.js';
|
||||
import { logActivity } from '../../utils/log.js';
|
||||
|
||||
/**
|
||||
* YouTube 봇 스키마
|
||||
|
|
@ -240,6 +241,7 @@ export default async function youtubeBotsRoutes(fastify) {
|
|||
|
||||
const [newBot] = await db.query('SELECT * FROM bot_youtube WHERE id = ?', [result.insertId]);
|
||||
reply.code(201);
|
||||
logActivity(db, { actor: 'admin', action: 'create', category: 'bot', targetType: 'youtube_bot', targetId: result.insertId, summary: `YouTube 봇 생성: ${channel_name}` });
|
||||
return formatBotResponse(newBot[0]);
|
||||
});
|
||||
|
||||
|
|
@ -348,6 +350,7 @@ export default async function youtubeBotsRoutes(fastify) {
|
|||
}
|
||||
|
||||
const [updatedBot] = await db.query('SELECT * FROM bot_youtube WHERE id = ?', [id]);
|
||||
logActivity(db, { actor: 'admin', action: 'update', category: 'bot', targetType: 'youtube_bot', targetId: parseInt(id), summary: `YouTube 봇 수정: ${existing[0].channel_name}` });
|
||||
return formatBotResponse(updatedBot[0]);
|
||||
});
|
||||
|
||||
|
|
@ -395,6 +398,7 @@ export default async function youtubeBotsRoutes(fastify) {
|
|||
// 스케줄러 캐시 무효화
|
||||
scheduler.invalidateCache();
|
||||
|
||||
logActivity(db, { actor: 'admin', action: 'delete', category: 'bot', targetType: 'youtube_bot', targetId: parseInt(id), summary: `YouTube 봇 삭제: ${existing[0].channel_name}` });
|
||||
return { success: true };
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
idParam,
|
||||
} from '../../schemas/index.js';
|
||||
import { badRequest, notFound, conflict, serverError } from '../../utils/error.js';
|
||||
import { logActivity } from '../../utils/log.js';
|
||||
|
||||
const YOUTUBE_CATEGORY_ID = CATEGORY_IDS.YOUTUBE;
|
||||
|
||||
|
|
@ -149,6 +150,7 @@ export default async function youtubeRoutes(fastify) {
|
|||
source_name: channelName || '',
|
||||
});
|
||||
|
||||
logActivity(db, { actor: 'admin', action: 'create', category: 'schedule', targetType: 'youtube_schedule', targetId: scheduleId, summary: `YouTube 일정 생성: ${title}` });
|
||||
return { success: true, scheduleId };
|
||||
} catch (err) {
|
||||
fastify.log.error(`YouTube 일정 저장 오류: ${err.message}`);
|
||||
|
|
@ -252,6 +254,7 @@ export default async function youtubeRoutes(fastify) {
|
|||
source_name: channelName,
|
||||
});
|
||||
|
||||
logActivity(db, { actor: 'admin', action: 'update', category: 'schedule', targetType: 'youtube_schedule', targetId: parseInt(id), summary: `YouTube 일정 수정: ${schedules[0].title}` });
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
fastify.log.error(`YouTube 일정 수정 오류: ${err.message}`);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import photosRoutes from './photos.js';
|
|||
import teasersRoutes from './teasers.js';
|
||||
import { errorResponse, successResponse, idParam } from '../../schemas/index.js';
|
||||
import { notFound, badRequest } from '../../utils/error.js';
|
||||
import { logActivity } from '../../utils/log.js';
|
||||
|
||||
/**
|
||||
* 앨범 라우트
|
||||
|
|
@ -203,6 +204,7 @@ export default async function albumsRoutes(fastify) {
|
|||
|
||||
const result = await createAlbum(db, data, coverBuffer);
|
||||
await invalidateAlbumCache(redis);
|
||||
logActivity(db, { actor: 'admin', action: 'create', category: 'album', targetType: 'album', targetId: result.albumId, summary: `앨범 생성: ${title}` });
|
||||
return result;
|
||||
});
|
||||
|
||||
|
|
@ -251,6 +253,7 @@ export default async function albumsRoutes(fastify) {
|
|||
return notFound(reply, '앨범을 찾을 수 없습니다.');
|
||||
}
|
||||
await invalidateAlbumCache(redis, id);
|
||||
logActivity(db, { actor: 'admin', action: 'update', category: 'album', targetType: 'album', targetId: parseInt(id), summary: `앨범 수정: ${data.title || id}` });
|
||||
return result;
|
||||
});
|
||||
|
||||
|
|
@ -277,6 +280,7 @@ export default async function albumsRoutes(fastify) {
|
|||
return notFound(reply, '앨범을 찾을 수 없습니다.');
|
||||
}
|
||||
await invalidateAlbumCache(redis, id);
|
||||
logActivity(db, { actor: 'admin', action: 'delete', category: 'album', targetType: 'album', targetId: parseInt(id), summary: `앨범 삭제: ${id}` });
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
} from '../../services/image.js';
|
||||
import { withTransaction } from '../../utils/transaction.js';
|
||||
import { notFound } from '../../utils/error.js';
|
||||
import { logActivity } from '../../utils/log.js';
|
||||
|
||||
/**
|
||||
* 앨범 사진 라우트
|
||||
|
|
@ -195,6 +196,8 @@ export default async function photosRoutes(fastify) {
|
|||
|
||||
await connection.commit();
|
||||
|
||||
logActivity(db, { actor: 'admin', action: 'upload', category: 'album', targetType: 'photo', targetId: parseInt(albumId), summary: `사진 업로드: ${uploadedPhotos.length}장 (앨범 ${albumId})` });
|
||||
|
||||
reply.raw.write(`data: ${JSON.stringify({
|
||||
done: true,
|
||||
message: `${uploadedPhotos.length}개의 사진이 업로드되었습니다.`,
|
||||
|
|
@ -245,6 +248,7 @@ export default async function photosRoutes(fastify) {
|
|||
await connection.query('DELETE FROM album_photo_members WHERE photo_id = ?', [photoId]);
|
||||
await connection.query('DELETE FROM album_photos WHERE id = ?', [photoId]);
|
||||
|
||||
logActivity(db, { actor: 'admin', action: 'delete', category: 'album', targetType: 'photo', targetId: parseInt(photoId), summary: `사진 삭제: 앨범 ${albumId}` });
|
||||
return { message: '사진이 삭제되었습니다.' };
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
} from '../../services/image.js';
|
||||
import { withTransaction } from '../../utils/transaction.js';
|
||||
import { notFound } from '../../utils/error.js';
|
||||
import { logActivity } from '../../utils/log.js';
|
||||
|
||||
/**
|
||||
* 앨범 티저 라우트
|
||||
|
|
@ -78,6 +79,7 @@ export default async function teasersRoutes(fastify) {
|
|||
|
||||
await connection.query('DELETE FROM album_teasers WHERE id = ?', [teaserId]);
|
||||
|
||||
logActivity(db, { actor: 'admin', action: 'delete', category: 'album', targetType: 'teaser', targetId: parseInt(teaserId), summary: `티저 삭제: 앨범 ${albumId}` });
|
||||
return { message: '티저가 삭제되었습니다.' };
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { uploadMemberImage } from '../../services/image.js';
|
||||
import { getAllMembers, getMemberByName, getMemberBasicByName, invalidateMemberCache } from '../../services/member.js';
|
||||
import { notFound, serverError } from '../../utils/error.js';
|
||||
import { logActivity } from '../../utils/log.js';
|
||||
|
||||
/**
|
||||
* 멤버 라우트
|
||||
|
|
@ -159,6 +160,7 @@ export default async function membersRoutes(fastify, opts) {
|
|||
// 멤버 캐시 무효화
|
||||
await invalidateMemberCache(redis);
|
||||
|
||||
logActivity(db, { actor: 'admin', action: 'update', category: 'member', targetType: 'member', targetId: memberId, summary: `멤버 수정: ${fields.name || decodedName}` });
|
||||
return { message: '멤버 정보가 수정되었습니다', id: memberId };
|
||||
} catch (err) {
|
||||
fastify.log.error(err);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
} from '../../schemas/index.js';
|
||||
import { badRequest, notFound, serverError } from '../../utils/error.js';
|
||||
import { withTransaction } from '../../utils/transaction.js';
|
||||
import { logActivity } from '../../utils/log.js';
|
||||
|
||||
export default async function schedulesRoutes(fastify) {
|
||||
const { db, meilisearch, redis } = fastify;
|
||||
|
|
@ -219,6 +220,7 @@ export default async function schedulesRoutes(fastify) {
|
|||
// Meilisearch에서도 삭제 (트랜잭션 외부, 실패해도 무시)
|
||||
await deleteSchedule(meilisearch, id);
|
||||
|
||||
logActivity(db, { actor: 'admin', action: 'delete', category: 'schedule', targetType: null, targetId: parseInt(id), summary: `일정 삭제: ${id}` });
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
fastify.log.error(err);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { readFile, writeFile } from 'fs/promises';
|
|||
import { SuggestionService } from '../../services/suggestions/index.js';
|
||||
import { reloadMorpheme, getUserDictPath } from '../../services/suggestions/morpheme.js';
|
||||
import { badRequest, serverError } from '../../utils/error.js';
|
||||
import { logActivity } from '../../utils/log.js';
|
||||
|
||||
let suggestionService = null;
|
||||
|
||||
|
|
@ -185,6 +186,7 @@ export default async function suggestionsRoutes(fastify) {
|
|||
// 형태소 분석기 리로드
|
||||
await reloadMorpheme();
|
||||
|
||||
logActivity(db, { actor: 'admin', action: 'update', category: 'dict', targetType: 'dict', targetId: null, summary: '사전 저장' });
|
||||
return { message: '사전이 저장되었습니다.' };
|
||||
} catch (error) {
|
||||
fastify.log.error(`[Suggestions] 사전 저장 오류: ${error.message}`);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { fetchVideoInfo } from '../youtube/api.js';
|
|||
import { formatDate, formatTime, nowKST } from '../../utils/date.js';
|
||||
import { withTransaction } from '../../utils/transaction.js';
|
||||
import { syncScheduleById } from '../meilisearch/index.js';
|
||||
import { logActivity } from '../../utils/log.js';
|
||||
|
||||
const X_CATEGORY_ID = 3;
|
||||
const YOUTUBE_CATEGORY_ID = 2;
|
||||
|
|
@ -191,6 +192,15 @@ async function xBotPlugin(fastify, opts) {
|
|||
if (scheduleId) {
|
||||
// Meilisearch 동기화
|
||||
await syncScheduleById(fastify.meilisearch, fastify.db, scheduleId);
|
||||
const title = extractTitle(tweet.text);
|
||||
logActivity(fastify.db, {
|
||||
actor: bot.id,
|
||||
action: 'create',
|
||||
category: 'schedule',
|
||||
targetType: 'x_schedule',
|
||||
targetId: scheduleId,
|
||||
summary: `X 트윗 추가: ${title}`,
|
||||
});
|
||||
addedCount++;
|
||||
// YouTube 링크 처리 (옵션이 켜져 있을 때만)
|
||||
if (bot.extractYoutube === true) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { fetchRecentVideoIds, fetchVideoInfo, fetchAllVideos } from './api.js';
|
|||
import { CATEGORY_IDS } from '../../config/index.js';
|
||||
import { withTransaction } from '../../utils/transaction.js';
|
||||
import { syncScheduleById, deleteSchedule } from '../meilisearch/index.js';
|
||||
import { logActivity } from '../../utils/log.js';
|
||||
|
||||
const YOUTUBE_CATEGORY_ID = CATEGORY_IDS.YOUTUBE;
|
||||
|
||||
|
|
@ -364,6 +365,14 @@ async function youtubeBotPlugin(fastify) {
|
|||
const scheduleId = await saveVideo(video, bot);
|
||||
if (scheduleId) {
|
||||
await syncScheduleById(fastify.meilisearch, fastify.db, scheduleId);
|
||||
logActivity(fastify.db, {
|
||||
actor: bot.id,
|
||||
action: 'create',
|
||||
category: 'schedule',
|
||||
targetType: 'youtube_schedule',
|
||||
targetId: scheduleId,
|
||||
summary: `YouTube 영상 추가: ${video.title}`,
|
||||
});
|
||||
addedCount++;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue