From aace8759b80f34028b2eb100cdd6064f26240ef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=98=84=EC=84=B1=ED=95=84?= Date: Fri, 31 Jan 2025 17:32:52 +0900 Subject: [PATCH] =?UTF-8?q?=EC=84=9C=EB=B2=84=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TestMasterUI/src/App.jsx | 58 ++-- .../components/servers/ServerEditDialog.jsx | 295 ++++++++++++++++++ .../src/components/servers/ServerList.jsx | 127 ++++++++ .../src/components/shared/DialogManager.jsx | 158 +++++++--- TestMasterUI/src/config/menuConfig.js | 10 +- TestMasterUI/src/config/screenConfig.js | 2 + TestMasterUI/src/providers/APIProvider.jsx | 73 +++++ TestMasterUI/src/services/serverService.js | 56 ++++ 8 files changed, 701 insertions(+), 78 deletions(-) diff --git a/TestMasterUI/src/App.jsx b/TestMasterUI/src/App.jsx index 76ddd99..f25d096 100644 --- a/TestMasterUI/src/App.jsx +++ b/TestMasterUI/src/App.jsx @@ -1,26 +1,26 @@ -import React, { useEffect, useState } from 'react'; -import { APIProvider, useAPI } from './providers/APIProvider'; -import { LoadTestProvider } from './providers/LoadTestProvider'; -import DialogManager from './components/shared/DialogManager'; -import { monacoConfig } from '@/config/monaco.js'; +import React, {useEffect, useState} from 'react'; +import {APIProvider, useAPI} from './providers/APIProvider'; +import {LoadTestProvider} from './providers/LoadTestProvider'; +import DialogManager from '@/components/shared/DialogManager'; +import {monacoConfig} from '@/config/monaco.js'; import LoginDialog from '@/components/account/LoginDialog.jsx'; import AppMenu from './components/app-menu/AppMenu'; -import { SCREENS } from './config/screenConfig'; +import {SCREENS} from './config/screenConfig'; window.MonacoEnvironment = monacoConfig; -const App = ({ options = { contextPath: '/' } }) => { +const App = ({options = {contextPath: '/'}}) => { const [showLogin, setShowLogin] = useState(false); return ( - + ); }; -const AppContent = ({ options, showLogin, setShowLogin }) => { - const { isAuthenticated, setIsAuthenticated, handleLogout } = useAPI(); +const AppContent = ({options, showLogin, setShowLogin}) => { + const {isAuthenticated, setIsAuthenticated, handleLogout} = useAPI(); const [isInitialized, setIsInitialized] = useState(false); const [currentScreen, setCurrentScreen] = useState('api-tester'); const [dialogState, setDialogState] = useState({ @@ -33,7 +33,8 @@ const AppContent = ({ options, showLogin, setShowLogin }) => { showQueueEdit: false, showUserEdit: false, showPasswordChange: false, - selectedItem: null, + showServerEdit: false, + selectedItem: null }); const handleMenuClick = async (action, screen, item = null) => { @@ -50,41 +51,45 @@ const AppContent = ({ options, showLogin, setShowLogin }) => { // Handle modal actions switch (action) { case 'request-add': - setDialogState(prev => ({ ...prev, showAddApi: true })); + setDialogState(prev => ({...prev, showAddApi: true})); break; case 'response-add': case 'response-edit': case 'response-clone': - setDialogState(prev => ({...prev, showMockRouteEdit: true, selectedItem: item })); + setDialogState(prev => ({...prev, showMockRouteEdit: true, selectedItem: item})); break; case 'queue-add': case 'queue-edit': - setDialogState(prev => ({ ...prev, showQueueEdit: true , selectedItem: item})); + setDialogState(prev => ({...prev, showQueueEdit: true, selectedItem: item})); break; case 'user-add': case 'user-edit': - setDialogState(prev => ({ ...prev, showUserEdit: true , selectedItem: item})); + setDialogState(prev => ({...prev, showUserEdit: true, selectedItem: item})); break; case 'socket-add': case 'socket-edit': - setDialogState(prev => ({ ...prev, showTCPServerEdit: true , selectedItem: item})); + setDialogState(prev => ({...prev, showTCPServerEdit: true, selectedItem: item})); + break; + case 'server-add': + case 'server-edit': + setDialogState(prev => ({...prev, showServerEdit: true, selectedItem: item})); break; case 'collection-add': - setDialogState(prev => ({ ...prev, showAddCollection: true })); + setDialogState(prev => ({...prev, showAddCollection: true})); break; case 'scenario-add': - setDialogState(prev => ({ ...prev, showAddScenario: true })); + setDialogState(prev => ({...prev, showAddScenario: true})); break; case 'variables-show': - setDialogState(prev => ({ ...prev, showVariableModal: true })); + setDialogState(prev => ({...prev, showVariableModal: true})); break; case 'change-password': - setDialogState(prev => ({ ...prev, showPasswordChange: true })); + setDialogState(prev => ({...prev, showPasswordChange: true})); break; case 'logout': const success = await handleLogout(); if (success) { - window.location.href = "/actionLogout.do"; + window.location.href = `${options.contextPath}/actionLogout.do`; } break; } @@ -117,14 +122,14 @@ const AppContent = ({ options, showLogin, setShowLogin }) => { setIsInitialized(true); checkAuth(); } - }, [isInitialized]); + }, [isInitialized, checkAuth]); const handleDialogChange = (dialogName, isOpen) => { - setDialogState(prev => ({ ...prev, [dialogName]: isOpen })); + setDialogState(prev => ({...prev, [dialogName]: isOpen})); }; if (!isAuthenticated) { - return ; + return ; } const CurrentScreenComponent = SCREENS[currentScreen] || SCREENS['api-tester']; @@ -132,7 +137,7 @@ const AppContent = ({ options, showLogin, setShowLogin }) => { return (
- + { showUserEdit={dialogState.showUserEdit} selectedItem={dialogState.selectedItem} showPasswordChange={dialogState.showPasswordChange} + showServerEdit={dialogState.showServerEdit} onDialogChange={handleDialogChange} />
- +
diff --git a/TestMasterUI/src/components/servers/ServerEditDialog.jsx b/TestMasterUI/src/components/servers/ServerEditDialog.jsx index e69de29..e90bd02 100644 --- a/TestMasterUI/src/components/servers/ServerEditDialog.jsx +++ b/TestMasterUI/src/components/servers/ServerEditDialog.jsx @@ -0,0 +1,295 @@ +import React, { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle +} from '@/components/ui/dialog'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Checkbox } from '@/components/ui/checkbox'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from '@/components/ui/table'; +import { useAPI } from '@/providers/APIProvider'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Plus, Trash2 } from 'lucide-react'; + +const DEFAULT_SERVER = { + id: null, + name: '', + scheme: 'http', + hostname: '', + port: 80, + basePath: '/', + enabled: true, + serverOptions: [] +}; + +const ServerEditDialog = ({ open, onOpenChange, item }) => { + const { createServer, updateServer } = useAPI(); + const [server, setServer] = useState(DEFAULT_SERVER); + + // useEffect(() => { + // if (item) { + // // Ensure serverOptions is always an array + // setServer({ + // ...item, + // serverOptions: item.serverOptions || [] + // }); + // } else { + // setServer(DEFAULT_SERVER); + // } + // }, [item]); + useEffect(() => { + console.log('Dialog useEffect - item:', item, 'open:', open); // Debug log + if (!open) { + setServer(DEFAULT_SERVER); + return; + } + + if (item) { + setServer({ + ...item, + serverOptions: item.serverOptions || [] + }); + } else { + setServer(DEFAULT_SERVER); + } + }, [item, open]); + + const handleChange = (field, value) => { + setServer(prev => ({ ...prev, [field]: value })); + }; + + const addOption = () => { + setServer(prev => ({ + ...prev, + serverOptions: [ + ...(prev.serverOptions || []), + { + optionKey: '', + optionName: '', + offset: 0, + length: 0, + defaultValue: '' + } + ] + })); + }; + + const removeOption = (index) => { + setServer(prev => { + const newOptions = [...prev.serverOptions]; + newOptions.splice(index, 1); + return { ...prev, serverOptions: newOptions }; + }); + }; + + const updateOptionField = (index, field, value) => { + setServer(prev => { + const newOptions = [...prev.serverOptions]; + newOptions[index] = { ...newOptions[index], [field]: value }; + return { ...prev, serverOptions: newOptions }; + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + try { + if (server.id) { + await updateServer(server); + } else { + await createServer(server); + } + onOpenChange(false); + } catch (error) { + console.error('Failed to save server:', error); + alert('Failed to save server'); + } + }; + + const handleDialogChange = (open) => { + if (!open) { + setServer(DEFAULT_SERVER); + } + onOpenChange(open); + }; + + return ( + + + + + {server.id == null ? '서버 등록' : '서버 수정'} + + + 서버의 정보를 입력하거나 수정할 수 있습니다. 필수 항목을 모두 입력해주세요. + + + + +
+ {/* Server Name */} +
+ + handleChange('name', e.target.value)} + /> +
+ + {/* Server Address */} +
+ +
+ + :// + handleChange('hostname', e.target.value)} + /> + : + handleChange('port', parseInt(e.target.value))} + /> + handleChange('basePath', e.target.value)} + /> +
+
+ + {/* Enabled Status */} +
+ handleChange('enabled', checked)} + /> + +
+ + {/* Server Options */} +
+
+ + +
+ + + + + + 이름 + 메시지 위치 + 메시지 길이 + 기본 값 + + + + + {(server.serverOptions || []).map((option, index) => ( + + + updateOptionField(index, 'optionKey', e.target.value)} + /> + + + updateOptionField(index, 'optionName', e.target.value)} + /> + + + updateOptionField(index, 'offset', parseInt(e.target.value))} + /> + + + updateOptionField(index, 'length', parseInt(e.target.value))} + /> + + + updateOptionField(index, 'defaultValue', e.target.value)} + /> + + + + + + ))} + +
+
+
+
+ + + + + +
+
+ ); +}; + +export default ServerEditDialog; diff --git a/TestMasterUI/src/components/servers/ServerList.jsx b/TestMasterUI/src/components/servers/ServerList.jsx index e69de29..34f54bc 100644 --- a/TestMasterUI/src/components/servers/ServerList.jsx +++ b/TestMasterUI/src/components/servers/ServerList.jsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { Button } from '@/components/ui/button'; +import DataTable from '@/components/shared/DataTable'; +import { useAPI } from '@/providers/APIProvider'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; + +const ServerList = ({ onMenuClick }) => { + const { + serverList, + loadServerList, + deleteServers, + loading + } = useAPI(); + + const [searchTerm, setSearchTerm] = React.useState(''); + const [selectedIds, setSelectedIds] = React.useState([]); + const [page, setPage] = React.useState(0); + + React.useEffect(() => { + loadServerList({ page }); + }, [page, loadServerList]); + + const columns = [ + { key: 'name', label: '이름' }, + { + key: 'address', + label: '주소', + render: (_, row) => `${row.scheme}://${row.hostname}:${row.port}${row.basePath}` + }, + { + key: 'enabled', + label: '사용 중', + render: (value) => ( + + {value ? '사용' : '미사용'} + + ) + } + ]; + + const handleSearch = () => { + setPage(0); + loadServerList({ name: searchTerm, page: 0 }); + }; + + const handleDelete = async () => { + const success = await deleteServers(selectedIds); + if (success) { + setSelectedIds([]); + } + }; + + const renderRowActions = (row) => ( + <> + + + ); + + const tableActions = ( + <> + + + + + ); + + return ( +
+ + + 서버 목록 + + +
+
+ setSearchTerm(e.target.value)} + onKeyUp={(e) => { + if (e.key === 'Enter') { + handleSearch(); + } + }} + /> +
+ {tableActions} +
+ +
+
+
+ ); +}; + +export default ServerList; diff --git a/TestMasterUI/src/components/shared/DialogManager.jsx b/TestMasterUI/src/components/shared/DialogManager.jsx index fe97139..33a6e1e 100644 --- a/TestMasterUI/src/components/shared/DialogManager.jsx +++ b/TestMasterUI/src/components/shared/DialogManager.jsx @@ -1,5 +1,5 @@ -import AddAPIDialog from '@/components/api-tester/AddAPIDialog.jsx'; import React from 'react'; +import AddAPIDialog from '@/components/api-tester/AddAPIDialog.jsx'; import AddCollectionDialog from '@/components/api-tester/AddCollectionDialog.jsx'; import AddScenarioDialog from '@/components/api-tester/AddScenarioDialog.jsx'; import VariableModal from '@/components/api-tester/VariableModal.jsx'; @@ -8,7 +8,71 @@ import TCPServerEditDialog from '@/components/tcp-servers/TCPServerEditDialog.js import QueueEditDialog from '@/components/queues/QueueEditDialog.jsx'; import UserEditDialog from '@/components/users/UserEditDialog.jsx'; import PasswordChangeDialog from '@/components/account/PasswordChangeDialog.jsx'; +import ServerEditDialog from '@/components/servers/ServerEditDialog.jsx'; +// const DialogManager = ({ +// showAddApi, +// showAddCollection, +// showAddScenario, +// showVariableModal, +// showMockRouteEdit, +// showTCPServerEdit, +// showQueueEdit, +// showUserEdit, +// selectedItem, +// showPasswordChange, +// showServerEdit, +// onDialogChange +// }) => { +// +// return <> +// onDialogChange('showAddCollection', isOpen)} +// /> +// onDialogChange('showAddApi', isOpen)} +// /> +// onDialogChange('showAddScenario', isOpen)} +// /> +// onDialogChange('showVariableModal', isOpen)} +// /> +// onDialogChange('showMockRouteEdit', isOpen)} +// item={selectedItem} +// /> +// onDialogChange('showTCPServerEdit', isOpen)} +// item={selectedItem} +// /> +// onDialogChange('showQueueEdit', isOpen)} +// item={selectedItem} +// /> +// onDialogChange('showUserEdit', isOpen)} +// item={selectedItem} +// /> +// onDialogChange('showPasswordChange', isOpen)} +// /> +// onDialogChange('showServerEdit', isOpen)} +// item={selectedItem} +// /> +// ; +// } const DialogManager = ({ showAddApi, showAddCollection, @@ -18,53 +82,55 @@ const DialogManager = ({ showTCPServerEdit, showQueueEdit, showUserEdit, + showServerEdit, selectedItem, - showPasswordChange, onDialogChange }) => { - - return <> - onDialogChange('showAddCollection', isOpen)} - /> - onDialogChange('showAddApi', isOpen)} - /> - onDialogChange('showAddScenario', isOpen)} - /> - onDialogChange('showVariableModal', isOpen)} - /> - onDialogChange('showMockRouteEdit', isOpen)} - item={selectedItem} - /> - onDialogChange('showTCPServerEdit', isOpen)} - item={selectedItem} - /> - onDialogChange('showQueueEdit', isOpen)} - item={selectedItem} - /> - onDialogChange('showUserEdit', isOpen)} - item={selectedItem} - /> - onDialogChange('showPasswordChange', isOpen)} - /> - ; -} + return ( + <> + onDialogChange('showAddCollection', isOpen)} + /> + onDialogChange('showAddApi', isOpen)} + /> + onDialogChange('showAddScenario', isOpen)} + /> + onDialogChange('showVariableModal', isOpen)} + /> + onDialogChange('showMockRouteEdit', isOpen)} + item={selectedItem} + /> + onDialogChange('showTCPServerEdit', isOpen)} + item={selectedItem} + /> + onDialogChange('showQueueEdit', isOpen)} + item={selectedItem} + /> + onDialogChange('showUserEdit', isOpen)} + item={selectedItem} + /> + onDialogChange('showServerEdit', isOpen)} + item={selectedItem} + /> + + ); +}; export default DialogManager; diff --git a/TestMasterUI/src/config/menuConfig.js b/TestMasterUI/src/config/menuConfig.js index 3114398..eda1aa0 100644 --- a/TestMasterUI/src/config/menuConfig.js +++ b/TestMasterUI/src/config/menuConfig.js @@ -5,10 +5,10 @@ export const menuConfig = [ label: '응답관리', items: [ {id: 'response-list', label: 'API 응답 목록', action: 'response-list', screen: 'response-list'}, - {id: 'response-add', label: 'API 응답 추가', action: 'response-add'}, - {type: 'separator'}, + // {id: 'response-add', label: 'API 응답 추가', action: 'response-add'}, + // {type: 'separator'}, {id: 'socket-list', label: 'TCP 서버 목록', action: 'socket-list', screen: 'socket-list'}, - {id: 'socket-add', label: 'TCP 서버 추가', action: 'socket-add'}, + // {id: 'socket-add', label: 'TCP 서버 추가', action: 'socket-add'}, ] }, { @@ -47,9 +47,7 @@ export const menuConfig = [ label: '시스템 관리', items: [ {id: 'user-list', label: '사용자 관리', action: 'user-list', screen: 'user-list'}, - {type: 'separator'}, - {id: 'server-management', label: '서버 관리', action: 'server-management', screen: 'server-management'}, - {id: 'server-add', label: '서버 추가', action: 'server-add', screen: 'server-add'}, + {id: 'server-list', label: '서버 관리', action: 'server-list', screen: 'server-list'}, ] }, { diff --git a/TestMasterUI/src/config/screenConfig.js b/TestMasterUI/src/config/screenConfig.js index 9f30999..5bf8cb0 100644 --- a/TestMasterUI/src/config/screenConfig.js +++ b/TestMasterUI/src/config/screenConfig.js @@ -5,6 +5,7 @@ import MockRouteList from '../components/mock-route/MockRouteList.jsx'; import TCPServerList from '@/components/tcp-servers/TCPServerList.jsx'; import QueueList from '@/components/queues/QueueList.jsx'; import UserList from '@/components/users/UserList.jsx'; +import ServerList from '@/components/servers/ServerList.jsx'; export const SCREENS = { 'api-tester': APITester, @@ -13,4 +14,5 @@ export const SCREENS = { 'socket-list': TCPServerList, 'queue-list': QueueList, 'user-list': UserList, + 'server-list': ServerList }; diff --git a/TestMasterUI/src/providers/APIProvider.jsx b/TestMasterUI/src/providers/APIProvider.jsx index 5ed379d..8719a2c 100644 --- a/TestMasterUI/src/providers/APIProvider.jsx +++ b/TestMasterUI/src/providers/APIProvider.jsx @@ -52,6 +52,14 @@ export const APIProvider = ({ last: true }); + const [serverList, setServerList] = useState({ + content: [], + totalPages: 0, + number: 0, + first: true, + last: true + }); + // Initialize services const collections$ = collectionService(contextPath); @@ -447,6 +455,66 @@ export const APIProvider = ({ } }, []); + const loadServerList = useCallback(async (params = { page: 0 }) => { + setLoading(true); + setError(null); + try { + const data = await servers$.loadServerList(params); + setServerList(data); + return data; + } catch (err) { + setError(err.message); + return null; + } finally { + setLoading(false); + } + }, []); + + const deleteServers = useCallback(async (ids) => { + setLoading(true); + setError(null); + try { + await servers$.deleteServers(ids); + await loadServerList(); + return true; + } catch (err) { + setError(err.message); + return false; + } finally { + setLoading(false); + } + }, [loadServerList]); + + const createServer = useCallback(async (server) => { + setLoading(true); + setError(null); + try { + const newServer = await servers$.createServer(server); + await loadServerList(); + return newServer; + } catch (err) { + setError(err.message); + return null; + } finally { + setLoading(false); + } + }, [loadServerList]); + + const updateServer = useCallback(async (server) => { + setLoading(true); + setError(null); + try { + const updatedServer = await servers$.updateServer(server.id, server); + await loadServerList(); + return updatedServer; + } catch (err) { + setError(err.message); + return null; + } finally { + setLoading(false); + } + }, [loadServerList]); + const contextValue = { apiClient, collections, @@ -454,6 +522,11 @@ export const APIProvider = ({ servers, loadServers, getServer, + serverList, + loadServerList, + deleteServers, + createServer, + updateServer, loading, error, loadCollections, diff --git a/TestMasterUI/src/services/serverService.js b/TestMasterUI/src/services/serverService.js index 06d881f..aee780b 100644 --- a/TestMasterUI/src/services/serverService.js +++ b/TestMasterUI/src/services/serverService.js @@ -1,3 +1,5 @@ +import {getCSRFToken} from '@/services/token.js'; + export const serverService = () => { return { async loadServers() { @@ -13,6 +15,60 @@ export const serverService = () => { basePath: server?.basePath || '', environment: server?.environment || 'unknown' })); + }, + + async loadServerList(params = { page: 0, name: '' }) { + const searchParams = new URLSearchParams({ + page: params.page, + size: 10, + ...(params.name && { name: params.name }) + }); + + const response = await fetch(`/servers?${searchParams}`); + if (!response.ok) throw new Error('Failed to load servers'); + return response.json(); + }, + + async createServer(server) { + const response = await fetch('/servers', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-XSRF-TOKEN': getCSRFToken() + }, + body: JSON.stringify(server) + }); + + if (!response.ok) throw new Error('Failed to create server'); + return response.json(); + }, + + async updateServer(id, server) { + const response = await fetch(`/servers/update/${id}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-XSRF-TOKEN': getCSRFToken() + }, + body: JSON.stringify(server) + }); + + if (!response.ok) throw new Error('Failed to update server'); + return response.json(); + }, + + async deleteServers(ids) { + const response = await fetch('/servers/delete', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-XSRF-TOKEN': getCSRFToken() + }, + body: JSON.stringify(ids) + }); + + if (!response.ok) throw new Error('Failed to delete servers'); + return response.json(); } }; };