feat: 데모 URL 검증 페이지 추가
- DemoPage 컴포넌트: 토큰 검증 및 상태별 UI - App.tsx: /d/:token 라우트 추가 - 활성/만료/철회/오류 상태 처리
This commit is contained in:
12
src/App.tsx
12
src/App.tsx
@@ -1,12 +1,19 @@
|
||||
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>
|
||||
<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">
|
||||
@@ -24,10 +31,13 @@ function App() {
|
||||
<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>
|
||||
} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
|
||||
168
src/pages/DemoPage.tsx
Normal file
168
src/pages/DemoPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user