import { useState, useEffect, useMemo } from 'react'; import { createKeyRequest } from '../services/apiKeyService'; import { getCatalog } from '../services/apiHubService'; import type { ServiceCatalog } from '../types/apihub'; import Button from './ui/Button'; interface ApiKeyRequestModalProps { isOpen: boolean; onClose: () => void; initialApiIds: number[]; initialApiNames?: string[]; onSuccess?: () => void; } const ApiKeyRequestModal = ({ isOpen, onClose, initialApiIds, onSuccess }: ApiKeyRequestModalProps) => { const [keyName, setKeyName] = useState(''); const [purpose, setPurpose] = useState(''); const [serviceIp, setServiceIp] = useState(''); const [servicePurpose, setServicePurpose] = useState(''); const [dailyEstimate, setDailyEstimate] = useState(''); const [periodMode, setPeriodMode] = useState<'preset' | 'custom'>('preset'); const [isPermanent, setIsPermanent] = useState(false); const [fromDate, setFromDate] = useState(''); const [toDate, setToDate] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); const [selectedApiIds, setSelectedApiIds] = useState>(new Set()); const [catalog, setCatalog] = useState([]); const [expandedDomains, setExpandedDomains] = useState>(new Set()); const [apiSearch, setApiSearch] = useState(''); useEffect(() => { if (isOpen) { setKeyName(''); setPurpose(''); setServiceIp(''); setServicePurpose(''); setDailyEstimate(''); setPeriodMode('preset'); setIsPermanent(false); setFromDate(''); setToDate(''); setError(null); setSuccess(false); setSelectedApiIds(new Set(initialApiIds)); setApiSearch(''); // 초기 API의 도메인을 펼침 if (catalog.length > 0) { const domains = new Set(); for (const svc of catalog) { for (const dg of svc.domains) { if (dg.apis.some((a) => initialApiIds.includes(a.apiId))) { domains.add(formatDomainKey(dg.domain)); } } } setExpandedDomains(domains); } if (catalog.length === 0) { getCatalog().then((res) => { if (res.data) setCatalog(res.data); }); } } }, [isOpen]); const formatDomainKey = (d: string) => (/^[a-zA-Z\s\-_]+$/.test(d) ? d.toUpperCase() : d); // 카탈로그에서 도메인 기준 플랫 그룹 const domainGroups = useMemo(() => { const map = new Map(); for (const svc of catalog) { for (const dg of svc.domains) { const key = formatDomainKey(dg.domain); const existing = map.get(key) ?? []; for (const a of dg.apis) { if (!existing.some((e) => e.apiId === a.apiId)) { if (!apiSearch.trim() || a.apiName.toLowerCase().includes(apiSearch.toLowerCase())) { existing.push({ apiId: a.apiId, apiName: a.apiName }); } } } if (existing.length > 0) map.set(key, existing); } } return Array.from(map.entries()); }, [catalog, apiSearch]); const handlePresetPeriod = (months: number) => { const from = new Date(); const to = new Date(); to.setMonth(to.getMonth() + months); setFromDate(from.toISOString().split('T')[0]); setToDate(to.toISOString().split('T')[0]); setPeriodMode('preset'); setIsPermanent(false); }; const handlePermanent = () => { setIsPermanent(true); setFromDate(''); setToDate(''); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (selectedApiIds.size === 0) { setError('최소 하나의 API를 선택해주세요.'); return; } if (!isPermanent && (!fromDate || !toDate)) { 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: dailyEstimate ? Number(dailyEstimate) : undefined, usageFromDate: isPermanent ? undefined : fromDate, usageToDate: isPermanent ? undefined : toDate, }); if (res.success) { setSuccess(true); setTimeout(() => { onClose(); onSuccess?.(); }, 2000); } else { setError(res.message || 'API Key 신청에 실패했습니다.'); } } catch { setError('API Key 신청에 실패했습니다.'); } finally { setIsSubmitting(false); } }; if (!isOpen) return null; return (
{ if (e.target === e.currentTarget) onClose(); }} >
{/* 헤더 */}

API 사용 신청

{/* 성공 메시지 */} {success ? (

신청이 완료되었습니다

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

) : (
{/* 에러 메시지 */} {error && (
{error}
)} {/* API 선택 (도메인 기반 체크박스 트리) */}

API 선택 {selectedApiIds.size > 0 && ( {selectedApiIds.size}건 선택 )}

setApiSearch(e.target.value)} placeholder="API 검색..." className="bg-[var(--color-bg-surface)] border border-[var(--color-border)] rounded-lg px-2.5 py-1 text-xs text-[var(--color-text-primary)] placeholder-[var(--color-text-tertiary)] focus:ring-1 focus:ring-[var(--color-primary)] focus:outline-none w-40" />
{domainGroups.length === 0 ? (

검색 결과가 없습니다

) : domainGroups.map(([domain, apis]) => { const isExpanded = expandedDomains.has(domain); const allSelected = apis.every((a) => selectedApiIds.has(a.apiId)); const someSelected = !allSelected && apis.some((a) => selectedApiIds.has(a.apiId)); return (
setExpandedDomains((prev) => { const n = new Set(prev); n.has(domain) ? n.delete(domain) : n.add(domain); return n; })} > { if (el) el.indeterminate = someSelected; }} onChange={() => { setSelectedApiIds((prev) => { const n = new Set(prev); apis.forEach((a) => allSelected ? n.delete(a.apiId) : n.add(a.apiId)); return n; }); }} onClick={(e) => e.stopPropagation()} className="rounded" /> {domain} {apis.length}
{isExpanded && apis.map((a) => (
setSelectedApiIds((prev) => { const n = new Set(prev); n.has(a.apiId) ? n.delete(a.apiId) : n.add(a.apiId); return n; })} > setSelectedApiIds((prev) => { const n = new Set(prev); n.has(a.apiId) ? n.delete(a.apiId) : n.add(a.apiId); return n; })} onClick={(e) => e.stopPropagation()} className="rounded" /> {a.apiName}
))}
); })}
{/* Key Name */}
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" />
{/* 사용 목적 */}