generated from gc/template-java-maven
feat(api): API 관리 상세 화면 구현
- API 명세(Spec) 및 파라미터(Param) CRUD 엔드포인트 추가 - API 관리 상세 편집 페이지(ApiEditPage) 구현 - API 목록 관리 페이지(ApisPage) 구현 - 요청인자/출력결과 편집 + JSON 파싱 기능 - 프론트엔드 타입/서비스 정의 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
부모
bf8a2e5fc4
커밋
a9cdf96481
822
frontend/src/pages/admin/ApiEditPage.tsx
Normal file
822
frontend/src/pages/admin/ApiEditPage.tsx
Normal file
@ -0,0 +1,822 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
const METHOD_COLOR: Record<string, string> = {
|
||||||
|
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',
|
||||||
|
};
|
||||||
|
|
||||||
|
const INPUT_CLS =
|
||||||
|
'w-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none text-sm';
|
||||||
|
|
||||||
|
const TABLE_INPUT_CLS =
|
||||||
|
'w-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded px-2 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none';
|
||||||
|
|
||||||
|
const LABEL_CLS = 'block text-sm font-medium text-gray-700 dark:text-gray-300 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<string | null>(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<ApiDomainInfo[]>([]);
|
||||||
|
|
||||||
|
// 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<SaveApiParamRequest[]>([]);
|
||||||
|
const [responseParams, setResponseParams] = useState<SaveApiParamRequest[]>([]);
|
||||||
|
|
||||||
|
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<string | null>(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<string, unknown>)[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 (
|
||||||
|
<div className="flex items-center justify-center py-20">
|
||||||
|
<div className="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not found
|
||||||
|
if (notFound) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-5xl mx-auto text-center py-20">
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 text-lg">API를 찾을 수 없습니다.</p>
|
||||||
|
<Link
|
||||||
|
to="/admin/apis"
|
||||||
|
className="mt-4 inline-block text-blue-600 dark:text-blue-400 hover:underline text-sm"
|
||||||
|
>
|
||||||
|
API 목록으로 돌아가기
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-5xl mx-auto py-10">
|
||||||
|
<div className="p-4 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-400 rounded-lg text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
to="/admin/apis"
|
||||||
|
className="mt-4 inline-block text-blue-600 dark:text-blue-400 hover:underline text-sm"
|
||||||
|
>
|
||||||
|
API 목록으로 돌아가기
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-5xl mx-auto">
|
||||||
|
{/* Breadcrumb */}
|
||||||
|
<nav className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 mb-4">
|
||||||
|
<Link
|
||||||
|
to="/admin/apis"
|
||||||
|
className="hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
|
||||||
|
>
|
||||||
|
API 관리
|
||||||
|
</Link>
|
||||||
|
<span>/</span>
|
||||||
|
<span className="text-gray-700 dark:text-gray-300">{serviceName || `서비스 #${serviceId}`}</span>
|
||||||
|
<span>/</span>
|
||||||
|
<span className="text-gray-900 dark:text-gray-100 font-medium">{apiName || `API #${apiId}`}</span>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">{apiName}</h1>
|
||||||
|
<div className="flex items-center gap-2 mt-1.5">
|
||||||
|
<span
|
||||||
|
className={`inline-block px-2 py-0.5 rounded text-xs font-bold ${
|
||||||
|
METHOD_COLOR[apiMethod] ?? 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{apiMethod}
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-sm text-gray-600 dark:text-gray-400">{apiPath}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={handleDelete}
|
||||||
|
className="px-4 py-2 rounded-lg bg-red-600 hover:bg-red-700 text-white text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
삭제
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={saving}
|
||||||
|
className="px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
{saving ? '저장 중...' : '저장'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Save message */}
|
||||||
|
{saveMessage && (
|
||||||
|
<div
|
||||||
|
className={`mb-4 p-3 rounded-lg text-sm ${
|
||||||
|
saveMessage.type === 'success'
|
||||||
|
? 'bg-green-50 dark:bg-green-900/30 text-green-700 dark:text-green-400'
|
||||||
|
: 'bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{saveMessage.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sections */}
|
||||||
|
<div className="space-y-6 mt-6">
|
||||||
|
{/* Section 1: 기본 정보 */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h2 className="text-base font-semibold text-gray-900 dark:text-gray-100 mb-4">기본 정보</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<label className={LABEL_CLS}>Method</label>
|
||||||
|
<select
|
||||||
|
value={apiMethod}
|
||||||
|
onChange={(e) => setApiMethod(e.target.value)}
|
||||||
|
className={INPUT_CLS}
|
||||||
|
>
|
||||||
|
{['GET', 'POST', 'PUT', 'DELETE'].map((m) => (
|
||||||
|
<option key={m} value={m}>{m}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={LABEL_CLS}>API Path</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={apiPath}
|
||||||
|
onChange={(e) => setApiPath(e.target.value)}
|
||||||
|
placeholder="/api/v1/example"
|
||||||
|
className={`${INPUT_CLS} font-mono`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={LABEL_CLS}>API명</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={apiName}
|
||||||
|
onChange={(e) => setApiName(e.target.value)}
|
||||||
|
placeholder="API 이름"
|
||||||
|
className={INPUT_CLS}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={LABEL_CLS}>Domain</label>
|
||||||
|
<select
|
||||||
|
value={apiDomain}
|
||||||
|
onChange={(e) => setApiDomain(e.target.value)}
|
||||||
|
className={INPUT_CLS}
|
||||||
|
>
|
||||||
|
<option value="">선택 안함</option>
|
||||||
|
{domains.map((d) => (
|
||||||
|
<option key={d.domainId} value={d.domainName}>{d.domainName}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={LABEL_CLS}>Section</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={apiSection}
|
||||||
|
onChange={(e) => setApiSection(e.target.value)}
|
||||||
|
placeholder="섹션 (선택)"
|
||||||
|
className={INPUT_CLS}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 pt-6">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="apiIsActive"
|
||||||
|
checked={apiIsActive}
|
||||||
|
onChange={(e) => setApiIsActive(e.target.checked)}
|
||||||
|
className="w-4 h-4 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<label htmlFor="apiIsActive" className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Active
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={LABEL_CLS}>설명</label>
|
||||||
|
<textarea
|
||||||
|
value={apiDescription}
|
||||||
|
onChange={(e) => setApiDescription(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
placeholder="API 설명 (선택)"
|
||||||
|
className={`${INPUT_CLS} resize-none`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section 2: API 명세 */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h2 className="text-base font-semibold text-gray-900 dark:text-gray-100 mb-4">API 명세</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className={LABEL_CLS}>샘플 URL</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={sampleUrl}
|
||||||
|
onChange={(e) => setSampleUrl(e.target.value)}
|
||||||
|
placeholder="https://example.com/api/v1/..."
|
||||||
|
className={INPUT_CLS}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="authRequired"
|
||||||
|
checked={authRequired}
|
||||||
|
onChange={(e) => setAuthRequired(e.target.checked)}
|
||||||
|
className="w-4 h-4 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<label htmlFor="authRequired" className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
인증 필요
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={LABEL_CLS}>인증 방식</label>
|
||||||
|
<select
|
||||||
|
value={authType}
|
||||||
|
onChange={(e) => setAuthType(e.target.value)}
|
||||||
|
className={INPUT_CLS}
|
||||||
|
>
|
||||||
|
<option value="">없음</option>
|
||||||
|
<option value="API_KEY">API_KEY</option>
|
||||||
|
<option value="JWT">JWT</option>
|
||||||
|
<option value="NONE">NONE</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="deprecated"
|
||||||
|
checked={deprecated}
|
||||||
|
onChange={(e) => setDeprecated(e.target.checked)}
|
||||||
|
className="w-4 h-4 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<label htmlFor="deprecated" className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Deprecated
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={LABEL_CLS}>데이터 포맷</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={dataFormat}
|
||||||
|
onChange={(e) => setDataFormat(e.target.value)}
|
||||||
|
placeholder="JSON, XML 등"
|
||||||
|
className={INPUT_CLS}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={LABEL_CLS}>참고자료 URL</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={referenceUrl}
|
||||||
|
onChange={(e) => setReferenceUrl(e.target.value)}
|
||||||
|
placeholder="https://..."
|
||||||
|
className={INPUT_CLS}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={LABEL_CLS}>비고</label>
|
||||||
|
<textarea
|
||||||
|
value={specNote}
|
||||||
|
onChange={(e) => setSpecNote(e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
placeholder="추가 참고사항 (선택)"
|
||||||
|
className={`${INPUT_CLS} resize-none`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section 3: 요청인자 */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-base font-semibold text-gray-900 dark:text-gray-100">요청인자</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => addParam()}
|
||||||
|
className="px-3 py-1.5 rounded-lg bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
행 추가
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<th className="px-2 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 w-10">#</th>
|
||||||
|
<th className="px-2 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400">인자명</th>
|
||||||
|
<th className="px-2 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400">의미</th>
|
||||||
|
<th className="px-2 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400">설명</th>
|
||||||
|
<th className="px-2 py-2 text-center text-xs font-medium text-gray-500 dark:text-gray-400 w-14">필수</th>
|
||||||
|
<th className="px-2 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 w-28">입력유형</th>
|
||||||
|
<th className="px-2 py-2 w-10" />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-100 dark:divide-gray-700">
|
||||||
|
{requestParams.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={7}
|
||||||
|
className="px-2 py-6 text-center text-gray-400 dark:text-gray-500 text-sm"
|
||||||
|
>
|
||||||
|
등록된 요청인자가 없습니다
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
requestParams.map((param, idx) => (
|
||||||
|
<tr key={idx}>
|
||||||
|
<td className="px-2 py-1.5 text-gray-500 dark:text-gray-400 text-center text-xs">
|
||||||
|
{idx + 1}
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={param.paramName}
|
||||||
|
onChange={(e) => updateParam(idx, 'paramName', e.target.value)}
|
||||||
|
placeholder="paramName"
|
||||||
|
className={TABLE_INPUT_CLS}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={param.paramMeaning ?? ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateParam(idx, 'paramMeaning', e.target.value || undefined)
|
||||||
|
}
|
||||||
|
placeholder="의미"
|
||||||
|
className={TABLE_INPUT_CLS}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={param.paramDescription ?? ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateParam(idx, 'paramDescription', e.target.value || undefined)
|
||||||
|
}
|
||||||
|
placeholder="설명"
|
||||||
|
className={TABLE_INPUT_CLS}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 text-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={param.required ?? false}
|
||||||
|
onChange={(e) => updateParam(idx, 'required', e.target.checked)}
|
||||||
|
className="w-4 h-4 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5">
|
||||||
|
<select
|
||||||
|
value={param.inputType ?? 'TEXT'}
|
||||||
|
onChange={(e) => updateParam(idx, 'inputType', e.target.value)}
|
||||||
|
className={TABLE_INPUT_CLS}
|
||||||
|
>
|
||||||
|
<option value="TEXT">TEXT</option>
|
||||||
|
<option value="NUMBER">NUMBER</option>
|
||||||
|
<option value="DATE">DATE</option>
|
||||||
|
<option value="DATETIME">DATETIME</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 text-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeParam(idx)}
|
||||||
|
className="text-red-500 hover:text-red-700 dark:hover:text-red-400 font-bold text-base leading-none"
|
||||||
|
title="삭제"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section 4: 출력결과 */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-base font-semibold text-gray-900 dark:text-gray-100">출력결과</h2>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowJsonInput((v) => !v)}
|
||||||
|
className="px-3 py-1.5 rounded-lg bg-indigo-100 hover:bg-indigo-200 dark:bg-indigo-900 dark:hover:bg-indigo-800 text-indigo-700 dark:text-indigo-300 text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
{showJsonInput ? 'JSON 닫기' : 'JSON 파싱'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={addResponseParam}
|
||||||
|
className="px-3 py-1.5 rounded-lg bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
행 추가
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showJsonInput && (
|
||||||
|
<div className="mb-4 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-600">
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
||||||
|
JSON 응답 예시를 붙여넣으면 키를 자동으로 추출합니다. (기존 목록을 대체합니다)
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
value={jsonInput}
|
||||||
|
onChange={(e) => { setJsonInput(e.target.value); setJsonError(null); }}
|
||||||
|
rows={6}
|
||||||
|
placeholder={'{\n "result": "success",\n "data": {\n "id": 1,\n "name": "example"\n }\n}'}
|
||||||
|
className={`${INPUT_CLS} font-mono resize-none`}
|
||||||
|
/>
|
||||||
|
{jsonError && (
|
||||||
|
<p className="mt-1.5 text-xs text-red-500">{jsonError}</p>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={parseJsonToParams}
|
||||||
|
disabled={!jsonInput.trim()}
|
||||||
|
className="mt-2 px-4 py-1.5 rounded-lg bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 text-white text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
파싱
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<th className="px-2 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 w-10">#</th>
|
||||||
|
<th className="px-2 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400">변수명</th>
|
||||||
|
<th className="px-2 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400">의미(단위)</th>
|
||||||
|
<th className="px-2 py-2 w-10" />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-100 dark:divide-gray-700">
|
||||||
|
{responseParams.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={4}
|
||||||
|
className="px-2 py-6 text-center text-gray-400 dark:text-gray-500 text-sm"
|
||||||
|
>
|
||||||
|
등록된 출력결과가 없습니다
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
responseParams.map((param, idx) => (
|
||||||
|
<tr key={idx}>
|
||||||
|
<td className="px-2 py-1.5 text-gray-500 dark:text-gray-400 text-center text-xs">
|
||||||
|
{idx + 1}
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={param.paramName}
|
||||||
|
onChange={(e) => updateResponseParam(idx, 'paramName', e.target.value)}
|
||||||
|
placeholder="variableName"
|
||||||
|
className={TABLE_INPUT_CLS}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={param.paramMeaning ?? ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateResponseParam(idx, 'paramMeaning', e.target.value || undefined)
|
||||||
|
}
|
||||||
|
placeholder="의미(단위)"
|
||||||
|
className={TABLE_INPUT_CLS}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 text-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeResponseParam(idx)}
|
||||||
|
className="text-red-500 hover:text-red-700 dark:hover:text-red-400 font-bold text-base leading-none"
|
||||||
|
title="삭제"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApiEditPage;
|
||||||
430
frontend/src/pages/admin/ApisPage.tsx
Normal file
430
frontend/src/pages/admin/ApisPage.tsx
Normal file
@ -0,0 +1,430 @@
|
|||||||
|
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<string, string> = {
|
||||||
|
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<ServiceInfo[]>([]);
|
||||||
|
const [allApis, setAllApis] = useState<FlatApi[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Filter state
|
||||||
|
const [filterServiceId, setFilterServiceId] = useState<number | 'all'>('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<string | null>(null);
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [modalServiceId, setModalServiceId] = useState<number | ''>('');
|
||||||
|
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 (
|
||||||
|
<div className="max-w-7xl mx-auto text-center py-10 text-gray-500 dark:text-gray-400">
|
||||||
|
로딩 중...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">API 관리</h1>
|
||||||
|
<button
|
||||||
|
onClick={handleOpenModal}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium"
|
||||||
|
>
|
||||||
|
API 생성
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Global error */}
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 p-3 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-400 rounded-lg text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<div className="flex flex-wrap gap-3 mb-4">
|
||||||
|
<select
|
||||||
|
value={filterServiceId}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFilterServiceId(e.target.value === 'all' ? 'all' : Number(e.target.value))
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<option value="all">전체 서비스</option>
|
||||||
|
{services.map((svc) => (
|
||||||
|
<option key={svc.serviceId} value={svc.serviceId}>
|
||||||
|
{svc.serviceName}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => 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]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex rounded-lg border border-gray-300 dark:border-gray-600 overflow-hidden text-sm">
|
||||||
|
{(['all', 'active', 'inactive'] as const).map((v) => (
|
||||||
|
<button
|
||||||
|
key={v}
|
||||||
|
onClick={() => setFilterActive(v)}
|
||||||
|
className={`px-3 py-2 font-medium ${
|
||||||
|
filterActive === v
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{v === 'all' ? '전체' : v === 'active' ? 'Active' : 'Inactive'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<div className="overflow-x-auto bg-white dark:bg-gray-800 rounded-lg shadow">
|
||||||
|
<table className="w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
||||||
|
<thead className="bg-gray-50 dark:bg-gray-700">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">서비스</th>
|
||||||
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Method</th>
|
||||||
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Path</th>
|
||||||
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">API명</th>
|
||||||
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Domain</th>
|
||||||
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Section</th>
|
||||||
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Active</th>
|
||||||
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">명세</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{filteredApis.map((api) => (
|
||||||
|
<tr
|
||||||
|
key={`${api.serviceId}-${api.apiId}`}
|
||||||
|
onClick={() => navigate(`/admin/apis/${api.serviceId}/${api.apiId}`)}
|
||||||
|
className="cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
<td className="px-4 py-3 text-gray-700 dark:text-gray-300">{api.serviceName}</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<span
|
||||||
|
className={`inline-block px-2 py-0.5 rounded text-xs font-bold ${
|
||||||
|
METHOD_COLOR[api.apiMethod] ?? 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{api.apiMethod}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 font-mono text-gray-800 dark:text-gray-200 truncate max-w-[240px]">
|
||||||
|
{api.apiPath}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-gray-900 dark:text-gray-100">{api.apiName}</td>
|
||||||
|
<td className="px-4 py-3 text-gray-500 dark:text-gray-400">{api.apiDomain || '-'}</td>
|
||||||
|
<td className="px-4 py-3 text-gray-500 dark:text-gray-400">{api.apiSection || '-'}</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<span
|
||||||
|
className={`inline-block px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||||
|
api.isActive
|
||||||
|
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'
|
||||||
|
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{api.isActive ? 'Active' : 'Inactive'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<span className="text-gray-400 dark:text-gray-500">-</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{filteredApis.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={8} className="px-4 py-8 text-center text-gray-400 dark:text-gray-500">
|
||||||
|
등록된 API가 없습니다
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Create API Modal */}
|
||||||
|
{isModalOpen && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl w-full max-w-lg mx-4">
|
||||||
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">API 생성</h2>
|
||||||
|
<button
|
||||||
|
onClick={handleCloseModal}
|
||||||
|
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 text-xl font-bold leading-none"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleModalSubmit} className="px-6 py-4 space-y-4">
|
||||||
|
{modalError && (
|
||||||
|
<div className="p-3 bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-400 rounded-lg text-sm">
|
||||||
|
{modalError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
서비스 <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={modalServiceId}
|
||||||
|
onChange={(e) => setModalServiceId(Number(e.target.value))}
|
||||||
|
required
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{services.map((svc) => (
|
||||||
|
<option key={svc.serviceId} value={svc.serviceId}>
|
||||||
|
{svc.serviceName}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Method <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={modalMethod}
|
||||||
|
onChange={(e) => setModalMethod(e.target.value)}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{['GET', 'POST', 'PUT', 'DELETE'].map((m) => (
|
||||||
|
<option key={m} value={m}>{m}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
API Path <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={modalPath}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
API명 <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={modalName}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Domain
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={modalDomain}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Section
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={modalSection}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
설명
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={modalDescription}
|
||||||
|
onChange={(e) => setModalDescription(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
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 resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCloseModal}
|
||||||
|
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={submitting}
|
||||||
|
className="px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white text-sm font-medium"
|
||||||
|
>
|
||||||
|
{submitting ? '생성 중...' : '생성'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApisPage;
|
||||||
@ -1,17 +1,17 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import type {
|
import type {
|
||||||
ServiceInfo,
|
ServiceInfo,
|
||||||
ServiceApi,
|
ServiceApi,
|
||||||
CreateServiceRequest,
|
CreateServiceRequest,
|
||||||
UpdateServiceRequest,
|
UpdateServiceRequest,
|
||||||
CreateServiceApiRequest,
|
|
||||||
} from '../../types/service';
|
} from '../../types/service';
|
||||||
import {
|
import {
|
||||||
getServices,
|
getServices,
|
||||||
createService,
|
createService,
|
||||||
updateService,
|
updateService,
|
||||||
|
deleteService,
|
||||||
getServiceApis,
|
getServiceApis,
|
||||||
createServiceApi,
|
|
||||||
} from '../../services/serviceService';
|
} from '../../services/serviceService';
|
||||||
|
|
||||||
const HEALTH_BADGE: Record<string, { dot: string; bg: string; text: string }> = {
|
const HEALTH_BADGE: Record<string, { dot: string; bg: string; text: string }> = {
|
||||||
@ -41,6 +41,7 @@ const formatRelativeTime = (dateStr: string | null): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ServicesPage = () => {
|
const ServicesPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [services, setServices] = useState<ServiceInfo[]>([]);
|
const [services, setServices] = useState<ServiceInfo[]>([]);
|
||||||
const [selectedService, setSelectedService] = useState<ServiceInfo | null>(null);
|
const [selectedService, setSelectedService] = useState<ServiceInfo | null>(null);
|
||||||
const [serviceApis, setServiceApis] = useState<ServiceApi[]>([]);
|
const [serviceApis, setServiceApis] = useState<ServiceApi[]>([]);
|
||||||
@ -57,14 +58,6 @@ const ServicesPage = () => {
|
|||||||
const [healthCheckInterval, setHealthCheckInterval] = useState(60);
|
const [healthCheckInterval, setHealthCheckInterval] = useState(60);
|
||||||
const [serviceIsActive, setServiceIsActive] = useState(true);
|
const [serviceIsActive, setServiceIsActive] = useState(true);
|
||||||
|
|
||||||
const [isApiModalOpen, setIsApiModalOpen] = useState(false);
|
|
||||||
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 fetchServices = async () => {
|
const fetchServices = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -97,8 +90,13 @@ const ServicesPage = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSelectService = (service: ServiceInfo) => {
|
const handleSelectService = (service: ServiceInfo) => {
|
||||||
|
if (selectedService?.serviceId === service.serviceId) {
|
||||||
|
setSelectedService(null);
|
||||||
|
setServiceApis([]);
|
||||||
|
} else {
|
||||||
setSelectedService(service);
|
setSelectedService(service);
|
||||||
fetchApis(service.serviceId);
|
fetchApis(service.serviceId);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenCreateService = () => {
|
const handleOpenCreateService = () => {
|
||||||
@ -172,44 +170,21 @@ const ServicesPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenCreateApi = () => {
|
const handleDeleteService = async (service: ServiceInfo) => {
|
||||||
setApiMethod('GET');
|
if (!window.confirm(`'${service.serviceName}' 서비스를 삭제하시겠습니까?`)) return;
|
||||||
setApiPath('');
|
|
||||||
setApiName('');
|
|
||||||
setApiDomain('');
|
|
||||||
setApiSection('');
|
|
||||||
setApiDescription('');
|
|
||||||
setIsApiModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseApiModal = () => {
|
|
||||||
setIsApiModalOpen(false);
|
|
||||||
setError(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleApiSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!selectedService) return;
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const req: CreateServiceApiRequest = {
|
const res = await deleteService(service.serviceId);
|
||||||
apiMethod,
|
|
||||||
apiPath,
|
|
||||||
apiName,
|
|
||||||
apiDomain: apiDomain || undefined,
|
|
||||||
apiSection: apiSection || undefined,
|
|
||||||
description: apiDescription || undefined,
|
|
||||||
};
|
|
||||||
const res = await createServiceApi(selectedService.serviceId, req);
|
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
setError(res.message || 'API 생성에 실패했습니다.');
|
setError(res.message || '서비스 삭제에 실패했습니다.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleCloseApiModal();
|
if (selectedService?.serviceId === service.serviceId) {
|
||||||
await fetchApis(selectedService.serviceId);
|
setSelectedService(null);
|
||||||
|
setServiceApis([]);
|
||||||
|
}
|
||||||
|
await fetchServices();
|
||||||
} catch {
|
} catch {
|
||||||
setError('API 생성에 실패했습니다.');
|
setError('서비스 삭제에 실패했습니다.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -229,7 +204,7 @@ const ServicesPage = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && !isServiceModalOpen && !isApiModalOpen && (
|
{error && !isServiceModalOpen && (
|
||||||
<div className="mb-4 p-3 bg-red-50 text-red-700 rounded-lg text-sm">{error}</div>
|
<div className="mb-4 p-3 bg-red-50 text-red-700 rounded-lg text-sm">{error}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -292,6 +267,7 @@ const ServicesPage = () => {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -301,6 +277,16 @@ const ServicesPage = () => {
|
|||||||
>
|
>
|
||||||
수정
|
수정
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteService(service);
|
||||||
|
}}
|
||||||
|
className="bg-red-100 hover:bg-red-200 text-red-700 dark:bg-red-900/30 dark:hover:bg-red-800/40 dark:text-red-400 px-3 py-1 rounded-lg text-sm font-medium"
|
||||||
|
>
|
||||||
|
삭제
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@ -323,10 +309,10 @@ const ServicesPage = () => {
|
|||||||
APIs for {selectedService.serviceName}
|
APIs for {selectedService.serviceName}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
onClick={handleOpenCreateApi}
|
onClick={() => navigate('/admin/apis')}
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-lg text-sm font-medium"
|
className="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-lg text-sm font-medium"
|
||||||
>
|
>
|
||||||
Add API
|
API 관리
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
@ -336,6 +322,8 @@ const ServicesPage = () => {
|
|||||||
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Method</th>
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Method</th>
|
||||||
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Path</th>
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Path</th>
|
||||||
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Name</th>
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Name</th>
|
||||||
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Domain</th>
|
||||||
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Section</th>
|
||||||
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Description</th>
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Description</th>
|
||||||
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Active</th>
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Active</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -354,6 +342,8 @@ const ServicesPage = () => {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 font-mono text-gray-900 dark:text-gray-100">{api.apiPath}</td>
|
<td className="px-4 py-3 font-mono text-gray-900 dark:text-gray-100">{api.apiPath}</td>
|
||||||
<td className="px-4 py-3 text-gray-900 dark:text-gray-100">{api.apiName}</td>
|
<td className="px-4 py-3 text-gray-900 dark:text-gray-100">{api.apiName}</td>
|
||||||
|
<td className="px-4 py-3 text-gray-500 dark:text-gray-400">{api.apiDomain || '-'}</td>
|
||||||
|
<td className="px-4 py-3 text-gray-500 dark:text-gray-400">{api.apiSection || '-'}</td>
|
||||||
<td className="px-4 py-3 text-gray-500 dark:text-gray-400">{api.description || '-'}</td>
|
<td className="px-4 py-3 text-gray-500 dark:text-gray-400">{api.description || '-'}</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<span
|
<span
|
||||||
@ -370,7 +360,7 @@ const ServicesPage = () => {
|
|||||||
))}
|
))}
|
||||||
{serviceApis.length === 0 && (
|
{serviceApis.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={5} className="px-4 py-8 text-center text-gray-400 dark:text-gray-500">
|
<td colSpan={7} className="px-4 py-8 text-center text-gray-400 dark:text-gray-500">
|
||||||
등록된 API가 없습니다.
|
등록된 API가 없습니다.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -499,103 +489,6 @@ const ServicesPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isApiModalOpen && (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md mx-4">
|
|
||||||
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">API 생성</h2>
|
|
||||||
</div>
|
|
||||||
<form onSubmit={handleApiSubmit}>
|
|
||||||
<div className="px-6 py-4 space-y-4">
|
|
||||||
{error && (
|
|
||||||
<div className="p-3 bg-red-50 text-red-700 rounded-lg text-sm">{error}</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Method</label>
|
|
||||||
<select
|
|
||||||
value={apiMethod}
|
|
||||||
onChange={(e) => setApiMethod(e.target.value)}
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
||||||
>
|
|
||||||
<option value="GET">GET</option>
|
|
||||||
<option value="POST">POST</option>
|
|
||||||
<option value="PUT">PUT</option>
|
|
||||||
<option value="DELETE">DELETE</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">API Path</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={apiPath}
|
|
||||||
onChange={(e) => setApiPath(e.target.value)}
|
|
||||||
required
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">API Name</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={apiName}
|
|
||||||
onChange={(e) => setApiName(e.target.value)}
|
|
||||||
required
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">도메인</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={apiDomain}
|
|
||||||
onChange={(e) => setApiDomain(e.target.value)}
|
|
||||||
placeholder="예: Compliance"
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">섹션</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={apiSection}
|
|
||||||
onChange={(e) => setApiSection(e.target.value)}
|
|
||||||
placeholder="예: 선박 규정"
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={apiDescription}
|
|
||||||
onChange={(e) => setApiDescription(e.target.value)}
|
|
||||||
rows={3}
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleCloseApiModal}
|
|
||||||
className="bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 px-4 py-2 rounded-lg text-sm font-medium"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,15 +1,50 @@
|
|||||||
import { get, post, put } from './apiClient';
|
import { get, post, put, del } from './apiClient';
|
||||||
import type {
|
import type {
|
||||||
ServiceInfo,
|
ServiceInfo,
|
||||||
ServiceApi,
|
ServiceApi,
|
||||||
CreateServiceRequest,
|
CreateServiceRequest,
|
||||||
UpdateServiceRequest,
|
UpdateServiceRequest,
|
||||||
CreateServiceApiRequest,
|
CreateServiceApiRequest,
|
||||||
|
UpdateServiceApiRequest,
|
||||||
|
ApiDetailInfo,
|
||||||
|
SaveApiSpecRequest,
|
||||||
|
SaveApiParamRequest,
|
||||||
|
ApiSpecInfo,
|
||||||
|
ApiParamInfo,
|
||||||
} from '../types/service';
|
} from '../types/service';
|
||||||
|
import type { ApiDomainInfo } from '../types/apihub';
|
||||||
|
|
||||||
|
export interface CreateDomainRequest {
|
||||||
|
domainName: string;
|
||||||
|
iconPath?: string | null;
|
||||||
|
sortOrder?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateDomainRequest {
|
||||||
|
domainName: string;
|
||||||
|
iconPath?: string | null;
|
||||||
|
sortOrder?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const getServices = () => get<ServiceInfo[]>('/services');
|
export const getServices = () => get<ServiceInfo[]>('/services');
|
||||||
export const createService = (req: CreateServiceRequest) => post<ServiceInfo>('/services', req);
|
export const createService = (req: CreateServiceRequest) => post<ServiceInfo>('/services', req);
|
||||||
export const updateService = (id: number, req: UpdateServiceRequest) => put<ServiceInfo>(`/services/${id}`, req);
|
export const updateService = (id: number, req: UpdateServiceRequest) => put<ServiceInfo>(`/services/${id}`, req);
|
||||||
|
export const deleteService = (id: number) => del<void>(`/services/${id}`);
|
||||||
export const getServiceApis = (serviceId: number) => get<ServiceApi[]>(`/services/${serviceId}/apis`);
|
export const getServiceApis = (serviceId: number) => get<ServiceApi[]>(`/services/${serviceId}/apis`);
|
||||||
export const createServiceApi = (serviceId: number, req: CreateServiceApiRequest) =>
|
export const createServiceApi = (serviceId: number, req: CreateServiceApiRequest) =>
|
||||||
post<ServiceApi>(`/services/${serviceId}/apis`, req);
|
post<ServiceApi>(`/services/${serviceId}/apis`, req);
|
||||||
|
export const updateServiceApi = (serviceId: number, apiId: number, req: UpdateServiceApiRequest) =>
|
||||||
|
put<ServiceApi>(`/services/${serviceId}/apis/${apiId}`, req);
|
||||||
|
export const deleteServiceApi = (serviceId: number, apiId: number) =>
|
||||||
|
del<void>(`/services/${serviceId}/apis/${apiId}`);
|
||||||
|
export const getApiDetail = (serviceId: number, apiId: number) =>
|
||||||
|
get<ApiDetailInfo>(`/services/${serviceId}/apis/${apiId}/spec`);
|
||||||
|
export const saveApiSpec = (serviceId: number, apiId: number, req: SaveApiSpecRequest) =>
|
||||||
|
put<ApiSpecInfo>(`/services/${serviceId}/apis/${apiId}/spec`, req);
|
||||||
|
export const saveApiParams = (serviceId: number, apiId: number, params: SaveApiParamRequest[]) =>
|
||||||
|
put<ApiParamInfo[]>(`/services/${serviceId}/apis/${apiId}/params`, params);
|
||||||
|
|
||||||
|
export const getDomains = () => get<ApiDomainInfo[]>('/domains');
|
||||||
|
export const createDomain = (req: CreateDomainRequest) => post<ApiDomainInfo>('/domains', req);
|
||||||
|
export const updateDomain = (id: number, req: UpdateDomainRequest) => put<ApiDomainInfo>(`/domains/${id}`, req);
|
||||||
|
export const deleteDomain = (id: number) => del<void>(`/domains/${id}`);
|
||||||
|
|||||||
@ -97,3 +97,77 @@ export interface HealthHistory {
|
|||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
checkedAt: string;
|
checkedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateServiceApiRequest {
|
||||||
|
apiPath?: string;
|
||||||
|
apiMethod?: string;
|
||||||
|
apiName?: string;
|
||||||
|
apiDomain?: string;
|
||||||
|
apiSection?: string;
|
||||||
|
description?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiSpecInfo {
|
||||||
|
specId: number;
|
||||||
|
apiId: number;
|
||||||
|
sampleUrl: string | null;
|
||||||
|
authRequired: boolean;
|
||||||
|
authType: string | null;
|
||||||
|
deprecated: boolean;
|
||||||
|
dataFormat: string | null;
|
||||||
|
referenceUrl: string | null;
|
||||||
|
note: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiParamInfo {
|
||||||
|
paramId: number;
|
||||||
|
apiId: number;
|
||||||
|
paramType: 'REQUEST' | 'RESPONSE';
|
||||||
|
paramName: string;
|
||||||
|
paramMeaning: string | null;
|
||||||
|
paramDescription: string | null;
|
||||||
|
required: boolean;
|
||||||
|
defaultValue: string | null;
|
||||||
|
inputType: string | null;
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiDetailInfo {
|
||||||
|
api: ServiceApi;
|
||||||
|
spec: ApiSpecInfo | null;
|
||||||
|
requestParams: ApiParamInfo[];
|
||||||
|
responseParams: ApiParamInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SaveApiSpecRequest {
|
||||||
|
sampleUrl?: string;
|
||||||
|
authRequired?: boolean;
|
||||||
|
authType?: string;
|
||||||
|
deprecated?: boolean;
|
||||||
|
dataFormat?: string;
|
||||||
|
referenceUrl?: string;
|
||||||
|
note?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemConfigInfo {
|
||||||
|
configId: number;
|
||||||
|
configKey: string;
|
||||||
|
configValue: string | null;
|
||||||
|
description: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SaveApiParamRequest {
|
||||||
|
paramType: 'REQUEST' | 'RESPONSE';
|
||||||
|
paramName: string;
|
||||||
|
paramMeaning?: string;
|
||||||
|
paramDescription?: string;
|
||||||
|
required?: boolean;
|
||||||
|
defaultValue?: string;
|
||||||
|
inputType?: string;
|
||||||
|
sortOrder?: number;
|
||||||
|
}
|
||||||
|
|||||||
@ -4,16 +4,19 @@ import com.gcsc.connection.apihub.dto.RecentApiResponse;
|
|||||||
import com.gcsc.connection.apihub.dto.ServiceCatalogResponse;
|
import com.gcsc.connection.apihub.dto.ServiceCatalogResponse;
|
||||||
import com.gcsc.connection.apihub.service.ApiHubService;
|
import com.gcsc.connection.apihub.service.ApiHubService;
|
||||||
import com.gcsc.connection.common.dto.ApiResponse;
|
import com.gcsc.connection.common.dto.ApiResponse;
|
||||||
|
import com.gcsc.connection.service.dto.ApiDetailResponse;
|
||||||
|
import com.gcsc.connection.service.service.ServiceManagementService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API Hub 카탈로그 및 최근 API 조회 컨트롤러
|
* API Hub 카탈로그 및 API 상세 조회 컨트롤러
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/api-hub")
|
@RequestMapping("/api/api-hub")
|
||||||
@ -21,6 +24,7 @@ import java.util.List;
|
|||||||
public class ApiHubController {
|
public class ApiHubController {
|
||||||
|
|
||||||
private final ApiHubService apiHubService;
|
private final ApiHubService apiHubService;
|
||||||
|
private final ServiceManagementService serviceManagementService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 활성 서비스와 해당 서비스의 활성 API를 도메인별로 그룹화하여 카탈로그 형태로 반환
|
* 활성 서비스와 해당 서비스의 활성 API를 도메인별로 그룹화하여 카탈로그 형태로 반환
|
||||||
@ -39,4 +43,24 @@ public class ApiHubController {
|
|||||||
List<RecentApiResponse> recentApis = apiHubService.getRecentApis();
|
List<RecentApiResponse> recentApis = apiHubService.getRecentApis();
|
||||||
return ResponseEntity.ok(ApiResponse.ok(recentApis));
|
return ResponseEntity.ok(ApiResponse.ok(recentApis));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 서비스 단건 카탈로그 조회
|
||||||
|
*/
|
||||||
|
@GetMapping("/services/{serviceId}")
|
||||||
|
public ResponseEntity<ApiResponse<ServiceCatalogResponse>> getServiceCatalog(
|
||||||
|
@PathVariable Long serviceId) {
|
||||||
|
ServiceCatalogResponse catalog = apiHubService.getServiceCatalog(serviceId);
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(catalog));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 상세 명세 조회
|
||||||
|
*/
|
||||||
|
@GetMapping("/services/{serviceId}/apis/{apiId}")
|
||||||
|
public ResponseEntity<ApiResponse<ApiDetailResponse>> getApiDetail(
|
||||||
|
@PathVariable Long serviceId, @PathVariable Long apiId) {
|
||||||
|
ApiDetailResponse detail = serviceManagementService.getApiDetail(serviceId, apiId);
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(detail));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,8 @@ public class WebViewController {
|
|||||||
@GetMapping({"/dashboard", "/dashboard/**",
|
@GetMapping({"/dashboard", "/dashboard/**",
|
||||||
"/monitoring/**", "/statistics/**",
|
"/monitoring/**", "/statistics/**",
|
||||||
"/apikeys", "/apikeys/**",
|
"/apikeys", "/apikeys/**",
|
||||||
"/admin/**"})
|
"/admin/**",
|
||||||
|
"/api-hub", "/api-hub/**"})
|
||||||
public String forward() {
|
public String forward() {
|
||||||
return "forward:/index.html";
|
return "forward:/index.html";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,22 @@
|
|||||||
package com.gcsc.connection.service.controller;
|
package com.gcsc.connection.service.controller;
|
||||||
|
|
||||||
import com.gcsc.connection.common.dto.ApiResponse;
|
import com.gcsc.connection.common.dto.ApiResponse;
|
||||||
|
import com.gcsc.connection.service.dto.ApiDetailResponse;
|
||||||
|
import com.gcsc.connection.service.dto.ApiParamResponse;
|
||||||
|
import com.gcsc.connection.service.dto.ApiSpecResponse;
|
||||||
import com.gcsc.connection.service.dto.CreateServiceApiRequest;
|
import com.gcsc.connection.service.dto.CreateServiceApiRequest;
|
||||||
import com.gcsc.connection.service.dto.CreateServiceRequest;
|
import com.gcsc.connection.service.dto.CreateServiceRequest;
|
||||||
|
import com.gcsc.connection.service.dto.SaveApiParamRequest;
|
||||||
|
import com.gcsc.connection.service.dto.SaveApiSpecRequest;
|
||||||
import com.gcsc.connection.service.dto.ServiceApiResponse;
|
import com.gcsc.connection.service.dto.ServiceApiResponse;
|
||||||
import com.gcsc.connection.service.dto.ServiceResponse;
|
import com.gcsc.connection.service.dto.ServiceResponse;
|
||||||
|
import com.gcsc.connection.service.dto.UpdateServiceApiRequest;
|
||||||
import com.gcsc.connection.service.dto.UpdateServiceRequest;
|
import com.gcsc.connection.service.dto.UpdateServiceRequest;
|
||||||
import com.gcsc.connection.service.service.ServiceManagementService;
|
import com.gcsc.connection.service.service.ServiceManagementService;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@ -39,6 +46,15 @@ public class ServiceController {
|
|||||||
return ResponseEntity.ok(ApiResponse.ok(services));
|
return ResponseEntity.ok(ApiResponse.ok(services));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 서비스 단건 조회
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<ApiResponse<ServiceResponse>> getService(@PathVariable Long id) {
|
||||||
|
ServiceResponse service = serviceManagementService.getService(id);
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(service));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 서비스 생성
|
* 서비스 생성
|
||||||
*/
|
*/
|
||||||
@ -60,6 +76,15 @@ public class ServiceController {
|
|||||||
return ResponseEntity.ok(ApiResponse.ok(service));
|
return ResponseEntity.ok(ApiResponse.ok(service));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 서비스 삭제 (비활성화)
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<ApiResponse<Void>> deleteService(@PathVariable Long id) {
|
||||||
|
serviceManagementService.deleteService(id);
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(null));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 서비스 API 목록 조회
|
* 서비스 API 목록 조회
|
||||||
*/
|
*/
|
||||||
@ -80,4 +105,57 @@ public class ServiceController {
|
|||||||
ServiceApiResponse api = serviceManagementService.createServiceApi(id, request);
|
ServiceApiResponse api = serviceManagementService.createServiceApi(id, request);
|
||||||
return ResponseEntity.ok(ApiResponse.ok(api));
|
return ResponseEntity.ok(ApiResponse.ok(api));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 서비스 API 수정
|
||||||
|
*/
|
||||||
|
@PutMapping("/{serviceId}/apis/{apiId}")
|
||||||
|
public ResponseEntity<ApiResponse<ServiceApiResponse>> updateServiceApi(
|
||||||
|
@PathVariable Long serviceId, @PathVariable Long apiId,
|
||||||
|
@RequestBody @Valid UpdateServiceApiRequest request) {
|
||||||
|
ServiceApiResponse api = serviceManagementService.updateServiceApi(serviceId, apiId, request);
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(api));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 서비스 API 삭제 (비활성화)
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/{serviceId}/apis/{apiId}")
|
||||||
|
public ResponseEntity<ApiResponse<Void>> deleteServiceApi(
|
||||||
|
@PathVariable Long serviceId, @PathVariable Long apiId) {
|
||||||
|
serviceManagementService.deleteServiceApi(serviceId, apiId);
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 상세 명세 조회 (스펙 + 파라미터)
|
||||||
|
*/
|
||||||
|
@GetMapping("/{serviceId}/apis/{apiId}/spec")
|
||||||
|
public ResponseEntity<ApiResponse<ApiDetailResponse>> getApiDetail(
|
||||||
|
@PathVariable Long serviceId, @PathVariable Long apiId) {
|
||||||
|
ApiDetailResponse detail = serviceManagementService.getApiDetail(serviceId, apiId);
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(detail));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 명세 저장 (upsert)
|
||||||
|
*/
|
||||||
|
@PutMapping("/{serviceId}/apis/{apiId}/spec")
|
||||||
|
public ResponseEntity<ApiResponse<ApiSpecResponse>> saveApiSpec(
|
||||||
|
@PathVariable Long serviceId, @PathVariable Long apiId,
|
||||||
|
@RequestBody @Valid SaveApiSpecRequest request) {
|
||||||
|
ApiSpecResponse spec = serviceManagementService.saveApiSpec(serviceId, apiId, request);
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(spec));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 파라미터 전체 교체
|
||||||
|
*/
|
||||||
|
@PutMapping("/{serviceId}/apis/{apiId}/params")
|
||||||
|
public ResponseEntity<ApiResponse<List<ApiParamResponse>>> saveApiParams(
|
||||||
|
@PathVariable Long serviceId, @PathVariable Long apiId,
|
||||||
|
@RequestBody List<SaveApiParamRequest> requests) {
|
||||||
|
List<ApiParamResponse> params = serviceManagementService.saveApiParams(serviceId, apiId, requests);
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(params));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.gcsc.connection.service.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record ApiDetailResponse(
|
||||||
|
ServiceApiResponse api,
|
||||||
|
ApiSpecResponse spec,
|
||||||
|
List<ApiParamResponse> requestParams,
|
||||||
|
List<ApiParamResponse> responseParams
|
||||||
|
) {
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package com.gcsc.connection.service.dto;
|
||||||
|
|
||||||
|
import com.gcsc.connection.service.entity.SnpServiceApiParam;
|
||||||
|
|
||||||
|
public record ApiParamResponse(
|
||||||
|
Long paramId,
|
||||||
|
Long apiId,
|
||||||
|
String paramType,
|
||||||
|
String paramName,
|
||||||
|
String paramMeaning,
|
||||||
|
String paramDescription,
|
||||||
|
Boolean required,
|
||||||
|
String defaultValue,
|
||||||
|
String inputType,
|
||||||
|
Integer sortOrder
|
||||||
|
) {
|
||||||
|
|
||||||
|
public static ApiParamResponse from(SnpServiceApiParam p) {
|
||||||
|
return new ApiParamResponse(
|
||||||
|
p.getParamId(),
|
||||||
|
p.getApi().getApiId(),
|
||||||
|
p.getParamType(),
|
||||||
|
p.getParamName(),
|
||||||
|
p.getParamMeaning(),
|
||||||
|
p.getParamDescription(),
|
||||||
|
p.getRequired(),
|
||||||
|
p.getDefaultValue(),
|
||||||
|
p.getInputType(),
|
||||||
|
p.getSortOrder()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
package com.gcsc.connection.service.dto;
|
||||||
|
|
||||||
|
import com.gcsc.connection.service.entity.SnpServiceApiSpec;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public record ApiSpecResponse(
|
||||||
|
Long specId,
|
||||||
|
Long apiId,
|
||||||
|
String sampleUrl,
|
||||||
|
String sampleCode,
|
||||||
|
String requestBodyExample,
|
||||||
|
String responseBodyExample,
|
||||||
|
Boolean authRequired,
|
||||||
|
String authType,
|
||||||
|
Boolean deprecated,
|
||||||
|
String dataFormat,
|
||||||
|
String referenceUrl,
|
||||||
|
String note,
|
||||||
|
LocalDateTime createdAt,
|
||||||
|
LocalDateTime updatedAt
|
||||||
|
) {
|
||||||
|
|
||||||
|
public static ApiSpecResponse from(SnpServiceApiSpec s) {
|
||||||
|
return new ApiSpecResponse(
|
||||||
|
s.getSpecId(),
|
||||||
|
s.getApi().getApiId(),
|
||||||
|
s.getSampleUrl(),
|
||||||
|
s.getSampleCode(),
|
||||||
|
s.getRequestBodyExample(),
|
||||||
|
s.getResponseBodyExample(),
|
||||||
|
s.getAuthRequired(),
|
||||||
|
s.getAuthType(),
|
||||||
|
s.getDeprecated(),
|
||||||
|
s.getDataFormat(),
|
||||||
|
s.getReferenceUrl(),
|
||||||
|
s.getNote(),
|
||||||
|
s.getCreatedAt(),
|
||||||
|
s.getUpdatedAt()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.gcsc.connection.service.dto;
|
||||||
|
|
||||||
|
public record SaveApiParamRequest(
|
||||||
|
String paramType,
|
||||||
|
String paramName,
|
||||||
|
String paramMeaning,
|
||||||
|
String paramDescription,
|
||||||
|
Boolean required,
|
||||||
|
String defaultValue,
|
||||||
|
String inputType,
|
||||||
|
Integer sortOrder
|
||||||
|
) {
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package com.gcsc.connection.service.dto;
|
||||||
|
|
||||||
|
public record SaveApiSpecRequest(
|
||||||
|
String sampleUrl,
|
||||||
|
String sampleCode,
|
||||||
|
String requestBodyExample,
|
||||||
|
String responseBodyExample,
|
||||||
|
Boolean authRequired,
|
||||||
|
String authType,
|
||||||
|
Boolean deprecated,
|
||||||
|
String dataFormat,
|
||||||
|
String referenceUrl,
|
||||||
|
String note
|
||||||
|
) {
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.gcsc.connection.service.dto;
|
||||||
|
|
||||||
|
public record UpdateServiceApiRequest(
|
||||||
|
String apiPath,
|
||||||
|
String apiMethod,
|
||||||
|
String apiName,
|
||||||
|
String apiDomain,
|
||||||
|
String apiSection,
|
||||||
|
String description,
|
||||||
|
Boolean isActive
|
||||||
|
) {
|
||||||
|
}
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
package com.gcsc.connection.service.entity;
|
||||||
|
|
||||||
|
import com.gcsc.connection.common.entity.BaseEntity;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Entity
|
||||||
|
@Table(name = "snp_service_api_param", schema = "common")
|
||||||
|
public class SnpServiceApiParam extends BaseEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "param_id")
|
||||||
|
private Long paramId;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "api_id", nullable = false)
|
||||||
|
private SnpServiceApi api;
|
||||||
|
|
||||||
|
@Column(name = "param_type", length = 10, nullable = false)
|
||||||
|
private String paramType;
|
||||||
|
|
||||||
|
@Column(name = "param_name", length = 100, nullable = false)
|
||||||
|
private String paramName;
|
||||||
|
|
||||||
|
@Column(name = "param_meaning", length = 200)
|
||||||
|
private String paramMeaning;
|
||||||
|
|
||||||
|
@Column(name = "param_description", columnDefinition = "TEXT")
|
||||||
|
private String paramDescription;
|
||||||
|
|
||||||
|
@Column(name = "required", nullable = false)
|
||||||
|
private Boolean required = false;
|
||||||
|
|
||||||
|
@Column(name = "default_value", length = 200)
|
||||||
|
private String defaultValue;
|
||||||
|
|
||||||
|
@Column(name = "input_type", length = 20)
|
||||||
|
private String inputType;
|
||||||
|
|
||||||
|
@Column(name = "sort_order", nullable = false)
|
||||||
|
private Integer sortOrder = 0;
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
public SnpServiceApiParam(SnpServiceApi api, String paramType, String paramName,
|
||||||
|
String paramMeaning, String paramDescription, Boolean required,
|
||||||
|
String defaultValue, String inputType, Integer sortOrder) {
|
||||||
|
this.api = api;
|
||||||
|
this.paramType = paramType;
|
||||||
|
this.paramName = paramName;
|
||||||
|
this.paramMeaning = paramMeaning;
|
||||||
|
this.paramDescription = paramDescription;
|
||||||
|
this.required = required != null ? required : false;
|
||||||
|
this.defaultValue = defaultValue;
|
||||||
|
this.inputType = inputType;
|
||||||
|
this.sortOrder = sortOrder != null ? sortOrder : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(String paramType, String paramName, String paramMeaning,
|
||||||
|
String paramDescription, Boolean required, String defaultValue,
|
||||||
|
String inputType, Integer sortOrder) {
|
||||||
|
if (paramType != null) this.paramType = paramType;
|
||||||
|
if (paramName != null) this.paramName = paramName;
|
||||||
|
if (paramMeaning != null) this.paramMeaning = paramMeaning;
|
||||||
|
if (paramDescription != null) this.paramDescription = paramDescription;
|
||||||
|
if (required != null) this.required = required;
|
||||||
|
if (defaultValue != null) this.defaultValue = defaultValue;
|
||||||
|
if (inputType != null) this.inputType = inputType;
|
||||||
|
if (sortOrder != null) this.sortOrder = sortOrder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
package com.gcsc.connection.service.entity;
|
||||||
|
|
||||||
|
import com.gcsc.connection.common.entity.BaseEntity;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.OneToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Entity
|
||||||
|
@Table(name = "snp_service_api_spec", schema = "common")
|
||||||
|
public class SnpServiceApiSpec extends BaseEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "spec_id")
|
||||||
|
private Long specId;
|
||||||
|
|
||||||
|
@OneToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "api_id", nullable = false, unique = true)
|
||||||
|
private SnpServiceApi api;
|
||||||
|
|
||||||
|
@Column(name = "sample_url", length = 1000)
|
||||||
|
private String sampleUrl;
|
||||||
|
|
||||||
|
@Column(name = "sample_code", columnDefinition = "TEXT")
|
||||||
|
private String sampleCode;
|
||||||
|
|
||||||
|
@Column(name = "request_body_example", columnDefinition = "TEXT")
|
||||||
|
private String requestBodyExample;
|
||||||
|
|
||||||
|
@Column(name = "response_body_example", columnDefinition = "TEXT")
|
||||||
|
private String responseBodyExample;
|
||||||
|
|
||||||
|
@Column(name = "auth_required", nullable = false)
|
||||||
|
private Boolean authRequired = false;
|
||||||
|
|
||||||
|
@Column(name = "auth_type", length = 20)
|
||||||
|
private String authType;
|
||||||
|
|
||||||
|
@Column(name = "deprecated", nullable = false)
|
||||||
|
private Boolean deprecated = false;
|
||||||
|
|
||||||
|
@Column(name = "data_format", length = 100)
|
||||||
|
private String dataFormat;
|
||||||
|
|
||||||
|
@Column(name = "reference_url", length = 500)
|
||||||
|
private String referenceUrl;
|
||||||
|
|
||||||
|
@Column(name = "note", columnDefinition = "TEXT")
|
||||||
|
private String note;
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
public SnpServiceApiSpec(SnpServiceApi api, String sampleUrl, String sampleCode,
|
||||||
|
String requestBodyExample, String responseBodyExample,
|
||||||
|
Boolean authRequired, String authType, Boolean deprecated,
|
||||||
|
String dataFormat, String referenceUrl, String note) {
|
||||||
|
this.api = api;
|
||||||
|
this.sampleUrl = sampleUrl;
|
||||||
|
this.sampleCode = sampleCode;
|
||||||
|
this.requestBodyExample = requestBodyExample;
|
||||||
|
this.responseBodyExample = responseBodyExample;
|
||||||
|
this.authRequired = authRequired != null ? authRequired : false;
|
||||||
|
this.authType = authType;
|
||||||
|
this.deprecated = deprecated != null ? deprecated : false;
|
||||||
|
this.dataFormat = dataFormat;
|
||||||
|
this.referenceUrl = referenceUrl;
|
||||||
|
this.note = note;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(String sampleUrl, String sampleCode, String requestBodyExample,
|
||||||
|
String responseBodyExample, Boolean authRequired, String authType,
|
||||||
|
Boolean deprecated, String dataFormat, String referenceUrl, String note) {
|
||||||
|
if (sampleUrl != null) this.sampleUrl = sampleUrl;
|
||||||
|
if (sampleCode != null) this.sampleCode = sampleCode;
|
||||||
|
if (requestBodyExample != null) this.requestBodyExample = requestBodyExample;
|
||||||
|
if (responseBodyExample != null) this.responseBodyExample = responseBodyExample;
|
||||||
|
if (authRequired != null) this.authRequired = authRequired;
|
||||||
|
if (authType != null) this.authType = authType;
|
||||||
|
if (deprecated != null) this.deprecated = deprecated;
|
||||||
|
if (dataFormat != null) this.dataFormat = dataFormat;
|
||||||
|
if (referenceUrl != null) this.referenceUrl = referenceUrl;
|
||||||
|
if (note != null) this.note = note;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package com.gcsc.connection.service.repository;
|
||||||
|
|
||||||
|
import com.gcsc.connection.service.entity.SnpServiceApiParam;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface SnpServiceApiParamRepository extends JpaRepository<SnpServiceApiParam, Long> {
|
||||||
|
|
||||||
|
List<SnpServiceApiParam> findByApiApiIdOrderBySortOrder(Long apiId);
|
||||||
|
|
||||||
|
List<SnpServiceApiParam> findByApiApiIdAndParamTypeOrderBySortOrder(Long apiId, String paramType);
|
||||||
|
|
||||||
|
void deleteByApiApiId(Long apiId);
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.gcsc.connection.service.repository;
|
||||||
|
|
||||||
|
import com.gcsc.connection.service.entity.SnpServiceApiSpec;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface SnpServiceApiSpecRepository extends JpaRepository<SnpServiceApiSpec, Long> {
|
||||||
|
|
||||||
|
Optional<SnpServiceApiSpec> findByApiApiId(Long apiId);
|
||||||
|
|
||||||
|
void deleteByApiApiId(Long apiId);
|
||||||
|
}
|
||||||
@ -2,14 +2,24 @@ package com.gcsc.connection.service.service;
|
|||||||
|
|
||||||
import com.gcsc.connection.common.exception.BusinessException;
|
import com.gcsc.connection.common.exception.BusinessException;
|
||||||
import com.gcsc.connection.common.exception.ErrorCode;
|
import com.gcsc.connection.common.exception.ErrorCode;
|
||||||
|
import com.gcsc.connection.service.dto.ApiDetailResponse;
|
||||||
|
import com.gcsc.connection.service.dto.ApiParamResponse;
|
||||||
|
import com.gcsc.connection.service.dto.ApiSpecResponse;
|
||||||
import com.gcsc.connection.service.dto.CreateServiceApiRequest;
|
import com.gcsc.connection.service.dto.CreateServiceApiRequest;
|
||||||
import com.gcsc.connection.service.dto.CreateServiceRequest;
|
import com.gcsc.connection.service.dto.CreateServiceRequest;
|
||||||
|
import com.gcsc.connection.service.dto.SaveApiParamRequest;
|
||||||
|
import com.gcsc.connection.service.dto.SaveApiSpecRequest;
|
||||||
import com.gcsc.connection.service.dto.ServiceApiResponse;
|
import com.gcsc.connection.service.dto.ServiceApiResponse;
|
||||||
import com.gcsc.connection.service.dto.ServiceResponse;
|
import com.gcsc.connection.service.dto.ServiceResponse;
|
||||||
|
import com.gcsc.connection.service.dto.UpdateServiceApiRequest;
|
||||||
import com.gcsc.connection.service.dto.UpdateServiceRequest;
|
import com.gcsc.connection.service.dto.UpdateServiceRequest;
|
||||||
import com.gcsc.connection.service.entity.SnpService;
|
import com.gcsc.connection.service.entity.SnpService;
|
||||||
import com.gcsc.connection.service.entity.SnpServiceApi;
|
import com.gcsc.connection.service.entity.SnpServiceApi;
|
||||||
|
import com.gcsc.connection.service.entity.SnpServiceApiParam;
|
||||||
|
import com.gcsc.connection.service.entity.SnpServiceApiSpec;
|
||||||
|
import com.gcsc.connection.service.repository.SnpServiceApiParamRepository;
|
||||||
import com.gcsc.connection.service.repository.SnpServiceApiRepository;
|
import com.gcsc.connection.service.repository.SnpServiceApiRepository;
|
||||||
|
import com.gcsc.connection.service.repository.SnpServiceApiSpecRepository;
|
||||||
import com.gcsc.connection.service.repository.SnpServiceRepository;
|
import com.gcsc.connection.service.repository.SnpServiceRepository;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -25,6 +35,8 @@ public class ServiceManagementService {
|
|||||||
|
|
||||||
private final SnpServiceRepository snpServiceRepository;
|
private final SnpServiceRepository snpServiceRepository;
|
||||||
private final SnpServiceApiRepository snpServiceApiRepository;
|
private final SnpServiceApiRepository snpServiceApiRepository;
|
||||||
|
private final SnpServiceApiSpecRepository snpServiceApiSpecRepository;
|
||||||
|
private final SnpServiceApiParamRepository snpServiceApiParamRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 전체 서비스 목록 조회
|
* 전체 서비스 목록 조회
|
||||||
@ -89,6 +101,163 @@ public class ServiceManagementService {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 상세 명세 조회
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public ApiDetailResponse getApiDetail(Long serviceId, Long apiId) {
|
||||||
|
snpServiceRepository.findById(serviceId)
|
||||||
|
.orElseThrow(() -> new BusinessException(ErrorCode.SERVICE_NOT_FOUND));
|
||||||
|
|
||||||
|
SnpServiceApi api = snpServiceApiRepository.findById(apiId)
|
||||||
|
.filter(a -> a.getService().getServiceId().equals(serviceId))
|
||||||
|
.orElseThrow(() -> new BusinessException(ErrorCode.SERVICE_API_NOT_FOUND));
|
||||||
|
|
||||||
|
SnpServiceApiSpec spec = snpServiceApiSpecRepository.findByApiApiId(apiId).orElse(null);
|
||||||
|
List<SnpServiceApiParam> requestParams = snpServiceApiParamRepository
|
||||||
|
.findByApiApiIdAndParamTypeOrderBySortOrder(apiId, "REQUEST");
|
||||||
|
List<SnpServiceApiParam> responseParams = snpServiceApiParamRepository
|
||||||
|
.findByApiApiIdAndParamTypeOrderBySortOrder(apiId, "RESPONSE");
|
||||||
|
|
||||||
|
return new ApiDetailResponse(
|
||||||
|
ServiceApiResponse.from(api),
|
||||||
|
spec != null ? ApiSpecResponse.from(spec) : null,
|
||||||
|
requestParams.stream().map(ApiParamResponse::from).toList(),
|
||||||
|
responseParams.stream().map(ApiParamResponse::from).toList()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 서비스 단건 조회
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public ServiceResponse getService(Long id) {
|
||||||
|
SnpService service = snpServiceRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new BusinessException(ErrorCode.SERVICE_NOT_FOUND));
|
||||||
|
return ServiceResponse.from(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 서비스 삭제 (soft delete)
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void deleteService(Long id) {
|
||||||
|
SnpService service = snpServiceRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new BusinessException(ErrorCode.SERVICE_NOT_FOUND));
|
||||||
|
service.update(null, null, null, null, null, false);
|
||||||
|
log.info("서비스 비활성화 완료: {}", service.getServiceCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 서비스 API 수정
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public ServiceApiResponse updateServiceApi(Long serviceId, Long apiId, UpdateServiceApiRequest request) {
|
||||||
|
snpServiceRepository.findById(serviceId)
|
||||||
|
.orElseThrow(() -> new BusinessException(ErrorCode.SERVICE_NOT_FOUND));
|
||||||
|
|
||||||
|
SnpServiceApi api = snpServiceApiRepository.findById(apiId)
|
||||||
|
.filter(a -> a.getService().getServiceId().equals(serviceId))
|
||||||
|
.orElseThrow(() -> new BusinessException(ErrorCode.SERVICE_API_NOT_FOUND));
|
||||||
|
|
||||||
|
api.update(request.apiPath(), request.apiMethod(), request.apiName(),
|
||||||
|
request.apiDomain(), request.apiSection(), request.description(), request.isActive());
|
||||||
|
|
||||||
|
log.info("서비스 API 수정 완료: {} {}", api.getApiMethod(), api.getApiPath());
|
||||||
|
return ServiceApiResponse.from(api);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 서비스 API 삭제 (soft delete)
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void deleteServiceApi(Long serviceId, Long apiId) {
|
||||||
|
snpServiceRepository.findById(serviceId)
|
||||||
|
.orElseThrow(() -> new BusinessException(ErrorCode.SERVICE_NOT_FOUND));
|
||||||
|
|
||||||
|
SnpServiceApi api = snpServiceApiRepository.findById(apiId)
|
||||||
|
.filter(a -> a.getService().getServiceId().equals(serviceId))
|
||||||
|
.orElseThrow(() -> new BusinessException(ErrorCode.SERVICE_API_NOT_FOUND));
|
||||||
|
|
||||||
|
api.update(null, null, null, null, null, null, false);
|
||||||
|
log.info("서비스 API 비활성화 완료: {} {}", api.getApiMethod(), api.getApiPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 명세 저장 (upsert)
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public ApiSpecResponse saveApiSpec(Long serviceId, Long apiId, SaveApiSpecRequest request) {
|
||||||
|
snpServiceRepository.findById(serviceId)
|
||||||
|
.orElseThrow(() -> new BusinessException(ErrorCode.SERVICE_NOT_FOUND));
|
||||||
|
|
||||||
|
SnpServiceApi api = snpServiceApiRepository.findById(apiId)
|
||||||
|
.filter(a -> a.getService().getServiceId().equals(serviceId))
|
||||||
|
.orElseThrow(() -> new BusinessException(ErrorCode.SERVICE_API_NOT_FOUND));
|
||||||
|
|
||||||
|
SnpServiceApiSpec spec = snpServiceApiSpecRepository.findByApiApiId(apiId)
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (spec != null) {
|
||||||
|
spec.update(request.sampleUrl(), request.sampleCode(),
|
||||||
|
request.requestBodyExample(), request.responseBodyExample(),
|
||||||
|
request.authRequired(), request.authType(), request.deprecated(),
|
||||||
|
request.dataFormat(), request.referenceUrl(), request.note());
|
||||||
|
} else {
|
||||||
|
spec = SnpServiceApiSpec.builder()
|
||||||
|
.api(api)
|
||||||
|
.sampleUrl(request.sampleUrl())
|
||||||
|
.sampleCode(request.sampleCode())
|
||||||
|
.requestBodyExample(request.requestBodyExample())
|
||||||
|
.responseBodyExample(request.responseBodyExample())
|
||||||
|
.authRequired(request.authRequired())
|
||||||
|
.authType(request.authType())
|
||||||
|
.deprecated(request.deprecated())
|
||||||
|
.dataFormat(request.dataFormat())
|
||||||
|
.referenceUrl(request.referenceUrl())
|
||||||
|
.note(request.note())
|
||||||
|
.build();
|
||||||
|
snpServiceApiSpecRepository.save(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("API 명세 저장 완료: apiId={}", apiId);
|
||||||
|
return ApiSpecResponse.from(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 파라미터 전체 교체
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public List<ApiParamResponse> saveApiParams(Long serviceId, Long apiId, List<SaveApiParamRequest> requests) {
|
||||||
|
snpServiceRepository.findById(serviceId)
|
||||||
|
.orElseThrow(() -> new BusinessException(ErrorCode.SERVICE_NOT_FOUND));
|
||||||
|
|
||||||
|
SnpServiceApi api = snpServiceApiRepository.findById(apiId)
|
||||||
|
.filter(a -> a.getService().getServiceId().equals(serviceId))
|
||||||
|
.orElseThrow(() -> new BusinessException(ErrorCode.SERVICE_API_NOT_FOUND));
|
||||||
|
|
||||||
|
snpServiceApiParamRepository.deleteByApiApiId(apiId);
|
||||||
|
snpServiceApiParamRepository.flush();
|
||||||
|
|
||||||
|
List<SnpServiceApiParam> params = requests.stream()
|
||||||
|
.map(req -> SnpServiceApiParam.builder()
|
||||||
|
.api(api)
|
||||||
|
.paramType(req.paramType())
|
||||||
|
.paramName(req.paramName())
|
||||||
|
.paramMeaning(req.paramMeaning())
|
||||||
|
.paramDescription(req.paramDescription())
|
||||||
|
.required(req.required())
|
||||||
|
.defaultValue(req.defaultValue())
|
||||||
|
.inputType(req.inputType())
|
||||||
|
.sortOrder(req.sortOrder())
|
||||||
|
.build())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
List<SnpServiceApiParam> saved = snpServiceApiParamRepository.saveAll(params);
|
||||||
|
log.info("API 파라미터 저장 완료: apiId={}, count={}", apiId, saved.size());
|
||||||
|
return saved.stream().map(ApiParamResponse::from).toList();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 서비스 API 생성
|
* 서비스 API 생성
|
||||||
*/
|
*/
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user