import { useState, useEffect, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import type { RecentApi, PopularApi, ServiceCatalog } from '../../types/apihub'; import { getRecentApis, getPopularApis, getCatalog } from '../../services/apiHubService'; const formatDomain = (d: string) => (/^[a-zA-Z\s\-_]+$/.test(d) ? d.toUpperCase() : d); // 도메인 컬러 팔레트 (해시 기반 매핑) const DOMAIN_COLOR_PALETTE = [ { color: 'text-emerald-400', bg: 'bg-emerald-500/10', border: 'border-emerald-500/30', line: 'from-emerald-500' }, { color: 'text-rose-400', bg: 'bg-rose-500/10', border: 'border-rose-500/30', line: 'from-rose-500' }, { color: 'text-blue-400', bg: 'bg-blue-500/10', border: 'border-blue-500/30', line: 'from-blue-500' }, { color: 'text-amber-400', bg: 'bg-amber-500/10', border: 'border-amber-500/30', line: 'from-amber-500' }, { color: 'text-violet-400', bg: 'bg-violet-500/10', border: 'border-violet-500/30', line: 'from-violet-500' }, { color: 'text-cyan-400', bg: 'bg-cyan-500/10', border: 'border-cyan-500/30', line: 'from-cyan-500' }, { color: 'text-orange-400', bg: 'bg-orange-500/10', border: 'border-orange-500/30', line: 'from-orange-500' }, { color: 'text-pink-400', bg: 'bg-pink-500/10', border: 'border-pink-500/30', line: 'from-pink-500' }, { color: 'text-lime-400', bg: 'bg-lime-500/10', border: 'border-lime-500/30', line: 'from-lime-500' }, { color: 'text-indigo-400', bg: 'bg-indigo-500/10', border: 'border-indigo-500/30', line: 'from-indigo-500' }, { color: 'text-teal-400', bg: 'bg-teal-500/10', border: 'border-teal-500/30', line: 'from-teal-500' }, { color: 'text-fuchsia-400', bg: 'bg-fuchsia-500/10', border: 'border-fuchsia-500/30', line: 'from-fuchsia-500' }, ]; const DEFAULT_ICON_PATHS = [ 'M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 00-1.883 2.542l.857 6a2.25 2.25 0 002.227 1.932H19.05a2.25 2.25 0 002.227-1.932l.857-6a2.25 2.25 0 00-1.883-2.542m-16.5 0V6A2.25 2.25 0 016 3.75h3.879a1.5 1.5 0 011.06.44l2.122 2.12a1.5 1.5 0 001.06.44H18A2.25 2.25 0 0120.25 9v.776', ]; /** iconPath 문자열에서 SVG path d 값 배열을 추출 */ const parseIconPaths = (iconPath: string | null): string[] => { if (!iconPath) return DEFAULT_ICON_PATHS; const pathRegex = /d="([^"]+)"/g; const matches: string[] = []; let m; while ((m = pathRegex.exec(iconPath)) !== null) { matches.push(m[1]); } return matches.length > 0 ? matches : [iconPath]; }; const domainColorCache = new Map(); let nextColorIdx = 0; const getDomainColorByHash = (domain: string) => { const key = domain.toUpperCase(); const cached = domainColorCache.get(key); if (cached) return cached; const color = DOMAIN_COLOR_PALETTE[nextColorIdx % DOMAIN_COLOR_PALETTE.length]; nextColorIdx++; domainColorCache.set(key, color); return color; }; const RANK_BADGE_STYLES = [ 'bg-gradient-to-br from-yellow-400 to-amber-500', 'bg-gradient-to-br from-gray-300 to-gray-400', 'bg-gradient-to-br from-amber-600 to-amber-700', ]; const extractSettled = (result: PromiseSettledResult<{ data?: T }>, fallback: T): T => { if (result.status === 'fulfilled' && result.value.data !== undefined) { return result.value.data; } return fallback; }; const truncate = (str: string, max: number): string => str.length > max ? str.slice(0, max) + '...' : str; const formatDate = (dateStr: string): string => { const d = new Date(dateStr); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; }; interface FlatDomain { domain: string; iconPath: string | null; sortOrder: number; apiCount: number; } const ApiHubDashboardPage = () => { const navigate = useNavigate(); const [catalog, setCatalog] = useState([]); const [recentApis, setRecentApis] = useState([]); const [popularApis, setPopularApis] = useState([]); const [isLoading, setIsLoading] = useState(true); const fetchAll = useCallback(async () => { try { const [catalogRes, recentRes, popularRes] = await Promise.allSettled([ getCatalog(), getRecentApis(), getPopularApis(), ]); setCatalog(extractSettled(catalogRes, [])); setRecentApis(extractSettled(recentRes, [])); setPopularApis(extractSettled(popularRes, [])); } finally { setIsLoading(false); } }, []); useEffect(() => { fetchAll(); }, [fetchAll]); // 카탈로그에서 도메인 기준으로 플랫하게 집계 const domainList = useMemo(() => { const map = new Map(); for (const svc of catalog) { for (const dg of svc.domains) { const key = dg.domain.toUpperCase(); const existing = map.get(key); const apiCount = (existing?.apiCount ?? 0) + dg.apis.length; const iconPath = existing?.iconPath !== undefined ? existing.iconPath : (dg.iconPath ?? null); const sortOrder = existing?.sortOrder !== undefined ? existing.sortOrder : (dg.sortOrder ?? Number.MAX_SAFE_INTEGER); map.set(key, { iconPath, sortOrder, apiCount }); } } return Array.from(map.entries()) .map(([domain, { iconPath, sortOrder, apiCount }]) => ({ domain, iconPath, sortOrder, apiCount })) .sort((a, b) => a.sortOrder - b.sortOrder || a.domain.localeCompare(b.domain)); }, [catalog]); const recentTop3 = recentApis.slice(0, 3); if (isLoading) { return (
로딩 중...
); } return (
{/* 히어로 배너 */}
{/* 장식 글로우 원 */}
{/* 제목 */}

S&P API HUB

S&P 해양/선박 세계데이터를 직접 만나보세요.

{/* 인기 API 섹션 */} {popularApis.length > 0 && (

인기 API

최근 7일 기준
{popularApis.map((api, idx) => { const palette = api.domain ? getDomainColorByHash(api.domain) : DOMAIN_COLOR_PALETTE[4]; return (
api.serviceId && api.apiId ? navigate(`/api-hub/services/${api.serviceId}/apis/${api.apiId}`) : undefined } >
{/* 랭킹 뱃지 */}
{idx + 1}
{api.domain && ( {formatDomain(api.domain)} )}

{api.apiName}

주간 호출

{api.count.toLocaleString()}

); })}
)} {/* 최신 등록 API 섹션 */}

