import { useCallback } from 'react'; import type { MarketingPlan } from '@/features/plan/types/plan'; /** * 마케팅 기획의 표 데이터 전체를 단일 CSV로 내보내는 훅. * 섹션마다 `=== Section Title ===` 헤더로 구분. * Excel/Numbers에서 한글 안 깨지도록 UTF-8 BOM 포함. */ type Cell = string | number | boolean | null | undefined; type Row = Cell[]; type Section = { title: string; rows: Row[] }; function escapeCell(value: Cell): string { const s = String(value ?? ''); if (/[",\n\r]/.test(s)) { return `"${s.replace(/"/g, '""')}"`; } return s; } function buildPlanCsv(plan: MarketingPlan): string { const sections: Section[] = []; // ─── 메타 ─── sections.push({ title: 'Plan Metadata', rows: [ ['Field', 'Value'], ['Clinic Name', plan.clinicName], ['Clinic Name (EN)', plan.clinicNameEn], ['Target URL', plan.targetUrl], ['Created At', plan.createdAt], ['Plan ID', plan.id], ['Source Report ID', plan.reportId], ], }); // ─── Brand Guide ─── const bg = plan.brandGuide; if (bg?.colors?.length) { sections.push({ title: 'Brand Guide: Colors', rows: [ ['Name', 'Hex', 'Usage'], ...bg.colors.map((c) => [c.name, c.hex, c.usage]), ], }); } if (bg?.fonts?.length) { sections.push({ title: 'Brand Guide: Fonts', rows: [ ['Family', 'Weight', 'Usage', 'Sample Text'], ...bg.fonts.map((f) => [f.family, f.weight, f.usage, f.sampleText]), ], }); } if (bg?.logoRules?.length) { sections.push({ title: 'Brand Guide: Logo Rules', rows: [ ['Rule', 'Description', 'Correct'], ...bg.logoRules.map((r) => [r.rule, r.description, r.correct ? 'Y' : 'N']), ], }); } if (bg?.toneOfVoice) { const t = bg.toneOfVoice; sections.push({ title: 'Brand Guide: Tone of Voice', rows: [ ['Field', 'Value'], ['Personality', (t.personality ?? []).join(' / ')], ['Communication Style', t.communicationStyle], ['Do Examples', (t.doExamples ?? []).join(' | ')], ['Dont Examples', (t.dontExamples ?? []).join(' | ')], ], }); } if (bg?.channelBranding?.length) { sections.push({ title: 'Brand Guide: Channel Branding', rows: [ ['Channel', 'Profile Photo', 'Banner Spec', 'Bio Template', 'Current Status'], ...bg.channelBranding.map((c) => [ c.channel, c.profilePhoto, c.bannerSpec, c.bioTemplate, c.currentStatus, ]), ], }); } if (bg?.brandInconsistencies?.length) { const rows: Row[] = [['Field', 'Channel', 'Value', 'Correct', 'Impact', 'Recommendation']]; bg.brandInconsistencies.forEach((b) => { b.values.forEach((v, i) => { rows.push([ i === 0 ? b.field : '', v.channel, v.value, v.isCorrect ? 'Y' : 'N', i === 0 ? b.impact : '', i === 0 ? b.recommendation : '', ]); }); }); sections.push({ title: 'Brand Guide: Brand Inconsistencies', rows }); } // ─── Channel Strategies ─── if (plan.channelStrategies?.length) { sections.push({ title: 'Channel Strategies', rows: [ ['Channel', 'Current Status', 'Target Goal', 'Content Types', 'Posting Frequency', 'Tone', 'Priority', 'Journey Stage'], ...plan.channelStrategies.map((s) => [ s.channelName, s.currentStatus, s.targetGoal, (s.contentTypes ?? []).join(' | '), s.postingFrequency, s.tone, s.priority, s.customerJourneyStage ?? '', ]), ], }); } // ─── Content Strategy ─── const cs = plan.contentStrategy; if (cs?.pillars?.length) { sections.push({ title: 'Content Pillars', rows: [ ['Title', 'Description', 'Related USP', 'Example Topics'], ...cs.pillars.map((p) => [p.title, p.description, p.relatedUSP, (p.exampleTopics ?? []).join(' | ')]), ], }); } if (cs?.typeMatrix?.length) { sections.push({ title: 'Content Type Matrix', rows: [ ['Format', 'Channels', 'Frequency', 'Purpose'], ...cs.typeMatrix.map((t) => [t.format, (t.channels ?? []).join(' | '), t.frequency, t.purpose]), ], }); } if (cs?.workflow?.length) { sections.push({ title: 'Content Workflow', rows: [ ['Step', 'Name', 'Description', 'Owner', 'Duration'], ...cs.workflow.map((w) => [w.step, w.name, w.description, w.owner, w.duration]), ], }); } if (cs?.repurposingOutputs?.length) { sections.push({ title: `Repurposing Outputs (source: ${cs.repurposingSource ?? ''})`, rows: [ ['Format', 'Channel', 'Description'], ...cs.repurposingOutputs.map((r) => [r.format, r.channel, r.description]), ], }); } // ─── Calendar (주차 → 일별 항목으로 풀어 펼침) ─── const cal = plan.calendar; if (cal?.weeks?.length) { const rows: Row[] = [['Week', 'Day of Week', 'Channel', 'Content Type', 'Title', 'Description', 'Pillar', 'Status']]; cal.weeks.forEach((w) => { w.entries.forEach((e, i) => { rows.push([ i === 0 ? w.label : '', e.dayOfWeek, e.channel, e.contentType, e.title, e.description ?? '', e.pillar ?? '', e.status ?? '', ]); }); }); sections.push({ title: 'Content Calendar', rows }); } if (cal?.monthlySummary?.length) { sections.push({ title: 'Monthly Content Summary', rows: [ ['Type', 'Label', 'Count'], ...cal.monthlySummary.map((s) => [s.type, s.label, s.count]), ], }); } // ─── Asset Collection ─── const ac = plan.assetCollection; if (ac?.assets?.length) { sections.push({ title: 'Asset Collection', rows: [ ['Source', 'Type', 'Title', 'Description', 'Status', 'Repurposing Suggestions'], ...ac.assets.map((a) => [ a.sourceLabel, a.type, a.title, a.description, a.status, (a.repurposingSuggestions ?? []).join(' | '), ]), ], }); } if (ac?.youtubeRepurpose?.length) { sections.push({ title: 'YouTube Repurpose Sources', rows: [ ['Title', 'Views', 'Type', 'Repurpose As'], ...ac.youtubeRepurpose.map((y) => [y.title, y.views, y.type, (y.repurposeAs ?? []).join(' | ')]), ], }); } // ─── Repurposing Proposals ─── if (plan.repurposingProposals?.length) { sections.push({ title: 'Repurposing Proposals', rows: [ ['Source Video', 'Source Views', 'Source Type', 'Output Count', 'Priority', 'Estimated Effort'], ...plan.repurposingProposals.map((p) => [ p.sourceVideo.title, p.sourceVideo.views, p.sourceVideo.type, p.outputs.length, p.priority, p.estimatedEffort, ]), ], }); } // ─── Workflow Items ─── if (plan.workflow?.items?.length) { sections.push({ title: 'Workflow Items', rows: [ ['Title', 'Content Type', 'Channel', 'Stage', 'Scheduled Date', 'User Notes'], ...plan.workflow.items.map((w) => [ w.title, w.contentType, w.channel, w.stage, w.scheduledDate ?? '', w.userNotes ?? '', ]), ], }); } // ─── 조립 ─── const lines: string[] = []; sections.forEach((sec, idx) => { if (idx > 0) lines.push(''); lines.push(`=== ${sec.title} ===`); sec.rows.forEach((row) => lines.push(row.map(escapeCell).join(','))); }); return lines.join('\r\n'); } export function useExportPlanCSV() { const exportPlanCSV = useCallback((filename: string, plan: MarketingPlan) => { const csv = '' + buildPlanCsv(plan); // UTF-8 BOM const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${filename}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }, []); return { exportPlanCSV }; }