feat: 백엔드 신스키마 (PlanApiResponse / MarketingReportResponse) 반영 + plan 페이지 통합

- orval SDK 재생성:
  · generated/plans → generated/plan, generated/reports → generated/report
  · planOutput → planApiResponse (clinicName/clinicNameEn/screenshots 등 메타 포함)
  · reportOutput → marketingReportResponse (clinicSnapshot 등 풍부한 스키마 복원)
  · clinicSnapshot, screenshotEvidence, transformationProposal, websiteAudit 등 모델 신설
  · clinicListResponse, fileUploadResponse 등 운영/업로드 모델 신설
- useMarketingPlan / useReport: SDK 응답을 사실상 그대로 패스스루 (메타 매핑 단순화)
- transformReport: 신 SDK 모델 시그니처 반영
- plan 페이지 통합: GuestPlanPage + UserPlanPage → 단일 PlanPage 로 합치고 routes 갱신
- dev 페이지 (ClinicsPage / routes) 갱신
- MyAssetUpload 컴포넌트 + constants 업데이트

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
main
Mina Choi 2026-05-19 16:52:39 +09:00
parent 82f166a80e
commit 0b139714ed
109 changed files with 1916 additions and 458 deletions

View File

@ -1,12 +1,12 @@
/**
* Dev: .
*
* `GET /api/clinics` (listClinics) .
* `GET /api/clinics` (getClinics) .
* DevOnly .
*/
import { useState } from 'react';
import { Link } from 'react-router';
import { useListClinics } from '@/shared/api/generated/clinics/clinics';
import { useGetClinics } from '@/shared/api/generated/clinics/clinics';
import { PageContainer } from '@/shared/ui/page-container';
import { Spinner } from '@/shared/ui/spinner';
import { EmptyState } from '@/shared/ui/empty-state';
@ -31,12 +31,13 @@ function formatDate(raw: string): string {
export default function ClinicsPage() {
const [offset, setOffset] = useState(0);
const { data, isLoading, error, refetch, isFetching } = useListClinics(
const { data, isLoading, error, refetch, isFetching } = useGetClinics(
{ limit: PAGE_SIZE, offset },
{ query: { staleTime: 0 } },
);
const items = data?.status === 200 ? data.data : [];
const items = data?.status === 200 ? data.data.items : [];
const total = data?.status === 200 ? data.data.total : 0;
return (
<div className="pt-20 pb-16">
@ -50,7 +51,7 @@ export default function ClinicsPage() {
</h1>
<p className="text-sm text-slate-500 mt-1">
GET /api/clinics · limit={PAGE_SIZE} · offset={offset}
GET /api/clinics · limit={PAGE_SIZE} · offset={offset} · total={total}
</p>
</div>
<div className="flex items-center gap-2">
@ -131,8 +132,10 @@ export default function ClinicsPage() {
</div>
)}
{/* 페이지네이션 — 정확한 total 이 없어서 단순 prev/next 만 노출 */}
<div className="flex items-center justify-end gap-2 mt-4">
<span className="text-xs text-slate-500 mr-2">
{total > 0 ? `${offset + 1}-${Math.min(offset + items.length, total)} / ${total}` : ''}
</span>
<Button
variant="outline"
size="sm"
@ -145,7 +148,7 @@ export default function ClinicsPage() {
variant="outline"
size="sm"
onClick={() => setOffset((o) => o + PAGE_SIZE)}
disabled={items.length < PAGE_SIZE || isFetching}
disabled={offset + items.length >= total || isFetching}
>
</Button>

View File

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

View File

@ -1,10 +1,26 @@
import { useState, useRef, useCallback, type DragEvent, type ChangeEvent } from 'react';
import { useState, useRef, useCallback, useEffect, type DragEvent, type ChangeEvent } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { SectionWrapper } from '@/features/report/components/ui/SectionWrapper';
import { VideoFilled, FileTextFilled } from '@/shared/icons/FilledIcons';
import { Button } from '@/shared/ui/button';
import { Spinner } from '@/shared/ui/spinner';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/shared/ui/dialog';
import {
useUploadAnalysisRunFile,
useGetAnalysisRunFiles,
useDeleteAnalysisRunFile,
} from '@/shared/api/generated/analysis/analysis';
import type { FileListItem } from '@/shared/api/model/fileListItem';
import {
type UploadCategory,
type AssetCategory,
categoryConfig,
categoryBadge,
ALL_ACCEPT,
@ -15,43 +31,141 @@ import {
// ─── Types ───
type AssetStatus = 'idle' | 'uploading' | 'done' | 'error';
interface UploadedAsset {
id: string;
file: File;
category: 'image' | 'video' | 'text';
/** 로컬 업로드 직후엔 File 보유, 서버에서 받은 항목은 null. */
file: File | null;
category: AssetCategory;
previewUrl: string | null;
name: string;
size: string;
uploadedAt: Date;
status: AssetStatus;
remoteId?: number;
errorMessage?: string;
}
interface MyAssetUploadProps {
/** 업로드 대상 analysis run id. 없으면 로컬 미리보기만 동작. */
analysisRunId?: string;
}
// 백엔드 FileType ('image'|'video'|'audio'|'document'|'file') → 로컬 AssetCategory 1:1.
function mapFileTypeToCategory(fileType: string): AssetCategory {
if (fileType === 'image' || fileType === 'video' || fileType === 'audio' || fileType === 'document') {
return fileType;
}
return 'file';
}
function serverFileToAsset(item: FileListItem): UploadedAsset {
const cat = mapFileTypeToCategory(item.file_type);
return {
id: `remote-${item.id}`,
file: null,
category: cat,
previewUrl: cat === 'image' || cat === 'video' ? item.file_url : null,
name: item.file_name,
size: typeof item.size_bytes === 'number' ? formatSize(item.size_bytes) : '',
uploadedAt: new Date(item.created_at),
status: 'done',
remoteId: item.id,
};
}
// ─── Component ───
export default function MyAssetUpload() {
export default function MyAssetUpload({ analysisRunId }: MyAssetUploadProps = {}) {
const [assets, setAssets] = useState<UploadedAsset[]>([]);
const [activeFilter, setActiveFilter] = useState<UploadCategory>('all');
const [isDragOver, setIsDragOver] = useState(false);
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const processFiles = useCallback((files: FileList | File[]) => {
const newAssets: UploadedAsset[] = Array.from(files).map((file) => {
const cat = categorize(file);
const previewUrl =
cat === 'image' || cat === 'video'
? URL.createObjectURL(file)
: null;
return {
id: uid(),
file,
category: cat,
previewUrl,
name: file.name,
size: formatSize(file.size),
uploadedAt: new Date(),
};
const { mutateAsync: uploadFile } = useUploadAnalysisRunFile();
const { mutateAsync: deleteFile } = useDeleteAnalysisRunFile();
const filesQuery = useGetAnalysisRunFiles(
analysisRunId ?? '',
{ query: { enabled: !!analysisRunId } },
);
// 서버 파일 목록 → 로컬 assets 머지. 진행 중/실패 로컬 항목은 보존.
useEffect(() => {
if (filesQuery.data?.status !== 200) return;
const serverAssets = filesQuery.data.data.map(serverFileToAsset);
const serverIds = new Set(serverAssets.map((a) => a.remoteId));
setAssets((prev) => {
const localPending = prev.filter(
(a) => a.remoteId == null || !serverIds.has(a.remoteId),
);
return [...localPending, ...serverAssets];
});
setAssets((prev) => [...newAssets, ...prev]);
}, []);
}, [filesQuery.data]);
const processFiles = useCallback(
(files: FileList | File[]) => {
const newAssets: UploadedAsset[] = Array.from(files).map((file) => {
const cat = categorize(file);
const previewUrl =
cat === 'image' || cat === 'video' ? URL.createObjectURL(file) : null;
return {
id: uid(),
file,
category: cat,
previewUrl,
name: file.name,
size: formatSize(file.size),
uploadedAt: new Date(),
status: analysisRunId ? 'uploading' : 'idle',
};
});
setAssets((prev) => [...newAssets, ...prev]);
if (!analysisRunId) return;
newAssets.forEach(async (asset) => {
try {
const res = await uploadFile({
runId: analysisRunId,
// orval 이 binary 필드를 string 으로 타입 생성하므로 캐스팅 필요. 런타임은 FormData 로 정상 처리.
data: { file: asset.file as unknown as string, file_type: asset.category },
});
if (res.status === 201) {
setAssets((prev) =>
prev.map((a) =>
a.id === asset.id ? { ...a, status: 'done', remoteId: res.data.id } : a,
),
);
filesQuery.refetch();
} else {
setAssets((prev) =>
prev.map((a) =>
a.id === asset.id
? { ...a, status: 'error', errorMessage: '업로드 검증 실패' }
: a,
),
);
}
} catch (err) {
setAssets((prev) =>
prev.map((a) =>
a.id === asset.id
? {
...a,
status: 'error',
errorMessage: err instanceof Error ? err.message : '업로드 실패',
}
: a,
),
);
}
});
},
[analysisRunId, uploadFile, filesQuery],
);
const handleDrop = useCallback(
(e: DragEvent) => {
@ -72,22 +186,48 @@ export default function MyAssetUpload() {
[processFiles],
);
const removeAsset = useCallback((id: string) => {
setAssets((prev) => {
const found = prev.find((a) => a.id === id);
if (found?.previewUrl) URL.revokeObjectURL(found.previewUrl);
return prev.filter((a) => a.id !== id);
});
}, []);
const confirmDelete = useCallback(async () => {
if (!pendingDeleteId) return;
const target = assets.find((a) => a.id === pendingDeleteId);
if (!target) {
setPendingDeleteId(null);
return;
}
setIsDeleting(true);
if (target.file && target.previewUrl) URL.revokeObjectURL(target.previewUrl);
if (analysisRunId && target.remoteId != null) {
try {
await deleteFile({ runId: analysisRunId, fileId: target.remoteId });
filesQuery.refetch();
} catch {
setIsDeleting(false);
setPendingDeleteId(null);
return;
}
}
setAssets((prev) => prev.filter((a) => a.id !== pendingDeleteId));
setIsDeleting(false);
setPendingDeleteId(null);
}, [pendingDeleteId, assets, analysisRunId, deleteFile, filesQuery]);
const pendingDeleteAsset = pendingDeleteId
? assets.find((a) => a.id === pendingDeleteId) ?? null
: null;
const filtered =
activeFilter === 'all' ? assets : assets.filter((a) => a.category === activeFilter);
const counts = {
const counts: Record<UploadCategory, number> = {
all: assets.length,
image: assets.filter((a) => a.category === 'image').length,
video: assets.filter((a) => a.category === 'video').length,
text: assets.filter((a) => a.category === 'text').length,
audio: assets.filter((a) => a.category === 'audio').length,
document: assets.filter((a) => a.category === 'document').length,
file: assets.filter((a) => a.category === 'file').length,
};
return (
@ -143,17 +283,17 @@ export default function MyAssetUpload() {
</p>
<p className="text-sm text-slate-400">
Image, Video, Text (JPG, PNG, MP4, MOV, TXT, PDF, DOC )
Image, Video, Audio, Document, File (JPG, MP4, MP3, PDF, DOCX, ZIP )
</p>
{/* File Type Badges */}
<div className="flex justify-center gap-2 mt-4">
{(['image', 'video', 'text'] as const).map((cat) => (
<div className="flex flex-wrap justify-center gap-2 mt-4">
{(['image', 'video', 'audio', 'document', 'file'] as const).map((cat) => (
<span
key={cat}
className={`rounded-full px-3 py-1 text-xs font-medium ${categoryBadge[cat]}`}
>
{cat === 'image' ? 'Image' : cat === 'video' ? 'Video' : 'Text'}
{categoryConfig[cat].label}
</span>
))}
</div>
@ -216,7 +356,9 @@ export default function MyAssetUpload() {
}}
/>
)}
{asset.category === 'text' && (
{(asset.category === 'audio' ||
asset.category === 'document' ||
asset.category === 'file') && (
<div className="flex flex-col items-center gap-2">
<FileTextFilled size={36} className="text-[#D4A872]" />
<span className="text-xs text-slate-400 font-medium">
@ -230,7 +372,7 @@ export default function MyAssetUpload() {
type="button"
variant="ghost"
size="icon-sm"
onClick={() => removeAsset(asset.id)}
onClick={() => setPendingDeleteId(asset.id)}
className="absolute top-2 right-2 w-7 h-7 size-7 rounded-full bg-white/90 backdrop-blur-sm border border-slate-100 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity shadow-sm hover:bg-brand-rose-bg"
>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
@ -242,11 +384,7 @@ export default function MyAssetUpload() {
<span
className={`absolute top-2 left-2 rounded-full px-3 py-1 text-xs font-semibold ${categoryBadge[asset.category]}`}
>
{asset.category === 'image'
? 'Image'
: asset.category === 'video'
? 'Video'
: 'Text'}
{categoryConfig[asset.category].label}
</span>
{/* Video Duration Overlay */}
@ -263,7 +401,28 @@ export default function MyAssetUpload() {
<p className="text-sm font-medium text-brand-navy truncate mb-1">
{asset.name}
</p>
<p className="text-xs text-slate-400">{asset.size}</p>
<div className="flex items-center justify-between gap-2">
<p className="text-xs text-slate-400">
{asset.size || categoryConfig[asset.category].label}
</p>
{asset.status === 'uploading' && (
<span className="inline-flex items-center gap-1 text-xs text-slate-500">
<Spinner size="xs" />
</span>
)}
{asset.status === 'done' && (
<span className="text-xs text-emerald-600"></span>
)}
{asset.status === 'error' && (
<span
className="text-xs text-[#7C3A4B] truncate"
title={asset.errorMessage}
>
</span>
)}
</div>
</div>
</motion.div>
))}
@ -271,6 +430,43 @@ export default function MyAssetUpload() {
</div>
</>
)}
<Dialog
open={pendingDeleteId !== null}
onOpenChange={(open) => {
if (!open && !isDeleting) setPendingDeleteId(null);
}}
>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle> ?</DialogTitle>
<DialogDescription>
{pendingDeleteAsset
? `"${pendingDeleteAsset.name}" 을(를) 삭제합니다. 이 동작은 되돌릴 수 없습니다.`
: '이 동작은 되돌릴 수 없습니다.'}
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2 sm:gap-2">
<Button
type="button"
variant="outline"
onClick={() => setPendingDeleteId(null)}
disabled={isDeleting}
>
</Button>
<Button
type="button"
onClick={confirmDelete}
disabled={isDeleting}
className="bg-[#7C3A4B] hover:bg-[#7C3A4B]/90 text-white"
>
{isDeleting ? <Spinner size="xs" className="mr-1" /> : null}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</SectionWrapper>
);
}

View File

@ -1,30 +1,54 @@
export type UploadCategory = 'all' | 'image' | 'video' | 'text';
/**
* FileType 1:1 5 .
* image jpg, png, gif, webp, heic
* video mp4, mov, avi, webm
* audio mp3, wav, m4a, ogg
* document pdf, docx, xlsx, pptx, txt, csv, hwp
* file / (zip, json )
*/
export type AssetCategory = 'image' | 'video' | 'audio' | 'document' | 'file';
export type UploadCategory = 'all' | AssetCategory;
export const categoryConfig: Record<UploadCategory, { label: string }> = {
all: { label: '전체' },
image: { label: 'Image' },
video: { label: 'Video' },
text: { label: 'Text' },
audio: { label: 'Audio' },
document: { label: 'Document' },
file: { label: 'File' },
};
export const categoryBadge: Record<'image' | 'video' | 'text', string> = {
export const categoryBadge: Record<AssetCategory, string> = {
image: 'bg-brand-tint-purple text-brand-purple-muted shadow-[2px_3px_6px_rgba(155,138,212,0.12)]',
video: 'bg-brand-rose-bg text-brand-rose shadow-[2px_3px_6px_rgba(212,136,154,0.12)]',
text: 'bg-brand-earth-bg text-brand-earth shadow-[2px_3px_6px_rgba(212,168,114,0.12)]',
audio: 'bg-sky-50 text-sky-700 shadow-[2px_3px_6px_rgba(56,189,248,0.12)]',
document: 'bg-brand-earth-bg text-brand-earth shadow-[2px_3px_6px_rgba(212,168,114,0.12)]',
file: 'bg-slate-100 text-slate-600 shadow-[2px_3px_6px_rgba(100,116,139,0.10)]',
};
export const ACCEPT_MAP: Record<string, string> = {
'image/*': '.jpg,.jpeg,.png,.gif,.webp,.svg',
'video/*': '.mp4,.mov,.webm,.avi',
'text/*': '.txt,.md,.doc,.docx,.pdf,.csv,.json',
// 확장자 → 카테고리. 백엔드 분류 규칙과 동일.
const EXT_BY_CATEGORY: Record<AssetCategory, string[]> = {
image: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic'],
video: ['mp4', 'mov', 'avi', 'webm'],
audio: ['mp3', 'wav', 'm4a', 'ogg'],
document: ['pdf', 'docx', 'xlsx', 'pptx', 'txt', 'csv', 'hwp'],
file: [], // 폴백
};
export const ALL_ACCEPT = Object.values(ACCEPT_MAP).join(',');
export const ALL_ACCEPT = (Object.entries(EXT_BY_CATEGORY) as [AssetCategory, string[]][])
.flatMap(([, exts]) => exts.map((e) => `.${e}`))
.join(',');
export function categorize(file: File): 'image' | 'video' | 'text' {
export function categorize(file: File): AssetCategory {
const ext = file.name.split('.').pop()?.toLowerCase() ?? '';
for (const [cat, exts] of Object.entries(EXT_BY_CATEGORY) as [AssetCategory, string[]][]) {
if (exts.includes(ext)) return cat;
}
// MIME 폴백 — 확장자 누락된 케이스 보완.
if (file.type.startsWith('image/')) return 'image';
if (file.type.startsWith('video/')) return 'video';
return 'text';
if (file.type.startsWith('audio/')) return 'audio';
return 'file';
}
export function formatSize(bytes: number): string {

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import type { MarketingPlan } from '@/features/plan/types/plan';
import { getPlan } from '@/shared/api/generated/plans/plans';
import { getPlan } from '@/shared/api/generated/plan/plan';
interface UseMarketingPlanResult {
data: MarketingPlan | null;
@ -32,19 +32,14 @@ export function useMarketingPlan(id: string | undefined): UseMarketingPlanResult
throw new Error('마케팅 기획이 아직 생성되지 않았습니다.');
}
// SDK PlanApiResponse 가 MarketingPlan 과 사실상 동일 — 패스스루.
// reportId/workflow 는 SDK 에 없는 로컬 전용 필드.
setData({
id: id!,
...(planOutput as unknown as MarketingPlan),
id: planOutput.id || id!,
reportId: id!,
clinicName: '',
clinicNameEn: '',
createdAt: '',
targetUrl: '',
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'],
clinicName: planOutput.clinicName ?? '',
clinicNameEn: planOutput.clinicNameEn ?? '',
});
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch marketing plan');

View File

@ -1,64 +0,0 @@
/**
* GuestPlanPage `/plan/:id`
*
* ( ) . UserPlanPage ,
* ( / /)
* CTA(PlanCTA) .
*/
import { useEffect } from 'react';
import { useParams, useLocation } from 'react-router';
import { useMarketingPlan } from '../hooks/useMarketingPlan';
import { ReportNav } from '@/features/report/components/ReportNav';
import { PLAN_SECTIONS } from '@/shared/constants/planSections';
import PlanBody from '../components/PlanBody';
export default function GuestPlanPage() {
const { id } = useParams<{ id: string }>();
const location = useLocation();
const { data, isLoading, error } = useMarketingPlan(id);
// 해시 기반 스크롤: /plan/:id#section-id → 렌더링 후 해당 섹션으로
useEffect(() => {
if (isLoading || !location.hash) return;
const sectionId = location.hash.slice(1);
const timer = setTimeout(() => {
const el = document.getElementById(sectionId);
if (!el) return;
const STICKY_OFFSET = 128;
const y = el.getBoundingClientRect().top + window.scrollY - STICKY_OFFSET;
window.scrollTo({ top: y, behavior: 'smooth' });
}, 300);
return () => clearTimeout(timer);
}, [isLoading, location.hash]);
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center pt-20">
<div className="flex flex-col items-center gap-4">
<div className="w-10 h-10 border-4 border-[#6C5CE7] border-t-transparent rounded-full animate-spin" />
<p className="text-slate-500 text-sm"> ...</p>
</div>
</div>
);
}
if (error || !data) {
return (
<div className="min-h-screen flex items-center justify-center pt-20">
<div className="text-center">
<p className="text-[#7C3A4B] font-medium mb-2"> </p>
<p className="text-slate-500 text-sm">
{error ?? '마케팅 기획을 찾을 수 없습니다.'}
</p>
</div>
</div>
);
}
return (
<div className="pt-20">
<ReportNav sections={PLAN_SECTIONS} />
<PlanBody data={data} />
</div>
);
}

View File

@ -1,12 +1,12 @@
/**
* UserPlanPage `/clinics/:clinicId/plan/:id`
* PlanPage ( ).
*
* .
* GuestPlanPage + +
* (MyAssetUpload / WorkflowTracker).
* :
* - `/plan/:id` ( )
* - `/clinics/:clinicId/plan/:id` ( )
*
* NOTE: StrategyAdjustmentSection( )
* , . import/render .
* (PlanBody) + (MyAssetUpload / WorkflowTracker) + CTA .
* analysisRunId(=URL :id) SDK .
*/
import { useEffect } from 'react';
import { useParams, useLocation } from 'react-router';
@ -15,15 +15,15 @@ import { ReportNav } from '@/features/report/components/ReportNav';
import { PLAN_SECTIONS } from '@/shared/constants/planSections';
import PlanBody from '../components/PlanBody';
import MyAssetUpload from '../components/MyAssetUpload';
// import StrategyAdjustmentSection from '../components/StrategyAdjustmentSection';
import WorkflowTracker from '../components/WorkflowTracker';
import PlanCTA from '../components/PlanCTA';
export default function UserPlanPage() {
const { id } = useParams<{ clinicId: string; id: string }>();
export default function PlanPage() {
const { id } = useParams<{ clinicId?: string; id: string }>();
const location = useLocation();
// const stateClinicId = (location.state as { clinicId?: string } | undefined)?.clinicId || null;
const { data, isLoading, error } = useMarketingPlan(id);
// 해시 기반 스크롤: /plan/:id#section-id → 렌더링 후 해당 섹션으로
useEffect(() => {
if (isLoading || !location.hash) return;
const sectionId = location.hash.slice(1);
@ -65,7 +65,6 @@ export default function UserPlanPage() {
<PlanBody data={data} />
{/* 유저 전용 인터랙티브 섹션 */}
{data.workflow && (
<div data-no-print>
<WorkflowTracker data={data.workflow} />
@ -73,13 +72,10 @@ export default function UserPlanPage() {
)}
<div data-no-print>
<MyAssetUpload />
<MyAssetUpload analysisRunId={id} />
</div>
{/* 성과 기반 전략 조정 — 본 차수 미포함, 후속 모듈 이관 */}
{/* <div data-no-print>
<StrategyAdjustmentSection clinicId={clinicId ?? stateClinicId} planId={data.id} />
</div> */}
<PlanCTA />
</div>
);
}

View File

@ -1,9 +1,10 @@
import { lazy } from 'react'
import type { RouteObject } from 'react-router'
const GuestPlanPage = lazy(() => import('./pages/GuestPlanPage'))
const PlanPage = lazy(() => import('./pages/PlanPage'))
export const planRoutes: RouteObject[] = [
// 손님(랜딩→분석→리포트→플랜) 흐름. 유저 워크스페이스 경로는 features/clinics/routes.tsx 참조.
{ path: 'plan/:id', element: <GuestPlanPage /> },
// `/plan/:id` 와 `/clinics/:clinicId/plan/:id` 모두 동일 PlanPage 사용.
// 워크스페이스 경로 정의는 features/clinics/routes.tsx 참조.
{ path: 'plan/:id', element: <PlanPage /> },
]

View File

@ -1,7 +1,6 @@
import { useState, useEffect } from 'react';
import type { MarketingReport } from '@/features/report/types/report';
import { getReport } from '@/shared/api/generated/reports/reports';
import { transformReportOutput } from '@/features/report/lib/transformReport';
import { getReport } from '@/shared/api/generated/report/report';
interface UseReportResult {
data: MarketingReport | null;
@ -33,8 +32,9 @@ export function useReport(id: string | undefined): UseReportResult {
if (!output) {
throw new Error('리포트 데이터가 비어있습니다.');
}
const transformed = transformReportOutput(id, output, { url: '', generatedAt: '' });
setData(transformed);
// SDK MarketingReportResponse 가 사실상 MarketingReport 와 같은 shape — 그대로 사용.
// 미세한 차이(ScreenshotEvidence 등)는 컴포넌트 단에서 옵셔널로 처리됨.
setData(output as unknown as MarketingReport);
})
.catch((err) => {
setError(err instanceof Error ? err.message : 'Failed to fetch report');

View File

@ -1,6 +1,4 @@
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 .
@ -1269,203 +1267,5 @@ export function mergeEnrichment(
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: [],
};
}
// SDK MarketingReportResponse ≈ MarketingReport — 별도 변환 함수 없이 useReport 에서
// 직접 캐스팅해 사용. (구버전 ReportOutput 매핑 코드 제거)

View File

@ -27,6 +27,9 @@ import type {
AnalysisCreate,
AnalysisStartResponse,
AnalysisStatusResponse,
BodyUploadAnalysisRunFileApiAnalysisRunIdFilesPost,
FileListItem,
FileUploadResponse,
HTTPValidationError
} from '../../model';
@ -128,6 +131,312 @@ export const useStartAnalysis = <TError = HTTPValidationError,
return useMutation(mutationOptions, queryClient);
}
/**
* @summary Upload Analysis Run File
*/
export type uploadAnalysisRunFileResponse201 = {
data: FileUploadResponse
status: 201
}
export type uploadAnalysisRunFileResponse422 = {
data: HTTPValidationError
status: 422
}
export type uploadAnalysisRunFileResponseSuccess = (uploadAnalysisRunFileResponse201) & {
headers: Headers;
};
export type uploadAnalysisRunFileResponseError = (uploadAnalysisRunFileResponse422) & {
headers: Headers;
};
export type uploadAnalysisRunFileResponse = (uploadAnalysisRunFileResponseSuccess | uploadAnalysisRunFileResponseError)
export const getUploadAnalysisRunFileUrl = (runId: string,) => {
return `/api/analysis/${runId}/files`
}
export const uploadAnalysisRunFile = async (runId: string,
bodyUploadAnalysisRunFileApiAnalysisRunIdFilesPost: BodyUploadAnalysisRunFileApiAnalysisRunIdFilesPost, options?: RequestInit): Promise<uploadAnalysisRunFileResponse> => {
const formData = new FormData();
formData.append(`file`, bodyUploadAnalysisRunFileApiAnalysisRunIdFilesPost.file)
if(bodyUploadAnalysisRunFileApiAnalysisRunIdFilesPost.file_type !== undefined) {
formData.append(`file_type`, bodyUploadAnalysisRunFileApiAnalysisRunIdFilesPost.file_type)
}
return customFetcher<uploadAnalysisRunFileResponse>(getUploadAnalysisRunFileUrl(runId),
{
...options,
method: 'POST'
,
body:
formData,
}
);}
export const getUploadAnalysisRunFileMutationOptions = <TError = HTTPValidationError,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof uploadAnalysisRunFile>>, TError,{runId: string;data: BodyUploadAnalysisRunFileApiAnalysisRunIdFilesPost}, TContext>, request?: SecondParameter<typeof customFetcher>}
): UseMutationOptions<Awaited<ReturnType<typeof uploadAnalysisRunFile>>, TError,{runId: string;data: BodyUploadAnalysisRunFileApiAnalysisRunIdFilesPost}, TContext> => {
const mutationKey = ['uploadAnalysisRunFile'];
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 uploadAnalysisRunFile>>, {runId: string;data: BodyUploadAnalysisRunFileApiAnalysisRunIdFilesPost}> = (props) => {
const {runId,data} = props ?? {};
return uploadAnalysisRunFile(runId,data,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type UploadAnalysisRunFileMutationResult = NonNullable<Awaited<ReturnType<typeof uploadAnalysisRunFile>>>
export type UploadAnalysisRunFileMutationBody = BodyUploadAnalysisRunFileApiAnalysisRunIdFilesPost
export type UploadAnalysisRunFileMutationError = HTTPValidationError
/**
* @summary Upload Analysis Run File
*/
export const useUploadAnalysisRunFile = <TError = HTTPValidationError,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof uploadAnalysisRunFile>>, TError,{runId: string;data: BodyUploadAnalysisRunFileApiAnalysisRunIdFilesPost}, TContext>, request?: SecondParameter<typeof customFetcher>}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof uploadAnalysisRunFile>>,
TError,
{runId: string;data: BodyUploadAnalysisRunFileApiAnalysisRunIdFilesPost},
TContext
> => {
const mutationOptions = getUploadAnalysisRunFileMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
/**
* @summary Get Analysis Run Files
*/
export type getAnalysisRunFilesResponse200 = {
data: FileListItem[]
status: 200
}
export type getAnalysisRunFilesResponse422 = {
data: HTTPValidationError
status: 422
}
export type getAnalysisRunFilesResponseSuccess = (getAnalysisRunFilesResponse200) & {
headers: Headers;
};
export type getAnalysisRunFilesResponseError = (getAnalysisRunFilesResponse422) & {
headers: Headers;
};
export type getAnalysisRunFilesResponse = (getAnalysisRunFilesResponseSuccess | getAnalysisRunFilesResponseError)
export const getGetAnalysisRunFilesUrl = (runId: string,) => {
return `/api/analysis/${runId}/files`
}
export const getAnalysisRunFiles = async (runId: string, options?: RequestInit): Promise<getAnalysisRunFilesResponse> => {
return customFetcher<getAnalysisRunFilesResponse>(getGetAnalysisRunFilesUrl(runId),
{
...options,
method: 'GET'
}
);}
export const getGetAnalysisRunFilesQueryKey = (runId?: string,) => {
return [
`/api/analysis/${runId}/files`
] as const;
}
export const getGetAnalysisRunFilesQueryOptions = <TData = Awaited<ReturnType<typeof getAnalysisRunFiles>>, TError = HTTPValidationError>(runId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getAnalysisRunFiles>>, TError, TData>>, request?: SecondParameter<typeof customFetcher>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetAnalysisRunFilesQueryKey(runId);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getAnalysisRunFiles>>> = () => getAnalysisRunFiles(runId, requestOptions);
return { queryKey, queryFn, enabled: !!(runId), staleTime: 60000, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getAnalysisRunFiles>>, TError, TData> & { queryKey: DataTag<QueryKey, TData> }
}
export type GetAnalysisRunFilesQueryResult = NonNullable<Awaited<ReturnType<typeof getAnalysisRunFiles>>>
export type GetAnalysisRunFilesQueryError = HTTPValidationError
export function useGetAnalysisRunFiles<TData = Awaited<ReturnType<typeof getAnalysisRunFiles>>, TError = HTTPValidationError>(
runId: string, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getAnalysisRunFiles>>, TError, TData>> & Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof getAnalysisRunFiles>>,
TError,
Awaited<ReturnType<typeof getAnalysisRunFiles>>
> , 'initialData'
>, request?: SecondParameter<typeof customFetcher>}
, queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> }
export function useGetAnalysisRunFiles<TData = Awaited<ReturnType<typeof getAnalysisRunFiles>>, TError = HTTPValidationError>(
runId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getAnalysisRunFiles>>, TError, TData>> & Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof getAnalysisRunFiles>>,
TError,
Awaited<ReturnType<typeof getAnalysisRunFiles>>
> , 'initialData'
>, request?: SecondParameter<typeof customFetcher>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> }
export function useGetAnalysisRunFiles<TData = Awaited<ReturnType<typeof getAnalysisRunFiles>>, TError = HTTPValidationError>(
runId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getAnalysisRunFiles>>, TError, TData>>, request?: SecondParameter<typeof customFetcher>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> }
/**
* @summary Get Analysis Run Files
*/
export function useGetAnalysisRunFiles<TData = Awaited<ReturnType<typeof getAnalysisRunFiles>>, TError = HTTPValidationError>(
runId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getAnalysisRunFiles>>, TError, TData>>, request?: SecondParameter<typeof customFetcher>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> } {
const queryOptions = getGetAnalysisRunFilesQueryOptions(runId,options)
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> };
query.queryKey = queryOptions.queryKey ;
return query;
}
/**
* @summary Delete Analysis Run File
*/
export type deleteAnalysisRunFileResponse204 = {
data: void
status: 204
}
export type deleteAnalysisRunFileResponse422 = {
data: HTTPValidationError
status: 422
}
export type deleteAnalysisRunFileResponseSuccess = (deleteAnalysisRunFileResponse204) & {
headers: Headers;
};
export type deleteAnalysisRunFileResponseError = (deleteAnalysisRunFileResponse422) & {
headers: Headers;
};
export type deleteAnalysisRunFileResponse = (deleteAnalysisRunFileResponseSuccess | deleteAnalysisRunFileResponseError)
export const getDeleteAnalysisRunFileUrl = (runId: string,
fileId: number,) => {
return `/api/analysis/${runId}/files/${fileId}`
}
export const deleteAnalysisRunFile = async (runId: string,
fileId: number, options?: RequestInit): Promise<deleteAnalysisRunFileResponse> => {
return customFetcher<deleteAnalysisRunFileResponse>(getDeleteAnalysisRunFileUrl(runId,fileId),
{
...options,
method: 'DELETE'
}
);}
export const getDeleteAnalysisRunFileMutationOptions = <TError = HTTPValidationError,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof deleteAnalysisRunFile>>, TError,{runId: string;fileId: number}, TContext>, request?: SecondParameter<typeof customFetcher>}
): UseMutationOptions<Awaited<ReturnType<typeof deleteAnalysisRunFile>>, TError,{runId: string;fileId: number}, TContext> => {
const mutationKey = ['deleteAnalysisRunFile'];
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 deleteAnalysisRunFile>>, {runId: string;fileId: number}> = (props) => {
const {runId,fileId} = props ?? {};
return deleteAnalysisRunFile(runId,fileId,requestOptions)
}
return { mutationFn, ...mutationOptions }}
export type DeleteAnalysisRunFileMutationResult = NonNullable<Awaited<ReturnType<typeof deleteAnalysisRunFile>>>
export type DeleteAnalysisRunFileMutationError = HTTPValidationError
/**
* @summary Delete Analysis Run File
*/
export const useDeleteAnalysisRunFile = <TError = HTTPValidationError,
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof deleteAnalysisRunFile>>, TError,{runId: string;fileId: number}, TContext>, request?: SecondParameter<typeof customFetcher>}
, queryClient?: QueryClient): UseMutationResult<
Awaited<ReturnType<typeof deleteAnalysisRunFile>>,
TError,
{runId: string;fileId: number},
TContext
> => {
const mutationOptions = getDeleteAnalysisRunFileMutationOptions(options);
return useMutation(mutationOptions, queryClient);
}
/**
* @summary Get Analysis Status
*/
export type getAnalysisStatusResponse200 = {

View File

@ -27,7 +27,9 @@ import type {
ClinicCreate,
ClinicCreateResponse,
ClinicHistoryResponse,
ClinicListResponse,
ClinicResponse,
GetClinicsParams,
HTTPValidationError
} from '../../model';
@ -38,6 +40,132 @@ type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/**
* @summary Get Clinics
*/
export type getClinicsResponse200 = {
data: ClinicListResponse
status: 200
}
export type getClinicsResponse422 = {
data: HTTPValidationError
status: 422
}
export type getClinicsResponseSuccess = (getClinicsResponse200) & {
headers: Headers;
};
export type getClinicsResponseError = (getClinicsResponse422) & {
headers: Headers;
};
export type getClinicsResponse = (getClinicsResponseSuccess | getClinicsResponseError)
export const getGetClinicsUrl = (params?: GetClinicsParams,) => {
const normalizedParams = new URLSearchParams();
Object.entries(params || {}).forEach(([key, value]) => {
if (value !== undefined) {
normalizedParams.append(key, value === null ? 'null' : value.toString())
}
});
const stringifiedParams = normalizedParams.toString();
return stringifiedParams.length > 0 ? `/api/clinics?${stringifiedParams}` : `/api/clinics`
}
export const getClinics = async (params?: GetClinicsParams, options?: RequestInit): Promise<getClinicsResponse> => {
return customFetcher<getClinicsResponse>(getGetClinicsUrl(params),
{
...options,
method: 'GET'
}
);}
export const getGetClinicsQueryKey = (params?: GetClinicsParams,) => {
return [
`/api/clinics`, ...(params ? [params]: [])
] as const;
}
export const getGetClinicsQueryOptions = <TData = Awaited<ReturnType<typeof getClinics>>, TError = HTTPValidationError>(params?: GetClinicsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getClinics>>, TError, TData>>, request?: SecondParameter<typeof customFetcher>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetClinicsQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getClinics>>> = () => getClinics(params, requestOptions);
return { queryKey, queryFn, staleTime: 60000, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getClinics>>, TError, TData> & { queryKey: DataTag<QueryKey, TData> }
}
export type GetClinicsQueryResult = NonNullable<Awaited<ReturnType<typeof getClinics>>>
export type GetClinicsQueryError = HTTPValidationError
export function useGetClinics<TData = Awaited<ReturnType<typeof getClinics>>, TError = HTTPValidationError>(
params: undefined | GetClinicsParams, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getClinics>>, TError, TData>> & Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof getClinics>>,
TError,
Awaited<ReturnType<typeof getClinics>>
> , 'initialData'
>, request?: SecondParameter<typeof customFetcher>}
, queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> }
export function useGetClinics<TData = Awaited<ReturnType<typeof getClinics>>, TError = HTTPValidationError>(
params?: GetClinicsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getClinics>>, TError, TData>> & Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof getClinics>>,
TError,
Awaited<ReturnType<typeof getClinics>>
> , 'initialData'
>, request?: SecondParameter<typeof customFetcher>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> }
export function useGetClinics<TData = Awaited<ReturnType<typeof getClinics>>, TError = HTTPValidationError>(
params?: GetClinicsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getClinics>>, TError, TData>>, request?: SecondParameter<typeof customFetcher>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> }
/**
* @summary Get Clinics
*/
export function useGetClinics<TData = Awaited<ReturnType<typeof getClinics>>, TError = HTTPValidationError>(
params?: GetClinicsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getClinics>>, TError, TData>>, request?: SecondParameter<typeof customFetcher>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> } {
const queryOptions = getGetClinicsQueryOptions(params,options)
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData> };
query.queryKey = queryOptions.queryKey ;
return query;
}
/**
* @summary Create Clinic
*/

View File

@ -20,8 +20,8 @@ import type {
} from '@tanstack/react-query';
import type {
GetPlan200,
HTTPValidationError
HTTPValidationError,
PlanApiResponse
} from '../../model';
import { customFetcher } from '../../api';
@ -35,7 +35,7 @@ type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
* @summary Get Plan
*/
export type getPlanResponse200 = {
data: GetPlan200
data: PlanApiResponse
status: 200
}
@ -58,7 +58,7 @@ export const getGetPlanUrl = (runId: string,) => {
return `/api/plans/${runId}`
return `/api/plan/${runId}`
}
export const getPlan = async (runId: string, options?: RequestInit): Promise<getPlanResponse> => {
@ -78,7 +78,7 @@ export const getPlan = async (runId: string, options?: RequestInit): Promise<get
export const getGetPlanQueryKey = (runId?: string,) => {
return [
`/api/plans/${runId}`
`/api/plan/${runId}`
] as const;
}

View File

@ -20,8 +20,8 @@ import type {
} from '@tanstack/react-query';
import type {
GetReport200,
HTTPValidationError
HTTPValidationError,
MarketingReportResponse
} from '../../model';
import { customFetcher } from '../../api';
@ -35,7 +35,7 @@ type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
* @summary Get Report
*/
export type getReportResponse200 = {
data: GetReport200
data: MarketingReportResponse
status: 200
}
@ -58,7 +58,7 @@ export const getGetReportUrl = (runId: string,) => {
return `/api/reports/${runId}`
return `/api/report/${runId}`
}
export const getReport = async (runId: string, options?: RequestInit): Promise<getReportResponse> => {
@ -78,7 +78,7 @@ export const getReport = async (runId: string, options?: RequestInit): Promise<g
export const getGetReportQueryKey = (runId?: string,) => {
return [
`/api/reports/${runId}`
`/api/report/${runId}`
] as const;
}

View File

@ -5,7 +5,7 @@
* OpenAPI spec version: 0.1.0
*/
export interface ConversionStrategy {
summary: string;
actions: string[];
export interface AdditionalDomain {
domain: string;
purpose: string;
}

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 AnnotationType = typeof AnnotationType[keyof typeof AnnotationType];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const AnnotationType = {
highlight: 'highlight',
arrow: 'arrow',
text: 'text',
} as const;

View File

@ -4,6 +4,9 @@
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { PlanOutput } from './planOutput';
export type GetPlan200 = PlanOutput | null;
export interface AsIsToBeItem {
area: string;
asIs: string;
toBe: 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 { FileType } from './fileType';
export interface BodyUploadAnalysisRunFileApiAnalysisRunIdFilesPost {
/** 업로드할 파일 */
file: string;
/** 파일 타입 (image/video/audio/document/file) */
file_type?: FileType;
}

View File

@ -4,6 +4,9 @@
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { ReportOutput } from './reportOutput';
export type GetReport200 = ReportOutput | null;
export interface BrandColors {
primary: string;
accent: string;
text: string;
}

View File

@ -9,7 +9,7 @@ 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';
import type { BrandPlanInconsistency } from './brandPlanInconsistency';
export interface BrandGuide {
colors: ColorSwatch[];
@ -17,5 +17,5 @@ export interface BrandGuide {
logoRules: LogoUsageRule[];
toneOfVoice: ToneOfVoice;
channelBranding: ChannelBrandingRule[];
brandInconsistencies: BrandInconsistency[];
brandInconsistencies: BrandPlanInconsistency[];
}

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 { BrandPlanInconsistencyValue } from './brandPlanInconsistencyValue';
export interface BrandPlanInconsistency {
field: string;
values: BrandPlanInconsistencyValue[];
impact: string;
recommendation: string;
}

View File

@ -4,6 +4,9 @@
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { ChannelScore } from './channelScore';
export type ReportOutputYoutube = ChannelScore | null;
export interface BrandPlanInconsistencyValue {
channel: string;
value: string;
isCorrect: boolean;
}

View File

@ -4,10 +4,13 @@
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { Severity } from './severity';
export interface ChannelScore {
channel: string;
icon: string;
score: number;
summary: string;
strengths: string[];
weaknesses: string[];
maxScore: number;
status: Severity;
headline: 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 ChannelStatus = typeof ChannelStatus[keyof typeof ChannelStatus];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const ChannelStatus = {
active: 'active',
inactive: 'inactive',
unknown: 'unknown',
not_found: 'not_found',
} 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 { ClinicListItemHospitalNameEn } from './clinicListItemHospitalNameEn';
import type { ClinicListItemRoadAddress } from './clinicListItemRoadAddress';
import type { ClinicListItemUrl } from './clinicListItemUrl';
export interface ClinicListItem {
hospital_id: string;
hospital_name: string;
hospital_name_en: ClinicListItemHospitalNameEn;
road_address: ClinicListItemRoadAddress;
url: ClinicListItemUrl;
status: string;
created_at: string;
updated_at: string;
}

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 ClinicListItemHospitalNameEn = 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 ClinicListItemRoadAddress = 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 ClinicListItemUrl = string | null;

View File

@ -0,0 +1,12 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { ClinicListItem } from './clinicListItem';
export interface ClinicListResponse {
items: ClinicListItem[];
total: number;
}

View File

@ -0,0 +1,35 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { LeadDoctor } from './leadDoctor';
import type { PriceRange } from './priceRange';
import type { ClinicSnapshotLogoImages } from './clinicSnapshotLogoImages';
import type { ClinicSnapshotBrandColors } from './clinicSnapshotBrandColors';
import type { ClinicSnapshotSource } from './clinicSnapshotSource';
import type { ClinicSnapshotRegistryData } from './clinicSnapshotRegistryData';
export interface ClinicSnapshot {
name: string;
nameEn: string;
established: string;
yearsInBusiness: number;
staffCount: number;
leadDoctor: LeadDoctor;
overallRating: number;
totalReviews: number;
priceRange: PriceRange;
certifications: string[];
mediaAppearances: string[];
medicalTourism: string[];
location: string;
nearestStation: string;
phone: string;
domain: string;
logoImages?: ClinicSnapshotLogoImages;
brandColors?: ClinicSnapshotBrandColors;
source?: ClinicSnapshotSource;
registryData?: ClinicSnapshotRegistryData;
}

View File

@ -4,6 +4,6 @@
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { ChannelScore } from './channelScore';
import type { BrandColors } from './brandColors';
export type ReportOutputFacebook = ChannelScore | null;
export type ClinicSnapshotBrandColors = BrandColors | null;

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 { LogoImages } from './logoImages';
export type ClinicSnapshotLogoImages = LogoImages | null;

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 { RegistryData } from './registryData';
export type ClinicSnapshotRegistryData = RegistryData | null;

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 { DataSource } from './dataSource';
export type ClinicSnapshotSource = DataSource | null;

View File

@ -8,7 +8,7 @@
export interface ContentPillar {
title: string;
description: string;
relatedUSP: string;
relatedUsp: string;
exampleTopics: string[];
color: 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 DataSource = typeof DataSource[keyof typeof DataSource];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const DataSource = {
registry: 'registry',
scrape: 'scrape',
} as const;

View File

@ -0,0 +1,15 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { Severity } from './severity';
import type { DiagnosisItemEvidenceIds } from './diagnosisItemEvidenceIds';
export interface DiagnosisItem {
category: string;
detail: string;
severity: Severity;
evidenceIds?: DiagnosisItemEvidenceIds;
}

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 DiagnosisItemEvidenceIds = string[] | null;

View File

@ -0,0 +1,11 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export interface EstimatedRevenue {
min: number;
max: number;
}

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 { FacebookPage } from './facebookPage';
import type { DiagnosisItem } from './diagnosisItem';
import type { BrandInconsistency } from './brandInconsistency';
export interface FacebookAudit {
pages: FacebookPage[];
diagnosis: DiagnosisItem[];
brandInconsistencies: BrandInconsistency[];
consolidationRecommendation: string;
}

View File

@ -0,0 +1,31 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { Language } from './language';
import type { FacebookPagePostFrequency } from './facebookPagePostFrequency';
import type { FacebookPageTopContentType } from './facebookPageTopContentType';
import type { FacebookPageEngagement } from './facebookPageEngagement';
export interface FacebookPage {
url: string;
pageName: string;
language: Language;
label: string;
followers: number;
following: number;
category: string;
bio: string;
logo: string;
logoDescription: string;
link: string;
linkedDomain: string;
reviews: number;
recentPostAge: string;
hasWhatsapp: boolean;
postFrequency?: FacebookPagePostFrequency;
topContentType?: FacebookPageTopContentType;
engagement?: FacebookPageEngagement;
}

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 FacebookPageEngagement = 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 FacebookPagePostFrequency = 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 FacebookPageTopContentType = 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
*/
import type { FileType } from './fileType';
import type { FileListItemSizeBytes } from './fileListItemSizeBytes';
export interface FileListItem {
id: number;
file_type: FileType;
file_name: string;
file_url: string;
size_bytes?: FileListItemSizeBytes;
created_at: string;
}

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 FileListItemSizeBytes = number | null;

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 FileType = typeof FileType[keyof typeof FileType];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const FileType = {
image: 'image',
video: 'video',
audio: 'audio',
document: 'document',
file: 'file',
} as const;

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 { FileType } from './fileType';
import type { FileUploadResponseSizeBytes } from './fileUploadResponseSizeBytes';
export interface FileUploadResponse {
id: number;
analysis_run_id: string;
file_type: FileType;
file_name: string;
file_url: string;
size_bytes?: FileUploadResponseSizeBytes;
}

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 FileUploadResponseSizeBytes = number | null;

View File

@ -0,0 +1,11 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export type GetClinicsParams = {
limit?: number;
offset?: number;
};

View File

@ -5,6 +5,7 @@
* OpenAPI spec version: 0.1.0
*/
export * from './additionalDomain';
export * from './analysisCreate';
export * from './analysisOptions';
export * from './analysisStartResponse';
@ -12,14 +13,20 @@ export * from './analysisStatus';
export * from './analysisStatusResponse';
export * from './analysisStatusResponseChannelErrors';
export * from './analysisStatusResponseCompletedAt';
export * from './annotationType';
export * from './asIsToBeItem';
export * from './assetCard';
export * from './assetCardSource';
export * from './assetCardStatus';
export * from './assetCardType';
export * from './assetCollectionData';
export * from './bodyUploadAnalysisRunFileApiAnalysisRunIdFilesPost';
export * from './brandColors';
export * from './brandGuide';
export * from './brandInconsistency';
export * from './brandInconsistencyValue';
export * from './brandPlanInconsistency';
export * from './brandPlanInconsistencyValue';
export * from './calendarData';
export * from './calendarEntry';
export * from './calendarEntryAiPromptSeed';
@ -33,6 +40,7 @@ export * from './calendarWeek';
export * from './channelBrandingRule';
export * from './channelBrandingRuleCurrentStatus';
export * from './channelScore';
export * from './channelStatus';
export * from './channelStrategyCard';
export * from './channelStrategyCardCustomerJourneyStage';
export * from './channelStrategyCardPriority';
@ -46,43 +54,111 @@ export * from './clinicCreate';
export * from './clinicCreateResponse';
export * from './clinicHistoryResponse';
export * from './clinicHistoryResponseMetricsTimeseries';
export * from './clinicListItem';
export * from './clinicListItemHospitalNameEn';
export * from './clinicListItemRoadAddress';
export * from './clinicListItemUrl';
export * from './clinicListResponse';
export * from './clinicResponse';
export * from './clinicResponseHospitalNameEn';
export * from './clinicResponseRawData';
export * from './clinicResponseRawDataAnyOf';
export * from './clinicResponseRoadAddress';
export * from './clinicResponseUrl';
export * from './clinicSnapshot';
export * from './clinicSnapshotBrandColors';
export * from './clinicSnapshotLogoImages';
export * from './clinicSnapshotRegistryData';
export * from './clinicSnapshotSource';
export * from './colorSwatch';
export * from './contentCountSummary';
export * from './contentCountSummaryType';
export * from './contentPillar';
export * from './contentStrategyData';
export * from './contentTypeRow';
export * from './conversionStrategy';
export * from './dataSource';
export * from './diagnosisItem';
export * from './diagnosisItemEvidenceIds';
export * from './estimatedRevenue';
export * from './facebookAudit';
export * from './facebookPage';
export * from './facebookPageEngagement';
export * from './facebookPagePostFrequency';
export * from './facebookPageTopContentType';
export * from './fileListItem';
export * from './fileListItemSizeBytes';
export * from './fileType';
export * from './fileUploadResponse';
export * from './fileUploadResponseSizeBytes';
export * from './fontSpec';
export * from './getPlan200';
export * from './getReport200';
export * from './getClinicsParams';
export * from './hTTPValidationError';
export * from './instagramAccount';
export * from './instagramAudit';
export * from './kPIMetric';
export * from './language';
export * from './leadDoctor';
export * from './linkedUrl';
export * from './logoImages';
export * from './logoImagesCircle';
export * from './logoImagesHorizontal';
export * from './logoImagesKorean';
export * from './logoUsageRule';
export * from './planOutput';
export * from './planOutputRepurposingProposals';
export * from './reportOutput';
export * from './reportOutputFacebook';
export * from './reportOutputGangnamUnni';
export * from './reportOutputInstagram';
export * from './reportOutputNaverBlog';
export * from './reportOutputYoutube';
export * from './marketingReportResponse';
export * from './marketingReportResponseClinicName';
export * from './marketingReportResponseClinicNameEn';
export * from './marketingReportResponseScreenshots';
export * from './newChannelProposal';
export * from './otherChannel';
export * from './otherChannelUrl';
export * from './planApiResponse';
export * from './planApiResponseClinicName';
export * from './planApiResponseClinicNameEn';
export * from './planApiResponseRepurposingProposals';
export * from './platformStrategy';
export * from './priceRange';
export * from './registryData';
export * from './registryDataBranches';
export * from './registryDataBrandGroup';
export * from './registryDataDistrict';
export * from './registryDataGangnamUnniUrl';
export * from './registryDataGoogleMapsUrl';
export * from './registryDataNaverPlaceUrl';
export * from './registryDataWebsiteEn';
export * from './repurposingOutput';
export * from './repurposingProposalItem';
export * from './repurposingProposalItemEstimatedEffort';
export * from './repurposingProposalItemPriority';
export * from './roadmapMonth';
export * from './roadmapTask';
export * from './runSummary';
export * from './runSummaryCompletedAt';
export * from './runSummaryOverallScore';
export * from './screenshotAnnotation';
export * from './screenshotAnnotationColor';
export * from './screenshotAnnotationHeight';
export * from './screenshotAnnotationLabel';
export * from './screenshotAnnotationWidth';
export * from './screenshotEvidence';
export * from './screenshotEvidenceAnnotations';
export * from './screenshotEvidenceSourceUrl';
export * from './severity';
export * from './snsLink';
export * from './strategyDetail';
export * from './toneOfVoice';
export * from './topVideo';
export * from './topVideoDuration';
export * from './trackingPixel';
export * from './trackingPixelDetails';
export * from './transformationProposal';
export * from './validationError';
export * from './validationErrorCtx';
export * from './validationErrorLocItem';
export * from './videoType';
export * from './websiteAudit';
export * from './websiteAuditSnsLinksDetail';
export * from './weeklyViewGrowth';
export * from './workflowStep';
export * from './youTubeAudit';
export * from './youTubeRepurposeItem';
export * from './youTubeRepurposeItemType';

View File

@ -0,0 +1,23 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { Language } from './language';
export interface InstagramAccount {
handle: string;
language: Language;
label: string;
posts: number;
followers: number;
following: number;
category: string;
profileLink: string;
highlights: string[];
reelsCount: number;
contentFormat: string;
profilePhoto: string;
bio: string;
}

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 { InstagramAccount } from './instagramAccount';
import type { DiagnosisItem } from './diagnosisItem';
export interface InstagramAudit {
accounts: InstagramAccount[];
diagnosis: DiagnosisItem[];
}

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 KPIMetric {
metric: string;
current: string;
target3Month: string;
target12Month: 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 Language = typeof Language[keyof typeof Language];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const Language = {
KR: 'KR',
EN: 'EN',
} 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 LeadDoctor {
name: string;
credentials: string;
rating: number;
reviewCount: number;
}

View File

@ -0,0 +1,11 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export interface LinkedUrl {
label: string;
url: string;
}

View File

@ -0,0 +1,15 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { LogoImagesCircle } from './logoImagesCircle';
import type { LogoImagesHorizontal } from './logoImagesHorizontal';
import type { LogoImagesKorean } from './logoImagesKorean';
export interface LogoImages {
circle?: LogoImagesCircle;
horizontal?: LogoImagesHorizontal;
korean?: LogoImagesKorean;
}

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 LogoImagesCircle = 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 LogoImagesHorizontal = 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 LogoImagesKorean = string | null;

View File

@ -0,0 +1,41 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { MarketingReportResponseClinicName } from './marketingReportResponseClinicName';
import type { MarketingReportResponseClinicNameEn } from './marketingReportResponseClinicNameEn';
import type { ClinicSnapshot } from './clinicSnapshot';
import type { ChannelScore } from './channelScore';
import type { YouTubeAudit } from './youTubeAudit';
import type { InstagramAudit } from './instagramAudit';
import type { FacebookAudit } from './facebookAudit';
import type { OtherChannel } from './otherChannel';
import type { WebsiteAudit } from './websiteAudit';
import type { DiagnosisItem } from './diagnosisItem';
import type { TransformationProposal } from './transformationProposal';
import type { RoadmapMonth } from './roadmapMonth';
import type { KPIMetric } from './kPIMetric';
import type { MarketingReportResponseScreenshots } from './marketingReportResponseScreenshots';
export interface MarketingReportResponse {
id: string;
clinicName?: MarketingReportResponseClinicName;
clinicNameEn?: MarketingReportResponseClinicNameEn;
createdAt: string;
targetUrl: string;
overallScore: number;
clinicSnapshot: ClinicSnapshot;
channelScores: ChannelScore[];
youtubeAudit: YouTubeAudit;
instagramAudit: InstagramAudit;
facebookAudit: FacebookAudit;
otherChannels: OtherChannel[];
websiteAudit: WebsiteAudit;
problemDiagnosis: DiagnosisItem[];
transformation: TransformationProposal;
roadmap: RoadmapMonth[];
kpiDashboard: KPIMetric[];
screenshots?: MarketingReportResponseScreenshots;
}

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 MarketingReportResponseClinicName = 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 MarketingReportResponseClinicNameEn = string | null;

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 { ScreenshotEvidence } from './screenshotEvidence';
export type MarketingReportResponseScreenshots = ScreenshotEvidence[] | null;

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 NewChannelProposal {
channel: string;
priority: string;
rationale: string;
}

View File

@ -0,0 +1,15 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { ChannelStatus } from './channelStatus';
import type { OtherChannelUrl } from './otherChannelUrl';
export interface OtherChannel {
name: string;
status: ChannelStatus;
details: string;
url?: OtherChannelUrl;
}

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 OtherChannelUrl = string | null;

View File

@ -4,18 +4,25 @@
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { PlanApiResponseClinicName } from './planApiResponseClinicName';
import type { PlanApiResponseClinicNameEn } from './planApiResponseClinicNameEn';
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';
import type { PlanApiResponseRepurposingProposals } from './planApiResponseRepurposingProposals';
export interface PlanOutput {
export interface PlanApiResponse {
id: string;
clinicName?: PlanApiResponseClinicName;
clinicNameEn?: PlanApiResponseClinicNameEn;
createdAt: string;
targetUrl: string;
brandGuide: BrandGuide;
channelStrategies: ChannelStrategyCard[];
contentStrategy: ContentStrategyData;
calendar: CalendarData;
assetCollection: AssetCollectionData;
repurposingProposals?: PlanOutputRepurposingProposals;
repurposingProposals?: PlanApiResponseRepurposingProposals;
}

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 PlanApiResponseClinicName = 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 PlanApiResponseClinicNameEn = string | null;

View File

@ -6,4 +6,4 @@
*/
import type { RepurposingProposalItem } from './repurposingProposalItem';
export type PlanOutputRepurposingProposals = RepurposingProposalItem[] | null;
export type PlanApiResponseRepurposingProposals = RepurposingProposalItem[] | null;

View File

@ -0,0 +1,15 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { StrategyDetail } from './strategyDetail';
export interface PlatformStrategy {
platform: string;
icon: string;
currentMetric: string;
targetMetric: string;
strategies: StrategyDetail[];
}

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 PriceRange {
min: string;
max: string;
currency: string;
}

View File

@ -0,0 +1,23 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { RegistryDataDistrict } from './registryDataDistrict';
import type { RegistryDataBranches } from './registryDataBranches';
import type { RegistryDataBrandGroup } from './registryDataBrandGroup';
import type { RegistryDataWebsiteEn } from './registryDataWebsiteEn';
import type { RegistryDataNaverPlaceUrl } from './registryDataNaverPlaceUrl';
import type { RegistryDataGangnamUnniUrl } from './registryDataGangnamUnniUrl';
import type { RegistryDataGoogleMapsUrl } from './registryDataGoogleMapsUrl';
export interface RegistryData {
district?: RegistryDataDistrict;
branches?: RegistryDataBranches;
brandGroup?: RegistryDataBrandGroup;
websiteEn?: RegistryDataWebsiteEn;
naverPlaceUrl?: RegistryDataNaverPlaceUrl;
gangnamUnniUrl?: RegistryDataGangnamUnniUrl;
googleMapsUrl?: RegistryDataGoogleMapsUrl;
}

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 RegistryDataBranches = 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 RegistryDataBrandGroup = 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 RegistryDataDistrict = 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 RegistryDataGangnamUnniUrl = 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 RegistryDataGoogleMapsUrl = 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 RegistryDataNaverPlaceUrl = 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 RegistryDataWebsiteEn = string | null;

View File

@ -1,24 +0,0 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
import type { ReportOutputInstagram } from './reportOutputInstagram';
import type { ReportOutputFacebook } from './reportOutputFacebook';
import type { ReportOutputNaverBlog } from './reportOutputNaverBlog';
import type { ReportOutputYoutube } from './reportOutputYoutube';
import type { ReportOutputGangnamUnni } from './reportOutputGangnamUnni';
import type { ConversionStrategy } from './conversionStrategy';
export interface ReportOutput {
overall_score: number;
instagram?: ReportOutputInstagram;
facebook?: ReportOutputFacebook;
naver_blog?: ReportOutputNaverBlog;
youtube?: ReportOutputYoutube;
gangnam_unni?: ReportOutputGangnamUnni;
conversion_strategy: ConversionStrategy;
roadmap: string[];
kpis: string[];
}

View File

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

View File

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

View File

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

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 { RoadmapTask } from './roadmapTask';
export interface RoadmapMonth {
month: number;
title: string;
subtitle: string;
tasks: RoadmapTask[];
}

View File

@ -0,0 +1,11 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export interface RoadmapTask {
task: string;
completed: 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 { AnnotationType } from './annotationType';
import type { ScreenshotAnnotationWidth } from './screenshotAnnotationWidth';
import type { ScreenshotAnnotationHeight } from './screenshotAnnotationHeight';
import type { ScreenshotAnnotationLabel } from './screenshotAnnotationLabel';
import type { ScreenshotAnnotationColor } from './screenshotAnnotationColor';
export interface ScreenshotAnnotation {
type: AnnotationType;
x: number;
y: number;
width?: ScreenshotAnnotationWidth;
height?: ScreenshotAnnotationHeight;
label?: ScreenshotAnnotationLabel;
color?: ScreenshotAnnotationColor;
}

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 ScreenshotAnnotationColor = 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 ScreenshotAnnotationHeight = number | 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 ScreenshotAnnotationLabel = 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 ScreenshotAnnotationWidth = number | null;

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 { ScreenshotEvidenceSourceUrl } from './screenshotEvidenceSourceUrl';
import type { ScreenshotEvidenceAnnotations } from './screenshotEvidenceAnnotations';
export interface ScreenshotEvidence {
id: string;
url: string;
channel: string;
capturedAt: string;
caption: string;
sourceUrl?: ScreenshotEvidenceSourceUrl;
annotations?: ScreenshotEvidenceAnnotations;
}

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 { ScreenshotAnnotation } from './screenshotAnnotation';
export type ScreenshotEvidenceAnnotations = ScreenshotAnnotation[] | 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 ScreenshotEvidenceSourceUrl = string | null;

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 Severity = typeof Severity[keyof typeof Severity];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const Severity = {
critical: 'critical',
warning: 'warning',
good: 'good',
excellent: 'excellent',
unknown: 'unknown',
} 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 SnsLink {
platform: string;
url: string;
location: string;
}

View File

@ -0,0 +1,11 @@
/**
* Generated by orval v7.21.0 🍺
* Do not edit manually.
* FastAPI
* OpenAPI spec version: 0.1.0
*/
export interface StrategyDetail {
strategy: string;
detail: string;
}

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 { VideoType } from './videoType';
import type { TopVideoDuration } from './topVideoDuration';
export interface TopVideo {
title: string;
views: number;
uploadedAgo: string;
type: VideoType;
duration?: TopVideoDuration;
}

Some files were not shown because too many files have changed in this diff Show More