diff --git a/TestMasterUI/src/App.jsx b/TestMasterUI/src/App.jsx index 12a234f..75da808 100644 --- a/TestMasterUI/src/App.jsx +++ b/TestMasterUI/src/App.jsx @@ -29,18 +29,22 @@ const AppContent = ({ options, showLogin, setShowLogin }) => { showAddScenario: false, showVariableModal: false, showMockRouteEdit: false, + showTCPServerEdit: false, + showQueueEdit: false, selectedItem: null, }); const handleMenuClick = async (action, screen, item = null) => { + console.log(action); + console.log(item); + if (screen) { console.log('Screen:', screen); setCurrentScreen(screen); return; } - console.log(item); // Handle modal actions switch (action) { case 'request-add': @@ -49,11 +53,15 @@ const AppContent = ({ options, showLogin, setShowLogin }) => { case 'response-add': case 'response-edit': case 'response-clone': - setDialogState(prev => ({ - ...prev, - showMockRouteEdit: true, - selectedItem: item // Pass the selected item - })); + setDialogState(prev => ({...prev, showMockRouteEdit: true, selectedItem: item })); + break; + case 'queue-add': + case 'queue-edit': + setDialogState(prev => ({ ...prev, showQueueEdit: true , selectedItem: item})); + break; + case 'socket-add': + case 'socket-edit': + setDialogState(prev => ({ ...prev, showTCPServerEdit: true , selectedItem: item})); break; case 'collection-add': setDialogState(prev => ({ ...prev, showAddCollection: true })); @@ -122,6 +130,8 @@ const AppContent = ({ options, showLogin, setShowLogin }) => { showAddScenario={dialogState.showAddScenario} showVariableModal={dialogState.showVariableModal} showMockRouteEdit={dialogState.showMockRouteEdit} + showTCPServerEdit={dialogState.showTCPServerEdit} + showQueueEdit={dialogState.showQueueEdit} selectedItem={dialogState.selectedItem} onDialogChange={handleDialogChange} /> diff --git a/TestMasterUI/src/components/mock-route/MockRouteEditDialog.jsx b/TestMasterUI/src/components/mock-route/MockRouteEditDialog.jsx index d3775f1..ca4a44d 100644 --- a/TestMasterUI/src/components/mock-route/MockRouteEditDialog.jsx +++ b/TestMasterUI/src/components/mock-route/MockRouteEditDialog.jsx @@ -229,7 +229,7 @@ const MockRouteEditDialog = ({open, onOpenChange, item}) => { return ( - + {mockRoute.id == null ? 'API 정보 등록' : 'API 정보 수정'} diff --git a/TestMasterUI/src/components/queues/QueueEditDialog.jsx b/TestMasterUI/src/components/queues/QueueEditDialog.jsx index e69de29..22914c3 100644 --- a/TestMasterUI/src/components/queues/QueueEditDialog.jsx +++ b/TestMasterUI/src/components/queues/QueueEditDialog.jsx @@ -0,0 +1,173 @@ +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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import MonacoEditor from '@/components/shared/MonacoEditor'; +import {getCSRFToken} from '@/services/token.js'; + +const defaultScript = ` +//message를 사용 가능 +var URL = Java.type('java.net.URL'); +var HttpURLConnection = Java.type('java.net.HttpURLConnection'); +var BufferedReader = Java.type('java.io.BufferedReader'); +var InputStreamReader = Java.type('java.io.InputStreamReader'); +var StringBuilder = Java.type('java.lang.StringBuilder'); + +try { + + var url = new URL('http://localhost:38080/'); + var connection = url.openConnection(); + connection.setRequestMethod('POST'); + connection.setDoOutput(true); + + // Write the body + var outputStream = connection.getOutputStream(); + var body = 'async_result' + message; + outputStream.write(new java.lang.String(body).getBytes('UTF-8')); + outputStream.close(); + + console.log(body); + // Read the response + var responseCode = connection.getResponseCode(); + if (responseCode === 200) { + var inputStream = connection.getInputStream(); + var reader = new BufferedReader(new InputStreamReader(inputStream)); + var builder = new StringBuilder(); + var line; + + while ((line = reader.readLine()) !== null) { + builder.append(line); + builder.append('\\n'); + } + + reader.close(); + console.log('POST Response:' + builder.toString()); + } else { + console.log('HTTP Error! Status:' + responseCode); + } +} catch (error) { + console.log('Error making POST request:' + error); +} +`; + +const QueueEditDialog = ({ open, onOpenChange, item }) => { + const [queueHandler, setQueueHandler] = useState({ + queueName: '', + handlerScript: defaultScript + }); + + // Initialize form when item changes + useEffect(() => { + if (item) { + setQueueHandler(item); + } else { + // Reset to default state when creating new + setQueueHandler({ + queueName: '', + handlerScript: defaultScript + }); + } + }, [item]); + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!queueHandler.queueName || !queueHandler.handlerScript) { + return; + } + + try { + const url = item ? `/api/queue-handlers/update/${item.queueName}` : '/api/queue-handlers'; + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-XSRF-TOKEN': getCSRFToken() + }, + body: JSON.stringify(queueHandler) + }); + + if (!response.ok) { + throw new Error('Failed to save queue handler'); + } + + onOpenChange(false); + } catch (error) { + console.error('Failed to save queue handler:', error); + alert('Failed to save queue handler'); + } + }; + + const handleChange = (field, value) => { + setQueueHandler(prev => ({ ...prev, [field]: value })); + }; + + return ( + + + + + {item ? '큐 핸들러 수정' : '큐 핸들러 등록'} + + + 큐 핸들러의 정보를 입력하거나 수정할 수 있습니다. 필수 항목을 모두 입력해주세요. + + + + +
+ {/* Queue Name */} +
+ + handleChange('queueName', e.target.value)} + disabled={!!item} // Disable editing of queue name for existing handlers + /> +
+ + {/* Handler Script */} + + + 핸들러 스크립트 + + + handleChange('handlerScript', value)} + height="full" + /> + + +
+
+ + + + + +
+
+ ); +}; + +export default QueueEditDialog; diff --git a/TestMasterUI/src/components/queues/QueueList.jsx b/TestMasterUI/src/components/queues/QueueList.jsx index e69de29..87334f8 100644 --- a/TestMasterUI/src/components/queues/QueueList.jsx +++ b/TestMasterUI/src/components/queues/QueueList.jsx @@ -0,0 +1,126 @@ +import React, { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import DataTable from '@/components/shared/DataTable'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { useAPI } from '@/providers/APIProvider'; + +const QueueList = ({ onMenuClick }) => { + const { queues, loading, loadQueues, deleteQueues, startQueue, stopQueue } = useAPI(); + const [searchTerm, setSearchTerm] = useState(''); + const [selectedIds, setSelectedIds] = useState([]); + const [page, setPage] = useState(0); + + useEffect(() => { + loadQueues({ page }); + }, [page, loadQueues]); + + const handleSearch = () => { + setPage(0); + loadQueues({ page: 0, queueName: searchTerm }); + }; + + const handleDelete = async () => { + if (!selectedIds.length) return; + const success = await deleteQueues(selectedIds); + if (success) { + setSelectedIds([]); + } + }; + + const columns = [ + { key: 'queueName', label: '큐 이름' }, + { + key: 'active', + label: '상태', + render: (value) => ( + + {value ? 'Active' : 'Inactive'} + + ) + } + ]; + + const renderRowActions = (row) => ( +
+ + + +
+ ); + + const tableActions = ( + <> + + + + + ); + + return ( +
+ + + 큐 목록 + + +
+
+ setSearchTerm(e.target.value)} + onKeyUp={(e) => { + if (e.key === 'Enter') { + handleSearch(); + } + }} + /> +
+ {tableActions} +
+ +
+
+
+ ); +}; + +export default QueueList; diff --git a/TestMasterUI/src/components/shared/DataTable.jsx b/TestMasterUI/src/components/shared/DataTable.jsx index 36c8f0c..f3c90ed 100644 --- a/TestMasterUI/src/components/shared/DataTable.jsx +++ b/TestMasterUI/src/components/shared/DataTable.jsx @@ -1,9 +1,18 @@ import React from 'react'; -import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from '@/components/ui/table'; -import {Button} from '@/components/ui/button'; -import {Card, CardContent} from '@/components/ui/card'; -import {Checkbox} from '@/components/ui/checkbox'; -import {AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle} from '@/components/ui/alert-dialog'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { Checkbox } from '@/components/ui/checkbox'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle +} from '@/components/ui/alert-dialog'; const DataTable = ({ // Data and columns @@ -28,34 +37,33 @@ const DataTable = ({ deleteDialogDescription = 'Are you sure you want to delete the selected items? This action cannot be undone.', onDelete }) => { - const [showDeleteDialog, setShowDeleteDialog] = React.useState(false); - const handleSelectAll = (checked) => { + const handleSelectAll = React.useCallback((checked) => { if (!onSelectIds) return; if (checked) { - onSelectIds(data.content.map(item => item.id)); + onSelectIds(data.content.map(item => item.id || item.queueName)); } else { onSelectIds([]); } - }; + }, [data.content, onSelectIds]); - const handleSelectItem = (id, checked) => { + const handleSelectItem = React.useCallback((id, checked) => { if (!onSelectIds) return; if (checked) { onSelectIds([...selectedIds, id]); } else { onSelectIds(selectedIds.filter(selectedId => selectedId !== id)); } - }; + }, [selectedIds, onSelectIds]); - const handlePageChange = (page) => { + const handlePageChange = React.useCallback((page) => { // Reset selections when changing pages if (onSelectIds) { onSelectIds([]); } onPageChange(page); - }; + }, [onSelectIds, onPageChange]); if (loading) { return ( @@ -82,34 +90,37 @@ const DataTable = ({ )} {columns.map((column) => ( - {column.label} + {column.label} ))} - {renderRowActions && Actions} + {renderRowActions && Actions} - {data.content.map((row) => ( - - {selectable && ( - - handleSelectItem(row.id, checked)} - /> - - )} - {columns.map((column) => ( - - {column.render ? column.render(row[column.key], row) : row[column.key]} - - ))} - {renderRowActions && ( - - {renderRowActions(row)} - - )} - - ))} + {data.content.map((row) => { + const rowId = row.id || row.queueName; + return ( + + {selectable && ( + + handleSelectItem(rowId, checked)} + /> + + )} + {columns.map((column) => ( + + {column.render ? column.render(row[column.key], row) : row[column.key]} + + ))} + {renderRowActions && ( + + {renderRowActions(row)} + + )} + + ); + })} @@ -117,7 +128,7 @@ const DataTable = ({ {onPageChange && (
- Showing {data.numberOfElements} of {data.totalElements} results + 전체 {data.totalElements} 중 {data.numberOfElements} 표시
{Array.from({ length: data.totalPages }, (_, i) => ( + + + +
+ ); +}; + +export default TCPServerEditDialog; diff --git a/TestMasterUI/src/components/tcp-servers/TCPServerList.jsx b/TestMasterUI/src/components/tcp-servers/TCPServerList.jsx index e69de29..bb1e340 100644 --- a/TestMasterUI/src/components/tcp-servers/TCPServerList.jsx +++ b/TestMasterUI/src/components/tcp-servers/TCPServerList.jsx @@ -0,0 +1,209 @@ +import React, { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import DataTable from '@/components/shared/DataTable'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { format } from 'date-fns'; + +const TCPServerList = ({ onMenuClick }) => { + const [servers, setServers] = useState({ + content: [], + totalPages: 0, + number: 0, + first: true, + last: true, + numberOfElements: 0, + totalElements: 0 + }); + const [loading, setLoading] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const [selectedIds, setSelectedIds] = useState([]); + const [page, setPage] = useState(0); + + const loadServers = async (params = { page: 0 }) => { + setLoading(true); + try { + const searchParams = new URLSearchParams({ + page: params.page, + size: 10, + ...(searchTerm && { name: searchTerm }) + }); + + const response = await fetch(`/api/socket?${searchParams}`); + const data = await response.json(); + setServers(data); + } catch (error) { + console.error('Error loading servers:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadServers({ page }); + }, [page]); + + const handleSearch = () => { + setPage(0); + loadServers({ page: 0 }); + }; + + const handleDelete = async () => { + if (!selectedIds.length) return; + + try { + const response = await fetch('/api/socket/delete', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content') + }, + body: JSON.stringify(selectedIds) + }); + + if (response.ok) { + setSelectedIds([]); + loadServers({ page }); + } + } catch (error) { + console.error('Error deleting servers:', error); + } + }; + + const handleStartServer = async (id) => { + try { + const response = await fetch(`/api/socket/${id}/start`, { + method: 'POST', + headers: { + 'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content') + } + }); + + if (response.ok) { + loadServers({ page }); + } + } catch (error) { + console.error('Error starting server:', error); + } + }; + + const handleStopServer = async (id) => { + try { + const response = await fetch(`/api/socket/${id}/stop`, { + method: 'POST', + headers: { + 'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content') + } + }); + + if (response.ok) { + loadServers({ page }); + } + } catch (error) { + console.error('Error stopping server:', error); + } + }; + + const columns = [ + { key: 'name', label: 'Name' }, + { key: 'port', label: 'Port' }, + { + key: 'active', + label: 'Status', + render: (value) => ( + + {value ? 'Active' : 'Inactive'} + + ) + }, + { + key: 'lastModified', + label: 'Last Modified', + render: (value) => format(new Date(value), 'yyyy-MM-dd HH:mm:ss') + } + ]; + + const renderRowActions = (row) => ( +
+ + + +
+ ); + + const tableActions = ( + <> + + + + + ); + + return ( +
+ + + TCP Server List + + +
+
+ setSearchTerm(e.target.value)} + onKeyUp={(e) => { + if (e.key === 'Enter') { + handleSearch(); + } + }} + /> +
+ {tableActions} +
+ +
+
+
+ ); +}; + +export default TCPServerList; diff --git a/TestMasterUI/src/config/menuConfig.js b/TestMasterUI/src/config/menuConfig.js index 1451cb5..68914d7 100644 --- a/TestMasterUI/src/config/menuConfig.js +++ b/TestMasterUI/src/config/menuConfig.js @@ -7,8 +7,8 @@ export const menuConfig = [ {id: 'response-list', label: 'API 응답 목록', action: 'response-list', screen: 'response-list'}, {id: 'response-add', label: 'API 응답 추가', action: 'response-add'}, {type: 'separator'}, - {id: 'tcp-list', label: 'TCP 서버 목록', action: 'tcp-list'}, - {id: 'tcp-add', label: 'TCP 서버 추가', action: 'tcp-add'}, + {id: 'socket-list', label: 'TCP 서버 목록', action: 'socket-list', screen: 'socket-list'}, + {id: 'socket-add', label: 'TCP 서버 추가', action: 'socket-add'}, ] }, { @@ -29,7 +29,7 @@ export const menuConfig = [ label: '큐 관리', items: [ {id: 'queue-list', label: '큐 목록', action: 'queue-list', screen: 'queue-list'}, - {id: 'queue-add', label: '큐 추가', action: 'queue-add', screen: 'queue-add'} + {id: 'queue-add', label: '큐 추가', action: 'queue-add'} ] }, { diff --git a/TestMasterUI/src/config/screenConfig.js b/TestMasterUI/src/config/screenConfig.js index d3c94f8..ec557ed 100644 --- a/TestMasterUI/src/config/screenConfig.js +++ b/TestMasterUI/src/config/screenConfig.js @@ -2,9 +2,13 @@ import APITester from '../components/api-tester/APITester'; import LoadTestManager from '../components/load-tester/LoadTestManager.jsx'; import MockRouteList from '../components/mock-route/MockRouteList.jsx'; +import TCPServerList from '@/components/tcp-servers/TCPServerList.jsx'; +import QueueList from '@/components/queues/QueueList.jsx'; export const SCREENS = { 'api-tester': APITester, 'load-test': LoadTestManager, 'response-list': MockRouteList, + 'socket-list': TCPServerList, + 'queue-list': QueueList, }; diff --git a/TestMasterUI/src/providers/APIProvider.jsx b/TestMasterUI/src/providers/APIProvider.jsx index 9fe4672..bc22ae5 100644 --- a/TestMasterUI/src/providers/APIProvider.jsx +++ b/TestMasterUI/src/providers/APIProvider.jsx @@ -4,6 +4,7 @@ import {apiRequestService} from '@/services/apiRequestService'; import {serverService} from '@/services/serverService'; import {authService} from '@/services/authService'; import APIClient from '@/services/APIClient.js'; +import {queueService} from '@/services/queueService.js'; const APIContext = createContext(null); @@ -34,11 +35,21 @@ export const APIProvider = ({ last: true }); + const [queues, setQueues] = useState({ + content: [], + totalPages: 0, + number: 0, + first: true, + last: true + }); + + // Initialize services const collections$ = collectionService(contextPath); const apiRequests$ = apiRequestService(contextPath); const servers$ = serverService(); const auth$ = authService(); + const queues$ = queueService(contextPath); const handleLogout = useCallback(async () => { try { @@ -292,6 +303,67 @@ export const APIProvider = ({ }, [loadMockRoutes]); + const loadQueues = useCallback(async (params = { page: 0 }) => { + setLoading(true); + setError(null); + try { + const data = await queues$.loadQueues(params); + setQueues(data); + return data; + } catch (err) { + setError(err.message); + return null; + } finally { + setLoading(false); + } + }, []); + + const deleteQueues = useCallback(async (ids) => { + setLoading(true); + setError(null); + try { + await queues$.deleteQueues(ids); + await loadQueues(); + return true; + } catch (err) { + setError(err.message); + return false; + } finally { + setLoading(false); + } + }, [loadQueues]); + + const startQueue = useCallback(async (id) => { + setLoading(true); + setError(null); + try { + await queues$.startQueue(id); + await loadQueues(); + return true; + } catch (err) { + setError(err.message); + return false; + } finally { + setLoading(false); + } + }, [loadQueues]); + + const stopQueue = useCallback(async (id) => { + setLoading(true); + setError(null); + try { + await queues$.stopQueue(id); + await loadQueues(); + return true; + } catch (err) { + setError(err.message); + return false; + } finally { + setLoading(false); + } + }, [loadQueues]); + + const contextValue = { apiClient, collections, @@ -316,7 +388,12 @@ export const APIProvider = ({ loadMockRoutes, deleteMockRoutes, createMockRoute, - updateMockRoute + updateMockRoute, + queues, + loadQueues, + deleteQueues, + startQueue, + stopQueue }; return ( diff --git a/TestMasterUI/src/services/queueService.js b/TestMasterUI/src/services/queueService.js new file mode 100644 index 0000000..c2c0681 --- /dev/null +++ b/TestMasterUI/src/services/queueService.js @@ -0,0 +1,73 @@ +// services/queueService.js +import { getCSRFToken } from '@/services/token.js'; + +export const queueService = (contextPath = '/') => { + const headers = { + 'Content-Type': 'application/json', + 'X-XSRF-TOKEN': getCSRFToken() + }; + + return { + async loadQueues(params = { page: 0 }) { + const searchParams = new URLSearchParams({ + page: params.page, + size: 10, + ...(params.queueName && { queueName: params.queueName }) + }); + + const response = await fetch(`${contextPath}api/queue-handlers?${searchParams}`); + if (!response.ok) throw new Error('Failed to fetch queues'); + return await response.json(); + }, + + async createQueue(queue) { + const response = await fetch(`${contextPath}api/queue-handlers`, { + method: 'POST', + headers, + body: JSON.stringify(queue) + }); + + if (!response.ok) throw new Error('Failed to create queue'); + return await response.json(); + }, + + async updateQueue(id, queue) { + const response = await fetch(`${contextPath}api/queue-handlers/update/${id}`, { + method: 'POST', + headers, + body: JSON.stringify(queue) + }); + + if (!response.ok) throw new Error('Failed to update queue'); + return await response.json(); + }, + + async deleteQueues(ids) { + const response = await fetch(`${contextPath}api/queue-handlers/delete`, { + method: 'POST', + headers, + body: JSON.stringify(ids) + }); + + if (!response.ok) throw new Error('Failed to delete queues'); + }, + + async startQueue(id) { + const response = await fetch(`${contextPath}api/queue-handlers/${id}/start`, { + method: 'POST', + headers + }); + + if (!response.ok) throw new Error('Failed to start queue'); + }, + + async stopQueue(id) { + const response = await fetch(`${contextPath}api/queue-handlers/${id}/stop`, { + method: 'POST', + headers + }); + + if (!response.ok) throw new Error('Failed to stop queue'); + } + }; +};