feat: 데모 URL 검증 페이지 추가

- DemoPage 컴포넌트: 토큰 검증 및 상태별 UI
- App.tsx: /d/:token 라우트 추가
- 활성/만료/철회/오류 상태 처리
This commit is contained in:
2025-10-14 14:30:05 +09:00
parent 9cfc08f3d1
commit 51cc9dd17d
2 changed files with 199 additions and 21 deletions

View File

@@ -1,33 +1,43 @@
import { QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { BrowserRouter } from 'react-router-dom'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { queryClient } from '@/lib/query-client'
import DemoPage from '@/pages/DemoPage'
function App() {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<div className="min-h-screen bg-background">
<div className="container mx-auto p-8">
<h1 className="text-4xl font-bold text-foreground mb-4">
SAM React Frontend
</h1>
<p className="text-muted-foreground">
React .
</p>
<div className="mt-8 p-6 bg-card rounded-lg border">
<h2 className="text-2xl font-semibold mb-2"> </h2>
<ul className="list-disc list-inside space-y-1 text-card-foreground">
<li>Vite + React + TypeScript</li>
<li>Tailwind CSS with shadcn/ui theming</li>
<li>React Query for server state</li>
<li>Zustand for global state</li>
<li>Axios with interceptors</li>
<li>React Router for routing</li>
</ul>
<Routes>
{/* 데모 페이지 */}
<Route path="/d/:token" element={<DemoPage />} />
{/* 홈 페이지 */}
<Route path="/" element={
<div className="min-h-screen bg-background">
<div className="container mx-auto p-8">
<h1 className="text-4xl font-bold text-foreground mb-4">
SAM React Frontend
</h1>
<p className="text-muted-foreground">
React .
</p>
<div className="mt-8 p-6 bg-card rounded-lg border">
<h2 className="text-2xl font-semibold mb-2"> </h2>
<ul className="list-disc list-inside space-y-1 text-card-foreground">
<li>Vite + React + TypeScript</li>
<li>Tailwind CSS with shadcn/ui theming</li>
<li>React Query for server state</li>
<li>Zustand for global state</li>
<li>Axios with interceptors</li>
<li>React Router for routing</li>
<li>Demo System (/d/:token)</li>
</ul>
</div>
</div>
</div>
</div>
</div>
} />
</Routes>
</BrowserRouter>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>

168
src/pages/DemoPage.tsx Normal file
View File

@@ -0,0 +1,168 @@
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import axios from 'axios';
interface DemoResponse {
status: 'active' | 'expired' | 'revoked' | 'error';
message: string;
company?: string;
expires_at?: string;
visit_count?: number;
expired_at?: string;
}
export default function DemoPage() {
const { token } = useParams<{ token: string }>();
const [data, setData] = useState<DemoResponse | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchDemoData = async () => {
try {
const response = await axios.get(`http://admin.sam.kr/d/${token}`);
setData(response.data);
} catch (error: any) {
setData(error.response?.data || {
status: 'error',
message: '데모 링크를 확인할 수 없습니다.',
});
} finally {
setLoading(false);
}
};
if (token) {
fetchDemoData();
}
}, [token]);
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600"> ...</p>
</div>
</div>
);
}
if (!data) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="bg-white p-8 rounded-lg shadow-lg max-w-md">
<h1 className="text-2xl font-bold text-red-600 mb-4"></h1>
<p className="text-gray-700"> .</p>
</div>
</div>
);
}
// 활성 상태
if (data.status === 'active') {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100">
<div className="bg-white p-10 rounded-2xl shadow-2xl max-w-2xl w-full">
<div className="text-center">
<div className="bg-green-100 rounded-full w-20 h-20 flex items-center justify-center mx-auto mb-6">
<svg className="w-10 h-10 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h1 className="text-4xl font-bold text-gray-900 mb-4">
{data.message}
</h1>
<div className="space-y-3 mt-8">
<div className="bg-blue-50 p-4 rounded-lg">
<p className="text-sm text-blue-600 font-medium"></p>
<p className="text-xl font-semibold text-blue-900">{data.company}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="bg-gray-50 p-4 rounded-lg">
<p className="text-xs text-gray-500"> </p>
<p className="text-sm font-medium text-gray-900">{data.expires_at}</p>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<p className="text-xs text-gray-500"> </p>
<p className="text-sm font-medium text-gray-900">{data.visit_count}</p>
</div>
</div>
</div>
<div className="mt-10 p-6 bg-gradient-to-r from-blue-500 to-indigo-600 rounded-xl">
<h2 className="text-2xl font-bold text-white mb-2">SAM </h2>
<p className="text-blue-100">
.
</p>
</div>
</div>
</div>
</div>
);
}
// 만료/철회/에러 상태
const statusConfig = {
expired: {
title: '만료된 링크',
color: 'text-orange-600',
bgColor: 'bg-orange-100',
iconColor: 'text-orange-600',
icon: (
<svg className="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
},
revoked: {
title: '철회된 링크',
color: 'text-red-600',
bgColor: 'bg-red-100',
iconColor: 'text-red-600',
icon: (
<svg className="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
),
},
error: {
title: '오류',
color: 'text-gray-600',
bgColor: 'bg-gray-100',
iconColor: 'text-gray-600',
icon: (
<svg className="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
),
},
};
const config = statusConfig[data.status as keyof typeof statusConfig] || statusConfig.error;
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="bg-white p-10 rounded-2xl shadow-xl max-w-md w-full text-center">
<div className={`${config.bgColor} rounded-full w-20 h-20 flex items-center justify-center mx-auto mb-6`}>
<div className={config.iconColor}>
{config.icon}
</div>
</div>
<h1 className={`text-3xl font-bold ${config.color} mb-4`}>
{config.title}
</h1>
<p className="text-gray-700 text-lg mb-6">
{data.message}
</p>
{data.expired_at && (
<div className="bg-gray-50 p-4 rounded-lg">
<p className="text-xs text-gray-500"> </p>
<p className="text-sm font-medium text-gray-900">{data.expired_at}</p>
</div>
)}
<p className="mt-8 text-sm text-gray-500">
.
</p>
</div>
</div>
);
}