fix(report): youtube_audit null 가드 + clinic overview 게이트 완화
- weeklyViewGrowth/estimatedMonthlyRevenue/avgVideoLength/uploadFrequency/subscriberRank 를 nullable 로 타입 표시, YouTubeAudit 컴포넌트에서 null 접근 가드 - ClinicSnapshot 게이트를 name 필수 → 의미있는 필드 하나라도 있으면 렌더로 완화 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>main
parent
156dad59dc
commit
03c3c7ab21
|
|
@ -32,6 +32,24 @@ function nonEmpty<T>(arr: T[] | null | undefined): arr is T[] {
|
|||
return Array.isArray(arr) && arr.length > 0;
|
||||
}
|
||||
|
||||
function hasClinicSnapshotData(s: MarketingReport['clinicSnapshot'] | null | undefined): boolean {
|
||||
if (!s) return false;
|
||||
return !!(
|
||||
s.name ||
|
||||
s.nameEn ||
|
||||
s.location ||
|
||||
s.phone ||
|
||||
s.domain ||
|
||||
s.established ||
|
||||
(s.staffCount ?? 0) > 0 ||
|
||||
(s.overallRating ?? 0) > 0 ||
|
||||
(s.totalReviews ?? 0) > 0 ||
|
||||
s.leadDoctor?.name ||
|
||||
nonEmpty(s.certifications) ||
|
||||
s.registryData
|
||||
);
|
||||
}
|
||||
|
||||
export default function ReportBody({ data }: ReportBodyProps) {
|
||||
// 각 audit에서 핸들 끌어와 헤더의 바로가기 버튼에 전달
|
||||
const socialHandles = {
|
||||
|
|
@ -56,7 +74,7 @@ export default function ReportBody({ data }: ReportBodyProps) {
|
|||
/>
|
||||
|
||||
<SectionErrorBoundary>
|
||||
{hasValue(data.clinicSnapshot) && data.clinicSnapshot.name ? (
|
||||
{hasClinicSnapshotData(data.clinicSnapshot) ? (
|
||||
<ClinicSnapshot data={data.clinicSnapshot} targetUrl={data.targetUrl} socialHandles={socialHandles} />
|
||||
) : (
|
||||
<EmptySection id="clinic-snapshot" title="Clinic Overview" subtitle="병원 기본 정보" />
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export default function YouTubeAudit({ data }: YouTubeAuditProps) {
|
|||
label="구독자"
|
||||
value={formatNumber(data.subscribers)}
|
||||
icon={<Users size={20} />}
|
||||
subtext={data.subscriberRank}
|
||||
subtext={data.subscriberRank ?? undefined}
|
||||
/>
|
||||
<MetricCard
|
||||
label="총 영상 수"
|
||||
|
|
@ -45,13 +45,22 @@ export default function YouTubeAudit({ data }: YouTubeAuditProps) {
|
|||
value={formatNumber(data.totalViews)}
|
||||
icon={<Eye size={20} />}
|
||||
/>
|
||||
{data.weeklyViewGrowth ? (
|
||||
<MetricCard
|
||||
label="주간 성장"
|
||||
value={`+${formatNumber(data.weeklyViewGrowth.absolute)}`}
|
||||
value={`${data.weeklyViewGrowth.absolute >= 0 ? '+' : ''}${formatNumber(data.weeklyViewGrowth.absolute)}`}
|
||||
icon={<TrendingUp size={20} />}
|
||||
subtext={`${data.weeklyViewGrowth.percentage > 0 ? '+' : ''}${data.weeklyViewGrowth.percentage}%`}
|
||||
trend={data.weeklyViewGrowth.percentage > 0 ? 'up' : data.weeklyViewGrowth.percentage < 0 ? 'down' : 'neutral'}
|
||||
/>
|
||||
) : (
|
||||
<MetricCard
|
||||
label="주간 성장"
|
||||
value="-"
|
||||
icon={<TrendingUp size={20} />}
|
||||
subtext="데이터 없음"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Channel info card */}
|
||||
|
|
@ -86,8 +95,8 @@ export default function YouTubeAudit({ data }: YouTubeAuditProps) {
|
|||
<p className="text-sm text-slate-600 mb-4">{data.channelDescription}</p>
|
||||
<div className="flex flex-wrap gap-3 text-sm text-slate-500">
|
||||
<span>개설일: {data.channelCreatedDate}</span>
|
||||
<span>평균 영상 길이: {data.avgVideoLength}</span>
|
||||
<span>업로드 빈도: {data.uploadFrequency}</span>
|
||||
<span>평균 영상 길이: {data.avgVideoLength ?? '-'}</span>
|
||||
<span>업로드 빈도: {data.uploadFrequency ?? '-'}</span>
|
||||
</div>
|
||||
|
||||
{/* Linked URLs */}
|
||||
|
|
|
|||
|
|
@ -87,12 +87,12 @@ export interface YouTubeAudit {
|
|||
subscribers: number;
|
||||
totalVideos: number;
|
||||
totalViews: number;
|
||||
weeklyViewGrowth: { absolute: number; percentage: number };
|
||||
estimatedMonthlyRevenue: { min: number; max: number };
|
||||
avgVideoLength: string;
|
||||
uploadFrequency: string;
|
||||
weeklyViewGrowth: { absolute: number; percentage: number } | null;
|
||||
estimatedMonthlyRevenue: { min: number; max: number } | null;
|
||||
avgVideoLength: string | null;
|
||||
uploadFrequency: string | null;
|
||||
channelCreatedDate: string;
|
||||
subscriberRank: string;
|
||||
subscriberRank: string | null;
|
||||
channelDescription: string;
|
||||
linkedUrls: { label: string; url: string }[];
|
||||
playlists: string[];
|
||||
|
|
|
|||
Loading…
Reference in New Issue