559 lines
22 KiB
TypeScript
559 lines
22 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Check, ChevronsUpDown } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
import { tableManagementApi, getTableColumns } from "@/lib/api/tableManagement";
|
|
import type { RelatedDataButtonsConfig } from "./types";
|
|
|
|
interface TableInfo {
|
|
tableName: string;
|
|
displayName?: string;
|
|
}
|
|
|
|
interface ColumnInfo {
|
|
columnName: string;
|
|
columnLabel?: string;
|
|
}
|
|
|
|
interface RelatedDataButtonsConfigPanelProps {
|
|
config: RelatedDataButtonsConfig;
|
|
onChange: (config: RelatedDataButtonsConfig) => void;
|
|
tables?: TableInfo[];
|
|
}
|
|
|
|
export const RelatedDataButtonsConfigPanel: React.FC<RelatedDataButtonsConfigPanelProps> = ({
|
|
config,
|
|
onChange,
|
|
tables: propTables = [],
|
|
}) => {
|
|
const [allTables, setAllTables] = useState<TableInfo[]>([]);
|
|
const [sourceTableColumns, setSourceTableColumns] = useState<ColumnInfo[]>([]);
|
|
const [buttonTableColumns, setButtonTableColumns] = useState<ColumnInfo[]>([]);
|
|
|
|
// Popover 상태
|
|
const [sourceTableOpen, setSourceTableOpen] = useState(false);
|
|
const [buttonTableOpen, setButtonTableOpen] = useState(false);
|
|
|
|
// 전체 테이블 로드
|
|
useEffect(() => {
|
|
const loadTables = async () => {
|
|
try {
|
|
const response = await tableManagementApi.getTableList();
|
|
if (response.success && response.data) {
|
|
setAllTables(response.data.map((t: any) => ({
|
|
tableName: t.tableName || t.table_name,
|
|
displayName: t.tableLabel || t.table_label || t.displayName,
|
|
})));
|
|
}
|
|
} catch (error) {
|
|
console.error("테이블 목록 로드 실패:", error);
|
|
}
|
|
};
|
|
loadTables();
|
|
}, []);
|
|
|
|
// 소스 테이블 컬럼 로드
|
|
useEffect(() => {
|
|
const loadColumns = async () => {
|
|
if (!config.sourceMapping?.sourceTable) {
|
|
setSourceTableColumns([]);
|
|
return;
|
|
}
|
|
try {
|
|
const response = await getTableColumns(config.sourceMapping.sourceTable);
|
|
if (response.success && response.data?.columns) {
|
|
setSourceTableColumns(response.data.columns.map((c: any) => ({
|
|
columnName: c.columnName || c.column_name,
|
|
columnLabel: c.columnLabel || c.column_label || c.displayName,
|
|
})));
|
|
}
|
|
} catch (error) {
|
|
console.error("소스 테이블 컬럼 로드 실패:", error);
|
|
}
|
|
};
|
|
loadColumns();
|
|
}, [config.sourceMapping?.sourceTable]);
|
|
|
|
// 버튼 테이블 컬럼 로드
|
|
useEffect(() => {
|
|
const loadColumns = async () => {
|
|
if (!config.buttonDataSource?.tableName) {
|
|
setButtonTableColumns([]);
|
|
return;
|
|
}
|
|
try {
|
|
const response = await getTableColumns(config.buttonDataSource.tableName);
|
|
if (response.success && response.data?.columns) {
|
|
setButtonTableColumns(response.data.columns.map((c: any) => ({
|
|
columnName: c.columnName || c.column_name,
|
|
columnLabel: c.columnLabel || c.column_label || c.displayName,
|
|
})));
|
|
}
|
|
} catch (error) {
|
|
console.error("버튼 테이블 컬럼 로드 실패:", error);
|
|
}
|
|
};
|
|
loadColumns();
|
|
}, [config.buttonDataSource?.tableName]);
|
|
|
|
// 설정 업데이트 헬퍼
|
|
const updateConfig = useCallback((updates: Partial<RelatedDataButtonsConfig>) => {
|
|
onChange({ ...config, ...updates });
|
|
}, [config, onChange]);
|
|
|
|
const updateSourceMapping = useCallback((updates: Partial<RelatedDataButtonsConfig["sourceMapping"]>) => {
|
|
onChange({
|
|
...config,
|
|
sourceMapping: { ...config.sourceMapping, ...updates },
|
|
});
|
|
}, [config, onChange]);
|
|
|
|
const updateHeaderDisplay = useCallback((updates: Partial<NonNullable<RelatedDataButtonsConfig["headerDisplay"]>>) => {
|
|
onChange({
|
|
...config,
|
|
headerDisplay: { ...config.headerDisplay, ...updates } as any,
|
|
});
|
|
}, [config, onChange]);
|
|
|
|
const updateButtonDataSource = useCallback((updates: Partial<RelatedDataButtonsConfig["buttonDataSource"]>) => {
|
|
onChange({
|
|
...config,
|
|
buttonDataSource: { ...config.buttonDataSource, ...updates },
|
|
});
|
|
}, [config, onChange]);
|
|
|
|
const updateButtonStyle = useCallback((updates: Partial<NonNullable<RelatedDataButtonsConfig["buttonStyle"]>>) => {
|
|
onChange({
|
|
...config,
|
|
buttonStyle: { ...config.buttonStyle, ...updates },
|
|
});
|
|
}, [config, onChange]);
|
|
|
|
const updateAddButton = useCallback((updates: Partial<NonNullable<RelatedDataButtonsConfig["addButton"]>>) => {
|
|
onChange({
|
|
...config,
|
|
addButton: { ...config.addButton, ...updates },
|
|
});
|
|
}, [config, onChange]);
|
|
|
|
const updateEvents = useCallback((updates: Partial<NonNullable<RelatedDataButtonsConfig["events"]>>) => {
|
|
onChange({
|
|
...config,
|
|
events: { ...config.events, ...updates },
|
|
});
|
|
}, [config, onChange]);
|
|
|
|
const tables = allTables.length > 0 ? allTables : propTables;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* 소스 매핑 (좌측 패널 연결) */}
|
|
<div className="space-y-3">
|
|
<Label className="text-sm font-semibold">소스 테이블 (좌측 패널)</Label>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-xs">테이블</Label>
|
|
<Popover open={sourceTableOpen} onOpenChange={setSourceTableOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button variant="outline" role="combobox" className="w-full justify-between">
|
|
{config.sourceMapping?.sourceTable || "테이블 선택"}
|
|
<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">
|
|
{tables.map((table) => (
|
|
<CommandItem
|
|
key={table.tableName}
|
|
value={`${table.displayName || ""} ${table.tableName}`}
|
|
onSelect={() => {
|
|
updateSourceMapping({ sourceTable: table.tableName });
|
|
setSourceTableOpen(false);
|
|
}}
|
|
>
|
|
<Check className={cn("mr-2 h-4 w-4", config.sourceMapping?.sourceTable === table.tableName ? "opacity-100" : "opacity-0")} />
|
|
{table.displayName || table.tableName}
|
|
{table.displayName && <span className="ml-2 text-xs text-gray-500">({table.tableName})</span>}
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-xs">필터 컬럼 (버튼 테이블 조회 시 사용)</Label>
|
|
<Select
|
|
value={config.sourceMapping?.sourceColumn || ""}
|
|
onValueChange={(value) => updateSourceMapping({ sourceColumn: value })}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{sourceTableColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.columnLabel || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 헤더 표시 설정 */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-sm font-semibold">헤더 표시</Label>
|
|
<Switch
|
|
checked={config.headerDisplay?.show !== false}
|
|
onCheckedChange={(checked) => updateHeaderDisplay({ show: checked })}
|
|
/>
|
|
</div>
|
|
|
|
{config.headerDisplay?.show !== false && (
|
|
<div className="space-y-2 pl-2 border-l-2 border-gray-200">
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">제목 컬럼</Label>
|
|
<Select
|
|
value={config.headerDisplay?.titleColumn || ""}
|
|
onValueChange={(value) => updateHeaderDisplay({ titleColumn: value })}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="제목 컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{sourceTableColumns.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-xs">부제목 컬럼 (선택)</Label>
|
|
<Select
|
|
value={config.headerDisplay?.subtitleColumn || "__none__"}
|
|
onValueChange={(value) => updateHeaderDisplay({ subtitleColumn: value === "__none__" ? "" : value })}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="부제목 컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="__none__">없음</SelectItem>
|
|
{sourceTableColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.columnLabel || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 버튼 데이터 소스 */}
|
|
<div className="space-y-3">
|
|
<Label className="text-sm font-semibold">버튼 데이터 소스</Label>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-xs">테이블</Label>
|
|
<Popover open={buttonTableOpen} onOpenChange={setButtonTableOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button variant="outline" role="combobox" className="w-full justify-between">
|
|
{config.buttonDataSource?.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">
|
|
{tables.map((table) => (
|
|
<CommandItem
|
|
key={table.tableName}
|
|
value={`${table.displayName || ""} ${table.tableName}`}
|
|
onSelect={() => {
|
|
updateButtonDataSource({ tableName: table.tableName });
|
|
setButtonTableOpen(false);
|
|
}}
|
|
>
|
|
<Check className={cn("mr-2 h-4 w-4", config.buttonDataSource?.tableName === table.tableName ? "opacity-100" : "opacity-0")} />
|
|
{table.displayName || table.tableName}
|
|
{table.displayName && <span className="ml-2 text-xs text-gray-500">({table.tableName})</span>}
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">필터 컬럼</Label>
|
|
<Select
|
|
value={config.buttonDataSource?.filterColumn || ""}
|
|
onValueChange={(value) => updateButtonDataSource({ filterColumn: value })}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{buttonTableColumns.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-xs">표시 컬럼</Label>
|
|
<Select
|
|
value={config.buttonDataSource?.displayColumn || ""}
|
|
onValueChange={(value) => updateButtonDataSource({ displayColumn: value })}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{buttonTableColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.columnLabel || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 버튼 스타일 */}
|
|
<div className="space-y-3">
|
|
<Label className="text-sm font-semibold">버튼 스타일</Label>
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">기본 스타일</Label>
|
|
<Select
|
|
value={config.buttonStyle?.variant || "outline"}
|
|
onValueChange={(value: any) => updateButtonStyle({ variant: value })}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="default">Default</SelectItem>
|
|
<SelectItem value="outline">Outline</SelectItem>
|
|
<SelectItem value="secondary">Secondary</SelectItem>
|
|
<SelectItem value="ghost">Ghost</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">선택 시 스타일</Label>
|
|
<Select
|
|
value={config.buttonStyle?.activeVariant || "default"}
|
|
onValueChange={(value: any) => updateButtonStyle({ activeVariant: value })}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="default">Default</SelectItem>
|
|
<SelectItem value="outline">Outline</SelectItem>
|
|
<SelectItem value="secondary">Secondary</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 기본 표시 설정 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs">기본 버전 표시 컬럼</Label>
|
|
<Select
|
|
value={config.buttonStyle?.defaultIndicator?.column || "__none__"}
|
|
onValueChange={(value) => updateButtonStyle({
|
|
defaultIndicator: {
|
|
...config.buttonStyle?.defaultIndicator,
|
|
column: value === "__none__" ? "" : value,
|
|
showStar: config.buttonStyle?.defaultIndicator?.showStar ?? true,
|
|
},
|
|
})}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="없음" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="__none__">없음</SelectItem>
|
|
{buttonTableColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.columnLabel || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{config.buttonStyle?.defaultIndicator?.column && (
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex items-center gap-2">
|
|
<Switch
|
|
checked={config.buttonStyle?.defaultIndicator?.showStar ?? true}
|
|
onCheckedChange={(checked) => updateButtonStyle({
|
|
defaultIndicator: {
|
|
...config.buttonStyle?.defaultIndicator,
|
|
column: config.buttonStyle?.defaultIndicator?.column || "",
|
|
showStar: checked,
|
|
},
|
|
})}
|
|
/>
|
|
<Label className="text-xs">별표 표시</Label>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 이벤트 설정 (하위 테이블 연동) */}
|
|
<div className="space-y-3">
|
|
<Label className="text-sm font-semibold">하위 테이블 연동</Label>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-xs">대상 테이블</Label>
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button variant="outline" role="combobox" className="w-full justify-between">
|
|
{config.events?.targetTable || "테이블 선택"}
|
|
<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">
|
|
{tables.map((table) => (
|
|
<CommandItem
|
|
key={table.tableName}
|
|
value={`${table.displayName || ""} ${table.tableName}`}
|
|
onSelect={() => {
|
|
updateEvents({ targetTable: table.tableName });
|
|
}}
|
|
>
|
|
<Check className={cn("mr-2 h-4 w-4", config.events?.targetTable === table.tableName ? "opacity-100" : "opacity-0")} />
|
|
{table.displayName || table.tableName}
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-xs">필터 컬럼 (버튼 값 컬럼 → 대상 테이블 컬럼)</Label>
|
|
<Input
|
|
value={config.events?.targetFilterColumn || ""}
|
|
onChange={(e) => updateEvents({ targetFilterColumn: e.target.value })}
|
|
placeholder="예: routing_version_id"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 추가 버튼 설정 */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-sm font-semibold">추가 버튼</Label>
|
|
<Switch
|
|
checked={config.addButton?.show ?? false}
|
|
onCheckedChange={(checked) => updateAddButton({ show: checked })}
|
|
/>
|
|
</div>
|
|
|
|
{config.addButton?.show && (
|
|
<div className="space-y-2 pl-2 border-l-2 border-gray-200">
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">버튼 라벨</Label>
|
|
<Input
|
|
value={config.addButton?.label || ""}
|
|
onChange={(e) => updateAddButton({ label: e.target.value })}
|
|
placeholder="+ 버전 추가"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">위치</Label>
|
|
<Select
|
|
value={config.addButton?.position || "header"}
|
|
onValueChange={(value: any) => updateAddButton({ position: value })}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="header">헤더 우측</SelectItem>
|
|
<SelectItem value="inline">버튼들과 함께</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">모달 화면 ID</Label>
|
|
<Input
|
|
type="number"
|
|
value={config.addButton?.modalScreenId || ""}
|
|
onChange={(e) => updateAddButton({ modalScreenId: parseInt(e.target.value) || undefined })}
|
|
placeholder="화면 ID"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 기타 설정 */}
|
|
<div className="space-y-3">
|
|
<Label className="text-sm font-semibold">기타 설정</Label>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Switch
|
|
checked={config.autoSelectFirst ?? true}
|
|
onCheckedChange={(checked) => updateConfig({ autoSelectFirst: checked })}
|
|
/>
|
|
<Label className="text-xs">첫 번째 항목 자동 선택</Label>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">빈 상태 메시지</Label>
|
|
<Input
|
|
value={config.emptyMessage || ""}
|
|
onChange={(e) => updateConfig({ emptyMessage: e.target.value })}
|
|
placeholder="데이터가 없습니다"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default RelatedDataButtonsConfigPanel;
|
|
|