import { useState, useEffect, useCallback, useMemo } from 'react'; import { bypassApi, type BypassConfigRequest, type BypassConfigResponse, type CodeGenerationResult, type WebClientBeanInfo, } from '../api/bypassApi'; import { useToastContext } from '../contexts/ToastContext'; import BypassConfigModal from '../components/bypass/BypassConfigModal'; import ConfirmModal from '../components/ConfirmModal'; import InfoModal from '../components/InfoModal'; import LoadingSpinner from '../components/LoadingSpinner'; interface ConfirmAction { type: 'delete' | 'generate'; config: BypassConfigResponse; } type ViewMode = 'card' | 'table'; const HTTP_METHOD_COLORS: Record = { GET: 'bg-emerald-100 text-emerald-700', POST: 'bg-blue-100 text-blue-700', PUT: 'bg-amber-100 text-amber-700', DELETE: 'bg-red-100 text-red-700', }; export default function BypassConfig() { const { showToast } = useToastContext(); const [configs, setConfigs] = useState([]); const [loading, setLoading] = useState(true); const [webclientBeans, setWebclientBeans] = useState([]); const [viewMode, setViewMode] = useState('table'); const [searchTerm, setSearchTerm] = useState(''); const [selectedDomain, setSelectedDomain] = useState(''); const [modalOpen, setModalOpen] = useState(false); const [editConfig, setEditConfig] = useState(null); const [confirmAction, setConfirmAction] = useState(null); const [generationResult, setGenerationResult] = useState(null); const [codeGenEnabled, setCodeGenEnabled] = useState(true); const loadConfigs = useCallback(async () => { try { const res = await bypassApi.getConfigs(); setConfigs(res.data ?? []); } catch (err) { showToast('Bypass API 목록 조회 실패', 'error'); console.error(err); } finally { setLoading(false); } }, [showToast]); useEffect(() => { loadConfigs(); bypassApi.getWebclientBeans() .then((res) => setWebclientBeans(res.data ?? [])) .catch((err) => console.error(err)); fetch('/snp-api/api/bypass-config/environment') .then(res => res.json()) .then(res => setCodeGenEnabled(res.data?.codeGenerationEnabled ?? true)) .catch(() => {}); }, [loadConfigs]); const handleCreate = () => { setEditConfig(null); setModalOpen(true); }; const handleEdit = (config: BypassConfigResponse) => { setEditConfig(config); setModalOpen(true); }; const handleSave = async (data: BypassConfigRequest) => { if (editConfig) { await bypassApi.updateConfig(editConfig.id, data); showToast('Bypass API가 수정되었습니다.', 'success'); } else { await bypassApi.createConfig(data); showToast('Bypass API가 등록되었습니다.', 'success'); } await loadConfigs(); }; const handleDeleteConfirm = async () => { if (!confirmAction || confirmAction.type !== 'delete') return; try { await bypassApi.deleteConfig(confirmAction.config.id); showToast('Bypass API가 삭제되었습니다.', 'success'); await loadConfigs(); } catch (err) { showToast('삭제 실패', 'error'); console.error(err); } finally { setConfirmAction(null); } }; const handleGenerateConfirm = async () => { if (!confirmAction || confirmAction.type !== 'generate') return; const targetConfig = confirmAction.config; setConfirmAction(null); try { const res = await bypassApi.generateCode(targetConfig.id, targetConfig.generated); setGenerationResult(res.data); showToast('코드가 생성되었습니다.', 'success'); await loadConfigs(); } catch (err) { showToast('코드 생성 실패', 'error'); console.error(err); } }; const domainNames = useMemo(() => { const names = [...new Set(configs.map((c) => c.domainName))]; return names.sort(); }, [configs]); const filteredConfigs = useMemo(() => { return configs.filter((c) => { const matchesSearch = !searchTerm.trim() || c.domainName.toLowerCase().includes(searchTerm.toLowerCase()) || c.displayName.toLowerCase().includes(searchTerm.toLowerCase()); const matchesDomain = !selectedDomain || c.domainName === selectedDomain; return matchesSearch && matchesDomain; }); }, [configs, searchTerm, selectedDomain]); if (loading) return ; return (
{/* 헤더 */}

Bypass API 관리

외부 Maritime API를 직접 프록시하는 Bypass API를 등록하고 코드를 생성합니다.

{/* 검색 + 뷰 전환 */}
{/* 검색 */}
setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-wing-border rounded-lg text-sm focus:ring-2 focus:ring-wing-accent focus:border-wing-accent outline-none bg-wing-surface text-wing-text" /> {searchTerm && ( )}
{/* 도메인 드롭다운 필터 */} {/* 뷰 전환 토글 */}
{(searchTerm || selectedDomain) && (

{filteredConfigs.length}개 API 검색됨

)}
{/* 빈 상태 */} {configs.length === 0 ? (

등록된 BYPASS API가 없습니다.

위 버튼을 눌러 새 API를 등록하세요.

) : filteredConfigs.length === 0 ? (

검색 결과가 없습니다.

다른 검색어를 사용해 보세요.

) : viewMode === 'card' ? ( /* 카드 뷰 */
{filteredConfigs.map((config) => (
{/* 카드 헤더 */}

{config.displayName}

{config.domainName}

{config.generated ? '생성 완료' : '미생성'}
{/* 카드 정보 */}
{config.httpMethod} {config.externalPath}

WebClient:{' '} {config.webclientBean}

{config.description && (

{config.description}

)}
{/* 카드 액션 */}
))}
) : ( /* 테이블 뷰 */
{filteredConfigs.map((config) => ( ))}
도메인명 표시명 HTTP 메서드 WebClient 외부 경로 생성 상태 등록일 액션
{config.domainName} {config.displayName} {config.httpMethod} {config.webclientBean} {config.externalPath} {config.generated ? '생성 완료' : '미생성'} {config.createdAt ? new Date(config.createdAt).toLocaleDateString('ko-KR') : '-'}
)} {/* 등록/수정 모달 */} setModalOpen(false)} /> {/* 삭제 확인 모달 */} setConfirmAction(null)} /> {/* 코드 생성 확인 모달 */} setConfirmAction(null)} /> {/* 코드 생성 결과 모달 */} setGenerationResult(null)} > {generationResult && (

{generationResult.message}

생성된 파일

Controller {generationResult.controllerPath}
{generationResult.servicePaths.map((path, idx) => (
Service {generationResult.servicePaths.length > 1 ? idx + 1 : ''} {path}
))}
서버를 재시작하면 새 API가 활성화됩니다.
)}
); }