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
Mina Choi 2026-05-18 14:18:53 +09:00
parent 670535c112
commit d85dc50bf3
1 changed files with 29 additions and 112 deletions

View File

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