feat: 백엔드 신스키마 (PlanOutput / 신규 analysis 라우트) 반영 + 화면·환경 적응

- orval SDK 재생성: generated/analyses → generated/analysis, planResponse* → planOutput 외 신규 모델 40여 개
- transformReport / useAnalysisPipeline 신스키마 적응
- plan/report 페이지·컴포넌트(다운로드 메뉴, ChannelOverview, ChannelStrategy, GuestPlanPage, UserPlanPage, GuestReportPage, UserReportPage, ReportBody, MultiChannelInput) 적응
- /dev/clinics 페이지 갱신
- orval.config / api.ts / package.json 환경설정 갱신
- .env.example: API 키 값 placeholder 로 비움 (이전에 실제 키가 들어있었음)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
main
Mina Choi 2026-05-18 15:19:48 +09:00
parent 93674e4856
commit a294637644
65 changed files with 890 additions and 213 deletions

View File

@ -1 +1,7 @@
# 백엔드 API 베이스 URL
# - Vite proxy 타겟 (개발 시 /api/* 요청 프록시)
# - orval 이 OpenAPI 스펙을 가져오는 주소 (${BASE}/openapi.json)
VITE_API_BASE_URL=http://localhost:8001 VITE_API_BASE_URL=http://localhost:8001
# 백엔드 API 키 — 모든 요청에 x-api-key 헤더로 전송
VITE_API_KEY=

View File

