범용폼모달 외부소스 지원
This commit is contained in:
parent
777429af48
commit
47ac9ecd8a
|
|
@ -6,7 +6,10 @@ import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Search, Database, RefreshCw, Settings, Plus, Activity, Trash2, Copy } from "lucide-react";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||||
|
import { Search, Database, RefreshCw, Settings, Plus, Activity, Trash2, Copy, Check, ChevronsUpDown } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useMultiLang } from "@/hooks/useMultiLang";
|
import { useMultiLang } from "@/hooks/useMultiLang";
|
||||||
|
|
@ -90,6 +93,13 @@ export default function TableManagementPage() {
|
||||||
// 🎯 Entity 조인 관련 상태
|
// 🎯 Entity 조인 관련 상태
|
||||||
const [referenceTableColumns, setReferenceTableColumns] = useState<Record<string, ReferenceTableColumn[]>>({});
|
const [referenceTableColumns, setReferenceTableColumns] = useState<Record<string, ReferenceTableColumn[]>>({});
|
||||||
|
|
||||||
|
// 🆕 Entity 타입 Combobox 열림/닫힘 상태 (컬럼별 관리)
|
||||||
|
const [entityComboboxOpen, setEntityComboboxOpen] = useState<Record<string, {
|
||||||
|
table: boolean;
|
||||||
|
joinColumn: boolean;
|
||||||
|
displayColumn: boolean;
|
||||||
|
}>>({});
|
||||||
|
|
||||||
// DDL 기능 관련 상태
|
// DDL 기능 관련 상태
|
||||||
const [createTableModalOpen, setCreateTableModalOpen] = useState(false);
|
const [createTableModalOpen, setCreateTableModalOpen] = useState(false);
|
||||||
const [addColumnModalOpen, setAddColumnModalOpen] = useState(false);
|
const [addColumnModalOpen, setAddColumnModalOpen] = useState(false);
|
||||||
|
|
@ -1388,113 +1398,266 @@ export default function TableManagementPage() {
|
||||||
{/* 입력 타입이 'entity'인 경우 참조 테이블 선택 */}
|
{/* 입력 타입이 'entity'인 경우 참조 테이블 선택 */}
|
||||||
{column.inputType === "entity" && (
|
{column.inputType === "entity" && (
|
||||||
<>
|
<>
|
||||||
{/* 참조 테이블 */}
|
{/* 참조 테이블 - 검색 가능한 Combobox */}
|
||||||
<div className="w-48">
|
<div className="w-56">
|
||||||
<label className="text-muted-foreground mb-1 block text-xs">참조 테이블</label>
|
<label className="text-muted-foreground mb-1 block text-xs">참조 테이블</label>
|
||||||
<Select
|
<Popover
|
||||||
value={column.referenceTable || "none"}
|
open={entityComboboxOpen[column.columnName]?.table || false}
|
||||||
onValueChange={(value) =>
|
onOpenChange={(open) =>
|
||||||
handleDetailSettingsChange(column.columnName, "entity", value)
|
setEntityComboboxOpen((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[column.columnName]: { ...prev[column.columnName], table: open },
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="bg-background h-8 w-full text-xs">
|
<PopoverTrigger asChild>
|
||||||
<SelectValue placeholder="선택" />
|
<Button
|
||||||
</SelectTrigger>
|
variant="outline"
|
||||||
<SelectContent>
|
role="combobox"
|
||||||
{referenceTableOptions.map((option, index) => (
|
aria-expanded={entityComboboxOpen[column.columnName]?.table || false}
|
||||||
<SelectItem key={`entity-${option.value}-${index}`} value={option.value}>
|
className="bg-background h-8 w-full justify-between text-xs"
|
||||||
<div className="flex flex-col">
|
>
|
||||||
<span className="font-medium">{option.label}</span>
|
{column.referenceTable && column.referenceTable !== "none"
|
||||||
<span className="text-muted-foreground text-xs">{option.value}</span>
|
? referenceTableOptions.find((opt) => opt.value === column.referenceTable)?.label ||
|
||||||
</div>
|
column.referenceTable
|
||||||
</SelectItem>
|
: "테이블 선택..."}
|
||||||
))}
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||||
</SelectContent>
|
</Button>
|
||||||
</Select>
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[280px] p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="테이블 검색..." className="h-8 text-xs" />
|
||||||
|
<CommandList className="max-h-[200px]">
|
||||||
|
<CommandEmpty className="py-2 text-center text-xs">
|
||||||
|
테이블을 찾을 수 없습니다.
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{referenceTableOptions.map((option) => (
|
||||||
|
<CommandItem
|
||||||
|
key={option.value}
|
||||||
|
value={`${option.label} ${option.value}`}
|
||||||
|
onSelect={() => {
|
||||||
|
handleDetailSettingsChange(column.columnName, "entity", option.value);
|
||||||
|
setEntityComboboxOpen((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[column.columnName]: { ...prev[column.columnName], table: false },
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-3 w-3",
|
||||||
|
column.referenceTable === option.value ? "opacity-100" : "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{option.label}</span>
|
||||||
|
{option.value !== "none" && (
|
||||||
|
<span className="text-muted-foreground text-[10px]">{option.value}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 조인 컬럼 */}
|
{/* 조인 컬럼 - 검색 가능한 Combobox */}
|
||||||
{column.referenceTable && column.referenceTable !== "none" && (
|
{column.referenceTable && column.referenceTable !== "none" && (
|
||||||
<div className="w-48">
|
<div className="w-56">
|
||||||
<label className="text-muted-foreground mb-1 block text-xs">조인 컬럼</label>
|
<label className="text-muted-foreground mb-1 block text-xs">조인 컬럼</label>
|
||||||
<Select
|
<Popover
|
||||||
value={column.referenceColumn || "none"}
|
open={entityComboboxOpen[column.columnName]?.joinColumn || false}
|
||||||
onValueChange={(value) =>
|
onOpenChange={(open) =>
|
||||||
handleDetailSettingsChange(
|
setEntityComboboxOpen((prev) => ({
|
||||||
column.columnName,
|
...prev,
|
||||||
"entity_reference_column",
|
[column.columnName]: { ...prev[column.columnName], joinColumn: open },
|
||||||
value,
|
}))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="bg-background h-8 w-full text-xs">
|
<PopoverTrigger asChild>
|
||||||
<SelectValue placeholder="선택" />
|
<Button
|
||||||
</SelectTrigger>
|
variant="outline"
|
||||||
<SelectContent>
|
role="combobox"
|
||||||
<SelectItem value="none">-- 선택 안함 --</SelectItem>
|
aria-expanded={entityComboboxOpen[column.columnName]?.joinColumn || false}
|
||||||
{referenceTableColumns[column.referenceTable]?.map((refCol, index) => (
|
className="bg-background h-8 w-full justify-between text-xs"
|
||||||
<SelectItem
|
disabled={!referenceTableColumns[column.referenceTable] || referenceTableColumns[column.referenceTable].length === 0}
|
||||||
key={`ref-col-${refCol.columnName}-${index}`}
|
>
|
||||||
value={refCol.columnName}
|
{!referenceTableColumns[column.referenceTable] || referenceTableColumns[column.referenceTable].length === 0 ? (
|
||||||
>
|
<span className="flex items-center gap-2">
|
||||||
<span className="font-medium">{refCol.columnName}</span>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
{(!referenceTableColumns[column.referenceTable] ||
|
|
||||||
referenceTableColumns[column.referenceTable].length === 0) && (
|
|
||||||
<SelectItem value="loading" disabled>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="border-primary h-3 w-3 animate-spin rounded-full border border-t-transparent"></div>
|
<div className="border-primary h-3 w-3 animate-spin rounded-full border border-t-transparent"></div>
|
||||||
로딩중
|
로딩중...
|
||||||
</div>
|
</span>
|
||||||
</SelectItem>
|
) : column.referenceColumn && column.referenceColumn !== "none" ? (
|
||||||
)}
|
column.referenceColumn
|
||||||
</SelectContent>
|
) : (
|
||||||
</Select>
|
"컬럼 선택..."
|
||||||
|
)}
|
||||||
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[280px] p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
|
||||||
|
<CommandList className="max-h-[200px]">
|
||||||
|
<CommandEmpty className="py-2 text-center text-xs">
|
||||||
|
컬럼을 찾을 수 없습니다.
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
<CommandItem
|
||||||
|
value="none"
|
||||||
|
onSelect={() => {
|
||||||
|
handleDetailSettingsChange(column.columnName, "entity_reference_column", "none");
|
||||||
|
setEntityComboboxOpen((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[column.columnName]: { ...prev[column.columnName], joinColumn: false },
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-3 w-3",
|
||||||
|
column.referenceColumn === "none" || !column.referenceColumn ? "opacity-100" : "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
-- 선택 안함 --
|
||||||
|
</CommandItem>
|
||||||
|
{referenceTableColumns[column.referenceTable]?.map((refCol) => (
|
||||||
|
<CommandItem
|
||||||
|
key={refCol.columnName}
|
||||||
|
value={`${refCol.columnLabel || ""} ${refCol.columnName}`}
|
||||||
|
onSelect={() => {
|
||||||
|
handleDetailSettingsChange(column.columnName, "entity_reference_column", refCol.columnName);
|
||||||
|
setEntityComboboxOpen((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[column.columnName]: { ...prev[column.columnName], joinColumn: false },
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-3 w-3",
|
||||||
|
column.referenceColumn === refCol.columnName ? "opacity-100" : "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{refCol.columnName}</span>
|
||||||
|
{refCol.columnLabel && (
|
||||||
|
<span className="text-muted-foreground text-[10px]">{refCol.columnLabel}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 표시 컬럼 */}
|
{/* 표시 컬럼 - 검색 가능한 Combobox */}
|
||||||
{column.referenceTable &&
|
{column.referenceTable &&
|
||||||
column.referenceTable !== "none" &&
|
column.referenceTable !== "none" &&
|
||||||
column.referenceColumn &&
|
column.referenceColumn &&
|
||||||
column.referenceColumn !== "none" && (
|
column.referenceColumn !== "none" && (
|
||||||
<div className="w-48">
|
<div className="w-56">
|
||||||
<label className="text-muted-foreground mb-1 block text-xs">표시 컬럼</label>
|
<label className="text-muted-foreground mb-1 block text-xs">표시 컬럼</label>
|
||||||
<Select
|
<Popover
|
||||||
value={column.displayColumn || "none"}
|
open={entityComboboxOpen[column.columnName]?.displayColumn || false}
|
||||||
onValueChange={(value) =>
|
onOpenChange={(open) =>
|
||||||
handleDetailSettingsChange(
|
setEntityComboboxOpen((prev) => ({
|
||||||
column.columnName,
|
...prev,
|
||||||
"entity_display_column",
|
[column.columnName]: { ...prev[column.columnName], displayColumn: open },
|
||||||
value,
|
}))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="bg-background h-8 w-full text-xs">
|
<PopoverTrigger asChild>
|
||||||
<SelectValue placeholder="선택" />
|
<Button
|
||||||
</SelectTrigger>
|
variant="outline"
|
||||||
<SelectContent>
|
role="combobox"
|
||||||
<SelectItem value="none">-- 선택 안함 --</SelectItem>
|
aria-expanded={entityComboboxOpen[column.columnName]?.displayColumn || false}
|
||||||
{referenceTableColumns[column.referenceTable]?.map((refCol, index) => (
|
className="bg-background h-8 w-full justify-between text-xs"
|
||||||
<SelectItem
|
disabled={!referenceTableColumns[column.referenceTable] || referenceTableColumns[column.referenceTable].length === 0}
|
||||||
key={`ref-col-${refCol.columnName}-${index}`}
|
>
|
||||||
value={refCol.columnName}
|
{!referenceTableColumns[column.referenceTable] || referenceTableColumns[column.referenceTable].length === 0 ? (
|
||||||
>
|
<span className="flex items-center gap-2">
|
||||||
<span className="font-medium">{refCol.columnName}</span>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
{(!referenceTableColumns[column.referenceTable] ||
|
|
||||||
referenceTableColumns[column.referenceTable].length === 0) && (
|
|
||||||
<SelectItem value="loading" disabled>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="border-primary h-3 w-3 animate-spin rounded-full border border-t-transparent"></div>
|
<div className="border-primary h-3 w-3 animate-spin rounded-full border border-t-transparent"></div>
|
||||||
로딩중
|
로딩중...
|
||||||
</div>
|
</span>
|
||||||
</SelectItem>
|
) : column.displayColumn && column.displayColumn !== "none" ? (
|
||||||
)}
|
column.displayColumn
|
||||||
</SelectContent>
|
) : (
|
||||||
</Select>
|
"컬럼 선택..."
|
||||||
|
)}
|
||||||
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[280px] p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
|
||||||
|
<CommandList className="max-h-[200px]">
|
||||||
|
<CommandEmpty className="py-2 text-center text-xs">
|
||||||
|
컬럼을 찾을 수 없습니다.
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
<CommandItem
|
||||||
|
value="none"
|
||||||
|
onSelect={() => {
|
||||||
|
handleDetailSettingsChange(column.columnName, "entity_display_column", "none");
|
||||||
|
setEntityComboboxOpen((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[column.columnName]: { ...prev[column.columnName], displayColumn: false },
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-3 w-3",
|
||||||
|
column.displayColumn === "none" || !column.displayColumn ? "opacity-100" : "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
-- 선택 안함 --
|
||||||
|
</CommandItem>
|
||||||
|
{referenceTableColumns[column.referenceTable]?.map((refCol) => (
|
||||||
|
<CommandItem
|
||||||
|
key={refCol.columnName}
|
||||||
|
value={`${refCol.columnLabel || ""} ${refCol.columnName}`}
|
||||||
|
onSelect={() => {
|
||||||
|
handleDetailSettingsChange(column.columnName, "entity_display_column", refCol.columnName);
|
||||||
|
setEntityComboboxOpen((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[column.columnName]: { ...prev[column.columnName], displayColumn: false },
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-3 w-3",
|
||||||
|
column.displayColumn === refCol.columnName ? "opacity-100" : "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{refCol.columnName}</span>
|
||||||
|
{refCol.columnLabel && (
|
||||||
|
<span className="text-muted-foreground text-[10px]">{refCol.columnLabel}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -1505,8 +1668,8 @@ export default function TableManagementPage() {
|
||||||
column.referenceColumn !== "none" &&
|
column.referenceColumn !== "none" &&
|
||||||
column.displayColumn &&
|
column.displayColumn &&
|
||||||
column.displayColumn !== "none" && (
|
column.displayColumn !== "none" && (
|
||||||
<div className="bg-primary/10 text-primary flex w-48 items-center gap-1 rounded px-2 py-1 text-xs">
|
<div className="bg-primary/10 text-primary flex items-center gap-1 rounded px-2 py-1 text-xs">
|
||||||
<span>✓</span>
|
<Check className="h-3 w-3" />
|
||||||
<span className="truncate">설정 완료</span>
|
<span className="truncate">설정 완료</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ interface TableSectionRendererProps {
|
||||||
onTableDataChange: (data: any[]) => void;
|
onTableDataChange: (data: any[]) => void;
|
||||||
// 조건부 테이블용 콜백 (조건별 데이터 변경)
|
// 조건부 테이블용 콜백 (조건별 데이터 변경)
|
||||||
onConditionalTableDataChange?: (conditionValue: string, data: any[]) => void;
|
onConditionalTableDataChange?: (conditionValue: string, data: any[]) => void;
|
||||||
|
// 외부 데이터 (데이터 전달 모달열기 액션으로 전달받은 데이터)
|
||||||
|
groupedData?: Record<string, any>[];
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -337,6 +339,7 @@ export function TableSectionRenderer({
|
||||||
onFormDataChange,
|
onFormDataChange,
|
||||||
onTableDataChange,
|
onTableDataChange,
|
||||||
onConditionalTableDataChange,
|
onConditionalTableDataChange,
|
||||||
|
groupedData,
|
||||||
className,
|
className,
|
||||||
}: TableSectionRendererProps) {
|
}: TableSectionRendererProps) {
|
||||||
// 테이블 데이터 상태 (일반 모드)
|
// 테이블 데이터 상태 (일반 모드)
|
||||||
|
|
@ -373,6 +376,13 @@ export function TableSectionRenderer({
|
||||||
// 초기 데이터 로드 완료 플래그 (무한 루프 방지)
|
// 초기 데이터 로드 완료 플래그 (무한 루프 방지)
|
||||||
const initialDataLoadedRef = React.useRef(false);
|
const initialDataLoadedRef = React.useRef(false);
|
||||||
|
|
||||||
|
// 외부 데이터 로드 완료 플래그
|
||||||
|
const externalDataLoadedRef = React.useRef(false);
|
||||||
|
|
||||||
|
// 외부 데이터 소스 설정
|
||||||
|
const externalDataConfig = tableConfig.externalDataSource;
|
||||||
|
const isExternalDataMode = externalDataConfig?.enabled && externalDataConfig?.tableName;
|
||||||
|
|
||||||
// 조건부 테이블 설정
|
// 조건부 테이블 설정
|
||||||
const conditionalConfig = tableConfig.conditionalTable;
|
const conditionalConfig = tableConfig.conditionalTable;
|
||||||
const isConditionalMode = conditionalConfig?.enabled ?? false;
|
const isConditionalMode = conditionalConfig?.enabled ?? false;
|
||||||
|
|
@ -388,6 +398,56 @@ export function TableSectionRenderer({
|
||||||
// 소스 테이블의 컬럼 라벨 (API에서 동적 로드)
|
// 소스 테이블의 컬럼 라벨 (API에서 동적 로드)
|
||||||
const [sourceColumnLabels, setSourceColumnLabels] = useState<Record<string, string>>({});
|
const [sourceColumnLabels, setSourceColumnLabels] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
// 외부 데이터(groupedData) 처리: 데이터 전달 모달열기 액션으로 전달받은 데이터를 초기 테이블 데이터로 설정
|
||||||
|
useEffect(() => {
|
||||||
|
// 외부 데이터 소스가 활성화되지 않았거나, groupedData가 없으면 스킵
|
||||||
|
if (!isExternalDataMode) return;
|
||||||
|
if (!groupedData || groupedData.length === 0) return;
|
||||||
|
// 이미 로드된 경우 스킵
|
||||||
|
if (externalDataLoadedRef.current) return;
|
||||||
|
|
||||||
|
console.log("[TableSectionRenderer] 외부 데이터 처리 시작:", {
|
||||||
|
externalTableName: externalDataConfig?.tableName,
|
||||||
|
groupedDataCount: groupedData.length,
|
||||||
|
columns: tableConfig.columns?.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
// groupedData를 테이블 컬럼 매핑에 따라 변환
|
||||||
|
const mappedData = groupedData.map((externalRow, index) => {
|
||||||
|
const newRow: Record<string, any> = {
|
||||||
|
_id: `external_${Date.now()}_${index}`,
|
||||||
|
_sourceData: externalRow, // 원본 데이터 보관
|
||||||
|
};
|
||||||
|
|
||||||
|
// 각 컬럼에 대해 externalField 또는 field로 값을 매핑
|
||||||
|
tableConfig.columns?.forEach((col) => {
|
||||||
|
// externalField가 설정되어 있으면 사용, 아니면 field와 동일한 이름으로 매핑
|
||||||
|
const externalFieldName = col.externalField || col.field;
|
||||||
|
const value = externalRow[externalFieldName];
|
||||||
|
|
||||||
|
// 값이 있으면 설정
|
||||||
|
if (value !== undefined) {
|
||||||
|
newRow[col.field] = value;
|
||||||
|
} else if (col.defaultValue !== undefined) {
|
||||||
|
// 기본값이 설정되어 있으면 기본값 사용
|
||||||
|
newRow[col.field] = col.defaultValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return newRow;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[TableSectionRenderer] 외부 데이터 매핑 완료:", {
|
||||||
|
mappedCount: mappedData.length,
|
||||||
|
sampleRow: mappedData[0],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 테이블 데이터 설정
|
||||||
|
setTableData(mappedData);
|
||||||
|
onTableDataChange(mappedData);
|
||||||
|
externalDataLoadedRef.current = true;
|
||||||
|
}, [isExternalDataMode, groupedData, tableConfig.columns, externalDataConfig?.tableName, onTableDataChange]);
|
||||||
|
|
||||||
// 소스 테이블의 카테고리 타입 컬럼 목록 로드
|
// 소스 테이블의 카테고리 타입 컬럼 목록 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadCategoryColumns = async () => {
|
const loadCategoryColumns = async () => {
|
||||||
|
|
|
||||||
|
|
@ -2295,6 +2295,7 @@ export function UniversalFormModalComponent({
|
||||||
// 테이블 섹션 데이터를 formData에 저장
|
// 테이블 섹션 데이터를 formData에 저장
|
||||||
handleFieldChange(`_tableSection_${section.id}`, data);
|
handleFieldChange(`_tableSection_${section.id}`, data);
|
||||||
}}
|
}}
|
||||||
|
groupedData={_groupedData}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -710,6 +710,9 @@ interface ColumnSettingItemProps {
|
||||||
displayColumns: string[]; // 검색 설정에서 선택한 표시 컬럼 목록
|
displayColumns: string[]; // 검색 설정에서 선택한 표시 컬럼 목록
|
||||||
sourceTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string }[]; // 소스 테이블 컬럼
|
sourceTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string }[]; // 소스 테이블 컬럼
|
||||||
sourceTableName: string; // 소스 테이블명
|
sourceTableName: string; // 소스 테이블명
|
||||||
|
externalTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string }[]; // 외부 데이터 테이블 컬럼
|
||||||
|
externalTableName?: string; // 외부 데이터 테이블명
|
||||||
|
externalDataEnabled?: boolean; // 외부 데이터 소스 활성화 여부
|
||||||
tables: { table_name: string; comment?: string }[]; // 전체 테이블 목록
|
tables: { table_name: string; comment?: string }[]; // 전체 테이블 목록
|
||||||
tableColumns: Record<string, { column_name: string; data_type: string; is_nullable: string; comment?: string }[]>; // 테이블별 컬럼
|
tableColumns: Record<string, { column_name: string; data_type: string; is_nullable: string; comment?: string }[]>; // 테이블별 컬럼
|
||||||
sections: { id: string; title: string }[]; // 섹션 목록
|
sections: { id: string; title: string }[]; // 섹션 목록
|
||||||
|
|
@ -731,6 +734,9 @@ function ColumnSettingItem({
|
||||||
displayColumns,
|
displayColumns,
|
||||||
sourceTableColumns,
|
sourceTableColumns,
|
||||||
sourceTableName,
|
sourceTableName,
|
||||||
|
externalTableColumns,
|
||||||
|
externalTableName,
|
||||||
|
externalDataEnabled,
|
||||||
tables,
|
tables,
|
||||||
tableColumns,
|
tableColumns,
|
||||||
sections,
|
sections,
|
||||||
|
|
@ -745,6 +751,7 @@ function ColumnSettingItem({
|
||||||
}: ColumnSettingItemProps) {
|
}: ColumnSettingItemProps) {
|
||||||
const [fieldSearchOpen, setFieldSearchOpen] = useState(false);
|
const [fieldSearchOpen, setFieldSearchOpen] = useState(false);
|
||||||
const [sourceFieldSearchOpen, setSourceFieldSearchOpen] = useState(false);
|
const [sourceFieldSearchOpen, setSourceFieldSearchOpen] = useState(false);
|
||||||
|
const [externalFieldSearchOpen, setExternalFieldSearchOpen] = useState(false);
|
||||||
const [parentFieldSearchOpen, setParentFieldSearchOpen] = useState(false);
|
const [parentFieldSearchOpen, setParentFieldSearchOpen] = useState(false);
|
||||||
const [lookupTableOpenMap, setLookupTableOpenMap] = useState<Record<string, boolean>>({});
|
const [lookupTableOpenMap, setLookupTableOpenMap] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
|
@ -1014,6 +1021,88 @@ function ColumnSettingItem({
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 외부 필드 - Combobox (외부 데이터에서 가져올 컬럼) */}
|
||||||
|
{externalDataEnabled && externalTableName && (
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">외부 필드</Label>
|
||||||
|
<Popover open={externalFieldSearchOpen} onOpenChange={setExternalFieldSearchOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={externalFieldSearchOpen}
|
||||||
|
className="h-8 w-full justify-between text-xs mt-1"
|
||||||
|
disabled={externalTableColumns.length === 0}
|
||||||
|
>
|
||||||
|
<span className="truncate">
|
||||||
|
{col.externalField || "(필드명과 동일)"}
|
||||||
|
</span>
|
||||||
|
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0 w-[250px]" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="외부 필드 검색..." className="text-xs" />
|
||||||
|
<CommandList className="max-h-[200px]">
|
||||||
|
<CommandEmpty className="text-xs py-4 text-center">
|
||||||
|
외부 필드를 찾을 수 없습니다.
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{/* 필드명과 동일 옵션 */}
|
||||||
|
<CommandItem
|
||||||
|
value="__same_as_field__"
|
||||||
|
onSelect={() => {
|
||||||
|
onUpdate({ externalField: undefined });
|
||||||
|
setExternalFieldSearchOpen(false);
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-3 w-3",
|
||||||
|
!col.externalField ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground">(필드명과 동일)</span>
|
||||||
|
</CommandItem>
|
||||||
|
{/* 외부 테이블 컬럼 목록 */}
|
||||||
|
{externalTableColumns.map((extCol) => (
|
||||||
|
<CommandItem
|
||||||
|
key={extCol.column_name}
|
||||||
|
value={extCol.column_name}
|
||||||
|
onSelect={() => {
|
||||||
|
onUpdate({ externalField: extCol.column_name });
|
||||||
|
setExternalFieldSearchOpen(false);
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-3 w-3",
|
||||||
|
col.externalField === extCol.column_name ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col flex-1 min-w-0">
|
||||||
|
<span className="font-medium truncate">{extCol.column_name}</span>
|
||||||
|
{extCol.comment && (
|
||||||
|
<span className="text-[10px] text-muted-foreground truncate">
|
||||||
|
{extCol.comment}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<p className="text-[10px] text-muted-foreground mt-0.5">
|
||||||
|
외부 데이터({externalTableName})에서 이 컬럼에 매핑할 필드
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 라벨 */}
|
{/* 라벨 */}
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">라벨</Label>
|
<Label className="text-xs">라벨</Label>
|
||||||
|
|
@ -2450,6 +2539,7 @@ export function TableSectionSettingsModal({
|
||||||
// 테이블 검색 Combobox 상태
|
// 테이블 검색 Combobox 상태
|
||||||
const [tableSearchOpen, setTableSearchOpen] = useState(false);
|
const [tableSearchOpen, setTableSearchOpen] = useState(false);
|
||||||
const [saveTableSearchOpen, setSaveTableSearchOpen] = useState(false);
|
const [saveTableSearchOpen, setSaveTableSearchOpen] = useState(false);
|
||||||
|
const [externalTableSearchOpen, setExternalTableSearchOpen] = useState(false);
|
||||||
|
|
||||||
// 활성 탭
|
// 활성 탭
|
||||||
const [activeTab, setActiveTab] = useState("source");
|
const [activeTab, setActiveTab] = useState("source");
|
||||||
|
|
@ -2623,6 +2713,24 @@ export function TableSectionSettingsModal({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateExternalDataSource = (updates: Partial<NonNullable<TableSectionConfig["externalDataSource"]>>) => {
|
||||||
|
updateTableConfig({
|
||||||
|
externalDataSource: { ...tableConfig.externalDataSource, enabled: false, tableName: "", ...updates },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 외부 데이터 소스 테이블 컬럼 목록
|
||||||
|
const externalTableColumns = useMemo(() => {
|
||||||
|
return tableColumns[tableConfig.externalDataSource?.tableName || ""] || [];
|
||||||
|
}, [tableColumns, tableConfig.externalDataSource?.tableName]);
|
||||||
|
|
||||||
|
// 외부 데이터 소스 테이블 변경 시 컬럼 로드
|
||||||
|
useEffect(() => {
|
||||||
|
if (tableConfig.externalDataSource?.enabled && tableConfig.externalDataSource?.tableName) {
|
||||||
|
onLoadTableColumns(tableConfig.externalDataSource.tableName);
|
||||||
|
}
|
||||||
|
}, [tableConfig.externalDataSource?.enabled, tableConfig.externalDataSource?.tableName, onLoadTableColumns]);
|
||||||
|
|
||||||
// 저장 함수
|
// 저장 함수
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
onSave({
|
onSave({
|
||||||
|
|
@ -2986,6 +3094,98 @@ export function TableSectionSettingsModal({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 외부 데이터 소스 설정 */}
|
||||||
|
<div className="space-y-3 border rounded-lg p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium">외부 데이터 소스</h4>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
"데이터 전달 모달열기" 액션으로 전달받은 데이터를 테이블에 표시합니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={tableConfig.externalDataSource?.enabled ?? false}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
updateExternalDataSource({ enabled: true, tableName: "" });
|
||||||
|
} else {
|
||||||
|
updateTableConfig({ externalDataSource: undefined });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tableConfig.externalDataSource?.enabled && (
|
||||||
|
<div className="space-y-3 pt-2">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs font-medium mb-1.5 block">외부 데이터 테이블</Label>
|
||||||
|
<Popover open={externalTableSearchOpen} onOpenChange={setExternalTableSearchOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={externalTableSearchOpen}
|
||||||
|
className="h-9 w-full justify-between text-sm"
|
||||||
|
>
|
||||||
|
{tableConfig.externalDataSource?.tableName || "테이블 선택..."}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0 w-full min-w-[400px]" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="테이블 검색..." className="text-sm" />
|
||||||
|
<CommandList className="max-h-[300px]">
|
||||||
|
<CommandEmpty className="text-sm py-6 text-center">
|
||||||
|
테이블을 찾을 수 없습니다.
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{tables.map((table) => (
|
||||||
|
<CommandItem
|
||||||
|
key={table.table_name}
|
||||||
|
value={table.table_name}
|
||||||
|
onSelect={() => {
|
||||||
|
updateExternalDataSource({ enabled: true, tableName: table.table_name });
|
||||||
|
onLoadTableColumns(table.table_name);
|
||||||
|
setExternalTableSearchOpen(false);
|
||||||
|
}}
|
||||||
|
className="text-sm"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
tableConfig.externalDataSource?.tableName === table.table_name ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{table.table_name}</span>
|
||||||
|
{table.comment && (
|
||||||
|
<span className="text-xs text-muted-foreground">{table.comment}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<HelpText>이전 화면에서 전달받을 데이터의 원본 테이블을 선택하세요. (예: 수주상세 데이터를 전달받는 경우 sales_order_detail)</HelpText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tableConfig.externalDataSource?.tableName && externalTableColumns.length > 0 && (
|
||||||
|
<div className="bg-muted/30 rounded-lg p-3">
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
선택한 테이블 컬럼: {externalTableColumns.length}개
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-muted-foreground mt-1">
|
||||||
|
"컬럼 설정" 탭에서 각 컬럼의 "외부 필드"를 설정하여 전달받은 데이터의 컬럼을 매핑하세요.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* 컬럼 설정 탭 */}
|
{/* 컬럼 설정 탭 */}
|
||||||
|
|
@ -3041,6 +3241,9 @@ export function TableSectionSettingsModal({
|
||||||
displayColumns={tableConfig.source.displayColumns || []}
|
displayColumns={tableConfig.source.displayColumns || []}
|
||||||
sourceTableColumns={sourceTableColumns}
|
sourceTableColumns={sourceTableColumns}
|
||||||
sourceTableName={tableConfig.source.tableName}
|
sourceTableName={tableConfig.source.tableName}
|
||||||
|
externalTableColumns={externalTableColumns}
|
||||||
|
externalTableName={tableConfig.externalDataSource?.tableName}
|
||||||
|
externalDataEnabled={tableConfig.externalDataSource?.enabled}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
tableColumns={tableColumns}
|
tableColumns={tableColumns}
|
||||||
sections={otherSections}
|
sections={otherSections}
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,12 @@ export interface TableSectionConfig {
|
||||||
columnLabels?: Record<string, string>; // 컬럼 라벨 (컬럼명 -> 표시 라벨)
|
columnLabels?: Record<string, string>; // 컬럼 라벨 (컬럼명 -> 표시 라벨)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 1-1. 외부 데이터 소스 설정 (데이터 전달 모달열기 액션으로 전달받은 데이터)
|
||||||
|
externalDataSource?: {
|
||||||
|
enabled: boolean; // 외부 데이터 소스 사용 여부
|
||||||
|
tableName: string; // 전달받을 데이터의 소스 테이블명 (예: sales_order_detail)
|
||||||
|
};
|
||||||
|
|
||||||
// 2. 필터 설정
|
// 2. 필터 설정
|
||||||
filters?: {
|
filters?: {
|
||||||
// 사전 필터 (항상 적용, 사용자에게 노출되지 않음)
|
// 사전 필터 (항상 적용, 사용자에게 노출되지 않음)
|
||||||
|
|
@ -374,6 +380,9 @@ export interface TableColumnConfig {
|
||||||
// 소스 필드 매핑 (검색 모달에서 가져올 컬럼명)
|
// 소스 필드 매핑 (검색 모달에서 가져올 컬럼명)
|
||||||
sourceField?: string; // 소스 테이블의 컬럼명 (미설정 시 field와 동일)
|
sourceField?: string; // 소스 테이블의 컬럼명 (미설정 시 field와 동일)
|
||||||
|
|
||||||
|
// 외부 데이터 필드 매핑 (데이터 전달 모달열기로 전달받은 데이터의 컬럼명)
|
||||||
|
externalField?: string; // 외부 데이터의 컬럼명 (미설정 시 field와 동일)
|
||||||
|
|
||||||
// 편집 설정
|
// 편집 설정
|
||||||
editable?: boolean; // 편집 가능 여부 (기본: true)
|
editable?: boolean; // 편집 가능 여부 (기본: true)
|
||||||
calculated?: boolean; // 계산 필드 여부 (자동 읽기전용)
|
calculated?: boolean; // 계산 필드 여부 (자동 읽기전용)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue