feat: P0 fixes — date formatting, channel labels, dynamic marketing plan
- ReportHeader/PlanHeader: format ISO dates as Korean (2026년 4월 2일) - ChannelOverview: map API keys to Korean labels (naverBlog → 네이버 블로그) - useMarketingPlan: replace mockPlan with real DB-based plan generation - transformPlan: build MarketingPlan from report data (channels, pillars, calendar) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>claude/bold-hawking
parent
bd7bc45192
commit
4484ac788a
|
|
@ -1,6 +1,18 @@
|
||||||
import { motion } from 'motion/react';
|
import { motion } from 'motion/react';
|
||||||
import { Calendar, Globe } from 'lucide-react';
|
import { Calendar, Globe } from 'lucide-react';
|
||||||
|
|
||||||
|
function formatDate(raw: string): string {
|
||||||
|
try {
|
||||||
|
return new Date(raw).toLocaleDateString('ko-KR', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface PlanHeaderProps {
|
interface PlanHeaderProps {
|
||||||
clinicName: string;
|
clinicName: string;
|
||||||
clinicNameEn: string;
|
clinicNameEn: string;
|
||||||
|
|
@ -82,7 +94,7 @@ export default function PlanHeader({
|
||||||
>
|
>
|
||||||
<span className="inline-flex items-center gap-2 rounded-full bg-white/60 backdrop-blur-sm border border-white/40 px-3 py-1 text-sm font-medium text-slate-700">
|
<span className="inline-flex items-center gap-2 rounded-full bg-white/60 backdrop-blur-sm border border-white/40 px-3 py-1 text-sm font-medium text-slate-700">
|
||||||
<Calendar size={14} className="text-slate-400" />
|
<Calendar size={14} className="text-slate-400" />
|
||||||
{date}
|
{formatDate(date)}
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-center gap-2 rounded-full bg-white/60 backdrop-blur-sm border border-white/40 px-3 py-1 text-sm font-medium text-slate-700">
|
<span className="inline-flex items-center gap-2 rounded-full bg-white/60 backdrop-blur-sm border border-white/40 px-3 py-1 text-sm font-medium text-slate-700">
|
||||||
<Globe size={14} className="text-slate-400" />
|
<Globe size={14} className="text-slate-400" />
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,18 @@ const iconMap: Record<string, ComponentType<{ size?: number; className?: string
|
||||||
search: Search,
|
search: Search,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const channelLabel: Record<string, string> = {
|
||||||
|
naverBlog: '네이버 블로그',
|
||||||
|
naverPlace: '네이버 플레이스',
|
||||||
|
gangnamUnni: '강남언니',
|
||||||
|
instagram: 'Instagram',
|
||||||
|
youtube: 'YouTube',
|
||||||
|
facebook: 'Facebook',
|
||||||
|
website: '웹사이트',
|
||||||
|
tiktok: 'TikTok',
|
||||||
|
blog: '블로그',
|
||||||
|
};
|
||||||
|
|
||||||
const brandColor: Record<string, string> = {
|
const brandColor: Record<string, string> = {
|
||||||
facebook: '#1877F2',
|
facebook: '#1877F2',
|
||||||
instagram: '#E1306C',
|
instagram: '#E1306C',
|
||||||
|
|
@ -49,7 +61,7 @@ export default function ChannelOverview({ channels }: ChannelOverviewProps) {
|
||||||
<div className="w-10 h-10 rounded-xl bg-slate-50 flex items-center justify-center">
|
<div className="w-10 h-10 rounded-xl bg-slate-50 flex items-center justify-center">
|
||||||
<Icon size={20} style={color ? { color } : undefined} className={color ? '' : 'text-slate-500'} />
|
<Icon size={20} style={color ? { color } : undefined} className={color ? '' : 'text-slate-500'} />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm font-medium text-[#0A1128]">{ch.channel}</p>
|
<p className="text-sm font-medium text-[#0A1128]">{channelLabel[ch.channel] || ch.channel}</p>
|
||||||
<ScoreRing score={ch.score} maxScore={ch.maxScore} size={60} color={color} />
|
<ScoreRing score={ch.score} maxScore={ch.maxScore} size={60} color={color} />
|
||||||
<p className="text-xs text-slate-500 line-clamp-2 leading-relaxed">
|
<p className="text-xs text-slate-500 line-clamp-2 leading-relaxed">
|
||||||
{ch.headline}
|
{ch.headline}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,18 @@ import { motion } from 'motion/react';
|
||||||
import { Calendar, Globe, MapPin } from 'lucide-react';
|
import { Calendar, Globe, MapPin } from 'lucide-react';
|
||||||
import { ScoreRing } from './ui/ScoreRing';
|
import { ScoreRing } from './ui/ScoreRing';
|
||||||
|
|
||||||
|
function formatDate(raw: string): string {
|
||||||
|
try {
|
||||||
|
return new Date(raw).toLocaleDateString('ko-KR', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface ReportHeaderProps {
|
interface ReportHeaderProps {
|
||||||
clinicName: string;
|
clinicName: string;
|
||||||
clinicNameEn: string;
|
clinicNameEn: string;
|
||||||
|
|
@ -108,7 +120,7 @@ export default function ReportHeader({
|
||||||
>
|
>
|
||||||
<span className="inline-flex items-center gap-2 rounded-full bg-white/60 backdrop-blur-sm border border-white/40 px-3 py-1 text-sm font-medium text-slate-700">
|
<span className="inline-flex items-center gap-2 rounded-full bg-white/60 backdrop-blur-sm border border-white/40 px-3 py-1 text-sm font-medium text-slate-700">
|
||||||
<Calendar size={14} className="text-slate-400" />
|
<Calendar size={14} className="text-slate-400" />
|
||||||
{date}
|
{formatDate(date)}
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-center gap-2 rounded-full bg-white/60 backdrop-blur-sm border border-white/40 px-3 py-1 text-sm font-medium text-slate-700">
|
<span className="inline-flex items-center gap-2 rounded-full bg-white/60 backdrop-blur-sm border border-white/40 px-3 py-1 text-sm font-medium text-slate-700">
|
||||||
<Globe size={14} className="text-slate-400" />
|
<Globe size={14} className="text-slate-400" />
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useLocation } from 'react-router';
|
||||||
import type { MarketingPlan } from '../types/plan';
|
import type { MarketingPlan } from '../types/plan';
|
||||||
import { mockPlan } from '../data/mockPlan';
|
import { fetchReportById } from '../lib/supabase';
|
||||||
|
import { transformReportToPlan } from '../lib/transformPlan';
|
||||||
|
|
||||||
interface UseMarketingPlanResult {
|
interface UseMarketingPlanResult {
|
||||||
data: MarketingPlan | null;
|
data: MarketingPlan | null;
|
||||||
|
|
@ -8,10 +10,17 @@ interface UseMarketingPlanResult {
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LocationState {
|
||||||
|
report?: Record<string, unknown>;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
reportId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function useMarketingPlan(id: string | undefined): UseMarketingPlanResult {
|
export function useMarketingPlan(id: string | undefined): UseMarketingPlanResult {
|
||||||
const [data, setData] = useState<MarketingPlan | null>(null);
|
const [data, setData] = useState<MarketingPlan | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
|
|
@ -20,15 +29,38 @@ export function useMarketingPlan(id: string | undefined): UseMarketingPlanResult
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 1: Return mock data
|
const state = location.state as LocationState | undefined;
|
||||||
// Phase 2+: Replace with real API call
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setData(mockPlan);
|
|
||||||
setIsLoading(false);
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
// Source 1: Report data passed via navigation state
|
||||||
}, [id]);
|
if (state?.report && state?.metadata) {
|
||||||
|
try {
|
||||||
|
const plan = transformReportToPlan({
|
||||||
|
id: (state.reportId || id),
|
||||||
|
url: (state.metadata.url as string) || '',
|
||||||
|
clinic_name: (state.metadata.clinicName as string) || '',
|
||||||
|
report: state.report,
|
||||||
|
created_at: (state.metadata.generatedAt as string) || new Date().toISOString(),
|
||||||
|
});
|
||||||
|
setData(plan);
|
||||||
|
setIsLoading(false);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to build marketing plan');
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source 2: Fetch report from Supabase and transform to plan
|
||||||
|
fetchReportById(id)
|
||||||
|
.then((row) => {
|
||||||
|
const plan = transformReportToPlan(row);
|
||||||
|
setData(plan);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to fetch marketing plan');
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}, [id, location.state]);
|
||||||
|
|
||||||
return { data, isLoading, error };
|
return { data, isLoading, error };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,222 @@
|
||||||
|
import type { MarketingPlan, ChannelStrategyCard, ContentPillar, CalendarWeek, CalendarEntry, ContentCountSummary } from '../types/plan';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raw report data from Supabase marketing_reports table.
|
||||||
|
* The `report` JSONB contains AI-generated analysis.
|
||||||
|
*/
|
||||||
|
interface RawReportRow {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
clinic_name: string;
|
||||||
|
report: Record<string, unknown>;
|
||||||
|
scrape_data?: Record<string, unknown>;
|
||||||
|
analysis_data?: Record<string, unknown>;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHANNEL_NAME_MAP: Record<string, string> = {
|
||||||
|
naverBlog: '네이버 블로그',
|
||||||
|
naverPlace: '네이버 플레이스',
|
||||||
|
gangnamUnni: '강남언니',
|
||||||
|
instagram: 'Instagram',
|
||||||
|
youtube: 'YouTube',
|
||||||
|
facebook: 'Facebook',
|
||||||
|
website: '웹사이트',
|
||||||
|
tiktok: 'TikTok',
|
||||||
|
};
|
||||||
|
|
||||||
|
const CHANNEL_ICON_MAP: Record<string, string> = {
|
||||||
|
naverBlog: 'blog',
|
||||||
|
instagram: 'instagram',
|
||||||
|
youtube: 'youtube',
|
||||||
|
facebook: 'facebook',
|
||||||
|
naverPlace: 'map',
|
||||||
|
gangnamUnni: 'star',
|
||||||
|
website: 'globe',
|
||||||
|
tiktok: 'video',
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildChannelStrategies(
|
||||||
|
channelAnalysis: Record<string, Record<string, unknown>> | undefined,
|
||||||
|
recommendations: Record<string, unknown>[] | undefined,
|
||||||
|
): ChannelStrategyCard[] {
|
||||||
|
if (!channelAnalysis) return [];
|
||||||
|
|
||||||
|
return Object.entries(channelAnalysis).map(([key, ch], i) => {
|
||||||
|
const score = (ch.score as number) ?? 0;
|
||||||
|
const relatedRecs = (recommendations || [])
|
||||||
|
.filter(r => {
|
||||||
|
const cat = ((r.category as string) || '').toLowerCase();
|
||||||
|
return cat.includes(key.toLowerCase()) || cat.includes(CHANNEL_NAME_MAP[key]?.toLowerCase() || '');
|
||||||
|
})
|
||||||
|
.map(r => (r.title as string) || (r.description as string) || '');
|
||||||
|
|
||||||
|
return {
|
||||||
|
channelId: key,
|
||||||
|
channelName: CHANNEL_NAME_MAP[key] || key,
|
||||||
|
icon: CHANNEL_ICON_MAP[key] || 'globe',
|
||||||
|
currentStatus: `점수: ${score}/100 (${(ch.status as string) || 'unknown'})`,
|
||||||
|
targetGoal: (ch.recommendation as string) || '',
|
||||||
|
contentTypes: relatedRecs.length > 0 ? relatedRecs : ['콘텐츠 전략 수립 필요'],
|
||||||
|
postingFrequency: score >= 80 ? '주 3-5회' : score >= 60 ? '주 2-3회' : '주 1-2회 (시작)',
|
||||||
|
tone: '전문적 · 친근한',
|
||||||
|
formatGuidelines: [],
|
||||||
|
priority: (score < 50 ? 'P0' : score < 70 ? 'P1' : 'P2') as 'P0' | 'P1' | 'P2',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildContentPillars(
|
||||||
|
recommendations: Record<string, unknown>[] | undefined,
|
||||||
|
services: string[] | undefined,
|
||||||
|
): ContentPillar[] {
|
||||||
|
const PILLAR_COLORS = ['#6C5CE7', '#E17055', '#00B894', '#FDCB6E'];
|
||||||
|
const pillars: ContentPillar[] = [
|
||||||
|
{
|
||||||
|
title: '전문성 · 신뢰',
|
||||||
|
description: '의료진 소개, 수술 과정, 인증/자격 콘텐츠로 신뢰 구축',
|
||||||
|
relatedUSP: '전문 의료진',
|
||||||
|
exampleTopics: services?.slice(0, 3).map(s => `${s} 시술 과정 소개`) || ['시술 과정 소개'],
|
||||||
|
color: PILLAR_COLORS[0],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '비포 · 애프터',
|
||||||
|
description: '실제 환자 사례, 수술 전후 비교로 결과 시각화',
|
||||||
|
relatedUSP: '검증된 결과',
|
||||||
|
exampleTopics: services?.slice(0, 3).map(s => `${s} 비포/애프터`) || ['비포/애프터 사례'],
|
||||||
|
color: PILLAR_COLORS[1],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '환자 후기 · 리뷰',
|
||||||
|
description: '실제 환자 인터뷰, 후기 콘텐츠로 사회적 증거 확보',
|
||||||
|
relatedUSP: '환자 만족도',
|
||||||
|
exampleTopics: ['환자 인터뷰 영상', '리뷰 하이라이트', '회복 일기'],
|
||||||
|
color: PILLAR_COLORS[2],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '트렌드 · 교육',
|
||||||
|
description: '시술 트렌드, Q&A, 의학 정보로 잠재 고객 유입',
|
||||||
|
relatedUSP: '최신 트렌드',
|
||||||
|
exampleTopics: ['자주 묻는 질문 Q&A', '시술별 비용 가이드', '최신 성형 트렌드'],
|
||||||
|
color: PILLAR_COLORS[3],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return pillars;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCalendar(channels: ChannelStrategyCard[]): {
|
||||||
|
weeks: CalendarWeek[];
|
||||||
|
monthlySummary: ContentCountSummary[];
|
||||||
|
} {
|
||||||
|
const DAYS = ['월', '화', '수', '목', '금', '토', '일'];
|
||||||
|
const activeChannels = channels.filter(c => c.priority !== 'P2').slice(0, 4);
|
||||||
|
|
||||||
|
const weeks: CalendarWeek[] = [1, 2, 3, 4].map(weekNum => {
|
||||||
|
const entries: CalendarEntry[] = [];
|
||||||
|
|
||||||
|
activeChannels.forEach((ch, chIdx) => {
|
||||||
|
const dayOffset = (weekNum - 1 + chIdx) % 7;
|
||||||
|
entries.push({
|
||||||
|
dayOfWeek: dayOffset,
|
||||||
|
channel: ch.channelName,
|
||||||
|
channelIcon: ch.icon,
|
||||||
|
contentType: ch.icon === 'youtube' ? 'video' : ch.icon === 'blog' ? 'blog' : 'social',
|
||||||
|
title: `${ch.channelName} 콘텐츠 ${weekNum}-${chIdx + 1}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
weekNumber: weekNum,
|
||||||
|
label: `${weekNum}주차`,
|
||||||
|
entries,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const videoCount = weeks.reduce((sum, w) => sum + w.entries.filter(e => e.contentType === 'video').length, 0);
|
||||||
|
const blogCount = weeks.reduce((sum, w) => sum + w.entries.filter(e => e.contentType === 'blog').length, 0);
|
||||||
|
const socialCount = weeks.reduce((sum, w) => sum + w.entries.filter(e => e.contentType === 'social').length, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
weeks,
|
||||||
|
monthlySummary: [
|
||||||
|
{ type: 'video', label: '영상', count: videoCount, color: '#6C5CE7' },
|
||||||
|
{ type: 'blog', label: '블로그', count: blogCount, color: '#00B894' },
|
||||||
|
{ type: 'social', label: '소셜', count: socialCount, color: '#E17055' },
|
||||||
|
{ type: 'ad', label: '광고', count: 0, color: '#FDCB6E' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform a raw Supabase report row into a MarketingPlan.
|
||||||
|
* Uses report data (channel analysis, recommendations, services)
|
||||||
|
* to dynamically generate plan content.
|
||||||
|
*/
|
||||||
|
export function transformReportToPlan(row: RawReportRow): MarketingPlan {
|
||||||
|
const report = row.report;
|
||||||
|
const clinicInfo = report.clinicInfo as Record<string, unknown> | undefined;
|
||||||
|
const channelAnalysis = report.channelAnalysis as Record<string, Record<string, unknown>> | undefined;
|
||||||
|
const recommendations = report.recommendations as Record<string, unknown>[] | undefined;
|
||||||
|
const services = (clinicInfo?.services as string[]) || [];
|
||||||
|
|
||||||
|
const channelStrategies = buildChannelStrategies(channelAnalysis, recommendations);
|
||||||
|
const pillars = buildContentPillars(recommendations, services);
|
||||||
|
const calendar = buildCalendar(channelStrategies);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
reportId: row.id,
|
||||||
|
clinicName: (clinicInfo?.name as string) || row.clinic_name || '',
|
||||||
|
clinicNameEn: '',
|
||||||
|
createdAt: row.created_at,
|
||||||
|
targetUrl: row.url,
|
||||||
|
|
||||||
|
brandGuide: {
|
||||||
|
colors: [],
|
||||||
|
fonts: [],
|
||||||
|
logoRules: [],
|
||||||
|
toneOfVoice: {
|
||||||
|
personality: ['전문적', '친근한', '신뢰할 수 있는'],
|
||||||
|
communicationStyle: '의료 전문 지식을 쉽고 친근하게 전달',
|
||||||
|
doExamples: ['정확한 의학 용어 사용', '환자 성공 사례 공유', '전문의 인사이트 제공'],
|
||||||
|
dontExamples: ['과장된 효과 주장', '비교 광고', '의학적 보장 표현'],
|
||||||
|
},
|
||||||
|
channelBranding: [],
|
||||||
|
brandInconsistencies: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
channelStrategies,
|
||||||
|
|
||||||
|
contentStrategy: {
|
||||||
|
pillars,
|
||||||
|
typeMatrix: [
|
||||||
|
{ format: '숏폼 영상 (60초)', channels: ['YouTube Shorts', 'Instagram Reels', 'TikTok'], frequency: '주 3-5회', purpose: '신규 유입 + 인지도' },
|
||||||
|
{ format: '롱폼 영상 (5-15분)', channels: ['YouTube'], frequency: '주 1회', purpose: '전문성 + 검색 SEO' },
|
||||||
|
{ format: '블로그 포스트', channels: ['네이버 블로그'], frequency: '주 2-3회', purpose: 'SEO + 상세 정보' },
|
||||||
|
{ format: '피드 이미지', channels: ['Instagram', 'Facebook'], frequency: '주 3회', purpose: '브랜드 인지도' },
|
||||||
|
],
|
||||||
|
workflow: [
|
||||||
|
{ step: 1, name: '기획', description: '콘텐츠 주제 선정 + 키워드 리서치', owner: 'AI + 마케터', duration: '30분' },
|
||||||
|
{ step: 2, name: '제작', description: 'AI 초안 생성 + 의료진 감수', owner: 'INFINITH AI', duration: '1시간' },
|
||||||
|
{ step: 3, name: '편집', description: '영상/이미지 편집 + 자막 추가', owner: 'INFINITH Studio', duration: '30분' },
|
||||||
|
{ step: 4, name: '배포', description: '채널별 최적화 + 스케줄 배포', owner: 'INFINITH Distribution', duration: '자동' },
|
||||||
|
{ step: 5, name: '분석', description: '성과 데이터 수집 + 최적화 제안', owner: 'INFINITH Analytics', duration: '자동' },
|
||||||
|
],
|
||||||
|
repurposingSource: '1개 롱폼 영상',
|
||||||
|
repurposingOutputs: [
|
||||||
|
{ format: '숏폼 영상 3개', channel: 'YouTube Shorts / Instagram Reels', description: '핵심 장면 추출' },
|
||||||
|
{ format: '블로그 포스트', channel: '네이버 블로그', description: '영상 스크립트 기반 SEO 글' },
|
||||||
|
{ format: '피드 이미지 4장', channel: 'Instagram / Facebook', description: '핵심 정보 카드뉴스' },
|
||||||
|
{ format: '카카오톡 메시지', channel: 'KakaoTalk', description: '환자 타겟 CTA 메시지' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
calendar,
|
||||||
|
|
||||||
|
assetCollection: {
|
||||||
|
assets: [],
|
||||||
|
youtubeRepurpose: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue