diff --git a/frontend/src/pages/admin/ServicesPage.tsx b/frontend/src/pages/admin/ServicesPage.tsx index 49de803..256fd2f 100644 --- a/frontend/src/pages/admin/ServicesPage.tsx +++ b/frontend/src/pages/admin/ServicesPage.tsx @@ -1,8 +1,573 @@ +import { useState, useEffect } from 'react'; +import type { + ServiceInfo, + ServiceApi, + CreateServiceRequest, + UpdateServiceRequest, + CreateServiceApiRequest, +} from '../../types/service'; +import { + getServices, + createService, + updateService, + getServiceApis, + createServiceApi, +} from '../../services/serviceService'; + +const HEALTH_BADGE: Record = { + UP: { dot: 'bg-green-500', bg: 'bg-green-100', text: 'text-green-800' }, + DOWN: { dot: 'bg-red-500', bg: 'bg-red-100', text: 'text-red-800' }, + UNKNOWN: { dot: 'bg-gray-400', bg: 'bg-gray-100', text: 'text-gray-800' }, +}; + +const METHOD_COLOR: Record = { + GET: 'bg-green-100 text-green-800', + POST: 'bg-blue-100 text-blue-800', + PUT: 'bg-orange-100 text-orange-800', + DELETE: 'bg-red-100 text-red-800', +}; + +const formatRelativeTime = (dateStr: string | null): string => { + if (!dateStr) return '-'; + const diff = Date.now() - new Date(dateStr).getTime(); + const seconds = Math.floor(diff / 1000); + if (seconds < 60) return `${seconds}초 전`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}분 전`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}시간 전`; + const days = Math.floor(hours / 24); + return `${days}일 전`; +}; + const ServicesPage = () => { + const [services, setServices] = useState([]); + const [selectedService, setSelectedService] = useState(null); + const [serviceApis, setServiceApis] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [isServiceModalOpen, setIsServiceModalOpen] = useState(false); + const [editingService, setEditingService] = useState(null); + const [serviceCode, setServiceCode] = useState(''); + const [serviceName, setServiceName] = useState(''); + const [serviceUrl, setServiceUrl] = useState(''); + const [serviceDescription, setServiceDescription] = useState(''); + const [healthCheckUrl, setHealthCheckUrl] = useState(''); + const [healthCheckInterval, setHealthCheckInterval] = useState(60); + const [serviceIsActive, setServiceIsActive] = useState(true); + + const [isApiModalOpen, setIsApiModalOpen] = useState(false); + const [apiMethod, setApiMethod] = useState('GET'); + const [apiPath, setApiPath] = useState(''); + const [apiName, setApiName] = useState(''); + const [apiDescription, setApiDescription] = useState(''); + + const fetchServices = async () => { + try { + setLoading(true); + const res = await getServices(); + if (res.success && res.data) { + setServices(res.data); + } else { + setError(res.message || '서비스 목록을 불러오는데 실패했습니다.'); + } + } catch { + setError('서비스 목록을 불러오는데 실패했습니다.'); + } finally { + setLoading(false); + } + }; + + const fetchApis = async (serviceId: number) => { + try { + const res = await getServiceApis(serviceId); + if (res.success && res.data) { + setServiceApis(res.data); + } + } catch { + setServiceApis([]); + } + }; + + useEffect(() => { + fetchServices(); + }, []); + + const handleSelectService = (service: ServiceInfo) => { + setSelectedService(service); + fetchApis(service.serviceId); + }; + + const handleOpenCreateService = () => { + setEditingService(null); + setServiceCode(''); + setServiceName(''); + setServiceUrl(''); + setServiceDescription(''); + setHealthCheckUrl(''); + setHealthCheckInterval(60); + setServiceIsActive(true); + setIsServiceModalOpen(true); + }; + + const handleOpenEditService = (service: ServiceInfo) => { + setEditingService(service); + setServiceCode(service.serviceCode); + setServiceName(service.serviceName); + setServiceUrl(service.serviceUrl || ''); + setServiceDescription(service.description || ''); + setHealthCheckUrl(service.healthCheckUrl || ''); + setHealthCheckInterval(service.healthCheckInterval); + setServiceIsActive(service.isActive); + setIsServiceModalOpen(true); + }; + + const handleCloseServiceModal = () => { + setIsServiceModalOpen(false); + setEditingService(null); + setError(null); + }; + + const handleServiceSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + try { + if (editingService) { + const req: UpdateServiceRequest = { + serviceName, + serviceUrl: serviceUrl || undefined, + description: serviceDescription || undefined, + healthCheckUrl: healthCheckUrl || undefined, + healthCheckInterval, + isActive: serviceIsActive, + }; + const res = await updateService(editingService.serviceId, req); + if (!res.success) { + setError(res.message || '서비스 수정에 실패했습니다.'); + return; + } + } else { + const req: CreateServiceRequest = { + serviceCode, + serviceName, + serviceUrl: serviceUrl || undefined, + description: serviceDescription || undefined, + healthCheckUrl: healthCheckUrl || undefined, + healthCheckInterval, + }; + const res = await createService(req); + if (!res.success) { + setError(res.message || '서비스 생성에 실패했습니다.'); + return; + } + } + handleCloseServiceModal(); + await fetchServices(); + } catch { + setError(editingService ? '서비스 수정에 실패했습니다.' : '서비스 생성에 실패했습니다.'); + } + }; + + const handleOpenCreateApi = () => { + setApiMethod('GET'); + setApiPath(''); + setApiName(''); + setApiDescription(''); + setIsApiModalOpen(true); + }; + + const handleCloseApiModal = () => { + setIsApiModalOpen(false); + setError(null); + }; + + const handleApiSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedService) return; + setError(null); + + try { + const req: CreateServiceApiRequest = { + apiMethod, + apiPath, + apiName, + description: apiDescription || undefined, + }; + const res = await createServiceApi(selectedService.serviceId, req); + if (!res.success) { + setError(res.message || 'API 생성에 실패했습니다.'); + return; + } + handleCloseApiModal(); + await fetchApis(selectedService.serviceId); + } catch { + setError('API 생성에 실패했습니다.'); + } + }; + + if (loading) { + return
로딩 중...
; + } + return (
-

Services

-

Coming soon

+
+

Services

+ +
+ + {error && !isServiceModalOpen && !isApiModalOpen && ( +
{error}
+ )} + +
+ + + + + + + + + + + + + + + {services.map((service) => { + const badge = HEALTH_BADGE[service.healthStatus] || HEALTH_BADGE.UNKNOWN; + const isSelected = selectedService?.serviceId === service.serviceId; + return ( + handleSelectService(service)} + className={`cursor-pointer ${ + isSelected ? 'bg-blue-50' : 'hover:bg-gray-50' + }`} + > + + + + + + + + + + ); + })} + {services.length === 0 && ( + + + + )} + +
CodeNameURLHealth StatusResponse TimeLast CheckedActiveActions
{service.serviceCode}{service.serviceName} + {service.serviceUrl || '-'} + + + + {service.healthStatus} + + + {service.healthResponseTime != null + ? `${service.healthResponseTime}ms` + : '-'} + + {formatRelativeTime(service.healthCheckedAt)} + + + {service.isActive ? 'Active' : 'Inactive'} + + + +
+ 등록된 서비스가 없습니다. +
+
+ + {selectedService && ( +
+
+

+ APIs for {selectedService.serviceName} +

+ +
+
+ + + + + + + + + + + + {serviceApis.map((api) => ( + + + + + + + + ))} + {serviceApis.length === 0 && ( + + + + )} + +
MethodPathNameDescriptionActive
+ + {api.apiMethod} + + {api.apiPath}{api.apiName}{api.description || '-'} + + {api.isActive ? 'Active' : 'Inactive'} + +
+ 등록된 API가 없습니다. +
+
+
+ )} + + {isServiceModalOpen && ( +
+
+
+

+ {editingService ? '서비스 수정' : '서비스 생성'} +

+
+
+
+ {error && ( +
{error}
+ )} +
+ + setServiceCode(e.target.value)} + disabled={!!editingService} + required + className="w-full border rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:bg-gray-100 disabled:text-gray-500" + /> +
+
+ + setServiceName(e.target.value)} + required + className="w-full border rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none" + /> +
+
+ + setServiceUrl(e.target.value)} + className="w-full border rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none" + /> +
+
+ +