fix: restore English loading steps, hide channel panel, fix blank report page

- Loading steps back to English (Scanning website, Collecting data, etc.)
- Removed verified channels panel from loading screen
- Fixed blank report page: detect empty report JSON from DB and show
  appropriate error message instead of rendering empty components
- Navigation state: only pass if report+metadata exist

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
claude/bold-hawking
Haewon Kam 2026-04-03 23:35:06 +09:00
parent caac2f22c5
commit a6bb31a093
2 changed files with 36 additions and 103 deletions

View File

@ -72,6 +72,16 @@ export function useReport(id: string | undefined): UseReportResult {
fetchReportById(id)
.then((row) => {
const reportJson = row.report as Record<string, unknown>;
// V2 pipeline: if report is empty but status is not 'complete', show message
if (!reportJson || Object.keys(reportJson).length === 0 || reportJson.parseError) {
const status = (row as Record<string, unknown>).status as string | undefined;
if (status && status !== 'complete') {
throw new Error(`리포트 생성 중입니다 (${status}). 잠시 후 새로고침 해주세요.`);
}
throw new Error('리포트 데이터가 비어있습니다. 분석을 다시 실행해주세요.');
}
const scrapeData = row.scrape_data as Record<string, unknown> | undefined;
const transformed = transformApiReport(

View File

@ -1,53 +1,21 @@
import { useState, useEffect, useRef } from 'react';
import { useNavigate, useLocation } from 'react-router';
import { motion, AnimatePresence } from 'motion/react';
import { Check, AlertCircle, CheckCircle2, XCircle } from 'lucide-react';
import { motion } from 'motion/react';
import { Check, AlertCircle } from 'lucide-react';
import { discoverChannels, collectChannelData, generateReportV2 } from '../lib/supabase';
/**
* Pipeline V2: 3-phase analysis with real progress.
* Phase 1: discover-channels verify social handles
* Phase 2: collect-channel-data gather all channel data + market analysis
* Phase 3: generate-report AI report from real data
*/
interface VerifiedChannel {
handle: string;
verified: boolean;
url?: string;
}
interface VerifiedChannels {
instagram?: VerifiedChannel[];
youtube?: VerifiedChannel | null;
facebook?: VerifiedChannel | null;
naverBlog?: VerifiedChannel | null;
gangnamUnni?: VerifiedChannel | null;
tiktok?: VerifiedChannel | null;
}
type Phase = 'discovering' | 'collecting' | 'generating' | 'complete';
const PHASE_STEPS = [
{ key: 'discovering' as Phase, label: '웹사이트 스캔 & 채널 발견 중...', labelDone: '채널 발견 완료' },
{ key: 'collecting' as Phase, label: '채널 데이터 수집 & 시장 분석 중...', labelDone: '데이터 수집 완료' },
{ key: 'generating' as Phase, label: 'AI 마케팅 리포트 생성 중...', labelDone: '리포트 생성 완료' },
{ key: 'complete' as Phase, label: '분석 완료!', labelDone: '분석 완료!' },
{ key: 'discovering' as Phase, label: 'Scanning website & discovering channels...', labelDone: 'Channels discovered' },
{ key: 'collecting' as Phase, label: 'Collecting channel data & market analysis...', labelDone: 'Data collected' },
{ key: 'generating' as Phase, label: 'Generating AI marketing report...', labelDone: 'Report generated' },
{ key: 'complete' as Phase, label: 'Finalizing report...', labelDone: 'Complete' },
];
const CHANNEL_LABELS: Record<string, string> = {
instagram: 'Instagram',
youtube: 'YouTube',
facebook: 'Facebook',
naverBlog: '네이버 블로그',
gangnamUnni: '강남언니',
tiktok: 'TikTok',
};
export default function AnalysisLoadingPage() {
const [phase, setPhase] = useState<Phase>('discovering');
const [error, setError] = useState<string | null>(null);
const [verifiedChannels, setVerifiedChannels] = useState<VerifiedChannels | null>(null);
const navigate = useNavigate();
const location = useLocation();
const url = (location.state as { url?: string })?.url;
@ -66,40 +34,33 @@ export default function AnalysisLoadingPage() {
const runPipeline = async () => {
try {
// ─── Phase 1: Discover Channels ───
// Phase 1: Discover Channels
setPhase('discovering');
const discovery = await discoverChannels(url);
if (!discovery.success) throw new Error(discovery.error || 'Channel discovery failed');
setVerifiedChannels(discovery.verifiedChannels);
const reportId = discovery.reportId;
// ─── Phase 2: Collect Channel Data ───
// Phase 2: Collect Channel Data
setPhase('collecting');
const collection = await collectChannelData(reportId);
if (!collection.success) throw new Error(collection.error || 'Data collection failed');
// ─── Phase 3: Generate Report ───
// Phase 3: Generate Report
setPhase('generating');
const result = await generateReportV2(reportId);
if (!result.success) throw new Error(result.error || 'Report generation failed');
// ─── Complete ───
// Complete — navigate to report
setPhase('complete');
setTimeout(() => {
navigate(`/report/${reportId}`, {
replace: true,
state: {
report: result.report,
metadata: result.metadata,
reportId,
},
state: result.report && result.metadata
? { report: result.report, metadata: result.metadata, reportId }
: undefined,
});
}, 1000);
}, 800);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
}
@ -133,7 +94,7 @@ export default function AnalysisLoadingPage() {
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-purple-300/80 text-sm font-mono mb-10 truncate max-w-full"
className="text-purple-300/80 text-sm font-mono mb-12 truncate max-w-full"
>
{url}
</motion.p>
@ -151,28 +112,26 @@ export default function AnalysisLoadingPage() {
onClick={() => navigate('/', { replace: true })}
className="px-6 py-2 text-sm font-medium text-white bg-white/10 rounded-lg hover:bg-white/20 transition-colors"
>
Try Again
</button>
</motion.div>
) : (
<>
{/* Pipeline steps */}
<div className="w-full space-y-5 mb-8">
<div className="w-full space-y-5 mb-14">
{PHASE_STEPS.map((step, index) => {
const isCompleted = phaseIndex > index;
const isCompleted = phaseIndex > index || (step.key === 'complete' && phase === 'complete');
const isActive = phaseIndex === index && phase !== 'complete';
const isDone = step.key === 'complete' && phase === 'complete';
return (
<motion.div
key={step.key}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: isActive || isCompleted || isDone ? 1 : 0.3, x: 0 }}
transition={{ duration: 0.4, delay: index * 0.1 }}
animate={{ opacity: isActive || isCompleted ? 1 : 0.3, x: 0 }}
transition={{ duration: 0.4, delay: index * 0.15 }}
className="flex items-center gap-4"
>
<div className="w-7 h-7 flex-shrink-0 flex items-center justify-center">
{isCompleted || isDone ? (
{isCompleted ? (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
@ -187,7 +146,11 @@ export default function AnalysisLoadingPage() {
<div className="w-7 h-7 rounded-full border-2 border-white/10" />
)}
</div>
<span className={`text-base font-sans transition-colors duration-300 ${isCompleted || isDone ? 'text-white' : isActive ? 'text-purple-200' : 'text-white/30'}`}>
<span
className={`text-base font-sans transition-colors duration-300 ${
isCompleted ? 'text-white' : isActive ? 'text-purple-200' : 'text-white/30'
}`}
>
{isCompleted ? step.labelDone : step.label}
</span>
</motion.div>
@ -195,46 +158,6 @@ export default function AnalysisLoadingPage() {
})}
</div>
{/* Verified channels panel — shows after Phase 1 */}
<AnimatePresence>
{verifiedChannels && (
<motion.div
initial={{ opacity: 0, y: 10, height: 0 }}
animate={{ opacity: 1, y: 0, height: 'auto' }}
exit={{ opacity: 0 }}
transition={{ duration: 0.4 }}
className="w-full mb-8 rounded-xl bg-white/[0.06] border border-white/10 p-4"
>
<p className="text-xs text-purple-300/60 uppercase tracking-wider mb-3 font-semibold">
</p>
<div className="space-y-2">
{Object.entries(verifiedChannels).map(([key, value]) => {
if (!value) return null;
const channels = Array.isArray(value) ? value : [value];
return channels.map((ch, i) => (
<div key={`${key}-${i}`} className="flex items-center gap-2">
{ch.verified ? (
<CheckCircle2 size={14} className="text-green-400 shrink-0" />
) : (
<XCircle size={14} className="text-red-400/60 shrink-0" />
)}
<span className={`text-sm ${ch.verified ? 'text-purple-100' : 'text-white/30'}`}>
{CHANNEL_LABELS[key] || key}
{ch.handle && key !== 'gangnamUnni' ? ` @${ch.handle}` : ''}
</span>
<span className={`text-[10px] ml-auto ${ch.verified ? 'text-green-400/60' : 'text-red-400/40'}`}>
{ch.verified ? 'verified' : 'not found'}
</span>
</div>
));
})}
</div>
</motion.div>
)}
</AnimatePresence>
{/* Progress bar */}
<div className="w-full h-2 bg-white/10 rounded-full overflow-hidden">
<motion.div
initial={{ width: '0%' }}