import { useState, useEffect, useMemo, useRef } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { getCatalog } from '../../services/apiHubService'; import { createKeyRequest } from '../../services/apiKeyService'; import type { ServiceCatalog } from '../../types/apihub'; import Button from '../../components/ui/Button'; const IndeterminateCheckbox = ({ checked, indeterminate, onChange, className }: { checked: boolean; indeterminate: boolean; onChange: () => void; className?: string }) => { const ref = useRef(null); useEffect(() => { if (ref.current) ref.current.indeterminate = indeterminate; }, [indeterminate]); return ; }; interface FlatApi { apiId: number; apiName: string; description: string | null; } interface FlatDomainGroup { domain: string; apis: FlatApi[]; } const KeyRequestPage = () => { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const preApiId = searchParams.get('apiId'); const preApiIds = searchParams.get('apiIds'); const [catalog, setCatalog] = useState([]); const [expandedDomains, setExpandedDomains] = useState>(new Set()); const [selectedApiIds, setSelectedApiIds] = useState>(new Set()); const [keyName, setKeyName] = useState(''); const [purpose, setPurpose] = useState(''); const [serviceIp, setServiceIp] = useState(''); const [servicePurpose, setServicePurpose] = useState(''); const [dailyRequestEstimate, setDailyRequestEstimate] = useState(''); const [usagePeriodMode, setUsagePeriodMode] = useState<'preset' | 'custom'>('preset'); const [isPermanent, setIsPermanent] = useState(false); const [usageFromDate, setUsageFromDate] = useState(''); const [usageToDate, setUsageToDate] = useState(''); const [searchQuery, setSearchQuery] = useState(''); const [isLoading, setIsLoading] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); useEffect(() => { const fetchData = async () => { try { setIsLoading(true); const catalogRes = await getCatalog(); if (catalogRes.success && catalogRes.data) { setCatalog(catalogRes.data); // 쿼리 파라미터로 전달된 API 자동 선택 if (preApiId) { const aId = Number(preApiId); for (const service of catalogRes.data) { for (const domainGroup of service.domains) { const targetApi = domainGroup.apis.find((a) => a.apiId === aId); if (targetApi) { setSelectedApiIds(new Set([aId])); setExpandedDomains(new Set([domainGroup.domain])); break; } } } } // 신청함에서 일괄 전달된 apiIds 자동 선택 if (preApiIds) { const aIds = preApiIds .split(',') .map((s) => Number(s.trim())) .filter((n) => !isNaN(n) && n > 0); if (aIds.length > 0) { const aIdSet = new Set(aIds); const domainsToExpand = new Set(); for (const service of catalogRes.data) { for (const domainGroup of service.domains) { const hasMatch = domainGroup.apis.some((a) => aIdSet.has(a.apiId)); if (hasMatch) { domainsToExpand.add(domainGroup.domain); } } } setSelectedApiIds(aIdSet); setExpandedDomains(domainsToExpand); } } } else { setError(catalogRes.message || '카탈로그를 불러오는데 실패했습니다.'); } } catch { setError('카탈로그를 불러오는데 실패했습니다.'); } finally { setIsLoading(false); } }; fetchData(); }, [preApiId, preApiIds]); // catalog → FlatDomainGroup[] 변환 (도메인 기준 플랫 그룹핑, 알파벳순 정렬) const flatDomainGroups = useMemo(() => { const domainMap = new Map(); for (const service of catalog) { for (const domainGroup of service.domains) { const domainName = domainGroup.domain || '미분류'; if (!domainMap.has(domainName)) { domainMap.set(domainName, []); } const existing = domainMap.get(domainName)!; for (const api of domainGroup.apis) { existing.push({ apiId: api.apiId, apiName: api.apiName, description: api.description, }); } } } const result: FlatDomainGroup[] = []; domainMap.forEach((apis, domain) => { result.push({ domain, apis }); }); return result; }, [catalog]); const allApis = useMemo(() => { 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(() => { 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) => { const next = new Set(prev); if (next.has(domain)) { next.delete(domain); } else { next.add(domain); } return next; }); }; const handleToggleApi = (apiId: number) => { setSelectedApiIds((prev) => { const next = new Set(prev); if (next.has(apiId)) { next.delete(apiId); } else { next.add(apiId); } return next; }); }; const handleToggleAllDomainApis = (domain: string) => { const domainGroup = flatDomainGroups.find((dg) => dg.domain === domain); if (!domainGroup) return; const domainApis = domainGroup.apis; const allSelected = domainApis.every((a) => selectedApiIds.has(a.apiId)); setSelectedApiIds((prev) => { const next = new Set(prev); domainApis.forEach((a) => { if (allSelected) { next.delete(a.apiId); } else { next.add(a.apiId); } }); return next; }); }; const handleToggleAll = () => { if (allApisSelected) { setSelectedApiIds(new Set()); } else { setSelectedApiIds(new Set(allApis.map((a) => a.apiId))); } }; const handleClearSelection = () => { setSelectedApiIds(new Set()); }; const handlePresetPeriod = (months: number) => { const from = new Date(); const to = new Date(); to.setMonth(to.getMonth() + months); setUsageFromDate(from.toISOString().split('T')[0]); setUsageToDate(to.toISOString().split('T')[0]); setUsagePeriodMode('preset'); setIsPermanent(false); }; const handlePermanent = () => { setIsPermanent(true); setUsageFromDate(''); setUsageToDate(''); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (selectedApiIds.size === 0) { setError('최소 하나의 API를 선택해주세요.'); return; } if (!isPermanent && (!usageFromDate || !usageToDate)) { setError('사용 기간을 설정해주세요.'); return; } setError(null); setIsSubmitting(true); try { const res = await createKeyRequest({ keyName, purpose: purpose || undefined, requestedApiIds: Array.from(selectedApiIds), serviceIp: serviceIp || undefined, servicePurpose: servicePurpose || undefined, dailyRequestEstimate: dailyRequestEstimate ? Number(dailyRequestEstimate) : undefined, usageFromDate: isPermanent ? undefined : usageFromDate, usageToDate: isPermanent ? undefined : usageToDate, }); if (res.success) { setSuccess(true); } else { setError(res.message || 'API Key 신청에 실패했습니다.'); } } catch { setError('API Key 신청에 실패했습니다.'); } finally { setIsSubmitting(false); } }; if (isLoading) { return
로딩 중...
; } if (success) { return (

신청이 완료되었습니다

관리자 승인 후 API Key가 생성됩니다.

); } return (

API Key 신청

{error && (
{error}
)}
setKeyName(e.target.value)} required placeholder="API Key 이름을 입력하세요" className="w-full border border-[var(--color-border-strong)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] rounded-lg px-3 py-2 focus:ring-2 focus:ring-[var(--color-primary)] focus:outline-none" />