From d1ee28e30813ab81e66a86e06d58de945656eeed Mon Sep 17 00:00:00 2001 From: caadiq Date: Fri, 12 Jun 2026 14:02:24 +0900 Subject: [PATCH] =?UTF-8?q?refactor(festival-bot):=20active=5Fmonths=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 축제봇이 대학+일반 행사를 통합 수집하게 되면서 연중 실행이 필요해 활성 월 제한 기능 제거. 스케줄러 게이트/라우트/유틸/DB 컬럼/다이얼로그 월 선택 UI/'대기 중' 상태 표시 모두 정리. 동기화 간격만 유지. Co-Authored-By: Claude Opus 4.7 --- backend/sql/bot_active_months.sql | 6 -- backend/src/plugins/scheduler.js | 23 +------- backend/src/routes/admin/bots.js | 2 - backend/src/routes/admin/festival-bots.js | 17 ++---- backend/src/utils/botMonths.js | 32 ----------- backend/src/utils/date.js | 7 --- .../pc/admin/bot/FestivalBotDialog.jsx | 57 +------------------ .../pages/pc/admin/schedules/ScheduleBots.jsx | 20 +------ 8 files changed, 9 insertions(+), 155 deletions(-) delete mode 100644 backend/sql/bot_active_months.sql delete mode 100644 backend/src/utils/botMonths.js diff --git a/backend/sql/bot_active_months.sql b/backend/sql/bot_active_months.sql deleted file mode 100644 index 469aebb..0000000 --- a/backend/sql/bot_active_months.sql +++ /dev/null @@ -1,6 +0,0 @@ --- 축제 봇 실행 활성 월 (시즌성 대응) --- active_months: JSON 정수 배열 (예: [4,5,8,9]) — 해당 월에만 동기화 실행 --- NULL 또는 12개 전체 = 모든 월 실행 (제한 없음) --- 대학 축제는 학기 중 한정 기간에만 열리므로, 비시즌에 불필요한 --- 크롤링/Gemini 호출(RPD 제한)을 막기 위해 축제 봇에만 적용. -ALTER TABLE bot_festival ADD COLUMN active_months JSON DEFAULT NULL AFTER cron_interval; diff --git a/backend/src/plugins/scheduler.js b/backend/src/plugins/scheduler.js index 3a03e2e..47bb244 100644 --- a/backend/src/plugins/scheduler.js +++ b/backend/src/plugins/scheduler.js @@ -2,24 +2,13 @@ import fp from 'fastify-plugin'; import cron from 'node-cron'; import staticBots from '../config/bots.js'; import { syncAllSchedules } from '../services/meilisearch/index.js'; -import { nowKST, monthKST } from '../utils/date.js'; -import { parseActiveMonths } from '../utils/botMonths.js'; +import { nowKST } from '../utils/date.js'; import { logActivity } from '../utils/log.js'; const REDIS_PREFIX = 'bot:status:'; const TIMEZONE = 'Asia/Seoul'; const MAX_CONSECUTIVE_ERRORS = 10; -/** - * 현재 KST 월이 봇의 활성 월에 포함되는지 - * - activeMonths가 null/빈배열/12개 전체 → 항상 실행 - */ -function isActiveMonth(bot) { - const months = bot.activeMonths; - if (!months || months.length === 0 || months.length >= 12) return true; - return months.includes(monthKST()); -} - /** * DB JSON 컬럼 안전 파싱. 깨진 값 하나가 getAllBots 전체를 막지 않도록 fallback 반환. */ @@ -145,7 +134,6 @@ async function schedulerPlugin(fastify, opts) { cron: intervalToCron(row.cron_interval), cronInterval: row.cron_interval, enabled: row.enabled === 1, - activeMonths: parseActiveMonths(row.active_months), })); } @@ -379,11 +367,6 @@ async function schedulerPlugin(fastify, opts) { // cron 태스크 등록 (한국 시간 기준) const task = cron.schedule(bot.cron, async () => { - // 활성 월이 아니면 스킵 (매 실행 시점의 현재 월로 재평가) - if (!isActiveMonth(bot)) { - fastify.log.info(`[${botId}] 비활성 월(${monthKST()}월) - 동기화 스킵`); - return; - } fastify.log.info(`[${botId}] 동기화 시작`); if (bot.weeklySchedule) { await startWeeklyBurst(botId, bot, syncFn); @@ -396,8 +379,8 @@ async function schedulerPlugin(fastify, opts) { await updateStatus(botId, { status: 'running' }); fastify.log.info(`[${botId}] 스케줄 시작 (cron: ${bot.cron})`); - // 즉시 1회 실행: meilisearch와 weekly 모드는 제외 (weekly는 지정 시각에만), 비활성 월도 제외 - if (bot.type !== 'meilisearch' && !bot.weeklySchedule && isActiveMonth(bot)) { + // 즉시 1회 실행: meilisearch와 weekly 모드는 제외 (weekly는 지정 시각에만) + if (bot.type !== 'meilisearch' && !bot.weeklySchedule) { await runSync(botId, bot, syncFn, { setRunningStatus: false }); } } diff --git a/backend/src/routes/admin/bots.js b/backend/src/routes/admin/bots.js index dbd9615..2e6bf6e 100644 --- a/backend/src/routes/admin/bots.js +++ b/backend/src/routes/admin/bots.js @@ -37,7 +37,6 @@ const botResponse = { text_filters: { type: 'array', items: { type: 'string' } }, // 축제 봇 전용 필드 search_url: { type: 'string' }, - active_months: { type: ['array', 'null'], items: { type: 'integer' } }, }, }; @@ -145,7 +144,6 @@ export default async function botsRoutes(fastify) { botData.db_id = bot.dbId; botData.search_url = bot.searchUrl; botData.cron_interval = checkInterval; - botData.active_months = bot.activeMonths || null; } result.push(botData); diff --git a/backend/src/routes/admin/festival-bots.js b/backend/src/routes/admin/festival-bots.js index 10fbc16..671a4ca 100644 --- a/backend/src/routes/admin/festival-bots.js +++ b/backend/src/routes/admin/festival-bots.js @@ -1,7 +1,6 @@ import { errorResponse } from '../../schemas/index.js'; import { badRequest, notFound } from '../../utils/error.js'; import { logActivity } from '../../utils/log.js'; -import { parseActiveMonths, serializeActiveMonths } from '../../utils/botMonths.js'; /** * 축제 봇 응답 스키마 @@ -13,7 +12,6 @@ const festivalBotResponse = { name: { type: 'string' }, search_url: { type: 'string' }, cron_interval: { type: 'integer' }, - active_months: { type: ['array', 'null'], items: { type: 'integer' } }, enabled: { type: 'boolean' }, }, }; @@ -35,7 +33,6 @@ function formatBotResponse(row) { name: row.name, search_url: row.search_url, cron_interval: row.cron_interval, - active_months: parseActiveMonths(row.active_months), enabled: row.enabled === 1, }; } @@ -100,7 +97,6 @@ export default async function festivalBotsRoutes(fastify) { name: { type: 'string' }, search_url: { type: 'string' }, cron_interval: { type: 'integer', default: 360 }, - active_months: { type: ['array', 'null'], items: { type: 'integer' } }, }, required: ['name', 'search_url'], }, @@ -108,16 +104,16 @@ export default async function festivalBotsRoutes(fastify) { }, preHandler: [fastify.authenticate], }, async (request, reply) => { - const { name, search_url, cron_interval = 360, active_months } = request.body; + const { name, search_url, cron_interval = 360 } = request.body; if (!name?.trim() || !search_url?.trim()) { return badRequest(reply, '이름과 크롤링 URL은 필수입니다.'); } const [result] = await db.query( - `INSERT INTO bot_festival (name, search_url, cron_interval, active_months, enabled) - VALUES (?, ?, ?, ?, 1)`, - [name.trim(), search_url.trim(), cron_interval, serializeActiveMonths(active_months)] + `INSERT INTO bot_festival (name, search_url, cron_interval, enabled) + VALUES (?, ?, ?, 1)`, + [name.trim(), search_url.trim(), cron_interval] ); scheduler.invalidateCache(); @@ -156,7 +152,6 @@ export default async function festivalBotsRoutes(fastify) { name: { type: 'string' }, search_url: { type: 'string' }, cron_interval: { type: 'integer' }, - active_months: { type: ['array', 'null'], items: { type: 'integer' } }, enabled: { type: 'boolean' }, }, }, @@ -186,10 +181,6 @@ export default async function festivalBotsRoutes(fastify) { fields.push('cron_interval = ?'); values.push(updates.cron_interval); } - if (updates.active_months !== undefined) { - fields.push('active_months = ?'); - values.push(serializeActiveMonths(updates.active_months)); - } if (updates.enabled !== undefined) { fields.push('enabled = ?'); values.push(updates.enabled ? 1 : 0); diff --git a/backend/src/utils/botMonths.js b/backend/src/utils/botMonths.js deleted file mode 100644 index cf09930..0000000 --- a/backend/src/utils/botMonths.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * 봇 활성 월(active_months) 파싱/직렬화 유틸 - * - 저장 형식: JSON 정수 배열 (예: [4,5,8,9]) 또는 NULL(전체 월 = 항상 실행) - */ - -/** - * DB 값(JSON 문자열/배열) → 정수 배열 또는 null - */ -export function parseActiveMonths(raw) { - if (!raw) return null; - try { - const arr = typeof raw === 'string' ? JSON.parse(raw) : raw; - if (!Array.isArray(arr) || arr.length === 0) return null; - const months = arr.map(Number).filter((m) => m >= 1 && m <= 12); - return months.length > 0 ? months : null; - } catch { - return null; - } -} - -/** - * 입력 배열 → DB 저장용 JSON 문자열 또는 null - * - 빈 배열 / 12개 전체 선택은 NULL(제한 없음)로 정규화 - */ -export function serializeActiveMonths(input) { - if (!Array.isArray(input)) return null; - const months = [...new Set(input.map(Number).filter((m) => m >= 1 && m <= 12))].sort( - (a, b) => a - b - ); - if (months.length === 0 || months.length >= 12) return null; - return JSON.stringify(months); -} diff --git a/backend/src/utils/date.js b/backend/src/utils/date.js index 21db4ae..da86c6e 100644 --- a/backend/src/utils/date.js +++ b/backend/src/utils/date.js @@ -36,13 +36,6 @@ export function nowKST() { return dayjs().tz(KST).format(); } -/** - * 현재 KST 기준 월 반환 (1~12) - */ -export function monthKST() { - return dayjs().tz(KST).month() + 1; -} - /** * Nitter 날짜 문자열 파싱 * 예: "Jan 15, 2026 · 10:30 PM UTC" diff --git a/frontend/src/components/pc/admin/bot/FestivalBotDialog.jsx b/frontend/src/components/pc/admin/bot/FestivalBotDialog.jsx index 8b68d60..dabe736 100644 --- a/frontend/src/components/pc/admin/bot/FestivalBotDialog.jsx +++ b/frontend/src/components/pc/admin/bot/FestivalBotDialog.jsx @@ -17,8 +17,6 @@ const INTERVAL_OPTIONS = [ { value: 1440, label: '24시간' }, ]; -const ALL_MONTHS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; - /** * 커스텀 드롭다운 (Portal 사용) */ @@ -120,17 +118,8 @@ function FestivalBotDialog({ isOpen, onClose, botId = null, onSuccess }) { const [name, setName] = useState(''); const [searchUrl, setSearchUrl] = useState(''); const [interval, setInterval] = useState(360); - const [activeMonths, setActiveMonths] = useState(ALL_MONTHS); const [submitting, setSubmitting] = useState(false); - const allMonthsSelected = activeMonths.length === ALL_MONTHS.length; - const toggleMonth = (m) => - setActiveMonths((prev) => - prev.includes(m) ? prev.filter((x) => x !== m) : [...prev, m].sort((a, b) => a - b) - ); - const toggleAllMonths = () => - setActiveMonths(allMonthsSelected ? [] : [...ALL_MONTHS]); - // 축제 봇 상세 조회 (수정 모드) const { data: bot, isLoading: botLoading } = useQuery({ queryKey: ['admin', 'festival-bot', botId], @@ -148,18 +137,11 @@ function FestivalBotDialog({ isOpen, onClose, botId = null, onSuccess }) { setName(bot.name || ''); setSearchUrl(bot.search_url || ''); setInterval(bot.cron_interval || 360); - // active_months: null(전체) → 전체 선택, 배열이면 그대로 - setActiveMonths( - Array.isArray(bot.active_months) && bot.active_months.length > 0 - ? bot.active_months - : [...ALL_MONTHS] - ); } else if (!botId) { - // 추가 모드 (기본: 전체 월 = 항상 실행) + // 추가 모드 setName(''); setSearchUrl(''); setInterval(360); - setActiveMonths([...ALL_MONTHS]); } }, [isOpen, bot, botId]); @@ -174,8 +156,6 @@ function FestivalBotDialog({ isOpen, onClose, botId = null, onSuccess }) { name: name.trim(), search_url: searchUrl.trim(), cron_interval: interval, - // 전체 선택(또는 미선택)은 제한 없음(null)으로 저장 - active_months: allMonthsSelected ? null : activeMonths, }; if (isEdit) { @@ -282,41 +262,6 @@ function FestivalBotDialog({ isOpen, onClose, botId = null, onSuccess }) { /> - {/* 활성 월 */} -
-
- - -
-
- {ALL_MONTHS.map((m) => { - const on = activeMonths.includes(m); - return ( - - ); - })} -
-

- 선택한 월에만 봇이 실행됩니다. 전체 선택 시 항상 실행됩니다. -

-
)} diff --git a/frontend/src/pages/pc/admin/schedules/ScheduleBots.jsx b/frontend/src/pages/pc/admin/schedules/ScheduleBots.jsx index 8ac2878..8bff2e7 100644 --- a/frontend/src/pages/pc/admin/schedules/ScheduleBots.jsx +++ b/frontend/src/pages/pc/admin/schedules/ScheduleBots.jsx @@ -2,7 +2,7 @@ import { useState, useEffect, useMemo } from 'react'; import { Link } from 'react-router-dom'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { motion, AnimatePresence } from 'framer-motion'; -import { Home, ChevronRight, Bot, CheckCircle, XCircle, RefreshCw, Plus, Youtube, PartyPopper, Clock } from 'lucide-react'; +import { Home, ChevronRight, Bot, CheckCircle, XCircle, RefreshCw, Plus, Youtube, PartyPopper } from 'lucide-react'; import { Toast, Tooltip, AnimatedNumber } from '@/components/common'; import { AdminLayout, XIcon, MeilisearchIcon, BotTableRow, BotTable, YouTubeBotDialog, XBotDialog, FestivalBotDialog } from '@/components/pc/admin'; import { useAdminAuth } from '@/hooks/pc/admin'; @@ -191,26 +191,8 @@ function ScheduleBots() { }; // 상태 아이콘 및 색상 - // 봇이 켜져 있지만 현재 달이 활성 월이 아니면(축제 봇) 이번 달은 동기화하지 않음 - const isInactiveMonth = (bot) => { - const months = bot?.active_months; - if (!Array.isArray(months) || months.length === 0 || months.length >= 12) return false; - const currentMonth = new Date().getMonth() + 1; - return !months.includes(currentMonth); - }; - const getStatusInfo = (bot) => { const status = bot?.status; - // 실행 중이지만 비활성 월 → 대기 중 - if (status === 'running' && isInactiveMonth(bot)) { - return { - icon: , - text: '대기 중', - color: 'text-amber-500', - bg: 'bg-amber-50', - dot: 'bg-amber-400', - }; - } switch (status) { case 'running': return {