diff --git a/TestMasterUI/package.json b/TestMasterUI/package.json index 70817c3..ed7ff7d 100644 --- a/TestMasterUI/package.json +++ b/TestMasterUI/package.json @@ -53,7 +53,6 @@ "globals": "^15.14.0", "postcss": "^8.4.49", "tailwindcss": "^3.4.17", - "vite": "^6.0.5", - "vite-plugin-monaco-editor": "^1.1.0" + "vite": "^6.0.5" } } diff --git a/TestMasterUI/src/App.jsx b/TestMasterUI/src/App.jsx index 5253cea..12a234f 100644 --- a/TestMasterUI/src/App.jsx +++ b/TestMasterUI/src/App.jsx @@ -1,16 +1,15 @@ -import React, {useEffect, useState} from 'react'; -import {APIProvider, useAPI} from './providers/APIProvider'; -import {LoadTestProvider} from './providers/LoadTestProvider'; -import APITester from './components/APITester'; -import LoadTestManager from './components/LoadTestManager'; -import DialogManager from './components/DialogManager'; -import {monacoConfig} from '@/config/monaco.js'; -import LoginDialog from '@/components/LoginDialog.jsx'; -import {Menubar, MenubarContent, MenubarItem, MenubarMenu, MenubarSeparator, MenubarTrigger} from '@/components/ui/menubar.jsx'; +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'; window.MonacoEnvironment = monacoConfig; -const App = ({ options = { type: 'api', contextPath: '/' } }) => { +const App = ({ options = { contextPath: '/' } }) => { const [showLogin, setShowLogin] = useState(false); return ( @@ -20,102 +19,50 @@ const App = ({ options = { type: 'api', contextPath: '/' } }) => { ); }; -const menuConfig = [ - { - id: 'response', - label: '응답관리', - items: [ - {id: 'response-list', label: '응답목록', action: 'response-list'}, - {id: 'response-add', label: '응답추가', action: 'response-add'} - ] - }, - { - id: 'request', - label: '요청관리', - items: [ - {id: 'collection-add', label: '컬렉션 추가', action: 'collection-add'}, - {id: 'request-add', label: '요청 추가', action: 'request-add'}, - {id: 'scenario-add', label: '시나리오 추가', action: 'scenario-add'}, - {id: 'variables-show', label: '변수 보기', action: 'variables-show'}, - ] - }, - { - id: 'queue', - label: '큐관리', - items: [ - {id: 'queue-status', label: '큐상태', action: 'queue-status'}, - {id: 'queue-settings', label: '큐설정', action: 'queue-settings'} - ] - }, - { - id: 'loadtest', - label: '부하테스트', - items: [ - {id: 'load-test-new', label: '새 테스트', action: 'load-test-new'}, - {id: 'load-test-history', label: '테스트 이력', action: 'load-test-history'}, - {id: 'load-test-reports', label: '결과 리포트', action: 'load-test-reports'} - ] - }, - { - id: 'system', - label: '시스템관리', - items: [ - {id: 'system-settings', label: '시스템설정', action: 'system-settings'}, - {id: 'user-management', label: '사용자관리', action: 'user-management'}, - {type: 'separator'}, - {id: 'system-logs', label: '시스템로그', action: 'system-logs'} - ] - }, - { - id: 'account', - label: '계정', - items: [ - {id: 'change-password', label: '비밀번호변경', action: 'change-password'}, - {type: 'separator'}, - {id: 'logout', label: '로그아웃', action: 'logout'} - ] - } -]; - -const MenuItem = ({item, onMenuClick}) => { - if (item.type === 'separator') { - return ; - } - - return ( - onMenuClick(item.action)}> - {item.label} - - ); -}; - 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({ showAddApi: false, showAddCollection: false, showAddScenario: false, - showVariableModal: false + showVariableModal: false, + showMockRouteEdit: false, + selectedItem: null, }); - const handleMenuClick = async (action) => { + const handleMenuClick = async (action, screen, item = null) => { - console.log('Menu action:', action); + if (screen) { + console.log('Screen:', screen); + setCurrentScreen(screen); + return; + } + + console.log(item); + // Handle modal actions switch (action) { case 'request-add': setDialogState(prev => ({ ...prev, showAddApi: true })); break; + case 'response-add': + case 'response-edit': + case 'response-clone': + setDialogState(prev => ({ + ...prev, + showMockRouteEdit: true, + selectedItem: item // Pass the selected item + })); + break; case 'collection-add': setDialogState(prev => ({ ...prev, showAddCollection: true })); break; case 'scenario-add': setDialogState(prev => ({ ...prev, showAddScenario: true })); - break; case 'variables-show': setDialogState(prev => ({ ...prev, showVariableModal: true })); - break; case 'logout': const success = await handleLogout(); @@ -123,10 +70,6 @@ const AppContent = ({ options, showLogin, setShowLogin }) => { window.location.href = "/actionLogout.do"; } break; - case 'change-password': - // Handle password change - break; - // Add other cases as needed } }; @@ -167,53 +110,27 @@ const AppContent = ({ options, showLogin, setShowLogin }) => { return ; } + const CurrentScreenComponent = SCREENS[currentScreen] || SCREENS['api-tester']; + return (
-
-
-
- testmaster -
- - {menuConfig.map((menu) => ( - - - {menu.label} - - - {menu.items.map((item, index) => ( - - ))} - - - ))} - -
-
+ - {options.type === 'api' ? ( - - ) : ( - - )} +
+ +
); }; -export default App +export default App; diff --git a/TestMasterUI/src/components/LoginDialog.jsx b/TestMasterUI/src/components/account/LoginDialog.jsx similarity index 100% rename from TestMasterUI/src/components/LoginDialog.jsx rename to TestMasterUI/src/components/account/LoginDialog.jsx diff --git a/TestMasterUI/src/components/account/PasswordEditDialog.jsx b/TestMasterUI/src/components/account/PasswordEditDialog.jsx new file mode 100644 index 0000000..e69de29 diff --git a/TestMasterUI/src/components/APITester.jsx b/TestMasterUI/src/components/api-tester/APITester.jsx similarity index 83% rename from TestMasterUI/src/components/APITester.jsx rename to TestMasterUI/src/components/api-tester/APITester.jsx index 58beed8..c61ba05 100644 --- a/TestMasterUI/src/components/APITester.jsx +++ b/TestMasterUI/src/components/api-tester/APITester.jsx @@ -1,5 +1,5 @@ import React, {useState} from 'react'; -import MainContentArea from '@/components/MainContentArea.jsx'; +import MainContentArea from '@/components/api-tester/MainContentArea.jsx'; const APITester = () => { // API 모드와 시나리오 모드 상태를 관리 @@ -13,7 +13,7 @@ const APITester = () => { return (
{/* Main content area with top padding for menubar */} -
+
diff --git a/TestMasterUI/src/components/api-tester/AddAPIDialog.jsx b/TestMasterUI/src/components/api-tester/AddAPIDialog.jsx index 3c7f639..dd8cd41 100644 --- a/TestMasterUI/src/components/api-tester/AddAPIDialog.jsx +++ b/TestMasterUI/src/components/api-tester/AddAPIDialog.jsx @@ -7,8 +7,8 @@ import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/c import ErrorAlertDialog from '@/components/shared/ErrorAlertDialog'; import {useAPI} from '@/providers/APIProvider'; -const AddAPIDialog = ({open, onOpenChange, onSubmit, selectedCollection}) => { - const {collections, loadCollections} = useAPI(); +const AddAPIDialog = ({open, onOpenChange, selectedCollection}) => { + const {collections, loadCollections, createAPIRequest} = useAPI(); const [formData, setFormData] = useState({ name: '', type: '', @@ -19,6 +19,15 @@ const AddAPIDialog = ({open, onOpenChange, onSubmit, selectedCollection}) => { const [showErrorDialog, setShowErrorDialog] = useState(false); const [isInitialized, setIsInitialized] = useState(false); + const handleAddApi = async (collectionId, apiData) => { + try { + await createAPIRequest(collectionId, apiData); + return true; + } catch (error) { + console.error('Failed to create API:', error); + return false; + } + }; useEffect(() => { if (selectedCollection?.id) { @@ -56,7 +65,7 @@ const AddAPIDialog = ({open, onOpenChange, onSubmit, selectedCollection}) => { setIsSubmitting(true); try { - const success = await onSubmit(formData.collectionId, { + const success = await handleAddApi(formData.collectionId, { name: formData.name.trim(), type: formData.type }); diff --git a/TestMasterUI/src/components/api-tester/AddCollectionDialog.jsx b/TestMasterUI/src/components/api-tester/AddCollectionDialog.jsx index fb0f161..cfc4b00 100644 --- a/TestMasterUI/src/components/api-tester/AddCollectionDialog.jsx +++ b/TestMasterUI/src/components/api-tester/AddCollectionDialog.jsx @@ -1,23 +1,26 @@ -// /components/AddCollectionDialog.jsx -import React, { useState } from 'react'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; +import React, {useState} from 'react'; +import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from '@/components/ui/dialog'; +import {Button} from '@/components/ui/button'; +import {Input} from '@/components/ui/input'; +import {Label} from '@/components/ui/label'; import ErrorAlertDialog from '@/components/shared/ErrorAlertDialog.jsx'; +import {useAPI} from '@/providers/APIProvider.jsx'; -const AddCollectionDialog = ({ open, onOpenChange, onSubmit }) => { +const AddCollectionDialog = ({ open, onOpenChange}) => { const [collectionName, setCollectionName] = useState(''); const [error, setError] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const [showErrorDialog, setShowErrorDialog] = useState(false); + const {createCollection} = useAPI(); + + const handleAddCollection = async (name) => { + try { + await createCollection(name); + return true; + } catch (error) { + throw new Error('컬렉션 생성에 실패했습니다'); + } + }; const handleSubmit = async (e) => { e.preventDefault(); @@ -29,7 +32,7 @@ const AddCollectionDialog = ({ open, onOpenChange, onSubmit }) => { setIsSubmitting(true); try { - const success = await onSubmit(collectionName.trim()); + const success = await handleAddCollection(collectionName.trim()); if (success) { setCollectionName(''); setError(''); diff --git a/TestMasterUI/src/components/MainContentArea.jsx b/TestMasterUI/src/components/api-tester/MainContentArea.jsx similarity index 100% rename from TestMasterUI/src/components/MainContentArea.jsx rename to TestMasterUI/src/components/api-tester/MainContentArea.jsx diff --git a/TestMasterUI/src/components/app-menu/AppMenu.jsx b/TestMasterUI/src/components/app-menu/AppMenu.jsx new file mode 100644 index 0000000..2d45883 --- /dev/null +++ b/TestMasterUI/src/components/app-menu/AppMenu.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Menubar, MenubarContent, MenubarMenu, MenubarTrigger } from '@/components/ui/menubar'; +import MenuItem from './MenuItem'; +import { menuConfig } from '@/config/menuConfig'; + +const AppMenu = ({ onMenuClick }) => { + return ( +
+
+
+ testmaster +
+ + {menuConfig.map((menu) => ( + + + {menu.label} + + + {menu.items.map((item, index) => ( + + ))} + + + ))} + +
+
+ ); +}; + +export default AppMenu; diff --git a/TestMasterUI/src/components/app-menu/MenuItem.jsx b/TestMasterUI/src/components/app-menu/MenuItem.jsx new file mode 100644 index 0000000..c896557 --- /dev/null +++ b/TestMasterUI/src/components/app-menu/MenuItem.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { MenubarItem, MenubarSeparator } from '@/components/ui/menubar'; + +const MenuItem = ({ item, onMenuClick }) => { + if (item.type === 'separator') { + return ; + } + + return ( + onMenuClick(item.action, item.screen)}> + {item.label} + + ); +}; + +export default MenuItem; diff --git a/TestMasterUI/src/components/client-views/HTTPClientView.jsx b/TestMasterUI/src/components/client-views/HTTPClientView.jsx index 79169fa..a3c75f2 100644 --- a/TestMasterUI/src/components/client-views/HTTPClientView.jsx +++ b/TestMasterUI/src/components/client-views/HTTPClientView.jsx @@ -5,7 +5,7 @@ import {Input} from '@/components/ui/input'; import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs'; import EditableInput from '../shared/EditableInput'; import PropertyTable from '../shared/PropertyTable'; -import ServerManager from '@/components/ServerManager.jsx'; +import ServerManager from '@/components/shared/ServerManager'; import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from '@/components/ui/table.jsx'; import {useAPI} from '@/providers/APIProvider'; import MonacoEditor from '@/components/shared/MonacoEditor'; diff --git a/TestMasterUI/src/components/client-views/TCPClientView.jsx b/TestMasterUI/src/components/client-views/TCPClientView.jsx index b9d696a..4938bbb 100644 --- a/TestMasterUI/src/components/client-views/TCPClientView.jsx +++ b/TestMasterUI/src/components/client-views/TCPClientView.jsx @@ -1,10 +1,10 @@ import React, {useCallback, useEffect, useState} from 'react'; import {Button} from '@/components/ui/button'; import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs'; -import ServerManager from '@/components/ServerManager.jsx'; +import ServerManager from '@/components/shared/ServerManager'; import {useAPI} from '@/providers/APIProvider'; import MonacoEditor from '@/components/shared/MonacoEditor'; -import APIClient from '@/services/APIClient.js'; +import APIClient from '@/services/APIClient'; import {SidebarTrigger} from '@/components/ui/sidebar.jsx'; import {Separator} from '@/components/ui/separator.jsx'; import LayoutGrid from '@/components/layout/LayoutGrid.jsx'; @@ -233,6 +233,7 @@ export const TCPClientView = ({model, index}) => {
+ {/* Response Section */}
@@ -242,13 +243,13 @@ export const TCPClientView = ({model, index}) => { {responseData.size}
- + Body Console -
+
{ + const { createMockRoute, updateMockRoute } = useAPI(); + const [mockRoute, setMockRoute] = useState({ + id: null, + method: 'POST', + pathPattern: '', + name: '', + description: '', + requestScript: '', + responses: [ + { + statusCode: 200, + delay: 0, + encodingCharset: '', + defaultResponse: 'true', + conditions: [], + headers: [], + body: '' + } + ] + }); + + // Initialize form when item changes + useEffect(() => { + if (item) { + setMockRoute(item); + } else { + // Reset to default state when creating new + setMockRoute({ + id: null, + method: 'POST', + pathPattern: '', + name: '', + description: '', + requestScript: '', + responses: [ + { + statusCode: 200, + delay: 0, + encodingCharset: '', + defaultResponse: 'true', + conditions: [], + headers: [], + body: '' + } + ] + }); + } + }, [item]); + + const handleChange = (field, value) => { + setMockRoute((prev) => ({...prev, [field]: value})); + }; + + const handleScriptChange = (value) => { + setMockRoute((prev) => ({...prev, requestScript: value})); + }; + + const addResponse = () => { + setMockRoute((prev) => ({ + ...prev, + responses: [ + ...prev.responses, + { + statusCode: 200, + delay: 0, + encodingCharset: '', + defaultResponse: 'false', + conditions: [], + headers: [], + body: '' + } + ] + })); + }; + + const handleOpenChange = (open) => { + if (!open) { + setMockRoute({ + id: null, + method: 'POST', + pathPattern: '', + name: '', + description: '', + requestScript: '', + responses: [ + { + statusCode: 200, + delay: 0, + encodingCharset: '', + defaultResponse: 'true', + conditions: [], + headers: [], + body: '' + } + ] + }); + } + onOpenChange(open); + }; + + const removeResponse = (index) => { + setMockRoute((prev) => { + const newResponses = [...prev.responses]; + if (newResponses[index].defaultResponse === 'true') { + alert('기본 응답은 삭제할 수 없습니다.'); + return prev; + } + newResponses.splice(index, 1); + return {...prev, responses: newResponses}; + }); + }; + + const updateResponseField = (index, field, value) => { + setMockRoute((prev) => { + const newResponses = [...prev.responses]; + newResponses[index] = {...newResponses[index], [field]: value}; + return {...prev, responses: newResponses}; + }); + }; + + const addCondition = (respIndex) => { + setMockRoute((prev) => { + const newResponses = [...prev.responses]; + newResponses[respIndex] = { + ...newResponses[respIndex], + conditions: [ + ...newResponses[respIndex].conditions, + {type: 'query', key: '', value: ''} + ] + }; + return {...prev, responses: newResponses}; + }); + }; + + const removeCondition = (respIndex, condIndex) => { + setMockRoute((prev) => { + const newResponses = [...prev.responses]; + const newConds = [...newResponses[respIndex].conditions]; + newConds.splice(condIndex, 1); + newResponses[respIndex] = { + ...newResponses[respIndex], + conditions: newConds + }; + return {...prev, responses: newResponses}; + }); + }; + + const updateConditionField = (respIndex, condIndex, field, value) => { + setMockRoute((prev) => { + const newResponses = [...prev.responses]; + const newConds = [...newResponses[respIndex].conditions]; + newConds[condIndex] = {...newConds[condIndex], [field]: value}; + newResponses[respIndex] = { + ...newResponses[respIndex], + conditions: newConds + }; + return {...prev, responses: newResponses}; + }); + }; + + const addHeader = (respIndex) => { + setMockRoute((prev) => { + const newResponses = [...prev.responses]; + newResponses[respIndex] = { + ...newResponses[respIndex], + headers: [...newResponses[respIndex].headers, {key: '', value: ''}] + }; + return {...prev, responses: newResponses}; + }); + }; + + const removeHeader = (respIndex, hdrIndex) => { + setMockRoute((prev) => { + const newResponses = [...prev.responses]; + const newHeaders = [...newResponses[respIndex].headers]; + newHeaders.splice(hdrIndex, 1); + newResponses[respIndex] = { + ...newResponses[respIndex], + headers: newHeaders + }; + return {...prev, responses: newResponses}; + }); + }; + + const updateHeaderField = (respIndex, hdrIndex, field, value) => { + setMockRoute((prev) => { + const newResponses = [...prev.responses]; + const newHeaders = [...newResponses[respIndex].headers]; + newHeaders[hdrIndex] = {...newHeaders[hdrIndex], [field]: value}; + newResponses[respIndex] = { + ...newResponses[respIndex], + headers: newHeaders + }; + return {...prev, responses: newResponses}; + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + try { + if (mockRoute.id) { + await updateMockRoute(mockRoute); + } else { + await createMockRoute(mockRoute); + } + onOpenChange(false); + } catch (error) { + console.error('Failed to save mock route:', error); + alert('Failed to save mock route'); + } + }; + + return ( + + + + + {mockRoute.id == null ? 'API 정보 등록' : 'API 정보 수정'} + + + API 라우트의 정보를 입력하거나 수정할 수 있습니다. 필수 항목을 모두 입력해주세요. + + + + + + + + + + + + + + ); +}; + +export default MockRouteEditDialog; diff --git a/TestMasterUI/src/components/mock-route/MockRouteList.jsx b/TestMasterUI/src/components/mock-route/MockRouteList.jsx new file mode 100644 index 0000000..942d663 --- /dev/null +++ b/TestMasterUI/src/components/mock-route/MockRouteList.jsx @@ -0,0 +1,132 @@ +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.jsx'; +import {Input} from '@/components/ui/input.jsx'; + +const MockRouteList = ({onMenuClick}) => { + const { + mockRoutes, + loadMockRoutes, + deleteMockRoutes, + loading + } = useAPI(); + + const [searchTerm, setSearchTerm] = React.useState(''); + const [selectedIds, setSelectedIds] = React.useState([]); + const [page, setPage] = React.useState(0); + + React.useEffect(() => { + loadMockRoutes({page}); + }, [page, loadMockRoutes]); + + const columns = [ + {key: 'method', label: 'Method'}, + {key: 'pathPattern', label: 'Path'}, + {key: 'name', label: '이름'} + ]; + + const handleSearch = () => { + setPage(0); + loadMockRoutes({name: searchTerm, page: 0}); + }; + + const handleDelete = async () => { + const success = await deleteMockRoutes(selectedIds); + if (success) { + setSelectedIds([]); + } + }; + + const handleEdit = (item) => { + setSelectedItem(item); + setDialogState(prev => ({...prev, showMockRouteEdit: true})); + }; + + const handleCreate = () => { + setSelectedItem(null); + setDialogState(prev => ({...prev, showMockRouteEdit: true})); + }; + + const handleDialogChange = (key, value) => { + setDialogState(prev => ({...prev, [key]: value})); + if (!value) { + setSelectedItem(null); + } + }; + + const renderRowActions = (row) => ( + <> + + + + ); + + const tableActions = ( + <> + + + + + + ); + + return ( +
+ + + 응답 API 목록 + + +
+
+ setSearchTerm(e.target.value)} + onKeyUp={(e) => { + if (e.key === 'Enter') { + handleSearch(); + } + }} + /> +
+ {tableActions} +
+ +
+
+
+ ); +}; + +export default MockRouteList; diff --git a/TestMasterUI/src/components/mock-route/MockRouteRequest.jsx b/TestMasterUI/src/components/mock-route/MockRouteRequest.jsx new file mode 100644 index 0000000..b573a5e --- /dev/null +++ b/TestMasterUI/src/components/mock-route/MockRouteRequest.jsx @@ -0,0 +1,86 @@ +import React from 'react' +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.jsx' + +const MockRouteRequest=({ + name, + method, + pathPattern, + requestScript, + onChange, + onScriptChange +}) => { + return ( +
+ {/* Name */} +
+ + onChange('name', e.target.value)} + /> +
+ + {/* Method & Path */} +
+
+ + +
+
+ + onChange('pathPattern', e.target.value)} + /> +
+
+ + {/* Request Script */} + + + 요청 스크립트 + + + + + +
+ ) +} + +export default MockRouteRequest; diff --git a/TestMasterUI/src/components/mock-route/MockRouteResponse.jsx b/TestMasterUI/src/components/mock-route/MockRouteResponse.jsx new file mode 100644 index 0000000..15da259 --- /dev/null +++ b/TestMasterUI/src/components/mock-route/MockRouteResponse.jsx @@ -0,0 +1,225 @@ +import React from 'react'; +import {Button} from '@/components/ui/button'; +import {Input} from '@/components/ui/input'; +import {Card, CardContent, CardHeader} from '@/components/ui/card'; +import {Label} from '@/components/ui/label'; +import {Trash2} from 'lucide-react'; +import MonacoEditor from '@/components/shared/MonacoEditor.jsx'; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from '@/components/ui/table'; + +const MockRouteResponse = ({ + resp, + idx, + onRemoveResponse, + onAddCondition, + onRemoveCondition, + onUpdateConditionField, + onUpdateResponseField, + onAddHeader, + onRemoveHeader, + onUpdateHeaderField +}) => { + return ( +
+
+ {/* Left side - Condition card */} +
+ + +
+

조건 항목

+ +
+
+ +
+ + + + 종류 + + + + + + + + + {resp.conditions.map((cond, cIndex) => ( + + + + + + + onUpdateConditionField(idx, cIndex, 'key', e.target.value) + } + /> + + + + onUpdateConditionField(idx, cIndex, 'value', e.target.value) + } + /> + + + + + + ))} + +
+
+
+
+
+ + {/* Right side - Response Content */} +
+ + +

응답 내용

+
+ +
+ + onUpdateResponseField(idx, 'statusCode', e.target.value)} + /> +
+ +
+ + onUpdateResponseField(idx, 'delay', e.target.value)} + /> +
+ +
+ + + onUpdateResponseField(idx, 'encodingCharset', e.target.value) + } + /> +
+ +
+ +
+ onUpdateResponseField(idx, 'body', v || '')} + height="100%" + /> +
+
+ +
+ +
+ + + + Header 이름 + + + + + + + + {resp.headers.map((header, hIndex) => ( + + + + onUpdateHeaderField(idx, hIndex, 'key', e.target.value) + } + /> + + + + onUpdateHeaderField(idx, hIndex, 'value', e.target.value) + } + /> + + + + + + ))} + +
+
+
+
+
+
+
+
+ ); +}; + +export default MockRouteResponse diff --git a/TestMasterUI/src/components/mock-route/MockRouteResponses.jsx b/TestMasterUI/src/components/mock-route/MockRouteResponses.jsx new file mode 100644 index 0000000..caf51ce --- /dev/null +++ b/TestMasterUI/src/components/mock-route/MockRouteResponses.jsx @@ -0,0 +1,62 @@ +import React from 'react' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Plus } from 'lucide-react' +import { Button } from '@/components/ui/button' +import MockRouteResponse from './MockRouteResponse.jsx' + +const MockRouteResponses = ({ + responses, + onAddResponse, + onRemoveResponse, + onAddCondition, + onRemoveCondition, + onUpdateConditionField, + onUpdateResponseField, + onAddHeader, + onRemoveHeader, + onUpdateHeaderField +}) => { + return ( + + + {responses.map((resp, idx) => { + const tabValue = idx.toString() + return ( + + {resp.defaultResponse === 'true' + ? '기본 응답' + : `조건 ${idx + 1}`} + + ) + })} + + + + {responses.map((resp, idx) => { + const tabValue = idx.toString() + return ( + + {/* Render a single response with all its fields and actions */} + + + ) + })} + + ) +} + +export default MockRouteResponses diff --git a/TestMasterUI/src/components/queues/QueueEditDialog.jsx b/TestMasterUI/src/components/queues/QueueEditDialog.jsx new file mode 100644 index 0000000..e69de29 diff --git a/TestMasterUI/src/components/queues/QueueList.jsx b/TestMasterUI/src/components/queues/QueueList.jsx new file mode 100644 index 0000000..e69de29 diff --git a/TestMasterUI/src/components/servers/ServerEditDialog.jsx b/TestMasterUI/src/components/servers/ServerEditDialog.jsx new file mode 100644 index 0000000..e69de29 diff --git a/TestMasterUI/src/components/servers/ServerList.jsx b/TestMasterUI/src/components/servers/ServerList.jsx new file mode 100644 index 0000000..e69de29 diff --git a/TestMasterUI/src/components/shared/DataTable.jsx b/TestMasterUI/src/components/shared/DataTable.jsx new file mode 100644 index 0000000..36c8f0c --- /dev/null +++ b/TestMasterUI/src/components/shared/DataTable.jsx @@ -0,0 +1,195 @@ +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'; + +const DataTable = ({ + // Data and columns + data, + columns, + + // Selection + selectable = false, + selectedIds = [], + onSelectIds, + + renderRowActions, + + // Pagination + onPageChange, + + // Loading state + loading = false, + + // Delete dialog + deleteDialogTitle = 'Delete Items', + 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) => { + if (!onSelectIds) return; + if (checked) { + onSelectIds(data.content.map(item => item.id)); + } else { + onSelectIds([]); + } + }; + + const handleSelectItem = (id, checked) => { + if (!onSelectIds) return; + if (checked) { + onSelectIds([...selectedIds, id]); + } else { + onSelectIds(selectedIds.filter(selectedId => selectedId !== id)); + } + }; + + const handlePageChange = (page) => { + // Reset selections when changing pages + if (onSelectIds) { + onSelectIds([]); + } + onPageChange(page); + }; + + if (loading) { + return ( + + + Loading... + + + ); + } + + return ( +
+
+ + + + {selectable && ( + + 0} + onCheckedChange={handleSelectAll} + /> + + )} + {columns.map((column) => ( + {column.label} + ))} + {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)} + + )} + + ))} + +
+
+ + {onPageChange && ( +
+
+ Showing {data.numberOfElements} of {data.totalElements} results +
+
+ + +
+ {Array.from({ length: data.totalPages }, (_, i) => ( + + ))} +
+ + +
+
+ )} + + {onDelete && ( + + + + {deleteDialogTitle} + + {deleteDialogDescription} + + + + Cancel + { + onDelete?.(); + setShowDeleteDialog(false); + }}>Delete + + + + )} +
+ ); +}; + +export default DataTable; diff --git a/TestMasterUI/src/components/DialogManager.jsx b/TestMasterUI/src/components/shared/DialogManager.jsx similarity index 50% rename from TestMasterUI/src/components/DialogManager.jsx rename to TestMasterUI/src/components/shared/DialogManager.jsx index d4dc134..eb93d10 100644 --- a/TestMasterUI/src/components/DialogManager.jsx +++ b/TestMasterUI/src/components/shared/DialogManager.jsx @@ -3,69 +3,40 @@ import React from 'react'; import AddCollectionDialog from '@/components/api-tester/AddCollectionDialog.jsx'; import AddScenarioDialog from '@/components/api-tester/AddScenarioDialog.jsx'; import VariableModal from '@/components/api-tester/VariableModal.jsx'; -import {useLoadTest} from '@/providers/LoadTestProvider.jsx'; -import {useAPI} from '@/providers/APIProvider.jsx'; +import MockRouteEditDialog from '@/components/mock-route/MockRouteEditDialog.jsx'; const DialogManager = ({ showAddApi, showAddCollection, showAddScenario, showVariableModal, + showMockRouteEdit, + selectedItem, onDialogChange }) => { - const {createCollection, createAPIRequest} = useAPI(); - const {createScenario} = useLoadTest(); - - const handleAddCollection = async (name) => { - try { - await createCollection(name); - return true; - } catch (error) { - throw new Error('컬렉션 생성에 실패했습니다'); - } - }; - - const handleAddApi = async (collectionId, apiData) => { - try { - await createAPIRequest(collectionId, apiData); - return true; - } catch (error) { - console.error('Failed to create API:', error); - return false; - } - }; - - const handleAddScenario = async (name) => { - try { - await createScenario(name); - return true; - } catch (error) { - console.error('시나리오 생성에 실패했습니다.'); - } - } return <> onDialogChange('showAddCollection', isOpen)} - onSubmit={handleAddCollection} /> - {/* Add these at the bottom of your return statement */} onDialogChange('showAddApi', isOpen)} - onSubmit={handleAddApi} /> onDialogChange('showAddScenario', isOpen)} - onSubmit={handleAddScenario} /> onDialogChange('showVariableModal', isOpen)} /> - + onDialogChange('showMockRouteEdit', isOpen)} + item={selectedItem} + /> ; } diff --git a/TestMasterUI/src/components/shared/MonacoEditor.jsx b/TestMasterUI/src/components/shared/MonacoEditor.jsx index a210211..fed0b62 100644 --- a/TestMasterUI/src/components/shared/MonacoEditor.jsx +++ b/TestMasterUI/src/components/shared/MonacoEditor.jsx @@ -1,5 +1,17 @@ import React, { useEffect, useRef, memo } from 'react'; import * as monaco from 'monaco-editor'; +import {loader} from '@monaco-editor/react'; + +loader.config({ + paths: { + vs: '/monaco-editor/min/vs' + }, + 'vs/nls': { + availableLanguages: {}, + }, + // Specify which languages to load + languages: ['javascript', 'json', 'xml','plaintext'] +}); const MonacoEditor = memo(({ id, @@ -10,10 +22,12 @@ const MonacoEditor = memo(({ onChange = () => {} }) => { const editorRef = useRef(null); - const modelRef = useRef(null); const containerRef = useRef(null); const subscriptionRef = useRef(null); + // Ensure value is always a string + const normalizedValue = value ?? ''; + const getEditorOptions = (lang) => { const baseOptions = { automaticLayout: true, @@ -57,63 +71,91 @@ const MonacoEditor = memo(({ useEffect(() => { if (!containerRef.current) return; - const modelUri = monaco.Uri.parse(`inmemory://model-${id}`); - modelRef.current = monaco.editor.createModel(value, language, modelUri); - const options = getEditorOptions(language); - editorRef.current = monaco.editor.create(containerRef.current, { + const editor = monaco.editor.create(containerRef.current, { ...options, - model: modelRef.current + value: normalizedValue, + language: language }); - subscriptionRef.current = editorRef.current.onDidChangeModelContent(() => { - onChange(editorRef.current.getValue()); + editorRef.current = editor; + + const subscription = editor.onDidChangeModelContent(() => { + const newValue = editor.getValue(); + onChange(newValue); }); - if (language === 'json' && value) { + subscriptionRef.current = subscription; + + if (language === 'json' && normalizedValue) { try { monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ validate: true, allowComments: false, schemas: [] }); - setTimeout(() => { - const action = editorRef.current?.getAction('editor.action.formatDocument'); - if (action) action.run(); - }, 100); + + const action = editor.getAction('editor.action.formatDocument'); + if (action) { + setTimeout(() => { + action.run(); + }, 100); + } } catch (e) { console.warn('Failed to format JSON:', e); } } const resizeObserver = new ResizeObserver(() => { - editorRef.current?.layout(); + editor.layout(); }); resizeObserver.observe(containerRef.current); return () => { resizeObserver.disconnect(); - subscriptionRef.current?.dispose(); - editorRef.current?.dispose(); - if (modelRef.current && !modelRef.current.isDisposed()) { - modelRef.current.dispose(); + subscription.dispose(); + + // Ensure proper cleanup + if (editor) { + try { + const model = editor.getModel(); + if (model) { + model.dispose(); + } + editor.dispose(); + } catch (e) { + console.warn('Editor disposal error:', e); + } } }; - }, [language]); + }, [language]); // Only recreate when language changes + // Handle value updates useEffect(() => { - if (modelRef.current && !modelRef.current.isDisposed()) { - modelRef.current.setValue(value); + if (editorRef.current) { + const currentValue = editorRef.current.getValue(); + if (currentValue !== normalizedValue) { + editorRef.current.setValue(normalizedValue); + } } - }, [value]); + }, [normalizedValue]); + + // Convert height to pixel value if it's a number + const containerHeight = typeof height === 'number' + ? `${height}px` + : height.endsWith('px') ? height : `${height}px`; return (
); }); diff --git a/TestMasterUI/src/components/ServerManager.jsx b/TestMasterUI/src/components/shared/ServerManager.jsx similarity index 100% rename from TestMasterUI/src/components/ServerManager.jsx rename to TestMasterUI/src/components/shared/ServerManager.jsx diff --git a/TestMasterUI/src/components/tcp-servers/TCPServerEditDialog.jsx b/TestMasterUI/src/components/tcp-servers/TCPServerEditDialog.jsx new file mode 100644 index 0000000..e69de29 diff --git a/TestMasterUI/src/components/tcp-servers/TCPServerList.jsx b/TestMasterUI/src/components/tcp-servers/TCPServerList.jsx new file mode 100644 index 0000000..e69de29 diff --git a/TestMasterUI/src/components/users/UserEditDialog.jsx b/TestMasterUI/src/components/users/UserEditDialog.jsx new file mode 100644 index 0000000..e69de29 diff --git a/TestMasterUI/src/components/users/UserList.jsx b/TestMasterUI/src/components/users/UserList.jsx new file mode 100644 index 0000000..e69de29 diff --git a/TestMasterUI/src/config/menuConfig.js b/TestMasterUI/src/config/menuConfig.js new file mode 100644 index 0000000..1451cb5 --- /dev/null +++ b/TestMasterUI/src/config/menuConfig.js @@ -0,0 +1,64 @@ +// config/menuConfig.js +export const menuConfig = [ + { + id: 'response', + label: '응답관리', + items: [ + {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: 'request', + label: '요청 관리', + items: [ + {id: 'api-tester', label: 'API 테스터', action: 'api-tester', screen: 'api-tester'}, + {type: 'separator'}, + {id: 'collection-add', label: '컬렉션 추가', action: 'collection-add'}, + {id: 'request-add', label: '요청 추가', action: 'request-add'}, + {id: 'scenario-add', label: '시나리오 추가', action: 'scenario-add'}, + {type: 'separator'}, + {id: 'variables-show', label: '변수 보기', action: 'variables-show'} + ] + }, + { + id: 'queue', + label: '큐 관리', + items: [ + {id: 'queue-list', label: '큐 목록', action: 'queue-list', screen: 'queue-list'}, + {id: 'queue-add', label: '큐 추가', action: 'queue-add', screen: 'queue-add'} + ] + }, + { + id: 'loadtest', + label: '부하 테스트', + items: [ + {id: 'load-test', label: '새 테스트', action: 'load-test', screen: 'load-test'}, + {type: 'separator'}, + {id: 'load-test-history', label: '테스트 이력', action: 'load-test-history', screen: 'load-test-history'}, + {id: 'load-test-reports', label: '결과 리포트', action: 'load-test-reports', screen: 'load-test-reports'} + ] + }, + { + id: 'system', + label: '시스템 관리', + items: [ + {id: 'user-management', label: '사용자 관리', action: 'user-management', screen: 'user-management'}, + {type: 'separator'}, + {id: 'server-management', label: '서버 관리', action: 'server-management', screen: 'server-management'}, + {id: 'server-add', label: '서버 추가', action: 'server-add', screen: 'server-add'}, + ] + }, + { + id: 'account', + label: '계정', + items: [ + {id: 'change-password', label: '비밀번호 변경', action: 'change-password'}, + {type: 'separator'}, + {id: 'logout', label: '로그 아웃', action: 'logout'} + ] + } +]; diff --git a/TestMasterUI/src/config/screenConfig.js b/TestMasterUI/src/config/screenConfig.js new file mode 100644 index 0000000..d3c94f8 --- /dev/null +++ b/TestMasterUI/src/config/screenConfig.js @@ -0,0 +1,10 @@ +// config/screenConfig.js +import APITester from '../components/api-tester/APITester'; +import LoadTestManager from '../components/load-tester/LoadTestManager.jsx'; +import MockRouteList from '../components/mock-route/MockRouteList.jsx'; + +export const SCREENS = { + 'api-tester': APITester, + 'load-test': LoadTestManager, + 'response-list': MockRouteList, +}; diff --git a/TestMasterUI/src/index.css b/TestMasterUI/src/index.css index f124e40..d036df1 100644 --- a/TestMasterUI/src/index.css +++ b/TestMasterUI/src/index.css @@ -44,7 +44,7 @@ h1 { button { border-radius: 8px; border: 1px solid transparent; - padding: 0.6em 1.2em; + /*padding: 0.6em 1.2em;*/ font-size: 1em; font-weight: 500; font-family: inherit; @@ -52,6 +52,7 @@ button { cursor: pointer; transition: border-color 0.25s; } + button:hover { border-color: #646cff; } diff --git a/TestMasterUI/src/providers/APIProvider.jsx b/TestMasterUI/src/providers/APIProvider.jsx index 2e2dc1e..9fe4672 100644 --- a/TestMasterUI/src/providers/APIProvider.jsx +++ b/TestMasterUI/src/providers/APIProvider.jsx @@ -26,6 +26,13 @@ export const APIProvider = ({ const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [isAuthenticated, setIsAuthenticated] = useState(false); + const [mockRoutes, setMockRoutes] = useState({ + content: [], + totalPages: 0, + number: 0, + first: true, + last: true + }); // Initialize services const collections$ = collectionService(contextPath); @@ -224,6 +231,67 @@ export const APIProvider = ({ } }, [apiClient]); + const loadMockRoutes = useCallback(async (params = { name: '', page: 0 }) => { + setLoading(true); + setError(null); + try { + const data = await apiRequests$.getMockRoutes(params); + setMockRoutes(data); + return data; + } catch (err) { + setError(err.message); + return null; + } finally { + setLoading(false); + } + }, []); + + const deleteMockRoutes = useCallback(async (ids) => { + setLoading(true); + setError(null); + try { + await apiRequests$.deleteMockRoutes(ids); + await loadMockRoutes(); // Reload the list after deletion + return true; + } catch (err) { + setError(err.message); + return false; + } finally { + setLoading(false); + } + }, [loadMockRoutes]); + + const createMockRoute = useCallback(async (mockRoute) => { + setLoading(true); + setError(null); + try { + const newMockRoute = await apiRequests$.createMockRoute(mockRoute); + await loadMockRoutes(); + return newMockRoute; + } catch (err) { + setError(err.message); + return null; + } finally { + setLoading(false); + } + }, [loadMockRoutes]); + + const updateMockRoute = useCallback(async (mockRoute) => { + setLoading(true); + setError(null); + try { + const updatedMockRoute = await apiRequests$.updateMockRoute(mockRoute); + await loadMockRoutes(); + return updatedMockRoute; + } catch (err) { + setError(err.message); + return null; + } finally { + setLoading(false); + } + }, [loadMockRoutes]); + + const contextValue = { apiClient, collections, @@ -243,7 +311,12 @@ export const APIProvider = ({ sendAPIRequest, isAuthenticated, setIsAuthenticated, - handleLogout + handleLogout, + mockRoutes, + loadMockRoutes, + deleteMockRoutes, + createMockRoute, + updateMockRoute }; return ( diff --git a/TestMasterUI/src/services/apiRequestService.js b/TestMasterUI/src/services/apiRequestService.js index 26f2f03..243fd6a 100644 --- a/TestMasterUI/src/services/apiRequestService.js +++ b/TestMasterUI/src/services/apiRequestService.js @@ -52,6 +52,53 @@ export const apiRequestService = (contextPath = '/') => { if (!response.ok) throw new Error('Failed to delete API request'); return true; + }, + + async getMockRoutes(params = { name: '', page: 0 }) { + const searchParams = new URLSearchParams(); + if (params.name) searchParams.append('name', params.name); + if (typeof params.page !== 'undefined') searchParams.append('page', params.page.toString()); + + const queryString = searchParams.toString(); + const url = `${contextPath}api/mock-routes${queryString ? `?${queryString}` : ''}`; + + const response = await fetch(url, { headers }); + if (!response.ok) throw new Error('Failed to fetch mock routes'); + return response.json(); + }, + + async deleteMockRoutes(ids) { + const response = await fetch(`${contextPath}api/mock-routes/delete`, { + method: 'POST', + headers, + body: JSON.stringify(ids) + }); + + if (!response.ok) throw new Error('Failed to delete mock routes'); + return true; + }, + + async createMockRoute(mockRoute) { + const response = await fetch(`${contextPath}api/mock-routes`, { + method: 'POST', + headers, + body: JSON.stringify(mockRoute) + }); + + if (!response.ok) throw new Error('Failed to create mock route'); + return response.json(); + }, + + async updateMockRoute(mockRoute) { + const response = await fetch(`${contextPath}api/mock-routes/update/${mockRoute.id}`, { + method: 'POST', + headers, + body: JSON.stringify(mockRoute) + }); + + if (!response.ok) throw new Error('Failed to update mock route'); + return response.json(); } + }; }; diff --git a/TestMasterUI/vite.config.js b/TestMasterUI/vite.config.js index 0e307b4..d358b72 100644 --- a/TestMasterUI/vite.config.js +++ b/TestMasterUI/vite.config.js @@ -1,63 +1,61 @@ -import path from "path" -import react from '@vitejs/plugin-react' -import { defineConfig } from 'vite' -import monacoEditorPlugin from './vite.monaco-editor'; +import path from 'path'; +import react from '@vitejs/plugin-react'; +import {defineConfig} from 'vite'; export default defineConfig({ define: { - global: {}, + global: {} }, - plugins: [react(),monacoEditorPlugin()], + plugins: [react()], resolve: { alias: { - "@": path.resolve(__dirname, "./src"), - }, + '@': path.resolve(__dirname, './src') + } }, server: { proxy: { '/mgmt': { target: 'http://localhost:38080', changeOrigin: true, - secure: false, + secure: false }, '/api': { target: 'http://localhost:38080', changeOrigin: true, - secure: false, + secure: false }, '/actionLogin.do': { target: 'http://localhost:38080', changeOrigin: true, - secure: false, + secure: false }, '/actionLogout.do': { target: 'http://localhost:38080', changeOrigin: true, - secure: false, + secure: false }, '/img': { target: 'http://localhost:38080', changeOrigin: true, - secure: false, + secure: false }, '/servers': { target: 'http://localhost:38080', changeOrigin: true, - secure: false, + secure: false } } }, optimizeDeps: { - include: ['@monaco-editor/react'], + include: ['@monaco-editor/react'] }, build: { rollupOptions: { output: { manualChunks: { - // Split Monaco Editor into a separate chunk - 'monaco-editor': ['monaco-editor'], - }, - }, - }, - }, -}) + 'monaco-editor': ['monaco-editor/esm/vs/editor/editor.api'] + } + } + } + } +}); diff --git a/TestMasterUI/vite.monaco-editor.js b/TestMasterUI/vite.monaco-editor.js deleted file mode 100644 index 4c9b3da..0000000 --- a/TestMasterUI/vite.monaco-editor.js +++ /dev/null @@ -1,20 +0,0 @@ -// vite.monaco-editor.js -export default function monacoEditorPlugin() { - return { - name: 'monaco-editor-plugin', - config(config) { - config.define = { - ...config.define, - 'process.platform': JSON.stringify('darwin'), - 'process.env': {}, - }; - }, - configureServer(server) { - return () => { - server.middlewares.use('/_monaco', (req, res) => { - res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin'); - }); - }; - } - }; -}