feat(variety): 방송사 프리셋을 입력 빈도수 기반으로 변경

- GET /admin/variety/broadcasters: DB에서 빈도수 상위 10개 조회 (Redis 1시간 캐시)
- 일정 생성/수정 시 캐시 무효화
- 프론트엔드: 하드코딩 프리셋 제거, API에서 동적으로 로드

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-04-05 13:37:52 +09:00
parent a01d368728
commit 48ed3bb9e0
4 changed files with 59 additions and 12 deletions

View file

@ -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) {

View file

@ -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');
}

View file

@ -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("");

View file

@ -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) => {