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)
|
fetchReportById(id)
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
const reportJson = row.report as Record<string, unknown>;
|
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 scrapeData = row.scrape_data as Record<string, unknown> | undefined;
|
||||||
|
|
||||||
const transformed = transformApiReport(
|
const transformed = transformApiReport(
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,21 @@
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useNavigate, useLocation } from 'react-router';
|
import { useNavigate, useLocation } from 'react-router';
|
||||||
import { motion, AnimatePresence } from 'motion/react';
|
import { motion } from 'motion/react';
|
||||||
import { Check, AlertCircle, CheckCircle2, XCircle } from 'lucide-react';
|
import { Check, AlertCircle } from 'lucide-react';
|
||||||
import { discoverChannels, collectChannelData, generateReportV2 } from '../lib/supabase';
|
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';
|
type Phase = 'discovering' | 'collecting' | 'generating' | 'complete';
|
||||||
|
|
||||||
const PHASE_STEPS = [
|
const PHASE_STEPS = [
|
||||||
{ key: 'discovering' as Phase, label: '웹사이트 스캔 & 채널 발견 중...', labelDone: '채널 발견 완료' },
|
{ key: 'discovering' as Phase, label: 'Scanning website & discovering channels...', labelDone: 'Channels discovered' },
|
||||||
{ key: 'collecting' as Phase, label: '채널 데이터 수집 & 시장 분석 중...', labelDone: '데이터 수집 완료' },
|
{ key: 'collecting' as Phase, label: 'Collecting channel data & market analysis...', labelDone: 'Data collected' },
|
||||||
{ key: 'generating' as Phase, label: 'AI 마케팅 리포트 생성 중...', labelDone: '리포트 생성 완료' },
|
{ key: 'generating' as Phase, label: 'Generating AI marketing report...', labelDone: 'Report generated' },
|
||||||
{ key: 'complete' as Phase, label: '분석 완료!', labelDone: '분석 완료!' },
|
{ 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() {
|
export default function AnalysisLoadingPage() {
|
||||||
const [phase, setPhase] = useState<Phase>('discovering');
|
const [phase, setPhase] = useState<Phase>('discovering');
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [verifiedChannels, setVerifiedChannels] = useState<VerifiedChannels | null>(null);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const url = (location.state as { url?: string })?.url;
|
const url = (location.state as { url?: string })?.url;
|
||||||
|
|
@ -66,40 +34,33 @@ export default function AnalysisLoadingPage() {
|
||||||
|
|
||||||
const runPipeline = async () => {
|
const runPipeline = async () => {
|
||||||
try {
|
try {
|
||||||
// ─── Phase 1: Discover Channels ───
|
// Phase 1: Discover Channels
|
||||||
setPhase('discovering');
|
setPhase('discovering');
|
||||||
|
|
||||||
const discovery = await discoverChannels(url);
|
const discovery = await discoverChannels(url);
|
||||||
if (!discovery.success) throw new Error(discovery.error || 'Channel discovery failed');
|
if (!discovery.success) throw new Error(discovery.error || 'Channel discovery failed');
|
||||||
|
|
||||||
setVerifiedChannels(discovery.verifiedChannels);
|
|
||||||
const reportId = discovery.reportId;
|
const reportId = discovery.reportId;
|
||||||
|
|
||||||
// ─── Phase 2: Collect Channel Data ───
|
// Phase 2: Collect Channel Data
|
||||||
setPhase('collecting');
|
setPhase('collecting');
|
||||||
|
|
||||||
const collection = await collectChannelData(reportId);
|
const collection = await collectChannelData(reportId);
|
||||||
if (!collection.success) throw new Error(collection.error || 'Data collection failed');
|
if (!collection.success) throw new Error(collection.error || 'Data collection failed');
|
||||||
|
|
||||||
// ─── Phase 3: Generate Report ───
|
// Phase 3: Generate Report
|
||||||
setPhase('generating');
|
setPhase('generating');
|
||||||
|
|
||||||
const result = await generateReportV2(reportId);
|
const result = await generateReportV2(reportId);
|
||||||
if (!result.success) throw new Error(result.error || 'Report generation failed');
|
if (!result.success) throw new Error(result.error || 'Report generation failed');
|
||||||
|
|
||||||
// ─── Complete ───
|
// Complete — navigate to report
|
||||||
setPhase('complete');
|
setPhase('complete');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate(`/report/${reportId}`, {
|
navigate(`/report/${reportId}`, {
|
||||||
replace: true,
|
replace: true,
|
||||||
state: {
|
state: result.report && result.metadata
|
||||||
report: result.report,
|
? { report: result.report, metadata: result.metadata, reportId }
|
||||||
metadata: result.metadata,
|
: undefined,
|
||||||
reportId,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}, 1000);
|
}, 800);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||||
}
|
}
|
||||||
|
|
@ -133,7 +94,7 @@ export default function AnalysisLoadingPage() {
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.6, delay: 0.1 }}
|
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}
|
{url}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
@ -151,28 +112,26 @@ export default function AnalysisLoadingPage() {
|
||||||
onClick={() => navigate('/', { replace: true })}
|
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"
|
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>
|
</button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Pipeline steps */}
|
<div className="w-full space-y-5 mb-14">
|
||||||
<div className="w-full space-y-5 mb-8">
|
|
||||||
{PHASE_STEPS.map((step, index) => {
|
{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 isActive = phaseIndex === index && phase !== 'complete';
|
||||||
const isDone = step.key === 'complete' && phase === 'complete';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={step.key}
|
key={step.key}
|
||||||
initial={{ opacity: 0, x: -20 }}
|
initial={{ opacity: 0, x: -20 }}
|
||||||
animate={{ opacity: isActive || isCompleted || isDone ? 1 : 0.3, x: 0 }}
|
animate={{ opacity: isActive || isCompleted ? 1 : 0.3, x: 0 }}
|
||||||
transition={{ duration: 0.4, delay: index * 0.1 }}
|
transition={{ duration: 0.4, delay: index * 0.15 }}
|
||||||
className="flex items-center gap-4"
|
className="flex items-center gap-4"
|
||||||
>
|
>
|
||||||
<div className="w-7 h-7 flex-shrink-0 flex items-center justify-center">
|
<div className="w-7 h-7 flex-shrink-0 flex items-center justify-center">
|
||||||
{isCompleted || isDone ? (
|
{isCompleted ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ scale: 0 }}
|
initial={{ scale: 0 }}
|
||||||
animate={{ scale: 1 }}
|
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 className="w-7 h-7 rounded-full border-2 border-white/10" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</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}
|
{isCompleted ? step.labelDone : step.label}
|
||||||
</span>
|
</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
@ -195,46 +158,6 @@ export default function AnalysisLoadingPage() {
|
||||||
})}
|
})}
|
||||||
</div>
|
</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">
|
<div className="w-full h-2 bg-white/10 rounded-full overflow-hidden">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ width: '0%' }}
|
initial={{ width: '0%' }}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue