- Enhanced the `ScreenManagementService` to include updates for V2 layouts in the `screen_layouts_v2` table. - Implemented logic to remap `screenId`, `targetScreenId`, `modalScreenId`, and other related IDs in layout data. - Added logging for the number of layouts updated in both V1 and V2, improving traceability of the update process. - This update ensures that screen references are correctly maintained across different layout versions, enhancing the overall functionality of the screen management system.
802 lines
37 KiB
TypeScript
802 lines
37 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
import { Check, ChevronsUpDown, ChevronRight, Link2, Move, Trash2 } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { SplitPanelLayoutConfig } from "../types";
|
|
import { PanelInlineComponent } from "../types";
|
|
import { ColumnInfo, TableInfo } from "@/types/screen";
|
|
import { DataFilterConfigPanel } from "@/components/screen/config-panels/DataFilterConfigPanel";
|
|
import { DndContext, closestCenter, type DragEndEvent } from "@dnd-kit/core";
|
|
import { SortableContext, verticalListSortingStrategy, arrayMove } from "@dnd-kit/sortable";
|
|
import { SortableColumnRow, ScreenSelector, GroupByColumnsSelector } from "./SharedComponents";
|
|
|
|
export interface RightPanelConfigTabProps {
|
|
config: SplitPanelLayoutConfig;
|
|
onChange: (config: SplitPanelLayoutConfig) => void;
|
|
updateRightPanel: (updates: Partial<SplitPanelLayoutConfig["rightPanel"]>) => void;
|
|
relationshipType: "join" | "detail";
|
|
localTitles: { left: string; right: string };
|
|
setLocalTitles: (fn: (prev: { left: string; right: string }) => { left: string; right: string }) => void;
|
|
setIsUserEditing: (v: boolean) => void;
|
|
rightTableOpen: boolean;
|
|
setRightTableOpen: (open: boolean) => void;
|
|
availableRightTables: TableInfo[];
|
|
rightTableColumns: ColumnInfo[];
|
|
entityJoinColumns: Record<string, {
|
|
availableColumns: Array<{ tableName: string; columnName: string; columnLabel: string; dataType: string; joinAlias: string; suggestedLabel: string }>;
|
|
joinTables: Array<{ tableName: string; currentDisplayColumn: string; joinConfig?: any; availableColumns: Array<{ columnName: string; columnLabel: string; dataType: string; inputType?: string; description?: string }> }>;
|
|
}>;
|
|
menuObjid?: number;
|
|
renderAdditionalTabs?: () => React.ReactNode;
|
|
}
|
|
|
|
export const RightPanelConfigTab: React.FC<RightPanelConfigTabProps> = ({
|
|
config,
|
|
onChange,
|
|
updateRightPanel,
|
|
relationshipType,
|
|
localTitles,
|
|
setLocalTitles,
|
|
setIsUserEditing,
|
|
rightTableOpen,
|
|
setRightTableOpen,
|
|
availableRightTables,
|
|
rightTableColumns,
|
|
entityJoinColumns,
|
|
menuObjid,
|
|
renderAdditionalTabs,
|
|
}) => {
|
|
const dbNumericTypes = ["numeric", "decimal", "integer", "bigint", "double precision", "real", "smallint", "int4", "int8", "float4", "float8"];
|
|
const inputNumericTypes = ["number", "decimal", "currency", "integer"];
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="space-y-4 rounded-lg border border-border/50 bg-muted/40 p-4">
|
|
<h3 className="border-l-2 border-l-primary/40 pl-2 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>우측 패널 테이블</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) => {
|
|
const tableName = (table as any).tableName || (table as any).table_name;
|
|
return (
|
|
<CommandItem
|
|
key={tableName}
|
|
value={`${(table as any).displayName || ""} ${tableName}`}
|
|
onSelect={() => {
|
|
updateRightPanel({ tableName });
|
|
setRightTableOpen(false);
|
|
}}
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
config.rightPanel?.tableName === tableName ? "opacity-100" : "opacity-0",
|
|
)}
|
|
/>
|
|
{(table as any).displayName || tableName}
|
|
{(table as any).displayName && <span className="ml-2 text-xs text-gray-500">({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" | "custom") => updateRightPanel({ displayMode: value })}
|
|
>
|
|
<SelectTrigger className="h-10 bg-white">
|
|
<SelectValue placeholder="표시 모드 선택">
|
|
{(config.rightPanel?.displayMode || "list") === "list" ? "목록 (LIST)" : (config.rightPanel?.displayMode || "list") === "table" ? "테이블 (TABLE)" : "커스텀 (CUSTOM)"}
|
|
</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="list">
|
|
<div className="flex flex-col py-1">
|
|
<span className="text-sm font-medium">목록 (LIST)</span>
|
|
<span className="text-xs text-gray-500">클릭 가능한 항목 목록 (기본)</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="table">
|
|
<div className="flex flex-col py-1">
|
|
<span className="text-sm font-medium">테이블 (TABLE)</span>
|
|
<span className="text-xs text-gray-500">컬럼 헤더가 있는 테이블 형식</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="custom">
|
|
<div className="flex flex-col py-1">
|
|
<span className="text-sm font-medium">커스텀 (CUSTOM)</span>
|
|
<span className="text-xs text-gray-500">패널 안에 컴포넌트 자유 배치</span>
|
|
</div>
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
{config.rightPanel?.displayMode === "custom" && (
|
|
<p className="text-xs text-amber-600">
|
|
화면 디자이너에서 우측 패널에 컴포넌트를 드래그하여 배치하세요.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{config.rightPanel?.displayMode === "custom" && (
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium">배치된 컴포넌트</Label>
|
|
{!config.rightPanel?.components || config.rightPanel.components.length === 0 ? (
|
|
<div className="rounded-md border border-dashed bg-muted/30 p-4 text-center">
|
|
<Move className="mx-auto mb-2 h-6 w-6 text-muted-foreground" />
|
|
<p className="text-muted-foreground text-xs">
|
|
디자인 화면에서 컴포넌트를 드래그하여 추가하세요
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{config.rightPanel.components.map((comp: PanelInlineComponent) => (
|
|
<div
|
|
key={comp.id}
|
|
className="flex items-center justify-between rounded-md border bg-background p-2"
|
|
>
|
|
<div className="flex-1">
|
|
<p className="text-xs font-medium">{comp.label || comp.componentType}</p>
|
|
<p className="text-muted-foreground text-[10px]">
|
|
{comp.componentType} | 위치: ({comp.position?.x || 0}, {comp.position?.y || 0}) | 크기: {comp.size?.width || 0}x{comp.size?.height || 0}
|
|
</p>
|
|
</div>
|
|
<Button
|
|
onClick={() => {
|
|
const updatedComponents = (config.rightPanel?.components || []).filter(
|
|
(c: PanelInlineComponent) => c.id !== comp.id
|
|
);
|
|
onChange({
|
|
...config,
|
|
rightPanel: {
|
|
...config.rightPanel,
|
|
components: updatedComponents,
|
|
},
|
|
});
|
|
}}
|
|
size="sm"
|
|
variant="ghost"
|
|
className="h-6 w-6 p-0 text-destructive hover:text-destructive"
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{(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>
|
|
)}
|
|
|
|
{config.rightPanel?.displayMode !== "custom" && (() => {
|
|
const selectedColumns = config.rightPanel?.columns || [];
|
|
const filteredTableColumns = rightTableColumns.filter((c) => !["company_code", "company_name"].includes(c.columnName));
|
|
const unselectedColumns = filteredTableColumns.filter((c) => !selectedColumns.some((sc) => sc.name === c.columnName));
|
|
|
|
const handleRightDragEnd = (event: DragEndEvent) => {
|
|
const { active, over } = event;
|
|
if (over && active.id !== over.id) {
|
|
const oldIndex = selectedColumns.findIndex((c) => c.name === active.id);
|
|
const newIndex = selectedColumns.findIndex((c) => c.name === over.id);
|
|
if (oldIndex !== -1 && newIndex !== -1) {
|
|
updateRightPanel({ columns: arrayMove([...selectedColumns], oldIndex, newIndex) });
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium">표시할 컬럼 ({selectedColumns.length}개 선택)</Label>
|
|
<div className="max-h-[400px] overflow-y-auto rounded-md border p-2">
|
|
{rightTableColumns.length === 0 ? (
|
|
<p className="text-muted-foreground py-2 text-center text-xs">테이블을 선택해주세요</p>
|
|
) : (
|
|
<>
|
|
{selectedColumns.length > 0 && (
|
|
<DndContext collisionDetection={closestCenter} onDragEnd={handleRightDragEnd}>
|
|
<SortableContext items={selectedColumns.map((c) => c.name)} strategy={verticalListSortingStrategy}>
|
|
<div className="space-y-1">
|
|
{selectedColumns.map((col, index) => {
|
|
const colInfo = rightTableColumns.find((c) => c.columnName === col.name);
|
|
const isNumeric = colInfo && (
|
|
dbNumericTypes.includes(colInfo.dataType?.toLowerCase() || "") ||
|
|
inputNumericTypes.includes(colInfo.input_type?.toLowerCase() || "") ||
|
|
inputNumericTypes.includes(colInfo.webType?.toLowerCase() || "")
|
|
);
|
|
return (
|
|
<SortableColumnRow
|
|
key={col.name}
|
|
id={col.name}
|
|
col={col}
|
|
index={index}
|
|
isNumeric={!!isNumeric}
|
|
isEntityJoin={!!(col as any).isEntityJoin}
|
|
onLabelChange={(value) => {
|
|
const newColumns = [...selectedColumns];
|
|
newColumns[index] = { ...newColumns[index], label: value };
|
|
updateRightPanel({ columns: newColumns });
|
|
}}
|
|
onWidthChange={(value) => {
|
|
const newColumns = [...selectedColumns];
|
|
newColumns[index] = { ...newColumns[index], width: value };
|
|
updateRightPanel({ columns: newColumns });
|
|
}}
|
|
onFormatChange={(checked) => {
|
|
const newColumns = [...selectedColumns];
|
|
newColumns[index] = { ...newColumns[index], format: { ...newColumns[index].format, type: "number", thousandSeparator: checked } };
|
|
updateRightPanel({ columns: newColumns });
|
|
}}
|
|
onRemove={() => updateRightPanel({ columns: selectedColumns.filter((_, i) => i !== index) })}
|
|
onShowInSummaryChange={(checked) => {
|
|
const newColumns = [...selectedColumns];
|
|
newColumns[index] = { ...newColumns[index], showInSummary: checked };
|
|
updateRightPanel({ columns: newColumns });
|
|
}}
|
|
onShowInDetailChange={(checked) => {
|
|
const newColumns = [...selectedColumns];
|
|
newColumns[index] = { ...newColumns[index], showInDetail: checked };
|
|
updateRightPanel({ columns: newColumns });
|
|
}}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
</SortableContext>
|
|
</DndContext>
|
|
)}
|
|
|
|
{selectedColumns.length > 0 && unselectedColumns.length > 0 && (
|
|
<div className="border-border/60 my-2 flex items-center gap-2 border-t pt-2">
|
|
<span className="text-muted-foreground text-[10px]">미선택 컬럼</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-0.5">
|
|
{unselectedColumns.map((column) => (
|
|
<div
|
|
key={column.columnName}
|
|
className="hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1.5"
|
|
onClick={() => {
|
|
updateRightPanel({ columns: [...selectedColumns, { name: column.columnName, label: column.columnLabel || column.columnName, width: 100 }] });
|
|
}}
|
|
>
|
|
<Checkbox checked={false} className="pointer-events-none h-3.5 w-3.5 shrink-0" />
|
|
<span className="text-muted-foreground truncate text-xs">{column.columnLabel || column.columnName}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{(() => {
|
|
const rightTable = config.rightPanel?.tableName;
|
|
const joinData = rightTable ? entityJoinColumns[rightTable] : null;
|
|
if (!joinData || joinData.joinTables.length === 0) return null;
|
|
|
|
return joinData.joinTables.map((joinTable, tableIndex) => {
|
|
const joinColumnsToShow = joinTable.availableColumns.filter((column) => {
|
|
const matchingJoinColumn = joinData.availableColumns.find(
|
|
(jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
|
|
);
|
|
if (!matchingJoinColumn) return false;
|
|
return !selectedColumns.some((c) => c.name === matchingJoinColumn.joinAlias);
|
|
});
|
|
const addedCount = joinTable.availableColumns.length - joinColumnsToShow.length;
|
|
|
|
if (joinColumnsToShow.length === 0 && addedCount === 0) return null;
|
|
|
|
return (
|
|
<details key={`join-${tableIndex}`} className="group">
|
|
<summary className="border-border/60 my-2 flex cursor-pointer list-none items-center gap-2 border-t pt-2 select-none">
|
|
<ChevronRight className="h-3 w-3 shrink-0 text-blue-500 transition-transform group-open:rotate-90" />
|
|
<Link2 className="h-3 w-3 shrink-0 text-blue-500" />
|
|
<span className="text-[10px] font-medium text-blue-600">{joinTable.tableName}</span>
|
|
{addedCount > 0 && (
|
|
<span className="rounded-full bg-blue-100 px-1.5 text-[9px] font-medium text-blue-600">{addedCount}개 선택</span>
|
|
)}
|
|
<span className="text-[9px] text-gray-400">{joinColumnsToShow.length}개 남음</span>
|
|
</summary>
|
|
<div className="space-y-0.5 pt-1">
|
|
{joinColumnsToShow.map((column, colIndex) => {
|
|
const matchingJoinColumn = joinData.availableColumns.find(
|
|
(jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
|
|
);
|
|
if (!matchingJoinColumn) return null;
|
|
|
|
return (
|
|
<div
|
|
key={colIndex}
|
|
className="flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 hover:bg-blue-50/60"
|
|
onClick={() => {
|
|
updateRightPanel({
|
|
columns: [...selectedColumns, {
|
|
name: matchingJoinColumn.joinAlias,
|
|
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
|
|
width: 100,
|
|
isEntityJoin: true,
|
|
joinInfo: {
|
|
sourceTable: rightTable!,
|
|
sourceColumn: (joinTable as any).joinConfig?.sourceColumn || "",
|
|
referenceTable: matchingJoinColumn.tableName,
|
|
joinAlias: matchingJoinColumn.joinAlias,
|
|
},
|
|
}],
|
|
});
|
|
}}
|
|
>
|
|
<Checkbox checked={false} className="pointer-events-none h-3.5 w-3.5 shrink-0" />
|
|
<Link2 className="h-3 w-3 shrink-0 text-blue-500" />
|
|
<span className="truncate text-xs text-blue-700">{column.columnLabel || column.columnName}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
{joinColumnsToShow.length === 0 && (
|
|
<p className="px-2 py-1 text-[10px] text-gray-400">모든 컬럼이 이미 추가되었습니다</p>
|
|
)}
|
|
</div>
|
|
</details>
|
|
);
|
|
});
|
|
})()}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})()}
|
|
</div>
|
|
|
|
<div className="space-y-4 rounded-lg border border-border/50 bg-muted/40 p-4">
|
|
<h3 className="border-l-2 border-l-primary/40 pl-2 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}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-4 rounded-lg border border-border/50 bg-muted/40 p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="border-l-2 border-l-primary/40 pl-2 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>
|
|
</div>
|
|
|
|
{(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.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.columnLabel || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 수정 버튼 설정 */}
|
|
{config.rightPanel?.showEdit && (
|
|
<div className="space-y-3 rounded-lg border border-border/50 bg-muted/40 p-4">
|
|
<h3 className="border-l-2 border-l-primary/40 pl-2 text-sm font-semibold">수정 버튼 설정</h3>
|
|
<div className="space-y-3">
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">수정 모드</Label>
|
|
<Select
|
|
value={config.rightPanel?.editButton?.mode || "auto"}
|
|
onValueChange={(value: "auto" | "modal") =>
|
|
updateRightPanel({
|
|
editButton: {
|
|
...config.rightPanel?.editButton,
|
|
enabled: config.rightPanel?.editButton?.enabled ?? true,
|
|
mode: value,
|
|
},
|
|
})
|
|
}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="auto">자동 (인라인)</SelectItem>
|
|
<SelectItem value="modal">모달 화면</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{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",
|
|
},
|
|
});
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{config.rightPanel?.editButton?.mode === "modal" && (
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">수정 모달 화면</Label>
|
|
<ScreenSelector
|
|
value={config.rightPanel?.editButton?.modalScreenId}
|
|
onChange={(screenId) =>
|
|
updateRightPanel({
|
|
editButton: {
|
|
...config.rightPanel?.editButton!,
|
|
modalScreenId: screenId,
|
|
},
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">버튼 라벨</Label>
|
|
<Input
|
|
value={config.rightPanel?.editButton?.buttonLabel || ""}
|
|
onChange={(e) =>
|
|
updateRightPanel({
|
|
editButton: {
|
|
...config.rightPanel?.editButton!,
|
|
buttonLabel: e.target.value || undefined,
|
|
enabled: config.rightPanel?.editButton?.enabled ?? true,
|
|
mode: config.rightPanel?.editButton?.mode || "auto",
|
|
},
|
|
})
|
|
}
|
|
placeholder="수정"
|
|
className="h-8 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 추가 버튼 설정 */}
|
|
{config.rightPanel?.showAdd && (
|
|
<div className="space-y-3 rounded-lg border border-border/50 bg-muted/40 p-4">
|
|
<h3 className="border-l-2 border-l-primary/40 pl-2 text-sm font-semibold">추가 버튼 설정</h3>
|
|
<div className="space-y-3">
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">추가 모드</Label>
|
|
<Select
|
|
value={config.rightPanel?.addButton?.mode || "auto"}
|
|
onValueChange={(value: "auto" | "modal") =>
|
|
updateRightPanel({
|
|
addButton: {
|
|
...config.rightPanel?.addButton,
|
|
mode: value,
|
|
enabled: true,
|
|
},
|
|
})
|
|
}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="auto">자동 (내장 폼)</SelectItem>
|
|
<SelectItem value="modal">모달 화면</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{config.rightPanel?.addButton?.mode === "modal" && (
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">모달 화면</Label>
|
|
<ScreenSelector
|
|
value={config.rightPanel?.addButton?.modalScreenId}
|
|
onChange={(screenId) =>
|
|
updateRightPanel({
|
|
addButton: {
|
|
...config.rightPanel?.addButton!,
|
|
modalScreenId: screenId,
|
|
},
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">버튼 라벨</Label>
|
|
<Input
|
|
value={config.rightPanel?.addButton?.buttonLabel || "추가"}
|
|
onChange={(e) =>
|
|
updateRightPanel({
|
|
addButton: {
|
|
...config.rightPanel?.addButton!,
|
|
buttonLabel: e.target.value,
|
|
enabled: true,
|
|
mode: config.rightPanel?.addButton?.mode || "auto",
|
|
},
|
|
})
|
|
}
|
|
placeholder="추가"
|
|
className="h-8 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 삭제 버튼 설정 */}
|
|
<div className="space-y-3 rounded-lg border border-border/50 bg-muted/40 p-4">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="border-l-2 border-l-primary/40 pl-2 text-sm font-semibold">삭제 버튼 설정</h3>
|
|
<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>
|
|
|
|
{/* 추가 탭 */}
|
|
{renderAdditionalTabs && (
|
|
<div className="space-y-4 rounded-lg border border-border/50 bg-muted/40 p-4">
|
|
<h3 className="border-l-2 border-l-primary/40 pl-2 text-sm font-semibold">추가 탭</h3>
|
|
<p className="text-muted-foreground text-xs">
|
|
우측 패널에 다른 테이블 데이터를 탭으로 추가합니다
|
|
</p>
|
|
{renderAdditionalTabs()}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|