1331 lines
49 KiB
TypeScript
1331 lines
49 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useCallback } from "react";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Plus, X, GripVertical, Check, ChevronsUpDown, Table, Layers } from "lucide-react";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import {
|
|
RepeatScreenModalProps,
|
|
CardRowConfig,
|
|
CardColumnConfig,
|
|
ColumnSourceConfig,
|
|
ColumnTargetConfig,
|
|
DataSourceConfig,
|
|
GroupingConfig,
|
|
AggregationConfig,
|
|
TableLayoutConfig,
|
|
TableColumnConfig,
|
|
} from "./types";
|
|
import { tableManagementApi } from "@/lib/api/tableManagement";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface RepeatScreenModalConfigPanelProps {
|
|
config: Partial<RepeatScreenModalProps>;
|
|
onChange: (config: Partial<RepeatScreenModalProps>) => void;
|
|
}
|
|
|
|
// 검색 가능한 컬럼 선택기 (Combobox) - 240px 최적화
|
|
function SourceColumnSelector({
|
|
sourceTable,
|
|
value,
|
|
onChange,
|
|
placeholder = "컬럼 선택",
|
|
}: {
|
|
sourceTable: string;
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
placeholder?: string;
|
|
showTableName?: boolean;
|
|
}) {
|
|
const [columns, setColumns] = useState<{ columnName: string; displayName?: string }[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [open, setOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const loadColumns = async () => {
|
|
if (!sourceTable) {
|
|
setColumns([]);
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
try {
|
|
const response = await tableManagementApi.getColumnList(sourceTable);
|
|
if (response.success && response.data) {
|
|
setColumns(response.data.columns);
|
|
}
|
|
} catch (error) {
|
|
console.error("컬럼 로드 실패:", error);
|
|
setColumns([]);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
loadColumns();
|
|
}, [sourceTable]);
|
|
|
|
const selectedColumn = columns.find((col) => col.columnName === value);
|
|
const displayText = selectedColumn ? selectedColumn.columnName : placeholder;
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={open}
|
|
className="h-6 w-full justify-between text-[10px]"
|
|
disabled={!sourceTable || isLoading}
|
|
>
|
|
<span className="truncate">{isLoading ? "..." : displayText}</span>
|
|
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0 max-h-[200px] w-[200px]" align="start">
|
|
<Command>
|
|
<CommandInput placeholder="검색..." className="text-[10px] h-7" />
|
|
<CommandList className="max-h-[160px] overflow-y-auto">
|
|
<CommandEmpty className="text-[10px] py-2">없음</CommandEmpty>
|
|
<CommandGroup>
|
|
{columns.map((col) => (
|
|
<CommandItem
|
|
key={col.columnName}
|
|
value={`${col.columnName} ${col.displayName || ""}`}
|
|
onSelect={() => {
|
|
onChange(col.columnName);
|
|
setOpen(false);
|
|
}}
|
|
className="text-[10px] py-1"
|
|
>
|
|
<Check
|
|
className={cn("mr-1 h-3 w-3", value === col.columnName ? "opacity-100" : "opacity-0")}
|
|
/>
|
|
<span className="truncate">{col.columnName}</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
}
|
|
|
|
// 테이블 선택기 (Combobox) - 240px 최적화
|
|
function TableSelector({ value, onChange }: { value: string; onChange: (value: string) => void }) {
|
|
const [tables, setTables] = useState<{ tableName: string; displayName?: string }[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [open, setOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const loadTables = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const response = await tableManagementApi.getTableList();
|
|
if (response.success && response.data) {
|
|
setTables(response.data.tables || []);
|
|
}
|
|
} catch (error) {
|
|
console.error("테이블 로드 실패:", error);
|
|
setTables([]);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
loadTables();
|
|
}, []);
|
|
|
|
const selectedTable = (tables || []).find((t) => t.tableName === value);
|
|
const displayText = selectedTable ? selectedTable.tableName : "테이블 선택";
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={open}
|
|
className="h-7 w-full justify-between text-[10px]"
|
|
disabled={isLoading}
|
|
>
|
|
<span className="truncate">{isLoading ? "..." : displayText}</span>
|
|
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0 max-h-[200px] w-[200px]" align="start">
|
|
<Command>
|
|
<CommandInput placeholder="검색..." className="text-[10px] h-7" />
|
|
<CommandList className="max-h-[160px] overflow-y-auto">
|
|
<CommandEmpty className="text-[10px] py-2">없음</CommandEmpty>
|
|
<CommandGroup>
|
|
{tables.map((table) => (
|
|
<CommandItem
|
|
key={table.tableName}
|
|
value={`${table.tableName} ${table.displayName || ""}`}
|
|
onSelect={() => {
|
|
onChange(table.tableName);
|
|
setOpen(false);
|
|
}}
|
|
className="text-[10px] py-1"
|
|
>
|
|
<Check
|
|
className={cn("mr-1 h-3 w-3", value === table.tableName ? "opacity-100" : "opacity-0")}
|
|
/>
|
|
<span className="truncate">{table.tableName}</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
}
|
|
|
|
export function RepeatScreenModalConfigPanel({ config, onChange }: RepeatScreenModalConfigPanelProps) {
|
|
const [localConfig, setLocalConfig] = useState<Partial<RepeatScreenModalProps>>({
|
|
cardLayout: [],
|
|
dataSource: { sourceTable: "" },
|
|
saveMode: "all",
|
|
cardSpacing: "24px",
|
|
showCardBorder: true,
|
|
cardTitle: "카드 {index}",
|
|
cardMode: "simple",
|
|
grouping: { enabled: false, groupByField: "", aggregations: [] },
|
|
tableLayout: { headerRows: [], tableColumns: [] },
|
|
...config,
|
|
});
|
|
|
|
const [allTables, setAllTables] = useState<{ tableName: string; displayName?: string }[]>([]);
|
|
|
|
// 테이블 목록 로드
|
|
useEffect(() => {
|
|
const loadTables = async () => {
|
|
try {
|
|
const response = await tableManagementApi.getTableList();
|
|
if (response.success && response.data) {
|
|
setAllTables(response.data.tables || []);
|
|
}
|
|
} catch (error) {
|
|
console.error("테이블 로드 실패:", error);
|
|
}
|
|
};
|
|
loadTables();
|
|
}, []);
|
|
|
|
// Debounced update for input fields
|
|
const updateConfigDebounced = useCallback(
|
|
(updates: Partial<RepeatScreenModalProps>) => {
|
|
const timeoutId = setTimeout(() => {
|
|
setLocalConfig((prev) => {
|
|
const newConfig = { ...prev, ...updates };
|
|
onChange(newConfig);
|
|
return newConfig;
|
|
});
|
|
}, 500);
|
|
|
|
return () => clearTimeout(timeoutId);
|
|
},
|
|
[onChange]
|
|
);
|
|
|
|
// Immediate update for select/switch fields
|
|
const updateConfig = (updates: Partial<RepeatScreenModalProps>) => {
|
|
setLocalConfig((prev) => {
|
|
const newConfig = { ...prev, ...updates };
|
|
onChange(newConfig);
|
|
return newConfig;
|
|
});
|
|
};
|
|
|
|
// === 그룹핑 관련 함수 ===
|
|
const updateGrouping = (updates: Partial<GroupingConfig>) => {
|
|
updateConfig({
|
|
grouping: {
|
|
...localConfig.grouping,
|
|
enabled: localConfig.grouping?.enabled ?? false,
|
|
groupByField: localConfig.grouping?.groupByField ?? "",
|
|
aggregations: localConfig.grouping?.aggregations ?? [],
|
|
...updates,
|
|
},
|
|
});
|
|
};
|
|
|
|
const addAggregation = () => {
|
|
const newAgg: AggregationConfig = {
|
|
sourceField: "",
|
|
type: "sum",
|
|
resultField: `agg_${Date.now()}`,
|
|
label: "",
|
|
};
|
|
updateGrouping({
|
|
aggregations: [...(localConfig.grouping?.aggregations || []), newAgg],
|
|
});
|
|
};
|
|
|
|
const removeAggregation = (index: number) => {
|
|
const newAggs = [...(localConfig.grouping?.aggregations || [])];
|
|
newAggs.splice(index, 1);
|
|
updateGrouping({ aggregations: newAggs });
|
|
};
|
|
|
|
const updateAggregation = (index: number, updates: Partial<AggregationConfig>) => {
|
|
const newAggs = [...(localConfig.grouping?.aggregations || [])];
|
|
newAggs[index] = { ...newAggs[index], ...updates };
|
|
updateGrouping({ aggregations: newAggs });
|
|
};
|
|
|
|
// === 테이블 레이아웃 관련 함수 ===
|
|
const updateTableLayout = (updates: Partial<TableLayoutConfig>) => {
|
|
updateConfig({
|
|
tableLayout: {
|
|
...localConfig.tableLayout,
|
|
headerRows: localConfig.tableLayout?.headerRows ?? [],
|
|
tableColumns: localConfig.tableLayout?.tableColumns ?? [],
|
|
...updates,
|
|
},
|
|
});
|
|
};
|
|
|
|
const addTableColumn = () => {
|
|
const newCol: TableColumnConfig = {
|
|
id: `tcol-${Date.now()}`,
|
|
field: "",
|
|
label: "",
|
|
type: "text",
|
|
width: "auto",
|
|
editable: false,
|
|
};
|
|
updateTableLayout({
|
|
tableColumns: [...(localConfig.tableLayout?.tableColumns || []), newCol],
|
|
});
|
|
};
|
|
|
|
const removeTableColumn = (index: number) => {
|
|
const newCols = [...(localConfig.tableLayout?.tableColumns || [])];
|
|
newCols.splice(index, 1);
|
|
updateTableLayout({ tableColumns: newCols });
|
|
};
|
|
|
|
const updateTableColumn = (index: number, updates: Partial<TableColumnConfig>) => {
|
|
const newCols = [...(localConfig.tableLayout?.tableColumns || [])];
|
|
newCols[index] = { ...newCols[index], ...updates };
|
|
updateTableLayout({ tableColumns: newCols });
|
|
};
|
|
|
|
// === 헤더 행 관련 함수 (simple 모드와 동일) ===
|
|
const addHeaderRow = () => {
|
|
const newRow: CardRowConfig = {
|
|
id: `hrow-${Date.now()}`,
|
|
columns: [],
|
|
gap: "16px",
|
|
layout: "horizontal",
|
|
};
|
|
updateTableLayout({
|
|
headerRows: [...(localConfig.tableLayout?.headerRows || []), newRow],
|
|
});
|
|
};
|
|
|
|
const removeHeaderRow = (rowIndex: number) => {
|
|
const newRows = [...(localConfig.tableLayout?.headerRows || [])];
|
|
newRows.splice(rowIndex, 1);
|
|
updateTableLayout({ headerRows: newRows });
|
|
};
|
|
|
|
const updateHeaderRow = (rowIndex: number, updates: Partial<CardRowConfig>) => {
|
|
const newRows = [...(localConfig.tableLayout?.headerRows || [])];
|
|
newRows[rowIndex] = { ...newRows[rowIndex], ...updates };
|
|
updateTableLayout({ headerRows: newRows });
|
|
};
|
|
|
|
const addHeaderColumn = (rowIndex: number) => {
|
|
const newRows = [...(localConfig.tableLayout?.headerRows || [])];
|
|
const newColumn: CardColumnConfig = {
|
|
id: `hcol-${Date.now()}`,
|
|
field: "",
|
|
label: "",
|
|
type: "text",
|
|
width: "auto",
|
|
editable: false,
|
|
};
|
|
newRows[rowIndex].columns.push(newColumn);
|
|
updateTableLayout({ headerRows: newRows });
|
|
};
|
|
|
|
const removeHeaderColumn = (rowIndex: number, colIndex: number) => {
|
|
const newRows = [...(localConfig.tableLayout?.headerRows || [])];
|
|
newRows[rowIndex].columns.splice(colIndex, 1);
|
|
updateTableLayout({ headerRows: newRows });
|
|
};
|
|
|
|
const updateHeaderColumn = (rowIndex: number, colIndex: number, updates: Partial<CardColumnConfig>) => {
|
|
const newRows = [...(localConfig.tableLayout?.headerRows || [])];
|
|
newRows[rowIndex].columns[colIndex] = { ...newRows[rowIndex].columns[colIndex], ...updates };
|
|
updateTableLayout({ headerRows: newRows });
|
|
};
|
|
|
|
// === Simple 모드 행/컬럼 관련 함수 ===
|
|
const addRow = () => {
|
|
const newRow: CardRowConfig = {
|
|
id: `row-${Date.now()}`,
|
|
columns: [],
|
|
gap: "16px",
|
|
layout: "horizontal",
|
|
};
|
|
updateConfig({
|
|
cardLayout: [...(localConfig.cardLayout || []), newRow],
|
|
});
|
|
};
|
|
|
|
const removeRow = (rowIndex: number) => {
|
|
const newLayout = [...(localConfig.cardLayout || [])];
|
|
newLayout.splice(rowIndex, 1);
|
|
updateConfig({ cardLayout: newLayout });
|
|
};
|
|
|
|
const updateRow = (rowIndex: number, updates: Partial<CardRowConfig>) => {
|
|
const newLayout = [...(localConfig.cardLayout || [])];
|
|
newLayout[rowIndex] = { ...newLayout[rowIndex], ...updates };
|
|
updateConfig({ cardLayout: newLayout });
|
|
};
|
|
|
|
const addColumn = (rowIndex: number) => {
|
|
const newLayout = [...(localConfig.cardLayout || [])];
|
|
const newColumn: CardColumnConfig = {
|
|
id: `col-${Date.now()}`,
|
|
field: "",
|
|
label: "",
|
|
type: "text",
|
|
width: "auto",
|
|
editable: true,
|
|
required: false,
|
|
};
|
|
newLayout[rowIndex].columns.push(newColumn);
|
|
updateConfig({ cardLayout: newLayout });
|
|
};
|
|
|
|
const removeColumn = (rowIndex: number, colIndex: number) => {
|
|
const newLayout = [...(localConfig.cardLayout || [])];
|
|
newLayout[rowIndex].columns.splice(colIndex, 1);
|
|
updateConfig({ cardLayout: newLayout });
|
|
};
|
|
|
|
const updateColumn = (rowIndex: number, colIndex: number, updates: Partial<CardColumnConfig>) => {
|
|
const newLayout = [...(localConfig.cardLayout || [])];
|
|
newLayout[rowIndex].columns[colIndex] = {
|
|
...newLayout[rowIndex].columns[colIndex],
|
|
...updates,
|
|
};
|
|
updateConfig({ cardLayout: newLayout });
|
|
};
|
|
|
|
return (
|
|
<ScrollArea className="h-[calc(100vh-200px)]">
|
|
<div className="space-y-4 p-2">
|
|
<Tabs defaultValue="basic" className="w-full">
|
|
<TabsList className="grid w-full grid-cols-4 h-auto p-1">
|
|
<TabsTrigger value="basic" className="text-[9px] px-1 py-1">
|
|
기본
|
|
</TabsTrigger>
|
|
<TabsTrigger value="data" className="text-[9px] px-1 py-1">
|
|
소스
|
|
</TabsTrigger>
|
|
<TabsTrigger value="grouping" className="text-[9px] px-1 py-1">
|
|
그룹
|
|
</TabsTrigger>
|
|
<TabsTrigger value="layout" className="text-[9px] px-1 py-1">
|
|
레이아웃
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* === 기본 설정 탭 === */}
|
|
<TabsContent value="basic" className="space-y-3 mt-3">
|
|
<div className="space-y-3 border rounded-lg p-3 bg-card">
|
|
<h3 className="text-xs font-semibold">카드 설정</h3>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label className="text-[10px]">카드 제목</Label>
|
|
<Input
|
|
value={localConfig.cardTitle}
|
|
onChange={(e) => {
|
|
setLocalConfig((prev) => ({ ...prev, cardTitle: e.target.value }));
|
|
updateConfigDebounced({ cardTitle: e.target.value });
|
|
}}
|
|
placeholder="{part_code} - {part_name}"
|
|
className="h-7 text-[10px]"
|
|
/>
|
|
<p className="text-[9px] text-muted-foreground">{"{field}"}: 필드값 사용</p>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label className="text-[10px]">카드 간격</Label>
|
|
<Select value={localConfig.cardSpacing} onValueChange={(value) => updateConfig({ cardSpacing: value })}>
|
|
<SelectTrigger className="h-7 text-[10px] w-full">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="8px">8px</SelectItem>
|
|
<SelectItem value="16px">16px</SelectItem>
|
|
<SelectItem value="24px">24px</SelectItem>
|
|
<SelectItem value="32px">32px</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-[10px]">테두리</Label>
|
|
<Switch
|
|
checked={localConfig.showCardBorder}
|
|
onCheckedChange={(checked) => updateConfig({ showCardBorder: checked })}
|
|
className="scale-75"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label className="text-[10px]">저장 모드</Label>
|
|
<Select
|
|
value={localConfig.saveMode}
|
|
onValueChange={(value) => updateConfig({ saveMode: value as "all" | "individual" })}
|
|
>
|
|
<SelectTrigger className="h-7 text-[10px] w-full">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">전체 저장</SelectItem>
|
|
<SelectItem value="individual">개별 저장</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 카드 모드 선택 */}
|
|
<div className="space-y-1.5 pt-2 border-t">
|
|
<Label className="text-[10px] font-semibold">카드 모드</Label>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant={localConfig.cardMode === "simple" ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={() => updateConfig({ cardMode: "simple" })}
|
|
className="flex-1 h-8 text-[9px]"
|
|
>
|
|
<Layers className="h-3 w-3 mr-1" />
|
|
단순
|
|
</Button>
|
|
<Button
|
|
variant={localConfig.cardMode === "withTable" ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={() => updateConfig({ cardMode: "withTable" })}
|
|
className="flex-1 h-8 text-[9px]"
|
|
>
|
|
<Table className="h-3 w-3 mr-1" />
|
|
테이블
|
|
</Button>
|
|
</div>
|
|
<p className="text-[9px] text-muted-foreground">
|
|
{localConfig.cardMode === "simple"
|
|
? "1행 = 1카드"
|
|
: "그룹핑된 행들을 테이블로 표시"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* === 데이터 소스 탭 === */}
|
|
<TabsContent value="data" className="space-y-3 mt-3">
|
|
<div className="space-y-3 border rounded-lg p-3 bg-card">
|
|
<h3 className="text-xs font-semibold">데이터 소스</h3>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label className="text-[10px]">소스 테이블</Label>
|
|
<TableSelector
|
|
value={localConfig.dataSource?.sourceTable || ""}
|
|
onChange={(value) =>
|
|
updateConfig({
|
|
dataSource: {
|
|
...localConfig.dataSource,
|
|
sourceTable: value,
|
|
},
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label className="text-[10px]">필터 필드</Label>
|
|
<Input
|
|
value={localConfig.dataSource?.filterField || ""}
|
|
onChange={(e) => {
|
|
setLocalConfig((prev) => ({
|
|
...prev,
|
|
dataSource: {
|
|
...prev.dataSource,
|
|
sourceTable: prev.dataSource?.sourceTable || "",
|
|
filterField: e.target.value,
|
|
},
|
|
}));
|
|
updateConfigDebounced({
|
|
dataSource: {
|
|
...localConfig.dataSource,
|
|
sourceTable: localConfig.dataSource?.sourceTable || "",
|
|
filterField: e.target.value,
|
|
},
|
|
});
|
|
}}
|
|
placeholder="selectedIds"
|
|
className="h-7 text-[10px]"
|
|
/>
|
|
<p className="text-[9px] text-muted-foreground">formData에서 가져올 필드</p>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* === 그룹핑 설정 탭 === */}
|
|
<TabsContent value="grouping" className="space-y-3 mt-3">
|
|
<div className="space-y-3 border rounded-lg p-3 bg-card">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-xs font-semibold">그룹핑</h3>
|
|
<Switch
|
|
checked={localConfig.grouping?.enabled}
|
|
onCheckedChange={(checked) => updateGrouping({ enabled: checked })}
|
|
className="scale-75"
|
|
/>
|
|
</div>
|
|
|
|
{localConfig.grouping?.enabled && (
|
|
<>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-[10px]">그룹 기준 필드</Label>
|
|
<SourceColumnSelector
|
|
sourceTable={localConfig.dataSource?.sourceTable || ""}
|
|
value={localConfig.grouping?.groupByField || ""}
|
|
onChange={(value) => updateGrouping({ groupByField: value })}
|
|
placeholder="예: part_code"
|
|
/>
|
|
<p className="text-[9px] text-muted-foreground">같은 값을 가진 행들을 하나의 카드로 묶음</p>
|
|
</div>
|
|
|
|
{/* 집계 설정 */}
|
|
<div className="space-y-2 pt-2 border-t">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-[10px] font-semibold">집계 설정</Label>
|
|
<Button size="sm" variant="outline" onClick={addAggregation} className="h-5 text-[9px] px-1">
|
|
<Plus className="h-2 w-2 mr-0.5" />
|
|
추가
|
|
</Button>
|
|
</div>
|
|
|
|
{(localConfig.grouping?.aggregations || []).map((agg, index) => (
|
|
<div key={agg.resultField || index} className="border rounded p-2 space-y-1.5 bg-background">
|
|
<div className="flex items-center justify-between">
|
|
<Badge variant="secondary" className="text-[9px] px-1 py-0">
|
|
집계 {index + 1}
|
|
</Badge>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => removeAggregation(index)}
|
|
className="h-5 w-5 p-0"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<Label className="text-[9px]">원본 필드</Label>
|
|
<SourceColumnSelector
|
|
sourceTable={localConfig.dataSource?.sourceTable || ""}
|
|
value={agg.sourceField}
|
|
onChange={(value) => updateAggregation(index, { sourceField: value })}
|
|
placeholder="합계할 필드"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<div className="flex-1 space-y-1 min-w-0">
|
|
<Label className="text-[9px]">집계 타입</Label>
|
|
<Select
|
|
value={agg.type}
|
|
onValueChange={(value) =>
|
|
updateAggregation(index, { type: value as AggregationConfig["type"] })
|
|
}
|
|
>
|
|
<SelectTrigger className="h-6 text-[10px] w-full">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="sum">합계</SelectItem>
|
|
<SelectItem value="count">개수</SelectItem>
|
|
<SelectItem value="avg">평균</SelectItem>
|
|
<SelectItem value="min">최소</SelectItem>
|
|
<SelectItem value="max">최대</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex-1 space-y-1 min-w-0">
|
|
<Label className="text-[9px]">라벨</Label>
|
|
<Input
|
|
value={agg.label}
|
|
onChange={(e) => updateAggregation(index, { label: e.target.value })}
|
|
placeholder="총수주잔량"
|
|
className="h-6 text-[10px]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<Label className="text-[9px]">결과 필드명</Label>
|
|
<Input
|
|
value={agg.resultField}
|
|
onChange={(e) => updateAggregation(index, { resultField: e.target.value })}
|
|
placeholder="total_balance_qty"
|
|
className="h-6 text-[10px]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{(localConfig.grouping?.aggregations || []).length === 0 && (
|
|
<p className="text-[9px] text-muted-foreground text-center py-2">
|
|
집계 설정이 없습니다
|
|
</p>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* === 레이아웃 설정 탭 === */}
|
|
<TabsContent value="layout" className="space-y-3 mt-3">
|
|
{/* Simple 모드 */}
|
|
{localConfig.cardMode === "simple" && (
|
|
<>
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-xs font-semibold">카드 레이아웃</h3>
|
|
<Button size="sm" variant="outline" onClick={addRow} className="h-6 text-[10px] px-2">
|
|
<Plus className="h-3 w-3 mr-1" />
|
|
행 추가
|
|
</Button>
|
|
</div>
|
|
|
|
{localConfig.cardLayout && localConfig.cardLayout.length > 0 ? (
|
|
<div className="space-y-3">
|
|
{localConfig.cardLayout.map((row, rowIndex) => (
|
|
<RowConfigSection
|
|
key={row.id || `row-${rowIndex}`}
|
|
row={row}
|
|
rowIndex={rowIndex}
|
|
allTables={allTables}
|
|
dataSourceTable={localConfig.dataSource?.sourceTable || ""}
|
|
aggregations={localConfig.grouping?.aggregations || []}
|
|
onUpdateRow={(updates) => updateRow(rowIndex, updates)}
|
|
onRemoveRow={() => removeRow(rowIndex)}
|
|
onAddColumn={() => addColumn(rowIndex)}
|
|
onRemoveColumn={(colIndex) => removeColumn(rowIndex, colIndex)}
|
|
onUpdateColumn={(colIndex, updates) => updateColumn(rowIndex, colIndex, updates)}
|
|
/>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-6 border border-dashed rounded-lg">
|
|
<p className="text-[10px] text-muted-foreground mb-2">행이 없습니다</p>
|
|
<Button size="sm" onClick={addRow} className="h-6 text-[10px]">
|
|
<Plus className="h-3 w-3 mr-1" />
|
|
첫 행 추가
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* WithTable 모드 */}
|
|
{localConfig.cardMode === "withTable" && (
|
|
<div className="space-y-4">
|
|
{/* 헤더 영역 */}
|
|
<div className="space-y-2 border rounded-lg p-3 bg-purple-50 dark:bg-purple-950">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-xs font-semibold text-purple-700 dark:text-purple-300">헤더 영역</h3>
|
|
<Button size="sm" variant="outline" onClick={addHeaderRow} className="h-5 text-[9px] px-1">
|
|
<Plus className="h-2 w-2 mr-0.5" />
|
|
행 추가
|
|
</Button>
|
|
</div>
|
|
<p className="text-[9px] text-muted-foreground">그룹 대표값, 집계값 표시</p>
|
|
|
|
{(localConfig.tableLayout?.headerRows || []).map((row, rowIndex) => (
|
|
<RowConfigSection
|
|
key={row.id || `hrow-${rowIndex}`}
|
|
row={row}
|
|
rowIndex={rowIndex}
|
|
allTables={allTables}
|
|
dataSourceTable={localConfig.dataSource?.sourceTable || ""}
|
|
aggregations={localConfig.grouping?.aggregations || []}
|
|
onUpdateRow={(updates) => updateHeaderRow(rowIndex, updates)}
|
|
onRemoveRow={() => removeHeaderRow(rowIndex)}
|
|
onAddColumn={() => addHeaderColumn(rowIndex)}
|
|
onRemoveColumn={(colIndex) => removeHeaderColumn(rowIndex, colIndex)}
|
|
onUpdateColumn={(colIndex, updates) => updateHeaderColumn(rowIndex, colIndex, updates)}
|
|
isHeader
|
|
/>
|
|
))}
|
|
|
|
{(localConfig.tableLayout?.headerRows || []).length === 0 && (
|
|
<p className="text-[9px] text-center text-muted-foreground py-2">헤더 행이 없습니다</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* 테이블 영역 */}
|
|
<div className="space-y-2 border rounded-lg p-3 bg-blue-50 dark:bg-blue-950">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-xs font-semibold text-blue-700 dark:text-blue-300">테이블 영역</h3>
|
|
<Button size="sm" variant="outline" onClick={addTableColumn} className="h-5 text-[9px] px-1">
|
|
<Plus className="h-2 w-2 mr-0.5" />
|
|
컬럼 추가
|
|
</Button>
|
|
</div>
|
|
<p className="text-[9px] text-muted-foreground">그룹 내 각 행을 테이블로 표시</p>
|
|
|
|
{(localConfig.tableLayout?.tableColumns || []).map((col, colIndex) => (
|
|
<TableColumnConfigSection
|
|
key={col.id || `tcol-${colIndex}`}
|
|
col={col}
|
|
colIndex={colIndex}
|
|
allTables={allTables}
|
|
dataSourceTable={localConfig.dataSource?.sourceTable || ""}
|
|
onUpdate={(updates) => updateTableColumn(colIndex, updates)}
|
|
onRemove={() => removeTableColumn(colIndex)}
|
|
/>
|
|
))}
|
|
|
|
{(localConfig.tableLayout?.tableColumns || []).length === 0 && (
|
|
<p className="text-[9px] text-center text-muted-foreground py-2">테이블 컬럼이 없습니다</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
</ScrollArea>
|
|
);
|
|
}
|
|
|
|
// === 행 설정 섹션 ===
|
|
function RowConfigSection({
|
|
row,
|
|
rowIndex,
|
|
allTables,
|
|
dataSourceTable,
|
|
aggregations,
|
|
onUpdateRow,
|
|
onRemoveRow,
|
|
onAddColumn,
|
|
onRemoveColumn,
|
|
onUpdateColumn,
|
|
isHeader = false,
|
|
}: {
|
|
row: CardRowConfig;
|
|
rowIndex: number;
|
|
allTables: { tableName: string; displayName?: string }[];
|
|
dataSourceTable: string;
|
|
aggregations: AggregationConfig[];
|
|
onUpdateRow: (updates: Partial<CardRowConfig>) => void;
|
|
onRemoveRow: () => void;
|
|
onAddColumn: () => void;
|
|
onRemoveColumn: (colIndex: number) => void;
|
|
onUpdateColumn: (colIndex: number, updates: Partial<CardColumnConfig>) => void;
|
|
isHeader?: boolean;
|
|
}) {
|
|
return (
|
|
<div className="border rounded-lg p-2 space-y-2 bg-background">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-1">
|
|
<GripVertical className="h-3 w-3 text-muted-foreground" />
|
|
<Badge variant="outline" className="text-[9px] px-1 py-0">
|
|
행 {rowIndex + 1}
|
|
</Badge>
|
|
<span className="text-[9px] text-muted-foreground">({row.columns.length}개)</span>
|
|
</div>
|
|
<Button size="sm" variant="ghost" onClick={onRemoveRow} className="h-5 w-5 p-0">
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 행 설정 */}
|
|
<div className="flex gap-2">
|
|
<div className="flex-1 space-y-1 min-w-0">
|
|
<Label className="text-[9px]">방향</Label>
|
|
<Select
|
|
value={row.layout || "horizontal"}
|
|
onValueChange={(value) => onUpdateRow({ layout: value as "horizontal" | "vertical" })}
|
|
>
|
|
<SelectTrigger className="h-6 text-[10px] w-full">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="horizontal">가로</SelectItem>
|
|
<SelectItem value="vertical">세로</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{isHeader && (
|
|
<div className="flex-1 space-y-1 min-w-0">
|
|
<Label className="text-[9px]">배경색</Label>
|
|
<Select
|
|
value={row.backgroundColor || "none"}
|
|
onValueChange={(value) => onUpdateRow({ backgroundColor: value === "none" ? undefined : value })}
|
|
>
|
|
<SelectTrigger className="h-6 text-[10px] w-full">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="none">없음</SelectItem>
|
|
<SelectItem value="blue">파랑</SelectItem>
|
|
<SelectItem value="green">초록</SelectItem>
|
|
<SelectItem value="purple">보라</SelectItem>
|
|
<SelectItem value="orange">주황</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 컬럼 목록 */}
|
|
<div className="space-y-2 pl-2 border-l-2 border-primary/30">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-[9px] font-semibold">컬럼</Label>
|
|
<Button size="sm" variant="outline" onClick={onAddColumn} className="h-5 text-[9px] px-1">
|
|
<Plus className="h-2 w-2 mr-0.5" />
|
|
추가
|
|
</Button>
|
|
</div>
|
|
|
|
{row.columns.map((col, colIndex) => (
|
|
<ColumnConfigSection
|
|
key={col.id || `col-${colIndex}`}
|
|
col={col}
|
|
colIndex={colIndex}
|
|
allTables={allTables}
|
|
dataSourceTable={dataSourceTable}
|
|
aggregations={aggregations}
|
|
onUpdate={(updates) => onUpdateColumn(colIndex, updates)}
|
|
onRemove={() => onRemoveColumn(colIndex)}
|
|
/>
|
|
))}
|
|
|
|
{row.columns.length === 0 && (
|
|
<div className="text-center py-2 text-[9px] text-muted-foreground border border-dashed rounded">
|
|
컬럼 추가
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// === 컬럼 설정 섹션 ===
|
|
function ColumnConfigSection({
|
|
col,
|
|
colIndex,
|
|
allTables,
|
|
dataSourceTable,
|
|
aggregations,
|
|
onUpdate,
|
|
onRemove,
|
|
}: {
|
|
col: CardColumnConfig;
|
|
colIndex: number;
|
|
allTables: { tableName: string; displayName?: string }[];
|
|
dataSourceTable: string;
|
|
aggregations: AggregationConfig[];
|
|
onUpdate: (updates: Partial<CardColumnConfig>) => void;
|
|
onRemove: () => void;
|
|
}) {
|
|
return (
|
|
<div className="border rounded p-2 space-y-2 bg-background">
|
|
<div className="flex items-center justify-between">
|
|
<Badge variant="secondary" className="text-[9px] px-1 py-0">
|
|
컬럼 {colIndex + 1}
|
|
</Badge>
|
|
<Button size="sm" variant="ghost" onClick={onRemove} className="h-5 w-5 p-0">
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 기본 설정 */}
|
|
<div className="space-y-1.5">
|
|
<div className="space-y-1">
|
|
<Label className="text-[9px]">필드명</Label>
|
|
<Input
|
|
value={col.field}
|
|
onChange={(e) => onUpdate({ field: e.target.value })}
|
|
placeholder="field_name"
|
|
className="h-6 text-[10px]"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<Label className="text-[9px]">라벨</Label>
|
|
<Input
|
|
value={col.label}
|
|
onChange={(e) => onUpdate({ label: e.target.value })}
|
|
placeholder="표시명"
|
|
className="h-6 text-[10px]"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<div className="flex-1 space-y-1 min-w-0">
|
|
<Label className="text-[9px]">타입</Label>
|
|
<Select value={col.type} onValueChange={(value) => onUpdate({ type: value as any })}>
|
|
<SelectTrigger className="h-6 text-[10px] w-full">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="text">텍스트</SelectItem>
|
|
<SelectItem value="number">숫자</SelectItem>
|
|
<SelectItem value="date">날짜</SelectItem>
|
|
<SelectItem value="select">선택</SelectItem>
|
|
<SelectItem value="aggregation">집계값</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex-1 space-y-1 min-w-0">
|
|
<Label className="text-[9px]">너비</Label>
|
|
<Select value={col.width || "auto"} onValueChange={(value) => onUpdate({ width: value })}>
|
|
<SelectTrigger className="h-6 text-[10px] w-full">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="auto">자동</SelectItem>
|
|
<SelectItem value="50%">50%</SelectItem>
|
|
<SelectItem value="100%">100%</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 집계값 타입일 때 */}
|
|
{col.type === "aggregation" && aggregations.length > 0 && (
|
|
<div className="space-y-1">
|
|
<Label className="text-[9px]">집계 필드</Label>
|
|
<Select
|
|
value={col.aggregationField || ""}
|
|
onValueChange={(value) => onUpdate({ aggregationField: value })}
|
|
>
|
|
<SelectTrigger className="h-6 text-[10px] w-full">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{aggregations.map((agg) => (
|
|
<SelectItem key={agg.resultField} value={agg.resultField}>
|
|
{agg.label || agg.resultField}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 편집/필수 설정 */}
|
|
<div className="flex items-center justify-between pt-1 border-t">
|
|
<div className="flex items-center gap-1">
|
|
<Switch
|
|
checked={col.editable}
|
|
onCheckedChange={(checked) => onUpdate({ editable: checked })}
|
|
className="scale-[0.6]"
|
|
/>
|
|
<Label className="text-[9px]">편집</Label>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Switch
|
|
checked={col.required}
|
|
onCheckedChange={(checked) => onUpdate({ required: checked })}
|
|
className="scale-[0.6]"
|
|
/>
|
|
<Label className="text-[9px]">필수</Label>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 데이터 소스 설정 */}
|
|
{col.type !== "aggregation" && (
|
|
<div className="space-y-1.5 p-1.5 bg-blue-50 dark:bg-blue-950 rounded border border-blue-200 dark:border-blue-800">
|
|
<Label className="text-[9px] font-semibold text-blue-600">소스</Label>
|
|
<Select
|
|
value={col.sourceConfig?.type || "manual"}
|
|
onValueChange={(value) =>
|
|
onUpdate({
|
|
sourceConfig: {
|
|
...col.sourceConfig,
|
|
type: value as "direct" | "join" | "manual",
|
|
},
|
|
})
|
|
}
|
|
>
|
|
<SelectTrigger className="h-6 text-[10px] w-full">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="direct">직접</SelectItem>
|
|
<SelectItem value="join">조인</SelectItem>
|
|
<SelectItem value="manual">수동</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
{col.sourceConfig?.type === "direct" && (
|
|
<SourceColumnSelector
|
|
sourceTable={dataSourceTable}
|
|
value={col.sourceConfig.sourceColumn || ""}
|
|
onChange={(value) =>
|
|
onUpdate({
|
|
sourceConfig: {
|
|
...col.sourceConfig,
|
|
type: "direct",
|
|
sourceColumn: value,
|
|
} as ColumnSourceConfig,
|
|
})
|
|
}
|
|
/>
|
|
)}
|
|
|
|
{col.sourceConfig?.type === "join" && (
|
|
<div className="space-y-1">
|
|
<Select
|
|
value={col.sourceConfig.joinTable || ""}
|
|
onValueChange={(value) =>
|
|
onUpdate({
|
|
sourceConfig: {
|
|
...col.sourceConfig,
|
|
type: "join",
|
|
joinTable: value,
|
|
},
|
|
})
|
|
}
|
|
>
|
|
<SelectTrigger className="h-6 text-[10px] w-full">
|
|
<SelectValue placeholder="조인 테이블" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{(allTables || []).map((table) => (
|
|
<SelectItem key={table.tableName} value={table.tableName}>
|
|
{table.tableName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<SourceColumnSelector
|
|
sourceTable={dataSourceTable}
|
|
value={col.sourceConfig.joinKey || ""}
|
|
onChange={(value) =>
|
|
onUpdate({
|
|
sourceConfig: {
|
|
...col.sourceConfig,
|
|
type: "join",
|
|
joinKey: value,
|
|
} as ColumnSourceConfig,
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 데이터 타겟 설정 */}
|
|
<div className="space-y-1.5 p-1.5 bg-green-50 dark:bg-green-950 rounded border border-green-200 dark:border-green-800">
|
|
<Label className="text-[9px] font-semibold text-green-600">저장</Label>
|
|
|
|
<Select
|
|
value={col.targetConfig?.targetTable || ""}
|
|
onValueChange={(value) =>
|
|
onUpdate({
|
|
targetConfig: {
|
|
...col.targetConfig,
|
|
targetTable: value,
|
|
targetColumn: col.targetConfig?.targetColumn || "",
|
|
saveEnabled: true,
|
|
},
|
|
})
|
|
}
|
|
>
|
|
<SelectTrigger className="h-6 text-[10px] w-full">
|
|
<SelectValue placeholder="테이블" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{(allTables || []).map((table) => (
|
|
<SelectItem key={table.tableName} value={table.tableName}>
|
|
{table.tableName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
{col.targetConfig?.targetTable && (
|
|
<SourceColumnSelector
|
|
sourceTable={col.targetConfig.targetTable}
|
|
value={col.targetConfig.targetColumn || ""}
|
|
onChange={(value) =>
|
|
onUpdate({
|
|
targetConfig: {
|
|
...col.targetConfig,
|
|
targetColumn: value,
|
|
} as ColumnTargetConfig,
|
|
})
|
|
}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// === 테이블 컬럼 설정 섹션 ===
|
|
function TableColumnConfigSection({
|
|
col,
|
|
colIndex,
|
|
allTables,
|
|
dataSourceTable,
|
|
onUpdate,
|
|
onRemove,
|
|
}: {
|
|
col: TableColumnConfig;
|
|
colIndex: number;
|
|
allTables: { tableName: string; displayName?: string }[];
|
|
dataSourceTable: string;
|
|
onUpdate: (updates: Partial<TableColumnConfig>) => void;
|
|
onRemove: () => void;
|
|
}) {
|
|
return (
|
|
<div className="border rounded p-2 space-y-2 bg-background">
|
|
<div className="flex items-center justify-between">
|
|
<Badge variant="secondary" className="text-[9px] px-1 py-0">
|
|
컬럼 {colIndex + 1}
|
|
</Badge>
|
|
<Button size="sm" variant="ghost" onClick={onRemove} className="h-5 w-5 p-0">
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<div className="space-y-1">
|
|
<Label className="text-[9px]">필드명</Label>
|
|
<SourceColumnSelector
|
|
sourceTable={dataSourceTable}
|
|
value={col.field}
|
|
onChange={(value) => onUpdate({ field: value })}
|
|
placeholder="필드 선택"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<Label className="text-[9px]">헤더 라벨</Label>
|
|
<Input
|
|
value={col.label}
|
|
onChange={(e) => onUpdate({ label: e.target.value })}
|
|
placeholder="표시명"
|
|
className="h-6 text-[10px]"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<div className="flex-1 space-y-1 min-w-0">
|
|
<Label className="text-[9px]">타입</Label>
|
|
<Select value={col.type} onValueChange={(value) => onUpdate({ type: value as any })}>
|
|
<SelectTrigger className="h-6 text-[10px] w-full">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="text">텍스트</SelectItem>
|
|
<SelectItem value="number">숫자</SelectItem>
|
|
<SelectItem value="date">날짜</SelectItem>
|
|
<SelectItem value="badge">배지</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex-1 space-y-1 min-w-0">
|
|
<Label className="text-[9px]">너비</Label>
|
|
<Input
|
|
value={col.width || ""}
|
|
onChange={(e) => onUpdate({ width: e.target.value })}
|
|
placeholder="100px"
|
|
className="h-6 text-[10px]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 편집 설정 */}
|
|
<div className="flex items-center justify-between pt-1 border-t">
|
|
<div className="flex items-center gap-1">
|
|
<Switch
|
|
checked={col.editable}
|
|
onCheckedChange={(checked) => onUpdate({ editable: checked })}
|
|
className="scale-[0.6]"
|
|
/>
|
|
<Label className="text-[9px]">편집 가능</Label>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 편집 가능할 때 저장 설정 */}
|
|
{col.editable && (
|
|
<div className="space-y-1.5 p-1.5 bg-green-50 dark:bg-green-950 rounded border border-green-200 dark:border-green-800">
|
|
<Label className="text-[9px] font-semibold text-green-600">저장</Label>
|
|
|
|
<Select
|
|
value={col.targetConfig?.targetTable || ""}
|
|
onValueChange={(value) =>
|
|
onUpdate({
|
|
targetConfig: {
|
|
...col.targetConfig,
|
|
targetTable: value,
|
|
targetColumn: col.targetConfig?.targetColumn || "",
|
|
saveEnabled: true,
|
|
},
|
|
})
|
|
}
|
|
>
|
|
<SelectTrigger className="h-6 text-[10px] w-full">
|
|
<SelectValue placeholder="테이블" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{(allTables || []).map((table) => (
|
|
<SelectItem key={table.tableName} value={table.tableName}>
|
|
{table.tableName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
{col.targetConfig?.targetTable && (
|
|
<SourceColumnSelector
|
|
sourceTable={col.targetConfig.targetTable}
|
|
value={col.targetConfig.targetColumn || ""}
|
|
onChange={(value) =>
|
|
onUpdate({
|
|
targetConfig: {
|
|
...col.targetConfig,
|
|
targetColumn: value,
|
|
} as ColumnTargetConfig,
|
|
})
|
|
}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|