레이아웃 트리

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 {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';
import LayoutTreeGrid from '@/components/layout/LayoutTreeGrid.jsx';
export const TCPClientView = ({model, index}) => {
const apiClient = new APIClient();
@@ -18,6 +17,7 @@ export const TCPClientView = ({model, index}) => {
const [currentModel, setCurrentModel] = useState(model);
const [currentTab, setCurrentTab] = useState('body');
const [currentResponseTab, setCurrentResponseTab] = useState('response-body');
const [layoutItems, setLayoutItems] = useState(model.layout?.layoutItems || []);
useEffect(() => {
setCurrentModel(prev => ({
@@ -28,13 +28,6 @@ export const TCPClientView = ({model, index}) => {
}));
}, [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({
headers: [],
status: '',
@@ -120,12 +113,14 @@ export const TCPClientView = ({model, index}) => {
};
const handleLayoutUpdate = useCallback((updatedLayout) => {
const updatedRoot = ApiLayoutItemTreeBuilder.createTree(updatedLayout.layoutItems);
const replicator = new ApiLayoutItemTreeDataBuilder(10000);
const newRoot = replicator.createReplicatedTree(updatedRoot);
setLayoutRoot(newRoot);
currentModel.layout = updatedLayout;
}, [currentModel]);
setLayoutItems(updatedLayout.layoutItems); // Add this
setCurrentModel(prev => ({ // Update this to use setState
...prev,
layout: updatedLayout
}));
console.log({updatedLayout});
}, []);
return (
<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">
<TabsList>
<TabsTrigger value="body">Layout</TabsTrigger>
<TabsTrigger value="layout-tree">Layout Tree</TabsTrigger>
<TabsTrigger value="layout-json">Layout JSON</TabsTrigger>
<TabsTrigger value="pre-request">Pre-Request</TabsTrigger>
<TabsTrigger value="post-request">Post-Request</TabsTrigger>
@@ -175,18 +171,21 @@ export const TCPClientView = ({model, index}) => {
<div className="h-[calc(100%-40px)]">
<LayoutGrid
layout={currentModel.layout}
layoutItems={layoutItems} // Add this
setLayoutItems={setLayoutItems} // Add this
onChange={handleLayoutUpdate}
/>
<MonacoEditor
id={`request-body-${model.id}`}
value={currentModel.requestBody}
language="plaintext"
onChange={(newValue) => {
setCurrentModel(prev => ({
...prev,
requestBody: newValue
}));
}}
</div>
</div>
</TabsContent>
<TabsContent value="layout-tree" className="h-full m-0">
<div className="flex flex-col h-full gap-2">
<div className="h-[calc(100%-40px)]">
<LayoutTreeGrid
layout={currentModel.layout}
layoutItems={layoutItems} // Add this
setLayoutItems={setLayoutItems} // Add this
onChange={handleLayoutUpdate}
/>
</div>
</div>
@@ -233,6 +232,17 @@ export const TCPClientView = ({model, index}) => {
</Tabs>
</div>
<MonacoEditor
id={`request-body-${model.id}`}
value={currentModel.requestBody}
language="plaintext"
onChange={(newValue) => {
setCurrentModel(prev => ({
...prev,
requestBody: newValue
}));
}}
/>
<Separator/>
{/* Response Section */}
<div className="flex-1 min-h-0">
@@ -21,7 +21,7 @@ import {
TableRow,
} from "@/components/ui/table";
const LayoutGrid = ({ layout, onChange }) => {
const LayoutGrid = ({ layout, layoutItems, setLayoutItems, onChange }) => {
const [items, setItems] = useState(layout?.layoutItems || []);
const getLastId = useCallback(() => {
@@ -111,18 +111,18 @@ const LayoutGrid = ({ layout, onChange }) => {
const handleFieldChange = useCallback(
(index, field, value) => {
const newItems = [...items];
const newItems = [...layoutItems]; // Use layoutItems from props
newItems[index][field] = value;
if (field === "value" && newItems[index].value === undefined) {
newItems[index].value = "";
}
setItems(newItems);
setLayoutItems(newItems); // Use setLayoutItems from props
layout.layoutItems = newItems;
onChange?.(layout);
},
[items, layout, onChange]
[layoutItems, layout, onChange, setLayoutItems] // Update dependencies
);
return (
@@ -157,7 +157,7 @@ const LayoutGrid = ({ layout, onChange }) => {
</TableRow>
</TableHeader>
<TableBody>
{items.map((item, index) => (
{layoutItems.map((item, index) => (
<TableRow key={item.id} className="layout-grid-row">
<TableCell className="text-center">
{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 EditableInput from '../shared/EditableInput';
import _ from 'lodash';
import APIClient from '@/services/APIClient.js';
import ApiLayoutItemTreeBuilder from "@/lib/ApiLayoutItemTreeBuilder";
// Helper function to get indentation for tree visualization
const getIndent = (depth) => {
return Array(depth + 1).fill().map((_, i) => (
<span key={i} className="treegrid-indent" />
));
};
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
const TreeNode = ({
node,
depth,
options,
isFirst,
onExpand,
expanded,
onValueChange,
getNodePath
}) => {
const hasChildren = node.children && node.children.length > 0;
const renderCell = (fieldInfo) => {
const padding = fieldInfo.isFirst ? getIndent(depth) : null;
const nodePath = getNodePath(node.id) + '.value';
if (node.loutItemType === 'Field' && fieldInfo.type === 'textfield') {
return (
<EditableInput
name={fieldInfo.name}
value={node[fieldInfo.name] || ''}
onChange={(value) => onValueChange(nodePath, value)}
/>
);
}
return (
<div className="flex items-center">
{padding}
{fieldInfo.isFirst && hasChildren && (
<button
onClick={() => onExpand(node.id)}
className="treegrid-expander w-4 h-4 flex items-center justify-center"
>
{expanded ? '-' : '+'}
</button>
)}
{node[fieldInfo.name]}
</div>
);
};
return (
<tr className={`treegrid-${node.id} ${node.parent ? `treegrid-parent-${node.parent}` : ''}`}>
{options.map((fieldInfo, index) => (
<td
key={fieldInfo.name}
className={cn(
"border p-2",
fieldInfo.align === 'center' && "text-center",
fieldInfo.align === 'right' && "text-right"
)}
style={{ width: fieldInfo.width }}
>
{renderCell(fieldInfo)}
</td>
))}
</tr>
);
};
const LayoutTreeDataGrid = ({
root,
apiRequest,
options,
totalMessageLengthSelector,
onUpdate
}) => {
const LayoutTreeGrid = ({ layout, onChange }) => {
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]);
const [items, setItems] = useState(layout?.layoutItems || []);
const [treeRoot, setTreeRoot] = useState(() => ApiLayoutItemTreeBuilder.createTree(items));
useEffect(() => {
if (totalMessageLengthSelector) {
totalMessageLengthSelector.textContent = sumFieldLoutItemLengths();
// Update tree structure when items change
const newRoot = ApiLayoutItemTreeBuilder.createTree(items);
setTreeRoot(newRoot);
}, [items]);
const toggleNode = (nodeId) => {
setExpandedNodes(prev => {
const newSet = new Set(prev);
if (newSet.has(nodeId)) {
newSet.delete(nodeId);
// Also collapse all children
const collapseChildren = (parentId) => {
items.forEach(item => {
if (item.parent === parentId) {
newSet.delete(item.id);
collapseChildren(item.id);
}
}, [root, sumFieldLoutItemLengths, totalMessageLengthSelector]);
});
};
collapseChildren(nodeId);
} else {
newSet.add(nodeId);
}
return newSet;
});
};
const renderNodes = useCallback((node, depth = 0) => {
const isExpanded = expandedNodes.has(node.id);
const result = [];
const getLastId = useCallback(() => {
let lastId = 0;
items.forEach((item) => {
if (item.id > lastId) {
lastId = item.id;
}
});
return lastId + 1;
}, [items]);
result.push(
<TreeNode
key={node.id}
node={node}
depth={depth}
options={options}
isFirst={true}
onExpand={handleExpand}
expanded={isExpanded}
onValueChange={handleValueChange}
getNodePath={getNodePath}
/>
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]
);
if (isExpanded && node.children?.length > 0) {
node.children.forEach(child => {
result.push(...renderNodes(child, depth + 1));
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 result;
}, [expandedNodes, options, handleExpand, handleValueChange, getNodePath]);
return (
<TableRow key={item.id} className="layout-grid-row" data-node-id={item.id}>
<TableCell className="text-center">
{item.loutItemSerno}
</TableCell>
<TableCell>
<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")}
>
<ChevronUp className="h-4 w-4" />
</Button>
<Button
variant="outline"
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 (
<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 }}
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="text-center w-[50px]">#</TableHead>
<TableHead className="text-center w-[80px]">ID</TableHead>
<TableHead className="text-center w-[80px]">Parent</TableHead>
<TableHead className="text-center w-[120px]">항목명(영문)</TableHead>
<TableHead className="text-center w-[150px]">항목설명</TableHead>
<TableHead className="text-center w-[60px]">깊이</TableHead>
<TableHead className="text-center w-[110px]">아이템 유형</TableHead>
<TableHead className="text-center w-[80px]">반복 횟수</TableHead>
<TableHead className="text-center w-[100px]">반복 참조 필드</TableHead>
<TableHead className="text-center w-[80px]">데이터 길이</TableHead>
<TableHead className="text-center w-[120px]"></TableHead>
<TableHead className="text-center w-[120px]">
비고
<Button
variant="outline"
size="sm"
onClick={handleAddItem}
className="ml-2"
>
{field.label}
</th>
))}
</tr>
</thead>
<tbody>
{renderNodes(root)}
</tbody>
</table>
<Plus className="h-4 w-4" />
</Button>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item, index) => renderTreeRow(item, index))}
</TableBody>
</Table>
</div>
);
};
export default LayoutTreeDataGrid;
export default LayoutTreeGrid;
@@ -1,36 +1,31 @@
import React, { useState, useCallback } from 'react';
import { cn } from "@/lib/utils";
import EditableInput from '../shared/EditableInput';
import React, { useEffect, useState } from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react';
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 [treeRoot, setTreeRoot] = useState(() =>
ApiLayoutItemTreeBuilder.createTree(layoutItems || [])
);
// Helper function to get node path
const getNodePath = useCallback((node, targetId, path = '') => {
if (node.id === targetId) {
return path;
}
// layoutItems가 변경되면 트리 구조 재생성
useEffect(() => {
setTreeRoot(ApiLayoutItemTreeBuilder.createTree(layoutItems || []));
}, [layoutItems]);
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
const currentPath = path ? `${path}.children[${i}]` : `children[${i}]`;
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 toggleNode = (nodeId) => {
setExpandedNodes((prev) => {
const newSet = new Set(prev);
if (newSet.has(nodeId)) {
newSet.delete(nodeId);
@@ -39,118 +34,86 @@ const LayoutTreeGrid = ({ root, options = [], onUpdate }) => {
}
return newSet;
});
}, []);
};
// Render cell content
const renderCell = useCallback((node, field, depth) => {
const nodePath = getNodePath(root, node.id) + '.value';
// 재귀적으로 트리 노드를 순회하며 테이블 행으로 렌더링하는 함수
const renderTreeRows = (node, indentLevel = 0, index) => {
// 부모 노드의 id나 인덱스를 함께 결합하여 키를 만듭니다.
const key = `${node.id}-${indentLevel}-${index}`;
const hasChildren = node.children && node.children.length > 0;
const isExpanded = expandedNodes.has(node.id);
if (node.loutItemType === 'Field' && field.type === 'textfield') {
return (
<EditableInput
name={field.name}
value={node[field.name]}
path={nodePath}
onChange={(value) => {
node[field.name] = value;
onUpdate?.(root);
}}
/>
);
}
if (field.isFirst) {
const hasChildren = node.children?.length > 0;
return (
<div className="flex items-center">
<span className="inline-block" style={{ width: `${depth * 20}px` }} />
{hasChildren && (
<button
onClick={() => handleExpanderClick(node.id)}
className="w-4 h-4 flex items-center justify-center mr-1"
<React.Fragment key={key}>
<TableRow className="layout-grid-row">
<TableCell className="text-center">{node.loutItemSerno}</TableCell>
<TableCell>
<div className="flex items-center" style={{ paddingLeft: `${indentLevel * 10}px` }}>
{hasChildren ? (
<Button
variant="ghost"
size="sm"
className="h-4 w-4 p-0"
onClick={() => toggleNode(node.id)}
>
{expandedNodes.has(node.id) ? '' : '+'}
</button>
{isExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
<span>{node[field.name]}</span>
</Button>
) : (
<div className="w-4" />
)}
<span>{node.id}</span>
</div>
);
}
return node[field.name];
}, [root, expandedNodes, handleExpanderClick, getNodePath, onUpdate]);
// Recursive function to render tree rows
const renderTreeRows = useCallback((node, depth = 0, parentId = null) => {
const rows = [];
const isVisible = depth === 0 || !parentId || expandedNodes.has(parentId);
if (!isVisible) {
return rows;
}
const row = (
<tr
key={node.id}
className={cn(
"layout-grid-row",
depth === 0 && "hidden",
parentId && `treegrid-parent-${parentId}`
</TableCell>
<TableCell className="text-center">{node.parent}</TableCell>
<TableCell>{node.loutItemName}</TableCell>
<TableCell>{node.loutItemDesc}</TableCell>
<TableCell className="text-center">{node.loutItemDepth}</TableCell>
<TableCell className="text-center">{node.loutItemType}</TableCell>
<TableCell>{node.loutItemOccCnt}</TableCell>
<TableCell>{node.loutItemOccRef}</TableCell>
<TableCell className="text-center">{node.loutItemLength}</TableCell>
<TableCell>
{node.loutItemType === 'Field' ? node.value : ''}
</TableCell>
</TableRow>
{hasChildren && isExpanded &&
node.children.map((child, childIndex) =>
renderTreeRows(child, indentLevel + 1, childIndex)
)}
data-node-id={node.id}
>
{options.map((field, fieldIndex) => (
<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>
</React.Fragment>
);
};
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 (
<div className="overflow-auto">
<table className="w-full border-collapse border">
<thead>
<tr>
{options.map((field, index) => (
<th
key={index}
className={cn(
"border p-2",
field.align && `text-${field.align}`,
"whitespace-nowrap"
)}
style={{ width: field.width }}
>
{field.label}
</th>
))}
</tr>
</thead>
<tbody>
{renderTreeRows(root)}
</tbody>
</table>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="text-center w-[50px]">#</TableHead>
<TableHead className="text-center w-[80px]">ID</TableHead>
<TableHead className="text-center w-[80px]">Parent</TableHead>
<TableHead className="text-center w-[120px]">항목명(영문)</TableHead>
<TableHead className="text-center w-[150px]">항목설명</TableHead>
<TableHead className="text-center w-[60px]">깊이</TableHead>
<TableHead className="text-center w-[110px]">아이템 유형</TableHead>
<TableHead className="text-center w-[80px]">반복 횟수</TableHead>
<TableHead className="text-center w-[100px]">반복 참조 필드</TableHead>
<TableHead className="text-center w-[80px]">데이터 길이</TableHead>
<TableHead className="text-center w-[120px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{/* treeRoot는 일반적으로 Root 노드(예: id가 0 또는 첫번째 parent)로 생성되므로,
실제 렌더링은 Root의 자식부터 시작 */}
{treeRoot && treeRoot.children.map((child) => renderTreeRows(child))}
</TableBody>
</Table>
</div>
);
};
@@ -9,7 +9,6 @@ loader.config({
'vs/nls': {
availableLanguages: {},
},
// Specify which languages to load
languages: ['javascript', 'json', 'xml', 'plaintext']
});
@@ -95,12 +94,28 @@ const MonacoEditor = memo(({
schemas: []
});
const action = editor.getAction('editor.action.formatDocument');
// 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) {
const action = editorRef.current.getAction('editor.action.formatDocument');
if (action) {
setTimeout(() => {
try {
action.run();
}, 100);
} catch (e) {
// Ignore formatting errors
console.debug('Format action failed:', e);
}
}
}
}, 100);
return () => {
isMounted.current = false;
clearTimeout(timeoutId);
};
} catch (e) {
console.warn('Failed to format JSON:', e);
}
@@ -116,7 +131,6 @@ const MonacoEditor = memo(({
resizeObserver.disconnect();
subscription.dispose();
// Ensure proper cleanup
if (editor) {
try {
const model = editor.getModel();
@@ -128,9 +142,14 @@ const MonacoEditor = memo(({
console.warn('Editor disposal error:', e);
}
}
if (editorRef.current) {
editorRef.current.dispose();
editorRef.current = null;
}
};
}, [language]); // Only recreate when language changes
// Handle value updates
useEffect(() => {
if (editorRef.current) {
@@ -141,11 +160,6 @@ const MonacoEditor = memo(({
}
}, [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) => {
if (height === 'full') {
return '100%';