feat(api-hub): API HUB 상세 화면 개선

- 요청 URL 생성 영역 아코디언 형태로 변경
- 샘플 URL 영역 추가 (기본 정보 하단)
- 출력결과 2열 레이아웃 (변수명|의미(단위)) 추가
- 공통 샘플 코드 연동

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
HYOJIN 2026-04-14 13:57:53 +09:00
부모 ac0f51b816
커밋 dd1ac022d2
2개의 변경된 파일24개의 추가작업 그리고 20개의 파일을 삭제

파일 보기

@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import type { ServiceCatalog, ServiceApiItem } from '../../types/apihub'; import type { ServiceCatalog, ServiceApiItem } from '../../types/apihub';
import { getCatalog } from '../../services/apiHubService'; import { getServiceCatalog } from '../../services/apiHubService';
const METHOD_COLORS: Record<string, string> = { const METHOD_COLORS: Record<string, string> = {
GET: 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300', GET: 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300',
@ -29,9 +29,6 @@ const HEALTH_LABEL: Record<string, string> = {
UNKNOWN: '알 수 없음', UNKNOWN: '알 수 없음',
}; };
const truncate = (str: string, max: number): string =>
str.length > max ? str.slice(0, max) + '...' : str;
interface DomainSectionProps { interface DomainSectionProps {
domainName: string; domainName: string;
apis: ServiceApiItem[]; apis: ServiceApiItem[];
@ -48,14 +45,21 @@ const DomainSection = ({ domainName, apis, serviceId, onNavigate }: DomainSectio
</span> </span>
</div> </div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden border border-gray-100 dark:border-gray-700"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden border border-gray-100 dark:border-gray-700">
<table className="w-full text-sm"> <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-gray-50 dark:bg-gray-700"> <thead className="bg-gray-50 dark:bg-gray-700">
<tr> <tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase w-24"></th> <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase"></th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase"></th> <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase"></th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">API명</th> <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">API명</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase"></th> <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase"></th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase w-16"></th> <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase"></th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700"> <tbody className="divide-y divide-gray-200 dark:divide-gray-700">
@ -72,14 +76,14 @@ const DomainSection = ({ domainName, apis, serviceId, onNavigate }: DomainSectio
{api.apiMethod} {api.apiMethod}
</span> </span>
</td> </td>
<td className="px-4 py-3 font-mono text-xs text-gray-700 dark:text-gray-300 max-w-xs truncate" title={api.apiPath}> <td className="px-4 py-3 font-mono text-xs text-gray-700 dark:text-gray-300 truncate" title={api.apiPath}>
{api.apiPath} {api.apiPath}
</td> </td>
<td className="px-4 py-3 text-gray-900 dark:text-gray-100 font-medium max-w-xs truncate" title={api.apiName}> <td className="px-4 py-3 text-gray-900 dark:text-gray-100 font-medium truncate" title={api.apiName}>
{api.apiName} {api.apiName}
</td> </td>
<td className="px-4 py-3 text-gray-500 dark:text-gray-400 max-w-sm"> <td className="px-4 py-3 text-gray-500 dark:text-gray-400 truncate" title={api.description || ''}>
{api.description ? truncate(api.description, 60) : <span className="text-gray-300 dark:text-gray-600">-</span>} {api.description || <span className="text-gray-300 dark:text-gray-600">-</span>}
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
{api.isActive ? ( {api.isActive ? (
@ -106,14 +110,9 @@ const ApiHubServicePage = () => {
const fetchData = useCallback(async () => { const fetchData = useCallback(async () => {
if (!serviceId) return; if (!serviceId) return;
try { try {
const res = await getCatalog(); const res = await getServiceCatalog(Number(serviceId));
if (res.success && res.data) { if (res.success && res.data) {
const found = res.data.find((s) => s.serviceId === Number(serviceId)); setService(res.data);
if (found) {
setService(found);
} else {
setError('서비스를 찾을 수 없습니다');
}
} else { } else {
setError('서비스 정보를 불러오지 못했습니다'); setError('서비스 정보를 불러오지 못했습니다');
} }
@ -158,7 +157,7 @@ const ApiHubServicePage = () => {
const domainsMap = new Map<string, ServiceApiItem[]>(); const domainsMap = new Map<string, ServiceApiItem[]>();
for (const dg of service.domains) { for (const dg of service.domains) {
const key = dg.domain || '기타'; const key = dg.domain ? dg.domain.toUpperCase() : '기타';
domainsMap.set(key, dg.apis); domainsMap.set(key, dg.apis);
} }
@ -166,7 +165,7 @@ const ApiHubServicePage = () => {
const domainEntries = [...domainsMap.entries()]; const domainEntries = [...domainsMap.entries()];
return ( return (
<div> <div className="max-w-7xl mx-auto">
<button <button
onClick={() => navigate('/api-hub')} onClick={() => navigate('/api-hub')}
className="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 mb-4 inline-block" className="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 mb-4 inline-block"

파일 보기

@ -1,5 +1,10 @@
import { get } from './apiClient'; import { get } from './apiClient';
import type { ServiceCatalog, RecentApi } from '../types/apihub'; import type { ServiceCatalog, RecentApi } from '../types/apihub';
import type { ApiDetailInfo } from '../types/service';
export const getCatalog = () => get<ServiceCatalog[]>('/api-hub/catalog'); export const getCatalog = () => get<ServiceCatalog[]>('/api-hub/catalog');
export const getRecentApis = () => get<RecentApi[]>('/api-hub/recent-apis'); export const getRecentApis = () => get<RecentApi[]>('/api-hub/recent-apis');
export const getServiceCatalog = (serviceId: number) =>
get<ServiceCatalog>(`/api-hub/services/${serviceId}`);
export const getApiHubApiDetail = (serviceId: number, apiId: number) =>
get<ApiDetailInfo>(`/api-hub/services/${serviceId}/apis/${apiId}`);