import { useState, useEffect, useCallback } from 'react'; import { useParams, useNavigate, Link } from 'react-router-dom'; import type { ServiceInfo, ApiDetailInfo, UpdateServiceApiRequest, SaveApiSpecRequest, SaveApiParamRequest, } from '../../types/service'; import { getServices, getApiDetail, updateServiceApi, saveApiSpec, saveApiParams, deleteServiceApi, getDomains, } from '../../services/serviceService'; import type { ApiDomainInfo } from '../../types/apihub'; import Badge from '../../components/ui/Badge'; import Button from '../../components/ui/Button'; const METHOD_CLASS: Record = { GET: 'bg-green-50 text-green-700 dark:bg-green-500/15 dark:text-green-400', POST: 'bg-blue-50 text-blue-700 dark:bg-blue-500/15 dark:text-blue-400', PUT: 'bg-amber-50 text-amber-700 dark:bg-amber-500/15 dark:text-amber-400', DELETE: 'bg-red-50 text-red-700 dark:bg-red-500/15 dark:text-red-400', }; const INPUT_CLS = '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 text-sm'; const TABLE_INPUT_CLS = 'w-full border border-[var(--color-border-strong)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] rounded px-2 py-1.5 text-sm focus:ring-2 focus:ring-[var(--color-primary)] focus:outline-none'; const LABEL_CLS = 'block text-sm font-medium text-[var(--color-text-primary)] mb-1'; const ApiEditPage = () => { const { serviceId: serviceIdStr, apiId: apiIdStr } = useParams<{ serviceId: string; apiId: string; }>(); const navigate = useNavigate(); const serviceId = Number(serviceIdStr); const apiId = Number(apiIdStr); // Page state const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [notFound, setNotFound] = useState(false); const [saving, setSaving] = useState(false); const [saveMessage, setSaveMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); // Meta const [serviceName, setServiceName] = useState(''); // Domains const [domains, setDomains] = useState([]); // Basic info const [apiMethod, setApiMethod] = useState('GET'); const [apiPath, setApiPath] = useState(''); const [apiName, setApiName] = useState(''); const [apiDomain, setApiDomain] = useState(''); const [apiSection, setApiSection] = useState(''); const [apiDescription, setApiDescription] = useState(''); const [apiIsActive, setApiIsActive] = useState(true); // Spec const [sampleUrl, setSampleUrl] = useState(''); const [authRequired, setAuthRequired] = useState(false); const [authType, setAuthType] = useState(''); const [deprecated, setDeprecated] = useState(false); const [dataFormat, setDataFormat] = useState(''); const [referenceUrl, setReferenceUrl] = useState(''); const [specNote, setSpecNote] = useState(''); // Params const [requestParams, setRequestParams] = useState([]); const [responseParams, setResponseParams] = useState([]); const populateForm = useCallback((data: ApiDetailInfo, services: ServiceInfo[]) => { const { api, spec, requestParams: rp, responseParams: resp } = data; const svc = services.find((s) => s.serviceId === api.serviceId); if (svc) setServiceName(svc.serviceName); setApiMethod(api.apiMethod); setApiPath(api.apiPath); setApiName(api.apiName); setApiDomain(api.apiDomain || ''); setApiSection(api.apiSection || ''); setApiDescription(api.description || ''); setApiIsActive(api.isActive); if (spec) { setSampleUrl(spec.sampleUrl || ''); setAuthRequired(spec.authRequired); setAuthType(spec.authType || ''); setDeprecated(spec.deprecated); setDataFormat(spec.dataFormat || ''); setReferenceUrl(spec.referenceUrl || ''); setSpecNote(spec.note || ''); } setRequestParams( rp.map((p, i) => ({ paramType: 'REQUEST' as const, paramName: p.paramName, paramMeaning: p.paramMeaning || undefined, paramDescription: p.paramDescription || undefined, required: p.required, defaultValue: p.defaultValue || undefined, inputType: p.inputType || 'TEXT', sortOrder: i, })), ); setResponseParams( resp.map((p, i) => ({ paramType: 'RESPONSE' as const, paramName: p.paramName, paramMeaning: p.paramMeaning || undefined, sortOrder: i, })), ); }, []); useEffect(() => { const fetchData = async () => { try { setLoading(true); setError(null); const [svcRes, detailRes, domainRes] = await Promise.all([ getServices(), getApiDetail(serviceId, apiId), getDomains(), ]); if (domainRes.success && domainRes.data) { setDomains(domainRes.data); } if (!detailRes.success || !detailRes.data) { if (detailRes.message?.includes('404') || detailRes.message?.includes('찾을 수 없')) { setNotFound(true); } else { setError(detailRes.message || 'API 정보를 불러오는데 실패했습니다.'); } return; } const services = svcRes.success && svcRes.data ? svcRes.data : []; populateForm(detailRes.data, services); } catch { setError('API 정보를 불러오는데 실패했습니다.'); } finally { setLoading(false); } }; fetchData(); }, [serviceId, apiId, populateForm]); // Param helpers const addParam = () => { const newParam: SaveApiParamRequest = { paramType: 'REQUEST', paramName: '', inputType: 'TEXT', sortOrder: requestParams.length, }; setRequestParams((prev) => [...prev, newParam]); }; const updateParam = (index: number, field: string, value: string | boolean | undefined) => { setRequestParams((prev) => prev.map((p, i) => (i === index ? { ...p, [field]: value } : p))); }; const removeParam = (index: number) => { setRequestParams((prev) => prev.filter((_, i) => i !== index)); }; const addResponseParam = () => { const newParam: SaveApiParamRequest = { paramType: 'RESPONSE', paramName: '', sortOrder: responseParams.length, }; setResponseParams((prev) => [...prev, newParam]); }; const updateResponseParam = (index: number, field: string, value: string | undefined) => { setResponseParams((prev) => prev.map((p, i) => (i === index ? { ...p, [field]: value } : p))); }; const removeResponseParam = (index: number) => { setResponseParams((prev) => prev.filter((_, i) => i !== index)); }; const [jsonInput, setJsonInput] = useState(''); const [jsonError, setJsonError] = useState(null); const [showJsonInput, setShowJsonInput] = useState(false); const parseJsonToParams = () => { setJsonError(null); try { const parsed = JSON.parse(jsonInput); const params: SaveApiParamRequest[] = []; const extract = (obj: unknown, prefix: string) => { if (obj === null || obj === undefined) return; if (Array.isArray(obj)) { params.push({ paramType: 'RESPONSE', paramName: prefix + '[]', sortOrder: params.length }); if (obj.length > 0 && typeof obj[0] === 'object' && obj[0] !== null) { extract(obj[0], prefix + '[].'); } } else if (typeof obj === 'object') { for (const key of Object.keys(obj)) { const fullKey = prefix ? prefix + key : key; const val = (obj as Record)[key]; if (val !== null && typeof val === 'object') { extract(val, fullKey + (Array.isArray(val) ? '' : '.')); } else { params.push({ paramType: 'RESPONSE', paramName: fullKey, sortOrder: params.length }); } } } }; extract(parsed, ''); if (params.length === 0) { setJsonError('파싱할 키가 없습니다.'); return; } setResponseParams(params); setJsonInput(''); setShowJsonInput(false); } catch { setJsonError('올바른 JSON 형식이 아닙니다.'); } }; const handleSave = async () => { setSaving(true); setSaveMessage(null); try { const basicReq: UpdateServiceApiRequest = { apiMethod, apiPath, apiName, apiDomain: apiDomain || undefined, apiSection: apiSection || undefined, description: apiDescription || undefined, isActive: apiIsActive, }; const specReq: SaveApiSpecRequest = { sampleUrl: sampleUrl || undefined, authRequired, authType: authType || undefined, deprecated, dataFormat: dataFormat || undefined, referenceUrl: referenceUrl || undefined, note: specNote || undefined, }; const allParams: SaveApiParamRequest[] = [ ...requestParams.map((p, i) => ({ ...p, sortOrder: i })), ...responseParams.map((p, i) => ({ ...p, sortOrder: i })), ]; const [basicRes, specRes, paramsRes] = await Promise.all([ updateServiceApi(serviceId, apiId, basicReq), saveApiSpec(serviceId, apiId, specReq), saveApiParams(serviceId, apiId, allParams), ]); const basicOk = basicRes.success; const specOk = specRes.success; const paramsOk = paramsRes.success; if (basicOk && specOk && paramsOk) { setSaveMessage({ type: 'success', text: '저장되었습니다.' }); } else { const errMsg = (!basicOk ? basicRes.message : null) || (!specOk ? specRes.message : null) || (!paramsOk ? paramsRes.message : null) || '일부 항목 저장에 실패했습니다.'; setSaveMessage({ type: 'error', text: errMsg }); } } catch { setSaveMessage({ type: 'error', text: '저장 중 오류가 발생했습니다.' }); } finally { setSaving(false); setTimeout(() => setSaveMessage(null), 3000); } }; const handleDelete = async () => { if (!window.confirm('이 API를 삭제하시겠습니까?')) return; try { const res = await deleteServiceApi(serviceId, apiId); if (res.success) { navigate('/admin/apis'); } else { setSaveMessage({ type: 'error', text: res.message || 'API 삭제에 실패했습니다.' }); } } catch { setSaveMessage({ type: 'error', text: 'API 삭제 중 오류가 발생했습니다.' }); } }; // Loading if (loading) { return (
); } // Not found if (notFound) { return (

API를 찾을 수 없습니다.

API 목록으로 돌아가기
); } // Error if (error) { return (
{error}
API 목록으로 돌아가기
); } return (
{/* Breadcrumb */} {/* Header */}

{apiName}

{apiMethod} {apiPath}
{/* Save message */} {saveMessage && (
{saveMessage.text}
)} {/* Sections */}
{/* Section 1: 기본 정보 */}

기본 정보

setApiPath(e.target.value)} placeholder="/api/v1/example" className={INPUT_CLS} />
setApiName(e.target.value)} placeholder="API 이름" className={INPUT_CLS} />
setApiSection(e.target.value)} placeholder="섹션 (선택)" className={INPUT_CLS} />
setApiIsActive(e.target.checked)} className="w-4 h-4 text-[var(--color-primary)] rounded border-[var(--color-border-strong)] focus:ring-[var(--color-primary)]" />