generated from gc/template-java-maven
- 디자인 시스템 가이드 문서 11개 생성 (docs/design/) - CSS 변수 토큰 시스템 (@theme + :root/.dark 전환) - cn() 유틸리티 (clsx + tailwind-merge) - Button/Badge 공통 컴포넌트 (variant/size, 다크모드 대응) - 하드코딩 Tailwind 색상 → CSS 변수 토큰 리팩토링 (30개 파일) - 차트 팔레트 다크모드 색상 업데이트 (CHART_COLORS_HEX) - 버튼 다크모드 채도/대비 강화 (primary-600 기반) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
224 lines
8.7 KiB
TypeScript
224 lines
8.7 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import type { ServiceCatalog, ServiceApiItem } from '../../types/apihub';
|
|
import { getServiceCatalog } from '../../services/apiHubService';
|
|
|
|
const METHOD_COLORS: Record<string, string> = {
|
|
GET: 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300',
|
|
POST: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300',
|
|
PUT: 'bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300',
|
|
PATCH: 'bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300',
|
|
DELETE: 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300',
|
|
};
|
|
|
|
const HEALTH_DOT: Record<string, string> = {
|
|
UP: 'bg-green-500',
|
|
DOWN: 'bg-red-500',
|
|
UNKNOWN: 'bg-gray-400',
|
|
};
|
|
|
|
const HEALTH_BADGE: Record<string, string> = {
|
|
UP: 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300',
|
|
DOWN: 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300',
|
|
UNKNOWN: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300',
|
|
};
|
|
|
|
const HEALTH_LABEL: Record<string, string> = {
|
|
UP: '정상',
|
|
DOWN: '중단',
|
|
UNKNOWN: '알 수 없음',
|
|
};
|
|
|
|
interface DomainSectionProps {
|
|
domainName: string;
|
|
apis: ServiceApiItem[];
|
|
serviceId: number;
|
|
onNavigate: (serviceId: number, apiId: number) => void;
|
|
}
|
|
|
|
const DomainSection = ({ domainName, apis, serviceId, onNavigate }: DomainSectionProps) => (
|
|
<div className="mb-6">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<h3 className="text-base font-semibold text-[var(--color-text-primary)]">{domainName}</h3>
|
|
<span className="text-xs px-2 py-0.5 rounded-full bg-[var(--color-bg-base)] text-[var(--color-text-secondary)]">
|
|
{apis.length}개
|
|
</span>
|
|
</div>
|
|
<div className="bg-[var(--color-bg-surface)] rounded-lg shadow overflow-hidden border border-[var(--color-border)]">
|
|
<table className="w-full text-sm table-fixed">
|
|
<colgroup>
|
|
<col className="w-[8%]" />
|
|
<col className="w-[27%]" />
|
|
<col className="w-[20%]" />
|
|
<col className="w-[40%]" />
|
|
<col className="w-[5%]" />
|
|
</colgroup>
|
|
<thead className="bg-[var(--color-bg-base)]">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--color-text-secondary)] uppercase">메서드</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--color-text-secondary)] uppercase">경로</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--color-text-secondary)] uppercase">API명</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--color-text-secondary)] uppercase">설명</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--color-text-secondary)] uppercase">상태</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-[var(--color-border)]">
|
|
{apis.map((api) => (
|
|
<tr
|
|
key={api.apiId}
|
|
className="hover:bg-blue-50 dark:hover:bg-gray-700 cursor-pointer transition-colors"
|
|
onClick={() => onNavigate(serviceId, api.apiId)}
|
|
>
|
|
<td className="px-4 py-3">
|
|
<span
|
|
className={`inline-block px-2 py-0.5 rounded text-xs font-bold uppercase ${METHOD_COLORS[api.apiMethod] ?? 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300'}`}
|
|
>
|
|
{api.apiMethod}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3 font-mono text-xs text-[var(--color-text-primary)] truncate" title={api.apiPath}>
|
|
{api.apiPath}
|
|
</td>
|
|
<td className="px-4 py-3 text-[var(--color-text-primary)] font-medium truncate" title={api.apiName}>
|
|
{api.apiName}
|
|
</td>
|
|
<td className="px-4 py-3 text-[var(--color-text-secondary)] truncate" title={api.description || ''}>
|
|
{api.description || <span className="text-[var(--color-text-tertiary)]">-</span>}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
{api.isActive ? (
|
|
<span className="inline-block w-2 h-2 rounded-full bg-green-500" title="활성" />
|
|
) : (
|
|
<span className="inline-block w-2 h-2 rounded-full bg-gray-300 dark:bg-gray-600" title="비활성" />
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const ApiHubServicePage = () => {
|
|
const { serviceId } = useParams<{ serviceId: string }>();
|
|
const navigate = useNavigate();
|
|
const [service, setService] = useState<ServiceCatalog | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const fetchData = useCallback(async () => {
|
|
if (!serviceId) return;
|
|
try {
|
|
const res = await getServiceCatalog(Number(serviceId));
|
|
if (res.success && res.data) {
|
|
setService(res.data);
|
|
} else {
|
|
setError('서비스 정보를 불러오지 못했습니다');
|
|
}
|
|
} catch {
|
|
setError('서비스 정보를 불러오는 중 오류가 발생했습니다');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [serviceId]);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, [fetchData]);
|
|
|
|
const handleApiNavigate = (svcId: number, apiId: number) => {
|
|
navigate(`/api-hub/services/${svcId}/apis/${apiId}`);
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="text-[var(--color-text-secondary)]">로딩 중...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error || !service) {
|
|
return (
|
|
<div>
|
|
<button
|
|
onClick={() => navigate('/api-hub')}
|
|
className="text-sm text-[var(--color-primary)] hover:text-blue-800 mb-4 inline-block"
|
|
>
|
|
← API HUB으로
|
|
</button>
|
|
<div className="text-center py-20 text-[var(--color-text-secondary)]">
|
|
{error ?? '서비스를 찾을 수 없습니다'}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const domainsMap = new Map<string, ServiceApiItem[]>();
|
|
for (const dg of service.domains) {
|
|
const key = dg.domain ? dg.domain.toUpperCase() : '기타';
|
|
domainsMap.set(key, dg.apis);
|
|
}
|
|
|
|
// If any apis fall under null domain and weren't covered by domains array, show them under 기타
|
|
const domainEntries = [...domainsMap.entries()];
|
|
|
|
return (
|
|
<div className="max-w-7xl mx-auto">
|
|
<button
|
|
onClick={() => navigate('/api-hub')}
|
|
className="text-sm text-[var(--color-primary)] hover:text-blue-800 mb-4 inline-block"
|
|
>
|
|
← API HUB으로
|
|
</button>
|
|
|
|
{/* Service Header */}
|
|
<div className="bg-[var(--color-bg-surface)] rounded-lg shadow p-6 mb-6 border border-[var(--color-border)]">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-3 mb-1">
|
|
<h1 className="text-2xl font-bold text-[var(--color-text-primary)]">{service.serviceName}</h1>
|
|
<span className="text-sm text-[var(--color-text-secondary)] font-mono">{service.serviceCode}</span>
|
|
</div>
|
|
{service.description && (
|
|
<p className="text-[var(--color-text-secondary)] mt-2">{service.description}</p>
|
|
)}
|
|
</div>
|
|
<div className="flex flex-col items-end gap-2 ml-6 shrink-0">
|
|
<span
|
|
className={`flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-medium ${HEALTH_BADGE[service.healthStatus] ?? 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300'}`}
|
|
>
|
|
<span className={`w-2 h-2 rounded-full ${HEALTH_DOT[service.healthStatus] ?? 'bg-gray-400'}`} />
|
|
{HEALTH_LABEL[service.healthStatus] ?? service.healthStatus}
|
|
</span>
|
|
<div className="flex items-center gap-4 text-sm text-[var(--color-text-secondary)]">
|
|
<span>API {service.apiCount}개</span>
|
|
<span>도메인 {service.domains.length}개</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* API 목록 by Domain */}
|
|
{domainEntries.length > 0 ? (
|
|
domainEntries.map(([domain, apis]) => (
|
|
<DomainSection
|
|
key={domain}
|
|
domainName={domain}
|
|
apis={apis}
|
|
serviceId={service.serviceId}
|
|
onNavigate={handleApiNavigate}
|
|
/>
|
|
))
|
|
) : (
|
|
<div className="bg-[var(--color-bg-surface)] rounded-lg shadow p-8 text-center text-[var(--color-text-tertiary)]">
|
|
등록된 API가 없습니다
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ApiHubServicePage;
|