173 lines
6.0 KiB
TypeScript
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 };
|
|
}
|