최신 등록 API

{recentTop3.length > 0 ? (
{recentTop3.map((api) => { const palette = api.apiDomain ? getDomainColorByHash(api.apiDomain) : DOMAIN_COLOR_PALETTE[4]; return (
navigate(`/api-hub/services/${api.serviceId}/apis/${api.apiId}`)} >
{api.apiDomain && ( {formatDomain(api.apiDomain)} )}

{api.apiName}

{api.description && (

{truncate(api.description, 80)}

)}
{formatDate(api.createdAt)}
); })}
) : (
등록된 API가 없습니다
)}
{/* 서비스 도메인 섹션 */} {domainList.length > 0 && (

서비스 도메인

{domainList.map((item) => { const palette = getDomainColorByHash(item.domain); const iconPaths = parseIconPaths(item.iconPath); const imgSrc = `${import.meta.env.BASE_URL}images/domains/${item.domain.toLowerCase()}.jpg`; return (
navigate(`/api-hub/domains/${encodeURIComponent(item.domain)}`)} className={`group relative overflow-hidden rounded-xl border bg-white dark:bg-gray-800 ${palette.border} cursor-pointer transition-all duration-200 hover:-translate-y-1 hover:shadow-xl`} >
{item.domain} { (e.target as HTMLImageElement).style.display = 'none'; }} />
{iconPaths.map((d, i) => ( ))}
{formatDomain(item.domain)}
{item.apiCount} APIs
); })}
)}
); }; export default ApiHubDashboardPage;