From 48ed3bb9e00461fae693b4a6a151402ec6ff0fa8 Mon Sep 17 00:00:00 2001 From: caadiq Date: Sun, 5 Apr 2026 13:37:52 +0900 Subject: [PATCH] =?UTF-8?q?feat(variety):=20=EB=B0=A9=EC=86=A1=EC=82=AC=20?= =?UTF-8?q?=ED=94=84=EB=A6=AC=EC=85=8B=EC=9D=84=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EB=B9=88=EB=8F=84=EC=88=98=20=EA=B8=B0=EB=B0=98=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /admin/variety/broadcasters: DB에서 빈도수 상위 10개 조회 (Redis 1시간 캐시) - 일정 생성/수정 시 캐시 무효화 - 프론트엔드: 하드코딩 프리셋 제거, API에서 동적으로 로드 Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/src/routes/admin/variety.js | 37 ++++++++++++++++++- frontend/src/api/admin/variety.js | 7 ++++ .../admin/schedules/edit/VarietyEditForm.jsx | 13 ++++--- .../pc/admin/schedules/form/VarietyForm.jsx | 14 ++++--- 4 files changed, 59 insertions(+), 12 deletions(-) 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) => {