layout data

This commit is contained in:
현성필
2025-02-02 21:45:02 +09:00
parent 14a004c9e5
commit b02596ed04
5 changed files with 450 additions and 552 deletions
@@ -9,6 +9,8 @@ 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 LayoutTreeGrid from '@/components/layout/LayoutTreeGrid.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}) => { export const TCPClientView = ({model, index}) => {
const apiClient = new APIClient(); const apiClient = new APIClient();
@@ -119,46 +121,100 @@ export const TCPClientView = ({model, index}) => {
layout: updatedLayout 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 ( return (
<div className="flex flex-col h-full p-4 gap-4"> <div className="flex flex-col h-full p-4 gap-4">
{/* Request Header Section */} {/* Request Header Section */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<SidebarTrigger className="-ml-1" /> <SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" /> <Separator orientation="vertical" className="h-4" />
<div> <div>
<span>{currentModel.collectionName}</span> <span className="font-medium">{currentModel.collectionName}</span>
{' > '} {' > '}
<span>{currentModel.name}</span> <span>{currentModel.name}</span>
</div> </div>
</div> </div>
</div>
{/* Request Controls */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="shrink-0">
<ServerManager <ServerManager
onServerChange={handleServerChange} onServerChange={handleServerChange}
defaultValue={currentModel.server} defaultValue={currentModel.server}
/> />
</div>
<div className="flex gap-2 shrink-0">
<Button disabled={isLoading} onClick={handleSendRequest}> <Button disabled={isLoading} onClick={handleSendRequest}>
{isLoading ? 'Sending...' : 'Send'} {isLoading ? 'Sending...' : 'Send'}
</Button> </Button>
<Button variant="secondary" onClick={handleSave}>Save</Button> <Button variant="secondary" onClick={handleSave}>Save</Button>
</div> </div>
</div>
{/* Request/Response Container */} <div className="grid flex-1 gap-4 min-h-0">
<div className="flex flex-col gap-4 flex-1 min-h-0"> {/* 요청 영역 */}
{/* Request Section */} <Card className="flex flex-col">
<div className="flex-1 min-h-0"> <CardHeader className="p-4 pb-2">
<Tabs value={currentTab} onValueChange={setCurrentTab} className="flex flex-col h-full"> <CardTitle>Request</CardTitle>
</CardHeader>
<CardContent className="flex flex-col flex-1 p-4 pt-2 min-h-0">
<Tabs value={currentTab} onValueChange={setCurrentTab} className="flex flex-col flex-1">
<TabsList> <TabsList>
<TabsTrigger value="body">Layout</TabsTrigger> <TabsTrigger value="body">Layout</TabsTrigger>
<TabsTrigger value="layout-tree">Layout Tree</TabsTrigger> <TabsTrigger value="layout-tree">Layout Tree</TabsTrigger>
<TabsTrigger value="layout-data">Layout Data</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>
@@ -167,28 +223,28 @@ export const TCPClientView = ({model, index}) => {
<div className="flex-1 min-h-0 mt-2"> <div className="flex-1 min-h-0 mt-2">
{/* Body Tab Content */} {/* Body Tab Content */}
<TabsContent value="body" className="h-full m-0"> <TabsContent value="body" className="h-full m-0">
<div className="flex flex-col h-full gap-2">
<div className="h-[calc(100%-40px)]">
<LayoutGrid <LayoutGrid
layout={currentModel.layout} layout={currentModel.layout}
layoutItems={layoutItems} // Add this layoutItems={layoutItems} // Add this
setLayoutItems={setLayoutItems} // Add this setLayoutItems={setLayoutItems} // Add this
onChange={handleLayoutUpdate} onChange={handleLayoutUpdate}
/> />
</div>
</div>
</TabsContent> </TabsContent>
<TabsContent value="layout-tree" className="h-full m-0"> <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 <LayoutTreeGrid
layout={currentModel.layout} layout={currentModel.layout}
layoutItems={layoutItems} // Add this layoutItems={layoutItems} // Add this
setLayoutItems={setLayoutItems} // Add this setLayoutItems={setLayoutItems} // Add this
onChange={handleLayoutUpdate} onChange={handleLayoutUpdate}
/> />
</div> </TabsContent>
</div> <TabsContent value="layout-data" className="h-full m-0">
<LayoutTreeDataGrid
layout={currentModel.layout}
layoutItems={layoutItems} // Add this
setLayoutItems={setLayoutItems} // Add this
onChange={handleLayoutDataUpdate}
/>
</TabsContent> </TabsContent>
<TabsContent value="layout-json" className="h-full m-0"> <TabsContent value="layout-json" className="h-full m-0">
<MonacoEditor <MonacoEditor
@@ -230,8 +286,11 @@ export const TCPClientView = ({model, index}) => {
</TabsContent> </TabsContent>
</div> </div>
</Tabs> </Tabs>
</div>
<Separator className="my-4" />
<div className="flex-none">
<div className="font-medium mb-2">Request Body</div>
<MonacoEditor <MonacoEditor
id={`request-body-${model.id}`} id={`request-body-${model.id}`}
value={currentModel.requestBody} value={currentModel.requestBody}
@@ -242,24 +301,37 @@ export const TCPClientView = ({model, index}) => {
requestBody: newValue requestBody: newValue
})); }));
}} }}
height="200px"
/> />
<Separator/> <div className="mt-2 text-sm text-gray-600">
{/* Response Section */} <strong>전체 메시지 길이:</strong> {currentModel.requestBody?.length || 0}
<div className="flex-1 min-h-0">
<div className="flex items-center gap-2 mb-2">
<span>Response</span>
<span className="text-sm text-gray-500">{responseData.status}</span>
<span className="text-sm text-gray-500">{responseData.time}</span>
<span className="text-sm text-gray-500">{responseData.size}</span>
</div> </div>
</div>
</CardContent>
</Card>
<Tabs value={currentResponseTab} onValueChange={setCurrentResponseTab} className="flex flex-col h-[calc(100%-1rem)]"> {/* 응답 영역 */}
<Card className="flex flex-col">
<CardHeader className="p-4 pb-2">
<CardTitle className="flex items-center gap-2">
<span>Response</span>
<span className="text-sm font-normal text-gray-500">{responseData.status}</span>
<span className="text-sm font-normal text-gray-500">{responseData.time}</span>
<span className="text-sm font-normal text-gray-500">{responseData.size}</span>
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col flex-1 p-4 pt-2 min-h-0">
<Tabs
value={currentResponseTab}
onValueChange={setCurrentResponseTab}
className="flex flex-col flex-1"
>
<TabsList> <TabsList>
<TabsTrigger value="response-body">Body</TabsTrigger> <TabsTrigger value="response-body">Body</TabsTrigger>
<TabsTrigger value="console">Console</TabsTrigger> <TabsTrigger value="console">Console</TabsTrigger>
</TabsList> </TabsList>
<div className="flex-1 min-h-0 mt-1"> <div className="flex-1 min-h-0 mt-2">
<TabsContent value="response-body" className="h-full m-0"> <TabsContent value="response-body" className="h-full m-0">
<MonacoEditor <MonacoEditor
id={`response-body-${model.id}`} id={`response-body-${model.id}`}
@@ -279,11 +351,10 @@ export const TCPClientView = ({model, index}) => {
</TabsContent> </TabsContent>
</div> </div>
</Tabs> </Tabs>
</CardContent>
</Card>
</div> </div>
</div> </div>
</div>
); );
}; };
@@ -135,14 +135,12 @@ const LayoutGrid = ({ layout, layoutItems, setLayoutItems, onChange }) => {
<TableHead className="text-center w-[80px]">Parent</TableHead> <TableHead className="text-center w-[80px]">Parent</TableHead>
<TableHead className="text-center w-[120px]">항목명(영문)</TableHead> <TableHead className="text-center w-[120px]">항목명(영문)</TableHead>
<TableHead className="text-center w-[150px]">항목설명</TableHead> <TableHead className="text-center w-[150px]">항목설명</TableHead>
<TableHead className="text-center w-[60px]">깊이</TableHead> <TableHead className="text-center w-[80px]">깊이</TableHead>
<TableHead className="text-center w-[110px]">아이템 유형</TableHead> <TableHead className="text-center w-[110px]">아이템 유형</TableHead>
<TableHead className="text-center w-[80px]">반복 횟수</TableHead> <TableHead className="text-center w-[80px]">반복 횟수</TableHead>
<TableHead className="text-center w-[100px]">반복 참조 필드</TableHead> <TableHead className="text-center w-[100px]">반복 참조 필드</TableHead>
<TableHead className="text-center w-[80px]">데이터 길이</TableHead> <TableHead className="text-center w-[80px]">데이터 길이</TableHead>
<TableHead className="text-center w-[120px]"> <TableHead className="text-center w-[120px]">기본 </TableHead>
</TableHead>
<TableHead className="text-center w-[120px]"> <TableHead className="text-center w-[120px]">
비고 비고
<Button <Button
@@ -281,10 +279,10 @@ const LayoutGrid = ({ layout, layoutItems, setLayoutItems, onChange }) => {
<TableCell> <TableCell>
{item.loutItemType === "Field" && ( {item.loutItemType === "Field" && (
<EditableInput <EditableInput
name={`items[${index}].value`} name={`items[${index}].loutItemDefault`}
value={item.value} value={item.loutItemDefault}
onChange={(value) => onChange={(value) =>
handleFieldChange(index, "value", value) handleFieldChange(index, "loutItemDefault", value)
} }
/> />
)} )}
@@ -1,53 +1,47 @@
import React, { useState, useCallback, useEffect } from "react"; import React, {useCallback, useEffect, useState} from 'react';
import { Button } from "@/components/ui/button"; import {ChevronDown, ChevronRight} from 'lucide-react';
import { Input } from "@/components/ui/input"; import {Button} from '@/components/ui/button';
import { import EditableInput from '../shared/EditableInput';
Select, import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from '@/components/ui/table';
SelectContent, import ApiLayoutItemTreeBuilder from '@/lib/ApiLayoutItemTreeBuilder';
SelectItem, import ApiLayoutItemTreeDataBuilder from '@/lib/ApiLayoutItemTreeDataBuilder';
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 ApiLayoutItemTreeBuilder from "@/lib/ApiLayoutItemTreeBuilder";
import { const LayoutTreeDataGrid = ({ layout, onChange }) => {
Table, // 원본 layoutItems를 state로 관리합니다.
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
const LayoutTreeGrid = ({ layout, onChange }) => {
const [expandedNodes, setExpandedNodes] = useState(new Set());
const [items, setItems] = useState(layout?.layoutItems || []); const [items, setItems] = useState(layout?.layoutItems || []);
const [treeRoot, setTreeRoot] = useState(() => ApiLayoutItemTreeBuilder.createTree(items));
useEffect(() => { // 노드 펼침 상태 관리
// Update tree structure when items change const [expandedNodes, setExpandedNodes] = useState(new Set());
const newRoot = ApiLayoutItemTreeBuilder.createTree(items); // 반복(replication)을 위해 tree data builder 인스턴스를 생성합니다.
setTreeRoot(newRoot);
// 기본 트리를 생성한 후, 반복 처리를 적용하여 최종 트리를 생성합니다.
const buildTree = useCallback(() => {
if (layout?.computedLayoutItemRoot) {
return layout.computedLayoutItemRoot;
}
let replicator = new ApiLayoutItemTreeDataBuilder(10000);
const layoutRoot = ApiLayoutItemTreeBuilder.createTree(items);
return replicator.createReplicatedTree(layoutRoot);
}, [items]); }, [items]);
// treeRoot는 dummy root를 포함한 트리 구조입니다.
const [treeRoot, setTreeRoot] = useState(buildTree);
// 모든 노드의 id를 얻어 초기 확장 상태로 설정
useEffect(() => {
setTreeRoot(buildTree());
const allNodeIds = new Set(flattenTreeNodes(buildTree().children || []).map(node => node.id));
setExpandedNodes(allNodeIds);
}, [items, buildTree]);
// 트리 노드의 펼침/접힘 토글
const toggleNode = (nodeId) => { const toggleNode = (nodeId) => {
setExpandedNodes(prev => { 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);
// Also collapse all children
const collapseChildren = (parentId) => {
items.forEach(item => {
if (item.parent === parentId) {
newSet.delete(item.id);
collapseChildren(item.id);
}
});
};
collapseChildren(nodeId);
} else { } else {
newSet.add(nodeId); newSet.add(nodeId);
} }
@@ -55,299 +49,121 @@ const LayoutTreeGrid = ({ layout, onChange }) => {
}); });
}; };
const getLastId = useCallback(() => { // 재귀적으로 트리 노드에서 target node를 찾아 해당 필드의 값을 업데이트합니다.
let lastId = 0; const updateTreeNode = (node, targetId, field, value) => {
items.forEach((item) => { if (node.id === targetId) {
if (item.id > lastId) { return { ...node, [field]: value };
lastId = item.id;
} }
}); if (node.children && node.children.length > 0) {
return lastId + 1; return {
}, [items]); ...node,
children: node.children.map((child) =>
const handleAddItem = useCallback(() => { updateTreeNode(child, targetId, field, value)
const newItem = { )
id: getLastId(), };
parent: 0, }
level: 1, return node;
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]; // dummy root를 제외한 트리의 자식들을 flatten하여 1차원 배열로 만듭니다.
setItems(newItems); const flattenTreeNodes = (nodes) => {
layout.layoutItems = newItems; return nodes.reduce((acc, node) => {
onChange?.(layout); return acc.concat([node], flattenTreeNodes(node.children || []));
}, [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); // 각 노드의 값 변경 시 호출 – tree와 원본 items, layout을 업데이트합니다.
return items.filter(item => !idsToDelete.has(item.id)); const handleNodeChange = (nodeId, field, value) => {
const updatedTree = updateTreeNode(treeRoot, nodeId, field, value);
setTreeRoot(updatedTree);
if (layout) {
layout.computedLayoutItemRoot = updatedTree;
onChange?.(layout);
}
}; };
const itemToDelete = items[index]; // 재귀적으로 트리 노드를 순회하며 테이블 row를 렌더링합니다.
const newItems = deleteItemAndChildren(itemToDelete.id); const renderTreeRows = (node, indentLevel = 0, index) => {
const key = `${node.id}-${indentLevel}-${index}`;
// Update loutItemSerno values const hasChildren = node.children && node.children.length > 0;
newItems.forEach((item, idx) => { const isExpanded = expandedNodes.has(node.id);
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 (
<TableRow key={item.id} className="layout-grid-row" data-node-id={item.id}> <React.Fragment key={key}>
<TableRow className="layout-grid-row">
{/* # 컬럼: children toggle 표시 (indent 적용) */}
<TableCell className="text-center"> <TableCell className="text-center">
{item.loutItemSerno} <div
</TableCell> className="flex items-center"
<TableCell> style={{ paddingLeft: `${indentLevel * 10}px` }}
<div className="flex items-center gap-2" style={{ paddingLeft: `${paddingLeft}px` }}> >
{hasChildren ? ( {hasChildren ? (
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-4 w-4 p-0" className="h-4 w-4 p-0"
onClick={() => toggleNode(item.id)} onClick={() => toggleNode(node.id)}
> >
{isExpanded ? {isExpanded ? (
<ChevronDown className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" /> <ChevronRight className="h-4 w-4" />
} )}
</Button> </Button>
) : ( ) : (
<div className="w-4" /> <div className="w-4" />
)} )}
<Input
type="text"
value={item.id}
onChange={(e) => handleFieldChange(index, "id", e.target.value)}
className="w-full"
/>
</div> </div>
</TableCell> </TableCell>
{/* 항목명(영문) (텍스트) */}
<TableCell> <TableCell>
<Input <span>{node.loutItemName}</span>
type="text"
value={item.parent}
onChange={(e) => handleFieldChange(index, "parent", e.target.value)}
className="w-full"
/>
</TableCell> </TableCell>
{/* 항목설명 (텍스트) */}
<TableCell> <TableCell>
<Input <span>{node.loutItemDesc}</span>
type="text"
value={item.loutItemName}
onChange={(e) => handleFieldChange(index, "loutItemName", e.target.value)}
className="w-full"
/>
</TableCell> </TableCell>
<TableCell> {/* 깊이 (텍스트) */}
<Input <TableCell className="text-center">
type="text" <span>{node.loutItemDepth}</span>
value={item.loutItemDesc}
onChange={(e) => handleFieldChange(index, "loutItemDesc", e.target.value)}
className="w-full"
/>
</TableCell> </TableCell>
{/* 아이템 유형 (텍스트) */}
<TableCell> <TableCell>
<Input <span>{node.loutItemType}</span>
type="number"
value={item.loutItemDepth}
onChange={(e) => handleFieldChange(index, "loutItemDepth", parseInt(e.target.value))}
min={1}
className="w-full"
/>
</TableCell> </TableCell>
<TableCell> {/* 반복 횟수 (텍스트) */}
<Select <TableCell className="text-center">
value={item.loutItemType} <span>{node.loutItemOccCnt}</span>
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>
<TableCell> {/* 반복 참조 필드 (텍스트) */}
<Input <TableCell className="text-center">
type="text" <span>{node.loutItemOccRef}</span>
value={item.loutItemOccCnt}
onChange={(e) => handleFieldChange(index, "loutItemOccCnt", e.target.value)}
className="w-full"
/>
</TableCell> </TableCell>
<TableCell> {/* 데이터 길이 (텍스트) */}
<Input <TableCell className="text-center">
type="text" <span>{node.loutItemType === 'Field'?node.loutItemLength:''}</span>
value={item.loutItemOccRef}
onChange={(e) => handleFieldChange(index, "loutItemOccRef", e.target.value)}
className="w-full"
/>
</TableCell> </TableCell>
{/* 값: Field 타입인 경우에만 EditableInput (입력 가능) */}
<TableCell> <TableCell>
<Input {node.loutItemType === 'Field' && (
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 <EditableInput
name={`items[${index}].value`} name={`node-${node.id}-value`}
value={item.value || ""} value={node.value || ''}
onChange={(value) => handleFieldChange(index, "value", value)} onChange={(value) =>
handleNodeChange(node.id, 'value', value)
}
/> />
)} )}
</TableCell> </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> </TableRow>
{/* 자식 노드 렌더링 (펼쳐진 경우) */}
{hasChildren &&
isExpanded &&
node.children.map((child, childIndex) =>
renderTreeRows(child, indentLevel + 1, childIndex)
)}
</React.Fragment>
); );
}; };
@@ -357,35 +173,30 @@ const LayoutTreeGrid = ({ layout, onChange }) => {
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead className="text-center w-[50px]">#</TableHead> <TableHead className="text-center w-[50px]">#</TableHead>
<TableHead className="text-center w-[80px]">ID</TableHead> <TableHead className="text-center w-[120px]">
<TableHead className="text-center w-[80px]">Parent</TableHead> 항목명(영문)
<TableHead className="text-center w-[120px]">항목명(영문)</TableHead> </TableHead>
<TableHead className="text-center w-[150px]">항목설명</TableHead> <TableHead className="text-center w-[150px]">항목설명</TableHead>
<TableHead className="text-center w-[60px]">깊이</TableHead> <TableHead className="text-center w-[60px]">깊이</TableHead>
<TableHead className="text-center w-[110px]">아이템 유형</TableHead> <TableHead className="text-center w-[110px]">아이템 유형</TableHead>
<TableHead className="text-center w-[80px]">반복 횟수</TableHead> <TableHead className="text-center w-[80px]">반복 횟수</TableHead>
<TableHead className="text-center w-[100px]">반복 참조 필드</TableHead> <TableHead className="text-center w-[100px]">
반복 참조 필드
</TableHead>
<TableHead className="text-center w-[80px]">데이터 길이</TableHead> <TableHead className="text-center w-[80px]">데이터 길이</TableHead>
<TableHead className="text-center w-[120px]"></TableHead> <TableHead className="text-center w-[120px]"></TableHead>
<TableHead className="text-center w-[120px]">
비고
<Button
variant="outline"
size="sm"
onClick={handleAddItem}
className="ml-2"
>
<Plus className="h-4 w-4" />
</Button>
</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{items.map((item, index) => renderTreeRow(item, index))} {treeRoot &&
treeRoot.children &&
treeRoot.children.map((child, index) =>
renderTreeRows(child, 0, index)
)}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
); );
}; };
export default LayoutTreeGrid; export default LayoutTreeDataGrid;
@@ -10,6 +10,7 @@ import {
TableRow TableRow
} from '@/components/ui/table'; } from '@/components/ui/table';
import ApiLayoutItemTreeBuilder from '@/lib/ApiLayoutItemTreeBuilder.js'; import ApiLayoutItemTreeBuilder from '@/lib/ApiLayoutItemTreeBuilder.js';
import EditableInput from '@/components/shared/EditableInput.jsx';
const LayoutTreeGrid = ({ layout, layoutItems }) => { const LayoutTreeGrid = ({ layout, layoutItems }) => {
// 트리구조를 만들고, 각 노드의 펼침 여부를 관리하는 상태 // 트리구조를 만들고, 각 노드의 펼침 여부를 관리하는 상태
@@ -74,11 +75,19 @@ const LayoutTreeGrid = ({ layout, layoutItems }) => {
<TableCell>{node.loutItemDesc}</TableCell> <TableCell>{node.loutItemDesc}</TableCell>
<TableCell className="text-center">{node.loutItemDepth}</TableCell> <TableCell className="text-center">{node.loutItemDepth}</TableCell>
<TableCell className="text-center">{node.loutItemType}</TableCell> <TableCell className="text-center">{node.loutItemType}</TableCell>
<TableCell>{node.loutItemOccCnt}</TableCell> <TableCell className="text-center">{node.loutItemOccCnt}</TableCell>
<TableCell>{node.loutItemOccRef}</TableCell> <TableCell>{node.loutItemOccRef}</TableCell>
<TableCell className="text-center">{node.loutItemLength}</TableCell> <TableCell className="text-center">{node.loutItemType === 'Field'?node.loutItemLength:''}</TableCell>
<TableCell> <TableCell>
{node.loutItemType === 'Field' ? node.value : ''} {node.loutItemType === 'Field' && (
<EditableInput
name={`node-${node.id}-loutItemDefault`}
value={node.loutItemDefault || ''}
onChange={(value) =>
handleNodeChange(node.id, 'loutItemDefault', value)
}
/>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
{hasChildren && isExpanded && {hasChildren && isExpanded &&
@@ -105,7 +114,7 @@ const LayoutTreeGrid = ({ layout, layoutItems }) => {
<TableHead className="text-center w-[80px]">반복 횟수</TableHead> <TableHead className="text-center w-[80px]">반복 횟수</TableHead>
<TableHead className="text-center w-[100px]">반복 참조 필드</TableHead> <TableHead className="text-center w-[100px]">반복 참조 필드</TableHead>
<TableHead className="text-center w-[80px]">데이터 길이</TableHead> <TableHead className="text-center w-[80px]">데이터 길이</TableHead>
<TableHead className="text-center w-[120px]"></TableHead> <TableHead className="text-center w-[120px]">기본 </TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
@@ -12,6 +12,14 @@ loader.config({
languages: ['javascript', 'json', 'xml', 'plaintext'] languages: ['javascript', 'json', 'xml', 'plaintext']
}); });
const debounce = (fn, ms) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
};
const MonacoEditor = memo(({ const MonacoEditor = memo(({
id, id,
value = '', value = '',
@@ -29,7 +37,7 @@ const MonacoEditor = memo(({
const getEditorOptions = (lang) => { const getEditorOptions = (lang) => {
const baseOptions = { const baseOptions = {
automaticLayout: true, automaticLayout: false, // Changed to false to handle layout manually
theme: 'vs-light', theme: 'vs-light',
minimap: { enabled: false }, minimap: { enabled: false },
readOnly, readOnly,
@@ -40,7 +48,8 @@ const MonacoEditor = memo(({
tabSize: 2, tabSize: 2,
scrollbar: { scrollbar: {
vertical: 'auto', vertical: 'auto',
horizontal: 'auto' horizontal: 'auto',
alwaysConsumeMouseWheel: false // Prevent scroll issues
} }
}; };
@@ -86,6 +95,26 @@ const MonacoEditor = memo(({
subscriptionRef.current = subscription; 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) { if (language === 'json' && normalizedValue) {
try { try {
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
@@ -94,70 +123,49 @@ const MonacoEditor = memo(({
schemas: [] schemas: []
}); });
// Use a ref to track if component is mounted // Delayed format action
const isMounted = { current: true }; setTimeout(() => {
if (editorRef.current) {
// 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'); const action = editorRef.current.getAction('editor.action.formatDocument');
if (action) { if (action) {
try { action.run().catch(console.debug);
action.run();
} catch (e) {
// Ignore formatting errors
console.debug('Format action failed:', e);
}
} }
} }
}, 100); }, 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);
} }
} }
const resizeObserver = new ResizeObserver(() => {
editor.layout();
});
resizeObserver.observe(containerRef.current);
return () => { return () => {
debouncedLayout.clear?.();
resizeObserver.disconnect(); resizeObserver.disconnect();
subscription.dispose(); subscription.dispose();
if (editor) { if (editor) {
try {
const model = editor.getModel(); const model = editor.getModel();
if (model) { if (model) {
model.dispose(); model.dispose();
} }
editor.dispose(); editor.dispose();
} catch (e) {
console.warn('Editor disposal error:', e);
}
}
if (editorRef.current) {
editorRef.current.dispose();
editorRef.current = null;
} }
}; };
}, [language]); // Only recreate when language changes }, [language]);
// Handle value updates with debouncing
// Handle value updates
useEffect(() => { useEffect(() => {
const updateValue = debounce(() => {
if (editorRef.current) { if (editorRef.current) {
const currentValue = editorRef.current.getValue(); const currentValue = editorRef.current.getValue();
if (currentValue !== normalizedValue) { if (currentValue !== normalizedValue) {
editorRef.current.setValue(normalizedValue); editorRef.current.setValue(normalizedValue);
} }
} }
}, 50);
updateValue();
return () => updateValue.clear?.();
}, [normalizedValue]); }, [normalizedValue]);
const getHeightStyle = (height) => { const getHeightStyle = (height) => {
@@ -178,7 +186,8 @@ const MonacoEditor = memo(({
minHeight: '100px', minHeight: '100px',
height: getHeightStyle(height), height: getHeightStyle(height),
border: '1px solid #e2e8f0', border: '1px solid #e2e8f0',
marginTop: '0.5rem' marginTop: '0.5rem',
position: 'relative' // Added for stable layout
}} }}
/> />
); );