-
📊
+
+
+
아직 성과 데이터가 없습니다
채널 분석을 2회 이상 실행하면 성과 비교 및 전략 조정이 가능합니다
diff --git a/src/components/plan/WorkflowTracker.tsx b/src/components/plan/WorkflowTracker.tsx
new file mode 100644
index 0000000..96e5c21
--- /dev/null
+++ b/src/components/plan/WorkflowTracker.tsx
@@ -0,0 +1,389 @@
+import { useState, useCallback } from 'react';
+import { motion, AnimatePresence } from 'motion/react';
+import {
+ YoutubeFilled,
+ InstagramFilled,
+ GlobeFilled,
+ VideoFilled,
+ TiktokFilled,
+ BoltFilled,
+ CheckFilled,
+ CalendarFilled,
+ FileTextFilled,
+ ShareFilled,
+} from '../icons/FilledIcons';
+import { SectionWrapper } from '../report/ui/SectionWrapper';
+import type { WorkflowData, WorkflowItem, WorkflowStage, WorkflowContentType } from '../../types/plan';
+
+interface WorkflowTrackerProps {
+ data: WorkflowData;
+}
+
+const STAGES: { key: WorkflowStage; label: string; short: string }[] = [
+ { key: 'planning', label: '기획 확정', short: '기획' },
+ { key: 'ai-draft', label: 'AI 초안', short: 'AI 초안' },
+ { key: 'review', label: '검토/수정', short: '검토' },
+ { key: 'approved', label: '승인', short: '승인' },
+ { key: 'scheduled', label: '배포 예약', short: '배포' },
+];
+
+const STAGE_COLORS: Record = {
+ planning: { bg: 'bg-slate-100', text: 'text-slate-600', border: 'border-slate-200', dot: 'bg-slate-400' },
+ 'ai-draft':{ bg: 'bg-[#EFF0FF]', text: 'text-[#3A3F7C]', border: 'border-[#C5CBF5]', dot: 'bg-[#7A84D4]' },
+ review: { bg: 'bg-[#FFF6ED]', text: 'text-[#7C5C3A]', border: 'border-[#F5E0C5]', dot: 'bg-[#D4A872]' },
+ approved: { bg: 'bg-[#F3F0FF]', text: 'text-[#4A3A7C]', border: 'border-[#D5CDF5]', dot: 'bg-[#9B8AD4]' },
+ scheduled: { bg: 'bg-[#F3F0FF]', text: 'text-[#4A3A7C]', border: 'border-[#D5CDF5]', dot: 'bg-[#6C5CE7]' },
+};
+
+const channelIconMap: Record = {
+ youtube: YoutubeFilled,
+ instagram: InstagramFilled,
+ globe: GlobeFilled,
+ video: TiktokFilled,
+ share: ShareFilled,
+};
+
+function StageBar({ currentStage }: { currentStage: WorkflowStage }) {
+ const currentIdx = STAGES.findIndex((s) => s.key === currentStage);
+ return (
+
+ {STAGES.map((stage, idx) => {
+ const isPast = idx < currentIdx;
+ const isCurrent = idx === currentIdx;
+ return (
+
+
+
+ {isPast ? : idx + 1}
+
+
+ {stage.short}
+
+
+ {idx < STAGES.length - 1 && (
+
+ )}
+
+ );
+ })}
+
+ );
+}
+
+function WorkflowCard({ item, onStageChange, onNotesChange }: {
+ item: WorkflowItem;
+ onStageChange: (id: string, stage: WorkflowStage) => void;
+ onNotesChange: (id: string, notes: string) => void;
+}) {
+ const [expanded, setExpanded] = useState(false);
+ const [editingNotes, setEditingNotes] = useState(false);
+ const [notesValue, setNotesValue] = useState(item.userNotes ?? '');
+
+ const stageColor = STAGE_COLORS[item.stage];
+ const ChannelIcon = channelIconMap[item.channelIcon] ?? GlobeFilled;
+ const currentStageIdx = STAGES.findIndex((s) => s.key === item.stage);
+ const nextStage = STAGES[currentStageIdx + 1];
+
+ const saveNotes = () => {
+ onNotesChange(item.id, notesValue);
+ setEditingNotes(false);
+ };
+
+ return (
+
+ {/* Card Header */}
+
+
+ {/* Expanded Body */}
+
+ {expanded && (
+
+
+ {/* Stage Progress */}
+
+
+ {/* AI Draft Content */}
+ {item.contentType === 'video' && item.videoDraft && (
+
+
+
+
AI 스크립트 초안
+
{item.videoDraft.duration}
+
+
+ {item.videoDraft.script}
+
+
+
+
+ {item.videoDraft.shootingGuide.map((guide, i) => (
+ -
+ {i + 1}
+ {guide}
+
+ ))}
+
+
+ )}
+
+ {item.contentType === 'image-text' && item.imageTextDraft && (
+
+
+
+
+ AI {item.imageTextDraft.type === 'cardnews' ? '카드뉴스 카피' : '블로그 초안'}
+
+
+
+
{item.imageTextDraft.headline}
+
+ {item.imageTextDraft.copy.map((line, i) => (
+ - {line}
+ ))}
+
+
+ {item.imageTextDraft.layoutHint && (
+
{item.imageTextDraft.layoutHint}
+ )}
+
+ )}
+
+ {/* User Notes */}
+
+
+
수정 요청 / 메모
+ {!editingNotes && (
+
+ )}
+
+ {editingNotes ? (
+
+ ) : notesValue ? (
+
+ {notesValue}
+
+ ) : (
+
없음
+ )}
+
+
+ {/* Stage Advance Button */}
+ {nextStage && item.stage !== 'scheduled' && (
+
+ )}
+ {item.stage === 'scheduled' && (
+
+
+ 배포 예약 완료
+
+ )}
+
+
+ )}
+
+
+ );
+}
+
+export default function WorkflowTracker({ data }: WorkflowTrackerProps) {
+ const [items, setItems] = useState(data.items);
+ const [activeTab, setActiveTab] = useState('video');
+ const [activeStageFilter, setActiveStageFilter] = useState(null);
+
+ const handleStageChange = useCallback((id: string, stage: WorkflowStage) => {
+ setItems((prev) => prev.map((item) => item.id === id ? { ...item, stage } : item));
+ }, []);
+
+ const handleNotesChange = useCallback((id: string, notes: string) => {
+ setItems((prev) => prev.map((item) => item.id === id ? { ...item, userNotes: notes } : item));
+ }, []);
+
+ const filtered = items.filter((item) => {
+ if (item.contentType !== activeTab) return false;
+ if (activeStageFilter && item.stage !== activeStageFilter) return false;
+ return true;
+ });
+
+ // Stage counts for current tab
+ const stageCounts = STAGES.map((s) => ({
+ ...s,
+ count: items.filter((i) => i.contentType === activeTab && i.stage === s.key).length,
+ }));
+
+ return (
+
+ {/* Content Type Tabs */}
+
+ {(['video', 'image-text'] as WorkflowContentType[]).map((type) => (
+
+ ))}
+
+
+ {/* Stage Filter Pills */}
+
+ {stageCounts.map((stage) => {
+ if (stage.count === 0) return null;
+ const isActive = activeStageFilter === stage.key;
+ const color = STAGE_COLORS[stage.key];
+ return (
+ setActiveStageFilter(isActive ? null : stage.key)}
+ className={`flex items-center gap-2 px-3 py-1.5 rounded-full border text-xs font-medium transition-all ${color.bg} ${color.text} ${color.border} ${
+ isActive ? 'ring-2 ring-white/40' : ''
+ } ${activeStageFilter && !isActive ? 'opacity-40' : ''}`}
+ initial={{ opacity: 0, scale: 0.9 }}
+ animate={{ opacity: 1, scale: 1 }}
+ transition={{ duration: 0.2 }}
+ >
+
+ {stage.label}
+ {stage.count}
+
+ );
+ })}
+ {activeStageFilter && (
+
+ )}
+
+
+ {/* Card List */}
+
+
+ {filtered.length === 0 ? (
+
+ 해당 단계의 콘텐츠가 없습니다
+
+ ) : (
+ filtered.map((item: WorkflowItem) => (
+
+ ))
+ )}
+
+
+
+ );
+}
diff --git a/src/components/studio/StrategySourceStep.tsx b/src/components/studio/StrategySourceStep.tsx
index 19d9c8a..4feea4e 100644
--- a/src/components/studio/StrategySourceStep.tsx
+++ b/src/components/studio/StrategySourceStep.tsx
@@ -16,7 +16,7 @@ const PILLARS = [
const SOURCES: { key: AssetSourceType; title: string; description: string }[] = [
{ key: 'collected', title: '수집된 에셋', description: '홈페이지, 블로그, SNS에서 수집한 기존 에셋' },
- { key: 'my_assets', title: 'My Assets', description: '직접 업로드한 이미지, 영상, 텍스트 파일' },
+ { key: 'my_assets', title: '나의 소재', description: '직접 업로드한 이미지, 영상, 텍스트 파일' },
{ key: 'ai_generated', title: 'AI 생성', description: 'AI가 새로 생성하는 이미지, 텍스트, 영상' },
];
diff --git a/src/data/mockPlan.ts b/src/data/mockPlan.ts
index 48767af..da0bcc6 100644
--- a/src/data/mockPlan.ts
+++ b/src/data/mockPlan.ts
@@ -269,4 +269,148 @@ export const mockPlan: MarketingPlan = {
{ title: '서울대 의학박사의 가슴재수술 성공전략', views: 1400, type: 'Long', repurposeAs: ['Shorts 추출', 'SEO 블로그', 'Carousel'] },
],
},
+
+ // ─── Section 6: Repurposing Proposals ───
+ repurposingProposals: [
+ {
+ sourceVideo: { title: '한번에 성공하는 성형', views: 574000, type: 'Short', repurposeAs: [] },
+ estimatedEffort: 'low',
+ priority: 'high',
+ outputs: [
+ { format: 'Instagram Reel', channel: 'Instagram KR', description: '자막 추가 + 한국어 해시태그 최적화 후 즉시 크로스포스팅' },
+ { format: 'TikTok', channel: 'TikTok', description: '트렌딩 사운드 교체 + 텍스트 오버레이 재구성' },
+ { format: '광고 소재', channel: 'Facebook / Instagram', description: '가장 임팩트 있는 3초 후크 장면 + CTA 오버레이' },
+ ],
+ },
+ {
+ sourceVideo: { title: '코성형! 내 얼굴에 가장 예쁜 코', views: 124000, type: 'Long', repurposeAs: [] },
+ estimatedEffort: 'medium',
+ priority: 'high',
+ outputs: [
+ { format: 'Shorts 5개 추출', channel: 'YouTube', description: '핵심 설명 구간 15-60초 클립 5개 자동 추출' },
+ { format: 'Carousel 3개', channel: 'Instagram KR', description: '코성형 타입별 비교 정보 카드뉴스로 재구성' },
+ { format: 'Blog Post', channel: 'Naver Blog', description: '영상 스크립트 → 2,000자 SEO 블로그 포스트 변환' },
+ { format: 'Stories 시리즈', channel: 'Instagram', description: '촬영 비하인드 + Q&A 스니펫 5개' },
+ ],
+ },
+ {
+ sourceVideo: { title: '아나운서 박은영, 가슴 할 결심', views: 127000, type: 'Long', repurposeAs: [] },
+ estimatedEffort: 'medium',
+ priority: 'medium',
+ outputs: [
+ { format: 'Shorts 3개 추출', channel: 'YouTube / Instagram / TikTok', description: '스토리 하이라이트 구간 크로스포스팅' },
+ { format: '스토리 시리즈', channel: 'Instagram KR', description: '상담 결정 과정 + 회복 타임라인 Stories' },
+ { format: '광고 소재', channel: 'Facebook', description: '환자 신뢰도 강화 소셜 프루프 광고 소재' },
+ ],
+ },
+ {
+ sourceVideo: { title: '코성형+지방이식 전후', views: 525000, type: 'Short', repurposeAs: [] },
+ estimatedEffort: 'low',
+ priority: 'high',
+ outputs: [
+ { format: 'Instagram Reel', channel: 'Instagram KR', description: 'Before/After 포맷 최적화 + 동의서 확인 후 게시' },
+ { format: 'TikTok', channel: 'TikTok', description: '트렌드 사운드 교체 + Stitch 유도 CTA 추가' },
+ { format: 'Naver 블로그 삽입', channel: 'Naver Blog', description: '코+지방이식 복합 시술 블로그 포스트에 영상 임베드' },
+ ],
+ },
+ ],
+ workflow: {
+ items: [
+ {
+ id: 'wf-001',
+ title: '코성형 전후 비교 YouTube Shorts',
+ contentType: 'video',
+ channel: 'YouTube Shorts',
+ channelIcon: 'youtube',
+ stage: 'ai-draft',
+ videoDraft: {
+ script: `[인트로 — 0~3초]\n"코가 달라지면 인상이 달라져요."\n(전 사진 → 후 사진 전환)\n\n[본문 — 3~25초]\n"VIEW 성형외과의 코성형, 단순히 높이는 것이 아닙니다."\n"얼굴 전체 비율을 분석한 맞춤형 디자인,"\n"21년 무사고 기록이 말해줍니다."\n(수술 과정 그래픽 삽입)\n\n[CTA — 25~30초]\n"지금 상담 예약 — 링크 프로필 참고"`,
+ shootingGuide: [
+ '전/후 고화질 사진 세로 4:5 비율로 준비',
+ '자연광 또는 소프트박스 조명에서 정면 + 3/4 앵글 촬영',
+ '배경: 클리닉 로고 배경 또는 화이트 배경',
+ '의사 얼굴 없이 사진 위주 편집 (환자 동의서 필수)',
+ ],
+ duration: '30초',
+ },
+ },
+ {
+ id: 'wf-002',
+ title: '가슴성형 궁금증 5가지 — Instagram Reel',
+ contentType: 'video',
+ channel: 'Instagram',
+ channelIcon: 'instagram',
+ stage: 'review',
+ userNotes: '마지막 슬라이드에 전화번호 대신 카카오톡 채널명으로 바꿔주세요',
+ videoDraft: {
+ script: `[후크 — 0~2초]\n"가슴성형 전에 꼭 알아야 할 5가지"\n\n[1] 보형물 종류: 라운드 vs 물방울\n[2] 절개 위치 선택법\n[3] 회복 기간 현실 (3일~2주)\n[4] 재수술률을 낮추는 병원 고르는 기준\n[5] VIEW 21년 무사고의 비결\n\n[CTA]\n"자세한 상담은 카카오채널 \'뷰성형외과의원\'"`,
+ shootingGuide: [
+ '텍스트 슬라이드 5장 제작 (배경: #7B2D8E 그라디언트)',
+ '각 슬라이드에 VIEW 로고 워터마크 우측 하단 배치',
+ '트랜지션: 빠른 슬라이드 컷 (0.2초)',
+ '배경음악: 경쾌한 팝 인스트루멘탈 (저작권 무료)',
+ ],
+ duration: '45초',
+ },
+ },
+ {
+ id: 'wf-003',
+ title: '코성형 Q&A 카드뉴스 — Naver 블로그',
+ contentType: 'image-text',
+ channel: 'Naver Blog',
+ channelIcon: 'globe',
+ stage: 'planning',
+ imageTextDraft: {
+ type: 'cardnews',
+ headline: '코성형, 궁금한 게 너무 많죠? 전문의가 직접 답합니다',
+ copy: [
+ '[카드 1] 코성형 후 붓기는 얼마나 지속되나요?\n→ 초기 붓기 1~2주, 완전 회복 3~6개월. 일상 복귀는 보통 1주일.',
+ '[카드 2] 실리콘 vs 연골, 어떤 재료가 좋나요?\n→ 높이와 형태 교정에 따라 다름. VIEW는 개인 맞춤형 복합 재료 활용.',
+ '[카드 3] 코성형 후 운동은 언제부터?\n→ 가벼운 걷기: 1주 후 / 격렬한 운동: 최소 4주 후.',
+ '[카드 4] VIEW 코성형이 다른 이유\n→ 21년 무사고 · 전담 의료진 · 3D 시뮬레이션 상담 제공.',
+ ],
+ layoutHint: '4장 카드 세로형, 보라+골드 브랜드 컬러, 마지막 카드에 CTA (상담 예약 버튼)',
+ },
+ },
+ {
+ id: 'wf-004',
+ title: '눈성형 회복기 솔직 후기 블로그 포스트',
+ contentType: 'image-text',
+ channel: 'Naver Blog',
+ channelIcon: 'globe',
+ stage: 'approved',
+ scheduledDate: '2026-04-14',
+ imageTextDraft: {
+ type: 'blog',
+ headline: '눈성형 2주 후 솔직 리뷰 — VIEW 성형외과 후기',
+ copy: [
+ '수술 당일: 2시간 소요, 국소마취. 통증 최소화 확인.',
+ '수술 다음날: 붓기 있지만 일상생활 가능 수준.',
+ '1주일 후: 자연스러운 라인 확인. 실밥 제거.',
+ '2주일 후: 친구들도 "뭔가 달라졌는데?" 반응.',
+ 'VIEW의 장점: 의사 선생님이 수술 전 충분히 상담해주셔서 기대치 조정이 잘 됐습니다.',
+ ],
+ layoutHint: '1200px 썸네일 + 본문 2000자 이상, 키워드: 눈성형 후기, 강남 눈성형, VIEW 성형외과',
+ },
+ },
+ {
+ id: 'wf-005',
+ title: '21년 무사고 스토리 TikTok',
+ contentType: 'video',
+ channel: 'TikTok',
+ channelIcon: 'video',
+ stage: 'scheduled',
+ scheduledDate: '2026-04-10',
+ videoDraft: {
+ script: `"21년 동안 단 한 건의 의료사고도 없었습니다."\n(숫자 카운터 애니메이션: 0 → 21)\n"이게 VIEW의 자랑입니다."\n#강남성형외과 #무사고 #VIEW성형외과`,
+ shootingGuide: [
+ '텍스트 애니메이션 위주 편집 (After Effects 또는 CapCut)',
+ '배경: 클리닉 내부 실사 영상 블러 처리',
+ '폰트: VIEW 브랜드 서체 일치',
+ ],
+ duration: '15초',
+ },
+ },
+ ],
+ },
};
diff --git a/src/hooks/useMarketingPlan.ts b/src/hooks/useMarketingPlan.ts
index 659e389..77133ee 100644
--- a/src/hooks/useMarketingPlan.ts
+++ b/src/hooks/useMarketingPlan.ts
@@ -3,6 +3,7 @@ import { useLocation } from 'react-router';
import type { MarketingPlan, ChannelStrategyCard, CalendarData, ContentStrategyData } from '../types/plan';
import { fetchReportById, fetchActiveContentPlan, supabase } from '../lib/supabase';
import { transformReportToPlan } from '../lib/transformPlan';
+import { mockPlan } from '../data/mockPlan';
interface UseMarketingPlanResult {
data: MarketingPlan | null;
@@ -87,6 +88,13 @@ export function useMarketingPlan(id: string | undefined): UseMarketingPlanResult
async function loadPlan() {
try {
+ // ─── Dev / Demo: return mock data immediately ───
+ if (id === 'demo') {
+ setData(mockPlan);
+ setIsLoading(false);
+ return;
+ }
+
// ─── Source 1: Try content_plans table (AI-generated strategy) ───
// First, resolve clinicId from navigation state or analysis_runs
let clinicId = state?.clinicId || null;
diff --git a/src/lib/calendarExport.ts b/src/lib/calendarExport.ts
new file mode 100644
index 0000000..4f22295
--- /dev/null
+++ b/src/lib/calendarExport.ts
@@ -0,0 +1,101 @@
+import type { CalendarWeek, CalendarEntry } from '../types/plan';
+
+/**
+ * Returns the Monday date of a given ISO week number in a year.
+ * Week 1 = the week containing the first Thursday of the year (ISO 8601).
+ */
+function isoWeekToDate(year: number, week: number, dayOffset: number): Date {
+ // Jan 4 is always in week 1
+ const jan4 = new Date(year, 0, 4);
+ const dayOfWeek = jan4.getDay() || 7; // convert Sun=0 to 7
+ const monday = new Date(jan4);
+ monday.setDate(jan4.getDate() - (dayOfWeek - 1) + (week - 1) * 7);
+ monday.setDate(monday.getDate() + dayOffset);
+ return monday;
+}
+
+function formatICSDate(date: Date): string {
+ const pad = (n: number) => String(n).padStart(2, '0');
+ return (
+ `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}` +
+ `T${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`
+ );
+}
+
+function escapeICS(str: string): string {
+ return str
+ .replace(/\\/g, '\\\\')
+ .replace(/;/g, '\\;')
+ .replace(/,/g, '\\,')
+ .replace(/\n/g, '\\n');
+}
+
+function buildVEvent(
+ entry: CalendarEntry,
+ weekNumber: number,
+ year: number,
+ uid: string,
+): string {
+ const startDate = isoWeekToDate(year, weekNumber, entry.dayOfWeek);
+ // All-day event: DTSTART is DATE only, DTEND is next day
+ const endDate = new Date(startDate);
+ endDate.setDate(endDate.getDate() + 1);
+
+ const formatDate = (d: Date) => {
+ const pad = (n: number) => String(n).padStart(2, '0');
+ return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
+ };
+
+ const lines = [
+ 'BEGIN:VEVENT',
+ `UID:${uid}`,
+ `DTSTAMP:${formatICSDate(new Date())}Z`,
+ `DTSTART;VALUE=DATE:${formatDate(startDate)}`,
+ `DTEND;VALUE=DATE:${formatDate(endDate)}`,
+ `SUMMARY:${escapeICS(`[${entry.channel}] ${entry.title}`)}`,
+ entry.description ? `DESCRIPTION:${escapeICS(entry.description)}` : null,
+ `CATEGORIES:${escapeICS(entry.contentType.toUpperCase())}`,
+ `STATUS:${entry.status === 'published' ? 'CONFIRMED' : entry.status === 'approved' ? 'TENTATIVE' : 'NEEDS-ACTION'}`,
+ 'END:VEVENT',
+ ].filter(Boolean) as string[];
+
+ return lines.join('\r\n');
+}
+
+export function exportCalendarToICS(
+ weeks: CalendarWeek[],
+ calendarName = 'INFINITH 콘텐츠 캘린더',
+): void {
+ const year = new Date().getFullYear();
+
+ const vEvents = weeks.flatMap((week) =>
+ week.entries.map((entry, idx) =>
+ buildVEvent(
+ entry,
+ week.weekNumber,
+ year,
+ `infinith-${week.weekNumber}-${entry.id ?? idx}@infinith.ai`,
+ ),
+ ),
+ );
+
+ const icsContent = [
+ 'BEGIN:VCALENDAR',
+ 'VERSION:2.0',
+ 'PRODID:-//INFINITH//Marketing Content Calendar//KO',
+ `X-WR-CALNAME:${escapeICS(calendarName)}`,
+ 'X-WR-TIMEZONE:Asia/Seoul',
+ 'CALSCALE:GREGORIAN',
+ 'METHOD:PUBLISH',
+ ...vEvents,
+ 'END:VCALENDAR',
+ ].join('\r\n');
+
+ const blob = new Blob([icsContent], { type: 'text/calendar;charset=utf-8' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'infinith-content-calendar.ics';
+ a.click();
+ URL.revokeObjectURL(url);
+}
diff --git a/src/pages/MarketingPlanPage.tsx b/src/pages/MarketingPlanPage.tsx
index 199e35d..6ab9e57 100644
--- a/src/pages/MarketingPlanPage.tsx
+++ b/src/pages/MarketingPlanPage.tsx
@@ -9,8 +9,10 @@ import ChannelStrategy from '../components/plan/ChannelStrategy';
import ContentStrategy from '../components/plan/ContentStrategy';
import ContentCalendar from '../components/plan/ContentCalendar';
import AssetCollection from '../components/plan/AssetCollection';
+import RepurposingProposal from '../components/plan/RepurposingProposal';
import MyAssetUpload from '../components/plan/MyAssetUpload';
import StrategyAdjustmentSection from '../components/plan/StrategyAdjustmentSection';
+import WorkflowTracker from '../components/plan/WorkflowTracker';
import PlanCTA from '../components/plan/PlanCTA';
const PLAN_SECTIONS = [
@@ -19,7 +21,9 @@ const PLAN_SECTIONS = [
{ id: 'content-strategy', label: '콘텐츠 전략' },
{ id: 'content-calendar', label: '콘텐츠 캘린더' },
{ id: 'asset-collection', label: '에셋 수집' },
- { id: 'my-asset-upload', label: 'My Assets' },
+ { id: 'repurposing-proposal', label: '리퍼포징 제안' },
+ { id: 'workflow-tracker', label: '제작 파이프라인' },
+ { id: 'my-asset-upload', label: '나의 소재' },
{ id: 'strategy-adjustment', label: '전략 조정' },
];
@@ -75,6 +79,14 @@ export default function MarketingPlanPage() {
+ {data.repurposingProposals && data.repurposingProposals.length > 0 && (
+
+ )}
+
+ {data.workflow && (
+
+ )}
+
diff --git a/src/types/plan.ts b/src/types/plan.ts
index c8a0c9c..7a1275b 100644
--- a/src/types/plan.ts
+++ b/src/types/plan.ts
@@ -167,6 +167,50 @@ export interface AssetCollectionData {
youtubeRepurpose: YouTubeRepurposeItem[];
}
+// ─── Section 6: Repurposing Proposal ───
+
+export interface RepurposingProposalItem {
+ sourceVideo: YouTubeRepurposeItem;
+ outputs: RepurposingOutput[];
+ estimatedEffort: 'low' | 'medium' | 'high';
+ priority: 'high' | 'medium' | 'low';
+}
+
+// ─── Section 7: Workflow Tracker ───
+
+export type WorkflowStage = 'planning' | 'ai-draft' | 'review' | 'approved' | 'scheduled';
+export type WorkflowContentType = 'video' | 'image-text';
+
+export interface WorkflowVideoDraft {
+ script: string;
+ shootingGuide: string[];
+ duration: string; // e.g. '60초', '15분'
+}
+
+export interface WorkflowImageTextDraft {
+ type: 'cardnews' | 'blog';
+ headline: string;
+ copy: string[];
+ layoutHint?: string;
+}
+
+export interface WorkflowItem {
+ id: string;
+ title: string;
+ contentType: WorkflowContentType;
+ channel: string;
+ channelIcon: string;
+ stage: WorkflowStage;
+ userNotes?: string; // 사람이 입력한 수정 사항
+ videoDraft?: WorkflowVideoDraft;
+ imageTextDraft?: WorkflowImageTextDraft;
+ scheduledDate?: string;
+}
+
+export interface WorkflowData {
+ items: WorkflowItem[];
+}
+
// ─── Root Plan Type ───
export interface MarketingPlan {
@@ -181,4 +225,6 @@ export interface MarketingPlan {
contentStrategy: ContentStrategyData;
calendar: CalendarData;
assetCollection: AssetCollectionData;
+ repurposingProposals?: RepurposingProposalItem[];
+ workflow?: WorkflowData;
}