1396 lines
63 KiB
TypeScript
1396 lines
63 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useMemo, useEffect } from "react";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Slider } from "@/components/ui/slider";
|
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Check, ChevronsUpDown, ArrowRight, Plus, X } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { SplitPanelLayoutConfig } from "./types";
|
|
import { TableInfo, ColumnInfo } from "@/types/screen";
|
|
import { tableTypeApi } from "@/lib/api/screen";
|
|
|
|
interface SplitPanelLayoutConfigPanelProps {
|
|
config: SplitPanelLayoutConfig;
|
|
onChange: (config: SplitPanelLayoutConfig) => void;
|
|
tables?: TableInfo[]; // 전체 테이블 목록 (선택적)
|
|
screenTableName?: string; // 현재 화면의 테이블명 (좌측 패널에서 사용)
|
|
}
|
|
|
|
/**
|
|
* SplitPanelLayout 설정 패널
|
|
*/
|
|
export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelProps> = ({
|
|
config,
|
|
onChange,
|
|
tables = [], // 기본값 빈 배열 (현재 화면 테이블만)
|
|
screenTableName, // 현재 화면의 테이블명
|
|
}) => {
|
|
const [rightTableOpen, setRightTableOpen] = useState(false);
|
|
const [leftColumnOpen, setLeftColumnOpen] = useState(false);
|
|
const [rightColumnOpen, setRightColumnOpen] = useState(false);
|
|
const [loadedTableColumns, setLoadedTableColumns] = useState<Record<string, ColumnInfo[]>>({});
|
|
const [loadingColumns, setLoadingColumns] = useState<Record<string, boolean>>({});
|
|
const [allTables, setAllTables] = useState<any[]>([]); // 조인 모드용 전체 테이블 목록
|
|
|
|
// 관계 타입
|
|
const relationshipType = config.rightPanel?.relation?.type || "detail";
|
|
|
|
// 조인 모드일 때만 전체 테이블 목록 로드
|
|
useEffect(() => {
|
|
if (relationshipType === "join") {
|
|
const loadAllTables = async () => {
|
|
try {
|
|
const { tableManagementApi } = await import("@/lib/api/tableManagement");
|
|
const response = await tableManagementApi.getTableList();
|
|
if (response.success && response.data) {
|
|
console.log("✅ 분할패널 조인 모드: 전체 테이블 목록 로드", response.data.length, "개");
|
|
setAllTables(response.data);
|
|
}
|
|
} catch (error) {
|
|
console.error("❌ 전체 테이블 목록 로드 실패:", error);
|
|
}
|
|
};
|
|
loadAllTables();
|
|
} else {
|
|
// 상세 모드일 때는 기본 테이블만 사용
|
|
setAllTables([]);
|
|
}
|
|
}, [relationshipType]);
|
|
|
|
// screenTableName이 변경되면 좌측 패널 테이블을 항상 화면 테이블로 설정
|
|
useEffect(() => {
|
|
if (screenTableName) {
|
|
// 좌측 패널은 항상 현재 화면의 테이블 사용
|
|
if (config.leftPanel?.tableName !== screenTableName) {
|
|
updateLeftPanel({ tableName: screenTableName });
|
|
}
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [screenTableName]);
|
|
|
|
// 좌측 패널 테이블 컬럼 로드 완료 시 PK 자동 추가
|
|
useEffect(() => {
|
|
const leftTableName = config.leftPanel?.tableName || screenTableName;
|
|
if (leftTableName && loadedTableColumns[leftTableName] && config.leftPanel?.showAdd) {
|
|
const currentAddModalColumns = config.leftPanel?.addModalColumns || [];
|
|
const updatedColumns = ensurePrimaryKeysInAddModal(leftTableName, currentAddModalColumns);
|
|
|
|
// PK가 추가되었으면 업데이트
|
|
if (updatedColumns.length !== currentAddModalColumns.length) {
|
|
console.log(`🔄 좌측 패널: PK 컬럼 자동 추가 (${leftTableName})`);
|
|
updateLeftPanel({ addModalColumns: updatedColumns });
|
|
}
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [config.leftPanel?.tableName, screenTableName, loadedTableColumns, config.leftPanel?.showAdd]);
|
|
|
|
// 좌측 패널 하위 항목 추가 모달 PK 자동 추가
|
|
useEffect(() => {
|
|
const leftTableName = config.leftPanel?.tableName || screenTableName;
|
|
if (leftTableName && loadedTableColumns[leftTableName] && config.leftPanel?.showItemAddButton) {
|
|
const currentAddModalColumns = config.leftPanel?.itemAddConfig?.addModalColumns || [];
|
|
const updatedColumns = ensurePrimaryKeysInAddModal(leftTableName, currentAddModalColumns);
|
|
|
|
// PK가 추가되었으면 업데이트
|
|
if (updatedColumns.length !== currentAddModalColumns.length) {
|
|
console.log(`🔄 좌측 패널 하위 항목 추가: PK 컬럼 자동 추가 (${leftTableName})`);
|
|
updateLeftPanel({
|
|
itemAddConfig: {
|
|
...config.leftPanel?.itemAddConfig,
|
|
addModalColumns: updatedColumns,
|
|
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
|
|
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
|
|
}
|
|
});
|
|
}
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [config.leftPanel?.tableName, screenTableName, loadedTableColumns, config.leftPanel?.showItemAddButton]);
|
|
|
|
// 우측 패널 테이블 컬럼 로드 완료 시 PK 자동 추가
|
|
useEffect(() => {
|
|
const rightTableName = config.rightPanel?.tableName;
|
|
if (rightTableName && loadedTableColumns[rightTableName] && config.rightPanel?.showAdd) {
|
|
const currentAddModalColumns = config.rightPanel?.addModalColumns || [];
|
|
const updatedColumns = ensurePrimaryKeysInAddModal(rightTableName, currentAddModalColumns);
|
|
|
|
// PK가 추가되었으면 업데이트
|
|
if (updatedColumns.length !== currentAddModalColumns.length) {
|
|
console.log(`🔄 우측 패널: PK 컬럼 자동 추가 (${rightTableName})`);
|
|
updateRightPanel({ addModalColumns: updatedColumns });
|
|
}
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [config.rightPanel?.tableName, loadedTableColumns, config.rightPanel?.showAdd]);
|
|
|
|
// 테이블 컬럼 로드 함수
|
|
const loadTableColumns = async (tableName: string) => {
|
|
if (loadedTableColumns[tableName] || loadingColumns[tableName]) {
|
|
return; // 이미 로드되었거나 로딩 중
|
|
}
|
|
|
|
setLoadingColumns((prev) => ({ ...prev, [tableName]: true }));
|
|
|
|
try {
|
|
const columnsResponse = await tableTypeApi.getColumns(tableName);
|
|
console.log(`📊 테이블 ${tableName} 컬럼 응답:`, columnsResponse);
|
|
|
|
const columns: ColumnInfo[] = (columnsResponse || []).map((col: any) => ({
|
|
tableName: col.tableName || tableName,
|
|
columnName: col.columnName || col.column_name,
|
|
columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
|
|
dataType: col.dataType || col.data_type || col.dbType,
|
|
webType: col.webType || col.web_type,
|
|
input_type: col.inputType || col.input_type,
|
|
widgetType: col.widgetType || col.widget_type || col.webType || col.web_type,
|
|
isNullable: col.isNullable || col.is_nullable,
|
|
required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO",
|
|
columnDefault: col.columnDefault || col.column_default,
|
|
characterMaximumLength: col.characterMaximumLength || col.character_maximum_length,
|
|
isPrimaryKey: col.isPrimaryKey || false, // PK 여부 추가
|
|
codeCategory: col.codeCategory || col.code_category,
|
|
codeValue: col.codeValue || col.code_value,
|
|
}));
|
|
|
|
console.log(`✅ 테이블 ${tableName} 컬럼 ${columns.length}개 로드됨:`, columns);
|
|
setLoadedTableColumns((prev) => ({ ...prev, [tableName]: columns }));
|
|
} catch (error) {
|
|
console.error(`테이블 ${tableName} 컬럼 로드 실패:`, error);
|
|
setLoadedTableColumns((prev) => ({ ...prev, [tableName]: [] }));
|
|
} finally {
|
|
setLoadingColumns((prev) => ({ ...prev, [tableName]: false }));
|
|
}
|
|
};
|
|
|
|
// 좌측/우측 테이블이 변경되면 해당 테이블의 컬럼 로드
|
|
useEffect(() => {
|
|
if (config.leftPanel?.tableName) {
|
|
loadTableColumns(config.leftPanel.tableName);
|
|
}
|
|
}, [config.leftPanel?.tableName]);
|
|
|
|
useEffect(() => {
|
|
if (config.rightPanel?.tableName) {
|
|
loadTableColumns(config.rightPanel.tableName);
|
|
}
|
|
}, [config.rightPanel?.tableName]);
|
|
|
|
console.log("🔧 SplitPanelLayoutConfigPanel 렌더링");
|
|
console.log(" - config:", config);
|
|
console.log(" - tables:", tables);
|
|
console.log(" - tablesCount:", tables.length);
|
|
console.log(" - screenTableName:", screenTableName);
|
|
console.log(" - leftTable:", config.leftPanel?.tableName);
|
|
console.log(" - rightTable:", config.rightPanel?.tableName);
|
|
|
|
const updateConfig = (updates: Partial<SplitPanelLayoutConfig>) => {
|
|
const newConfig = { ...config, ...updates };
|
|
console.log("🔄 Config 업데이트:", newConfig);
|
|
onChange(newConfig);
|
|
};
|
|
|
|
// PK 컬럼을 추가 모달에 자동으로 포함시키는 함수
|
|
const ensurePrimaryKeysInAddModal = (
|
|
tableName: string,
|
|
existingColumns: Array<{ name: string; label: string; required?: boolean }> = []
|
|
) => {
|
|
const tableColumns = loadedTableColumns[tableName];
|
|
if (!tableColumns) {
|
|
console.warn(`⚠️ 테이블 ${tableName}의 컬럼 정보가 로드되지 않음`);
|
|
return existingColumns;
|
|
}
|
|
|
|
// PK 컬럼 찾기
|
|
const pkColumns = tableColumns.filter((col) => col.isPrimaryKey);
|
|
console.log(`🔑 테이블 ${tableName}의 PK 컬럼:`, pkColumns.map(c => c.columnName));
|
|
|
|
// 자동으로 처리되는 컬럼 (백엔드에서 자동 추가)
|
|
const autoHandledColumns = ['company_code', 'company_name'];
|
|
|
|
// 기존 컬럼 이름 목록
|
|
const existingColumnNames = existingColumns.map((col) => col.name);
|
|
|
|
// PK 컬럼을 맨 앞에 추가 (이미 있거나 자동 처리되는 컬럼은 제외)
|
|
const pkColumnsToAdd = pkColumns
|
|
.filter((col) => !existingColumnNames.includes(col.columnName))
|
|
.filter((col) => !autoHandledColumns.includes(col.columnName)) // 자동 처리 컬럼 제외
|
|
.map((col) => ({
|
|
name: col.columnName,
|
|
label: col.columnLabel || col.columnName,
|
|
required: true, // PK는 항상 필수
|
|
}));
|
|
|
|
if (pkColumnsToAdd.length > 0) {
|
|
console.log(`✅ PK 컬럼 ${pkColumnsToAdd.length}개 자동 추가:`, pkColumnsToAdd.map(c => c.name));
|
|
}
|
|
|
|
return [...pkColumnsToAdd, ...existingColumns];
|
|
};
|
|
|
|
const updateLeftPanel = (updates: Partial<SplitPanelLayoutConfig["leftPanel"]>) => {
|
|
const newConfig = {
|
|
...config,
|
|
leftPanel: { ...config.leftPanel, ...updates },
|
|
};
|
|
console.log("🔄 Left Panel 업데이트:", newConfig);
|
|
onChange(newConfig);
|
|
};
|
|
|
|
const updateRightPanel = (updates: Partial<SplitPanelLayoutConfig["rightPanel"]>) => {
|
|
const newConfig = {
|
|
...config,
|
|
rightPanel: { ...config.rightPanel, ...updates },
|
|
};
|
|
console.log("🔄 Right Panel 업데이트:", newConfig);
|
|
onChange(newConfig);
|
|
};
|
|
|
|
// 좌측 테이블 컬럼 (로드된 컬럼 사용)
|
|
const leftTableColumns = useMemo(() => {
|
|
const tableName = config.leftPanel?.tableName || screenTableName;
|
|
return tableName ? loadedTableColumns[tableName] || [] : [];
|
|
}, [loadedTableColumns, config.leftPanel?.tableName, screenTableName]);
|
|
|
|
// 우측 테이블 컬럼 (로드된 컬럼 사용)
|
|
const rightTableColumns = useMemo(() => {
|
|
const tableName = config.rightPanel?.tableName;
|
|
return tableName ? loadedTableColumns[tableName] || [] : [];
|
|
}, [loadedTableColumns, config.rightPanel?.tableName]);
|
|
|
|
// 테이블 데이터 로딩 상태 확인
|
|
if (!tables || tables.length === 0) {
|
|
return (
|
|
<div className="rounded-lg border p-4">
|
|
<p className="text-sm font-medium">테이블 데이터를 불러올 수 없습니다.</p>
|
|
<p className="mt-1 text-xs text-gray-600">
|
|
화면에 테이블이 연결되지 않았거나 테이블 목록이 로드되지 않았습니다.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 조인 모드에서 우측 테이블 선택 시 사용할 테이블 목록
|
|
const availableRightTables = relationshipType === "join" ? allTables : tables;
|
|
|
|
console.log("📊 분할패널 테이블 목록 상태:");
|
|
console.log(" - relationshipType:", relationshipType);
|
|
console.log(" - allTables:", allTables.length, "개");
|
|
console.log(" - availableRightTables:", availableRightTables.length, "개");
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* 관계 타입 선택 */}
|
|
<div className="space-y-3">
|
|
<h3 className="text-sm font-semibold">패널 관계 타입</h3>
|
|
<Select
|
|
value={relationshipType}
|
|
onValueChange={(value: "join" | "detail") => {
|
|
// 상세 모드로 변경 시 우측 테이블을 현재 화면 테이블로 설정
|
|
if (value === "detail" && screenTableName) {
|
|
updateRightPanel({
|
|
relation: { ...config.rightPanel?.relation, type: value },
|
|
tableName: screenTableName,
|
|
});
|
|
} else {
|
|
updateRightPanel({
|
|
relation: { ...config.rightPanel?.relation, type: value },
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
<SelectTrigger className="bg-white">
|
|
<SelectValue placeholder="관계 타입 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="detail">
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">상세 (DETAIL)</span>
|
|
<span className="text-xs text-gray-500">좌측 목록 → 우측 상세 정보 (동일 테이블)</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="join">
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">조인 (JOIN)</span>
|
|
<span className="text-xs text-gray-500">좌측 테이블 → 우측 관련 테이블 (다른 테이블)</span>
|
|
</div>
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 좌측 패널 설정 (마스터) */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-sm font-semibold">좌측 패널 설정 (마스터)</h3>
|
|
|
|
<div className="space-y-2">
|
|
<Label>패널 제목</Label>
|
|
<Input
|
|
value={config.leftPanel?.title || ""}
|
|
onChange={(e) => updateLeftPanel({ title: e.target.value })}
|
|
placeholder="좌측 패널 제목"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>테이블 (현재 화면 고정)</Label>
|
|
<div className="rounded-lg border border-gray-200 bg-gray-50 p-3">
|
|
<p className="text-sm font-medium text-gray-900">
|
|
{config.leftPanel?.tableName || screenTableName || "테이블이 지정되지 않음"}
|
|
</p>
|
|
<p className="mt-1 text-xs text-gray-500">좌측 패널은 현재 화면의 테이블 데이터를 표시합니다</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label>검색 기능</Label>
|
|
<Switch
|
|
checked={config.leftPanel?.showSearch ?? true}
|
|
onCheckedChange={(checked) => updateLeftPanel({ showSearch: checked })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label>추가 버튼</Label>
|
|
<Switch
|
|
checked={config.leftPanel?.showAdd ?? false}
|
|
onCheckedChange={(checked) => updateLeftPanel({ showAdd: checked })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label>각 항목에 + 버튼</Label>
|
|
<Switch
|
|
checked={config.leftPanel?.showItemAddButton ?? false}
|
|
onCheckedChange={(checked) => updateLeftPanel({ showItemAddButton: checked })}
|
|
/>
|
|
</div>
|
|
|
|
{/* 항목별 + 버튼 설정 (하위 항목 추가) */}
|
|
{config.leftPanel?.showItemAddButton && (
|
|
<div className="space-y-3 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
|
<Label className="text-sm font-semibold">하위 항목 추가 설정</Label>
|
|
<p className="text-xs text-gray-600">
|
|
+ 버튼 클릭 시 선택된 항목의 하위 항목을 추가합니다 (예: 부서 → 하위 부서)
|
|
</p>
|
|
|
|
{/* 현재 항목의 값을 가져올 컬럼 (sourceColumn) */}
|
|
<div>
|
|
<Label className="text-xs">현재 항목 ID 컬럼</Label>
|
|
<p className="mb-2 text-[10px] text-gray-500">
|
|
선택된 항목의 어떤 컬럼 값을 사용할지 (예: dept_code)
|
|
</p>
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
className="h-8 w-full justify-between text-xs"
|
|
>
|
|
{config.leftPanel?.itemAddConfig?.sourceColumn || "컬럼 선택"}
|
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-full p-0">
|
|
<Command>
|
|
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
|
|
<CommandEmpty className="text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup className="max-h-[200px] overflow-auto">
|
|
{leftTableColumns
|
|
.filter((column) => !['company_code', 'company_name'].includes(column.columnName))
|
|
.map((column) => (
|
|
<CommandItem
|
|
key={column.columnName}
|
|
value={column.columnName}
|
|
onSelect={(value) => {
|
|
updateLeftPanel({
|
|
itemAddConfig: {
|
|
...config.leftPanel?.itemAddConfig,
|
|
sourceColumn: value,
|
|
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
|
|
}
|
|
});
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
config.leftPanel?.itemAddConfig?.sourceColumn === column.columnName ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
{column.columnLabel || column.columnName}
|
|
<span className="ml-2 text-[10px] text-gray-500">
|
|
({column.columnName})
|
|
</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
{/* 상위 항목 ID를 저장할 컬럼 (parentColumn) */}
|
|
<div>
|
|
<Label className="text-xs">상위 항목 저장 컬럼</Label>
|
|
<p className="mb-2 text-[10px] text-gray-500">
|
|
하위 항목에서 상위 항목 ID를 저장할 컬럼 (예: parent_dept_code)
|
|
</p>
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
className="h-8 w-full justify-between text-xs"
|
|
>
|
|
{config.leftPanel?.itemAddConfig?.parentColumn || "컬럼 선택"}
|
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-full p-0">
|
|
<Command>
|
|
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
|
|
<CommandEmpty className="text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup className="max-h-[200px] overflow-auto">
|
|
{leftTableColumns
|
|
.filter((column) => !['company_code', 'company_name'].includes(column.columnName))
|
|
.map((column) => (
|
|
<CommandItem
|
|
key={column.columnName}
|
|
value={column.columnName}
|
|
onSelect={(value) => {
|
|
updateLeftPanel({
|
|
itemAddConfig: {
|
|
...config.leftPanel?.itemAddConfig,
|
|
parentColumn: value,
|
|
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
|
|
}
|
|
});
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
config.leftPanel?.itemAddConfig?.parentColumn === column.columnName ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
{column.columnLabel || column.columnName}
|
|
<span className="ml-2 text-[10px] text-gray-500">
|
|
({column.columnName})
|
|
</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
{/* 하위 항목 추가 모달 컬럼 설정 */}
|
|
<div className="space-y-2 rounded border border-blue-300 bg-white p-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs font-semibold">추가 모달 입력 컬럼</Label>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
const currentColumns = config.leftPanel?.itemAddConfig?.addModalColumns || [];
|
|
const newColumns = [
|
|
...currentColumns,
|
|
{ name: "", label: "", required: false },
|
|
];
|
|
updateLeftPanel({
|
|
itemAddConfig: {
|
|
...config.leftPanel?.itemAddConfig,
|
|
addModalColumns: newColumns,
|
|
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
|
|
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
|
|
}
|
|
});
|
|
}}
|
|
className="h-6 text-[10px]"
|
|
>
|
|
<Plus className="mr-1 h-3 w-3" />
|
|
컬럼 추가
|
|
</Button>
|
|
</div>
|
|
<p className="text-[10px] text-gray-600">
|
|
하위 항목 추가 시 입력받을 필드를 선택하세요
|
|
</p>
|
|
|
|
<div className="space-y-2">
|
|
{(config.leftPanel?.itemAddConfig?.addModalColumns || []).length === 0 ? (
|
|
<div className="rounded-md border border-dashed border-gray-300 bg-gray-50 p-2 text-center">
|
|
<p className="text-[10px] text-gray-500">설정된 컬럼이 없습니다</p>
|
|
</div>
|
|
) : (
|
|
(config.leftPanel?.itemAddConfig?.addModalColumns || []).map((col, index) => {
|
|
const column = leftTableColumns.find(c => c.columnName === col.name);
|
|
const isPK = column?.isPrimaryKey || false;
|
|
|
|
return (
|
|
<div
|
|
key={index}
|
|
className={cn(
|
|
"flex items-center gap-2 rounded-md border p-2",
|
|
isPK ? "border-yellow-300 bg-yellow-50" : "bg-white"
|
|
)}
|
|
>
|
|
{isPK && (
|
|
<span className="rounded bg-yellow-200 px-1.5 py-0.5 text-[10px] font-semibold text-yellow-700">
|
|
PK
|
|
</span>
|
|
)}
|
|
<div className="flex-1">
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
disabled={isPK}
|
|
className="h-7 w-full justify-between text-[10px]"
|
|
>
|
|
{col.name || "컬럼 선택"}
|
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-full p-0">
|
|
<Command>
|
|
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
|
|
<CommandEmpty className="text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup className="max-h-[200px] overflow-auto">
|
|
{leftTableColumns
|
|
.filter((column) => !['company_code', 'company_name'].includes(column.columnName))
|
|
.map((column) => (
|
|
<CommandItem
|
|
key={column.columnName}
|
|
value={column.columnName}
|
|
onSelect={(value) => {
|
|
const newColumns = [...(config.leftPanel?.itemAddConfig?.addModalColumns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
name: value,
|
|
label: column.columnLabel || value,
|
|
};
|
|
updateLeftPanel({
|
|
itemAddConfig: {
|
|
...config.leftPanel?.itemAddConfig,
|
|
addModalColumns: newColumns,
|
|
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
|
|
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
|
|
}
|
|
});
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
col.name === column.columnName ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
{column.columnLabel || column.columnName}
|
|
<span className="ml-2 text-[10px] text-gray-500">
|
|
({column.columnName})
|
|
</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<label className="flex cursor-pointer items-center gap-1 text-[10px] text-gray-600">
|
|
<input
|
|
type="checkbox"
|
|
checked={col.required ?? false}
|
|
disabled={isPK}
|
|
onChange={(e) => {
|
|
const newColumns = [...(config.leftPanel?.itemAddConfig?.addModalColumns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
required: e.target.checked,
|
|
};
|
|
updateLeftPanel({
|
|
itemAddConfig: {
|
|
...config.leftPanel?.itemAddConfig,
|
|
addModalColumns: newColumns,
|
|
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
|
|
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
|
|
}
|
|
});
|
|
}}
|
|
className="h-3 w-3"
|
|
/>
|
|
필수
|
|
</label>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
disabled={isPK}
|
|
onClick={() => {
|
|
const newColumns = (config.leftPanel?.itemAddConfig?.addModalColumns || []).filter(
|
|
(_, i) => i !== index
|
|
);
|
|
updateLeftPanel({
|
|
itemAddConfig: {
|
|
...config.leftPanel?.itemAddConfig,
|
|
addModalColumns: newColumns,
|
|
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
|
|
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
|
|
}
|
|
});
|
|
}}
|
|
className="h-7 w-7 p-0"
|
|
title={isPK ? "PK 컬럼은 삭제할 수 없습니다" : ""}
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 좌측 패널 추가 모달 컬럼 설정 */}
|
|
{config.leftPanel?.showAdd && (
|
|
<div className="space-y-3 rounded-lg border border-purple-200 bg-purple-50 p-3">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-sm font-semibold">추가 모달 입력 컬럼</Label>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
const currentColumns = config.leftPanel?.addModalColumns || [];
|
|
const newColumns = [
|
|
...currentColumns,
|
|
{ name: "", label: "", required: false },
|
|
];
|
|
updateLeftPanel({ addModalColumns: newColumns });
|
|
}}
|
|
className="h-7 text-xs"
|
|
>
|
|
<Plus className="mr-1 h-3 w-3" />
|
|
컬럼 추가
|
|
</Button>
|
|
</div>
|
|
<p className="text-xs text-gray-600">
|
|
추가 버튼 클릭 시 모달에 표시될 입력 필드를 선택하세요
|
|
</p>
|
|
|
|
<div className="space-y-2">
|
|
{(config.leftPanel?.addModalColumns || []).length === 0 ? (
|
|
<div className="rounded-md border border-dashed border-gray-300 bg-white p-3 text-center">
|
|
<p className="text-xs text-gray-500">설정된 컬럼이 없습니다</p>
|
|
</div>
|
|
) : (
|
|
(config.leftPanel?.addModalColumns || []).map((col, index) => {
|
|
// 현재 컬럼이 PK인지 확인
|
|
const column = leftTableColumns.find(c => c.columnName === col.name);
|
|
const isPK = column?.isPrimaryKey || false;
|
|
|
|
return (
|
|
<div
|
|
key={index}
|
|
className={cn(
|
|
"flex items-center gap-2 rounded-md border p-2",
|
|
isPK ? "bg-yellow-50 border-yellow-300" : "bg-white"
|
|
)}
|
|
>
|
|
{isPK && (
|
|
<span className="text-[10px] font-semibold text-yellow-700 px-1.5 py-0.5 bg-yellow-200 rounded">
|
|
PK
|
|
</span>
|
|
)}
|
|
<div className="flex-1">
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
disabled={isPK}
|
|
className="h-8 w-full justify-between text-xs"
|
|
>
|
|
{col.name || "컬럼 선택"}
|
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-full p-0">
|
|
<Command>
|
|
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
|
|
<CommandEmpty className="text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup className="max-h-[200px] overflow-auto">
|
|
{leftTableColumns
|
|
.filter((column) => !['company_code', 'company_name'].includes(column.columnName))
|
|
.map((column) => (
|
|
<CommandItem
|
|
key={column.columnName}
|
|
value={column.columnName}
|
|
onSelect={(value) => {
|
|
const newColumns = [...(config.leftPanel?.addModalColumns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
name: value,
|
|
label: column.columnLabel || value,
|
|
};
|
|
updateLeftPanel({ addModalColumns: newColumns });
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
col.name === column.columnName ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
{column.columnLabel || column.columnName}
|
|
<span className="ml-2 text-[10px] text-gray-500">
|
|
({column.columnName})
|
|
</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<label className="flex items-center gap-1 text-xs text-gray-600 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={col.required ?? false}
|
|
disabled={isPK}
|
|
onChange={(e) => {
|
|
const newColumns = [...(config.leftPanel?.addModalColumns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
required: e.target.checked,
|
|
};
|
|
updateLeftPanel({ addModalColumns: newColumns });
|
|
}}
|
|
className="h-3 w-3"
|
|
/>
|
|
필수
|
|
</label>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
disabled={isPK}
|
|
onClick={() => {
|
|
const newColumns = (config.leftPanel?.addModalColumns || []).filter(
|
|
(_, i) => i !== index
|
|
);
|
|
updateLeftPanel({ addModalColumns: newColumns });
|
|
}}
|
|
className="h-8 w-8 p-0"
|
|
title={isPK ? "PK 컬럼은 삭제할 수 없습니다" : ""}
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 우측 패널 설정 */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-sm font-semibold">우측 패널 설정 ({relationshipType === "detail" ? "상세" : "조인"})</h3>
|
|
|
|
<div className="space-y-2">
|
|
<Label>패널 제목</Label>
|
|
<Input
|
|
value={config.rightPanel?.title || ""}
|
|
onChange={(e) => updateRightPanel({ title: e.target.value })}
|
|
placeholder="우측 패널 제목"
|
|
/>
|
|
</div>
|
|
|
|
{/* 관계 타입에 따라 테이블 선택 UI 변경 */}
|
|
{relationshipType === "detail" ? (
|
|
// 상세 모드: 좌측과 동일한 테이블 (자동 설정)
|
|
<div className="space-y-2">
|
|
<Label>테이블 (좌측과 동일)</Label>
|
|
<div className="rounded-lg border border-gray-200 bg-gray-50 p-3">
|
|
<p className="text-sm font-medium text-gray-900">
|
|
{config.leftPanel?.tableName || screenTableName || "테이블이 지정되지 않음"}
|
|
</p>
|
|
<p className="mt-1 text-xs text-gray-500">상세 모드에서는 좌측과 동일한 테이블을 사용합니다</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
// 조인 모드: 전체 테이블에서 선택 가능
|
|
<div className="space-y-2">
|
|
<Label>테이블 선택 (전체 테이블)</Label>
|
|
<Popover open={rightTableOpen} onOpenChange={setRightTableOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={rightTableOpen}
|
|
className="w-full justify-between"
|
|
>
|
|
{config.rightPanel?.tableName || "테이블을 선택하세요"}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-full p-0">
|
|
<Command>
|
|
<CommandInput placeholder="테이블 검색..." />
|
|
<CommandEmpty>테이블을 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup className="max-h-[200px] overflow-auto">
|
|
{availableRightTables.map((table) => (
|
|
<CommandItem
|
|
key={table.tableName}
|
|
value={table.tableName}
|
|
onSelect={(value) => {
|
|
updateRightPanel({ tableName: value });
|
|
setRightTableOpen(false);
|
|
}}
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
config.rightPanel?.tableName === table.tableName ? "opacity-100" : "opacity-0",
|
|
)}
|
|
/>
|
|
{table.displayName || table.tableName}
|
|
{table.displayName && <span className="ml-2 text-xs text-gray-500">({table.tableName})</span>}
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
)}
|
|
|
|
{/* 컬럼 매핑 - 조인 모드에서만 표시 */}
|
|
{relationshipType !== "detail" && (
|
|
<div className="space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-3">
|
|
<Label className="text-sm font-semibold">컬럼 매핑 (외래키 관계)</Label>
|
|
<p className="text-xs text-gray-600">좌측 테이블의 컬럼을 우측 테이블의 컬럼과 연결합니다</p>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-xs">좌측 컬럼</Label>
|
|
<Popover open={leftColumnOpen} onOpenChange={setLeftColumnOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={leftColumnOpen}
|
|
className="w-full justify-between"
|
|
disabled={!config.leftPanel?.tableName}
|
|
>
|
|
{config.rightPanel?.relation?.leftColumn || "좌측 컬럼 선택"}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-full p-0">
|
|
<Command>
|
|
<CommandInput placeholder="컬럼 검색..." />
|
|
<CommandEmpty>컬럼을 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup className="max-h-[200px] overflow-auto">
|
|
{leftTableColumns.map((column) => (
|
|
<CommandItem
|
|
key={column.columnName}
|
|
value={column.columnName}
|
|
onSelect={(value) => {
|
|
updateRightPanel({
|
|
relation: { ...config.rightPanel?.relation, leftColumn: value },
|
|
});
|
|
setLeftColumnOpen(false);
|
|
}}
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
config.rightPanel?.relation?.leftColumn === column.columnName
|
|
? "opacity-100"
|
|
: "opacity-0",
|
|
)}
|
|
/>
|
|
{column.columnName}
|
|
<span className="ml-2 text-xs text-gray-500">({column.columnLabel || ""})</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-center">
|
|
<ArrowRight className="h-4 w-4 text-gray-400" />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-xs">우측 컬럼 (외래키)</Label>
|
|
<Popover open={rightColumnOpen} onOpenChange={setRightColumnOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={rightColumnOpen}
|
|
className="w-full justify-between"
|
|
disabled={!config.rightPanel?.tableName}
|
|
>
|
|
{config.rightPanel?.relation?.foreignKey || "우측 컬럼 선택"}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-full p-0">
|
|
<Command>
|
|
<CommandInput placeholder="컬럼 검색..." />
|
|
<CommandEmpty>컬럼을 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup className="max-h-[200px] overflow-auto">
|
|
{rightTableColumns.map((column) => (
|
|
<CommandItem
|
|
key={column.columnName}
|
|
value={column.columnName}
|
|
onSelect={(value) => {
|
|
updateRightPanel({
|
|
relation: { ...config.rightPanel?.relation, foreignKey: value },
|
|
});
|
|
setRightColumnOpen(false);
|
|
}}
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
config.rightPanel?.relation?.foreignKey === column.columnName
|
|
? "opacity-100"
|
|
: "opacity-0",
|
|
)}
|
|
/>
|
|
{column.columnName}
|
|
<span className="ml-2 text-xs text-gray-500">({column.columnLabel || ""})</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label>검색 기능</Label>
|
|
<Switch
|
|
checked={config.rightPanel?.showSearch ?? true}
|
|
onCheckedChange={(checked) => updateRightPanel({ showSearch: checked })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label>추가 버튼</Label>
|
|
<Switch
|
|
checked={config.rightPanel?.showAdd ?? false}
|
|
onCheckedChange={(checked) => updateRightPanel({ showAdd: checked })}
|
|
/>
|
|
</div>
|
|
|
|
{/* 우측 패널 표시 컬럼 설정 */}
|
|
<div className="space-y-3 rounded-lg border border-green-200 bg-green-50 p-3">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-sm font-semibold">표시할 컬럼 선택</Label>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
const currentColumns = config.rightPanel?.columns || [];
|
|
const newColumns = [
|
|
...currentColumns,
|
|
{ name: "", label: "", width: 100 },
|
|
];
|
|
updateRightPanel({ columns: newColumns });
|
|
}}
|
|
className="h-7 text-xs"
|
|
disabled={!config.rightPanel?.tableName}
|
|
>
|
|
<Plus className="mr-1 h-3 w-3" />
|
|
컬럼 추가
|
|
</Button>
|
|
</div>
|
|
<p className="text-xs text-gray-600">
|
|
우측 패널에 표시할 컬럼을 선택하세요. 선택하지 않으면 모든 컬럼이 표시됩니다.
|
|
</p>
|
|
|
|
{/* 선택된 컬럼 목록 */}
|
|
<div className="space-y-2">
|
|
{(config.rightPanel?.columns || []).length === 0 ? (
|
|
<div className="rounded-md border border-dashed border-gray-300 bg-white p-3 text-center">
|
|
<p className="text-xs text-gray-500">설정된 컬럼이 없습니다</p>
|
|
<p className="mt-1 text-[10px] text-gray-400">
|
|
컬럼을 추가하지 않으면 모든 컬럼이 표시됩니다
|
|
</p>
|
|
</div>
|
|
) : (
|
|
(config.rightPanel?.columns || []).map((col, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-center gap-2 rounded-md border bg-white p-2"
|
|
>
|
|
<div className="flex-1">
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
className="h-8 w-full justify-between text-xs"
|
|
>
|
|
{col.name || "컬럼 선택"}
|
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-full p-0">
|
|
<Command>
|
|
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
|
|
<CommandEmpty className="text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup className="max-h-[200px] overflow-auto">
|
|
{rightTableColumns.map((column) => (
|
|
<CommandItem
|
|
key={column.columnName}
|
|
value={column.columnName}
|
|
onSelect={(value) => {
|
|
const newColumns = [...(config.rightPanel?.columns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
name: value,
|
|
label: column.columnLabel || value,
|
|
};
|
|
updateRightPanel({ columns: newColumns });
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
col.name === column.columnName ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
{column.columnLabel || column.columnName}
|
|
<span className="ml-2 text-[10px] text-gray-500">
|
|
({column.columnName})
|
|
</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => {
|
|
const newColumns = (config.rightPanel?.columns || []).filter(
|
|
(_, i) => i !== index
|
|
);
|
|
updateRightPanel({ columns: newColumns });
|
|
}}
|
|
className="h-8 w-8 p-0"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 우측 패널 추가 모달 컬럼 설정 */}
|
|
{config.rightPanel?.showAdd && (
|
|
<div className="space-y-3 rounded-lg border border-purple-200 bg-purple-50 p-3">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-sm font-semibold">추가 모달 입력 컬럼</Label>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
const currentColumns = config.rightPanel?.addModalColumns || [];
|
|
const newColumns = [
|
|
...currentColumns,
|
|
{ name: "", label: "", required: false },
|
|
];
|
|
updateRightPanel({ addModalColumns: newColumns });
|
|
}}
|
|
className="h-7 text-xs"
|
|
disabled={!config.rightPanel?.tableName}
|
|
>
|
|
<Plus className="mr-1 h-3 w-3" />
|
|
컬럼 추가
|
|
</Button>
|
|
</div>
|
|
<p className="text-xs text-gray-600">
|
|
추가 버튼 클릭 시 모달에 표시될 입력 필드를 선택하세요
|
|
</p>
|
|
|
|
<div className="space-y-2">
|
|
{(config.rightPanel?.addModalColumns || []).length === 0 ? (
|
|
<div className="rounded-md border border-dashed border-gray-300 bg-white p-3 text-center">
|
|
<p className="text-xs text-gray-500">설정된 컬럼이 없습니다</p>
|
|
</div>
|
|
) : (
|
|
(config.rightPanel?.addModalColumns || []).map((col, index) => {
|
|
// 현재 컬럼이 PK인지 확인
|
|
const column = rightTableColumns.find(c => c.columnName === col.name);
|
|
const isPK = column?.isPrimaryKey || false;
|
|
|
|
return (
|
|
<div
|
|
key={index}
|
|
className={cn(
|
|
"flex items-center gap-2 rounded-md border p-2",
|
|
isPK ? "bg-yellow-50 border-yellow-300" : "bg-white"
|
|
)}
|
|
>
|
|
{isPK && (
|
|
<span className="text-[10px] font-semibold text-yellow-700 px-1.5 py-0.5 bg-yellow-200 rounded">
|
|
PK
|
|
</span>
|
|
)}
|
|
<div className="flex-1">
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
disabled={isPK}
|
|
className="h-8 w-full justify-between text-xs"
|
|
>
|
|
{col.name || "컬럼 선택"}
|
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-full p-0">
|
|
<Command>
|
|
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
|
|
<CommandEmpty className="text-xs">컬럼을 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup className="max-h-[200px] overflow-auto">
|
|
{rightTableColumns
|
|
.filter((column) => !['company_code', 'company_name'].includes(column.columnName))
|
|
.map((column) => (
|
|
<CommandItem
|
|
key={column.columnName}
|
|
value={column.columnName}
|
|
onSelect={(value) => {
|
|
const newColumns = [...(config.rightPanel?.addModalColumns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
name: value,
|
|
label: column.columnLabel || value,
|
|
};
|
|
updateRightPanel({ addModalColumns: newColumns });
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
col.name === column.columnName ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
{column.columnLabel || column.columnName}
|
|
<span className="ml-2 text-[10px] text-gray-500">
|
|
({column.columnName})
|
|
</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<label className="flex items-center gap-1 text-xs text-gray-600 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={col.required ?? false}
|
|
disabled={isPK}
|
|
onChange={(e) => {
|
|
const newColumns = [...(config.rightPanel?.addModalColumns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
required: e.target.checked,
|
|
};
|
|
updateRightPanel({ addModalColumns: newColumns });
|
|
}}
|
|
className="h-3 w-3"
|
|
/>
|
|
필수
|
|
</label>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
disabled={isPK}
|
|
onClick={() => {
|
|
const newColumns = (config.rightPanel?.addModalColumns || []).filter(
|
|
(_, i) => i !== index
|
|
);
|
|
updateRightPanel({ addModalColumns: newColumns });
|
|
}}
|
|
className="h-8 w-8 p-0"
|
|
title={isPK ? "PK 컬럼은 삭제할 수 없습니다" : ""}
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
|
|
{/* 중계 테이블 설정 */}
|
|
<div className="space-y-3 rounded-lg border border-orange-200 bg-orange-50 p-3 mt-3">
|
|
<Label className="text-sm font-semibold">중계 테이블 설정 (N:M 관계)</Label>
|
|
<p className="text-xs text-gray-600">
|
|
중계 테이블을 사용하여 다대다 관계를 구현합니다
|
|
</p>
|
|
|
|
<div>
|
|
<Label className="text-xs text-gray-700">실제 저장할 테이블</Label>
|
|
<Input
|
|
value={config.rightPanel?.addConfig?.targetTable || ""}
|
|
onChange={(e) => {
|
|
const addConfig = config.rightPanel?.addConfig || {};
|
|
updateRightPanel({
|
|
addConfig: {
|
|
...addConfig,
|
|
targetTable: e.target.value,
|
|
},
|
|
});
|
|
}}
|
|
placeholder="예: user_dept"
|
|
className="mt-1 h-8 text-xs"
|
|
/>
|
|
<p className="mt-1 text-[10px] text-gray-500">
|
|
데이터가 실제로 저장될 중계 테이블명
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label className="text-xs text-gray-700">좌측 패널 컬럼</Label>
|
|
<Input
|
|
value={config.rightPanel?.addConfig?.leftPanelColumn || ""}
|
|
onChange={(e) => {
|
|
const addConfig = config.rightPanel?.addConfig || {};
|
|
updateRightPanel({
|
|
addConfig: {
|
|
...addConfig,
|
|
leftPanelColumn: e.target.value,
|
|
},
|
|
});
|
|
}}
|
|
placeholder="예: dept_code"
|
|
className="mt-1 h-8 text-xs"
|
|
/>
|
|
<p className="mt-1 text-[10px] text-gray-500">
|
|
좌측 패널에서 선택한 항목의 어떤 컬럼값을 가져올지
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label className="text-xs text-gray-700">중계 테이블 대상 컬럼</Label>
|
|
<Input
|
|
value={config.rightPanel?.addConfig?.targetColumn || ""}
|
|
onChange={(e) => {
|
|
const addConfig = config.rightPanel?.addConfig || {};
|
|
updateRightPanel({
|
|
addConfig: {
|
|
...addConfig,
|
|
targetColumn: e.target.value,
|
|
},
|
|
});
|
|
}}
|
|
placeholder="예: dept_code"
|
|
className="mt-1 h-8 text-xs"
|
|
/>
|
|
<p className="mt-1 text-[10px] text-gray-500">
|
|
중계 테이블의 어떤 컬럼에 좌측값을 저장할지
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label className="text-xs text-gray-700">자동 채움 컬럼 (JSON)</Label>
|
|
<textarea
|
|
value={JSON.stringify(config.rightPanel?.addConfig?.autoFillColumns || {}, null, 2)}
|
|
onChange={(e) => {
|
|
try {
|
|
const parsed = JSON.parse(e.target.value);
|
|
const addConfig = config.rightPanel?.addConfig || {};
|
|
updateRightPanel({
|
|
addConfig: {
|
|
...addConfig,
|
|
autoFillColumns: parsed,
|
|
},
|
|
});
|
|
} catch (err) {
|
|
// JSON 파싱 오류는 무시 (입력 중)
|
|
}
|
|
}}
|
|
placeholder='{ "is_primary": false }'
|
|
className="mt-1 h-20 w-full rounded-md border border-input bg-white px-3 py-2 text-xs font-mono"
|
|
/>
|
|
<p className="mt-1 text-[10px] text-gray-500">
|
|
자동으로 채워질 컬럼과 기본값 (예: is_primary: false)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 레이아웃 설정 */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-sm font-semibold">레이아웃 설정</h3>
|
|
|
|
<div className="space-y-2">
|
|
<Label>좌측 패널 너비: {config.splitRatio || 30}%</Label>
|
|
<Slider
|
|
value={[config.splitRatio || 30]}
|
|
onValueChange={(value) => updateConfig({ splitRatio: value[0] })}
|
|
min={20}
|
|
max={80}
|
|
step={5}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label>크기 조절 가능</Label>
|
|
<Switch
|
|
checked={config.resizable ?? true}
|
|
onCheckedChange={(checked) => updateConfig({ resizable: checked })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label>자동 데이터 로드</Label>
|
|
<Switch
|
|
checked={config.autoLoad ?? true}
|
|
onCheckedChange={(checked) => updateConfig({ autoLoad: checked })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|