fix: /plan 페이지에서 /report 대신 /plan 호출하도록 교체
useMarketingPlan 이 getReport(id) + transformReportToPlan 으로 우회하던 코드를 getPlan(id) 직접 호출로 변경. PlanOutput 에 없는 메타 필드(id/clinicName/ targetUrl/createdAt 등)는 nav state metadata 또는 빈 값으로 채움. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>main
parent
670535c112
commit
d85dc50bf3
|
|
@ -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<string, MarketingPlan> = {
|
||||
'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<string, unknown>;
|
||||
metadata?: Record<string, unknown>;
|
||||
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<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);
|
||||
|
|
@ -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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue