레이아웃 트리

This commit is contained in:
현성필
2025-02-02 15:38:09 +09:00
parent ff0208db2f
commit 53d33fd4d3
5 changed files with 539 additions and 375 deletions
@@ -8,8 +8,7 @@ import APIClient from '@/services/APIClient';
import {SidebarTrigger} from '@/components/ui/sidebar.jsx'; import {SidebarTrigger} from '@/components/ui/sidebar.jsx';
import {Separator} from '@/components/ui/separator.jsx'; import {Separator} from '@/components/ui/separator.jsx';
import LayoutGrid from '@/components/layout/LayoutGrid.jsx'; import LayoutGrid from '@/components/layout/LayoutGrid.jsx';
import ApiLayoutItemTreeBuilder from '@/lib/ApiLayoutItemTreeBuilder.js'; import LayoutTreeGrid from '@/components/layout/LayoutTreeGrid.jsx';
import ApiLayoutItemTreeDataBuilder from '@/lib/ApiLayoutItemTreeDataBuilder.js';
export const TCPClientView = ({model, index}) => { export const TCPClientView = ({model, index}) => {
const apiClient = new APIClient(); const apiClient = new APIClient();
@@ -18,6 +17,7 @@ export const TCPClientView = ({model, index}) => {
const [currentModel, setCurrentModel] = useState(model); const [currentModel, setCurrentModel] = useState(model);
const [currentTab, setCurrentTab] = useState('body'); const [currentTab, setCurrentTab] = useState('body');
const [currentResponseTab, setCurrentResponseTab] = useState('response-body'); const [currentResponseTab, setCurrentResponseTab] = useState('response-body');
const [layoutItems, setLayoutItems] = useState(model.layout?.layoutItems || []);
useEffect(() => { useEffect(() => {
setCurrentModel(prev => ({ setCurrentModel(prev => ({
@@ -28,13 +28,6 @@ export const TCPClientView = ({model, index}) => {
})); }));
}, [model]); }, [model]);
const [layoutRoot, setLayoutRoot] = useState(() => {
const root = ApiLayoutItemTreeBuilder.createTree(model.layout.layoutItems);
const replicator = new ApiLayoutItemTreeDataBuilder(10000);
return replicator.createReplicatedTree(root);
});
const [responseData, setResponseData] = useState({ const [responseData, setResponseData] = useState({
headers: [], headers: [],
status: '', status: '',
@@ -120,12 +113,14 @@ export const TCPClientView = ({model, index}) => {
}; };
const handleLayoutUpdate = useCallback((updatedLayout) => { const handleLayoutUpdate = useCallback((updatedLayout) => {
const updatedRoot = ApiLayoutItemTreeBuilder.createTree(updatedLayout.layoutItems); setLayoutItems(updatedLayout.layoutItems); // Add this
const replicator = new ApiLayoutItemTreeDataBuilder(10000); setCurrentModel(prev => ({ // Update this to use setState
const newRoot = replicator.createReplicatedTree(updatedRoot); ...prev,
setLayoutRoot(newRoot); layout: updatedLayout
currentModel.layout = updatedLayout; }));
}, [currentModel]);
console.log({updatedLayout});
}, []);
return ( return (
<div className="flex flex-col h-full p-4 gap-4"> <div className="flex flex-col h-full p-4 gap-4">
@@ -163,6 +158,7 @@ export const TCPClientView = ({model, index}) => {
<Tabs value={currentTab} onValueChange={setCurrentTab} className="flex flex-col h-full"> <Tabs value={currentTab} onValueChange={setCurrentTab} className="flex flex-col h-full">
<TabsList> <TabsList>
<TabsTrigger value="body">Layout</TabsTrigger> <TabsTrigger value="body">Layout</TabsTrigger>
<TabsTrigger value="layout-tree">Layout Tree</TabsTrigger>
<TabsTrigger value="layout-json">Layout JSON</TabsTrigger> <TabsTrigger value="layout-json">Layout JSON</TabsTrigger>
<TabsTrigger value="pre-request">Pre-Request</TabsTrigger> <TabsTrigger value="pre-request">Pre-Request</TabsTrigger>
<TabsTrigger value="post-request">Post-Request</TabsTrigger> <TabsTrigger value="post-request">Post-Request</TabsTrigger>
@@ -175,18 +171,21 @@ export const TCPClientView = ({model, index}) => {
<div className="h-[calc(100%-40px)]"> <div className="h-[calc(100%-40px)]">
<LayoutGrid <LayoutGrid
layout={currentModel.layout} layout={currentModel.layout}
layoutItems={layoutItems} // Add this
setLayoutItems={setLayoutItems} // Add this
onChange={handleLayoutUpdate} onChange={handleLayoutUpdate}
/> />
<MonacoEditor </div>
id={`request-body-${model.id}`} </div>
value={currentModel.requestBody} </TabsContent>
language="plaintext" <TabsContent value="layout-tree" className="h-full m-0">
onChange={(newValue) => { <div className="flex flex-col h-full gap-2">
setCurrentModel(prev => ({ <div className="h-[calc(100%-40px)]">
...prev, <LayoutTreeGrid
requestBody: newValue layout={currentModel.layout}
})); layoutItems={layoutItems} // Add this
}} setLayoutItems={setLayoutItems} // Add this
onChange={handleLayoutUpdate}
/> />
</div> </div>
</div> </div>
@@ -233,6 +232,17 @@ export const TCPClientView = ({model, index}) => {
</Tabs> </Tabs>
</div> </div>
<MonacoEditor
id={`request-body-${model.id}`}
value={currentModel.requestBody}
language="plaintext"
onChange={(newValue) => {
setCurrentModel(prev => ({
...prev,
requestBody: newValue
}));
}}
/>
<Separator/> <Separator/>
{/* Response Section */} {/* Response Section */}
<div className="flex-1 min-h-0"> <div className="flex-1 min-h-0">
@@ -21,7 +21,7 @@ import {
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
const LayoutGrid = ({ layout, onChange }) => { const LayoutGrid = ({ layout, layoutItems, setLayoutItems, onChange }) => {
const [items, setItems] = useState(layout?.layoutItems || []); const [items, setItems] = useState(layout?.layoutItems || []);
const getLastId = useCallback(() => { const getLastId = useCallback(() => {
@@ -111,18 +111,18 @@ const LayoutGrid = ({ layout, onChange }) => {
const handleFieldChange = useCallback( const handleFieldChange = useCallback(
(index, field, value) => { (index, field, value) => {
const newItems = [...items]; const newItems = [...layoutItems]; // Use layoutItems from props
newItems[index][field] = value; newItems[index][field] = value;
if (field === "value" && newItems[index].value === undefined) { if (field === "value" && newItems[index].value === undefined) {
newItems[index].value = ""; newItems[index].value = "";
} }
setItems(newItems); setLayoutItems(newItems); // Use setLayoutItems from props
layout.layoutItems = newItems; layout.layoutItems = newItems;
onChange?.(layout); onChange?.(layout);
}, },
[items, layout, onChange] [layoutItems, layout, onChange, setLayoutItems] // Update dependencies
); );
return ( return (
@@ -157,7 +157,7 @@ const LayoutGrid = ({ layout, onChange }) => {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{items.map((item, index) => ( {layoutItems.map((item, index) => (
<TableRow key={item.id} className="layout-grid-row"> <TableRow key={item.id} className="layout-grid-row">
<TableCell className="text-center"> <TableCell className="text-center">
{item.loutItemSerno} {item.loutItemSerno}
@@ -1,214 +1,391 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useCallback, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ChevronRight, ChevronDown, Trash2, Plus } from "lucide-react";
import EditableInput from "../shared/EditableInput";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import EditableInput from '../shared/EditableInput'; import ApiLayoutItemTreeBuilder from "@/lib/ApiLayoutItemTreeBuilder";
import _ from 'lodash';
import APIClient from '@/services/APIClient.js';
// Helper function to get indentation for tree visualization import {
const getIndent = (depth) => { Table,
return Array(depth + 1).fill().map((_, i) => ( TableBody,
<span key={i} className="treegrid-indent" /> TableCell,
)); TableHead,
}; TableHeader,
TableRow,
} from "@/components/ui/table";
const TreeNode = ({ const LayoutTreeGrid = ({ layout, onChange }) => {
node, const [expandedNodes, setExpandedNodes] = useState(new Set());
depth, const [items, setItems] = useState(layout?.layoutItems || []);
options, const [treeRoot, setTreeRoot] = useState(() => ApiLayoutItemTreeBuilder.createTree(items));
isFirst,
onExpand,
expanded,
onValueChange,
getNodePath
}) => {
const hasChildren = node.children && node.children.length > 0;
const renderCell = (fieldInfo) => { useEffect(() => {
const padding = fieldInfo.isFirst ? getIndent(depth) : null; // Update tree structure when items change
const nodePath = getNodePath(node.id) + '.value'; const newRoot = ApiLayoutItemTreeBuilder.createTree(items);
setTreeRoot(newRoot);
}, [items]);
if (node.loutItemType === 'Field' && fieldInfo.type === 'textfield') { const toggleNode = (nodeId) => {
return ( setExpandedNodes(prev => {
<EditableInput const newSet = new Set(prev);
name={fieldInfo.name} if (newSet.has(nodeId)) {
value={node[fieldInfo.name] || ''} newSet.delete(nodeId);
onChange={(value) => onValueChange(nodePath, value)} // Also collapse all children
/> const collapseChildren = (parentId) => {
); items.forEach(item => {
if (item.parent === parentId) {
newSet.delete(item.id);
collapseChildren(item.id);
}
});
};
collapseChildren(nodeId);
} else {
newSet.add(nodeId);
}
return newSet;
});
};
const getLastId = useCallback(() => {
let lastId = 0;
items.forEach((item) => {
if (item.id > lastId) {
lastId = item.id;
}
});
return lastId + 1;
}, [items]);
const handleAddItem = useCallback(() => {
const newItem = {
id: getLastId(),
parent: 0,
level: 1,
loutName: "",
loutItemName: "",
loutItemDesc: "",
loutItemDepth: 1,
loutItemType: "Field",
loutItemOccCnt: "",
loutItemOccRef: "",
loutItemDataType: "String",
loutItemLength: 1,
loutItemDecimal: 0,
loutItemDefault: "",
loutItemMaskYn: "N",
loutItemMaskOffset: 0,
loutItemMaskLength: 0,
parentLoutItemIndex: 0,
expanded: true,
isLeaf: true,
LOUTITEMPATH: "",
value: "",
loutItemSerno: items.length + 1,
};
const newItems = [...items, newItem];
setItems(newItems);
layout.layoutItems = newItems;
onChange?.(layout);
}, [items, layout, onChange, getLastId]);
const handleFieldChange = useCallback(
(index, field, value) => {
const newItems = [...items];
newItems[index][field] = value;
if (field === "value" && newItems[index].value === undefined) {
newItems[index].value = "";
}
// Handle special cases for Grid and Group types
if (field === "loutItemType" && ["Grid", "Group"].includes(value)) {
const item = newItems[index];
if (item.loutItemOccCnt === "*" && item.loutItemOccRef) {
const refItem = newItems.find(i => i.loutItemName === item.loutItemOccRef);
if (refItem) {
item.childOccCnt = parseInt(refItem.value, 10);
}
} else if (item.loutItemOccCnt) {
item.childOccCnt = parseInt(item.loutItemOccCnt, 10);
}
}
setItems(newItems);
layout.layoutItems = newItems;
onChange?.(layout);
},
[items, layout, onChange]
);
const handleDelete = useCallback(
(index) => {
const deleteItemAndChildren = (itemId) => {
const idsToDelete = new Set();
const collectIds = (id) => {
idsToDelete.add(id);
items.forEach(item => {
if (item.parent === id) {
collectIds(item.id);
}
});
};
collectIds(itemId);
return items.filter(item => !idsToDelete.has(item.id));
};
const itemToDelete = items[index];
const newItems = deleteItemAndChildren(itemToDelete.id);
// Update loutItemSerno values
newItems.forEach((item, idx) => {
item.loutItemSerno = idx + 1;
});
setItems(newItems);
layout.layoutItems = newItems;
onChange?.(layout);
},
[items, layout, onChange]
);
const isVisible = useCallback((item) => {
if (item.parent === 0 || item.parent === treeRoot.id) return true;
let current = item;
while (current && current.parent !== 0 && current.parent !== treeRoot.id) {
if (!expandedNodes.has(current.parent)) {
return false;
}
current = items.find(i => i.id === current.parent);
if (!current) return false;
}
return true;
}, [expandedNodes, items, treeRoot.id]);
const handleMoveNode = useCallback((item, direction) => {
const parent = ApiLayoutItemTreeBuilder.findParent(treeRoot, item);
if (!parent) return;
const siblings = parent.children;
const currentIndex = siblings.indexOf(item);
if (currentIndex === -1) return;
const newIndex = direction === "up" ? currentIndex - 1 : currentIndex + 1;
if (newIndex >= 0 && newIndex < siblings.length) {
// Swap nodes in the tree
[siblings[currentIndex], siblings[newIndex]] = [siblings[newIndex], siblings[currentIndex]];
// Update the layout items accordingly
const newItems = [...items];
newItems.forEach((item, idx) => {
item.loutItemSerno = idx + 1;
});
setItems(newItems);
layout.layoutItems = newItems;
onChange?.(layout);
}
}, [items, layout, onChange, treeRoot]);
const renderTreeRow = (item, index) => {
const hasChildren = items.some(i => i.parent === item.id);
const isExpanded = expandedNodes.has(item.id);
const indentLevel = item.loutItemDepth || 1;
const paddingLeft = (indentLevel - 1) * 20;
if (!isVisible(item)) {
return null;
} }
return ( return (
<div className="flex items-center"> <TableRow key={item.id} className="layout-grid-row" data-node-id={item.id}>
{padding} <TableCell className="text-center">
{fieldInfo.isFirst && hasChildren && ( {item.loutItemSerno}
<button </TableCell>
onClick={() => onExpand(node.id)} <TableCell>
className="treegrid-expander w-4 h-4 flex items-center justify-center" <div className="flex items-center gap-2" style={{ paddingLeft: `${paddingLeft}px` }}>
{hasChildren ? (
<Button
variant="ghost"
size="sm"
className="h-4 w-4 p-0"
onClick={() => toggleNode(item.id)}
>
{isExpanded ?
<ChevronDown className="h-4 w-4" /> :
<ChevronRight className="h-4 w-4" />
}
</Button>
) : (
<div className="w-4" />
)}
<Input
type="text"
value={item.id}
onChange={(e) => handleFieldChange(index, "id", e.target.value)}
className="w-full"
/>
</div>
</TableCell>
<TableCell>
<Input
type="text"
value={item.parent}
onChange={(e) => handleFieldChange(index, "parent", e.target.value)}
className="w-full"
/>
</TableCell>
<TableCell>
<Input
type="text"
value={item.loutItemName}
onChange={(e) => handleFieldChange(index, "loutItemName", e.target.value)}
className="w-full"
/>
</TableCell>
<TableCell>
<Input
type="text"
value={item.loutItemDesc}
onChange={(e) => handleFieldChange(index, "loutItemDesc", e.target.value)}
className="w-full"
/>
</TableCell>
<TableCell>
<Input
type="number"
value={item.loutItemDepth}
onChange={(e) => handleFieldChange(index, "loutItemDepth", parseInt(e.target.value))}
min={1}
className="w-full"
/>
</TableCell>
<TableCell>
<Select
value={item.loutItemType}
onValueChange={(value) => handleFieldChange(index, "loutItemType", value)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Field">Field</SelectItem>
<SelectItem value="Grid">Grid</SelectItem>
<SelectItem value="Group">Group</SelectItem>
<SelectItem value="Attr">Attr</SelectItem>
</SelectContent>
</Select>
</TableCell>
<TableCell>
<Input
type="text"
value={item.loutItemOccCnt}
onChange={(e) => handleFieldChange(index, "loutItemOccCnt", e.target.value)}
className="w-full"
/>
</TableCell>
<TableCell>
<Input
type="text"
value={item.loutItemOccRef}
onChange={(e) => handleFieldChange(index, "loutItemOccRef", e.target.value)}
className="w-full"
/>
</TableCell>
<TableCell>
<Input
type="number"
value={item.loutItemLength}
onChange={(e) => handleFieldChange(index, "loutItemLength", parseInt(e.target.value))}
min={1}
className="w-full"
/>
</TableCell>
<TableCell>
{item.loutItemType === "Field" && (
<EditableInput
name={`items[${index}].value`}
value={item.value || ""}
onChange={(value) => handleFieldChange(index, "value", value)}
/>
)}
</TableCell>
<TableCell className="text-center">
<div className="flex justify-center space-x-1">
<Button
variant="outline"
size="sm"
onClick={() => handleMoveNode(item, "up")}
> >
{expanded ? '-' : '+'} <ChevronUp className="h-4 w-4" />
</button> </Button>
)} <Button
{node[fieldInfo.name]} variant="outline"
</div> size="sm"
onClick={() => handleMoveNode(item, "down")}
>
<ChevronDown className="h-4 w-4" />
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => handleDelete(index)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
); );
}; };
return ( return (
<tr className={`treegrid-${node.id} ${node.parent ? `treegrid-parent-${node.parent}` : ''}`}> <div className="overflow-x-auto">
{options.map((fieldInfo, index) => ( <Table>
<td <TableHeader>
key={fieldInfo.name} <TableRow>
className={cn( <TableHead className="text-center w-[50px]">#</TableHead>
"border p-2", <TableHead className="text-center w-[80px]">ID</TableHead>
fieldInfo.align === 'center' && "text-center", <TableHead className="text-center w-[80px]">Parent</TableHead>
fieldInfo.align === 'right' && "text-right" <TableHead className="text-center w-[120px]">항목명(영문)</TableHead>
)} <TableHead className="text-center w-[150px]">항목설명</TableHead>
style={{ width: fieldInfo.width }} <TableHead className="text-center w-[60px]">깊이</TableHead>
> <TableHead className="text-center w-[110px]">아이템 유형</TableHead>
{renderCell(fieldInfo)} <TableHead className="text-center w-[80px]">반복 횟수</TableHead>
</td> <TableHead className="text-center w-[100px]">반복 참조 필드</TableHead>
))} <TableHead className="text-center w-[80px]">데이터 길이</TableHead>
</tr> <TableHead className="text-center w-[120px]"></TableHead>
); <TableHead className="text-center w-[120px]">
}; 비고
<Button
const LayoutTreeDataGrid = ({ variant="outline"
root, size="sm"
apiRequest, onClick={handleAddItem}
options, className="ml-2"
totalMessageLengthSelector,
onUpdate
}) => {
const [expandedNodes, setExpandedNodes] = useState(new Set());
const [apiClient] = useState(() => new APIClient());
const getNodePath = useCallback((targetId, node = root, path = '') => {
if (node.id === targetId) return path;
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
const currentPath = path ? `${path}.children[${i}]` : `children[${i}]`;
const resultPath = getNodePath(targetId, node.children[i], currentPath);
if (resultPath) return resultPath;
}
}
return null;
}, [root]);
const handleExpand = useCallback((nodeId) => {
setExpandedNodes(prev => {
const next = new Set(prev);
if (next.has(nodeId)) {
next.delete(nodeId);
} else {
next.add(nodeId);
}
return next;
});
}, []);
const fillPadding = useCallback(async (value, dataLength) => {
let paddedValue = await apiClient.processPreScriptAndVariables(apiRequest, value.toString());
while (paddedValue.length < dataLength) {
paddedValue = ' ' + paddedValue;
}
return paddedValue;
}, [apiRequest]);
const concatenateFieldValues = useCallback(async (node) => {
let concatenatedValues = '';
if (node.loutItemType === 'Field') {
concatenatedValues += await fillPadding(node.value || '', node.loutItemLength) || '';
}
if (node.children?.length > 0) {
for (const child of node.children) {
concatenatedValues += await concatenateFieldValues(child);
}
}
return concatenatedValues;
}, [fillPadding]);
const sumFieldLoutItemLengths = useCallback(() => {
const queue = [root];
let sum = 0;
while (queue.length > 0) {
const node = queue.shift();
if (node.loutItemType === 'Field') {
sum += parseInt(node.loutItemLength) || 0;
}
if (node.children?.length > 0) {
queue.push(...node.children);
}
}
return sum;
}, [root]);
const handleValueChange = useCallback(async (path, value) => {
_.set(root, path, value);
const finalMessage = await concatenateFieldValues(root);
onUpdate?.(finalMessage, root);
}, [root, concatenateFieldValues, onUpdate]);
useEffect(() => {
if (totalMessageLengthSelector) {
totalMessageLengthSelector.textContent = sumFieldLoutItemLengths();
}
}, [root, sumFieldLoutItemLengths, totalMessageLengthSelector]);
const renderNodes = useCallback((node, depth = 0) => {
const isExpanded = expandedNodes.has(node.id);
const result = [];
result.push(
<TreeNode
key={node.id}
node={node}
depth={depth}
options={options}
isFirst={true}
onExpand={handleExpand}
expanded={isExpanded}
onValueChange={handleValueChange}
getNodePath={getNodePath}
/>
);
if (isExpanded && node.children?.length > 0) {
node.children.forEach(child => {
result.push(...renderNodes(child, depth + 1));
});
}
return result;
}, [expandedNodes, options, handleExpand, handleValueChange, getNodePath]);
return (
<div className="overflow-auto">
<table className="w-full border-collapse border">
<thead>
<tr>
{options.map(field => (
<th
key={field.name}
className="border p-2 text-center"
style={{ width: field.width }}
> >
{field.label} <Plus className="h-4 w-4" />
</th> </Button>
))} </TableHead>
</tr> </TableRow>
</thead> </TableHeader>
<tbody> <TableBody>
{renderNodes(root)} {items.map((item, index) => renderTreeRow(item, index))}
</tbody> </TableBody>
</table> </Table>
</div> </div>
); );
}; };
export default LayoutTreeDataGrid; export default LayoutTreeGrid;
@@ -1,36 +1,31 @@
import React, { useState, useCallback } from 'react'; import React, { useEffect, useState } from 'react';
import { cn } from "@/lib/utils"; import { ChevronDown, ChevronRight } from 'lucide-react';
import EditableInput from '../shared/EditableInput'; import { Button } from '@/components/ui/button';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table';
import ApiLayoutItemTreeBuilder from '@/lib/ApiLayoutItemTreeBuilder.js';
const LayoutTreeGrid = ({ root, options = [], onUpdate }) => { const LayoutTreeGrid = ({ layout, layoutItems }) => {
// 트리구조를 만들고, 각 노드의 펼침 여부를 관리하는 상태
const [expandedNodes, setExpandedNodes] = useState(new Set()); const [expandedNodes, setExpandedNodes] = useState(new Set());
const [treeRoot, setTreeRoot] = useState(() =>
ApiLayoutItemTreeBuilder.createTree(layoutItems || [])
);
// Helper function to get node path // layoutItems가 변경되면 트리 구조 재생성
const getNodePath = useCallback((node, targetId, path = '') => { useEffect(() => {
if (node.id === targetId) { setTreeRoot(ApiLayoutItemTreeBuilder.createTree(layoutItems || []));
return path; }, [layoutItems]);
}
if (node.children) { // 노드 토글 함수: 해당 노드의 펼침 여부를 토글합니다.
for (let i = 0; i < node.children.length; i++) { const toggleNode = (nodeId) => {
const currentPath = path ? `${path}.children[${i}]` : `children[${i}]`; setExpandedNodes((prev) => {
const resultPath = getNodePath(node.children[i], targetId, currentPath);
if (resultPath) {
return resultPath;
}
}
}
return null;
}, []);
// Get indentation for tree structure
const getIndent = useCallback((depth) => {
return Array(depth + 1).join(' ');
}, []);
// Handle expand/collapse
const handleExpanderClick = useCallback((nodeId) => {
setExpandedNodes(prev => {
const newSet = new Set(prev); const newSet = new Set(prev);
if (newSet.has(nodeId)) { if (newSet.has(nodeId)) {
newSet.delete(nodeId); newSet.delete(nodeId);
@@ -39,118 +34,86 @@ const LayoutTreeGrid = ({ root, options = [], onUpdate }) => {
} }
return newSet; return newSet;
}); });
}, []); };
// Render cell content // 재귀적으로 트리 노드를 순회하며 테이블 행으로 렌더링하는 함수
const renderCell = useCallback((node, field, depth) => { const renderTreeRows = (node, indentLevel = 0, index) => {
const nodePath = getNodePath(root, node.id) + '.value'; // 부모 노드의 id나 인덱스를 함께 결합하여 키를 만듭니다.
const key = `${node.id}-${indentLevel}-${index}`;
if (node.loutItemType === 'Field' && field.type === 'textfield') { const hasChildren = node.children && node.children.length > 0;
return ( const isExpanded = expandedNodes.has(node.id);
<EditableInput
name={field.name}
value={node[field.name]}
path={nodePath}
onChange={(value) => {
node[field.name] = value;
onUpdate?.(root);
}}
/>
);
}
if (field.isFirst) { return (
const hasChildren = node.children?.length > 0; <React.Fragment key={key}>
return ( <TableRow className="layout-grid-row">
<div className="flex items-center"> <TableCell className="text-center">{node.loutItemSerno}</TableCell>
<span className="inline-block" style={{ width: `${depth * 20}px` }} /> <TableCell>
{hasChildren && ( <div className="flex items-center" style={{ paddingLeft: `${indentLevel * 10}px` }}>
<button {hasChildren ? (
onClick={() => handleExpanderClick(node.id)} <Button
className="w-4 h-4 flex items-center justify-center mr-1" variant="ghost"
> size="sm"
{expandedNodes.has(node.id) ? '' : '+'} className="h-4 w-4 p-0"
</button> onClick={() => toggleNode(node.id)}
)} >
<span>{node[field.name]}</span> {isExpanded ? (
</div> <ChevronDown className="h-4 w-4" />
); ) : (
} <ChevronRight className="h-4 w-4" />
)}
return node[field.name]; </Button>
}, [root, expandedNodes, handleExpanderClick, getNodePath, onUpdate]); ) : (
<div className="w-4" />
// Recursive function to render tree rows )}
const renderTreeRows = useCallback((node, depth = 0, parentId = null) => { <span>{node.id}</span>
const rows = []; </div>
</TableCell>
const isVisible = depth === 0 || !parentId || expandedNodes.has(parentId); <TableCell className="text-center">{node.parent}</TableCell>
<TableCell>{node.loutItemName}</TableCell>
if (!isVisible) { <TableCell>{node.loutItemDesc}</TableCell>
return rows; <TableCell className="text-center">{node.loutItemDepth}</TableCell>
} <TableCell className="text-center">{node.loutItemType}</TableCell>
<TableCell>{node.loutItemOccCnt}</TableCell>
const row = ( <TableCell>{node.loutItemOccRef}</TableCell>
<tr <TableCell className="text-center">{node.loutItemLength}</TableCell>
key={node.id} <TableCell>
className={cn( {node.loutItemType === 'Field' ? node.value : ''}
"layout-grid-row", </TableCell>
depth === 0 && "hidden", </TableRow>
parentId && `treegrid-parent-${parentId}` {hasChildren && isExpanded &&
)} node.children.map((child, childIndex) =>
data-node-id={node.id} renderTreeRows(child, indentLevel + 1, childIndex)
> )}
{options.map((field, fieldIndex) => ( </React.Fragment>
<td
key={fieldIndex}
className={cn(
"border p-2",
field.align && `text-${field.align}`,
"whitespace-nowrap"
)}
style={{ width: field.width }}
>
{renderCell(node, field, depth)}
</td>
))}
</tr>
); );
};
rows.push(row);
if (node.children?.length > 0) {
node.children.forEach(child => {
rows.push(...renderTreeRows(child, depth + 1, node.id));
});
}
return rows;
}, [options, expandedNodes, renderCell]);
return ( return (
<div className="overflow-auto"> <div className="overflow-x-auto">
<table className="w-full border-collapse border"> <Table>
<thead> <TableHeader>
<tr> <TableRow>
{options.map((field, index) => ( <TableHead className="text-center w-[50px]">#</TableHead>
<th <TableHead className="text-center w-[80px]">ID</TableHead>
key={index} <TableHead className="text-center w-[80px]">Parent</TableHead>
className={cn( <TableHead className="text-center w-[120px]">항목명(영문)</TableHead>
"border p-2", <TableHead className="text-center w-[150px]">항목설명</TableHead>
field.align && `text-${field.align}`, <TableHead className="text-center w-[60px]">깊이</TableHead>
"whitespace-nowrap" <TableHead className="text-center w-[110px]">아이템 유형</TableHead>
)} <TableHead className="text-center w-[80px]">반복 횟수</TableHead>
style={{ width: field.width }} <TableHead className="text-center w-[100px]">반복 참조 필드</TableHead>
> <TableHead className="text-center w-[80px]">데이터 길이</TableHead>
{field.label} <TableHead className="text-center w-[120px]"></TableHead>
</th> </TableRow>
))} </TableHeader>
</tr> <TableBody>
</thead> {/* treeRoot는 일반적으로 Root 노드(예: id가 0 또는 첫번째 parent)로 생성되므로,
<tbody> 실제 렌더링은 Root의 자식부터 시작 */}
{renderTreeRows(root)} {treeRoot && treeRoot.children.map((child) => renderTreeRows(child))}
</tbody> </TableBody>
</table> </Table>
</div> </div>
); );
}; };
@@ -1,6 +1,6 @@
import React, { useEffect, useRef, memo } from 'react'; import React, { useEffect, useRef, memo } from 'react';
import * as monaco from 'monaco-editor'; import * as monaco from 'monaco-editor';
import {loader} from '@monaco-editor/react'; import { loader } from '@monaco-editor/react';
loader.config({ loader.config({
paths: { paths: {
@@ -9,8 +9,7 @@ loader.config({
'vs/nls': { 'vs/nls': {
availableLanguages: {}, availableLanguages: {},
}, },
// Specify which languages to load languages: ['javascript', 'json', 'xml', 'plaintext']
languages: ['javascript', 'json', 'xml','plaintext']
}); });
const MonacoEditor = memo(({ const MonacoEditor = memo(({
@@ -95,12 +94,28 @@ const MonacoEditor = memo(({
schemas: [] schemas: []
}); });
const action = editor.getAction('editor.action.formatDocument'); // Use a ref to track if component is mounted
if (action) { const isMounted = { current: true };
setTimeout(() => {
action.run(); // Format after a short delay to ensure editor is ready
}, 100); const timeoutId = setTimeout(() => {
} if (isMounted.current && 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);
}
}
}
}, 100);
return () => {
isMounted.current = false;
clearTimeout(timeoutId);
};
} catch (e) { } catch (e) {
console.warn('Failed to format JSON:', e); console.warn('Failed to format JSON:', e);
} }
@@ -116,7 +131,6 @@ const MonacoEditor = memo(({
resizeObserver.disconnect(); resizeObserver.disconnect();
subscription.dispose(); subscription.dispose();
// Ensure proper cleanup
if (editor) { if (editor) {
try { try {
const model = editor.getModel(); const model = editor.getModel();
@@ -128,9 +142,14 @@ const MonacoEditor = memo(({
console.warn('Editor disposal error:', e); console.warn('Editor disposal error:', e);
} }
} }
if (editorRef.current) {
editorRef.current.dispose();
editorRef.current = null;
}
}; };
}, [language]); // Only recreate when language changes }, [language]); // Only recreate when language changes
// Handle value updates // Handle value updates
useEffect(() => { useEffect(() => {
if (editorRef.current) { if (editorRef.current) {
@@ -141,11 +160,6 @@ const MonacoEditor = memo(({
} }
}, [normalizedValue]); }, [normalizedValue]);
// Convert height to pixel value if it's a number
// const containerHeight = typeof height === 'number'
// ? `${height}px`
// : height.endsWith('px') ? height : `${height}px`;
const getHeightStyle = (height) => { const getHeightStyle = (height) => {
if (height === 'full') { if (height === 'full') {
return '100%'; return '100%';