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
parent
caac2f22c5
commit
a6bb31a093
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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%' }}
|
||||
|
|
|
|||
Loading…
Reference in New Issue