diff --git a/TestMasterUI/src/components/client-views/TCPClientView.jsx b/TestMasterUI/src/components/client-views/TCPClientView.jsx index e9d50ae..7fdb017 100644 --- a/TestMasterUI/src/components/client-views/TCPClientView.jsx +++ b/TestMasterUI/src/components/client-views/TCPClientView.jsx @@ -9,6 +9,8 @@ import {SidebarTrigger} from '@/components/ui/sidebar.jsx'; import {Separator} from '@/components/ui/separator.jsx'; import LayoutGrid from '@/components/layout/LayoutGrid.jsx'; import LayoutTreeGrid from '@/components/layout/LayoutTreeGrid.jsx'; +import LayoutTreeDataGrid from '@/components/layout/LayoutTreeDataGrid.jsx'; +import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card.jsx'; export const TCPClientView = ({model, index}) => { const apiClient = new APIClient(); @@ -119,170 +121,239 @@ export const TCPClientView = ({model, index}) => { layout: updatedLayout })); - console.log({updatedLayout}); + setEditorContents(prev => ({ + ...prev, + layout: JSON.stringify(updatedLayout, null, 2) + })); + + }, []); + + const handleLayoutDataUpdate = useCallback((updatedLayout) => { + console.log(updatedLayout); + + // Update layout and request body based on the data tree + setCurrentModel(prev => ({ + ...prev, + layout: updatedLayout, + requestBody: generateRequestBody(updatedLayout.computedLayoutItemRoot) + })); + + // Update editor contents with new request body + setEditorContents(prev => ({ + ...prev, + requestBody: generateRequestBody(updatedLayout.computedLayoutItemRoot) + })); + }, []); + + const generateRequestBody = useCallback((layoutRoot) => { + console.log(layoutRoot); + if (!layoutRoot) return ''; + + // Helper function to pad field values with spaces + const padField = (value, length) => { + // Convert to string and ensure it's not null/undefined + const stringValue = String(value || ''); + // Left pad with spaces to match the specified length + return stringValue.padStart(length, ' '); + }; + + // Process nodes using recursive function + const processNodes = (node) => { + // If it's a Field type node, pad the value + if (node.loutItemType === 'Field') { + return padField(node.value, node.loutItemLength); + } + + // If node has children + if (node.children && node.children.length > 0) { + // For non-repeating nodes, just process children + return node.children + .map(child => processNodes(child)) + .join(''); + } + + return ''; + }; + + return processNodes(layoutRoot); }, []); return (
{/* Request Header Section */} -
- - -
- {currentModel.collectionName} - {' > '} - {currentModel.name} -
-
- - {/* Request Controls */} -
-
- -
-
- - -
-
- - {/* Request/Response Container */} -
- {/* Request Section */} -
- - - Layout - Layout Tree - Layout JSON - Pre-Request - Post-Request - - -
- {/* Body Tab Content */} - -
-
- -
-
-
- -
-
- -
-
-
- - { - 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" - /> - -
-
-
- - { - setCurrentModel(prev => ({ - ...prev, - requestBody: newValue - })); - }} - /> - - {/* Response Section */} -
-
- Response - {responseData.status} - {responseData.time} - {responseData.size} +
+
+ + +
+ {currentModel.collectionName} + {' > '} + {currentModel.name}
- - - - Body - Console - - -
- - - - - - -
-
+
+ + + +
+
+ {/* 요청 영역 */} + + + Request + + + + + Layout + Layout Tree + Layout Data + Layout JSON + Pre-Request + Post-Request + +
+ {/* Body Tab Content */} + + + + + + + + + + + { + 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" + /> + +
+
+ + + +
+
Request Body
+ { + setCurrentModel(prev => ({ + ...prev, + requestBody: newValue + })); + }} + height="200px" + /> +
+ 전체 메시지 길이: {currentModel.requestBody?.length || 0} +
+
+
+
+ + {/* 응답 영역 */} + + + + Response + {responseData.status} + {responseData.time} + {responseData.size} + + + + + + Body + Console + + +
+ + + + + + +
+
+
+
+
); }; diff --git a/TestMasterUI/src/components/layout/LayoutGrid.jsx b/TestMasterUI/src/components/layout/LayoutGrid.jsx index 5b777ca..61d39c0 100644 --- a/TestMasterUI/src/components/layout/LayoutGrid.jsx +++ b/TestMasterUI/src/components/layout/LayoutGrid.jsx @@ -135,14 +135,12 @@ const LayoutGrid = ({ layout, layoutItems, setLayoutItems, onChange }) => { Parent 항목명(영문) 항목설명 - 깊이 + 깊이 아이템 유형 반복 횟수 반복 참조 필드 데이터 길이 - - 값 - + 기본 값 비고 - ) : ( -
+ + + {/* # 컬럼: children toggle 표시 (indent 적용) */} + +
+ {hasChildren ? ( + + ) : ( +
+ )} +
+ + {/* 항목명(영문) (텍스트) */} + + {node.loutItemName} + + {/* 항목설명 (텍스트) */} + + {node.loutItemDesc} + + {/* 깊이 (텍스트) */} + + {node.loutItemDepth} + + {/* 아이템 유형 (텍스트) */} + + {node.loutItemType} + + {/* 반복 횟수 (텍스트) */} + + {node.loutItemOccCnt} + + {/* 반복 참조 필드 (텍스트) */} + + {node.loutItemOccRef} + + {/* 데이터 길이 (텍스트) */} + + {node.loutItemType === 'Field'?node.loutItemLength:''} + + {/* 값: Field 타입인 경우에만 EditableInput (입력 가능) */} + + {node.loutItemType === 'Field' && ( + + handleNodeChange(node.id, 'value', value) + } + /> )} - 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)} - /> - )} - - -
- - - -
-
-
+ + + {/* 자식 노드 렌더링 (펼쳐진 경우) */} + {hasChildren && + isExpanded && + node.children.map((child, childIndex) => + renderTreeRows(child, indentLevel + 1, childIndex) + )} +
); }; @@ -357,35 +173,30 @@ const LayoutTreeGrid = ({ layout, onChange }) => { # - ID - Parent - 항목명(영문) + + 항목명(영문) + 항목설명 깊이 아이템 유형 반복 횟수 - 반복 참조 필드 + + 반복 참조 필드 + 데이터 길이 - - 비고 - - - {items.map((item, index) => renderTreeRow(item, index))} + {treeRoot && + treeRoot.children && + treeRoot.children.map((child, index) => + renderTreeRows(child, 0, index) + )}
); }; -export default LayoutTreeGrid; +export default LayoutTreeDataGrid; diff --git a/TestMasterUI/src/components/layout/LayoutTreeGrid.jsx b/TestMasterUI/src/components/layout/LayoutTreeGrid.jsx index 9a439f0..406aa4f 100644 --- a/TestMasterUI/src/components/layout/LayoutTreeGrid.jsx +++ b/TestMasterUI/src/components/layout/LayoutTreeGrid.jsx @@ -10,6 +10,7 @@ import { TableRow } from '@/components/ui/table'; import ApiLayoutItemTreeBuilder from '@/lib/ApiLayoutItemTreeBuilder.js'; +import EditableInput from '@/components/shared/EditableInput.jsx'; const LayoutTreeGrid = ({ layout, layoutItems }) => { // 트리구조를 만들고, 각 노드의 펼침 여부를 관리하는 상태 @@ -74,11 +75,19 @@ const LayoutTreeGrid = ({ layout, layoutItems }) => { {node.loutItemDesc} {node.loutItemDepth} {node.loutItemType} - {node.loutItemOccCnt} + {node.loutItemOccCnt} {node.loutItemOccRef} - {node.loutItemLength} + {node.loutItemType === 'Field'?node.loutItemLength:''} - {node.loutItemType === 'Field' ? node.value : ''} + {node.loutItemType === 'Field' && ( + + handleNodeChange(node.id, 'loutItemDefault', value) + } + /> + )} {hasChildren && isExpanded && @@ -105,7 +114,7 @@ const LayoutTreeGrid = ({ layout, layoutItems }) => { 반복 횟수 반복 참조 필드 데이터 길이 - + 기본 값 diff --git a/TestMasterUI/src/components/shared/MonacoEditor.jsx b/TestMasterUI/src/components/shared/MonacoEditor.jsx index fccc673..6f0a3f8 100644 --- a/TestMasterUI/src/components/shared/MonacoEditor.jsx +++ b/TestMasterUI/src/components/shared/MonacoEditor.jsx @@ -12,6 +12,14 @@ loader.config({ languages: ['javascript', 'json', 'xml', 'plaintext'] }); +const debounce = (fn, ms) => { + let timer; + return (...args) => { + clearTimeout(timer); + timer = setTimeout(() => fn(...args), ms); + }; +}; + const MonacoEditor = memo(({ id, value = '', @@ -29,7 +37,7 @@ const MonacoEditor = memo(({ const getEditorOptions = (lang) => { const baseOptions = { - automaticLayout: true, + automaticLayout: false, // Changed to false to handle layout manually theme: 'vs-light', minimap: { enabled: false }, readOnly, @@ -40,7 +48,8 @@ const MonacoEditor = memo(({ tabSize: 2, scrollbar: { vertical: 'auto', - horizontal: 'auto' + horizontal: 'auto', + alwaysConsumeMouseWheel: false // Prevent scroll issues } }; @@ -86,6 +95,26 @@ const MonacoEditor = memo(({ subscriptionRef.current = subscription; + // Debounced layout update function + const debouncedLayout = debounce(() => { + if (editorRef.current) { + editorRef.current.layout(); + } + }, 100); // 100ms debounce + + const resizeObserver = new ResizeObserver(() => { + requestAnimationFrame(debouncedLayout); + }); + + // Observe container size changes + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + // Initial layout call + editor.layout(); + + // Format JSON if needed if (language === 'json' && normalizedValue) { try { monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ @@ -94,70 +123,49 @@ const MonacoEditor = memo(({ schemas: [] }); - // Use a ref to track if component is mounted - const isMounted = { current: true }; - - // Format after a short delay to ensure editor is ready - const timeoutId = setTimeout(() => { - if (isMounted.current && editorRef.current) { + // Delayed format action + setTimeout(() => { + if (editorRef.current) { const action = editorRef.current.getAction('editor.action.formatDocument'); if (action) { - try { - action.run(); - } catch (e) { - // Ignore formatting errors - console.debug('Format action failed:', e); - } + action.run().catch(console.debug); } } }, 100); - - return () => { - isMounted.current = false; - clearTimeout(timeoutId); - }; } catch (e) { console.warn('Failed to format JSON:', e); } } - const resizeObserver = new ResizeObserver(() => { - editor.layout(); - }); - - resizeObserver.observe(containerRef.current); - return () => { + debouncedLayout.clear?.(); resizeObserver.disconnect(); subscription.dispose(); if (editor) { - try { - const model = editor.getModel(); - if (model) { - model.dispose(); - } - editor.dispose(); - } catch (e) { - console.warn('Editor disposal error:', e); + const model = editor.getModel(); + if (model) { + model.dispose(); } - } - if (editorRef.current) { - editorRef.current.dispose(); - editorRef.current = null; + editor.dispose(); } }; - }, [language]); // Only recreate when language changes + }, [language]); - - // Handle value updates + // Handle value updates with debouncing useEffect(() => { - if (editorRef.current) { - const currentValue = editorRef.current.getValue(); - if (currentValue !== normalizedValue) { - editorRef.current.setValue(normalizedValue); + const updateValue = debounce(() => { + if (editorRef.current) { + const currentValue = editorRef.current.getValue(); + if (currentValue !== normalizedValue) { + editorRef.current.setValue(normalizedValue); + } } - } + }, 50); + + updateValue(); + + return () => updateValue.clear?.(); }, [normalizedValue]); const getHeightStyle = (height) => { @@ -178,7 +186,8 @@ const MonacoEditor = memo(({ minHeight: '100px', height: getHeightStyle(height), border: '1px solid #e2e8f0', - marginTop: '0.5rem' + marginTop: '0.5rem', + position: 'relative' // Added for stable layout }} /> );