diff --git a/src/features/plan/hooks/useMarketingPlan.ts b/src/features/plan/hooks/useMarketingPlan.ts index 7d2df6b..434aaa2 100644 --- a/src/features/plan/hooks/useMarketingPlan.ts +++ b/src/features/plan/hooks/useMarketingPlan.ts @@ -1,7 +1,6 @@ import { useState, useEffect } from 'react'; import { useLocation } from 'react-router'; -import type { MarketingPlan, ChannelStrategyCard, CalendarData, ContentStrategyData } from '@/features/plan/types/plan'; -import { transformReportToPlan } from '../lib/transformPlan'; +import type { MarketingPlan } from '@/features/plan/types/plan'; import { mockPlan } from '../data/mockPlan'; import { mockPlanBanobagi } from '../data/mockPlan_banobagi'; import { mockPlanGrand } from '../data/mockPlan_grand'; @@ -9,13 +8,8 @@ import { mockPlanWonjin } from '../data/mockPlan_wonjin'; import { mockPlanTs } from '../data/mockPlan_ts'; import { mockPlanIrum } from '../data/mockPlan_irum'; import { mockPlanO2O } from '../data/mockPlan_o2o'; -import { getReport } from '@/shared/api/generated/reports/reports'; import { getPlan } from '@/shared/api/generated/plans/plans'; -// TODO(migration): 'analysis_runs' / 'clinics' / 'content_plans' 테이블 직접 조회는 -// 현재 백엔드에 대응 엔드포인트 없음. 우선 reports/plans 엔드포인트만 활용하고, -// clinic 메타 일부 필드는 빈 값으로 둠. - const DEMO_PLANS: Record = { 'view-clinic': mockPlan, 'banobagi': mockPlanBanobagi, @@ -30,70 +24,20 @@ interface UseMarketingPlanResult { data: MarketingPlan | null; isLoading: boolean; error: string | null; - /** DB row 의 clinic_id — 게스트 페이지에서 워크스페이스 점프 버튼 노출에 사용 */ clinicId: string | null; } interface LocationState { - report?: Record; - metadata?: Record; + metadata?: { + url?: string; + clinicName?: string; + clinicNameEn?: string; + generatedAt?: string; + }; reportId?: string; clinicId?: string; } -/** - * content_plans DB row으로부터 MarketingPlan을 빌드. - * content_plans는 AI 생성 전략을 JSONB 컬럼에 저장. - */ -function buildPlanFromContentPlans( - row: Record, - 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(null); const [isLoading, setIsLoading] = useState(true); @@ -110,10 +54,10 @@ export function useMarketingPlan(id: string | undefined): UseMarketingPlanResult const state = location.state as LocationState | undefined; const stateClinicId = state?.clinicId ?? null; + const metadata = state?.metadata; async function loadPlan() { try { - // ─── 개발 / 데모: mock 데이터를 즉시 반환 ─── if (id === 'demo') { setData(mockPlan); setClinicId(null); @@ -127,57 +71,30 @@ export function useMarketingPlan(id: string | undefined): UseMarketingPlanResult return; } - // ─── 소스 1: plan 엔드포인트 시도 (AI 생성 전략) ─── - const clinicName = ''; - const clinicNameEn = ''; - const targetUrl = ''; - - try { - const planRes = await getPlan(id!); - if (planRes.status === 200 && planRes.data) { - const planRow = planRes.data as unknown as Record; - const plan = buildPlanFromContentPlans( - planRow, - clinicName, - clinicNameEn, - targetUrl, - ); - setData(plan); - setClinicId((planRow.clinic_id as string) || stateClinicId); - setIsLoading(false); - return; - } - } catch { - // report 기반 폴백으로 넘어감 + const planRes = await getPlan(id!); + if (planRes.status !== 200) { + throw new Error('마케팅 기획 조회에 실패했습니다.'); + } + const planOutput = planRes.data; + if (!planOutput) { + throw new Error('마케팅 기획이 아직 생성되지 않았습니다. 분석을 다시 실행해주세요.'); } - // ─── 소스 2: 네비게이션 state의 report 데이터 (폴백) ─── - 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); - setClinicId(stateClinicId); - setIsLoading(false); - return; - } - - // ─── 소스 3: report를 fetch해서 변환 ─── - const reportRes = await getReport(id!); - const reportRow = reportRes.data as unknown as Record; - const plan = transformReportToPlan({ - id: (reportRow.id as string) || id!, - url: (reportRow.url as string) || '', - clinic_name: (reportRow.clinic_name as string) || '', - report: reportRow, - created_at: (reportRow.created_at as string) || new Date().toISOString(), + setData({ + id: id!, + reportId: state?.reportId || id!, + clinicName: metadata?.clinicName || '', + clinicNameEn: metadata?.clinicNameEn || '', + createdAt: metadata?.generatedAt || new Date().toISOString(), + targetUrl: metadata?.url || '', + brandGuide: planOutput.brandGuide as MarketingPlan['brandGuide'], + channelStrategies: planOutput.channelStrategies as MarketingPlan['channelStrategies'], + contentStrategy: planOutput.contentStrategy as MarketingPlan['contentStrategy'], + calendar: planOutput.calendar as MarketingPlan['calendar'], + assetCollection: planOutput.assetCollection as MarketingPlan['assetCollection'], + repurposingProposals: (planOutput.repurposingProposals ?? undefined) as MarketingPlan['repurposingProposals'], }); - setData(plan); - setClinicId((reportRow.clinic_id as string) || stateClinicId); + setClinicId(stateClinicId); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to fetch marketing plan'); } finally {