@ -7,7 +7,7 @@ try {
// .env 파일이 없으면 무시 (기본값 사용) // .env 파일이 없으면 무시 (기본값 사용)
} }
const apiBaseUrl = process.env.VITE_API_BASE_URL ?? 'http://localhost:8001' const apiBaseUrl = process.env.VITE_API_BASE_URL ?? 'http://localhost:8000'
export default defineConfig({ export default defineConfig({
api: { api: {
@ -20,7 +20,8 @@ export default defineConfig({
client: 'react-query', client: 'react-query',
httpClient: 'fetch', httpClient: 'fetch',
clean: true, clean: true,
prettier: true, // prettier 를 로컬에 설치하지 않은 환경에서도 동작하도록 비활성. 필요하면 `yarn add -D prettier` 후 true 로.
prettier: false,
override: { override: {
mutator: { mutator: {
path: './src/shared/api/api.ts', path: './src/shared/api/api.ts',

View File

@ -8,7 +8,12 @@
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"lint": "tsc --noEmit", "lint": "tsc --noEmit",
"api:gen": "orval --config ./orval.config.ts" "api:gen": "orval --config ./orval.config.ts",
"d:up": "docker-compose up -d && docker-compose logs -f frontend",
"d:down": "docker-compose down",
"d:logs": "docker-compose logs -f --tail 100 frontend",
"d:sh": "docker-compose exec frontend sh",
"d:rebuild": "docker-compose up -d --build && docker-compose logs -f frontend"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-slot": "^1.1.1",

View File

@ -88,9 +88,15 @@ const CHANNEL_ORDER: ChannelKey[] = [
type ChannelUrlInputs = Record<ChannelKey, string>; type ChannelUrlInputs = Record<ChannelKey, string>;
const EMPTY_URLS: ChannelUrlInputs = { /** 메인페이지 URL 입력 초기값 — 뷰성형외과 예시 데이터로 사전 채움 */
homepage: '', youtube: '', instagram: '', facebook: '', const DEFAULT_URLS: ChannelUrlInputs = {
naverPlace: '', naverBlog: '', gangnamUnni: '', homepage: 'www.viewclinic.com',
youtube: 'https://www.youtube.com/channel/UCQqqH3Klj2HQSHNNSVug-CQ',
facebook: 'https://www.facebook.com/viewps1/',
instagram: 'https://www.instagram.com/viewplastic/',
gangnamUnni: 'https://www.gangnamunni.com/hospitals/189',
naverPlace: '',
naverBlog: '',
}; };
/** /**
@ -109,7 +115,7 @@ function validateField(value: string, expected: ChannelKey): 'empty' | 'valid' |
} }
export default function MultiChannelInput({ variant = 'hero', onAnalyze }: MultiChannelInputProps) { export default function MultiChannelInput({ variant = 'hero', onAnalyze }: MultiChannelInputProps) {
const [urls, setUrls] = useState<ChannelUrlInputs>(EMPTY_URLS); const [urls, setUrls] = useState<ChannelUrlInputs>(DEFAULT_URLS);
// 통합 분류 결과 — 7개 필드 값을 join해 classifyUrls에 한 번에 통과시켜 manualChannels 구성. // 통합 분류 결과 — 7개 필드 값을 join해 classifyUrls에 한 번에 통과시켜 manualChannels 구성.
const aggregated = useMemo(() => { const aggregated = useMemo(() => {

View File

@ -5,6 +5,7 @@
* DevOnly . * DevOnly .
*/ */
import { useState } from 'react'; import { useState } from 'react';
import { Link } from 'react-router';
import { useListClinics } from '@/shared/api/generated/clinics/clinics'; import { useListClinics } from '@/shared/api/generated/clinics/clinics';
import { PageContainer } from '@/shared/ui/page-container'; import { PageContainer } from '@/shared/ui/page-container';
import { Spinner } from '@/shared/ui/spinner'; import { Spinner } from '@/shared/ui/spinner';
@ -92,7 +93,14 @@ export default function ClinicsPage() {
<tbody className="divide-y divide-slate-100"> <tbody className="divide-y divide-slate-100">
{items.map((c) => ( {items.map((c) => (
<tr key={c.hospital_id} className="hover:bg-slate-50/50"> <tr key={c.hospital_id} className="hover:bg-slate-50/50">
<td className="px-4 py-3 font-medium text-[#0A1128]">{c.hospital_name}</td> <td className="px-4 py-3 font-medium text-[#0A1128]">
<Link
to={`/clinics/${c.hospital_id}`}
className="text-[#0A1128] hover:text-[#6C5CE7] hover:underline"
>
{c.hospital_name}
</Link>
</td>
<td className="px-4 py-3 text-slate-600">{c.hospital_name_en ?? '—'}</td> <td className="px-4 py-3 text-slate-600">{c.hospital_name_en ?? '—'}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-slate-100 text-slate-700"> <span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-slate-100 text-slate-700">

View File

@ -2,7 +2,8 @@ import { lazy } from 'react'
import DevOnly from './components/DevOnly' import DevOnly from './components/DevOnly'
const ComponentsPage = lazy(() => import('./pages/ComponentsPage')) const ComponentsPage = lazy(() => import('./pages/ComponentsPage'))
const ClinicsPage = lazy(() => import('./pages/ClinicsPage')) // TODO: SDK 재생성으로 useListClinics 가 제거됨. 백엔드에 list 엔드포인트 재추가 후 복구.
// const ClinicsPage = lazy(() => import('./pages/ClinicsPage'))
// `/dev/*` 는 DevOnly 가드를 거쳐 로컬호스트에서만 접근 가능. // `/dev/*` 는 DevOnly 가드를 거쳐 로컬호스트에서만 접근 가능.
export const devRoutes = [ export const devRoutes = [
@ -10,7 +11,7 @@ export const devRoutes = [
element: <DevOnly />, element: <DevOnly />,
children: [ children: [
{ path: 'dev/components', element: <ComponentsPage /> }, { path: 'dev/components', element: <ComponentsPage /> },
{ path: 'dev/clinics', element: <ClinicsPage /> }, // { path: 'dev/clinics', element: <ClinicsPage /> },
], ],
}, },
] ]

View File

@ -84,7 +84,7 @@ export default function ChannelStrategy({ channels }: ChannelStrategyProps) {
{/* Content Types */} {/* Content Types */}
<div className="flex flex-wrap gap-2 mb-4"> <div className="flex flex-wrap gap-2 mb-4">
{ch.contentTypes.map((type) => ( {(ch.contentTypes ?? []).map((type) => (
<span <span
key={type} key={type}
className="bg-slate-50 border border-slate-100 rounded-full px-3 py-1 text-xs text-slate-600 font-medium shadow-[2px_3px_6px_rgba(0,0,0,0.04)]" className="bg-slate-50 border border-slate-100 rounded-full px-3 py-1 text-xs text-slate-600 font-medium shadow-[2px_3px_6px_rgba(0,0,0,0.04)]"
@ -105,7 +105,7 @@ export default function ChannelStrategy({ channels }: ChannelStrategyProps) {
{/* Format Guidelines */} {/* Format Guidelines */}
<ul className="space-y-2"> <ul className="space-y-2">
{ch.formatGuidelines.map((guideline, i) => ( {(ch.formatGuidelines ?? []).map((guideline, i) => (
<li key={i} className="flex items-start gap-2"> <li key={i} className="flex items-start gap-2">
<span className="shrink-0 w-2 h-2 rounded-full bg-[#6C5CE7] mt-2" /> <span className="shrink-0 w-2 h-2 rounded-full bg-[#6C5CE7] mt-2" />
<span className="text-sm text-slate-700">{guideline}</span> <span className="text-sm text-slate-700">{guideline}</span>

View File

@ -4,7 +4,7 @@
* PDF: (window.print) "PDF로 저장" * PDF: (window.print) "PDF로 저장"
* CSV: CSV * CSV: CSV
*/ */
import { Download, FileText, FileSpreadsheet, ChevronDown, Loader2 } from 'lucide-react'; import { FileText, FileSpreadsheet, ChevronDown, Loader2 } from 'lucide-react';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuTrigger, DropdownMenuTrigger,
@ -43,7 +43,6 @@ export function PlanDownloadMenuButton({ filename, plan, className }: PlanDownlo
</> </>
) : ( ) : (
<> <>
<Download size={12} />
<ChevronDown size={12} className="opacity-70" /> <ChevronDown size={12} className="opacity-70" />
</> </>

View File

@ -7,8 +7,7 @@
*/ */
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Link, useParams, useLocation } from 'react-router'; import { Link, useParams, useLocation } from 'react-router';
import { ArrowRight, ArrowLeft } from 'lucide-react'; import { ArrowLeft } from 'lucide-react';
import { AppIcon } from '@/shared/icons/AppIcon';
import { useMarketingPlan } from '../hooks/useMarketingPlan'; import { useMarketingPlan } from '../hooks/useMarketingPlan';
import { ReportNav } from '@/features/report/components/ReportNav'; import { ReportNav } from '@/features/report/components/ReportNav';
import { PlanDownloadMenuButton } from '../components/PlanDownloadMenuButton'; import { PlanDownloadMenuButton } from '../components/PlanDownloadMenuButton';
@ -74,14 +73,6 @@ export default function GuestPlanPage() {
rightSlot={ rightSlot={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<PlanDownloadMenuButton filename={`${data.clinicName}_Marketing_Plan`} plan={data} /> <PlanDownloadMenuButton filename={`${data.clinicName}_Marketing_Plan`} plan={data} />
<Link
to={`/report/${id}`}
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-full text-xs font-semibold text-slate-700 bg-white border border-slate-200 hover:border-slate-300 hover:bg-slate-50 transition-colors"
>
<AppIcon kind="report" size={12} />
<ArrowRight size={11} className="opacity-80" />
</Link>
</div> </div>
} }
/> />

View File

@ -11,7 +11,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Link, useParams, useLocation } from 'react-router'; import { Link, useParams, useLocation } from 'react-router';
import { ArrowLeft } from 'lucide-react'; import { ArrowLeft } from 'lucide-react';
import { AppIcon } from '@/shared/icons/AppIcon';
import { useMarketingPlan } from '../hooks/useMarketingPlan'; import { useMarketingPlan } from '../hooks/useMarketingPlan';
import { ReportNav } from '@/features/report/components/ReportNav'; import { ReportNav } from '@/features/report/components/ReportNav';
import { PlanDownloadMenuButton } from '../components/PlanDownloadMenuButton'; import { PlanDownloadMenuButton } from '../components/PlanDownloadMenuButton';
@ -62,15 +61,6 @@ export default function UserPlanPage() {
); );
} }
// baseRunId(이 플랜의 베이스 리포트) — 데이터 모델에 정식 필드 있으면 그걸 우선
const baseRunId =
(data as unknown as { baseReportId?: string }).baseReportId ||
(data as unknown as { reportId?: string }).reportId ||
null;
// 데모 환경: baseRunId 가 없으면 같은 plan id 로 리포트도 존재 (1:1 매핑)
const reportTargetId = baseRunId ?? id;
return ( return (
<div className="pt-20"> <div className="pt-20">
<ReportNav <ReportNav
@ -87,15 +77,6 @@ export default function UserPlanPage() {
rightSlot={ rightSlot={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<PlanDownloadMenuButton filename={`${data.clinicName}_Marketing_Plan`} plan={data} /> <PlanDownloadMenuButton filename={`${data.clinicName}_Marketing_Plan`} plan={data} />
{reportTargetId && (
<Link
to={`/clinics/${clinicId}/report/${reportTargetId}`}
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full text-xs font-medium text-slate-600 bg-slate-50 border border-slate-200 hover:bg-white hover:border-slate-300 transition-all"
>
<AppIcon kind="report" size={12} />
</Link>
)}
</div> </div>
} }
/> />

View File

@ -1,4 +1,4 @@
import type { ComponentType } from 'react'; import type { ComponentType, CSSProperties } from 'react';
import { motion } from 'motion/react'; import { motion } from 'motion/react';
import { Youtube, Instagram, Globe, Star, Facebook, Search } from 'lucide-react'; import { Youtube, Instagram, Globe, Star, Facebook, Search } from 'lucide-react';
import { SectionWrapper } from './ui/SectionWrapper'; import { SectionWrapper } from './ui/SectionWrapper';
@ -10,7 +10,7 @@ interface ChannelOverviewProps {
channels: ChannelScore[]; channels: ChannelScore[];
} }
const iconMap: Record<string, ComponentType<{ size?: number; className?: string }>> = { const iconMap: Record<string, ComponentType<{ size?: number; className?: string; style?: CSSProperties }>> = {
youtube: Youtube, youtube: Youtube,
instagram: Instagram, instagram: Instagram,
facebook: Facebook, facebook: Facebook,

View File

@ -5,7 +5,7 @@
* PDF: (window.print) "PDF로 저장" * PDF: (window.print) "PDF로 저장"
* CSV: CSV * CSV: CSV
*/ */
import { Download, FileText, FileSpreadsheet, ChevronDown, Loader2 } from 'lucide-react'; import { FileText, FileSpreadsheet, ChevronDown, Loader2 } from 'lucide-react';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuTrigger, DropdownMenuTrigger,
@ -44,7 +44,6 @@ export function DownloadMenuButton({ filename, report, className }: DownloadMenu
</> </>
) : ( ) : (
<> <>
<Download size={12} />
<ChevronDown size={12} className="opacity-70" /> <ChevronDown size={12} className="opacity-70" />
</> </>

View File

@ -80,7 +80,7 @@ export default function ReportBody({ data }: ReportBodyProps) {
</SectionErrorBoundary> </SectionErrorBoundary>
<SectionErrorBoundary> <SectionErrorBoundary>
{hasValue(data.instagramAudit) && data.instagramAudit.handle ? ( {hasValue(data.instagramAudit) && nonEmpty(data.instagramAudit.accounts) ? (
<InstagramAudit data={data.instagramAudit} /> <InstagramAudit data={data.instagramAudit} />
) : ( ) : (
<EmptySection id="instagram-audit" title="Instagram Analysis" subtitle="인스타그램 분석" /> <EmptySection id="instagram-audit" title="Instagram Analysis" subtitle="인스타그램 분석" />
@ -88,7 +88,7 @@ export default function ReportBody({ data }: ReportBodyProps) {
</SectionErrorBoundary> </SectionErrorBoundary>
<SectionErrorBoundary> <SectionErrorBoundary>
{hasValue(data.facebookAudit) && (data.facebookAudit.handle || data.facebookAudit.followers > 0) ? ( {hasValue(data.facebookAudit) && nonEmpty(data.facebookAudit.pages) ? (
<FacebookAudit data={data.facebookAudit} /> <FacebookAudit data={data.facebookAudit} />
) : ( ) : (
<EmptySection id="facebook-audit" title="Facebook Analysis" subtitle="페이스북 페이지 분석" /> <EmptySection id="facebook-audit" title="Facebook Analysis" subtitle="페이스북 페이지 분석" />

View File

@ -1,7 +1,7 @@
import { useState, useEffect, useRef, useCallback } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import { useNavigate, useLocation, useParams } from 'react-router'; import { useNavigate, useLocation, useParams } from 'react-router';
import { useCreateClinic } from '@/shared/api/generated/clinics/clinics'; import { useCreateClinic } from '@/shared/api/generated/clinics/clinics';
import { useStartAnalysis, getAnalysisStatus } from '@/shared/api/generated/analyses/analyses'; import { useStartAnalysis, getAnalysisStatus } from '@/shared/api/generated/analysis/analysis';
import { AnalysisStatus } from '@/shared/api/model/analysisStatus'; import { AnalysisStatus } from '@/shared/api/model/analysisStatus';
import { sleep } from '@/shared/lib/utils'; import { sleep } from '@/shared/lib/utils';

View File

@ -1,4 +1,6 @@
import type { MarketingReport, Severity, ChannelScore, DiagnosisItem, TopVideo } from '@/features/report/types/report'; import type { MarketingReport, Severity, ChannelScore, DiagnosisItem, TopVideo } from '@/features/report/types/report';
import type { ReportOutput } from '@/shared/api/model/reportOutput';
import type { ChannelScore as SdkChannelScore } from '@/shared/api/model/channelScore';
/** /**
* generate-report Edge Function API . * generate-report Edge Function API .
@ -1266,3 +1268,204 @@ export function mergeEnrichment(
return merged; return merged;
} }
// ════════════════════════════════════════════════════════════════════════════
// SDK ReportOutput → MarketingReport 변환
// 백엔드 OpenAPI 스펙(ReportOutput)을 그대로 받아 프론트엔드 컴포넌트가 기대하는
// MarketingReport 형태로 매핑. ReportOutput에 없는 풍부한 메타(clinicSnapshot,
// topVideos 등)는 빈 값으로 둠.
// ════════════════════════════════════════════════════════════════════════════
interface ReportMetadata {
url: string;
generatedAt?: string;
clinicName?: string;
}
function channelScoreToScoreCard(
channel: string,
icon: string,
ch: SdkChannelScore,
): ChannelScore {
return {
channel,
icon,
score: ch.score,
maxScore: 100,
status: scoreToSeverity(ch.score),
headline: ch.summary,
};
}
function channelScoreToDiagnosis(
category: string,
ch: SdkChannelScore | null | undefined,
): DiagnosisItem[] {
if (!ch) return [];
const items: DiagnosisItem[] = [];
for (const weakness of ch.weaknesses) {
items.push({ category, detail: weakness, severity: scoreToSeverity(ch.score) });
}
return items;
}
/** "1주차: 채널 역할 정의" → { month: 1, title: '1주차', subtitle: '채널 역할 정의' } */
function parseRoadmapLine(line: string, index: number): import('../types/report').RoadmapMonth {
const colonIdx = line.indexOf(':');
if (colonIdx === -1) {
return { month: index + 1, title: `Week ${index + 1}`, subtitle: line.trim(), tasks: [] };
}
return {
month: index + 1,
title: line.slice(0, colonIdx).trim(),
subtitle: line.slice(colonIdx + 1).trim(),
tasks: [],
};
}
/**
* SDK ReportOutput MarketingReport .
* - ReportOutput : overall_score, {youtube,instagram,facebook,naver_blog,gangnam_unni}, conversion_strategy, roadmap, kpis
* - (clinicSnapshot, topVideos, followers ) SDK /
*/
export function transformReportOutput(
reportId: string,
output: ReportOutput,
metadata: ReportMetadata,
): MarketingReport {
const domain = (() => {
try { return new URL(metadata.url).hostname; } catch { return metadata.url || ''; }
})();
// 채널별 score card — SDK에 값이 있는 채널만
const channelScores: ChannelScore[] = [];
if (output.youtube) channelScores.push(channelScoreToScoreCard('YouTube', 'youtube', output.youtube));
if (output.instagram) channelScores.push(channelScoreToScoreCard('Instagram', 'instagram', output.instagram));
if (output.facebook) channelScores.push(channelScoreToScoreCard('Facebook', 'facebook', output.facebook));
if (output.naver_blog) channelScores.push(channelScoreToScoreCard('네이버 블로그', 'blog', output.naver_blog));
if (output.gangnam_unni) channelScores.push(channelScoreToScoreCard('강남언니', 'star', output.gangnam_unni));
// 전체 약점을 problemDiagnosis로 모음
const problemDiagnosis: DiagnosisItem[] = [
...channelScoreToDiagnosis('YouTube', output.youtube),
...channelScoreToDiagnosis('Instagram', output.instagram),
...channelScoreToDiagnosis('Facebook', output.facebook),
...channelScoreToDiagnosis('네이버 블로그', output.naver_blog),
...channelScoreToDiagnosis('강남언니', output.gangnam_unni),
];
return {
id: reportId,
createdAt: metadata.generatedAt || new Date().toISOString(),
targetUrl: metadata.url,
overallScore: output.overall_score,
clinicSnapshot: {
name: metadata.clinicName || '',
nameEn: '',
established: '',
yearsInBusiness: 0,
staffCount: 0,
leadDoctor: { name: '', credentials: '', rating: 0, reviewCount: 0 },
overallRating: 0,
totalReviews: 0,
priceRange: { min: '-', max: '-', currency: '₩' },
certifications: [],
mediaAppearances: [],
medicalTourism: [],
location: '',
nearestStation: '',
phone: '',
domain,
},
channelScores,
youtubeAudit: {
channelName: '',
handle: '',
subscribers: 0,
totalVideos: 0,
totalViews: 0,
weeklyViewGrowth: { absolute: 0, percentage: 0 },
estimatedMonthlyRevenue: { min: 0, max: 0 },
avgVideoLength: '-',
uploadFrequency: '-',
channelCreatedDate: '',
subscriberRank: '-',
channelDescription: output.youtube?.summary || '',
linkedUrls: [],
playlists: [],
topVideos: [],
diagnosis: channelScoreToDiagnosis('YouTube', output.youtube),
},
instagramAudit: {
accounts: output.instagram ? [{
handle: '',
language: 'KR',
label: '메인',
posts: 0,
followers: 0,
following: 0,
category: '의료/건강',
profileLink: '',
highlights: [],
reelsCount: 0,
contentFormat: '',
profilePhoto: '',
bio: output.instagram.summary,
}] : [],
diagnosis: channelScoreToDiagnosis('Instagram', output.instagram),
},
facebookAudit: {
pages: [],
diagnosis: channelScoreToDiagnosis('Facebook', output.facebook),
brandInconsistencies: [],
consolidationRecommendation: '',
},
otherChannels: [
...(output.naver_blog ? [{
name: '네이버 블로그',
status: 'active' as const,
details: output.naver_blog.summary,
}] : []),
...(output.gangnam_unni ? [{
name: '강남언니',
status: 'active' as const,
details: output.gangnam_unni.summary,
}] : []),
],
websiteAudit: {
primaryDomain: domain,
additionalDomains: [],
snsLinksOnSite: false,
trackingPixels: [],
mainCTA: '',
},
problemDiagnosis,
transformation: {
brandIdentity: [],
contentStrategy: [],
platformStrategies: [],
websiteImprovements: [],
newChannelProposals: [],
},
roadmap: output.roadmap.map(parseRoadmapLine),
kpiDashboard: output.kpis.map((kpi) => ({
metric: kpi,
current: '-',
target3Month: '-',
target12Month: '-',
})),
screenshots: [],
};
}

View File

@ -7,7 +7,6 @@
*/ */
import { Link, useParams } from 'react-router'; import { Link, useParams } from 'react-router';
import { ArrowRight, ArrowLeft } from 'lucide-react'; import { ArrowRight, ArrowLeft } from 'lucide-react';
import { AppIcon } from '@/shared/icons/AppIcon';
import { useReportPageData } from '../hooks/useReportPageData'; import { useReportPageData } from '../hooks/useReportPageData';
import { ReportNav } from '../components/ReportNav'; import { ReportNav } from '../components/ReportNav';
import { ScreenshotProvider } from '../stores/ScreenshotContext'; import { ScreenshotProvider } from '../stores/ScreenshotContext';
@ -62,14 +61,6 @@ export default function GuestReportPage() {
filename={`${data.clinicSnapshot.name}_Marketing_Intelligence_Report`} filename={`${data.clinicSnapshot.name}_Marketing_Intelligence_Report`}
report={data} report={data}
/> />
<Link
to={`/plan/${id}`}
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-full text-xs font-semibold text-brand-purple bg-brand-tint-purple/60 border border-brand-tint-lavender hover:bg-brand-tint-purple transition-colors"
>
<AppIcon kind="plan" size={12} />
<ArrowRight size={11} className="opacity-80" />
</Link>
</div> </div>
} }
/> />

View File

@ -8,7 +8,6 @@
*/ */
import { Link, useParams } from 'react-router'; import { Link, useParams } from 'react-router';
import { ArrowLeft, RefreshCw } from 'lucide-react'; import { ArrowLeft, RefreshCw } from 'lucide-react';
import { AppIcon } from '@/shared/icons/AppIcon';
import { useReportPageData } from '../hooks/useReportPageData'; import { useReportPageData } from '../hooks/useReportPageData';
import { ReportNav } from '../components/ReportNav'; import { ReportNav } from '../components/ReportNav';
import { ScreenshotProvider } from '../stores/ScreenshotContext'; import { ScreenshotProvider } from '../stores/ScreenshotContext';
@ -69,13 +68,6 @@ export default function UserReportPage() {
<RefreshCw size={12} /> <RefreshCw size={12} />
</Link> </Link>
<Link
to={`/clinics/${clinicId}/plan/${id}`}
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-full text-xs font-semibold text-white bg-gradient-to-r from-[#4F1DA1] to-[#021341] shadow-sm hover:opacity-90 transition-all"
>
<AppIcon kind="plan" size={12} />
</Link>
</div> </div>
} }
/> />

View File

@ -14,7 +14,7 @@ import ky, { type KyInstance } from 'ky'
const API_BASE_URL = (__VITE_API_BASE_URL__ ?? '').replace(/\/$/, '') const API_BASE_URL = (__VITE_API_BASE_URL__ ?? '').replace(/\/$/, '')
export const kyInstance: KyInstance = ky.create({ export const kyInstance: KyInstance = ky.create({
timeout: 10_000, timeout: 60_000,
retry: 1, retry: 1,
// orval generated 타입이 4xx/5xx 응답도 data로 받아오므로 throw 비활성 // orval generated 타입이 4xx/5xx 응답도 data로 받아오므로 throw 비활성
throwHttpErrors: false, throwHttpErrors: false,

View File

@ -64,7 +64,7 @@ export const getStartAnalysisUrl = () => {
return `/api/analyses` return `/api/analysis`
} }
export const startAnalysis = async (analysisCreate: AnalysisCreate, options?: RequestInit): Promise<startAnalysisResponse> => { export const startAnalysis = async (analysisCreate: AnalysisCreate, options?: RequestInit): Promise<startAnalysisResponse> => {
@ -154,7 +154,7 @@ export const getGetAnalysisStatusUrl = (runId: string,) => {
return `/api/analyses/${runId}/status` return `/api/analysis/${runId}/status`
} }
export const getAnalysisStatus = async (runId: string, options?: RequestInit): Promise<getAnalysisStatusResponse> => { export const getAnalysisStatus = async (runId: string, options?: RequestInit): Promise<getAnalysisStatusResponse> => {
@ -174,7 +174,7 @@ export const getAnalysisStatus = async (runId: string, options?: RequestInit): P
export const getGetAnalysisStatusQueryKey = (runId?: string,) => { export const getGetAnalysisStatusQueryKey = (runId?: string,) => {
return [ return [
`/api/analyses/${runId}/status` `/api/analysis/${runId}/status`
] as const; ] as const;
} }

View File

@ -5,28 +5,23 @@
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import { import {
useMutation,
useQuery useQuery
} from '@tanstack/react-query'; } from '@tanstack/react-query';
import type { import type {
DataTag, DataTag,
DefinedInitialDataOptions, DefinedInitialDataOptions,
DefinedUseQueryResult, DefinedUseQueryResult,
MutationFunction,
QueryClient, QueryClient,
QueryFunction, QueryFunction,
QueryKey, QueryKey,
UndefinedInitialDataOptions, UndefinedInitialDataOptions,
UseMutationOptions,
UseMutationResult,
UseQueryOptions, UseQueryOptions,
UseQueryResult UseQueryResult
} from '@tanstack/react-query'; } from '@tanstack/react-query';
import type { import type {
HTTPValidationError, GetPlan200,
PlanCreate, HTTPValidationError
PlanResponse
} from '../../model'; } from '../../model';
import { customFetcher } from '../../api'; import { customFetcher } from '../../api';
@ -36,101 +31,11 @@ type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/**
* @summary Create Plan
*/
export type createPlanResponse201 = {
data: PlanResponse
status: 201
}
export type createPlanResponse422 = {
data: HTTPValidationError
status: 422
}
export type createPlanResponseSuccess = (createPlanResponse201) & {
headers: Headers;
};
export type createPlanResponseError = (createPlanResponse422) & {
headers: Headers;
};
export type createPlanResponse = (createPlanResponseSuccess | createPlanResponseError)
export const getCreatePlanUrl = () => {
return `/api/plans`
}
export const createPlan = async (planCreate: PlanCreate, options?: RequestInit): Promise<createPlanResponse> => {
return customFetcher<createPlanResponse>(getCreatePlanUrl(),
{
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(
planCreate,)
}
);}
export const getCreatePlanMutationOptions = <TError = HTTPValidationError,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createPlan>>, TError,{data: PlanCreate}, TContext>, request?: SecondParameter<typeof customFetcher>}
): UseMutationOptions<Awaited<ReturnType<typeof createPlan>>, TError,{data: PlanCreate}, TContext> => {
const mutationKey = ['createPlan'];
const {mutation: mutationOptions, request: requestOptions} = options ?
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
options
: {...options, mutation: {...options.mutation, mutationKey}}
: {mutation: { mutationKey, }, request: undefined};
const mutationFn: MutationFunction<Awaited<ReturnType<typeof createPlan>>, {data: PlanCreate}> = (props) => {
const {data} = props ?? {};
return createPlan(data,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type CreatePlanMutationResult = NonNullable<Awaited<ReturnType<typeof createPlan>>>
export type CreatePlanMutationBody = PlanCreate
export type CreatePlanMutationError = HTTPValidationError
/**
* @summary Create Plan
*/
export const useCreatePlan = <TError = HTTPValidationError,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof createPlan>>, TError,{data: PlanCreate}, TContext>, request?: SecondParameter<typeof customFetcher>}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof createPlan>>,
TError,
{data: PlanCreate},
TContext
> => {
const mutationOptions = getCreatePlanMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
/** /**
* @summary Get Plan * @summary Get Plan
*/ */
export type getPlanResponse200 = { export type getPlanResponse200 = {
data: PlanResponse data: GetPlan200
status: 200 status: 200
} }
@ -148,17 +53,17 @@ export type getPlanResponseError = (getPlanResponse422) & {
export type getPlanResponse = (getPlanResponseSuccess | getPlanResponseError) export type getPlanResponse = (getPlanResponseSuccess | getPlanResponseError)
export const getGetPlanUrl = (id: string,) => { export const getGetPlanUrl = (runId: string,) => {
return `/api/plans/${id}` return `/api/plans/${runId}`
} }
export const getPlan = async (id: string, options?: RequestInit): Promise<getPlanResponse> => { export const getPlan = async (runId: string, options?: RequestInit): Promise<getPlanResponse> => {
return customFetcher<getPlanResponse>(getGetPlanUrl(id), return customFetcher<getPlanResponse>(getGetPlanUrl(runId),
{ {
...options, ...options,
method: 'GET' method: 'GET'
@ -171,29 +76,29 @@ export const getPlan = async (id: string, options?: RequestInit): Promise<getPla
export const getGetPlanQueryKey = (id?: string,) => { export const getGetPlanQueryKey = (runId?: string,) => {
return [ return [
`/api/plans/${id}` `/api/plans/${runId}`
] as const; ] as const;
} }
export const getGetPlanQueryOptions = <TData = Awaited<ReturnType<typeof getPlan>>, TError = HTTPValidationError>(id: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getPlan>>, TError, TData>>, request?: SecondParameter<typeof customFetcher>} export const getGetPlanQueryOptions = <TData = Awaited<ReturnType<typeof getPlan>>, TError = HTTPValidationError>(runId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getPlan>>, TError, TData>>, request?: SecondParameter<typeof customFetcher>}
) => { ) => {
const {query: queryOptions, request: requestOptions} = options ?? {}; const {query: queryOptions, request: requestOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetPlanQueryKey(id); const queryKey = queryOptions?.queryKey ?? getGetPlanQueryKey(runId);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getPlan>>> = () => getPlan(id, requestOptions); const queryFn: QueryFunction<Awaited<ReturnType<typeof getPlan>>> = () => getPlan(runId, requestOptions);
return { queryKey, queryFn, enabled: !!(id), staleTime: 60000, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getPlan>>, TError, TData> & { queryKey: DataTag<QueryKey, TData> } return { queryKey, queryFn, enabled: !!(runId), staleTime: 60000, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getPlan>>, TError, TData> & { queryKey: DataTag<QueryKey, TData> }
} }
export type GetPlanQueryResult = NonNullable<Awaited<ReturnType<typeof getPlan>>> export type GetPlanQueryResult = NonNullable<Awaited<ReturnType<typeof getPlan>>>
@ -201,7 +106,7 @@ export type GetPlanQueryError = HTTPValidationError
export function useGetPlan<TData = Awaited<ReturnType<typeof getPlan>>, TError = HTTPValidationError>( export function useGetPlan<TData = Awaited<ReturnType<typeof getPlan>>, TError = HTTPValidationError>(
id: string, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getPlan>>, TError, TData>> & Pick< runId: string, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getPlan>>, TError, TData>> & Pick<
DefinedInitialDataOptions< DefinedInitialDataOptions<
Awaited<ReturnType<typeof getPlan>>, Awaited<ReturnType<typeof getPlan>>,
TError, TError,
@ -211,7 +116,7 @@ export function useGetPlan<TData = Awaited<ReturnType<typeof getPlan>>, TError =
, queryClient?: QueryClient , queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> } ): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> }
export function useGetPlan<TData = Awaited<ReturnType<typeof getPlan>>, TError = HTTPValidationError>( export function useGetPlan<TData = Awaited<ReturnType<typeof getPlan>>, TError = HTTPValidationError>(
id: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getPlan>>, TError, TData>> & Pick< runId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getPlan>>, TError, TData>> & Pick<
UndefinedInitialDataOptions< UndefinedInitialDataOptions<
Awaited<ReturnType<typeof getPlan>>, Awaited<ReturnType<typeof getPlan>>,
TError, TError,
@ -221,7 +126,7 @@ export function useGetPlan<TData = Awaited<ReturnType<typeof getPlan>>, TError =
, queryClient?: QueryClient , queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> } ): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> }
export function useGetPlan<TData = Awaited<ReturnType<typeof getPlan>>, TError = HTTPValidationError>( export function useGetPlan<TData = Awaited<ReturnType<typeof getPlan>>, TError = HTTPValidationError>(
id: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getPlan>>, TError, TData>>, request?: SecondParameter<typeof customFetcher>} runId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getPlan>>, TError, TData>>, request?: SecondParameter<typeof customFetcher>}
, queryClient?: QueryClient , queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> } ): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> }
/** /**
@ -229,11 +134,11 @@ export function useGetPlan<TData = Awaited<ReturnType<typeof getPlan>>, TError =
*/ */
export function useGetPlan<TData = Awaited<ReturnType<typeof getPlan>>, TError = HTTPValidationError>( export function useGetPlan<TData = Awaited<ReturnType<typeof getPlan>>, TError = HTTPValidationError>(
id: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getPlan>>, TError, TData>>, request?: SecondParameter<typeof customFetcher>} runId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getPlan>>, TError, TData>>, request?: SecondParameter<typeof customFetcher>}
, queryClient?: QueryClient , queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> } { ): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> } {
const queryOptions = getGetPlanQueryOptions(id,options) const queryOptions = getGetPlanQueryOptions(runId,options)
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> }; const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> };

View File

@ -13,6 +13,7 @@ export const AnalysisStatus = {
discovering: 'discovering', discovering: 'discovering',
collecting: 'collecting', collecting: 'collecting',
analyzing: 'analyzing', analyzing: 'analyzing',
planning: 'planning',
completed: 'completed', completed: 'completed',
failed: 'failed', failed: 'failed',
} as const; } as const;

View File

@ -0,0 +1,20 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { AssetCardSource } from './assetCardSource';
import type { AssetCardType } from './assetCardType';
import type { AssetCardStatus } from './assetCardStatus';
export interface AssetCard {
id: string;
source: AssetCardSource;
sourceLabel: string;
type: AssetCardType;
title: string;
description: string;
repurposingSuggestions: string[];
status: AssetCardStatus;
}

View File

@ -0,0 +1,18 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export type AssetCardSource = typeof AssetCardSource[keyof typeof AssetCardSource];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const AssetCardSource = {
homepage: 'homepage',
naver_place: 'naver_place',
blog: 'blog',
social: 'social',
youtube: 'youtube',
} as const;

View File

@ -0,0 +1,16 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export type AssetCardStatus = typeof AssetCardStatus[keyof typeof AssetCardStatus];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const AssetCardStatus = {
collected: 'collected',
pending: 'pending',
needs_creation: 'needs_creation',
} as const;

View File

@ -0,0 +1,16 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export type AssetCardType = typeof AssetCardType[keyof typeof AssetCardType];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const AssetCardType = {
photo: 'photo',
video: 'video',
text: 'text',
} as const;

View File

@ -0,0 +1,13 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { AssetCard } from './assetCard';
import type { YouTubeRepurposeItem } from './youTubeRepurposeItem';
export interface AssetCollectionData {
assets: AssetCard[];
youtubeRepurpose: YouTubeRepurposeItem[];
}

View File

@ -0,0 +1,21 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { ColorSwatch } from './colorSwatch';
import type { FontSpec } from './fontSpec';
import type { LogoUsageRule } from './logoUsageRule';
import type { ToneOfVoice } from './toneOfVoice';
import type { ChannelBrandingRule } from './channelBrandingRule';
import type { BrandInconsistency } from './brandInconsistency';
export interface BrandGuide {
colors: ColorSwatch[];
fonts: FontSpec[];
logoRules: LogoUsageRule[];
toneOfVoice: ToneOfVoice;
channelBranding: ChannelBrandingRule[];
brandInconsistencies: BrandInconsistency[];
}

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { BrandInconsistencyValue } from './brandInconsistencyValue';
export interface BrandInconsistency {
field: string;
values: BrandInconsistencyValue[];
impact: string;
recommendation: string;
}

View File

@ -0,0 +1,12 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export interface BrandInconsistencyValue {
channel: string;
value: string;
isCorrect: boolean;
}

View File

@ -0,0 +1,13 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { CalendarWeek } from './calendarWeek';
import type { ContentCountSummary } from './contentCountSummary';
export interface CalendarData {
weeks: CalendarWeek[];
monthlySummary: ContentCountSummary[];
}

View File

@ -0,0 +1,27 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { CalendarEntryContentType } from './calendarEntryContentType';
import type { CalendarEntryId } from './calendarEntryId';
import type { CalendarEntryDescription } from './calendarEntryDescription';
import type { CalendarEntryPillar } from './calendarEntryPillar';
import type { CalendarEntryStatus } from './calendarEntryStatus';
import type { CalendarEntryIsManualEdit } from './calendarEntryIsManualEdit';
import type { CalendarEntryAiPromptSeed } from './calendarEntryAiPromptSeed';
export interface CalendarEntry {
dayOfWeek: number;
channel: string;
channelIcon: string;
contentType: CalendarEntryContentType;
title: string;
id?: CalendarEntryId;
description?: CalendarEntryDescription;
pillar?: CalendarEntryPillar;
status?: CalendarEntryStatus;
isManualEdit?: CalendarEntryIsManualEdit;
aiPromptSeed?: CalendarEntryAiPromptSeed;
}

View File

@ -5,4 +5,4 @@
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
export type PlanResponseBrandGuide = { [key: string]: unknown }; export type CalendarEntryAiPromptSeed = string | null;

View File

@ -0,0 +1,17 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export type CalendarEntryContentType = typeof CalendarEntryContentType[keyof typeof CalendarEntryContentType];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const CalendarEntryContentType = {
video: 'video',
blog: 'blog',
social: 'social',
ad: 'ad',
} as const;

View File

@ -5,4 +5,4 @@
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
export type PlanResponseContentStrategy = { [key: string]: unknown }; export type CalendarEntryDescription = string | null;

View File

@ -5,7 +5,4 @@
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
export interface PlanCreate { export type CalendarEntryId = string | null;
report_id: string;
regenerate?: boolean;
}

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export type CalendarEntryIsManualEdit = boolean | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export type CalendarEntryPillar = string | null;

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export type CalendarEntryStatus = 'draft' | 'approved' | 'published' | null;

View File

@ -0,0 +1,13 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { CalendarEntry } from './calendarEntry';
export interface CalendarWeek {
weekNumber: number;
label: string;
entries: CalendarEntry[];
}

View File

@ -0,0 +1,16 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { ChannelBrandingRuleCurrentStatus } from './channelBrandingRuleCurrentStatus';
export interface ChannelBrandingRule {
channel: string;
icon: string;
profilePhoto: string;
bannerSpec: string;
bioTemplate: string;
currentStatus: ChannelBrandingRuleCurrentStatus;
}

View File

@ -0,0 +1,16 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export type ChannelBrandingRuleCurrentStatus = typeof ChannelBrandingRuleCurrentStatus[keyof typeof ChannelBrandingRuleCurrentStatus];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const ChannelBrandingRuleCurrentStatus = {
correct: 'correct',
incorrect: 'incorrect',
missing: 'missing',
} as const;

View File

@ -0,0 +1,22 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { ChannelStrategyCardPriority } from './channelStrategyCardPriority';
import type { ChannelStrategyCardCustomerJourneyStage } from './channelStrategyCardCustomerJourneyStage';
export interface ChannelStrategyCard {
channelId: string;
channelName: string;
icon: string;
currentStatus: string;
targetGoal: string;
contentTypes: string[];
postingFrequency: string;
tone: string;
formatGuidelines: string[];
priority: ChannelStrategyCardPriority;
customerJourneyStage?: ChannelStrategyCardCustomerJourneyStage;
}

View File

@ -0,0 +1,8 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export type ChannelStrategyCardCustomerJourneyStage = 'awareness' | 'interest' | 'consideration' | 'conversion' | 'loyalty' | null;

View File

@ -0,0 +1,16 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export type ChannelStrategyCardPriority = typeof ChannelStrategyCardPriority[keyof typeof ChannelStrategyCardPriority];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const ChannelStrategyCardPriority = {
P0: 'P0',
P1: 'P1',
P2: 'P2',
} as const;

View File

@ -0,0 +1,12 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export interface ColorSwatch {
name: string;
hex: string;
usage: string;
}

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { ContentCountSummaryType } from './contentCountSummaryType';
export interface ContentCountSummary {
type: ContentCountSummaryType;
label: string;
count: number;
color: string;
}

View File

@ -0,0 +1,17 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export type ContentCountSummaryType = typeof ContentCountSummaryType[keyof typeof ContentCountSummaryType];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const ContentCountSummaryType = {
video: 'video',
blog: 'blog',
social: 'social',
ad: 'ad',
} as const;

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export interface ContentPillar {
title: string;
description: string;
relatedUSP: string;
exampleTopics: string[];
color: string;
}

View File

@ -0,0 +1,18 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { ContentPillar } from './contentPillar';
import type { ContentTypeRow } from './contentTypeRow';
import type { WorkflowStep } from './workflowStep';
import type { RepurposingOutput } from './repurposingOutput';
export interface ContentStrategyData {
pillars: ContentPillar[];
typeMatrix: ContentTypeRow[];
workflow: WorkflowStep[];
repurposingSource: string;
repurposingOutputs: RepurposingOutput[];
}

View File

@ -0,0 +1,13 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export interface ContentTypeRow {
format: string;
channels: string[];
frequency: string;
purpose: string;
}

View File

@ -0,0 +1,13 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export interface FontSpec {
family: string;
weight: string;
usage: string;
sampleText: string;
}

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { PlanOutput } from './planOutput';
export type GetPlan200 = PlanOutput | null;

View File

@ -12,7 +12,30 @@ export * from './analysisStatus';
export * from './analysisStatusResponse'; export * from './analysisStatusResponse';
export * from './analysisStatusResponseChannelErrors'; export * from './analysisStatusResponseChannelErrors';
export * from './analysisStatusResponseCompletedAt'; export * from './analysisStatusResponseCompletedAt';
export * from './assetCard';
export * from './assetCardSource';
export * from './assetCardStatus';
export * from './assetCardType';
export * from './assetCollectionData';
export * from './brandGuide';
export * from './brandInconsistency';
export * from './brandInconsistencyValue';
export * from './calendarData';
export * from './calendarEntry';
export * from './calendarEntryAiPromptSeed';
export * from './calendarEntryContentType';
export * from './calendarEntryDescription';
export * from './calendarEntryId';
export * from './calendarEntryIsManualEdit';
export * from './calendarEntryPillar';
export * from './calendarEntryStatus';
export * from './calendarWeek';
export * from './channelBrandingRule';
export * from './channelBrandingRuleCurrentStatus';
export * from './channelScore'; export * from './channelScore';
export * from './channelStrategyCard';
export * from './channelStrategyCardCustomerJourneyStage';
export * from './channelStrategyCardPriority';
export * from './channels'; export * from './channels';
export * from './channelsFacebook'; export * from './channelsFacebook';
export * from './channelsGangnamUnni'; export * from './channelsGangnamUnni';
@ -29,22 +52,37 @@ export * from './clinicResponseRawData';
export * from './clinicResponseRawDataAnyOf'; export * from './clinicResponseRawDataAnyOf';
export * from './clinicResponseRoadAddress'; export * from './clinicResponseRoadAddress';
export * from './clinicResponseUrl'; export * from './clinicResponseUrl';
export * from './colorSwatch';
export * from './contentCountSummary';
export * from './contentCountSummaryType';
export * from './contentPillar';
export * from './contentStrategyData';
export * from './contentTypeRow';
export * from './conversionStrategy'; export * from './conversionStrategy';
export * from './fontSpec';
export * from './getPlan200';
export * from './getReport200'; export * from './getReport200';
export * from './hTTPValidationError'; export * from './hTTPValidationError';
export * from './planCreate'; export * from './logoUsageRule';
export * from './planResponse'; export * from './planOutput';
export * from './planResponseBrandGuide'; export * from './planOutputRepurposingProposals';
export * from './planResponseContentStrategy';
export * from './reportOutput'; export * from './reportOutput';
export * from './reportOutputFacebook'; export * from './reportOutputFacebook';
export * from './reportOutputGangnamUnni'; export * from './reportOutputGangnamUnni';
export * from './reportOutputInstagram'; export * from './reportOutputInstagram';
export * from './reportOutputNaverBlog'; export * from './reportOutputNaverBlog';
export * from './reportOutputYoutube'; export * from './reportOutputYoutube';
export * from './repurposingOutput';
export * from './repurposingProposalItem';
export * from './repurposingProposalItemEstimatedEffort';
export * from './repurposingProposalItemPriority';
export * from './runSummary'; export * from './runSummary';
export * from './runSummaryCompletedAt'; export * from './runSummaryCompletedAt';
export * from './runSummaryOverallScore'; export * from './runSummaryOverallScore';
export * from './toneOfVoice';
export * from './validationError'; export * from './validationError';
export * from './validationErrorCtx'; export * from './validationErrorCtx';
export * from './validationErrorLocItem'; export * from './validationErrorLocItem';
export * from './workflowStep';
export * from './youTubeRepurposeItem';
export * from './youTubeRepurposeItemType';

View File

@ -0,0 +1,12 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export interface LogoUsageRule {
rule: string;
description: string;
correct: boolean;
}

View File

@ -0,0 +1,21 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { BrandGuide } from './brandGuide';
import type { ChannelStrategyCard } from './channelStrategyCard';
import type { ContentStrategyData } from './contentStrategyData';
import type { CalendarData } from './calendarData';
import type { AssetCollectionData } from './assetCollectionData';
import type { PlanOutputRepurposingProposals } from './planOutputRepurposingProposals';
export interface PlanOutput {
brandGuide: BrandGuide;
channelStrategies: ChannelStrategyCard[];
contentStrategy: ContentStrategyData;
calendar: CalendarData;
assetCollection: AssetCollectionData;
repurposingProposals?: PlanOutputRepurposingProposals;
}

View File

@ -0,0 +1,9 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { RepurposingProposalItem } from './repurposingProposalItem';
export type PlanOutputRepurposingProposals = RepurposingProposalItem[] | null;

View File

@ -1,18 +0,0 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { PlanResponseBrandGuide } from './planResponseBrandGuide';
import type { PlanResponseContentStrategy } from './planResponseContentStrategy';
export interface PlanResponse {
id: string;
analysis_run_id: string;
brand_guide: PlanResponseBrandGuide;
channel_strategies: unknown[];
content_strategy: PlanResponseContentStrategy;
calendar: unknown[];
created_at: string;
}

View File

@ -0,0 +1,12 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export interface RepurposingOutput {
format: string;
channel: string;
description: string;
}

View File

@ -0,0 +1,17 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { YouTubeRepurposeItem } from './youTubeRepurposeItem';
import type { RepurposingOutput } from './repurposingOutput';
import type { RepurposingProposalItemEstimatedEffort } from './repurposingProposalItemEstimatedEffort';
import type { RepurposingProposalItemPriority } from './repurposingProposalItemPriority';
export interface RepurposingProposalItem {
sourceVideo: YouTubeRepurposeItem;
outputs: RepurposingOutput[];
estimatedEffort: RepurposingProposalItemEstimatedEffort;
priority: RepurposingProposalItemPriority;
}

View File

@ -0,0 +1,16 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export type RepurposingProposalItemEstimatedEffort = typeof RepurposingProposalItemEstimatedEffort[keyof typeof RepurposingProposalItemEstimatedEffort];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const RepurposingProposalItemEstimatedEffort = {
low: 'low',
medium: 'medium',
high: 'high',
} as const;

View File

@ -0,0 +1,16 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export type RepurposingProposalItemPriority = typeof RepurposingProposalItemPriority[keyof typeof RepurposingProposalItemPriority];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const RepurposingProposalItemPriority = {
high: 'high',
medium: 'medium',
low: 'low',
} as const;

View File

@ -0,0 +1,13 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export interface ToneOfVoice {
personality: string[];
communicationStyle: string;
doExamples: string[];
dontExamples: string[];
}

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export interface WorkflowStep {
step: number;
name: string;
description: string;
owner: string;
duration: string;
}

View File

@ -0,0 +1,14 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { YouTubeRepurposeItemType } from './youTubeRepurposeItemType';
export interface YouTubeRepurposeItem {
title: string;
views: number;
type: YouTubeRepurposeItemType;
repurposeAs: string[];
}

View File

@ -0,0 +1,15 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export type YouTubeRepurposeItemType = typeof YouTubeRepurposeItemType[keyof typeof YouTubeRepurposeItemType];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const YouTubeRepurposeItemType = {
Short: 'Short',
Long: 'Long',
} as const;