o2o-infinith-demo/src/hooks/useMarketingPlan.ts

173 lines
6.0 KiB
TypeScript

import { useState, useEffect } from 'react';
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;
isLoading: boolean;
error: string | null;
}
interface LocationState {
report?: Record<string, unknown>;
metadata?: Record<string, unknown>;
reportId?: string;
clinicId?: string;
}
/**
* Build a MarketingPlan from content_plans DB row.
* content_plans stores AI-generated strategy in JSONB columns.
*/
function buildPlanFromContentPlans(
row: Record<string, unknown>,
clinicName: string,
clinicNameEn: string,
targetUrl: string,
): MarketingPlan {
const channelStrategies = (row.channel_strategies || []) as ChannelStrategyCard[];
const contentStrategy = (row.content_strategy || {}) as ContentStrategyData;
const calendar = (row.calendar || { weeks: [], monthlySummary: [] }) as CalendarData;
return {
id: row.id as string,
reportId: (row.run_id as string) || (row.id as string),
clinicName,
clinicNameEn,
createdAt: (row.created_at as string) || new Date().toISOString(),
targetUrl,
brandGuide: (row.brand_guide as MarketingPlan['brandGuide']) || {
colors: [],
fonts: [],
logoRules: [],
toneOfVoice: {
personality: ['전문적', '친근한', '신뢰할 수 있는'],
communicationStyle: '의료 전문 지식을 쉽고 친근하게 전달',
doExamples: [],
dontExamples: [],
},
channelBranding: [],
brandInconsistencies: [],
},
channelStrategies,
contentStrategy: {
pillars: contentStrategy.pillars || [],
typeMatrix: contentStrategy.typeMatrix || [],
workflow: contentStrategy.workflow || [
{ step: 1, name: '기획', description: 'AI 콘텐츠 주제 선정', owner: 'INFINITH AI', duration: '자동' },
{ step: 2, name: '제작', description: 'AI 초안 생성 + 의료진 감수', owner: 'INFINITH AI', duration: '1시간' },
{ step: 3, name: '편집', description: '영상/이미지 편집', owner: 'INFINITH Studio', duration: '30분' },
{ step: 4, name: '배포', description: '채널별 최적화 배포', owner: 'INFINITH Distribution', duration: '자동' },
{ step: 5, name: '분석', description: '성과 데이터 수집 + 전략 조정', owner: 'INFINITH Analytics', duration: '자동' },
],
repurposingSource: contentStrategy.repurposingSource || '1개 롱폼 영상',
repurposingOutputs: contentStrategy.repurposingOutputs || [],
},
calendar,
assetCollection: { assets: [], youtubeRepurpose: [] },
};
}
export function useMarketingPlan(id: string | undefined): UseMarketingPlanResult {
const [data, setData] = useState<MarketingPlan | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const location = useLocation();
useEffect(() => {
if (!id) {
setError('No plan ID provided');
setIsLoading(false);
return;
}
const state = location.state as LocationState | undefined;
async function loadPlan() {
try {
// ─── Dev / Demo: return mock data immediately ───
if (id === 'demo' || id === 'view-clinic') {
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;
let clinicName = '';
let clinicNameEn = '';
let targetUrl = '';
if (!clinicId) {
// Try to find clinicId from analysis_runs by run/report ID
const { data: run } = await supabase
.from('analysis_runs')
.select('clinic_id')
.eq('id', id)
.single();
if (run) clinicId = run.clinic_id;
}
if (clinicId) {
// Fetch clinic info for plan metadata
const { data: clinic } = await supabase
.from('clinics')
.select('name, name_en, url')
.eq('id', clinicId)
.single();
if (clinic) {
clinicName = clinic.name || '';
clinicNameEn = clinic.name_en || '';
targetUrl = clinic.url || '';
}
// Try fetching active content plan
const contentPlan = await fetchActiveContentPlan(clinicId);
if (contentPlan) {
const plan = buildPlanFromContentPlans(
contentPlan,
clinicName,
clinicNameEn,
targetUrl,
);
setData(plan);
setIsLoading(false);
return;
}
}
// ─── Source 2: Report data from navigation state (fallback) ───
if (state?.report && state?.metadata) {
const plan = transformReportToPlan({
id: (state.reportId || id),
url: (state.metadata.url as string) || '',
clinic_name: (state.metadata.clinicName as string) || '',
report: state.report,
created_at: (state.metadata.generatedAt as string) || new Date().toISOString(),
});
setData(plan);
setIsLoading(false);
return;
}
// ─── Source 3: Fetch report from Supabase and transform (fallback) ───
const row = await fetchReportById(id);
const plan = transformReportToPlan(row);
setData(plan);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch marketing plan');
} finally {
setIsLoading(false);
}
}
loadPlan();
}, [id, location.state]);
return { data, isLoading, error };
}