import { useState, useEffect, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import type { ServiceInfo, ServiceApi, CreateServiceApiRequest } from '../../types/service'; import { getServices, getServiceApis, createServiceApi } from '../../services/serviceService'; const METHOD_COLOR: Record = { 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', DELETE: 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300', }; interface FlatApi extends ServiceApi { serviceName: string; } const ApisPage = () => { const navigate = useNavigate(); const [services, setServices] = useState([]); const [allApis, setAllApis] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Filter state const [filterServiceId, setFilterServiceId] = useState('all'); const [searchText, setSearchText] = useState(''); const [filterActive, setFilterActive] = useState<'all' | 'active' | 'inactive'>('all'); // Create API modal state const [isModalOpen, setIsModalOpen] = useState(false); const [modalError, setModalError] = useState(null); const [submitting, setSubmitting] = useState(false); const [modalServiceId, setModalServiceId] = useState(''); const [modalMethod, setModalMethod] = useState('GET'); const [modalPath, setModalPath] = useState(''); const [modalName, setModalName] = useState(''); const [modalDomain, setModalDomain] = useState(''); const [modalSection, setModalSection] = useState(''); const [modalDescription, setModalDescription] = useState(''); const fetchAll = async () => { try { setLoading(true); setError(null); const svcRes = await getServices(); if (!svcRes.success || !svcRes.data) { setError(svcRes.message || '서비스 목록을 불러오는데 실패했습니다.'); return; } const loadedServices = svcRes.data; setServices(loadedServices); if (loadedServices.length === 0) { setAllApis([]); return; } const results = await Promise.allSettled( loadedServices.map((svc) => getServiceApis(svc.serviceId)), ); const flat: FlatApi[] = []; results.forEach((result, idx) => { if (result.status === 'fulfilled' && result.value.success && result.value.data) { result.value.data.forEach((api) => { flat.push({ ...api, serviceName: loadedServices[idx].serviceName }); }); } }); setAllApis(flat); } catch { setError('API 목록을 불러오는데 실패했습니다.'); } finally { setLoading(false); } }; useEffect(() => { fetchAll(); }, []); const filteredApis = useMemo(() => { return allApis.filter((api) => { if (filterServiceId !== 'all' && api.serviceId !== filterServiceId) return false; if (filterActive === 'active' && !api.isActive) return false; if (filterActive === 'inactive' && api.isActive) return false; if (searchText.trim()) { const q = searchText.trim().toLowerCase(); if (!api.apiName.toLowerCase().includes(q) && !api.apiPath.toLowerCase().includes(q)) { return false; } } return true; }); }, [allApis, filterServiceId, filterActive, searchText]); const handleOpenModal = () => { setModalServiceId(services.length > 0 ? services[0].serviceId : ''); setModalMethod('GET'); setModalPath(''); setModalName(''); setModalDomain(''); setModalSection(''); setModalDescription(''); setModalError(null); setIsModalOpen(true); }; const handleCloseModal = () => { setIsModalOpen(false); setModalError(null); }; const handleModalSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (modalServiceId === '') return; setModalError(null); setSubmitting(true); try { const req: CreateServiceApiRequest = { apiMethod: modalMethod, apiPath: modalPath, apiName: modalName, apiDomain: modalDomain || undefined, apiSection: modalSection || undefined, description: modalDescription || undefined, }; const res = await createServiceApi(modalServiceId as number, req); if (!res.success) { setModalError(res.message || 'API 생성에 실패했습니다.'); return; } handleCloseModal(); await fetchAll(); if (res.data) { navigate(`/admin/apis/${res.data.serviceId}/${res.data.apiId}`); } } catch { setModalError('API 생성에 실패했습니다.'); } finally { setSubmitting(false); } }; if (loading) { return (
로딩 중...
); } return (
{/* Header */}

API 관리

{/* Global error */} {error && (
{error}
)} {/* Filters */}
setSearchText(e.target.value)} placeholder="API명, Path 검색" className="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 min-w-[200px]" />
{(['all', 'active', 'inactive'] as const).map((v) => ( ))}
{/* Table */}
{filteredApis.map((api) => ( navigate(`/admin/apis/${api.serviceId}/${api.apiId}`)} className="cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700" > ))} {filteredApis.length === 0 && ( )}
서비스 Method Path API명 Domain Section Active 명세
{api.serviceName} {api.apiMethod} {api.apiPath} {api.apiName} {api.apiDomain || '-'} {api.apiSection || '-'} {api.isActive ? 'Active' : 'Inactive'} -
등록된 API가 없습니다
{/* Create API Modal */} {isModalOpen && (

API 생성

{modalError && (
{modalError}
)}
setModalPath(e.target.value)} required placeholder="/api/v1/example" className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500" />
setModalName(e.target.value)} required placeholder="API 이름" className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
setModalDomain(e.target.value)} placeholder="도메인 (선택)" className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
setModalSection(e.target.value)} placeholder="섹션 (선택)" className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />