3799 lines
175 KiB
TypeScript
3799 lines
175 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 { Checkbox } from "@/components/ui/checkbox";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Slider } from "@/components/ui/slider";
|
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
import { Button } from "@/components/ui/button";
|
|
// Accordion 제거 - 단순 섹션으로 변경
|
|
import { Check, ChevronsUpDown, ArrowRight, Plus, X, ArrowUp, ArrowDown, Trash2, GripVertical } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { SplitPanelLayoutConfig, AdditionalTabConfig } from "./types";
|
|
import { TableInfo, ColumnInfo } from "@/types/screen";
|
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
|
import { tableTypeApi } from "@/lib/api/screen";
|
|
import { DataFilterConfigPanel } from "@/components/screen/config-panels/DataFilterConfigPanel";
|
|
|
|
interface SplitPanelLayoutConfigPanelProps {
|
|
config: SplitPanelLayoutConfig;
|
|
onChange: (config: SplitPanelLayoutConfig) => void;
|
|
tables?: TableInfo[]; // 전체 테이블 목록 (선택적)
|
|
screenTableName?: string; // 현재 화면의 테이블명 (좌측 패널에서 사용)
|
|
menuObjid?: number; // 🆕 메뉴 OBJID (카테고리 값 조회 시 필요)
|
|
}
|
|
|
|
/**
|
|
* 그룹핑 기준 컬럼 선택 컴포넌트
|
|
*/
|
|
const GroupByColumnsSelector: React.FC<{
|
|
tableName?: string;
|
|
selectedColumns: string[];
|
|
onChange: (columns: string[]) => void;
|
|
}> = ({ tableName, selectedColumns, onChange }) => {
|
|
const [columns, setColumns] = useState<any[]>([]); // ColumnTypeInfo 타입
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!tableName) {
|
|
setColumns([]);
|
|
return;
|
|
}
|
|
|
|
const loadColumns = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const { tableManagementApi } = await import("@/lib/api/tableManagement");
|
|
const response = await tableManagementApi.getColumnList(tableName);
|
|
if (response.success && response.data && response.data.columns) {
|
|
setColumns(response.data.columns);
|
|
}
|
|
} catch (error) {
|
|
console.error("컬럼 정보 로드 실패:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
loadColumns();
|
|
}, [tableName]);
|
|
|
|
const toggleColumn = (columnName: string) => {
|
|
const newSelection = selectedColumns.includes(columnName)
|
|
? selectedColumns.filter((c) => c !== columnName)
|
|
: [...selectedColumns, columnName];
|
|
onChange(newSelection);
|
|
};
|
|
|
|
if (!tableName) {
|
|
return (
|
|
<div className="rounded-md border border-dashed p-3">
|
|
<p className="text-muted-foreground text-center text-xs">먼저 우측 패널의 테이블을 선택하세요</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<Label className="text-xs">그룹핑 기준 컬럼</Label>
|
|
{loading ? (
|
|
<div className="rounded-md border p-3">
|
|
<p className="text-muted-foreground text-center text-xs">로딩 중...</p>
|
|
</div>
|
|
) : columns.length === 0 ? (
|
|
<div className="rounded-md border border-dashed p-3">
|
|
<p className="text-muted-foreground text-center text-xs">컬럼을 찾을 수 없습니다</p>
|
|
</div>
|
|
) : (
|
|
<div className="max-h-[200px] space-y-1 overflow-y-auto rounded-md border p-3">
|
|
{columns.map((col) => (
|
|
<div key={col.columnName} className="flex items-center gap-2">
|
|
<Checkbox
|
|
id={`groupby-${col.columnName}`}
|
|
checked={selectedColumns.includes(col.columnName)}
|
|
onCheckedChange={() => toggleColumn(col.columnName)}
|
|
/>
|
|
<label htmlFor={`groupby-${col.columnName}`} className="flex-1 cursor-pointer text-xs">
|
|
{col.columnLabel || col.columnName}
|
|
<span className="text-muted-foreground ml-1">({col.columnName})</span>
|
|
</label>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
<p className="text-muted-foreground mt-1 text-[10px]">
|
|
선택된 컬럼: {selectedColumns.length > 0 ? selectedColumns.join(", ") : "없음"}
|
|
<br />
|
|
같은 값을 가진 모든 레코드를 함께 불러옵니다
|
|
</p>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 화면 선택 Combobox 컴포넌트
|
|
*/
|
|
const ScreenSelector: React.FC<{
|
|
value?: number;
|
|
onChange: (screenId?: number) => void;
|
|
}> = ({ value, onChange }) => {
|
|
const [open, setOpen] = useState(false);
|
|
const [screens, setScreens] = useState<Array<{ screenId: number; screenName: string; screenCode: string }>>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const loadScreens = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const { screenApi } = await import("@/lib/api/screen");
|
|
const response = await screenApi.getScreens({ page: 1, size: 1000 });
|
|
setScreens(
|
|
response.data.map((s) => ({ screenId: s.screenId, screenName: s.screenName, screenCode: s.screenCode })),
|
|
);
|
|
} catch (error) {
|
|
console.error("화면 목록 로드 실패:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
loadScreens();
|
|
}, []);
|
|
|
|
const selectedScreen = screens.find((s) => s.screenId === value);
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={open}
|
|
className="h-8 w-full justify-between text-xs"
|
|
disabled={loading}
|
|
>
|
|
{loading ? "로딩 중..." : selectedScreen ? selectedScreen.screenName : "화면 선택"}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-[400px] p-0" align="start">
|
|
<Command>
|
|
<CommandInput placeholder="화면 검색..." className="text-xs" />
|
|
<CommandList>
|
|
<CommandEmpty className="py-6 text-center text-xs">화면을 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup className="max-h-[300px] overflow-auto">
|
|
{screens.map((screen) => (
|
|
<CommandItem
|
|
key={screen.screenId}
|
|
value={`${screen.screenName.toLowerCase()} ${screen.screenCode.toLowerCase()} ${screen.screenId}`}
|
|
onSelect={() => {
|
|
onChange(screen.screenId === value ? undefined : screen.screenId);
|
|
setOpen(false);
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check className={cn("mr-2 h-4 w-4", value === screen.screenId ? "opacity-100" : "opacity-0")} />
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{screen.screenName}</span>
|
|
<span className="text-muted-foreground text-[10px]">{screen.screenCode}</span>
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 추가 탭 설정 패널 (우측 패널과 동일한 구조)
|
|
*/
|
|
interface AdditionalTabConfigPanelProps {
|
|
tab: AdditionalTabConfig;
|
|
tabIndex: number;
|
|
config: SplitPanelLayoutConfig;
|
|
updateRightPanel: (updates: Partial<SplitPanelLayoutConfig["rightPanel"]>) => void;
|
|
availableRightTables: TableInfo[];
|
|
leftTableColumns: ColumnInfo[];
|
|
menuObjid?: number;
|
|
// 공유 컬럼 로드 상태
|
|
loadedTableColumns: Record<string, ColumnInfo[]>;
|
|
loadTableColumns: (tableName: string) => Promise<void>;
|
|
loadingColumns: Record<string, boolean>;
|
|
}
|
|
|
|
const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
|
tab,
|
|
tabIndex,
|
|
config,
|
|
updateRightPanel,
|
|
availableRightTables,
|
|
leftTableColumns,
|
|
menuObjid,
|
|
loadedTableColumns,
|
|
loadTableColumns,
|
|
loadingColumns,
|
|
}) => {
|
|
// 탭 테이블 변경 시 컬럼 로드
|
|
useEffect(() => {
|
|
if (tab.tableName && !loadedTableColumns[tab.tableName] && !loadingColumns[tab.tableName]) {
|
|
loadTableColumns(tab.tableName);
|
|
}
|
|
}, [tab.tableName, loadedTableColumns, loadingColumns, loadTableColumns]);
|
|
|
|
// 현재 탭의 컬럼 목록
|
|
const tabColumns = useMemo(() => {
|
|
return tab.tableName ? loadedTableColumns[tab.tableName] || [] : [];
|
|
}, [tab.tableName, loadedTableColumns]);
|
|
|
|
// 로딩 상태
|
|
const loadingTabColumns = tab.tableName ? loadingColumns[tab.tableName] || false : false;
|
|
|
|
// 탭 업데이트 헬퍼
|
|
const updateTab = (updates: Partial<AdditionalTabConfig>) => {
|
|
const newTabs = [...(config.rightPanel?.additionalTabs || [])];
|
|
newTabs[tabIndex] = { ...tab, ...updates };
|
|
updateRightPanel({ additionalTabs: newTabs });
|
|
};
|
|
|
|
return (
|
|
<AccordionItem
|
|
key={tab.tabId}
|
|
value={tab.tabId}
|
|
className="rounded-lg border bg-gray-50"
|
|
>
|
|
<AccordionTrigger className="px-3 py-2 hover:no-underline">
|
|
<div className="flex flex-1 items-center gap-2">
|
|
<GripVertical className="h-4 w-4 text-gray-400" />
|
|
<span className="text-sm font-medium">
|
|
{tab.label || `탭 ${tabIndex + 1}`}
|
|
</span>
|
|
{tab.tableName && (
|
|
<span className="text-xs text-gray-500">({tab.tableName})</span>
|
|
)}
|
|
</div>
|
|
</AccordionTrigger>
|
|
<AccordionContent className="px-3 pb-3">
|
|
<div className="space-y-4">
|
|
{/* ===== 1. 기본 정보 ===== */}
|
|
<div className="space-y-3 rounded-lg border bg-white p-3">
|
|
<Label className="text-xs font-semibold text-blue-600">기본 정보</Label>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">탭 라벨</Label>
|
|
<Input
|
|
value={tab.label}
|
|
onChange={(e) => updateTab({ label: e.target.value })}
|
|
placeholder="탭 이름"
|
|
className="h-8 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">패널 제목</Label>
|
|
<Input
|
|
value={tab.title}
|
|
onChange={(e) => updateTab({ title: e.target.value })}
|
|
placeholder="패널 제목"
|
|
className="h-8 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">패널 헤더 높이</Label>
|
|
<Input
|
|
type="number"
|
|
value={tab.panelHeaderHeight ?? 48}
|
|
onChange={(e) => updateTab({ panelHeaderHeight: parseInt(e.target.value) || 48 })}
|
|
placeholder="48"
|
|
className="h-8 w-24 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ===== 2. 테이블 선택 ===== */}
|
|
<div className="space-y-3 rounded-lg border bg-white p-3">
|
|
<Label className="text-xs font-semibold text-blue-600">테이블 설정</Label>
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">테이블 선택</Label>
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
className="h-8 w-full justify-between text-xs"
|
|
>
|
|
{tab.tableName || "테이블을 선택하세요"}
|
|
<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>테이블을 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup className="max-h-[200px] overflow-auto">
|
|
{availableRightTables.map((table) => (
|
|
<CommandItem
|
|
key={table.tableName}
|
|
value={`${table.displayName || ""} ${table.tableName}`}
|
|
onSelect={() => updateTab({ tableName: table.tableName, columns: [] })}
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
tab.tableName === table.tableName ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
{table.displayName || table.tableName}
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ===== 3. 표시 모드 ===== */}
|
|
<div className="space-y-3 rounded-lg border bg-white p-3">
|
|
<Label className="text-xs font-semibold text-blue-600">표시 설정</Label>
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">표시 모드</Label>
|
|
<Select
|
|
value={tab.displayMode || "list"}
|
|
onValueChange={(value: "list" | "table") => updateTab({ displayMode: value })}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="list">목록 (카드)</SelectItem>
|
|
<SelectItem value="table">테이블</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 요약 설정 (목록 모드) */}
|
|
{tab.displayMode === "list" && (
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">요약 컬럼 수</Label>
|
|
<Input
|
|
type="number"
|
|
value={tab.summaryColumnCount ?? 3}
|
|
onChange={(e) => updateTab({ summaryColumnCount: parseInt(e.target.value) || 3 })}
|
|
min={1}
|
|
max={10}
|
|
className="h-8 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2 pt-5">
|
|
<Checkbox
|
|
id={`tab-${tabIndex}-summary-label`}
|
|
checked={tab.summaryShowLabel ?? true}
|
|
onCheckedChange={(checked) => updateTab({ summaryShowLabel: !!checked })}
|
|
/>
|
|
<label htmlFor={`tab-${tabIndex}-summary-label`} className="text-xs">라벨 표시</label>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* ===== 4. 컬럼 매핑 (조인 키) ===== */}
|
|
<div className="space-y-3 rounded-lg border bg-white p-3">
|
|
<Label className="text-xs font-semibold text-blue-600">컬럼 매핑 (조인 키)</Label>
|
|
<p className="text-[10px] text-gray-500">
|
|
좌측 패널 선택 시 관련 데이터만 표시합니다
|
|
</p>
|
|
<div className="mt-2 grid grid-cols-2 gap-2">
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px]">좌측 컬럼</Label>
|
|
<Select
|
|
value={tab.relation?.keys?.[0]?.leftColumn || tab.relation?.leftColumn || ""}
|
|
onValueChange={(value) => {
|
|
updateTab({
|
|
relation: {
|
|
...tab.relation,
|
|
type: "join",
|
|
keys: [{ leftColumn: value, rightColumn: tab.relation?.keys?.[0]?.rightColumn || "" }],
|
|
},
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{leftTableColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.columnLabel || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px]">우측 컬럼</Label>
|
|
<Select
|
|
value={tab.relation?.keys?.[0]?.rightColumn || tab.relation?.foreignKey || ""}
|
|
onValueChange={(value) => {
|
|
updateTab({
|
|
relation: {
|
|
...tab.relation,
|
|
type: "join",
|
|
keys: [{ leftColumn: tab.relation?.keys?.[0]?.leftColumn || "", rightColumn: value }],
|
|
},
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{tabColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.columnLabel || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ===== 5. 기능 버튼 ===== */}
|
|
<div className="space-y-3 rounded-lg border bg-white p-3">
|
|
<Label className="text-xs font-semibold text-blue-600">기능 버튼</Label>
|
|
<div className="grid grid-cols-4 gap-2">
|
|
<div className="flex items-center gap-1">
|
|
<Checkbox
|
|
id={`tab-${tabIndex}-search`}
|
|
checked={tab.showSearch}
|
|
onCheckedChange={(checked) => updateTab({ showSearch: !!checked })}
|
|
/>
|
|
<label htmlFor={`tab-${tabIndex}-search`} className="text-xs">검색</label>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Checkbox
|
|
id={`tab-${tabIndex}-add`}
|
|
checked={tab.showAdd}
|
|
onCheckedChange={(checked) => updateTab({ showAdd: !!checked })}
|
|
/>
|
|
<label htmlFor={`tab-${tabIndex}-add`} className="text-xs">추가</label>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Checkbox
|
|
id={`tab-${tabIndex}-edit`}
|
|
checked={tab.showEdit}
|
|
onCheckedChange={(checked) => updateTab({ showEdit: !!checked })}
|
|
/>
|
|
<label htmlFor={`tab-${tabIndex}-edit`} className="text-xs">수정</label>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Checkbox
|
|
id={`tab-${tabIndex}-delete`}
|
|
checked={tab.showDelete}
|
|
onCheckedChange={(checked) => updateTab({ showDelete: !!checked })}
|
|
/>
|
|
<label htmlFor={`tab-${tabIndex}-delete`} className="text-xs">삭제</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ===== 6. 표시 컬럼 설정 ===== */}
|
|
<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-xs font-semibold text-green-700">표시할 컬럼 선택</Label>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
const currentColumns = tab.columns || [];
|
|
const newColumns = [...currentColumns, { name: "", label: "", width: 100 }];
|
|
updateTab({ columns: newColumns });
|
|
}}
|
|
className="h-7 text-xs"
|
|
disabled={!tab.tableName || loadingTabColumns}
|
|
>
|
|
<Plus className="mr-1 h-3 w-3" />
|
|
컬럼 추가
|
|
</Button>
|
|
</div>
|
|
<p className="text-[10px] text-gray-600">
|
|
표시할 컬럼을 선택하세요. 선택하지 않으면 모든 컬럼이 표시됩니다.
|
|
</p>
|
|
|
|
{/* 테이블 미선택 상태 */}
|
|
{!tab.tableName && (
|
|
<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>
|
|
)}
|
|
|
|
{/* 테이블 선택됨 - 컬럼 목록 */}
|
|
{tab.tableName && (
|
|
<div className="space-y-2">
|
|
{/* 로딩 상태 */}
|
|
{loadingTabColumns && (
|
|
<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>
|
|
)}
|
|
|
|
{/* 설정된 컬럼이 없을 때 */}
|
|
{!loadingTabColumns && (tab.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>
|
|
)}
|
|
|
|
{/* 설정된 컬럼 목록 */}
|
|
{!loadingTabColumns && (tab.columns || []).length > 0 && (
|
|
(tab.columns || []).map((col, colIndex) => (
|
|
<div key={colIndex} className="space-y-2 rounded-md border bg-white p-3">
|
|
{/* 상단: 순서 변경 + 삭제 버튼 */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-1">
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => {
|
|
if (colIndex === 0) return;
|
|
const newColumns = [...(tab.columns || [])];
|
|
[newColumns[colIndex - 1], newColumns[colIndex]] = [newColumns[colIndex], newColumns[colIndex - 1]];
|
|
updateTab({ columns: newColumns });
|
|
}}
|
|
disabled={colIndex === 0}
|
|
className="h-6 w-6 p-0"
|
|
>
|
|
<ArrowUp className="h-3 w-3" />
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => {
|
|
const columns = tab.columns || [];
|
|
if (colIndex === columns.length - 1) return;
|
|
const newColumns = [...columns];
|
|
[newColumns[colIndex], newColumns[colIndex + 1]] = [newColumns[colIndex + 1], newColumns[colIndex]];
|
|
updateTab({ columns: newColumns });
|
|
}}
|
|
disabled={colIndex === (tab.columns || []).length - 1}
|
|
className="h-6 w-6 p-0"
|
|
>
|
|
<ArrowDown className="h-3 w-3" />
|
|
</Button>
|
|
<span className="text-[10px] text-gray-400">#{colIndex + 1}</span>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => {
|
|
const newColumns = (tab.columns || []).filter((_, i) => i !== colIndex);
|
|
updateTab({ columns: newColumns });
|
|
}}
|
|
className="h-6 w-6 p-0 text-red-500 hover:bg-red-50 hover:text-red-600"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 컬럼 선택 */}
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px] text-gray-500">컬럼</Label>
|
|
<Select
|
|
value={col.name}
|
|
onValueChange={(value) => {
|
|
const selectedCol = tabColumns.find((c) => c.columnName === value);
|
|
const newColumns = [...(tab.columns || [])];
|
|
newColumns[colIndex] = {
|
|
...col,
|
|
name: value,
|
|
label: selectedCol?.columnLabel || value,
|
|
};
|
|
updateTab({ columns: newColumns });
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue placeholder="컬럼을 선택하세요" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{tabColumns.map((column) => (
|
|
<SelectItem key={column.columnName} value={column.columnName}>
|
|
{column.columnLabel || column.columnName}
|
|
<span className="ml-1 text-[10px] text-gray-400">({column.columnName})</span>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 라벨 + 너비 */}
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px] text-gray-500">라벨</Label>
|
|
<Input
|
|
value={col.label}
|
|
onChange={(e) => {
|
|
const newColumns = [...(tab.columns || [])];
|
|
newColumns[colIndex] = { ...col, label: e.target.value };
|
|
updateTab({ columns: newColumns });
|
|
}}
|
|
placeholder="표시 라벨"
|
|
className="h-8 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px] text-gray-500">너비 (px)</Label>
|
|
<Input
|
|
type="number"
|
|
value={col.width || 100}
|
|
onChange={(e) => {
|
|
const newColumns = [...(tab.columns || [])];
|
|
newColumns[colIndex] = { ...col, width: parseInt(e.target.value) || 100 };
|
|
updateTab({ columns: newColumns });
|
|
}}
|
|
placeholder="100"
|
|
className="h-8 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* ===== 7. 추가 모달 컬럼 설정 (showAdd일 때) ===== */}
|
|
{tab.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-xs font-semibold text-purple-700">추가 모달 컬럼 설정</Label>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
const currentColumns = tab.addModalColumns || [];
|
|
const newColumns = [...currentColumns, { name: "", label: "", required: false }];
|
|
updateTab({ addModalColumns: newColumns });
|
|
}}
|
|
className="h-7 text-xs"
|
|
disabled={!tab.tableName}
|
|
>
|
|
<Plus className="mr-1 h-3 w-3" />
|
|
컬럼 추가
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
{(tab.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>
|
|
) : (
|
|
(tab.addModalColumns || []).map((col, colIndex) => (
|
|
<div key={colIndex} className="flex items-center gap-2 rounded-md border bg-white p-2">
|
|
<Select
|
|
value={col.name}
|
|
onValueChange={(value) => {
|
|
const selectedCol = tabColumns.find((c) => c.columnName === value);
|
|
const newColumns = [...(tab.addModalColumns || [])];
|
|
newColumns[colIndex] = {
|
|
...col,
|
|
name: value,
|
|
label: selectedCol?.columnLabel || value,
|
|
};
|
|
updateTab({ addModalColumns: newColumns });
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-8 flex-1 text-xs">
|
|
<SelectValue placeholder="컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{tabColumns.map((column) => (
|
|
<SelectItem key={column.columnName} value={column.columnName}>
|
|
{column.columnLabel || column.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Input
|
|
value={col.label}
|
|
onChange={(e) => {
|
|
const newColumns = [...(tab.addModalColumns || [])];
|
|
newColumns[colIndex] = { ...col, label: e.target.value };
|
|
updateTab({ addModalColumns: newColumns });
|
|
}}
|
|
placeholder="라벨"
|
|
className="h-8 w-24 text-xs"
|
|
/>
|
|
<div className="flex items-center gap-1">
|
|
<Checkbox
|
|
checked={col.required}
|
|
onCheckedChange={(checked) => {
|
|
const newColumns = [...(tab.addModalColumns || [])];
|
|
newColumns[colIndex] = { ...col, required: !!checked };
|
|
updateTab({ addModalColumns: newColumns });
|
|
}}
|
|
/>
|
|
<span className="text-[10px]">필수</span>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => {
|
|
const newColumns = (tab.addModalColumns || []).filter((_, i) => i !== colIndex);
|
|
updateTab({ addModalColumns: newColumns });
|
|
}}
|
|
className="h-8 w-8 p-0 text-red-500"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ===== 8. 데이터 필터링 ===== */}
|
|
<div className="space-y-3 rounded-lg border bg-white p-3">
|
|
<Label className="text-xs font-semibold text-blue-600">데이터 필터링</Label>
|
|
<DataFilterConfigPanel
|
|
tableName={tab.tableName}
|
|
columns={tabColumns}
|
|
config={tab.dataFilter}
|
|
onConfigChange={(dataFilter) => updateTab({ dataFilter })}
|
|
menuObjid={menuObjid}
|
|
/>
|
|
</div>
|
|
|
|
{/* ===== 9. 중복 데이터 제거 ===== */}
|
|
<div className="space-y-3 rounded-lg border bg-white p-3">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs font-semibold text-blue-600">중복 데이터 제거</Label>
|
|
<Switch
|
|
checked={tab.deduplication?.enabled ?? false}
|
|
onCheckedChange={(checked) => {
|
|
if (checked) {
|
|
updateTab({
|
|
deduplication: {
|
|
enabled: true,
|
|
groupByColumn: "",
|
|
keepStrategy: "latest",
|
|
sortColumn: "start_date",
|
|
},
|
|
});
|
|
} else {
|
|
updateTab({ deduplication: undefined });
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
{tab.deduplication?.enabled && (
|
|
<div className="mt-2 space-y-2">
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px]">기준 컬럼</Label>
|
|
<Select
|
|
value={tab.deduplication?.groupByColumn || ""}
|
|
onValueChange={(value) => {
|
|
updateTab({
|
|
deduplication: { ...tab.deduplication!, groupByColumn: value },
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs">
|
|
<SelectValue placeholder="컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{tabColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.columnLabel || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px]">정렬 컬럼</Label>
|
|
<Select
|
|
value={tab.deduplication?.sortColumn || ""}
|
|
onValueChange={(value) => {
|
|
updateTab({
|
|
deduplication: { ...tab.deduplication!, sortColumn: value },
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs">
|
|
<SelectValue placeholder="컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{tabColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.columnLabel || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px]">유지 전략</Label>
|
|
<Select
|
|
value={tab.deduplication?.keepStrategy || "latest"}
|
|
onValueChange={(value: "latest" | "earliest" | "base_price" | "current_date") => {
|
|
updateTab({
|
|
deduplication: { ...tab.deduplication!, keepStrategy: value },
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="latest">최신</SelectItem>
|
|
<SelectItem value="earliest">가장 오래된</SelectItem>
|
|
<SelectItem value="current_date">현재 날짜 기준</SelectItem>
|
|
<SelectItem value="base_price">기준가</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* ===== 10. 수정 버튼 설정 ===== */}
|
|
{tab.showEdit && (
|
|
<div className="space-y-3 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
|
<Label className="text-xs font-semibold text-blue-700">수정 버튼 설정</Label>
|
|
<div className="space-y-2">
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px]">수정 모드</Label>
|
|
<Select
|
|
value={tab.editButton?.mode || "auto"}
|
|
onValueChange={(value: "auto" | "modal") => {
|
|
updateTab({
|
|
editButton: { ...tab.editButton, enabled: true, mode: value },
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="auto">자동 (인라인)</SelectItem>
|
|
<SelectItem value="modal">모달 화면</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{tab.editButton?.mode === "modal" && (
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px]">수정 모달 화면</Label>
|
|
<ScreenSelector
|
|
value={tab.editButton?.modalScreenId}
|
|
onChange={(screenId) => {
|
|
updateTab({
|
|
editButton: { ...tab.editButton, enabled: true, mode: "modal", modalScreenId: screenId },
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px]">버튼 라벨</Label>
|
|
<Input
|
|
value={tab.editButton?.buttonLabel || ""}
|
|
onChange={(e) => {
|
|
updateTab({
|
|
editButton: { ...tab.editButton, enabled: true, buttonLabel: e.target.value || undefined },
|
|
});
|
|
}}
|
|
placeholder="수정"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px]">버튼 스타일</Label>
|
|
<Select
|
|
value={tab.editButton?.buttonVariant || "ghost"}
|
|
onValueChange={(value: "default" | "outline" | "ghost") => {
|
|
updateTab({
|
|
editButton: { ...tab.editButton, enabled: true, buttonVariant: value },
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="ghost">Ghost (기본)</SelectItem>
|
|
<SelectItem value="outline">Outline</SelectItem>
|
|
<SelectItem value="default">Default</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 그룹핑 기준 컬럼 */}
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px]">그룹핑 기준 컬럼</Label>
|
|
<p className="text-[9px] text-gray-500">수정 시 같은 값을 가진 레코드를 함께 불러옵니다</p>
|
|
<div className="max-h-[120px] space-y-1 overflow-y-auto rounded-md border bg-white p-2">
|
|
{tabColumns.map((col) => (
|
|
<div key={col.columnName} className="flex items-center gap-2">
|
|
<Checkbox
|
|
id={`tab-${tabIndex}-groupby-${col.columnName}`}
|
|
checked={(tab.editButton?.groupByColumns || []).includes(col.columnName)}
|
|
onCheckedChange={(checked) => {
|
|
const current = tab.editButton?.groupByColumns || [];
|
|
const newColumns = checked
|
|
? [...current, col.columnName]
|
|
: current.filter((c) => c !== col.columnName);
|
|
updateTab({
|
|
editButton: { ...tab.editButton, enabled: true, groupByColumns: newColumns },
|
|
});
|
|
}}
|
|
/>
|
|
<label htmlFor={`tab-${tabIndex}-groupby-${col.columnName}`} className="text-[10px]">
|
|
{col.columnLabel || col.columnName}
|
|
</label>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ===== 11. 삭제 버튼 설정 ===== */}
|
|
{tab.showDelete && (
|
|
<div className="space-y-3 rounded-lg border border-red-200 bg-red-50 p-3">
|
|
<Label className="text-xs font-semibold text-red-700">삭제 버튼 설정</Label>
|
|
<div className="space-y-2">
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px]">버튼 라벨</Label>
|
|
<Input
|
|
value={tab.deleteButton?.buttonLabel || ""}
|
|
onChange={(e) => {
|
|
updateTab({
|
|
deleteButton: { ...tab.deleteButton, enabled: true, buttonLabel: e.target.value || undefined },
|
|
});
|
|
}}
|
|
placeholder="삭제"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px]">버튼 스타일</Label>
|
|
<Select
|
|
value={tab.deleteButton?.buttonVariant || "ghost"}
|
|
onValueChange={(value: "default" | "outline" | "ghost" | "destructive") => {
|
|
updateTab({
|
|
deleteButton: { ...tab.deleteButton, enabled: true, buttonVariant: value },
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="ghost">Ghost (기본)</SelectItem>
|
|
<SelectItem value="outline">Outline</SelectItem>
|
|
<SelectItem value="default">Default</SelectItem>
|
|
<SelectItem value="destructive">Destructive (빨강)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px]">삭제 확인 메시지</Label>
|
|
<Input
|
|
value={tab.deleteButton?.confirmMessage || ""}
|
|
onChange={(e) => {
|
|
updateTab({
|
|
deleteButton: { ...tab.deleteButton, enabled: true, confirmMessage: e.target.value || undefined },
|
|
});
|
|
}}
|
|
placeholder="정말 삭제하시겠습니까?"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ===== 탭 삭제 버튼 ===== */}
|
|
<div className="flex justify-end border-t pt-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="text-red-500 hover:bg-red-50 hover:text-red-600"
|
|
onClick={() => {
|
|
const newTabs = config.rightPanel?.additionalTabs?.filter((_, i) => i !== tabIndex) || [];
|
|
updateRightPanel({ additionalTabs: newTabs });
|
|
}}
|
|
>
|
|
<Trash2 className="mr-1 h-3 w-3" />
|
|
탭 삭제
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* SplitPanelLayout 설정 패널
|
|
*/
|
|
export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelProps> = ({
|
|
config,
|
|
onChange,
|
|
tables = [], // 기본값 빈 배열 (현재 화면 테이블만)
|
|
screenTableName, // 현재 화면의 테이블명
|
|
menuObjid, // 🆕 메뉴 OBJID
|
|
}) => {
|
|
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[]>([]); // 조인 모드용 전체 테이블 목록
|
|
// 엔티티 참조 테이블 컬럼
|
|
type EntityRefTable = { tableName: string; columns: ColumnInfo[] };
|
|
const [entityReferenceTables, setEntityReferenceTables] = useState<Record<string, EntityRefTable[]>>({});
|
|
|
|
// 🆕 입력 필드용 로컬 상태
|
|
const [isUserEditing, setIsUserEditing] = useState(false);
|
|
const [localTitles, setLocalTitles] = useState({
|
|
left: config.leftPanel?.title || "",
|
|
right: config.rightPanel?.title || "",
|
|
});
|
|
|
|
// 관계 타입
|
|
const relationshipType = config.rightPanel?.relation?.type || "detail";
|
|
|
|
// config 변경 시 로컬 타이틀 동기화 (사용자가 입력 중이 아닐 때만)
|
|
useEffect(() => {
|
|
if (!isUserEditing) {
|
|
setLocalTitles({
|
|
left: config.leftPanel?.title || "",
|
|
right: config.rightPanel?.title || "",
|
|
});
|
|
}
|
|
}, [config.leftPanel?.title, config.rightPanel?.title, isUserEditing]);
|
|
|
|
// 조인 모드일 때만 전체 테이블 목록 로드
|
|
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,
|
|
referenceTable: col.referenceTable || col.reference_table, // 🆕 참조 테이블
|
|
referenceColumn: col.referenceColumn || col.reference_column, // 🆕 참조 컬럼
|
|
displayColumn: col.displayColumn || col.display_column, // 🆕 표시 컬럼
|
|
}));
|
|
|
|
console.log(`✅ 테이블 ${tableName} 컬럼 ${columns.length}개 로드됨:`, columns);
|
|
setLoadedTableColumns((prev) => ({ ...prev, [tableName]: columns }));
|
|
|
|
// 🆕 엔티티 타입 컬럼의 참조 테이블 컬럼도 로드
|
|
await loadEntityReferenceColumns(tableName, columns);
|
|
} catch (error) {
|
|
console.error(`테이블 ${tableName} 컬럼 로드 실패:`, error);
|
|
setLoadedTableColumns((prev) => ({ ...prev, [tableName]: [] }));
|
|
} finally {
|
|
setLoadingColumns((prev) => ({ ...prev, [tableName]: false }));
|
|
}
|
|
};
|
|
|
|
// 🆕 엔티티 참조 테이블의 컬럼 로드
|
|
const loadEntityReferenceColumns = async (sourceTableName: string, columns: ColumnInfo[]) => {
|
|
const entityColumns = columns.filter(
|
|
(col) => (col.input_type === "entity" || col.webType === "entity") && col.referenceTable,
|
|
);
|
|
|
|
if (entityColumns.length === 0) {
|
|
return;
|
|
}
|
|
|
|
console.log(
|
|
`🔗 테이블 ${sourceTableName}의 엔티티 참조 ${entityColumns.length}개 발견:`,
|
|
entityColumns.map((c) => `${c.columnName} -> ${c.referenceTable}`),
|
|
);
|
|
|
|
const referenceTableData: Array<{ tableName: string; columns: ColumnInfo[] }> = [];
|
|
|
|
// 각 참조 테이블의 컬럼 로드
|
|
for (const entityCol of entityColumns) {
|
|
const refTableName = entityCol.referenceTable!;
|
|
|
|
// 이미 로드했으면 스킵
|
|
if (referenceTableData.some((t) => t.tableName === refTableName)) continue;
|
|
|
|
try {
|
|
const refColumnsResponse = await tableTypeApi.getColumns(refTableName);
|
|
const refColumns: ColumnInfo[] = (refColumnsResponse || []).map((col: any) => ({
|
|
tableName: col.tableName || refTableName,
|
|
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,
|
|
input_type: col.inputType || col.input_type,
|
|
}));
|
|
|
|
referenceTableData.push({ tableName: refTableName, columns: refColumns });
|
|
console.log(` ✅ 참조 테이블 ${refTableName} 컬럼 ${refColumns.length}개 로드됨`);
|
|
} catch (error) {
|
|
console.error(` ❌ 참조 테이블 ${refTableName} 컬럼 로드 실패:`, error);
|
|
}
|
|
}
|
|
|
|
// 참조 테이블 정보 저장
|
|
setEntityReferenceTables((prev) => ({
|
|
...prev,
|
|
[sourceTableName]: referenceTableData,
|
|
}));
|
|
|
|
console.log(`✅ [엔티티 참조] ${sourceTableName}의 참조 테이블 저장 완료:`, {
|
|
sourceTableName,
|
|
referenceTableCount: referenceTableData.length,
|
|
referenceTables: referenceTableData.map((t) => `${t.tableName}(${t.columns.length}개)`),
|
|
});
|
|
};
|
|
|
|
// 좌측/우측 테이블이 변경되면 해당 테이블의 컬럼 로드
|
|
useEffect(() => {
|
|
if (config.leftPanel?.tableName) {
|
|
loadTableColumns(config.leftPanel.tableName);
|
|
}
|
|
}, [config.leftPanel?.tableName]);
|
|
|
|
useEffect(() => {
|
|
if (config.rightPanel?.tableName) {
|
|
loadTableColumns(config.rightPanel.tableName);
|
|
}
|
|
}, [config.rightPanel?.tableName]);
|
|
|
|
// 🆕 좌측/우측 테이블이 모두 선택되면 엔티티 관계 자동 감지
|
|
const [autoDetectedRelations, setAutoDetectedRelations] = useState<
|
|
Array<{
|
|
leftColumn: string;
|
|
rightColumn: string;
|
|
direction: "left_to_right" | "right_to_left";
|
|
inputType: string;
|
|
displayColumn?: string;
|
|
}>
|
|
>([]);
|
|
const [isDetectingRelations, setIsDetectingRelations] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const detectRelations = async () => {
|
|
const leftTable = config.leftPanel?.tableName || screenTableName;
|
|
const rightTable = config.rightPanel?.tableName;
|
|
|
|
// 조인 모드이고 양쪽 테이블이 모두 있을 때만 감지
|
|
if (relationshipType !== "join" || !leftTable || !rightTable) {
|
|
setAutoDetectedRelations([]);
|
|
return;
|
|
}
|
|
|
|
setIsDetectingRelations(true);
|
|
try {
|
|
const { tableManagementApi } = await import("@/lib/api/tableManagement");
|
|
const response = await tableManagementApi.getTableEntityRelations(leftTable, rightTable);
|
|
|
|
if (response.success && response.data?.relations) {
|
|
console.log("🔍 엔티티 관계 자동 감지:", response.data.relations);
|
|
setAutoDetectedRelations(response.data.relations);
|
|
|
|
// 감지된 관계가 있고, 현재 설정된 키가 없으면 자동으로 첫 번째 관계를 설정
|
|
const currentKeys = config.rightPanel?.relation?.keys || [];
|
|
if (response.data.relations.length > 0 && currentKeys.length === 0) {
|
|
// 첫 번째 관계만 자동 설정 (사용자가 추가로 설정 가능)
|
|
const firstRel = response.data.relations[0];
|
|
console.log("✅ 첫 번째 엔티티 관계 자동 설정:", firstRel);
|
|
updateRightPanel({
|
|
relation: {
|
|
...config.rightPanel?.relation,
|
|
type: "join",
|
|
useMultipleKeys: true,
|
|
keys: [
|
|
{
|
|
leftColumn: firstRel.leftColumn,
|
|
rightColumn: firstRel.rightColumn,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("❌ 엔티티 관계 감지 실패:", error);
|
|
setAutoDetectedRelations([]);
|
|
} finally {
|
|
setIsDetectingRelations(false);
|
|
}
|
|
};
|
|
|
|
detectRelations();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [config.leftPanel?.tableName, config.rightPanel?.tableName, screenTableName, relationshipType]);
|
|
|
|
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 leftTableName = config.leftPanel?.tableName || screenTableName || "";
|
|
|
|
// 좌측 테이블 컬럼 (로드된 컬럼 사용)
|
|
const leftTableColumns = useMemo(() => {
|
|
return leftTableName ? loadedTableColumns[leftTableName] || [] : [];
|
|
}, [loadedTableColumns, leftTableName]);
|
|
|
|
// 우측 테이블명
|
|
const rightTableName = config.rightPanel?.tableName || "";
|
|
|
|
// 우측 테이블 컬럼 (로드된 컬럼 사용)
|
|
const rightTableColumns = useMemo(() => {
|
|
return rightTableName ? loadedTableColumns[rightTableName] || [] : [];
|
|
}, [loadedTableColumns, rightTableName]);
|
|
|
|
// 테이블 데이터 로딩 상태 확인
|
|
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-4">
|
|
{/* 관계 타입 선택 */}
|
|
<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="mt-4 space-y-4 border-t pt-4">
|
|
<h3 className="text-sm font-semibold">좌측 패널 설정 (마스터)</h3>
|
|
|
|
<div className="space-y-2">
|
|
<Label>패널 제목</Label>
|
|
<Input
|
|
value={localTitles.left}
|
|
onChange={(e) => {
|
|
setIsUserEditing(true);
|
|
setLocalTitles((prev) => ({ ...prev, left: e.target.value }));
|
|
}}
|
|
onBlur={() => {
|
|
setIsUserEditing(false);
|
|
updateLeftPanel({ title: localTitles.left });
|
|
}}
|
|
placeholder="좌측 패널 제목"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>헤더 높이 (px)</Label>
|
|
<Input
|
|
type="number"
|
|
value={config.leftPanel?.panelHeaderHeight || 48}
|
|
onChange={(e) => updateLeftPanel({ panelHeaderHeight: parseInt(e.target.value) || 48 })}
|
|
placeholder="48"
|
|
min={32}
|
|
max={120}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">패널 상단 헤더의 높이 (기본: 48px)</p>
|
|
</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="space-y-2">
|
|
<Label>표시 모드</Label>
|
|
<Select
|
|
value={config.leftPanel?.displayMode || "list"}
|
|
onValueChange={(value: "list" | "table") => updateLeftPanel({ displayMode: value })}
|
|
>
|
|
<SelectTrigger className="bg-white">
|
|
<SelectValue placeholder="표시 모드 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="list">
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">목록 (LIST)</span>
|
|
<span className="text-xs text-gray-500">클릭 가능한 항목 목록 (기본)</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="table">
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">테이블 (TABLE)</span>
|
|
<span className="text-xs text-gray-500">컬럼 헤더가 있는 테이블 형식</span>
|
|
</div>
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</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>
|
|
)}
|
|
|
|
{/* 좌측 패널 표시 컬럼 설정 */}
|
|
<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.leftPanel?.columns || [];
|
|
const newColumns = [...currentColumns, { name: "", label: "", width: 100 }];
|
|
updateLeftPanel({ columns: newColumns });
|
|
}}
|
|
className="h-7 text-xs"
|
|
disabled={!config.leftPanel?.tableName && !screenTableName}
|
|
>
|
|
<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?.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.leftPanel?.columns || []).map((col, index) => {
|
|
const isTableMode = config.leftPanel?.displayMode === "table";
|
|
|
|
return (
|
|
<div key={index} className="space-y-2 rounded-md border bg-white p-2">
|
|
<div className="flex items-center gap-2">
|
|
{/* 순서 변경 버튼 */}
|
|
<div className="flex flex-col gap-0.5">
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => {
|
|
if (index === 0) return;
|
|
const newColumns = [...(config.leftPanel?.columns || [])];
|
|
[newColumns[index - 1], newColumns[index]] = [newColumns[index], newColumns[index - 1]];
|
|
updateLeftPanel({ columns: newColumns });
|
|
}}
|
|
disabled={index === 0}
|
|
className="h-4 w-6 p-0"
|
|
title="위로 이동"
|
|
>
|
|
<ArrowUp className="h-3 w-3" />
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => {
|
|
const columns = config.leftPanel?.columns || [];
|
|
if (index === columns.length - 1) return;
|
|
const newColumns = [...columns];
|
|
[newColumns[index], newColumns[index + 1]] = [newColumns[index + 1], newColumns[index]];
|
|
updateLeftPanel({ columns: newColumns });
|
|
}}
|
|
disabled={index === (config.leftPanel?.columns || []).length - 1}
|
|
className="h-4 w-6 p-0"
|
|
title="아래로 이동"
|
|
>
|
|
<ArrowDown className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="flex-1">
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button variant="outline" role="combobox" className="h-8 w-full justify-between text-xs">
|
|
{col.label || 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>
|
|
<div className="max-h-[300px] overflow-auto">
|
|
{/* 기본 테이블 컬럼 */}
|
|
<CommandGroup heading={leftTableName ? `📋 ${leftTableName} 컬럼` : "📋 기본 컬럼"}>
|
|
{leftTableColumns.map((column) => (
|
|
<CommandItem
|
|
key={column.columnName}
|
|
value={column.columnName}
|
|
onSelect={(value) => {
|
|
const newColumns = [...(config.leftPanel?.columns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
name: value,
|
|
label: column.columnLabel || value,
|
|
};
|
|
updateLeftPanel({ columns: newColumns });
|
|
// Popover 닫기
|
|
document.body.click();
|
|
}}
|
|
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>
|
|
|
|
{/* 🆕 엔티티 참조 테이블 컬럼 */}
|
|
{leftTableName &&
|
|
entityReferenceTables[leftTableName]?.map((refTable) => (
|
|
<CommandGroup
|
|
key={refTable.tableName}
|
|
heading={`🔗 ${refTable.tableName} (엔티티)`}
|
|
>
|
|
{refTable.columns.map((column) => {
|
|
const fullColumnName = `${refTable.tableName}.${column.columnName}`;
|
|
return (
|
|
<CommandItem
|
|
key={fullColumnName}
|
|
value={fullColumnName}
|
|
onSelect={(value) => {
|
|
const newColumns = [...(config.leftPanel?.columns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
name: value,
|
|
label: column.columnLabel || column.columnName,
|
|
};
|
|
updateLeftPanel({ columns: newColumns });
|
|
// Popover 닫기
|
|
document.body.click();
|
|
}}
|
|
className="pl-6 text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
col.name === fullColumnName ? "opacity-100" : "opacity-0",
|
|
)}
|
|
/>
|
|
{column.columnLabel || column.columnName}
|
|
<span className="ml-2 text-[10px] text-gray-500">
|
|
({column.columnName})
|
|
</span>
|
|
</CommandItem>
|
|
);
|
|
})}
|
|
</CommandGroup>
|
|
))}
|
|
</div>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => {
|
|
const newColumns = (config.leftPanel?.columns || []).filter((_, i) => i !== index);
|
|
updateLeftPanel({ columns: newColumns });
|
|
}}
|
|
className="h-8 w-8 p-0"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 테이블 모드 전용 옵션 */}
|
|
{isTableMode && (
|
|
<div className="grid grid-cols-3 gap-2 pt-1">
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px] text-gray-600">너비 (px)</Label>
|
|
<Input
|
|
type="number"
|
|
min="50"
|
|
value={col.width || 100}
|
|
onChange={(e) => {
|
|
const newColumns = [...(config.leftPanel?.columns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
width: parseInt(e.target.value) || 100,
|
|
};
|
|
updateLeftPanel({ columns: newColumns });
|
|
}}
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px] text-gray-600">정렬</Label>
|
|
<Select
|
|
value={col.align || "left"}
|
|
onValueChange={(value: "left" | "center" | "right") => {
|
|
const newColumns = [...(config.leftPanel?.columns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
align: value,
|
|
};
|
|
updateLeftPanel({ columns: newColumns });
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="left">왼쪽</SelectItem>
|
|
<SelectItem value="center">가운데</SelectItem>
|
|
<SelectItem value="right">오른쪽</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="flex items-end">
|
|
<label className="flex h-7 cursor-pointer items-center gap-1 text-[10px]">
|
|
<input
|
|
type="checkbox"
|
|
checked={col.sortable ?? false}
|
|
onChange={(e) => {
|
|
const newColumns = [...(config.leftPanel?.columns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
sortable: e.target.checked,
|
|
};
|
|
updateLeftPanel({ columns: newColumns });
|
|
}}
|
|
className="h-3 w-3"
|
|
/>
|
|
정렬가능
|
|
</label>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 🆕 날짜 타입 포맷 설정 (좌측) */}
|
|
{(() => {
|
|
const column = leftTableColumns.find((c) => c.columnName === col.name);
|
|
const dbDateTypes = [
|
|
"date",
|
|
"timestamp",
|
|
"timestamptz",
|
|
"timestamp with time zone",
|
|
"timestamp without time zone",
|
|
"time",
|
|
"timetz",
|
|
];
|
|
const inputDateTypes = ["date", "datetime", "time"];
|
|
|
|
const isDate =
|
|
column &&
|
|
(dbDateTypes.includes(column.dataType?.toLowerCase() || "") ||
|
|
inputDateTypes.includes(column.input_type?.toLowerCase() || "") ||
|
|
inputDateTypes.includes(column.webType?.toLowerCase() || ""));
|
|
if (!isDate) return null;
|
|
return (
|
|
<div className="space-y-2 border-t pt-2">
|
|
<Label className="text-[10px] text-gray-600">날짜 포맷</Label>
|
|
<Select
|
|
value={col.format?.dateFormat || "YYYY-MM-DD"}
|
|
onValueChange={(value) => {
|
|
const newColumns = [...(config.leftPanel?.columns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
format: { ...newColumns[index].format, type: "date", dateFormat: value },
|
|
};
|
|
updateLeftPanel({ columns: newColumns });
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs">
|
|
<SelectValue placeholder="날짜 형식" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="YYYY-MM-DD">2024-01-15</SelectItem>
|
|
<SelectItem value="YYYY.MM.DD">2024.01.15</SelectItem>
|
|
<SelectItem value="YYYY/MM/DD">2024/01/15</SelectItem>
|
|
<SelectItem value="YYYY-MM-DD HH:mm">2024-01-15 14:30</SelectItem>
|
|
<SelectItem value="relative">상대 시간</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
);
|
|
})()}
|
|
|
|
{/* 🆕 숫자 타입 포맷 설정 (좌측) */}
|
|
{(() => {
|
|
const column = leftTableColumns.find((c) => c.columnName === col.name);
|
|
const dbNumericTypes = [
|
|
"numeric",
|
|
"decimal",
|
|
"integer",
|
|
"bigint",
|
|
"double precision",
|
|
"real",
|
|
"smallint",
|
|
"int4",
|
|
"int8",
|
|
"float4",
|
|
"float8",
|
|
];
|
|
const inputNumericTypes = ["number", "decimal", "currency", "integer"];
|
|
|
|
const isNumeric =
|
|
column &&
|
|
(dbNumericTypes.includes(column.dataType?.toLowerCase() || "") ||
|
|
inputNumericTypes.includes(column.input_type?.toLowerCase() || "") ||
|
|
inputNumericTypes.includes(column.webType?.toLowerCase() || ""));
|
|
if (!isNumeric) return null;
|
|
return (
|
|
<div className="space-y-2 border-t pt-2">
|
|
<Label className="text-[10px] text-gray-600">숫자 포맷</Label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<label className="flex cursor-pointer items-center gap-1 text-[10px]">
|
|
<input
|
|
type="checkbox"
|
|
checked={col.format?.thousandSeparator ?? false}
|
|
onChange={(e) => {
|
|
const newColumns = [...(config.leftPanel?.columns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
format: {
|
|
...newColumns[index].format,
|
|
type: "number",
|
|
thousandSeparator: e.target.checked,
|
|
},
|
|
};
|
|
updateLeftPanel({ columns: newColumns });
|
|
}}
|
|
className="h-3 w-3"
|
|
/>
|
|
천 단위 (,)
|
|
</label>
|
|
<div className="flex items-center gap-1">
|
|
<Label className="text-[10px]">소수점:</Label>
|
|
<Input
|
|
type="number"
|
|
min="0"
|
|
max="10"
|
|
value={col.format?.decimalPlaces ?? ""}
|
|
onChange={(e) => {
|
|
const newColumns = [...(config.leftPanel?.columns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
format: {
|
|
...newColumns[index].format,
|
|
type: "number",
|
|
decimalPlaces: e.target.value ? parseInt(e.target.value) : undefined,
|
|
},
|
|
};
|
|
updateLeftPanel({ columns: newColumns });
|
|
}}
|
|
className="h-6 w-12 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</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 ? "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-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 cursor-pointer items-center gap-1 text-xs text-gray-600">
|
|
<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="mt-4 space-y-4 border-t pt-4">
|
|
<h3 className="text-sm font-semibold">좌측 패널 데이터 필터링</h3>
|
|
<p className="text-muted-foreground text-xs">특정 컬럼 값으로 좌측 패널 데이터를 필터링합니다</p>
|
|
<DataFilterConfigPanel
|
|
tableName={config.leftPanel?.tableName || screenTableName}
|
|
columns={leftTableColumns.map(
|
|
(col) =>
|
|
({
|
|
columnName: col.columnName,
|
|
columnLabel: col.columnLabel || col.columnName,
|
|
dataType: col.dataType || "text",
|
|
input_type: (col as any).input_type,
|
|
}) as any,
|
|
)}
|
|
config={config.leftPanel?.dataFilter}
|
|
onConfigChange={(dataFilter) => updateLeftPanel({ dataFilter })}
|
|
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
|
/>
|
|
</div>
|
|
|
|
{/* 우측 패널 설정 */}
|
|
<div className="mt-4 space-y-4 border-t pt-4">
|
|
<h3 className="text-sm font-semibold">우측 패널 설정 ({relationshipType === "detail" ? "상세" : "조인"})</h3>
|
|
|
|
<div className="space-y-2">
|
|
<Label>패널 제목</Label>
|
|
<Input
|
|
value={localTitles.right}
|
|
onChange={(e) => {
|
|
setIsUserEditing(true);
|
|
setLocalTitles((prev) => ({ ...prev, right: e.target.value }));
|
|
}}
|
|
onBlur={() => {
|
|
setIsUserEditing(false);
|
|
updateRightPanel({ title: localTitles.right });
|
|
}}
|
|
placeholder="우측 패널 제목"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>헤더 높이 (px)</Label>
|
|
<Input
|
|
type="number"
|
|
value={config.rightPanel?.panelHeaderHeight || 48}
|
|
onChange={(e) => updateRightPanel({ panelHeaderHeight: parseInt(e.target.value) || 48 })}
|
|
placeholder="48"
|
|
min={32}
|
|
max={120}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">패널 상단 헤더의 높이 (기본: 48px)</p>
|
|
</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.displayName || ""} ${table.tableName}`}
|
|
onSelect={() => {
|
|
updateRightPanel({ tableName: table.tableName });
|
|
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>
|
|
)}
|
|
|
|
<div className="space-y-2">
|
|
<Label>표시 모드</Label>
|
|
<Select
|
|
value={config.rightPanel?.displayMode || "list"}
|
|
onValueChange={(value: "list" | "table") => updateRightPanel({ displayMode: value })}
|
|
>
|
|
<SelectTrigger className="bg-white">
|
|
<SelectValue placeholder="표시 모드 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="list">
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">목록 (LIST)</span>
|
|
<span className="text-xs text-gray-500">클릭 가능한 항목 목록 (기본)</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="table">
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">테이블 (TABLE)</span>
|
|
<span className="text-xs text-gray-500">컬럼 헤더가 있는 테이블 형식</span>
|
|
</div>
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 요약 표시 설정 (LIST 모드에서만) */}
|
|
{(config.rightPanel?.displayMode || "list") === "list" && (
|
|
<div className="space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-3">
|
|
<Label className="text-sm font-semibold">요약 표시 설정</Label>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-xs">표시할 컬럼 개수</Label>
|
|
<Input
|
|
type="number"
|
|
min="1"
|
|
max="10"
|
|
value={config.rightPanel?.summaryColumnCount ?? 3}
|
|
onChange={(e) => {
|
|
const value = parseInt(e.target.value) || 3;
|
|
updateRightPanel({ summaryColumnCount: value });
|
|
}}
|
|
className="bg-white"
|
|
/>
|
|
<p className="text-xs text-gray-500">접기 전에 표시할 컬럼 개수 (기본: 3개)</p>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between space-x-2">
|
|
<div className="flex-1">
|
|
<Label className="text-xs">라벨 표시</Label>
|
|
<p className="text-xs text-gray-500">컬럼명 표시 여부</p>
|
|
</div>
|
|
<Checkbox
|
|
checked={config.rightPanel?.summaryShowLabel ?? true}
|
|
onCheckedChange={(checked) => {
|
|
updateRightPanel({ summaryShowLabel: checked as boolean });
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 엔티티 관계 자동 감지 (읽기 전용) - 조인 모드에서만 표시 */}
|
|
{relationshipType !== "detail" && (
|
|
<div className="space-y-3 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
|
<div>
|
|
<Label className="text-sm font-semibold">테이블 관계 (자동 감지)</Label>
|
|
<p className="text-xs text-gray-600">테이블 타입관리에서 정의된 엔티티 관계입니다</p>
|
|
</div>
|
|
|
|
{isDetectingRelations ? (
|
|
<div className="flex items-center gap-2 text-xs text-gray-500">
|
|
<div className="h-3 w-3 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
|
관계 감지 중...
|
|
</div>
|
|
) : autoDetectedRelations.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{autoDetectedRelations.map((rel, index) => (
|
|
<div key={index} className="flex items-center gap-2 rounded-md border border-blue-300 bg-white p-2">
|
|
<span className="rounded bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700">
|
|
{leftTableName}.{rel.leftColumn}
|
|
</span>
|
|
<ArrowRight className="h-3 w-3 text-blue-400" />
|
|
<span className="rounded bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700">
|
|
{rightTableName}.{rel.rightColumn}
|
|
</span>
|
|
<span className="ml-auto text-[10px] text-gray-500">
|
|
{rel.inputType === "entity" ? "엔티티" : "카테고리"}
|
|
</span>
|
|
</div>
|
|
))}
|
|
<p className="text-[10px] text-blue-600">
|
|
테이블 타입관리에서 엔티티/카테고리 설정을 변경하면 자동으로 적용됩니다
|
|
</p>
|
|
</div>
|
|
) : config.rightPanel?.tableName ? (
|
|
<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>
|
|
) : (
|
|
<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>
|
|
)}
|
|
</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) => {
|
|
const isTableMode = config.rightPanel?.displayMode === "table";
|
|
|
|
return (
|
|
<div key={index} className="space-y-2 rounded-md border bg-white p-2">
|
|
<div className="flex items-center gap-2">
|
|
{/* 순서 변경 버튼 */}
|
|
<div className="flex flex-col gap-0.5">
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => {
|
|
if (index === 0) return;
|
|
const newColumns = [...(config.rightPanel?.columns || [])];
|
|
[newColumns[index - 1], newColumns[index]] = [newColumns[index], newColumns[index - 1]];
|
|
updateRightPanel({ columns: newColumns });
|
|
}}
|
|
disabled={index === 0}
|
|
className="h-4 w-6 p-0"
|
|
title="위로 이동"
|
|
>
|
|
<ArrowUp className="h-3 w-3" />
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => {
|
|
const columns = config.rightPanel?.columns || [];
|
|
if (index === columns.length - 1) return;
|
|
const newColumns = [...columns];
|
|
[newColumns[index], newColumns[index + 1]] = [newColumns[index + 1], newColumns[index]];
|
|
updateRightPanel({ columns: newColumns });
|
|
}}
|
|
disabled={index === (config.rightPanel?.columns || []).length - 1}
|
|
className="h-4 w-6 p-0"
|
|
title="아래로 이동"
|
|
>
|
|
<ArrowDown className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="flex-1">
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button variant="outline" role="combobox" className="h-8 w-full justify-between text-xs">
|
|
{col.label || 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>
|
|
<div className="max-h-[300px] overflow-auto">
|
|
{/* 기본 테이블 컬럼 */}
|
|
<CommandGroup heading={rightTableName ? `📋 ${rightTableName} 컬럼` : "📋 기본 컬럼"}>
|
|
{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 });
|
|
// Popover 닫기
|
|
document.body.click();
|
|
}}
|
|
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>
|
|
|
|
{/* 🆕 엔티티 참조 테이블 컬럼 */}
|
|
{rightTableName &&
|
|
entityReferenceTables[rightTableName]?.map((refTable) => (
|
|
<CommandGroup
|
|
key={refTable.tableName}
|
|
heading={`🔗 ${refTable.tableName} (엔티티)`}
|
|
>
|
|
{refTable.columns.map((column) => {
|
|
const fullColumnName = `${refTable.tableName}.${column.columnName}`;
|
|
return (
|
|
<CommandItem
|
|
key={fullColumnName}
|
|
value={fullColumnName}
|
|
onSelect={(value) => {
|
|
const newColumns = [...(config.rightPanel?.columns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
name: value,
|
|
label: column.columnLabel || column.columnName,
|
|
};
|
|
updateRightPanel({ columns: newColumns });
|
|
// Popover 닫기
|
|
document.body.click();
|
|
}}
|
|
className="pl-6 text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
col.name === fullColumnName ? "opacity-100" : "opacity-0",
|
|
)}
|
|
/>
|
|
{column.columnLabel || column.columnName}
|
|
<span className="ml-2 text-[10px] text-gray-500">
|
|
({column.columnName})
|
|
</span>
|
|
</CommandItem>
|
|
);
|
|
})}
|
|
</CommandGroup>
|
|
))}
|
|
</div>
|
|
</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>
|
|
|
|
{/* 테이블 모드 전용 옵션 */}
|
|
{isTableMode && (
|
|
<div className="grid grid-cols-3 gap-2 pt-1">
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px] text-gray-600">너비 (px)</Label>
|
|
<Input
|
|
type="number"
|
|
min="50"
|
|
value={col.width || 100}
|
|
onChange={(e) => {
|
|
const newColumns = [...(config.rightPanel?.columns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
width: parseInt(e.target.value) || 100,
|
|
};
|
|
updateRightPanel({ columns: newColumns });
|
|
}}
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px] text-gray-600">정렬</Label>
|
|
<Select
|
|
value={col.align || "left"}
|
|
onValueChange={(value: "left" | "center" | "right") => {
|
|
const newColumns = [...(config.rightPanel?.columns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
align: value,
|
|
};
|
|
updateRightPanel({ columns: newColumns });
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="left">왼쪽</SelectItem>
|
|
<SelectItem value="center">가운데</SelectItem>
|
|
<SelectItem value="right">오른쪽</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="flex items-end">
|
|
<label className="flex h-7 cursor-pointer items-center gap-1 text-[10px]">
|
|
<input
|
|
type="checkbox"
|
|
checked={col.sortable ?? false}
|
|
onChange={(e) => {
|
|
const newColumns = [...(config.rightPanel?.columns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
sortable: e.target.checked,
|
|
};
|
|
updateRightPanel({ columns: newColumns });
|
|
}}
|
|
className="h-3 w-3"
|
|
/>
|
|
정렬가능
|
|
</label>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* LIST 모드: 볼드 설정 */}
|
|
{!isTableMode && (
|
|
<div className="space-y-2 border-t pt-2">
|
|
<Label className="text-[10px] text-gray-600">요약 표시 옵션</Label>
|
|
<label className="flex cursor-pointer items-center gap-1.5 text-[10px]">
|
|
<input
|
|
type="checkbox"
|
|
checked={col.bold ?? false}
|
|
onChange={(e) => {
|
|
const newColumns = [...(config.rightPanel?.columns || [])];
|
|
newColumns[index] = { ...newColumns[index], bold: e.target.checked };
|
|
updateRightPanel({ columns: newColumns });
|
|
}}
|
|
className="h-3 w-3"
|
|
/>
|
|
값 굵게 표시
|
|
</label>
|
|
</div>
|
|
)}
|
|
|
|
{/* 🆕 날짜 타입 포맷 설정 */}
|
|
{(() => {
|
|
// 컬럼 타입 확인 (DB 타입 + 입력 타입 모두 체크)
|
|
const column = rightTableColumns.find((c) => c.columnName === col.name);
|
|
const dbDateTypes = [
|
|
"date",
|
|
"timestamp",
|
|
"timestamptz",
|
|
"timestamp with time zone",
|
|
"timestamp without time zone",
|
|
"time",
|
|
"timetz",
|
|
];
|
|
const inputDateTypes = ["date", "datetime", "time"];
|
|
|
|
const isDate =
|
|
column &&
|
|
(dbDateTypes.includes(column.dataType?.toLowerCase() || "") ||
|
|
inputDateTypes.includes(column.input_type?.toLowerCase() || "") ||
|
|
inputDateTypes.includes(column.webType?.toLowerCase() || ""));
|
|
|
|
if (!isDate) return null;
|
|
|
|
return (
|
|
<div className="space-y-2 border-t pt-2">
|
|
<Label className="text-[10px] text-gray-600">날짜 포맷 설정</Label>
|
|
<Select
|
|
value={col.format?.dateFormat || "YYYY-MM-DD"}
|
|
onValueChange={(value) => {
|
|
const newColumns = [...(config.rightPanel?.columns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
format: {
|
|
...newColumns[index].format,
|
|
type: "date",
|
|
dateFormat: value,
|
|
},
|
|
};
|
|
updateRightPanel({ columns: newColumns });
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue placeholder="날짜 형식 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="YYYY-MM-DD">2024-01-15 (YYYY-MM-DD)</SelectItem>
|
|
<SelectItem value="YYYY.MM.DD">2024.01.15 (YYYY.MM.DD)</SelectItem>
|
|
<SelectItem value="YYYY/MM/DD">2024/01/15 (YYYY/MM/DD)</SelectItem>
|
|
<SelectItem value="MM-DD-YYYY">01-15-2024 (MM-DD-YYYY)</SelectItem>
|
|
<SelectItem value="DD-MM-YYYY">15-01-2024 (DD-MM-YYYY)</SelectItem>
|
|
<SelectItem value="YYYY-MM-DD HH:mm">2024-01-15 14:30 (날짜+시간)</SelectItem>
|
|
<SelectItem value="YYYY-MM-DD HH:mm:ss">2024-01-15 14:30:45 (날짜+시간+초)</SelectItem>
|
|
<SelectItem value="relative">상대 시간 (3일 전)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
{/* 미리보기 */}
|
|
<div className="rounded bg-gray-100 p-2">
|
|
<p className="mb-1 text-[10px] text-gray-600">미리보기:</p>
|
|
<p className="text-xs font-medium">
|
|
{(() => {
|
|
const now = new Date();
|
|
const format = col.format?.dateFormat || "YYYY-MM-DD";
|
|
if (format === "relative") return "3일 전";
|
|
return format
|
|
.replace("YYYY", String(now.getFullYear()))
|
|
.replace("MM", String(now.getMonth() + 1).padStart(2, "0"))
|
|
.replace("DD", String(now.getDate()).padStart(2, "0"))
|
|
.replace("HH", String(now.getHours()).padStart(2, "0"))
|
|
.replace("mm", String(now.getMinutes()).padStart(2, "0"))
|
|
.replace("ss", String(now.getSeconds()).padStart(2, "0"));
|
|
})()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})()}
|
|
|
|
{/* 🆕 숫자 타입 포맷 설정 */}
|
|
{(() => {
|
|
// 컬럼 타입 확인 (DB 타입 + 입력 타입 모두 체크)
|
|
const column = rightTableColumns.find((c) => c.columnName === col.name);
|
|
const dbNumericTypes = [
|
|
"numeric",
|
|
"decimal",
|
|
"integer",
|
|
"bigint",
|
|
"double precision",
|
|
"real",
|
|
"smallint",
|
|
"int4",
|
|
"int8",
|
|
"float4",
|
|
"float8",
|
|
];
|
|
const inputNumericTypes = ["number", "decimal", "currency", "integer"];
|
|
|
|
const isNumeric =
|
|
column &&
|
|
(dbNumericTypes.includes(column.dataType?.toLowerCase() || "") ||
|
|
inputNumericTypes.includes(column.input_type?.toLowerCase() || "") ||
|
|
inputNumericTypes.includes(column.webType?.toLowerCase() || ""));
|
|
|
|
if (!isNumeric) return null;
|
|
|
|
return (
|
|
<div className="space-y-2 border-t pt-2">
|
|
<Label className="text-[10px] text-gray-600">숫자 포맷 설정</Label>
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{/* 천 단위 구분자 */}
|
|
<label className="flex cursor-pointer items-center gap-1.5 text-[10px]">
|
|
<input
|
|
type="checkbox"
|
|
checked={col.format?.thousandSeparator ?? false}
|
|
onChange={(e) => {
|
|
const newColumns = [...(config.rightPanel?.columns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
format: {
|
|
...newColumns[index].format,
|
|
thousandSeparator: e.target.checked,
|
|
},
|
|
};
|
|
updateRightPanel({ columns: newColumns });
|
|
}}
|
|
className="h-3 w-3"
|
|
/>
|
|
천 단위 구분자 (,)
|
|
</label>
|
|
|
|
{/* 소수점 자릿수 */}
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px] text-gray-600">소수점</Label>
|
|
<Input
|
|
type="number"
|
|
min="0"
|
|
max="10"
|
|
placeholder="0"
|
|
value={col.format?.decimalPlaces ?? ""}
|
|
onChange={(e) => {
|
|
const newColumns = [...(config.rightPanel?.columns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
format: {
|
|
...newColumns[index].format,
|
|
decimalPlaces: e.target.value ? parseInt(e.target.value) : undefined,
|
|
},
|
|
};
|
|
updateRightPanel({ columns: newColumns });
|
|
}}
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{/* 접두사 */}
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px] text-gray-600">접두사</Label>
|
|
<Input
|
|
type="text"
|
|
placeholder="₩, $, 등"
|
|
value={col.format?.prefix ?? ""}
|
|
onChange={(e) => {
|
|
const newColumns = [...(config.rightPanel?.columns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
format: {
|
|
...newColumns[index].format,
|
|
prefix: e.target.value || undefined,
|
|
},
|
|
};
|
|
updateRightPanel({ columns: newColumns });
|
|
}}
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{/* 접미사 */}
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px] text-gray-600">접미사</Label>
|
|
<Input
|
|
type="text"
|
|
placeholder="원, 개, 등"
|
|
value={col.format?.suffix ?? ""}
|
|
onChange={(e) => {
|
|
const newColumns = [...(config.rightPanel?.columns || [])];
|
|
newColumns[index] = {
|
|
...newColumns[index],
|
|
format: {
|
|
...newColumns[index].format,
|
|
suffix: e.target.value || undefined,
|
|
},
|
|
};
|
|
updateRightPanel({ columns: newColumns });
|
|
}}
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 미리보기 */}
|
|
{(col.format?.thousandSeparator ||
|
|
col.format?.prefix ||
|
|
col.format?.suffix ||
|
|
col.format?.decimalPlaces !== undefined) && (
|
|
<div className="rounded bg-gray-100 p-2">
|
|
<p className="mb-1 text-[10px] text-gray-600">미리보기:</p>
|
|
<p className="text-xs font-medium">
|
|
{col.format?.prefix || ""}
|
|
{(1234567.89).toLocaleString("ko-KR", {
|
|
minimumFractionDigits: col.format?.decimalPlaces ?? 0,
|
|
maximumFractionDigits: col.format?.decimalPlaces ?? 10,
|
|
useGrouping: col.format?.thousandSeparator ?? false,
|
|
})}
|
|
{col.format?.suffix || ""}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})()}
|
|
</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 ? "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-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 cursor-pointer items-center gap-1 text-xs text-gray-600">
|
|
<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="mt-3 space-y-3 rounded-lg border border-orange-200 bg-orange-50 p-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="border-input mt-1 h-20 w-full rounded-md border bg-white px-3 py-2 font-mono text-xs"
|
|
/>
|
|
<p className="mt-1 text-[10px] text-gray-500">자동으로 채워질 컬럼과 기본값 (예: is_primary: false)</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 우측 패널 데이터 필터링 */}
|
|
<div className="mt-4 space-y-4 border-t pt-4">
|
|
<h3 className="text-sm font-semibold">우측 패널 데이터 필터링</h3>
|
|
<p className="text-muted-foreground text-xs">특정 컬럼 값으로 우측 패널 데이터를 필터링합니다</p>
|
|
<DataFilterConfigPanel
|
|
tableName={config.rightPanel?.tableName}
|
|
columns={rightTableColumns.map(
|
|
(col) =>
|
|
({
|
|
columnName: col.columnName,
|
|
columnLabel: col.columnLabel || col.columnName,
|
|
dataType: col.dataType || "text",
|
|
input_type: (col as any).input_type,
|
|
}) as any,
|
|
)}
|
|
config={config.rightPanel?.dataFilter}
|
|
onConfigChange={(dataFilter) => updateRightPanel({ dataFilter })}
|
|
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
|
/>
|
|
</div>
|
|
|
|
{/* 우측 패널 중복 제거 */}
|
|
<div className="mt-4 space-y-4 border-t pt-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-sm font-semibold">중복 데이터 제거</h3>
|
|
<p className="text-muted-foreground text-xs">같은 값을 가진 데이터를 하나로 통합하여 표시</p>
|
|
</div>
|
|
<Switch
|
|
checked={config.rightPanel?.deduplication?.enabled ?? false}
|
|
onCheckedChange={(checked) => {
|
|
if (checked) {
|
|
updateRightPanel({
|
|
deduplication: {
|
|
enabled: true,
|
|
groupByColumn: "",
|
|
keepStrategy: "latest",
|
|
sortColumn: "start_date",
|
|
},
|
|
});
|
|
} else {
|
|
updateRightPanel({ deduplication: undefined });
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{config.rightPanel?.deduplication?.enabled && (
|
|
<div className="space-y-3 border-l-2 pl-4">
|
|
{/* 중복 제거 기준 컬럼 */}
|
|
<div>
|
|
<Label className="text-xs">중복 제거 기준 컬럼</Label>
|
|
<Select
|
|
value={config.rightPanel?.deduplication?.groupByColumn || ""}
|
|
onValueChange={(value) =>
|
|
updateRightPanel({
|
|
deduplication: { ...config.rightPanel?.deduplication!, groupByColumn: value },
|
|
})
|
|
}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue placeholder="기준 컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{rightTableColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.columnLabel || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-muted-foreground mt-1 text-[10px]">
|
|
이 컬럼의 값이 같은 데이터들 중 하나만 표시합니다
|
|
</p>
|
|
</div>
|
|
|
|
{/* 유지 전략 */}
|
|
<div>
|
|
<Label className="text-xs">유지 전략</Label>
|
|
<Select
|
|
value={config.rightPanel?.deduplication?.keepStrategy || "latest"}
|
|
onValueChange={(value: any) =>
|
|
updateRightPanel({
|
|
deduplication: { ...config.rightPanel?.deduplication!, keepStrategy: value },
|
|
})
|
|
}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="latest">최신 데이터 (가장 최근)</SelectItem>
|
|
<SelectItem value="earliest">최초 데이터 (가장 오래된)</SelectItem>
|
|
<SelectItem value="current_date">현재 유효한 데이터 (날짜 기준)</SelectItem>
|
|
<SelectItem value="base_price">기준단가로 설정된 데이터</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-muted-foreground mt-1 text-[10px]">
|
|
{config.rightPanel?.deduplication?.keepStrategy === "latest" &&
|
|
"가장 최근에 추가된 데이터를 표시합니다"}
|
|
{config.rightPanel?.deduplication?.keepStrategy === "earliest" &&
|
|
"가장 먼저 추가된 데이터를 표시합니다"}
|
|
{config.rightPanel?.deduplication?.keepStrategy === "current_date" &&
|
|
"오늘 날짜 기준으로 유효한 기간의 데이터를 표시합니다"}
|
|
{config.rightPanel?.deduplication?.keepStrategy === "base_price" &&
|
|
"기준단가(base_price)로 체크된 데이터를 표시합니다"}
|
|
</p>
|
|
</div>
|
|
|
|
{/* 정렬 기준 컬럼 (latest/earliest만) */}
|
|
{(config.rightPanel?.deduplication?.keepStrategy === "latest" ||
|
|
config.rightPanel?.deduplication?.keepStrategy === "earliest") && (
|
|
<div>
|
|
<Label className="text-xs">정렬 기준 컬럼</Label>
|
|
<Select
|
|
value={config.rightPanel?.deduplication?.sortColumn || ""}
|
|
onValueChange={(value) =>
|
|
updateRightPanel({
|
|
deduplication: { ...config.rightPanel?.deduplication!, sortColumn: value },
|
|
})
|
|
}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue placeholder="정렬 기준 컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{rightTableColumns
|
|
.filter(
|
|
(col) =>
|
|
col.dataType === "date" || col.dataType === "timestamp" || col.columnName.includes("date"),
|
|
)
|
|
.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.columnLabel || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-muted-foreground mt-1 text-[10px]">
|
|
이 컬럼의 값으로 최신/최초를 판단합니다 (보통 날짜 컬럼)
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 🆕 우측 패널 수정 버튼 설정 */}
|
|
<div className="mt-4 space-y-4 border-t pt-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-sm font-semibold">수정 버튼 설정</h3>
|
|
<p className="text-muted-foreground text-xs">우측 리스트의 수정 버튼 동작 방식 설정</p>
|
|
</div>
|
|
<Switch
|
|
checked={config.rightPanel?.editButton?.enabled ?? true}
|
|
onCheckedChange={(checked) => {
|
|
updateRightPanel({
|
|
editButton: {
|
|
enabled: checked,
|
|
mode: config.rightPanel?.editButton?.mode || "auto",
|
|
buttonLabel: config.rightPanel?.editButton?.buttonLabel,
|
|
buttonVariant: config.rightPanel?.editButton?.buttonVariant,
|
|
},
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{(config.rightPanel?.editButton?.enabled ?? true) && (
|
|
<div className="space-y-3 border-l-2 pl-4">
|
|
{/* 수정 모드 */}
|
|
<div>
|
|
<Label className="text-xs">수정 모드</Label>
|
|
<Select
|
|
value={config.rightPanel?.editButton?.mode || "auto"}
|
|
onValueChange={(value: "auto" | "modal") =>
|
|
updateRightPanel({
|
|
editButton: {
|
|
...config.rightPanel?.editButton,
|
|
mode: value,
|
|
enabled: config.rightPanel?.editButton?.enabled ?? true,
|
|
},
|
|
})
|
|
}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="auto">자동 편집 (인라인)</SelectItem>
|
|
<SelectItem value="modal">커스텀 모달 화면</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-muted-foreground mt-1 text-[10px]">
|
|
{config.rightPanel?.editButton?.mode === "modal"
|
|
? "지정한 화면을 모달로 열어 데이터를 수정합니다"
|
|
: "현재 위치에서 직접 데이터를 수정합니다"}
|
|
</p>
|
|
</div>
|
|
|
|
{/* 모달 화면 선택 (modal 모드일 때만) */}
|
|
{config.rightPanel?.editButton?.mode === "modal" && (
|
|
<div>
|
|
<Label className="text-xs">모달 화면</Label>
|
|
<ScreenSelector
|
|
value={config.rightPanel?.editButton?.modalScreenId}
|
|
onChange={(screenId) =>
|
|
updateRightPanel({
|
|
editButton: {
|
|
...config.rightPanel?.editButton!,
|
|
modalScreenId: screenId,
|
|
},
|
|
})
|
|
}
|
|
/>
|
|
<p className="text-muted-foreground mt-1 text-[10px]">수정 버튼 클릭 시 열릴 화면을 선택하세요</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 버튼 라벨 */}
|
|
<div>
|
|
<Label className="text-xs">버튼 라벨</Label>
|
|
<Input
|
|
value={config.rightPanel?.editButton?.buttonLabel || "수정"}
|
|
onChange={(e) =>
|
|
updateRightPanel({
|
|
editButton: {
|
|
...config.rightPanel?.editButton!,
|
|
buttonLabel: e.target.value,
|
|
enabled: config.rightPanel?.editButton?.enabled ?? true,
|
|
mode: config.rightPanel?.editButton?.mode || "auto",
|
|
},
|
|
})
|
|
}
|
|
className="h-8 text-xs"
|
|
placeholder="수정"
|
|
/>
|
|
</div>
|
|
|
|
{/* 버튼 스타일 */}
|
|
<div>
|
|
<Label className="text-xs">버튼 스타일</Label>
|
|
<Select
|
|
value={config.rightPanel?.editButton?.buttonVariant || "outline"}
|
|
onValueChange={(value: any) =>
|
|
updateRightPanel({
|
|
editButton: {
|
|
...config.rightPanel?.editButton!,
|
|
buttonVariant: value,
|
|
enabled: config.rightPanel?.editButton?.enabled ?? true,
|
|
mode: config.rightPanel?.editButton?.mode || "auto",
|
|
},
|
|
})
|
|
}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="default">기본 (파란색)</SelectItem>
|
|
<SelectItem value="outline">외곽선</SelectItem>
|
|
<SelectItem value="ghost">투명</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 🆕 그룹핑 기준 컬럼 설정 (modal 모드일 때만 표시) */}
|
|
{config.rightPanel?.editButton?.mode === "modal" && (
|
|
<GroupByColumnsSelector
|
|
tableName={config.rightPanel?.tableName}
|
|
selectedColumns={config.rightPanel?.editButton?.groupByColumns || []}
|
|
onChange={(columns) => {
|
|
updateRightPanel({
|
|
editButton: {
|
|
...config.rightPanel?.editButton!,
|
|
groupByColumns: columns,
|
|
enabled: config.rightPanel?.editButton?.enabled ?? true,
|
|
mode: config.rightPanel?.editButton?.mode || "auto",
|
|
},
|
|
});
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 🆕 우측 패널 삭제 버튼 설정 */}
|
|
<div className="space-y-3 rounded-lg border border-red-200 bg-red-50 p-3">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<h3 className="text-sm font-semibold">삭제 버튼 설정</h3>
|
|
</div>
|
|
<Switch
|
|
checked={config.rightPanel?.deleteButton?.enabled ?? true}
|
|
onCheckedChange={(checked) => {
|
|
updateRightPanel({
|
|
deleteButton: {
|
|
enabled: checked,
|
|
buttonLabel: config.rightPanel?.deleteButton?.buttonLabel,
|
|
buttonVariant: config.rightPanel?.deleteButton?.buttonVariant,
|
|
},
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{(config.rightPanel?.deleteButton?.enabled ?? true) && (
|
|
<div className="space-y-3">
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{/* 버튼 라벨 */}
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">버튼 라벨</Label>
|
|
<Input
|
|
value={config.rightPanel?.deleteButton?.buttonLabel || ""}
|
|
placeholder="삭제"
|
|
onChange={(e) => {
|
|
updateRightPanel({
|
|
deleteButton: {
|
|
...config.rightPanel?.deleteButton!,
|
|
buttonLabel: e.target.value || undefined,
|
|
enabled: config.rightPanel?.deleteButton?.enabled ?? true,
|
|
},
|
|
});
|
|
}}
|
|
className="h-8 text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{/* 버튼 스타일 */}
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">버튼 스타일</Label>
|
|
<Select
|
|
value={config.rightPanel?.deleteButton?.buttonVariant || "ghost"}
|
|
onValueChange={(value: "default" | "outline" | "ghost" | "destructive") => {
|
|
updateRightPanel({
|
|
deleteButton: {
|
|
...config.rightPanel?.deleteButton!,
|
|
buttonVariant: value,
|
|
enabled: config.rightPanel?.deleteButton?.enabled ?? true,
|
|
},
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="ghost">Ghost (기본)</SelectItem>
|
|
<SelectItem value="outline">Outline</SelectItem>
|
|
<SelectItem value="default">Default</SelectItem>
|
|
<SelectItem value="destructive">Destructive (빨강)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 삭제 확인 메시지 */}
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">삭제 확인 메시지</Label>
|
|
<Input
|
|
value={config.rightPanel?.deleteButton?.confirmMessage || ""}
|
|
placeholder="정말 삭제하시겠습니까?"
|
|
onChange={(e) => {
|
|
updateRightPanel({
|
|
deleteButton: {
|
|
...config.rightPanel?.deleteButton!,
|
|
confirmMessage: e.target.value || undefined,
|
|
enabled: config.rightPanel?.deleteButton?.enabled ?? true,
|
|
},
|
|
});
|
|
}}
|
|
className="h-8 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* ======================================== */}
|
|
{/* 추가 탭 설정 (우측 패널과 동일한 구조) */}
|
|
{/* ======================================== */}
|
|
<div className="mt-4 space-y-4 border-t pt-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-sm font-semibold">추가 탭</h3>
|
|
<p className="text-muted-foreground text-xs">
|
|
우측 패널에 다른 테이블 데이터를 탭으로 추가합니다
|
|
</p>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
const newTab: AdditionalTabConfig = {
|
|
tabId: `tab_${Date.now()}`,
|
|
label: `탭 ${(config.rightPanel?.additionalTabs?.length || 0) + 1}`,
|
|
title: "",
|
|
tableName: "",
|
|
displayMode: "list",
|
|
showSearch: false,
|
|
showAdd: false,
|
|
showEdit: true,
|
|
showDelete: true,
|
|
summaryColumnCount: 3,
|
|
summaryShowLabel: true,
|
|
};
|
|
updateRightPanel({
|
|
additionalTabs: [...(config.rightPanel?.additionalTabs || []), newTab],
|
|
});
|
|
}}
|
|
>
|
|
<Plus className="mr-1 h-3 w-3" />
|
|
탭 추가
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 추가된 탭 목록 */}
|
|
{(config.rightPanel?.additionalTabs?.length || 0) > 0 ? (
|
|
<Accordion type="multiple" className="space-y-2">
|
|
{config.rightPanel?.additionalTabs?.map((tab, tabIndex) => (
|
|
<AdditionalTabConfigPanel
|
|
key={tab.tabId}
|
|
tab={tab}
|
|
tabIndex={tabIndex}
|
|
config={config}
|
|
updateRightPanel={updateRightPanel}
|
|
availableRightTables={availableRightTables}
|
|
leftTableColumns={leftTableColumns}
|
|
menuObjid={menuObjid}
|
|
loadedTableColumns={loadedTableColumns}
|
|
loadTableColumns={loadTableColumns}
|
|
loadingColumns={loadingColumns}
|
|
/>
|
|
))}
|
|
</Accordion>
|
|
) : (
|
|
<div className="rounded-lg border border-dashed p-4 text-center">
|
|
<p className="text-xs text-gray-500">
|
|
추가된 탭이 없습니다. [탭 추가] 버튼을 클릭하여 새 탭을 추가하세요.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 레이아웃 설정 */}
|
|
<div className="mt-4 space-y-4 border-t pt-4">
|
|
<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>
|
|
);
|
|
};
|
|
|