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;
|
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) {
|
export default function ReportBody({ data }: ReportBodyProps) {
|
||||||
// 각 audit에서 핸들 끌어와 헤더의 바로가기 버튼에 전달
|
// 각 audit에서 핸들 끌어와 헤더의 바로가기 버튼에 전달
|
||||||
const socialHandles = {
|
const socialHandles = {
|
||||||
|
|
@ -56,7 +74,7 @@ export default function ReportBody({ data }: ReportBodyProps) {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SectionErrorBoundary>
|
<SectionErrorBoundary>
|
||||||
{hasValue(data.clinicSnapshot) && data.clinicSnapshot.name ? (
|
{hasClinicSnapshotData(data.clinicSnapshot) ? (
|
||||||
<ClinicSnapshot data={data.clinicSnapshot} targetUrl={data.targetUrl} socialHandles={socialHandles} />
|
<ClinicSnapshot data={data.clinicSnapshot} targetUrl={data.targetUrl} socialHandles={socialHandles} />
|
||||||
) : (
|
) : (
|
||||||
<EmptySection id="clinic-snapshot" title="Clinic Overview" subtitle="병원 기본 정보" />
|
<EmptySection id="clinic-snapshot" title="Clinic Overview" subtitle="병원 기본 정보" />
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export default function YouTubeAudit({ data }: YouTubeAuditProps) {
|
||||||
label="구독자"
|
label="구독자"
|
||||||
value={formatNumber(data.subscribers)}
|
value={formatNumber(data.subscribers)}
|
||||||
icon={<Users size={20} />}
|
icon={<Users size={20} />}
|
||||||
subtext={data.subscriberRank}
|
subtext={data.subscriberRank ?? undefined}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="총 영상 수"
|
label="총 영상 수"
|
||||||
|
|
@ -45,13 +45,22 @@ export default function YouTubeAudit({ data }: YouTubeAuditProps) {
|
||||||
value={formatNumber(data.totalViews)}
|
value={formatNumber(data.totalViews)}
|
||||||
icon={<Eye size={20} />}
|
icon={<Eye size={20} />}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
{data.weeklyViewGrowth ? (
|
||||||
label="주간 성장"
|
<MetricCard
|
||||||
value={`+${formatNumber(data.weeklyViewGrowth.absolute)}`}
|
label="주간 성장"
|
||||||
icon={<TrendingUp size={20} />}
|
value={`${data.weeklyViewGrowth.absolute >= 0 ? '+' : ''}${formatNumber(data.weeklyViewGrowth.absolute)}`}
|
||||||
subtext={`${data.weeklyViewGrowth.percentage > 0 ? '+' : ''}${data.weeklyViewGrowth.percentage}%`}
|
icon={<TrendingUp size={20} />}
|
||||||
trend={data.weeklyViewGrowth.percentage > 0 ? 'up' : data.weeklyViewGrowth.percentage < 0 ? 'down' : 'neutral'}
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Channel info card */}
|
{/* 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>
|
<p className="text-sm text-slate-600 mb-4">{data.channelDescription}</p>
|
||||||
<div className="flex flex-wrap gap-3 text-sm text-slate-500">
|
<div className="flex flex-wrap gap-3 text-sm text-slate-500">
|
||||||
<span>개설일: {data.channelCreatedDate}</span>
|
<span>개설일: {data.channelCreatedDate}</span>
|
||||||
<span>평균 영상 길이: {data.avgVideoLength}</span>
|
<span>평균 영상 길이: {data.avgVideoLength ?? '-'}</span>
|
||||||
<span>업로드 빈도: {data.uploadFrequency}</span>
|
<span>업로드 빈도: {data.uploadFrequency ?? '-'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Linked URLs */}
|
{/* Linked URLs */}
|
||||||
|
|
|
||||||
|
|
@ -87,12 +87,12 @@ export interface YouTubeAudit {
|
||||||
subscribers: number;
|
subscribers: number;
|
||||||
totalVideos: number;
|
totalVideos: number;
|
||||||
totalViews: number;
|
totalViews: number;
|
||||||
weeklyViewGrowth: { absolute: number; percentage: number };
|
weeklyViewGrowth: { absolute: number; percentage: number } | null;
|
||||||
estimatedMonthlyRevenue: { min: number; max: number };
|
estimatedMonthlyRevenue: { min: number; max: number } | null;
|
||||||
avgVideoLength: string;
|
avgVideoLength: string | null;
|
||||||
uploadFrequency: string;
|
uploadFrequency: string | null;
|
||||||
channelCreatedDate: string;
|
channelCreatedDate: string;
|
||||||
subscriberRank: string;
|
subscriberRank: string | null;
|
||||||
channelDescription: string;
|
channelDescription: string;
|
||||||
linkedUrls: { label: string; url: string }[];
|
linkedUrls: { label: string; url: string }[];
|
||||||
playlists: string[];
|
playlists: string[];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue