diff --git a/backend/src/routes/admin/variety.js b/backend/src/routes/admin/variety.js index 27c2f4c..63e1439 100644 --- a/backend/src/routes/admin/variety.js +++ b/backend/src/routes/admin/variety.js @@ -5,12 +5,43 @@ import { badRequest, notFound, serverError } from '../../utils/error.js'; import { logActivity } from '../../utils/log.js'; const VARIETY_CATEGORY_ID = CATEGORY_IDS.VARIETY; +const BROADCASTER_KEY = 'variety:broadcasters'; /** * 예능 관련 관리자 라우트 */ export default async function varietyRoutes(fastify) { - const { db, meilisearch } = fastify; + const { db, meilisearch, redis } = fastify; + + /** + * GET /api/admin/variety/broadcasters + * 자주 사용된 방송사/플랫폼 목록 (상위 10개) + */ + fastify.get('/broadcasters', { + preHandler: [fastify.authenticate], + }, async () => { + // Redis에 캐시가 있으면 사용 + const cached = await redis.get(BROADCASTER_KEY); + if (cached) { + return JSON.parse(cached); + } + + // DB에서 빈도수 조회 + const [rows] = await db.query( + `SELECT broadcaster, COUNT(*) as cnt + FROM schedule_variety + GROUP BY broadcaster + ORDER BY cnt DESC + LIMIT 10` + ); + + const broadcasters = rows.map(r => r.broadcaster); + + // Redis 캐시 (1시간) + await redis.setex(BROADCASTER_KEY, 3600, JSON.stringify(broadcasters)); + + return broadcasters; + }); /** * POST /api/admin/variety/schedule @@ -96,6 +127,9 @@ export default async function varietyRoutes(fastify) { member_names: memberNames, }); + // 방송사 캐시 무효화 + await redis.del(BROADCASTER_KEY); + logActivity(db, { actor: 'admin', action: 'create', category: 'schedule', targetType: 'variety_schedule', targetId: scheduleId, summary: `예능 일정 생성: ${title.trim()}` }); return { success: true, scheduleId }; } catch (err) { @@ -181,6 +215,7 @@ export default async function varietyRoutes(fastify) { } await syncScheduleById(meilisearch, db, parseInt(id)); + await redis.del(BROADCASTER_KEY); logActivity(db, { actor: 'admin', action: 'update', category: 'schedule', targetType: 'variety_schedule', targetId: parseInt(id), summary: `예능 일정 수정: ${title.trim()}` }); return { success: true }; } catch (err) { diff --git a/frontend/src/api/admin/variety.js b/frontend/src/api/admin/variety.js index 4b44e9e..df9d418 100644 --- a/frontend/src/api/admin/variety.js +++ b/frontend/src/api/admin/variety.js @@ -23,3 +23,10 @@ export async function getVarietySchedule(id) { export async function updateVarietySchedule(id, formData) { return fetchFormData(`/admin/variety/schedule/${id}`, formData, 'PUT'); } + +/** + * 자주 사용된 방송사/플랫폼 목록 + */ +export async function getBroadcasters() { + return fetchAuthApi('/admin/variety/broadcasters'); +} diff --git a/frontend/src/pages/pc/admin/schedules/edit/VarietyEditForm.jsx b/frontend/src/pages/pc/admin/schedules/edit/VarietyEditForm.jsx index 6270f36..a959f3f 100644 --- a/frontend/src/pages/pc/admin/schedules/edit/VarietyEditForm.jsx +++ b/frontend/src/pages/pc/admin/schedules/edit/VarietyEditForm.jsx @@ -11,12 +11,8 @@ import Toast from "@/components/common/Toast"; import { useToast } from "@/hooks/common"; import { useAdminAuth } from "@/hooks/pc/admin"; import { getMembers } from "@/api/public/members"; -import { getVarietySchedule, updateVarietySchedule } from "@/api/admin/variety"; +import { getVarietySchedule, updateVarietySchedule, getBroadcasters } from "@/api/admin/variety"; -const broadcasterPresets = [ - "KBS", "MBC", "SBS", "tvN", "JTBC", - "Mnet", "유튜브", "티빙", "웨이브", "쿠팡플레이", -]; /** * 예능 일정 수정 폼 @@ -41,6 +37,13 @@ function VarietyEditForm() { enabled: isAuthenticated && !!id, }); + const { data: broadcasterPresets = [] } = useQuery({ + queryKey: ["broadcasters"], + queryFn: getBroadcasters, + enabled: isAuthenticated, + staleTime: 5 * 60 * 1000, + }); + const [title, setTitle] = useState(""); const [broadcaster, setBroadcaster] = useState(""); const [date, setDate] = useState(""); diff --git a/frontend/src/pages/pc/admin/schedules/form/VarietyForm.jsx b/frontend/src/pages/pc/admin/schedules/form/VarietyForm.jsx index 9ed5c88..df1c505 100644 --- a/frontend/src/pages/pc/admin/schedules/form/VarietyForm.jsx +++ b/frontend/src/pages/pc/admin/schedules/form/VarietyForm.jsx @@ -10,7 +10,7 @@ import TimePicker from "@/components/pc/admin/common/TimePicker"; import { useToast } from "@/hooks/common"; import { useAdminAuth } from "@/hooks/pc/admin"; import { getMembers } from "@/api/public/members"; -import { createVarietySchedule } from "@/api/admin/variety"; +import { createVarietySchedule, getBroadcasters } from "@/api/admin/variety"; /** * 예능 일정 추가 폼 @@ -40,11 +40,13 @@ function VarietyForm() { const [selectedMemberIds, setSelectedMemberIds] = useState([]); const [saving, setSaving] = useState(false); - // 방송사 프리셋 - const broadcasterPresets = [ - "KBS", "MBC", "SBS", "tvN", "JTBC", - "Mnet", "유튜브", "티빙", "웨이브", "쿠팡플레이", - ]; + // 자주 사용된 방송사 목록 + const { data: broadcasterPresets = [] } = useQuery({ + queryKey: ["broadcasters"], + queryFn: getBroadcasters, + enabled: isAuthenticated, + staleTime: 5 * 60 * 1000, + }); // 멤버 토글 const toggleMember = (memberId) => {