Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management
This commit is contained in:
@@ -11,10 +11,11 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
// Accordion 제거 - 단순 섹션으로 변경
|
||||
import { Check, ChevronsUpDown, ArrowRight, Plus, X, ArrowUp, ArrowDown } from "lucide-react";
|
||||
import { Check, ChevronsUpDown, ArrowRight, Plus, X, ArrowUp, ArrowDown, Trash2, GripVertical } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SplitPanelLayoutConfig } from "./types";
|
||||
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";
|
||||
|
||||
@@ -189,6 +190,848 @@ const ScreenSelector: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 추가 탭 설정 패널 (우측 패널과 동일한 구조)
|
||||
*/
|
||||
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 설정 패널
|
||||
*/
|
||||
@@ -2854,6 +3697,72 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
)}
|
||||
</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">
|
||||
|
||||
Reference in New Issue
Block a user