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
Mina Choi 2026-06-04 09:42:14 +09:00
parent 156dad59dc
commit 03c3c7ab21
3 changed files with 43 additions and 16 deletions

View File

@ -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="병원 기본 정보" />

View File

@ -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} />}
/>
<MetricCard
label="주간 성장"
value={`+${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'}
/>
{data.weeklyViewGrowth ? (
<MetricCard
label="주간 성장"
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 */}

View File

@ -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[];