generated from gc/template-java-maven
feat(api-hub): API 사용 신청 모달 및 API 선택 UI 도메인 기반 변경
- API HUB 상세 화면에 API 사용 신청 모달 추가 - 모달 내 도메인 기반 체크박스 트리로 API 선택 - KeyRequestPage API 선택: 서비스 기반 → 도메인 기반 변경 - API 행에서 Path/Method 제거, API명만 표시 - 도메인 정렬순서 카탈로그(sortOrder) 기준으로 통일 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
부모
17d870c06a
커밋
01fe6e62f7
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
Load Diff
@ -1,16 +1,8 @@
|
|||||||
import { useState, useEffect, useMemo, useRef } from 'react';
|
import { useState, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import type { ServiceInfo, ServiceApi } from '../../types/service';
|
import { getCatalog } from '../../services/apiHubService';
|
||||||
import { getServices, getServiceApis } from '../../services/serviceService';
|
|
||||||
import { createKeyRequest } from '../../services/apiKeyService';
|
import { createKeyRequest } from '../../services/apiKeyService';
|
||||||
|
import type { ServiceCatalog } from '../../types/apihub';
|
||||||
const METHOD_BADGE_STYLE: Record<string, string> = {
|
|
||||||
GET: 'bg-emerald-900/80 text-emerald-300 border border-emerald-700/50',
|
|
||||||
POST: 'bg-amber-900/80 text-amber-300 border border-amber-700/50',
|
|
||||||
PUT: 'bg-blue-900/80 text-blue-300 border border-blue-700/50',
|
|
||||||
DELETE: 'bg-red-900/80 text-red-300 border border-red-700/50',
|
|
||||||
PATCH: 'bg-purple-900/80 text-purple-300 border border-purple-700/50',
|
|
||||||
};
|
|
||||||
|
|
||||||
const IndeterminateCheckbox = ({ checked, indeterminate, onChange, className }: { checked: boolean; indeterminate: boolean; onChange: () => void; className?: string }) => {
|
const IndeterminateCheckbox = ({ checked, indeterminate, onChange, className }: { checked: boolean; indeterminate: boolean; onChange: () => void; className?: string }) => {
|
||||||
const ref = useRef<HTMLInputElement>(null);
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
@ -20,36 +12,23 @@ const IndeterminateCheckbox = ({ checked, indeterminate, onChange, className }:
|
|||||||
return <input ref={ref} type="checkbox" checked={checked} onChange={onChange} className={className || 'rounded'} />;
|
return <input ref={ref} type="checkbox" checked={checked} onChange={onChange} className={className || 'rounded'} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface DomainGroup {
|
interface FlatApi {
|
||||||
domain: string;
|
apiId: number;
|
||||||
apis: ServiceApi[];
|
apiName: string;
|
||||||
|
description: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupApisByDomain = (apis: ServiceApi[]): DomainGroup[] => {
|
interface FlatDomainGroup {
|
||||||
const domainMap = new Map<string, ServiceApi[]>();
|
domain: string;
|
||||||
|
apis: FlatApi[];
|
||||||
apis.forEach((api) => {
|
}
|
||||||
const domain = api.apiDomain || '미분류';
|
|
||||||
if (!domainMap.has(domain)) {
|
|
||||||
domainMap.set(domain, []);
|
|
||||||
}
|
|
||||||
domainMap.get(domain)!.push(api);
|
|
||||||
});
|
|
||||||
|
|
||||||
const result: DomainGroup[] = [];
|
|
||||||
domainMap.forEach((domainApis, domain) => {
|
|
||||||
result.push({ domain, apis: domainApis });
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const KeyRequestPage = () => {
|
const KeyRequestPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const preApiId = searchParams.get('apiId');
|
||||||
|
|
||||||
const [services, setServices] = useState<ServiceInfo[]>([]);
|
const [catalog, setCatalog] = useState<ServiceCatalog[]>([]);
|
||||||
const [serviceApisMap, setServiceApisMap] = useState<Record<number, ServiceApi[]>>({});
|
|
||||||
const [expandedServices, setExpandedServices] = useState<Set<number>>(new Set());
|
|
||||||
const [expandedDomains, setExpandedDomains] = useState<Set<string>>(new Set());
|
const [expandedDomains, setExpandedDomains] = useState<Set<string>>(new Set());
|
||||||
const [selectedApiIds, setSelectedApiIds] = useState<Set<number>>(new Set());
|
const [selectedApiIds, setSelectedApiIds] = useState<Set<number>>(new Set());
|
||||||
const [keyName, setKeyName] = useState('');
|
const [keyName, setKeyName] = useState('');
|
||||||
@ -71,61 +50,95 @@ const KeyRequestPage = () => {
|
|||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const servicesRes = await getServices();
|
const catalogRes = await getCatalog();
|
||||||
if (servicesRes.success && servicesRes.data) {
|
if (catalogRes.success && catalogRes.data) {
|
||||||
const activeServices = servicesRes.data.filter((s) => s.isActive);
|
setCatalog(catalogRes.data);
|
||||||
setServices(activeServices);
|
|
||||||
|
|
||||||
const apisMap: Record<number, ServiceApi[]> = {};
|
// 쿼리 파라미터로 전달된 API 자동 선택
|
||||||
await Promise.all(
|
if (preApiId) {
|
||||||
activeServices.map(async (service) => {
|
const aId = Number(preApiId);
|
||||||
const apisRes = await getServiceApis(service.serviceId);
|
for (const service of catalogRes.data) {
|
||||||
if (apisRes.success && apisRes.data) {
|
for (const domainGroup of service.domains) {
|
||||||
apisMap[service.serviceId] = apisRes.data.filter((a) => a.isActive);
|
const targetApi = domainGroup.apis.find((a) => a.apiId === aId);
|
||||||
|
if (targetApi) {
|
||||||
|
setSelectedApiIds(new Set([aId]));
|
||||||
|
setExpandedDomains(new Set([domainGroup.domain]));
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
);
|
}
|
||||||
setServiceApisMap(apisMap);
|
|
||||||
} else {
|
} else {
|
||||||
setError(servicesRes.message || '서비스 목록을 불러오는데 실패했습니다.');
|
setError(catalogRes.message || '카탈로그를 불러오는데 실패했습니다.');
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setError('서비스 목록을 불러오는데 실패했습니다.');
|
setError('카탈로그를 불러오는데 실패했습니다.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, [preApiId]);
|
||||||
|
|
||||||
const groupedApisMap = useMemo(() => {
|
// catalog → FlatDomainGroup[] 변환 (도메인 기준 플랫 그룹핑, 알파벳순 정렬)
|
||||||
const result: Record<number, DomainGroup[]> = {};
|
const flatDomainGroups = useMemo<FlatDomainGroup[]>(() => {
|
||||||
Object.entries(serviceApisMap).forEach(([serviceId, apis]) => {
|
const domainMap = new Map<string, FlatApi[]>();
|
||||||
result[Number(serviceId)] = groupApisByDomain(apis);
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}, [serviceApisMap]);
|
|
||||||
|
|
||||||
const handleToggleService = (serviceId: number) => {
|
for (const service of catalog) {
|
||||||
setExpandedServices((prev) => {
|
for (const domainGroup of service.domains) {
|
||||||
const next = new Set(prev);
|
const domainName = domainGroup.domain || '미분류';
|
||||||
if (next.has(serviceId)) {
|
if (!domainMap.has(domainName)) {
|
||||||
next.delete(serviceId);
|
domainMap.set(domainName, []);
|
||||||
} else {
|
}
|
||||||
next.add(serviceId);
|
const existing = domainMap.get(domainName)!;
|
||||||
|
for (const api of domainGroup.apis) {
|
||||||
|
existing.push({
|
||||||
|
apiId: api.apiId,
|
||||||
|
apiName: api.apiName,
|
||||||
|
description: api.description,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return next;
|
}
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleDomain = (key: string) => {
|
const result: FlatDomainGroup[] = [];
|
||||||
|
domainMap.forEach((apis, domain) => {
|
||||||
|
result.push({ domain, apis });
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, [catalog]);
|
||||||
|
|
||||||
|
const allApis = useMemo<FlatApi[]>(() => {
|
||||||
|
return flatDomainGroups.flatMap((dg) => dg.apis);
|
||||||
|
}, [flatDomainGroups]);
|
||||||
|
|
||||||
|
const allApisSelected = allApis.length > 0 && allApis.every((a) => selectedApiIds.has(a.apiId));
|
||||||
|
const someApisSelected = allApis.some((a) => selectedApiIds.has(a.apiId));
|
||||||
|
|
||||||
|
const filteredDomainGroups = useMemo<FlatDomainGroup[]>(() => {
|
||||||
|
if (!searchQuery.trim()) return flatDomainGroups;
|
||||||
|
const query = searchQuery.toLowerCase();
|
||||||
|
return flatDomainGroups
|
||||||
|
.map((dg) => ({
|
||||||
|
domain: dg.domain,
|
||||||
|
apis: dg.apis.filter(
|
||||||
|
(a) =>
|
||||||
|
a.apiName.toLowerCase().includes(query) ||
|
||||||
|
(a.description?.toLowerCase().includes(query) ?? false),
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.filter((dg) => dg.apis.length > 0);
|
||||||
|
}, [flatDomainGroups, searchQuery]);
|
||||||
|
|
||||||
|
const handleToggleDomain = (domain: string) => {
|
||||||
setExpandedDomains((prev) => {
|
setExpandedDomains((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
if (next.has(key)) {
|
if (next.has(domain)) {
|
||||||
next.delete(key);
|
next.delete(domain);
|
||||||
} else {
|
} else {
|
||||||
next.add(key);
|
next.add(domain);
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
@ -143,26 +156,8 @@ const KeyRequestPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleAllServiceApis = (serviceId: number) => {
|
const handleToggleAllDomainApis = (domain: string) => {
|
||||||
const apis = serviceApisMap[serviceId] || [];
|
const domainGroup = flatDomainGroups.find((dg) => dg.domain === domain);
|
||||||
const allSelected = apis.every((a) => selectedApiIds.has(a.apiId));
|
|
||||||
|
|
||||||
setSelectedApiIds((prev) => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
apis.forEach((a) => {
|
|
||||||
if (allSelected) {
|
|
||||||
next.delete(a.apiId);
|
|
||||||
} else {
|
|
||||||
next.add(a.apiId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleAllDomainApis = (serviceId: number, domain: string) => {
|
|
||||||
const domainGroups = groupedApisMap[serviceId] || [];
|
|
||||||
const domainGroup = domainGroups.find((d) => d.domain === domain);
|
|
||||||
if (!domainGroup) return;
|
if (!domainGroup) return;
|
||||||
|
|
||||||
const domainApis = domainGroup.apis;
|
const domainApis = domainGroup.apis;
|
||||||
@ -181,15 +176,6 @@ const KeyRequestPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const allApis = useMemo(() => {
|
|
||||||
return Object.values(serviceApisMap).flat();
|
|
||||||
}, [serviceApisMap]);
|
|
||||||
|
|
||||||
const allApisSelected = allApis.length > 0 && allApis.every((a) => selectedApiIds.has(a.apiId));
|
|
||||||
const someApisSelected = allApis.some((a) => selectedApiIds.has(a.apiId));
|
|
||||||
|
|
||||||
const handleToggleAll = () => {
|
const handleToggleAll = () => {
|
||||||
if (allApisSelected) {
|
if (allApisSelected) {
|
||||||
setSelectedApiIds(new Set());
|
setSelectedApiIds(new Set());
|
||||||
@ -202,28 +188,6 @@ const KeyRequestPage = () => {
|
|||||||
setSelectedApiIds(new Set());
|
setSelectedApiIds(new Set());
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredGroupedApisMap = useMemo(() => {
|
|
||||||
if (!searchQuery.trim()) return groupedApisMap;
|
|
||||||
const query = searchQuery.toLowerCase();
|
|
||||||
const result: Record<number, DomainGroup[]> = {};
|
|
||||||
Object.entries(groupedApisMap).forEach(([serviceId, domainGroups]) => {
|
|
||||||
const filtered = domainGroups
|
|
||||||
.map((dg) => ({
|
|
||||||
domain: dg.domain,
|
|
||||||
apis: dg.apis.filter(
|
|
||||||
(a) =>
|
|
||||||
a.apiName.toLowerCase().includes(query) ||
|
|
||||||
a.apiPath.toLowerCase().includes(query),
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
.filter((dg) => dg.apis.length > 0);
|
|
||||||
if (filtered.length > 0) {
|
|
||||||
result[Number(serviceId)] = filtered;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}, [groupedApisMap, searchQuery]);
|
|
||||||
|
|
||||||
const handlePresetPeriod = (months: number) => {
|
const handlePresetPeriod = (months: number) => {
|
||||||
const from = new Date();
|
const from = new Date();
|
||||||
const to = new Date();
|
const to = new Date();
|
||||||
@ -480,42 +444,39 @@ const KeyRequestPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Service cards */}
|
{/* Domain cards */}
|
||||||
<div className="p-4 space-y-3">
|
<div className="p-4 space-y-3">
|
||||||
{services.map((service) => {
|
{filteredDomainGroups.map((domainGroup) => {
|
||||||
const apis = serviceApisMap[service.serviceId] || [];
|
const isDomainExpanded = expandedDomains.has(domainGroup.domain);
|
||||||
const domainGroups = filteredGroupedApisMap[service.serviceId] || [];
|
const domainApis = domainGroup.apis;
|
||||||
const isServiceExpanded = expandedServices.has(service.serviceId);
|
const allDomainSelected = domainApis.length > 0 && domainApis.every((a) => selectedApiIds.has(a.apiId));
|
||||||
const selectedCount = apis.filter((a) => selectedApiIds.has(a.apiId)).length;
|
const someDomainSelected = !allDomainSelected && domainApis.some((a) => selectedApiIds.has(a.apiId));
|
||||||
const allServiceSelected = apis.length > 0 && apis.every((a) => selectedApiIds.has(a.apiId));
|
const selectedCount = domainApis.filter((a) => selectedApiIds.has(a.apiId)).length;
|
||||||
const someServiceSelected = !allServiceSelected && apis.some((a) => selectedApiIds.has(a.apiId));
|
|
||||||
const hasSelections = selectedCount > 0;
|
const hasSelections = selectedCount > 0;
|
||||||
|
|
||||||
if (searchQuery.trim() && domainGroups.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={service.serviceId}
|
key={domainGroup.domain}
|
||||||
className={`rounded-xl border overflow-hidden transition-colors ${hasSelections ? 'border-blue-300 dark:border-blue-700' : 'border-gray-200 dark:border-gray-700'}`}
|
className={`rounded-xl border overflow-hidden transition-colors ${hasSelections ? 'border-blue-300 dark:border-blue-700' : 'border-gray-200 dark:border-gray-700'}`}
|
||||||
>
|
>
|
||||||
{/* Service header */}
|
{/* Domain header */}
|
||||||
<div
|
<div
|
||||||
className={`flex items-center justify-between px-5 py-3.5 cursor-pointer ${hasSelections ? 'bg-blue-50/50 dark:bg-blue-900/20' : 'bg-gray-50 dark:bg-gray-800/80'}`}
|
className={`flex items-center justify-between px-5 py-3.5 cursor-pointer ${hasSelections ? 'bg-blue-50/50 dark:bg-blue-900/20' : 'bg-gray-50 dark:bg-gray-800/80'}`}
|
||||||
onClick={() => handleToggleService(service.serviceId)}
|
onClick={() => handleToggleDomain(domainGroup.domain)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<svg className={`h-4 w-4 text-gray-400 transition-transform ${isServiceExpanded ? 'rotate-90' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg className={`h-4 w-4 text-gray-400 transition-transform ${isDomainExpanded ? 'rotate-90' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
<label className="flex items-center cursor-pointer" onClick={(e) => e.stopPropagation()}>
|
<label className="flex items-center cursor-pointer" onClick={(e) => e.stopPropagation()}>
|
||||||
<IndeterminateCheckbox
|
<IndeterminateCheckbox
|
||||||
checked={allServiceSelected}
|
checked={allDomainSelected}
|
||||||
indeterminate={someServiceSelected}
|
indeterminate={someDomainSelected}
|
||||||
onChange={() => handleToggleAllServiceApis(service.serviceId)}
|
onChange={() => handleToggleAllDomainApis(domainGroup.domain)}
|
||||||
className="rounded"
|
className="rounded"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<span className="font-semibold text-gray-900 dark:text-gray-100">{service.serviceName}</span>
|
<span className="font-semibold text-gray-900 dark:text-gray-100">{/^[a-zA-Z\s\-_]+$/.test(domainGroup.domain) ? domainGroup.domain.toUpperCase() : domainGroup.domain}</span>
|
||||||
{selectedCount > 0 && (
|
{selectedCount > 0 && (
|
||||||
<span className="text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 px-2 py-0.5 rounded-full">
|
<span className="text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 px-2 py-0.5 rounded-full">
|
||||||
{selectedCount} selected
|
{selectedCount} selected
|
||||||
@ -523,98 +484,44 @@ const KeyRequestPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 px-2 py-0.5 rounded-full">
|
<span className="text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 px-2 py-0.5 rounded-full">
|
||||||
{apis.length}개 API
|
{domainApis.length}개 API
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Service body */}
|
{/* API list */}
|
||||||
{isServiceExpanded && (
|
{isDomainExpanded && (
|
||||||
<div className="px-4 py-3 space-y-2 bg-white dark:bg-gray-900">
|
<div className="divide-y divide-gray-100 dark:divide-gray-700/50 bg-white dark:bg-gray-900">
|
||||||
{domainGroups.map((domainGroup) => {
|
{domainApis.map((api) => {
|
||||||
const domainKey = `${service.serviceId}-${domainGroup.domain}`;
|
const isSelected = selectedApiIds.has(api.apiId);
|
||||||
const isDomainExpanded = expandedDomains.has(domainKey);
|
|
||||||
const domainApis = domainGroup.apis;
|
|
||||||
const allDomainSelected = domainApis.length > 0 && domainApis.every((a) => selectedApiIds.has(a.apiId));
|
|
||||||
const someDomainSelected = !allDomainSelected && domainApis.some((a) => selectedApiIds.has(a.apiId));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={domainKey}>
|
<div
|
||||||
{/* Domain row */}
|
key={api.apiId}
|
||||||
<div
|
className={`flex items-start gap-3 px-5 py-3 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/30 ${isSelected ? 'bg-blue-50/50 dark:bg-blue-900/10' : ''}`}
|
||||||
className="ml-5 flex items-center gap-2.5 px-3 py-2 cursor-pointer rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50"
|
onClick={() => handleToggleApi(api.apiId)}
|
||||||
onClick={() => handleToggleDomain(domainKey)}
|
>
|
||||||
>
|
<div className="flex items-center pt-0.5">
|
||||||
<svg className={`h-3.5 w-3.5 text-gray-400 transition-transform ${isDomainExpanded ? 'rotate-90' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<input
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
type="checkbox"
|
||||||
</svg>
|
checked={isSelected}
|
||||||
<label className="flex items-center cursor-pointer" onClick={(e) => e.stopPropagation()}>
|
onChange={() => handleToggleApi(api.apiId)}
|
||||||
<IndeterminateCheckbox
|
onClick={(e) => e.stopPropagation()}
|
||||||
checked={allDomainSelected}
|
className="rounded"
|
||||||
indeterminate={someDomainSelected}
|
/>
|
||||||
onChange={() => handleToggleAllDomainApis(service.serviceId, domainGroup.domain)}
|
</div>
|
||||||
className="rounded"
|
<div className="flex-1 min-w-0">
|
||||||
/>
|
<p className="text-sm font-semibold text-gray-900 dark:text-gray-100 truncate">{api.apiName}</p>
|
||||||
</label>
|
|
||||||
<span className="text-sm font-semibold text-gray-800 dark:text-gray-200">{domainGroup.domain}</span>
|
|
||||||
<span className="text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 px-2 py-0.5 rounded-full">
|
|
||||||
{domainApis.length}개
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* API table */}
|
|
||||||
{isDomainExpanded && (
|
|
||||||
<div className="ml-3 mr-3 mb-3 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
|
||||||
{/* Table header */}
|
|
||||||
<div className="grid grid-cols-[24px_56px_1fr_1fr] gap-3 items-center bg-gray-50 dark:bg-gray-800 px-4 py-1.5 text-xs font-semibold text-gray-500 dark:text-gray-500 uppercase tracking-wider">
|
|
||||||
<div></div>
|
|
||||||
<div>Method</div>
|
|
||||||
<div>Path</div>
|
|
||||||
<div>Name</div>
|
|
||||||
</div>
|
|
||||||
{/* Table rows */}
|
|
||||||
{domainApis.map((api) => {
|
|
||||||
const isSelected = selectedApiIds.has(api.apiId);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={api.apiId}
|
|
||||||
className={`grid grid-cols-[24px_56px_1fr_1fr] gap-3 items-center px-4 py-2.5 hover:bg-gray-50 dark:hover:bg-gray-700/30 border-b border-gray-100 dark:border-gray-700/50 last:border-b-0 cursor-pointer ${isSelected ? 'bg-blue-50/50 dark:bg-blue-900/10' : ''}`}
|
|
||||||
onClick={() => handleToggleApi(api.apiId)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={isSelected}
|
|
||||||
onChange={() => handleToggleApi(api.apiId)}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
className="rounded"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className={`inline-block px-2 py-0.5 rounded text-xs font-bold ${METHOD_BADGE_STYLE[api.apiMethod] || 'bg-gray-700 text-gray-300 border border-gray-600'}`}>
|
|
||||||
{api.apiMethod}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="font-mono text-xs text-gray-700 dark:text-gray-300 truncate">{api.apiPath}</div>
|
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400 truncate">{api.apiName}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{apis.length === 0 && (
|
|
||||||
<p className="text-sm text-gray-400 dark:text-gray-500 py-3 ml-5">등록된 API가 없습니다.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{services.length === 0 && (
|
{filteredDomainGroups.length === 0 && (
|
||||||
<div className="px-6 py-8 text-center text-gray-400 dark:text-gray-500">
|
<div className="px-6 py-8 text-center text-gray-400 dark:text-gray-500">
|
||||||
등록된 서비스가 없습니다.
|
{searchQuery.trim() ? '검색 결과가 없습니다.' : '등록된 API가 없습니다.'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user