/** * Content Director Engine v2 * * Deterministic content planning agent that generates a 4-week editorial * calendar by combining channel strategies, content pillars, clinic services, * and existing assets. No AI API calls — all logic is data-driven. * * v2 changes (Audit-driven): * - Added 강남언니, TikTok, YouTube Live/Community slots * - Added Instagram Feed slot, Naver column format * - Split Facebook into organic/ad * - 5th Pillar: 안전·케어 * - Customer journey-based week themes * - Channel-specific tone matrix * - Keyword-driven topic generation */ import type { ChannelStrategyCard, ContentPillar, CalendarWeek, CalendarEntry, ContentCountSummary, ContentCategory, } from '../types/plan'; // ─── Input / Output Types ─── export interface ContentDirectorInput { channels: ChannelStrategyCard[]; pillars: ContentPillar[]; services: string[]; youtubeVideos?: { title: string; views: number; type: 'Short' | 'Long' }[]; clinicName: string; keywords?: { keyword: string; monthlySearches?: number }[]; gangnamUnniData?: { rating?: number; reviews?: number; doctors?: number }; } export interface ContentDirectorOutput { weeks: CalendarWeek[]; monthlySummary: ContentCountSummary[]; } // ─── Channel-Format Slot Definitions ─── interface FormatSlot { channel: string; channelIcon: string; format: string; contentType: ContentCategory; preferredDays: number[]; // 0=월 ~ 6=일 perWeek: number; titleTemplate: (topic: string, idx: number) => string; tone: string; journeyStage: 'awareness' | 'interest' | 'consideration' | 'conversion' | 'loyalty'; } // ─── YouTube: 4 formats ─── const YOUTUBE_SLOTS: FormatSlot[] = [ { channel: 'YouTube', channelIcon: 'youtube', format: 'Shorts', contentType: 'video', preferredDays: [0, 2, 4], perWeek: 3, titleTemplate: (t, i) => `Shorts: ${t} #${i + 1}`, tone: '캐주얼·후킹', journeyStage: 'awareness', }, { channel: 'YouTube', channelIcon: 'youtube', format: 'Long-form', contentType: 'video', preferredDays: [3], perWeek: 1, titleTemplate: (t) => `${t} 상세 설명`, tone: '교육적·권위', journeyStage: 'interest', }, { channel: 'YouTube', channelIcon: 'youtube', format: 'Live Q&A', contentType: 'video', preferredDays: [5], perWeek: 1, titleTemplate: (t) => `Live: ${t} 실시간 Q&A`, tone: '친근·대화', journeyStage: 'consideration', }, { channel: 'YouTube', channelIcon: 'youtube', format: 'Community', contentType: 'social', preferredDays: [1, 4], perWeek: 2, titleTemplate: (t) => `[커뮤니티] ${t} 투표/질문`, tone: '친근·참여유도', journeyStage: 'loyalty', }, ]; // ─── Instagram: 4 formats ─── const INSTAGRAM_SLOTS: FormatSlot[] = [ { channel: 'Instagram', channelIcon: 'instagram', format: 'Reel', contentType: 'video', preferredDays: [0, 2, 4], perWeek: 3, titleTemplate: (t, i) => `Reel: ${t} #${i + 1}`, tone: '트렌디·공감', journeyStage: 'awareness', }, { channel: 'Instagram', channelIcon: 'instagram', format: 'Carousel', contentType: 'social', preferredDays: [1, 4], perWeek: 2, titleTemplate: (t) => `Carousel: ${t}`, tone: '정보성·교육', journeyStage: 'interest', }, { channel: 'Instagram', channelIcon: 'instagram', format: 'Feed', contentType: 'social', preferredDays: [3], perWeek: 1, titleTemplate: (t) => `Feed: ${t} 포트폴리오`, tone: '감성적·프리미엄', journeyStage: 'consideration', }, { channel: 'Instagram', channelIcon: 'instagram', format: 'Stories', contentType: 'social', preferredDays: [2, 5], perWeek: 2, titleTemplate: (t) => `Stories: ${t}`, tone: '친근·일상', journeyStage: 'loyalty', }, ]; // ─── 강남언니: 3 formats (Audit C1) ─── const GANGNAMUNNI_SLOTS: FormatSlot[] = [ { channel: '강남언니', channelIcon: 'star', format: '리뷰관리', contentType: 'social', preferredDays: [1, 4], perWeek: 2, titleTemplate: (t) => `리뷰 응대: ${t}`, tone: '전문·응대', journeyStage: 'consideration', }, { channel: '강남언니', channelIcon: 'star', format: '프로필최적화', contentType: 'social', preferredDays: [0], perWeek: 1, titleTemplate: (t) => `프로필 업데이트: ${t}`, tone: '전문·신뢰', journeyStage: 'consideration', }, { channel: '강남언니', channelIcon: 'star', format: '이벤트/가격', contentType: 'ad', preferredDays: [3], perWeek: 1, titleTemplate: (t) => `이벤트: ${t}`, tone: '프로모션·CTA', journeyStage: 'conversion', }, ]; // ─── TikTok: 1 format (Audit C2) ─── const TIKTOK_SLOTS: FormatSlot[] = [ { channel: 'TikTok', channelIcon: 'video', format: '숏폼', contentType: 'video', preferredDays: [0, 2, 4], perWeek: 3, titleTemplate: (t, i) => `TikTok: ${t} #${i + 1}`, tone: '밈·교육', journeyStage: 'awareness', }, ]; // ─── 네이버 블로그: 2 formats (Audit H3) ─── const NAVER_SLOTS: FormatSlot[] = [ { channel: '네이버 블로그', channelIcon: 'blog', format: 'SEO글', contentType: 'blog', preferredDays: [1, 3], perWeek: 2, titleTemplate: (t) => `${t}`, tone: '정보성·SEO', journeyStage: 'interest', }, { channel: '네이버 블로그', channelIcon: 'blog', format: '의사칼럼', contentType: 'blog', preferredDays: [5], perWeek: 1, titleTemplate: (t) => `[칼럼] ${t}`, tone: '전문·교육', journeyStage: 'consideration', }, ]; // ─── Facebook: organic + ad (Audit H4) ─── const FACEBOOK_SLOTS: FormatSlot[] = [ { channel: 'Facebook', channelIcon: 'facebook', format: '오가닉', contentType: 'social', preferredDays: [2], perWeek: 1, titleTemplate: (t) => `${t}`, tone: '커뮤니티·공유', journeyStage: 'interest', }, { channel: 'Facebook', channelIcon: 'facebook', format: '광고', contentType: 'ad', preferredDays: [4, 6], perWeek: 2, titleTemplate: (t) => `광고: ${t}`, tone: '타겟팅·CTA', journeyStage: 'conversion', }, ]; // ─── Customer Journey-Based Week Themes (Audit C4) ─── const WEEK_THEMES = [ { label: 'Week 1: 인지 확대 & 브랜드 정비', pillarFocus: 0, // 전문성·신뢰 journeyFocus: 'awareness' as const, description: '신규 유입 극대화 — YouTube Shorts, TikTok, Instagram Reels 집중', }, { label: 'Week 2: 관심 유도 & 교육 콘텐츠', pillarFocus: 1, // 비포·애프터 journeyFocus: 'interest' as const, description: '정보 탐색 — Long-form, 블로그 SEO, Carousel 집중', }, { label: 'Week 3: 신뢰 구축 & 소셜 증거', pillarFocus: 2, // 환자 후기 journeyFocus: 'consideration' as const, description: '비교 검토 — 강남언니 리뷰, Before/After, 의사 소개 집중', }, { label: 'Week 4: 전환 최적화 & CTA', pillarFocus: 3, // 트렌드·교육 journeyFocus: 'conversion' as const, description: '상담 예약 유도 — Facebook 광고, Instagram DM CTA, 프로모션', }, ]; // ─── Topic Generation (with keyword injection — Audit C5) ─── interface TopicPool { topics: string[]; cursor: number; } function buildTopicPool( pillars: ContentPillar[], services: string[], pillarIndex: number, keywords?: { keyword: string; monthlySearches?: number }[], ): TopicPool { const pillar = pillars[pillarIndex % pillars.length]; const svcList = services.length > 0 ? services : ['시술']; const topics: string[] = []; // Inject high-volume keywords as topics (Audit C5) if (keywords?.length) { const topKeywords = keywords .sort((a, b) => (b.monthlySearches || 0) - (a.monthlySearches || 0)) .slice(0, 5) .map(k => k.keyword); for (const kw of topKeywords) { topics.push(kw); } } // Pillar-specific templates switch (pillarIndex % 5) { case 0: // 전문성·신뢰 for (const svc of svcList) { topics.push(`${svc} 전문의 Q&A`); topics.push(`${svc} 과정 완전정복`); topics.push(`${svc} 수술실 CCTV 공개`); } topics.push('의료진 학회 발표·해외 연수'); topics.push('최신 장비 도입 소개'); topics.push('마취 안전 관리 시스템'); break; case 1: // 비포·애프터 for (const svc of svcList) { topics.push(`${svc} 전후 비교`); topics.push(`${svc} Before/After 타임랩스`); topics.push(`${svc} 3D 시뮬레이션 미리보기`); } topics.push('자연스러운 라인 완성'); topics.push('시간별 회복 과정 기록'); break; case 2: // 환자 후기 for (const svc of svcList) { topics.push(`${svc} 실제 환자 인터뷰`); topics.push(`${svc} 회복 일기`); } topics.push('해외 환자 케이스'); topics.push('환자가 말하는: 왜 여기를 선택했나'); topics.push('리뷰 하이라이트 모음'); break; case 3: // 트렌드·교육 for (const svc of svcList) { topics.push(`${svc} 비용 완벽 가이드`); topics.push(`${svc} FAQ 총정리`); } topics.push('2026 성형 트렌드: 자연주의'); topics.push('성형 전 필수 체크리스트'); topics.push('시술 비교: 이걸로 고민 끝'); break; case 4: // 안전·케어 (5th Pillar — Audit H5) for (const svc of svcList) { topics.push(`${svc} 수술 후 관리 가이드`); } topics.push('24시간 집중 케어 시스템'); topics.push('전담 간호사 1:1 관리'); topics.push('리커버리 프로그램 소개'); topics.push('응급 상황 대응 프로토콜'); break; } // Add pillar example topics as fallback for (const t of pillar?.exampleTopics || []) { if (!topics.includes(t)) topics.push(t); } return { topics, cursor: 0 }; } function nextTopic(pool: TopicPool): string { const topic = pool.topics[pool.cursor % pool.topics.length]; pool.cursor++; return topic; } // ─── Repurpose Topics from YouTube Videos ─── function buildRepurposeTopics( videos: { title: string; views: number; type: 'Short' | 'Long' }[], ): string[] { return videos .sort((a, b) => b.views - a.views) .slice(0, 6) .map(v => v.title.replace(/[||\-–—].*$/, '').trim()) .filter(t => t.length > 2); } // ─── 강남언니 Special Topics ─── function buildGangnamUnniTopics( services: string[], data?: { rating?: number; reviews?: number; doctors?: number }, ): string[] { const topics: string[] = []; const svcList = services.length > 0 ? services : ['시술']; // Review management topics topics.push('신규 리뷰 감사 응대'); topics.push('부정 리뷰 전문 응대'); // Profile optimization for (const svc of svcList.slice(0, 3)) { topics.push(`${svc} 시술 정보 업데이트`); } topics.push('의사 프로필 사진·경력 갱신'); topics.push('대표 시술 가격 재검토'); // Data-driven topics if (data?.rating && data.rating < 9.0) { topics.push('평점 개선 액션 플랜'); } if (data?.reviews && data.reviews < 500) { topics.push('리뷰 수 증가 캠페인'); } // Events for (const svc of svcList.slice(0, 2)) { topics.push(`${svc} 시즌 이벤트`); } return topics; } // ─── Main Engine ─── export function generateContentPlan(input: ContentDirectorInput): ContentDirectorOutput { const { channels, pillars, services, youtubeVideos, clinicName, keywords, gangnamUnniData } = input; // 1. Determine active format slots based on available channels const activeSlots: FormatSlot[] = []; const channelIds = new Set(channels.map(c => c.channelId.toLowerCase())); if (channelIds.has('youtube')) activeSlots.push(...YOUTUBE_SLOTS); if (channelIds.has('instagram')) activeSlots.push(...INSTAGRAM_SLOTS); if (channelIds.has('gangnamunni') || channelIds.has('gangnamUnni')) activeSlots.push(...GANGNAMUNNI_SLOTS); if (channelIds.has('tiktok')) activeSlots.push(...TIKTOK_SLOTS); if (channelIds.has('naverblog') || channelIds.has('naver_blog')) activeSlots.push(...NAVER_SLOTS); if (channelIds.has('facebook')) activeSlots.push(...FACEBOOK_SLOTS); // Fallback: if no channels matched, use core 3 channels if (activeSlots.length === 0) { activeSlots.push(...YOUTUBE_SLOTS, ...INSTAGRAM_SLOTS, ...NAVER_SLOTS); } // 2. Build repurpose topics from existing YouTube videos const repurposeTopics = youtubeVideos ? buildRepurposeTopics(youtubeVideos) : []; // 3. Build 강남언니 special topic pool const guTopics = buildGangnamUnniTopics(services, gangnamUnniData); let guCursor = 0; // 4. Generate 4 weeks of content const weeks: CalendarWeek[] = []; for (let weekIdx = 0; weekIdx < 4; weekIdx++) { const theme = WEEK_THEMES[weekIdx]; // Build topic pools focused on this week's pillar + 5th pillar rotation const primaryPool = buildTopicPool(pillars, services, theme.pillarFocus, keywords); const secondaryPool = buildTopicPool(pillars, services, (theme.pillarFocus + 1) % 5, keywords); // 5th pillar (안전·케어) injected every other week const carePool = weekIdx % 2 === 1 ? buildTopicPool(pillars, services, 4, keywords) : null; const entries: CalendarEntry[] = []; // Week 1 special: brand setup tasks if (weekIdx === 0) { entries.push({ dayOfWeek: 0, channel: clinicName, channelIcon: 'globe', contentType: 'social', title: '전 채널 프로필/브랜드 일관성 정비', description: '프로필 사진, 배너, 바이오를 모든 채널에서 통일', pillar: '전문성 · 신뢰', status: 'draft', }); } // Fill format slots for this week for (const slot of activeSlots) { // Use care pool occasionally const pool = carePool && entries.length % 5 === 0 ? carePool : entries.length % 2 === 0 ? primaryPool : secondaryPool; for (let i = 0; i < slot.perWeek; i++) { let topic: string; // 강남언니: use dedicated topic pool if (slot.channel === '강남언니') { topic = guTopics[guCursor % guTopics.length]; guCursor++; } // Repurpose popular YouTube content for Shorts/TikTok in weeks 2-4 else if (weekIdx >= 1 && repurposeTopics.length > 0 && i === 0 && (slot.format === 'Shorts' || slot.format === '숏폼')) { const rIdx = (weekIdx - 1 + i) % repurposeTopics.length; topic = repurposeTopics[rIdx]; } else { topic = nextTopic(pool); } const dayOfWeek = slot.preferredDays[i % slot.preferredDays.length]; entries.push({ dayOfWeek, channel: slot.channel, channelIcon: slot.channelIcon, contentType: slot.contentType, title: slot.titleTemplate(topic, i), pillar: pillars[theme.pillarFocus % pillars.length]?.title, status: 'draft', }); } } // Week 3 special: 강남언니 리뷰 집중 (소셜 증거 강화) if (weekIdx === 2 && !channelIds.has('gangnamunni') && !channelIds.has('gangnamUnni')) { entries.push({ dayOfWeek: 3, channel: '강남언니', channelIcon: 'star', contentType: 'social', title: '강남언니 프로필 개설/최적화', description: '아직 강남언니 계정이 없다면 개설, 있다면 프로필 최적화', pillar: '환자 후기 · 리뷰', status: 'draft', }); } // Week 4 special: conversion-focused entries if (weekIdx === 3) { entries.push({ dayOfWeek: 5, channel: 'Instagram', channelIcon: 'instagram', contentType: 'social', title: 'Stories: 상담 예약 CTA + 카카오톡 링크', description: 'DM→카카오톡→전화 전환 퍼널 활성화', pillar: '트렌드 · 교육', status: 'draft', }); entries.push({ dayOfWeek: 6, channel: 'Facebook', channelIcon: 'facebook', contentType: 'ad', title: '광고: 리타겟팅 — 웹사이트 방문자 재유입', description: '최근 30일 웹사이트 방문자 대상 리타겟팅 광고', pillar: '트렌드 · 교육', status: 'draft', }); } // Sort entries by day of week entries.sort((a, b) => a.dayOfWeek - b.dayOfWeek); weeks.push({ weekNumber: weekIdx + 1, label: theme.label, entries, }); } // 5. Calculate monthly summary const allEntries = weeks.flatMap(w => w.entries); const monthlySummary: ContentCountSummary[] = [ { type: 'video', label: '영상', count: allEntries.filter(e => e.contentType === 'video').length, color: '#6C5CE7', }, { type: 'blog', label: '블로그', count: allEntries.filter(e => e.contentType === 'blog').length, color: '#00B894', }, { type: 'social', label: '소셜', count: allEntries.filter(e => e.contentType === 'social').length, color: '#E17055', }, { type: 'ad', label: '광고', count: allEntries.filter(e => e.contentType === 'ad').length, color: '#FDCB6E', }, ]; return { weeks, monthlySummary }; }