From ef11d6eaa48110cf1e9aef7cd6138fc58b2d984a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=98=84=EC=84=B1=ED=95=84?= Date: Mon, 27 Jan 2025 08:59:12 +0900 Subject: [PATCH] layout grid --- TestMasterUI/src/App.jsx | 102 +-- TestMasterUI/src/components/LoginDialog.jsx | 21 +- .../src/components/MainContentArea.jsx | 54 +- .../components/api-tester/APINavigation.jsx | 2 +- .../components/api-tester/ScenarioEditor.jsx | 2 +- .../api-tester/ScenarioNavigation.jsx | 2 +- .../components/client-views/TCPClientView.jsx | 652 +++++++----------- .../src/components/layout/LayoutGrid.jsx | 471 +++++++------ .../src/components/shared/MonacoEditor.jsx | 87 +-- TestMasterUI/src/components/ui/sidebar.jsx | 6 +- TestMasterUI/src/services/APIClient.js | 2 +- 11 files changed, 604 insertions(+), 797 deletions(-) diff --git a/TestMasterUI/src/App.jsx b/TestMasterUI/src/App.jsx index 608a3f4..7394490 100644 --- a/TestMasterUI/src/App.jsx +++ b/TestMasterUI/src/App.jsx @@ -20,36 +20,37 @@ const App = ({ options = { type: 'api', contextPath: '/' } }) => { const AppContent = ({ options, showLogin, setShowLogin }) => { const { isAuthenticated, setIsAuthenticated } = useAPI(); + const [isInitialized, setIsInitialized] = useState(false); - useEffect(() => { - const checkAuth = async () => { - try { - const response = await fetch('/api/account', { - headers: { - 'X-Requested-With': 'XMLHttpRequest' - } - }); - if (response.status !== 200) { - setShowLogin(true); - setIsAuthenticated(false); - return; - } + const checkAuth = async () => { + try { + const response = await fetch('/api/account', {}); - const data = await response.json(); - setIsAuthenticated(!!data.authorities); - if (!data.authorities) { - setShowLogin(true); - } - } catch (error) { - console.error('Auth check failed:', error); + if (response.status !== 200) { setShowLogin(true); setIsAuthenticated(false); + return; } - }; - checkAuth(); - }, [setIsAuthenticated]); + const data = await response.json(); + setIsAuthenticated(!!data.authorities); + if (!data.authorities) { + setShowLogin(true); + } + } catch (error) { + console.error('Auth check failed:', error); + setShowLogin(true); + setIsAuthenticated(false); + } + }; + + useEffect(() => { + if (!isInitialized) { + setIsInitialized(true); + checkAuth(); + } + }, [isInitialized]); if (!isAuthenticated) { return ; @@ -68,59 +69,4 @@ const AppContent = ({ options, showLogin, setShowLogin }) => { ); }; - -// const App = ({ options = { type: 'api', contextPath: '/' } }) => { -// -// const [isAuthenticated, setIsAuthenticated] = useState(false); -// const [showLogin, setShowLogin] = useState(false); -// -// useEffect(() => { -// // Check authentication status -// const checkAuth = async () => { -// try { -// const response = await fetch('/api/accouont', { -// headers: { -// 'X-Requested-With': 'XMLHttpRequest' -// } -// }); -// -// if (response.status !== 200) { -// setShowLogin(true); -// return; -// } -// -// const data = await response.json(); -// console.log(data); -// setIsAuthenticated(data.authorities); -// if (!data.authorities) { -// setShowLogin(true); -// } -// } catch (error) { -// console.error('Auth check failed:', error); -// setShowLogin(true); -// } -// }; -// -// checkAuth(); -// }, []); -// -// if (!isAuthenticated) { -// return ; -// } -// -// return ( -// -// -//
-// {options.type === 'api' ? ( -// -// ) : ( -// -// )} -//
-//
-//
-// ); -// }; - export default App diff --git a/TestMasterUI/src/components/LoginDialog.jsx b/TestMasterUI/src/components/LoginDialog.jsx index 7408bd0..068af95 100644 --- a/TestMasterUI/src/components/LoginDialog.jsx +++ b/TestMasterUI/src/components/LoginDialog.jsx @@ -10,26 +10,13 @@ import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Label } from '@/components/ui/label'; +import {getCSRFToken} from '@/services/token.js'; const LoginDialog = ({ open, onOpenChange }) => { const [formData, setFormData] = useState({ id: '', password: '' }); const [message, setMessage] = useState({ type: '', content: '' }); const [isLoading, setIsLoading] = useState(false); - const getCsrfToken = () => { - const name = 'XSRF-TOKEN='; - const decodedCookie = decodeURIComponent(document.cookie); - const cookieArray = decodedCookie.split(';'); - - for (let cookie of cookieArray) { - cookie = cookie.trim(); - if (cookie.indexOf(name) === 0) { - return cookie.substring(name.length, cookie.length); - } - } - return null; - }; - const showMessage = (type, content) => { setMessage({ type, content }); }; @@ -45,7 +32,7 @@ const LoginDialog = ({ open, onOpenChange }) => { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', - 'X-XSRF-TOKEN': getCsrfToken() + 'X-XSRF-TOKEN': getCSRFToken() }, body: new URLSearchParams({ id: formData.id, @@ -54,7 +41,7 @@ const LoginDialog = ({ open, onOpenChange }) => { }); const data = await response.json(); - + console.log(data); switch(data.status) { case 'SUCCESS': window.location.href = data.redirectUrl; @@ -84,7 +71,7 @@ const LoginDialog = ({ open, onOpenChange }) => { - 로그인 + TestMaster 로그인
diff --git a/TestMasterUI/src/components/MainContentArea.jsx b/TestMasterUI/src/components/MainContentArea.jsx index 8493c1d..f226097 100644 --- a/TestMasterUI/src/components/MainContentArea.jsx +++ b/TestMasterUI/src/components/MainContentArea.jsx @@ -11,6 +11,19 @@ const MainContentArea = ({isApiMode, toggleMode}) => { const [apiRequests, setApiRequests] = useState([]); const [currentIndex, setCurrentIndex] = useState(0); + useEffect(() => { + const handleKeyDown = (event) => { + if (event.key === 'm' && (event.metaKey || event.ctrlKey)) { + event.preventDefault(); + toggleMode(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [toggleMode]); + + return (
{isApiMode ? ( @@ -93,7 +106,8 @@ const APIMode = ({ collectionId: collectionId, collectionName: collection.name, server: api.server || null, - description: api.description || '' + description: api.description || '', + layout: api.layout || {}, }; const existingIndex = apiRequests.findIndex( @@ -113,12 +127,12 @@ const APIMode = ({ const renderFallback = () => (
-

No APIs opened. Select an API to test from the sidebar.

+

왼쪽에서 API를 선택해 주세요

); return ( - +
{ }; return ( - -
-
+
+ +
-
-
- -
-
- -
+
+
+
+
+ + + +
+
); }; diff --git a/TestMasterUI/src/components/api-tester/APINavigation.jsx b/TestMasterUI/src/components/api-tester/APINavigation.jsx index 9de651b..69bd896 100644 --- a/TestMasterUI/src/components/api-tester/APINavigation.jsx +++ b/TestMasterUI/src/components/api-tester/APINavigation.jsx @@ -239,7 +239,7 @@ const APINavigation = ({ - Collections + Collections (Ctrl + /) {collections.map(renderCollectionItem)} diff --git a/TestMasterUI/src/components/api-tester/ScenarioEditor.jsx b/TestMasterUI/src/components/api-tester/ScenarioEditor.jsx index 2763b8e..398ace2 100644 --- a/TestMasterUI/src/components/api-tester/ScenarioEditor.jsx +++ b/TestMasterUI/src/components/api-tester/ScenarioEditor.jsx @@ -102,7 +102,7 @@ const ScenarioEditor = () => { }, []); if (!currentFlowData) { - return
Select a scenario to edit
; + return
시나리오를 선택하세요 (Ctrl + .)
; } return ( diff --git a/TestMasterUI/src/components/api-tester/ScenarioNavigation.jsx b/TestMasterUI/src/components/api-tester/ScenarioNavigation.jsx index 47656d5..698d23a 100644 --- a/TestMasterUI/src/components/api-tester/ScenarioNavigation.jsx +++ b/TestMasterUI/src/components/api-tester/ScenarioNavigation.jsx @@ -156,7 +156,7 @@ const ScenarioNavigation = ({ - Scenarios + Scenarios (Ctrl + .) {scenarios.map(renderScenarioItem)} diff --git a/TestMasterUI/src/components/client-views/TCPClientView.jsx b/TestMasterUI/src/components/client-views/TCPClientView.jsx index c9a76ef..b9d696a 100644 --- a/TestMasterUI/src/components/client-views/TCPClientView.jsx +++ b/TestMasterUI/src/components/client-views/TCPClientView.jsx @@ -1,457 +1,279 @@ -import React, {useCallback, useEffect, useRef, useState} from 'react'; -import * as monaco from 'monaco-editor'; +import React, {useCallback, useEffect, useState} from 'react'; import {Button} from '@/components/ui/button'; import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs'; -import {Card, CardContent} from '@/components/ui/card'; -import LayoutGrid from '../layout/LayoutGrid'; -import LayoutTreeDataGrid from '../layout/LayoutTreeDataGrid'; -import ApiLayoutItemTreeBuilder from '../../lib/ApiLayoutItemTreeBuilder'; -import ApiLayoutItemTreeDataBuilder from '../../lib/ApiLayoutItemTreeDataBuilder'; -import VariableSnippet from '../shared/VariableSnippet'; +import ServerManager from '@/components/ServerManager.jsx'; +import {useAPI} from '@/providers/APIProvider'; +import MonacoEditor from '@/components/shared/MonacoEditor'; import APIClient from '@/services/APIClient.js'; +import {SidebarTrigger} from '@/components/ui/sidebar.jsx'; +import {Separator} from '@/components/ui/separator.jsx'; +import LayoutGrid from '@/components/layout/LayoutGrid.jsx'; +import ApiLayoutItemTreeBuilder from '@/lib/ApiLayoutItemTreeBuilder.js'; +import ApiLayoutItemTreeDataBuilder from '@/lib/ApiLayoutItemTreeDataBuilder.js'; -export const TCPClientView = ({ parent, serverManager, model, index }) => { - const [apiClient] = useState(() => new APIClient()); +export const TCPClientView = ({model, index}) => { + const apiClient = new APIClient(); + const {getServer, updateAPIRequest} = useAPI(); const [isLoading, setIsLoading] = useState(false); - const [editors, setEditors] = useState({ - layoutJson: null, - requestBody: null, - responseBody: null, - console: null, - preRequest: null, - postRequest: null + const [currentModel, setCurrentModel] = useState(model); + const [currentTab, setCurrentTab] = useState('body'); + const [currentResponseTab, setCurrentResponseTab] = useState('response-body'); + + useEffect(() => { + setCurrentModel(prev => ({ + ...prev, + requestBody: model.requestBody || '', + layout: model.layout || {}, + contentType: 'plaintext' + })); + }, [model]); + + + const [layoutRoot, setLayoutRoot] = useState(() => { + const root = ApiLayoutItemTreeBuilder.createTree(model.layout.layoutItems); + const replicator = new ApiLayoutItemTreeDataBuilder(10000); + return replicator.createReplicatedTree(root); }); - const [layoutData, setLayoutData] = useState(null); - // Refs - const layoutJsonRef = useRef(null); - const requestBodyRef = useRef(null); - const responseBodyRef = useRef(null); - const consoleRef = useRef(null); - const preRequestRef = useRef(null); - const postRequestRef = useRef(null); - const totalMessageLengthRef = useRef(null); - const layoutGridRef = useRef(null); + const [responseData, setResponseData] = useState({ + headers: [], + status: '', + time: '', + size: '' + }); - const handleResponseDisplay = useCallback((response) => { - try { - editors.responseBody?.setValue(response.body || ''); - if (response.time) { - const responseTimeEl = document.querySelector('.response_time'); - if (responseTimeEl) { - responseTimeEl.textContent = `time: ${response.time}ms`; - } - } - if (response.size) { - const responseSizeEl = document.querySelector('.response_size'); - if (responseSizeEl) { - responseSizeEl.textContent = `size: ${response.size}bytes`; - } - } - } catch (error) { - console.error('Error displaying response:', error); + const [editorContents, setEditorContents] = useState({ + requestBody: currentModel.requestBody || '', + responseBody: '', + consoleLog: '', + layout: JSON.stringify(currentModel.layout, null, 2) || '', + preRequestScript: currentModel.preRequestScript || '', + postRequestScript: currentModel.postRequestScript || '' + }); + + useEffect(() => { + if (currentModel.server) { + const selectedServer = getServer(currentModel.server); } - }, [editors.responseBody]); + }, [currentModel.server]); - const handleLoadLayout = useCallback(() => { - try { - const newLayout = JSON.parse(editors.layoutJson?.getValue() || '{}'); - model.layout = newLayout; - if (newLayout.layoutItems) { - newLayout.layoutItems.forEach((item) => { - if (item.value === undefined) { - item.value = ''; - } - }); - } + const handleServerChange = (serverId) => { + setCurrentModel(prev => ({ + ...prev, + server: serverId + })); + }; - const layoutRoot = ApiLayoutItemTreeBuilder.createTree(newLayout.layoutItems); - const replicator = new ApiLayoutItemTreeDataBuilder(10000); - const layoutDataRoot = replicator.createReplicatedTree(layoutRoot); - - setLayoutData(layoutDataRoot); - - if (layoutGridRef.current) { - layoutGridRef.current.rebuild(newLayout); - } - } catch (error) { - console.error('Error loading layout:', error); - alert(error.message); - } - }, [model, editors.layoutJson]); - - const handleSendRequest = useCallback(async () => { - if (!model.server) { - alert('서버를 등록해주세요.'); + const handleSendRequest = async () => { + if (isLoading) { return; } setIsLoading(true); - try { - editors.responseBody?.setValue(''); - editors.console?.setValue(''); + setEditorContents(prev => ({...prev, consoleLog: ''})); - const startTime = Date.now(); + try { const response = await apiClient.sendAPIRequest( - model, - null, - (message) => { - const consoleEditor = editors.console; - if (consoleEditor) { - const model = consoleEditor.getModel(); - if (model) { - const lastLine = model.getLineCount(); - const range = new monaco.Range(lastLine, 1, lastLine, 1); - const text = typeof message === 'object' ? JSON.stringify(message, null, 2) : message; - consoleEditor.executeEdits('console', [{ - identifier: { major: 1, minor: 1 }, - range, - text: text + '\n', - forceMoveMarkers: true - }]); - } - } - } + currentModel, + (log) => setEditorContents(prev => ({ + ...prev, + consoleLog: prev.consoleLog + log + '\n' + })) ); - if (response) { - const endTime = Date.now(); - response.time = endTime - startTime; - handleResponseDisplay(response); - } + setResponseData({ + time: `${response.time}ms`, + size: `Size: ${response.size}` + }); + + setEditorContents(prev => ({ + ...prev, + responseBody: response.body + })); + + } catch (error) { - console.error('Error sending request:', error); - alert(error.message); + console.error(error); + setEditorContents(prev => ({ + ...prev, + consoleLog: prev.consoleLog + `Error: ${error.message}\n` + })); } finally { setIsLoading(false); } - }, [model, editors, apiClient, handleResponseDisplay]); + }; - const handleResize = useCallback(() => { - Object.values(editors).forEach(editor => { - if (editor) { - editor.layout({ width: 0 }); - editor.layout(); - } - }); - }, [editors]); + const handleSave = async () => { + try { + const updatedModel = { + ...currentModel, + preRequestScript: editorContents.preRequestScript, + postRequestScript: editorContents.postRequestScript, + layout: JSON.parse(editorContents.layout), + requestBody: currentModel.requestBody + }; - // Layout update handler - const handleLayoutUpdate = useCallback(async (finalMessage, layoutDataTree) => { - const prefix = (model.server && model.server.serverOptions) - ? await serverManager.renderServerPrefix(model.server.serverOptions, model) - : ''; - - editors.requestBody?.setValue(prefix + finalMessage); - model.layout.computedLayoutItemRoot = layoutDataTree; - }, [model, editors.requestBody, serverManager]); - - // Initialize Monaco editors - useEffect(() => { - if (layoutJsonRef.current && !editors.layoutJson) { - const layoutJson = JSON.stringify(model.layout, null, 2); - const layoutJsonEditor = monaco.editor.create(layoutJsonRef.current, { - value: layoutJson, - language: 'json', - automaticLayout: true, - quickSuggestions: false - }); - - layoutJsonEditor.onDidChangeModelContent(() => { - try { - const content = layoutJsonEditor.getValue(); - JSON.parse(content); // Validate JSON - } catch (e) { - // Invalid JSON - ignore - } - }); - - setEditors(prev => ({ ...prev, layoutJson: layoutJsonEditor })); + await updateAPIRequest(updatedModel); + } catch (error) { + console.error('Failed to save API:', error); } + }; - if (requestBodyRef.current && !editors.requestBody) { - const requestBodyEditor = monaco.editor.create(requestBodyRef.current, { - value: model.requestBody || '', - language: 'text', - automaticLayout: true, - quickSuggestions: false - }); - - requestBodyEditor.onDidChangeModelContent(() => { - model.requestBody = requestBodyEditor.getValue(); - }); - - requestBodyEditor.onMouseMove((e) => { - const position = e.target.position; - if (position) { - const editorModel = requestBodyEditor.getModel(); - if (!editorModel) return; - - const word = editorModel.getWordAtPosition(position); - const lineContent = editorModel.getLineContent(position.lineNumber); - - if (word && lineContent.includes(`{{${word.word}}}`)) { - parent?.showPopper?.(e.target.element, word.word); - } else { - parent?.hidePopper?.(e.target.element); - } - } - }); - - setEditors(prev => ({ ...prev, requestBody: requestBodyEditor })); - } - - if (responseBodyRef.current && !editors.responseBody) { - const responseBodyEditor = monaco.editor.create(responseBodyRef.current, { - value: '', - language: 'text', - automaticLayout: true, - quickSuggestions: false, - readOnly: true - }); - setEditors(prev => ({ ...prev, responseBody: responseBodyEditor })); - } - - if (consoleRef.current && !editors.console) { - const consoleEditor = monaco.editor.create(consoleRef.current, { - value: '', - automaticLayout: true, - quickSuggestions: false, - readOnly: true - }); - setEditors(prev => ({ ...prev, console: consoleEditor })); - } - - if (preRequestRef.current && !editors.preRequest) { - const preRequestEditor = monaco.editor.create(preRequestRef.current, { - value: model.preRequestScript || '', - language: 'javascript', - automaticLayout: true, - quickSuggestions: false - }); - - preRequestEditor.onDidChangeModelContent(() => { - model.preRequestScript = preRequestEditor.getValue(); - }); - - setEditors(prev => ({ ...prev, preRequest: preRequestEditor })); - } - - if (postRequestRef.current && !editors.postRequest) { - const postRequestEditor = monaco.editor.create(postRequestRef.current, { - value: model.postRequestScript || '', - language: 'javascript', - automaticLayout: true, - quickSuggestions: false - }); - - postRequestEditor.onDidChangeModelContent(() => { - model.postRequestScript = postRequestEditor.getValue(); - }); - - setEditors(prev => ({ ...prev, postRequest: postRequestEditor })); - } - - // Initialize layout data - const layoutRoot = ApiLayoutItemTreeBuilder.createTree(model.layout.layoutItems); + const handleLayoutUpdate = useCallback((updatedLayout) => { + const updatedRoot = ApiLayoutItemTreeBuilder.createTree(updatedLayout.layoutItems); const replicator = new ApiLayoutItemTreeDataBuilder(10000); - const layoutDataRoot = replicator.createReplicatedTree(layoutRoot); - setLayoutData(layoutDataRoot); - - return () => { - Object.values(editors).forEach(editor => editor?.dispose()); - }; - }, []); - - // Handle window resize - useEffect(() => { - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, [handleResize]); - - // Tab change handlers - useEffect(() => { - const handleTabChange = (tab) => { - const currentEditor = { - 'requestBodyTab': editors.requestBody, - 'preRequestTab': editors.preRequest, - 'postRequestTab': editors.postRequest - }[tab]; - - if (currentEditor) { - currentEditor.layout(); - } - }; - - const tabs = ['requestBodyTab', 'preRequestTab', 'postRequestTab']; - tabs.forEach(tab => { - const element = document.querySelector(`#api_request_${model.id} .btn_${tab}`); - if (element) { - const handler = () => handleTabChange(tab); - element.addEventListener('shown.bs.tab', handler); - return () => element.removeEventListener('shown.bs.tab', handler); - } - }); - }, [editors, model.id]); - - // Server options effect - useEffect(() => { - if (!model.server) return; - - if (model.server.headers === undefined) { - model.server.headers = []; - } - - if (model.server.serverOptions) { - serverManager.renderTcpServerOptions( - '.server_options', - model.server.serverOptions, - model.headers, - parent - ); - } - }, [model.server, model.headers, parent, serverManager]); + const newRoot = replicator.createReplicatedTree(updatedRoot); + setLayoutRoot(newRoot); + currentModel.layout = updatedLayout; + }, [currentModel]); return ( -
- {/* Header Section */} -
- - {model.collectionName} > {model.name} - -
- - {/* Server Selection */} -
-
- {serverManager.renderTcpServers(model.server)} - +
+ {/* Request Header Section */} +
+ + +
+ {currentModel.collectionName} + {' > '} + {currentModel.name}
- -
- {/* Server Options */} -
- - {/* Main Content */} - - - Layout - Layout JSON - Layout Tree - Layout Data - Pre-Request - Post-Request - - - -
- { - model.layout = updatedLayout; - editors.layoutJson?.setValue(JSON.stringify(updatedLayout, null, 2)); - }} - /> -
-
- - -
-
- -
- - - -
-
- {layoutData && ( - - )} -
-
-
- - -
-
-
-
-
- -
-
- - - -
-
-
-
-
- -
-
- - - - {/* Message Length */} -
- 전체 메시지 길이: - + {/* Request Controls */} +
+
+ +
+
+ + +
- {/* Response Section */} - - -
-
- Response - - + {/* Request/Response Container */} +
+ {/* Request Section */} +
+ + + Layout + Layout JSON + Pre-Request + Post-Request + + +
+ {/* Body Tab Content */} + +
+
+ + { + setCurrentModel(prev => ({ + ...prev, + requestBody: newValue + })); + }} + /> +
+
+
+ + { + setEditorContents(prev => ({...prev, layout: value})); + setCurrentModel(prev => ({...prev, layout: JSON.parse(value)})); + }} + height="full" + /> + + + { + setEditorContents(prev => ({...prev, preRequestScript: value})); + setCurrentModel(prev => ({...prev, preRequestScript: value})); + }} + height="full" + /> + + + {/* Post-Request Tab Content */} + + { + setEditorContents(prev => ({...prev, postRequestScript: value})); + setCurrentModel(prev => ({...prev, postRequestScript: value})); + }} + height="full" + /> +
- - - Body - Console - + +
- -
- - - -
- - + {/* Response Section */} +
+
+ Response + {responseData.status} + {responseData.time} + {responseData.size}
- - + + + + Body + Console + + +
+ + + + + + +
+
+
+
+ +
); }; -// export default TCPClientView; +export default TCPClientView; diff --git a/TestMasterUI/src/components/layout/LayoutGrid.jsx b/TestMasterUI/src/components/layout/LayoutGrid.jsx index 761f7ae..97affa3 100644 --- a/TestMasterUI/src/components/layout/LayoutGrid.jsx +++ b/TestMasterUI/src/components/layout/LayoutGrid.jsx @@ -1,15 +1,32 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Select } from "@/components/ui/select"; -import EditableInput from '../shared/EditableInput'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { ArrowUp, ArrowDown, Trash2, Plus } from "lucide-react"; +import EditableInput from "../shared/EditableInput"; + +// shadcn table imports +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; const LayoutGrid = ({ layout, onChange }) => { const [items, setItems] = useState(layout?.layoutItems || []); const getLastId = useCallback(() => { let lastId = 0; - items.forEach(item => { + items.forEach((item) => { if (item.id > lastId) { lastId = item.id; } @@ -22,26 +39,26 @@ const LayoutGrid = ({ layout, onChange }) => { id: getLastId(), parent: 0, level: 1, - loutName: '', - loutItemName: '', - loutItemDesc: '', + loutName: "", + loutItemName: "", + loutItemDesc: "", loutItemDepth: 1, - loutItemType: 'Field', - loutItemOccCnt: '', - loutItemOccRef: '', - loutItemDataType: 'String', + loutItemType: "Field", + loutItemOccCnt: "", + loutItemOccRef: "", + loutItemDataType: "String", loutItemLength: 1, loutItemDecimal: 0, - loutItemDefault: '', - loutItemMaskYn: 'N', + loutItemDefault: "", + loutItemMaskYn: "N", loutItemMaskOffset: 0, loutItemMaskLength: 0, parentLoutItemIndex: 0, expanded: true, isLeaf: true, - LOUTITEMPATH: '', - value: '', - loutItemSerno: items.length + 1 + LOUTITEMPATH: "", + value: "", + loutItemSerno: items.length + 1, }; const newItems = [...items, newItem]; @@ -50,201 +67,257 @@ const LayoutGrid = ({ layout, onChange }) => { onChange?.(layout); }, [items, layout, onChange, getLastId]); - const handleMove = useCallback((index, direction) => { - if ((direction === 'up' && index > 0) || - (direction === 'down' && index < items.length - 1)) { - const newItems = [...items]; - const swapIndex = direction === 'up' ? index - 1 : index + 1; - [newItems[index], newItems[swapIndex]] = [newItems[swapIndex], newItems[index]]; + const handleMove = useCallback( + (index, direction) => { + if ( + (direction === "up" && index > 0) || + (direction === "down" && index < items.length - 1) + ) { + const newItems = [...items]; + const swapIndex = direction === "up" ? index - 1 : index + 1; + [newItems[index], newItems[swapIndex]] = [ + newItems[swapIndex], + newItems[index], + ]; - // Update loutItemSerno values - newItems.forEach((item, idx) => { - item.loutItemSerno = idx + 1; - }); + // Update loutItemSerno values + newItems.forEach((item, idx) => { + item.loutItemSerno = idx + 1; + }); - setItems(newItems); - layout.layoutItems = newItems; - onChange?.(layout); - } - }, [items, layout, onChange]); + setItems(newItems); + layout.layoutItems = newItems; + onChange?.(layout); + } + }, + [items, layout, onChange] + ); - const handleDelete = useCallback((index) => { - const newItems = items.filter((_, idx) => idx !== index); + const handleDelete = useCallback( + (index) => { + const newItems = items.filter((_, idx) => idx !== index); - // Update loutItemSerno values - newItems.forEach((item, idx) => { - item.loutItemSerno = idx + 1; - }); + // Update loutItemSerno values + newItems.forEach((item, idx) => { + item.loutItemSerno = idx + 1; + }); - setItems(newItems); - layout.layoutItems = newItems; - onChange?.(layout); - }, [items, layout, onChange]); + setItems(newItems); + layout.layoutItems = newItems; + onChange?.(layout); + }, + [items, layout, onChange] + ); - const handleFieldChange = useCallback((index, field, value) => { - const newItems = [...items]; - newItems[index][field] = value; + const handleFieldChange = useCallback( + (index, field, value) => { + const newItems = [...items]; + newItems[index][field] = value; - if (field === 'value' && newItems[index].value === undefined) { - newItems[index].value = ''; - } + if (field === "value" && newItems[index].value === undefined) { + newItems[index].value = ""; + } - setItems(newItems); - layout.layoutItems = newItems; - onChange?.(layout); - }, [items, layout, onChange]); + setItems(newItems); + layout.layoutItems = newItems; + onChange?.(layout); + }, + [items, layout, onChange] + ); return (
- - - - - - - - - - - - - - - - - - - {items.map((item, index) => ( - - - - - - - - - - - - -
#IDParent항목명(영문)항목설명깊이아이템 유형반복 횟수반복 참조 필드데이터 길이 - 비고 - -
- {item.loutItemSerno} - - handleFieldChange(index, 'id', e.target.value)} - className="w-full" - /> - - handleFieldChange(index, 'parent', e.target.value)} - className="w-full" - /> - - handleFieldChange(index, 'loutItemName', e.target.value)} - className="w-full" - /> - - handleFieldChange(index, 'loutItemDesc', e.target.value)} - className="w-full" - /> - - handleFieldChange(index, 'loutItemDepth', parseInt(e.target.value))} - min={1} - className="w-full" - /> - - - - handleFieldChange(index, 'loutItemOccCnt', e.target.value)} - className="w-full" - /> - - handleFieldChange(index, 'loutItemOccRef', e.target.value)} - className="w-full" - /> - - handleFieldChange(index, 'loutItemLength', parseInt(e.target.value))} - min={1} - className="w-full" - /> - - {item.loutItemType === 'Field' && ( - handleFieldChange(index, 'value', value)} - /> - )} - -
- + + +
+ + + ))} + +
); }; diff --git a/TestMasterUI/src/components/shared/MonacoEditor.jsx b/TestMasterUI/src/components/shared/MonacoEditor.jsx index 88633fb..a210211 100644 --- a/TestMasterUI/src/components/shared/MonacoEditor.jsx +++ b/TestMasterUI/src/components/shared/MonacoEditor.jsx @@ -10,10 +10,10 @@ const MonacoEditor = memo(({ onChange = () => {} }) => { const editorRef = useRef(null); + const modelRef = useRef(null); const containerRef = useRef(null); const subscriptionRef = useRef(null); - // Configure editor options based on language const getEditorOptions = (lang) => { const baseOptions = { automaticLayout: true, @@ -31,7 +31,6 @@ const MonacoEditor = memo(({ } }; - // Language-specific options switch (lang) { case 'json': return { @@ -55,97 +54,59 @@ const MonacoEditor = memo(({ } }; - // Initialize or update editor - const setupEditor = () => { + useEffect(() => { if (!containerRef.current) return; - // Dispose existing editor if it exists - if (editorRef.current) { - editorRef.current.dispose(); - } + const modelUri = monaco.Uri.parse(`inmemory://model-${id}`); + modelRef.current = monaco.editor.createModel(value, language, modelUri); - // Create new editor with language-specific options const options = getEditorOptions(language); editorRef.current = monaco.editor.create(containerRef.current, { - value, - language, - ...options + ...options, + model: modelRef.current }); - // Set up change event handler - if (subscriptionRef.current) { - subscriptionRef.current.dispose(); - } - subscriptionRef.current = editorRef.current.onDidChangeModelContent(() => { - const newValue = editorRef.current.getValue(); - onChange(newValue); + onChange(editorRef.current.getValue()); }); - // Format document if it's JSON if (language === 'json' && value) { try { - const modelUri = monaco.Uri.parse(`inmemory://model-${id}.json`); - const model = monaco.editor.createModel(value, 'json', modelUri); + monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ + validate: true, + allowComments: false, + schemas: [] + }); setTimeout(() => { - monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ - validate: true, - allowComments: false, - schemas: [] - }); - editorRef.current.setModel(model); + const action = editorRef.current?.getAction('editor.action.formatDocument'); + if (action) action.run(); }, 100); } catch (e) { console.warn('Failed to format JSON:', e); } } - }; - // Initial setup - useEffect(() => { - setupEditor(); - - // Set up resize observer const resizeObserver = new ResizeObserver(() => { - if (editorRef.current) { - editorRef.current.layout(); - } + editorRef.current?.layout(); }); - if (containerRef.current) { - resizeObserver.observe(containerRef.current); - } + resizeObserver.observe(containerRef.current); - // Cleanup return () => { resizeObserver.disconnect(); - if (subscriptionRef.current) { - subscriptionRef.current.dispose(); - } - if (editorRef.current) { - editorRef.current.dispose(); + subscriptionRef.current?.dispose(); + editorRef.current?.dispose(); + if (modelRef.current && !modelRef.current.isDisposed()) { + modelRef.current.dispose(); } }; - }, [language]); // Reinitialize when language changes + }, [language]); - // Update value if it changes externally useEffect(() => { - if (editorRef.current && value !== editorRef.current.getValue()) { - editorRef.current.setValue(value); - - // Add formatting for JSON - if (language === 'json' && value) { - try { - const action = editorRef.current.getAction('editor.action.formatDocument'); - if (action) { - action.run(); - } - } catch (e) { - console.warn('Failed to format JSON:', e); - } - } + if (modelRef.current && !modelRef.current.isDisposed()) { + modelRef.current.setValue(value); } - }, [value, language]); + }, [value]); return (
{ const handleKeyDown = (event) => { if ( - event.key === SIDEBAR_KEYBOARD_SHORTCUT && + event.key === shortcut && (event.metaKey || event.ctrlKey) ) { event.preventDefault() diff --git a/TestMasterUI/src/services/APIClient.js b/TestMasterUI/src/services/APIClient.js index 5cbf37d..e7ef9e4 100644 --- a/TestMasterUI/src/services/APIClient.js +++ b/TestMasterUI/src/services/APIClient.js @@ -73,7 +73,7 @@ class APIClient { return { body: data.body, status: data.status, - headers: Object.fromEntries(data.headers.map(h => [h.key, h.value])), + headers: data.headers ? Object.fromEntries(data.headers.map(h => [h.key, h.value])) : {}, size: data.size }; }