layout data
This commit is contained in:
@@ -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,170 +121,239 @@ 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-2">
|
<div className="flex items-center gap-4">
|
||||||
<SidebarTrigger className="-ml-1" />
|
<div className="flex items-center gap-2">
|
||||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
<SidebarTrigger className="-ml-1" />
|
||||||
<div>
|
<Separator orientation="vertical" className="h-4" />
|
||||||
<span>{currentModel.collectionName}</span>
|
<div>
|
||||||
{' > '}
|
<span className="font-medium">{currentModel.collectionName}</span>
|
||||||
<span>{currentModel.name}</span>
|
{' > '}
|
||||||
</div>
|
<span>{currentModel.name}</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Request Controls */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="shrink-0">
|
|
||||||
<ServerManager
|
|
||||||
onServerChange={handleServerChange}
|
|
||||||
defaultValue={currentModel.server}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 shrink-0">
|
|
||||||
<Button disabled={isLoading} onClick={handleSendRequest}>
|
|
||||||
{isLoading ? 'Sending...' : 'Send'}
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" onClick={handleSave}>Save</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Request/Response Container */}
|
|
||||||
<div className="flex flex-col gap-4 flex-1 min-h-0">
|
|
||||||
{/* Request Section */}
|
|
||||||
<div className="flex-1 min-h-0">
|
|
||||||
<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>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<div className="flex-1 min-h-0 mt-2">
|
|
||||||
{/* Body Tab Content */}
|
|
||||||
<TabsContent value="body" className="h-full m-0">
|
|
||||||
<div className="flex flex-col h-full gap-2">
|
|
||||||
<div className="h-[calc(100%-40px)]">
|
|
||||||
<LayoutGrid
|
|
||||||
layout={currentModel.layout}
|
|
||||||
layoutItems={layoutItems} // Add this
|
|
||||||
setLayoutItems={setLayoutItems} // Add this
|
|
||||||
onChange={handleLayoutUpdate}
|
|
||||||
/>
|
|
||||||
</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>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="layout-json" className="h-full m-0">
|
|
||||||
<MonacoEditor
|
|
||||||
id={`layout-json-${model.id}`}
|
|
||||||
value={editorContents.layout}
|
|
||||||
language="json"
|
|
||||||
onChange={(value) => {
|
|
||||||
setEditorContents(prev => ({...prev, layout: value}));
|
|
||||||
setCurrentModel(prev => ({...prev, layout: JSON.parse(value)}));
|
|
||||||
}}
|
|
||||||
height="full"
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="pre-request" className="h-full m-0">
|
|
||||||
<MonacoEditor
|
|
||||||
id={`pre-request-${model.id}`}
|
|
||||||
value={editorContents.preRequestScript}
|
|
||||||
language="javascript"
|
|
||||||
onChange={(value) => {
|
|
||||||
setEditorContents(prev => ({...prev, preRequestScript: value}));
|
|
||||||
setCurrentModel(prev => ({...prev, preRequestScript: value}));
|
|
||||||
}}
|
|
||||||
height="full"
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Post-Request Tab Content */}
|
|
||||||
<TabsContent value="post-request" className="h-full m-0">
|
|
||||||
<MonacoEditor
|
|
||||||
id={`post-request-${model.id}`}
|
|
||||||
value={editorContents.postRequestScript}
|
|
||||||
language="javascript"
|
|
||||||
onChange={(value) => {
|
|
||||||
setEditorContents(prev => ({...prev, postRequestScript: value}));
|
|
||||||
setCurrentModel(prev => ({...prev, postRequestScript: value}));
|
|
||||||
}}
|
|
||||||
height="full"
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
</div>
|
|
||||||
</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">
|
|
||||||
<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>
|
||||||
|
|
||||||
<Tabs value={currentResponseTab} onValueChange={setCurrentResponseTab} className="flex flex-col h-[calc(100%-1rem)]">
|
|
||||||
<TabsList>
|
|
||||||
<TabsTrigger value="response-body">Body</TabsTrigger>
|
|
||||||
<TabsTrigger value="console">Console</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<div className="flex-1 min-h-0 mt-1">
|
|
||||||
<TabsContent value="response-body" className="h-full m-0">
|
|
||||||
<MonacoEditor
|
|
||||||
id={`response-body-${model.id}`}
|
|
||||||
value={editorContents.responseBody}
|
|
||||||
readOnly={true}
|
|
||||||
height="full"
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="console" className="h-full m-0">
|
|
||||||
<MonacoEditor
|
|
||||||
id={`console-${model.id}`}
|
|
||||||
value={editorContents.consoleLog}
|
|
||||||
language="plaintext"
|
|
||||||
readOnly={true}
|
|
||||||
height="full"
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
</div>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ServerManager
|
||||||
|
onServerChange={handleServerChange}
|
||||||
|
defaultValue={currentModel.server}
|
||||||
|
/>
|
||||||
|
<Button disabled={isLoading} onClick={handleSendRequest}>
|
||||||
|
{isLoading ? 'Sending...' : 'Send'}
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" onClick={handleSave}>Save</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid flex-1 gap-4 min-h-0">
|
||||||
|
{/* 요청 영역 */}
|
||||||
|
<Card className="flex flex-col">
|
||||||
|
<CardHeader className="p-4 pb-2">
|
||||||
|
<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>
|
||||||
|
<TabsTrigger value="body">Layout</TabsTrigger>
|
||||||
|
<TabsTrigger value="layout-tree">Layout Tree</TabsTrigger>
|
||||||
|
<TabsTrigger value="layout-data">Layout Data</TabsTrigger>
|
||||||
|
<TabsTrigger value="layout-json">Layout JSON</TabsTrigger>
|
||||||
|
<TabsTrigger value="pre-request">Pre-Request</TabsTrigger>
|
||||||
|
<TabsTrigger value="post-request">Post-Request</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex-1 min-h-0 mt-2">
|
||||||
|
{/* Body Tab Content */}
|
||||||
|
<TabsContent value="body" className="h-full m-0">
|
||||||
|
<LayoutGrid
|
||||||
|
layout={currentModel.layout}
|
||||||
|
layoutItems={layoutItems} // Add this
|
||||||
|
setLayoutItems={setLayoutItems} // Add this
|
||||||
|
onChange={handleLayoutUpdate}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="layout-tree" className="h-full m-0">
|
||||||
|
<LayoutTreeGrid
|
||||||
|
layout={currentModel.layout}
|
||||||
|
layoutItems={layoutItems} // Add this
|
||||||
|
setLayoutItems={setLayoutItems} // Add this
|
||||||
|
onChange={handleLayoutUpdate}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
<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 value="layout-json" className="h-full m-0">
|
||||||
|
<MonacoEditor
|
||||||
|
id={`layout-json-${model.id}`}
|
||||||
|
value={editorContents.layout}
|
||||||
|
language="json"
|
||||||
|
onChange={(value) => {
|
||||||
|
setEditorContents(prev => ({...prev, layout: value}));
|
||||||
|
setCurrentModel(prev => ({...prev, layout: JSON.parse(value)}));
|
||||||
|
}}
|
||||||
|
height="full"
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="pre-request" className="h-full m-0">
|
||||||
|
<MonacoEditor
|
||||||
|
id={`pre-request-${model.id}`}
|
||||||
|
value={editorContents.preRequestScript}
|
||||||
|
language="javascript"
|
||||||
|
onChange={(value) => {
|
||||||
|
setEditorContents(prev => ({...prev, preRequestScript: value}));
|
||||||
|
setCurrentModel(prev => ({...prev, preRequestScript: value}));
|
||||||
|
}}
|
||||||
|
height="full"
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Post-Request Tab Content */}
|
||||||
|
<TabsContent value="post-request" className="h-full m-0">
|
||||||
|
<MonacoEditor
|
||||||
|
id={`post-request-${model.id}`}
|
||||||
|
value={editorContents.postRequestScript}
|
||||||
|
language="javascript"
|
||||||
|
onChange={(value) => {
|
||||||
|
setEditorContents(prev => ({...prev, postRequestScript: value}));
|
||||||
|
setCurrentModel(prev => ({...prev, postRequestScript: value}));
|
||||||
|
}}
|
||||||
|
height="full"
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<Separator className="my-4" />
|
||||||
|
|
||||||
|
<div className="flex-none">
|
||||||
|
<div className="font-medium mb-2">Request Body</div>
|
||||||
|
<MonacoEditor
|
||||||
|
id={`request-body-${model.id}`}
|
||||||
|
value={currentModel.requestBody}
|
||||||
|
language="plaintext"
|
||||||
|
onChange={(newValue) => {
|
||||||
|
setCurrentModel(prev => ({
|
||||||
|
...prev,
|
||||||
|
requestBody: newValue
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
height="200px"
|
||||||
|
/>
|
||||||
|
<div className="mt-2 text-sm text-gray-600">
|
||||||
|
<strong>전체 메시지 길이:</strong> {currentModel.requestBody?.length || 0}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 응답 영역 */}
|
||||||
|
<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>
|
||||||
|
<TabsTrigger value="response-body">Body</TabsTrigger>
|
||||||
|
<TabsTrigger value="console">Console</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex-1 min-h-0 mt-2">
|
||||||
|
<TabsContent value="response-body" className="h-full m-0">
|
||||||
|
<MonacoEditor
|
||||||
|
id={`response-body-${model.id}`}
|
||||||
|
value={editorContents.responseBody}
|
||||||
|
readOnly={true}
|
||||||
|
height="full"
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="console" className="h-full m-0">
|
||||||
|
<MonacoEditor
|
||||||
|
id={`console-${model.id}`}
|
||||||
|
value={editorContents.consoleLog}
|
||||||
|
language="plaintext"
|
||||||
|
readOnly={true}
|
||||||
|
height="full"
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
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;
|
if (node.children && node.children.length > 0) {
|
||||||
}, [expandedNodes, items, treeRoot.id]);
|
return {
|
||||||
|
...node,
|
||||||
|
children: node.children.map((child) =>
|
||||||
|
updateTreeNode(child, targetId, field, value)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
};
|
||||||
|
|
||||||
const handleMoveNode = useCallback((item, direction) => {
|
// dummy root를 제외한 트리의 자식들을 flatten하여 1차원 배열로 만듭니다.
|
||||||
const parent = ApiLayoutItemTreeBuilder.findParent(treeRoot, item);
|
const flattenTreeNodes = (nodes) => {
|
||||||
if (!parent) return;
|
return nodes.reduce((acc, node) => {
|
||||||
|
return acc.concat([node], flattenTreeNodes(node.children || []));
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
|
||||||
const siblings = parent.children;
|
// 각 노드의 값 변경 시 호출 – tree와 원본 items, layout을 업데이트합니다.
|
||||||
const currentIndex = siblings.indexOf(item);
|
const handleNodeChange = (nodeId, field, value) => {
|
||||||
if (currentIndex === -1) return;
|
const updatedTree = updateTreeNode(treeRoot, nodeId, field, value);
|
||||||
|
setTreeRoot(updatedTree);
|
||||||
|
|
||||||
const newIndex = direction === "up" ? currentIndex - 1 : currentIndex + 1;
|
if (layout) {
|
||||||
if (newIndex >= 0 && newIndex < siblings.length) {
|
layout.computedLayoutItemRoot = updatedTree;
|
||||||
// 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);
|
onChange?.(layout);
|
||||||
}
|
}
|
||||||
}, [items, layout, onChange, treeRoot]);
|
};
|
||||||
|
|
||||||
const renderTreeRow = (item, index) => {
|
// 재귀적으로 트리 노드를 순회하며 테이블 row를 렌더링합니다.
|
||||||
const hasChildren = items.some(i => i.parent === item.id);
|
const renderTreeRows = (node, indentLevel = 0, index) => {
|
||||||
const isExpanded = expandedNodes.has(item.id);
|
const key = `${node.id}-${indentLevel}-${index}`;
|
||||||
const indentLevel = item.loutItemDepth || 1;
|
const hasChildren = node.children && node.children.length > 0;
|
||||||
const paddingLeft = (indentLevel - 1) * 20;
|
const isExpanded = expandedNodes.has(node.id);
|
||||||
|
|
||||||
if (!isVisible(item)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={item.id} className="layout-grid-row" data-node-id={item.id}>
|
<React.Fragment key={key}>
|
||||||
<TableCell className="text-center">
|
<TableRow className="layout-grid-row">
|
||||||
{item.loutItemSerno}
|
{/* # 컬럼: children toggle 표시 (indent 적용) */}
|
||||||
</TableCell>
|
<TableCell className="text-center">
|
||||||
<TableCell>
|
<div
|
||||||
<div className="flex items-center gap-2" style={{ paddingLeft: `${paddingLeft}px` }}>
|
className="flex items-center"
|
||||||
{hasChildren ? (
|
style={{ paddingLeft: `${indentLevel * 10}px` }}
|
||||||
<Button
|
>
|
||||||
variant="ghost"
|
{hasChildren ? (
|
||||||
size="sm"
|
<Button
|
||||||
className="h-4 w-4 p-0"
|
variant="ghost"
|
||||||
onClick={() => toggleNode(item.id)}
|
size="sm"
|
||||||
>
|
className="h-4 w-4 p-0"
|
||||||
{isExpanded ?
|
onClick={() => toggleNode(node.id)}
|
||||||
<ChevronDown className="h-4 w-4" /> :
|
>
|
||||||
<ChevronRight className="h-4 w-4" />
|
{isExpanded ? (
|
||||||
}
|
<ChevronDown className="h-4 w-4" />
|
||||||
</Button>
|
) : (
|
||||||
) : (
|
<ChevronRight className="h-4 w-4" />
|
||||||
<div className="w-4" />
|
)}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<div className="w-4" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
{/* 항목명(영문) (텍스트) */}
|
||||||
|
<TableCell>
|
||||||
|
<span>{node.loutItemName}</span>
|
||||||
|
</TableCell>
|
||||||
|
{/* 항목설명 (텍스트) */}
|
||||||
|
<TableCell>
|
||||||
|
<span>{node.loutItemDesc}</span>
|
||||||
|
</TableCell>
|
||||||
|
{/* 깊이 (텍스트) */}
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<span>{node.loutItemDepth}</span>
|
||||||
|
</TableCell>
|
||||||
|
{/* 아이템 유형 (텍스트) */}
|
||||||
|
<TableCell>
|
||||||
|
<span>{node.loutItemType}</span>
|
||||||
|
</TableCell>
|
||||||
|
{/* 반복 횟수 (텍스트) */}
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<span>{node.loutItemOccCnt}</span>
|
||||||
|
</TableCell>
|
||||||
|
{/* 반복 참조 필드 (텍스트) */}
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<span>{node.loutItemOccRef}</span>
|
||||||
|
</TableCell>
|
||||||
|
{/* 데이터 길이 (텍스트) */}
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<span>{node.loutItemType === 'Field'?node.loutItemLength:''}</span>
|
||||||
|
</TableCell>
|
||||||
|
{/* 값: Field 타입인 경우에만 EditableInput (입력 가능) */}
|
||||||
|
<TableCell>
|
||||||
|
{node.loutItemType === 'Field' && (
|
||||||
|
<EditableInput
|
||||||
|
name={`node-${node.id}-value`}
|
||||||
|
value={node.value || ''}
|
||||||
|
onChange={(value) =>
|
||||||
|
handleNodeChange(node.id, 'value', value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<Input
|
</TableCell>
|
||||||
type="text"
|
</TableRow>
|
||||||
value={item.id}
|
{/* 자식 노드 렌더링 (펼쳐진 경우) */}
|
||||||
onChange={(e) => handleFieldChange(index, "id", e.target.value)}
|
{hasChildren &&
|
||||||
className="w-full"
|
isExpanded &&
|
||||||
/>
|
node.children.map((child, childIndex) =>
|
||||||
</div>
|
renderTreeRows(child, indentLevel + 1, childIndex)
|
||||||
</TableCell>
|
)}
|
||||||
<TableCell>
|
</React.Fragment>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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();
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Editor disposal error:', e);
|
|
||||||
}
|
}
|
||||||
}
|
editor.dispose();
|
||||||
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(() => {
|
||||||
if (editorRef.current) {
|
const updateValue = debounce(() => {
|
||||||
const currentValue = editorRef.current.getValue();
|
if (editorRef.current) {
|
||||||
if (currentValue !== normalizedValue) {
|
const currentValue = editorRef.current.getValue();
|
||||||
editorRef.current.setValue(normalizedValue);
|
if (currentValue !== 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
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user