1457 lines
68 KiB
TypeScript
1457 lines
68 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } 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 { 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 { Switch } from "@/components/ui/switch";
|
|
import { Plus, X, Check, ChevronsUpDown } from "lucide-react";
|
|
import { ModalRepeaterTableProps, RepeaterColumnConfig, ColumnMapping, CalculationRule } from "./types";
|
|
import { tableManagementApi } from "@/lib/api/tableManagement";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface ModalRepeaterTableConfigPanelProps {
|
|
config: Partial<ModalRepeaterTableProps>;
|
|
onConfigChange: (config: Partial<ModalRepeaterTableProps>) => void;
|
|
}
|
|
|
|
// 소스 컬럼 선택기 (동적 테이블별 컬럼 로드)
|
|
function SourceColumnSelector({
|
|
sourceTable,
|
|
value,
|
|
onChange,
|
|
}: {
|
|
sourceTable: string;
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
}) {
|
|
const [columns, setColumns] = useState<{ columnName: string; displayName?: string }[]>([]);
|
|
const [isLoading, setIsLoading] = 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]);
|
|
|
|
return (
|
|
<Select value={value} onValueChange={onChange} disabled={!sourceTable || isLoading}>
|
|
<SelectTrigger className="h-10 text-sm w-full">
|
|
<SelectValue placeholder={isLoading ? "로딩 중..." : "컬럼 선택"} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{columns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
);
|
|
}
|
|
|
|
// 참조 컬럼 선택기 (동적 테이블별 컬럼 로드)
|
|
function ReferenceColumnSelector({
|
|
referenceTable,
|
|
value,
|
|
onChange,
|
|
}: {
|
|
referenceTable: string;
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
}) {
|
|
const [columns, setColumns] = useState<{ columnName: string; displayName?: string }[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const loadColumns = async () => {
|
|
if (!referenceTable) {
|
|
setColumns([]);
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
try {
|
|
const response = await tableManagementApi.getColumnList(referenceTable);
|
|
if (response.success && response.data) {
|
|
setColumns(response.data.columns);
|
|
}
|
|
} catch (error) {
|
|
console.error("참조 컬럼 로드 실패:", error);
|
|
setColumns([]);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
loadColumns();
|
|
}, [referenceTable]);
|
|
|
|
return (
|
|
<Select value={value} onValueChange={onChange} disabled={!referenceTable || isLoading}>
|
|
<SelectTrigger className="h-10 text-sm w-full">
|
|
<SelectValue placeholder={isLoading ? "로딩 중..." : "컬럼 선택"} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{columns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
);
|
|
}
|
|
|
|
export function ModalRepeaterTableConfigPanel({
|
|
config,
|
|
onConfigChange,
|
|
}: ModalRepeaterTableConfigPanelProps) {
|
|
// 초기 설정 정리: 계산 규칙과 컬럼 설정 동기화
|
|
const cleanupInitialConfig = (initialConfig: Partial<ModalRepeaterTableProps>): Partial<ModalRepeaterTableProps> => {
|
|
// 계산 규칙이 없으면 모든 컬럼의 calculated 속성 제거
|
|
if (!initialConfig.calculationRules || initialConfig.calculationRules.length === 0) {
|
|
const cleanedColumns = (initialConfig.columns || []).map((col) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const { calculated, ...rest } = col;
|
|
return { ...rest, editable: true };
|
|
});
|
|
return { ...initialConfig, columns: cleanedColumns };
|
|
}
|
|
|
|
// 계산 규칙이 있으면 결과 필드만 calculated=true
|
|
const resultFields = new Set(initialConfig.calculationRules.map((rule) => rule.result));
|
|
const cleanedColumns = (initialConfig.columns || []).map((col) => {
|
|
if (resultFields.has(col.field)) {
|
|
// 계산 결과 필드는 calculated=true, editable=false
|
|
return { ...col, calculated: true, editable: false };
|
|
} else {
|
|
// 나머지 필드는 calculated 제거, editable=true
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const { calculated, ...rest } = col;
|
|
return { ...rest, editable: true };
|
|
}
|
|
});
|
|
|
|
return { ...initialConfig, columns: cleanedColumns };
|
|
};
|
|
|
|
const [localConfig, setLocalConfig] = useState(cleanupInitialConfig(config));
|
|
const [allTables, setAllTables] = useState<{ tableName: string; displayName?: string }[]>([]);
|
|
const [tableColumns, setTableColumns] = useState<{ columnName: string; displayName?: string }[]>([]);
|
|
const [targetTableColumns, setTargetTableColumns] = useState<{ columnName: string; displayName?: string }[]>([]);
|
|
const [isLoadingTables, setIsLoadingTables] = useState(false);
|
|
const [isLoadingColumns, setIsLoadingColumns] = useState(false);
|
|
const [isLoadingTargetColumns, setIsLoadingTargetColumns] = useState(false);
|
|
const [openTableCombo, setOpenTableCombo] = useState(false);
|
|
const [openTargetTableCombo, setOpenTargetTableCombo] = useState(false);
|
|
const [openUniqueFieldCombo, setOpenUniqueFieldCombo] = useState(false);
|
|
|
|
// config 변경 시 localConfig 동기화 (cleanupInitialConfig 적용)
|
|
useEffect(() => {
|
|
const cleanedConfig = cleanupInitialConfig(config);
|
|
setLocalConfig(cleanedConfig);
|
|
}, [config]);
|
|
|
|
// 전체 테이블 목록 로드
|
|
useEffect(() => {
|
|
const loadTables = async () => {
|
|
setIsLoadingTables(true);
|
|
try {
|
|
const response = await tableManagementApi.getTableList();
|
|
if (response.success && response.data) {
|
|
setAllTables(response.data);
|
|
}
|
|
} catch (error) {
|
|
console.error("테이블 목록 로드 실패:", error);
|
|
} finally {
|
|
setIsLoadingTables(false);
|
|
}
|
|
};
|
|
loadTables();
|
|
}, []);
|
|
|
|
// 소스 테이블의 컬럼 목록 로드
|
|
useEffect(() => {
|
|
const loadColumns = async () => {
|
|
if (!localConfig.sourceTable) {
|
|
setTableColumns([]);
|
|
return;
|
|
}
|
|
|
|
setIsLoadingColumns(true);
|
|
try {
|
|
const response = await tableManagementApi.getColumnList(localConfig.sourceTable);
|
|
if (response.success && response.data) {
|
|
setTableColumns(response.data.columns);
|
|
}
|
|
} catch (error) {
|
|
console.error("컬럼 목록 로드 실패:", error);
|
|
setTableColumns([]);
|
|
} finally {
|
|
setIsLoadingColumns(false);
|
|
}
|
|
};
|
|
loadColumns();
|
|
}, [localConfig.sourceTable]);
|
|
|
|
// 저장 테이블의 컬럼 목록 로드
|
|
useEffect(() => {
|
|
const loadTargetColumns = async () => {
|
|
if (!localConfig.targetTable) {
|
|
setTargetTableColumns([]);
|
|
return;
|
|
}
|
|
|
|
setIsLoadingTargetColumns(true);
|
|
try {
|
|
const response = await tableManagementApi.getColumnList(localConfig.targetTable);
|
|
if (response.success && response.data) {
|
|
setTargetTableColumns(response.data.columns);
|
|
}
|
|
} catch (error) {
|
|
console.error("저장 테이블 컬럼 로드 실패:", error);
|
|
setTargetTableColumns([]);
|
|
} finally {
|
|
setIsLoadingTargetColumns(false);
|
|
}
|
|
};
|
|
loadTargetColumns();
|
|
}, [localConfig.targetTable]);
|
|
|
|
const updateConfig = (updates: Partial<ModalRepeaterTableProps>) => {
|
|
const newConfig = { ...localConfig, ...updates };
|
|
setLocalConfig(newConfig);
|
|
onConfigChange(newConfig);
|
|
};
|
|
|
|
const addSourceColumn = () => {
|
|
const columns = localConfig.sourceColumns || [];
|
|
updateConfig({ sourceColumns: [...columns, ""] });
|
|
};
|
|
|
|
const updateSourceColumn = (index: number, value: string) => {
|
|
const columns = [...(localConfig.sourceColumns || [])];
|
|
columns[index] = value;
|
|
updateConfig({ sourceColumns: columns });
|
|
};
|
|
|
|
const removeSourceColumn = (index: number) => {
|
|
const columns = [...(localConfig.sourceColumns || [])];
|
|
columns.splice(index, 1);
|
|
updateConfig({ sourceColumns: columns });
|
|
};
|
|
|
|
const addSearchField = () => {
|
|
const fields = localConfig.sourceSearchFields || [];
|
|
updateConfig({ sourceSearchFields: [...fields, ""] });
|
|
};
|
|
|
|
const updateSearchField = (index: number, value: string) => {
|
|
const fields = [...(localConfig.sourceSearchFields || [])];
|
|
fields[index] = value;
|
|
updateConfig({ sourceSearchFields: fields });
|
|
};
|
|
|
|
const removeSearchField = (index: number) => {
|
|
const fields = [...(localConfig.sourceSearchFields || [])];
|
|
fields.splice(index, 1);
|
|
updateConfig({ sourceSearchFields: fields });
|
|
};
|
|
|
|
// 🆕 저장 테이블 컬럼에서 반복 테이블 컬럼 추가
|
|
const addRepeaterColumnFromTarget = (columnName: string) => {
|
|
const columns = localConfig.columns || [];
|
|
|
|
// 이미 존재하는 컬럼인지 확인
|
|
if (columns.some((col) => col.field === columnName)) {
|
|
alert("이미 추가된 컬럼입니다.");
|
|
return;
|
|
}
|
|
|
|
const targetCol = targetTableColumns.find((c) => c.columnName === columnName);
|
|
|
|
const newColumn: RepeaterColumnConfig = {
|
|
field: columnName,
|
|
label: targetCol?.displayName || columnName,
|
|
type: "text",
|
|
width: "150px",
|
|
editable: true,
|
|
mapping: {
|
|
type: "manual",
|
|
referenceTable: localConfig.targetTable,
|
|
referenceField: columnName,
|
|
},
|
|
};
|
|
|
|
updateConfig({ columns: [...columns, newColumn] });
|
|
};
|
|
|
|
// 🆕 반복 테이블 컬럼 삭제
|
|
const removeRepeaterColumn = (index: number) => {
|
|
const columns = [...(localConfig.columns || [])];
|
|
columns.splice(index, 1);
|
|
updateConfig({ columns });
|
|
};
|
|
|
|
// 🆕 반복 테이블 컬럼 개별 수정
|
|
const updateRepeaterColumn = (index: number, updates: Partial<RepeaterColumnConfig>) => {
|
|
const columns = [...(localConfig.columns || [])];
|
|
columns[index] = { ...columns[index], ...updates };
|
|
updateConfig({ columns });
|
|
};
|
|
|
|
// 🆕 계산 규칙 관리
|
|
const addCalculationRule = () => {
|
|
const rules = localConfig.calculationRules || [];
|
|
const newRule: CalculationRule = {
|
|
result: "",
|
|
formula: "",
|
|
dependencies: [],
|
|
};
|
|
updateConfig({ calculationRules: [...rules, newRule] });
|
|
};
|
|
|
|
const updateCalculationRule = (index: number, updates: Partial<CalculationRule>) => {
|
|
const rules = [...(localConfig.calculationRules || [])];
|
|
const oldRule = rules[index];
|
|
const newRule = { ...oldRule, ...updates };
|
|
|
|
// 결과 필드가 변경된 경우
|
|
if (updates.result !== undefined && oldRule.result !== updates.result) {
|
|
const columns = [...(localConfig.columns || [])];
|
|
|
|
// 이전 결과 필드의 calculated 속성 제거
|
|
if (oldRule.result) {
|
|
const oldResultIndex = columns.findIndex((c) => c.field === oldRule.result);
|
|
if (oldResultIndex !== -1) {
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const { calculated, ...rest } = columns[oldResultIndex];
|
|
columns[oldResultIndex] = { ...rest, editable: true };
|
|
}
|
|
}
|
|
|
|
// 새 결과 필드를 calculated=true, editable=false로 설정
|
|
if (updates.result) {
|
|
const newResultIndex = columns.findIndex((c) => c.field === updates.result);
|
|
if (newResultIndex !== -1) {
|
|
columns[newResultIndex] = {
|
|
...columns[newResultIndex],
|
|
calculated: true,
|
|
editable: false,
|
|
};
|
|
}
|
|
}
|
|
|
|
rules[index] = newRule;
|
|
updateConfig({ calculationRules: rules, columns });
|
|
return;
|
|
}
|
|
|
|
rules[index] = newRule;
|
|
updateConfig({ calculationRules: rules });
|
|
};
|
|
|
|
const removeCalculationRule = (index: number) => {
|
|
const rules = [...(localConfig.calculationRules || [])];
|
|
const removedRule = rules[index];
|
|
|
|
// 결과 필드의 calculated 속성 제거
|
|
if (removedRule.result) {
|
|
const columns = [...(localConfig.columns || [])];
|
|
const resultIndex = columns.findIndex((c) => c.field === removedRule.result);
|
|
if (resultIndex !== -1) {
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const { calculated, ...rest } = columns[resultIndex];
|
|
columns[resultIndex] = { ...rest, editable: true };
|
|
}
|
|
rules.splice(index, 1);
|
|
updateConfig({ calculationRules: rules, columns });
|
|
return;
|
|
}
|
|
|
|
rules.splice(index, 1);
|
|
updateConfig({ calculationRules: rules });
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6 p-4">
|
|
{/* 소스/저장 테이블 설정 */}
|
|
<div className="space-y-4 border rounded-lg p-4 bg-card">
|
|
<div>
|
|
<h3 className="text-sm font-semibold mb-1">1. 소스 테이블 (데이터 검색)</h3>
|
|
<p className="text-xs text-muted-foreground">
|
|
항목 검색 모달에서 어떤 테이블의 어떤 컬럼 정보를 보여줄 건지 설정
|
|
</p>
|
|
</div>
|
|
|
|
{/* 소스 테이블 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs sm:text-sm">소스 테이블 *</Label>
|
|
<Popover open={openTableCombo} onOpenChange={setOpenTableCombo}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={openTableCombo}
|
|
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
|
disabled={isLoadingTables}
|
|
>
|
|
{localConfig.sourceTable
|
|
? allTables.find((t) => t.tableName === localConfig.sourceTable)?.displayName || localConfig.sourceTable
|
|
: isLoadingTables ? "로딩 중..." : "테이블 선택"}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
|
<Command>
|
|
<CommandInput placeholder="테이블 검색..." className="text-xs sm:text-sm" />
|
|
<CommandList>
|
|
<CommandEmpty className="text-xs sm:text-sm">테이블을 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup>
|
|
{allTables.map((table) => (
|
|
<CommandItem
|
|
key={table.tableName}
|
|
value={table.tableName}
|
|
onSelect={() => {
|
|
updateConfig({ sourceTable: table.tableName });
|
|
setOpenTableCombo(false);
|
|
}}
|
|
className="text-xs sm:text-sm"
|
|
>
|
|
<Check className={cn("mr-2 h-4 w-4", localConfig.sourceTable === table.tableName ? "opacity-100" : "opacity-0")} />
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{table.displayName || table.tableName}</span>
|
|
{table.displayName && <span className="text-[10px] text-gray-500">{table.tableName}</span>}
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<p className="text-xs text-muted-foreground">
|
|
모달에서 검색할 데이터 테이블
|
|
</p>
|
|
</div>
|
|
|
|
<div className="border-t pt-4 mt-4">
|
|
<h3 className="text-sm font-semibold mb-1">2. 저장 테이블 (데이터 저장)</h3>
|
|
<p className="text-xs text-muted-foreground mb-2">
|
|
반복 테이블에 입력된 정보를 어떤 테이블에 저장할 건지 선택
|
|
</p>
|
|
</div>
|
|
|
|
{/* 저장 테이블 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs sm:text-sm">저장 테이블 *</Label>
|
|
<Popover open={openTargetTableCombo} onOpenChange={setOpenTargetTableCombo}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={openTargetTableCombo}
|
|
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
|
disabled={isLoadingTables}
|
|
>
|
|
{localConfig.targetTable
|
|
? allTables.find((t) => t.tableName === localConfig.targetTable)?.displayName || localConfig.targetTable
|
|
: isLoadingTables ? "로딩 중..." : "저장할 테이블 선택"}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
|
<Command>
|
|
<CommandInput placeholder="테이블 검색..." className="text-xs sm:text-sm" />
|
|
<CommandList>
|
|
<CommandEmpty className="text-xs sm:text-sm">테이블을 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup>
|
|
{allTables.map((table) => (
|
|
<CommandItem
|
|
key={table.tableName}
|
|
value={table.tableName}
|
|
onSelect={() => {
|
|
updateConfig({ targetTable: table.tableName });
|
|
setOpenTargetTableCombo(false);
|
|
}}
|
|
className="text-xs sm:text-sm"
|
|
>
|
|
<Check className={cn("mr-2 h-4 w-4", localConfig.targetTable === table.tableName ? "opacity-100" : "opacity-0")} />
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{table.displayName || table.tableName}</span>
|
|
{table.displayName && <span className="text-[10px] text-gray-500">{table.tableName}</span>}
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<p className="text-xs text-muted-foreground">
|
|
반복 테이블 데이터를 저장할 대상 테이블
|
|
</p>
|
|
</div>
|
|
|
|
{/* 소스 컬럼 */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs sm:text-sm">소스 컬럼</Label>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={addSourceColumn}
|
|
className="h-7 text-xs"
|
|
disabled={!localConfig.sourceTable || isLoadingColumns}
|
|
>
|
|
<Plus className="h-3 w-3 mr-1" />
|
|
추가
|
|
</Button>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
모달 테이블에 표시할 컬럼들
|
|
</p>
|
|
<div className="space-y-2">
|
|
{(localConfig.sourceColumns || []).map((column, index) => (
|
|
<div key={index} className="flex items-center gap-2">
|
|
<Select
|
|
value={column}
|
|
onValueChange={(value) => updateSourceColumn(index, value)}
|
|
disabled={!localConfig.sourceTable || isLoadingColumns}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm flex-1">
|
|
<SelectValue placeholder="컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{tableColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => removeSourceColumn(index)}
|
|
className="h-8 w-8 p-0"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 검색 필드 */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs sm:text-sm">검색 필드</Label>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={addSearchField}
|
|
className="h-7 text-xs"
|
|
disabled={!localConfig.sourceTable || isLoadingColumns}
|
|
>
|
|
<Plus className="h-3 w-3 mr-1" />
|
|
추가
|
|
</Button>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
모달에서 검색 가능한 필드들
|
|
</p>
|
|
<div className="space-y-2">
|
|
{(localConfig.sourceSearchFields || []).map((field, index) => (
|
|
<div key={index} className="flex items-center gap-2">
|
|
<Select
|
|
value={field}
|
|
onValueChange={(value) => updateSearchField(index, value)}
|
|
disabled={!localConfig.sourceTable || isLoadingColumns}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm flex-1">
|
|
<SelectValue placeholder="필드 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{tableColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => removeSearchField(index)}
|
|
className="h-8 w-8 p-0"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 중복 체크 필드 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs sm:text-sm">중복 체크 필드</Label>
|
|
<Popover open={openUniqueFieldCombo} onOpenChange={setOpenUniqueFieldCombo}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={openUniqueFieldCombo}
|
|
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
|
disabled={!localConfig.sourceTable || isLoadingColumns}
|
|
>
|
|
{localConfig.uniqueField
|
|
? tableColumns.find((c) => c.columnName === localConfig.uniqueField)?.displayName || localConfig.uniqueField
|
|
: isLoadingColumns ? "로딩 중..." : "필드 선택"}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
|
<Command>
|
|
<CommandInput placeholder="필드 검색..." className="text-xs sm:text-sm" />
|
|
<CommandList>
|
|
<CommandEmpty className="text-xs sm:text-sm">필드를 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup>
|
|
{tableColumns.map((column) => (
|
|
<CommandItem
|
|
key={column.columnName}
|
|
value={column.columnName}
|
|
onSelect={() => {
|
|
updateConfig({ uniqueField: column.columnName });
|
|
setOpenUniqueFieldCombo(false);
|
|
}}
|
|
className="text-xs sm:text-sm"
|
|
>
|
|
<Check className={cn("mr-2 h-4 w-4", localConfig.uniqueField === column.columnName ? "opacity-100" : "opacity-0")} />
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{column.displayName || column.columnName}</span>
|
|
{column.displayName && <span className="text-[10px] text-gray-500">{column.columnName}</span>}
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<p className="text-xs text-muted-foreground">
|
|
중복 추가를 방지할 고유 필드 (예: 품목 코드)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 모달 설정 */}
|
|
<div className="space-y-4 border rounded-lg p-4">
|
|
<h3 className="text-sm font-semibold">모달 설정</h3>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-xs sm:text-sm">모달 제목</Label>
|
|
<Input
|
|
value={localConfig.modalTitle || ""}
|
|
onChange={(e) => updateConfig({ modalTitle: e.target.value })}
|
|
placeholder="항목 검색 및 선택"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-xs sm:text-sm">모달 버튼 텍스트</Label>
|
|
<Input
|
|
value={localConfig.modalButtonText || ""}
|
|
onChange={(e) => updateConfig({ modalButtonText: e.target.value })}
|
|
placeholder="항목 검색"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs sm:text-sm">다중 선택</Label>
|
|
<Switch
|
|
checked={localConfig.multiSelect ?? true}
|
|
onCheckedChange={(checked) =>
|
|
updateConfig({ multiSelect: checked })
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 반복 테이블 컬럼 관리 */}
|
|
<div className="space-y-4 border rounded-lg p-4 bg-card">
|
|
<div>
|
|
<h3 className="text-sm font-semibold mb-1">3. 반복 테이블 컬럼 (헤더 구성)</h3>
|
|
<p className="text-xs text-muted-foreground">
|
|
반복 테이블 헤더에 표시할 컬럼 옵션 - 어떤 컬럼을 화면에 보여줄지 선택
|
|
</p>
|
|
</div>
|
|
|
|
{/* 저장 테이블 컬럼 선택 */}
|
|
{localConfig.targetTable && targetTableColumns.length > 0 && (
|
|
<div className="space-y-2">
|
|
<Label className="text-xs sm:text-sm">저장 테이블 컬럼 선택</Label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{targetTableColumns.map((col) => (
|
|
<Button
|
|
key={col.columnName}
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => addRepeaterColumnFromTarget(col.columnName)}
|
|
className="h-7 text-xs"
|
|
disabled={isLoadingTargetColumns}
|
|
>
|
|
<Plus className="h-3 w-3 mr-1" />
|
|
{col.displayName || col.columnName}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 추가된 컬럼 목록 */}
|
|
{localConfig.columns && localConfig.columns.length > 0 && (
|
|
<div className="space-y-3">
|
|
<Label className="text-xs sm:text-sm">추가된 컬럼</Label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{localConfig.columns.map((col, index) => (
|
|
<div
|
|
key={index}
|
|
className="inline-flex items-center gap-2 px-3 py-1.5 bg-primary/10 text-primary rounded-md text-xs"
|
|
>
|
|
<span className="font-medium">{col.label}</span>
|
|
<span className="text-[10px] opacity-70">
|
|
({col.mapping?.type === "source" ? "소스" : col.mapping?.type === "reference" ? "참조" : "수동입력"})
|
|
</span>
|
|
<button
|
|
onClick={() => removeRepeaterColumn(index)}
|
|
className="ml-1 hover:bg-primary/20 rounded p-0.5"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* 개별 컬럼 설정 - 세로 레이아웃 */}
|
|
<div className="space-y-4">
|
|
{localConfig.columns.map((col, index) => (
|
|
<div key={index} className="border rounded-lg p-4 space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h4 className="text-sm font-semibold">{col.label}</h4>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => removeRepeaterColumn(index)}
|
|
className="h-7 w-7 p-0"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 세로 레이아웃: 모든 항목 동일한 너비/높이 */}
|
|
<div className="grid grid-cols-1 gap-4">
|
|
{/* 1. 품목명 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium text-muted-foreground">
|
|
품목명
|
|
</Label>
|
|
<Input
|
|
value={col.label}
|
|
onChange={(e) => updateRepeaterColumn(index, { label: e.target.value })}
|
|
placeholder="화면에 표시될 이름"
|
|
className="h-10 text-sm w-full"
|
|
/>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
반복 테이블 헤더에 표시될 이름
|
|
</p>
|
|
</div>
|
|
|
|
{/* 2. 컬럼명 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium text-muted-foreground">
|
|
컬럼명
|
|
</Label>
|
|
<Input
|
|
value={col.field}
|
|
onChange={(e) => updateRepeaterColumn(index, { field: e.target.value })}
|
|
placeholder="데이터베이스 필드명"
|
|
className="h-10 text-sm w-full"
|
|
/>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
저장 테이블의 실제 컬럼명
|
|
</p>
|
|
</div>
|
|
|
|
{/* 3. 컬럼 타입 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium text-muted-foreground">
|
|
컬럼 타입
|
|
</Label>
|
|
<Select
|
|
value={col.type || "text"}
|
|
onValueChange={(value) => updateRepeaterColumn(index, { type: value as "text" | "number" | "date" | "select" })}
|
|
>
|
|
<SelectTrigger className="h-10 text-sm w-full">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="text">텍스트</SelectItem>
|
|
<SelectItem value="number">숫자</SelectItem>
|
|
<SelectItem value="date">날짜</SelectItem>
|
|
<SelectItem value="select">선택</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
입력 필드 타입
|
|
</p>
|
|
</div>
|
|
|
|
{/* 4. 매핑 설정 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium text-muted-foreground">
|
|
매핑 설정
|
|
</Label>
|
|
<Select
|
|
value={col.mapping?.type || "manual"}
|
|
onValueChange={(value) => updateRepeaterColumn(index, {
|
|
mapping: {
|
|
...col.mapping,
|
|
type: value as "source" | "reference" | "manual"
|
|
}
|
|
})}
|
|
>
|
|
<SelectTrigger className="h-10 text-sm w-full">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="source">소스 테이블에서 복사</SelectItem>
|
|
<SelectItem value="reference">외부 테이블 참조</SelectItem>
|
|
<SelectItem value="manual">화면에서 입력 (수동)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
{col.mapping?.type === "source" && "모달에서 선택한 항목의 값을 가져옴"}
|
|
{col.mapping?.type === "reference" && "다른 테이블의 값을 참조"}
|
|
{col.mapping?.type === "manual" && "사용자가 직접 입력"}
|
|
</p>
|
|
</div>
|
|
|
|
{/* 5. 소스 정보 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium text-muted-foreground">
|
|
소스 정보
|
|
</Label>
|
|
|
|
{col.mapping?.type === "source" && (
|
|
<div className="space-y-3">
|
|
<div className="p-3 bg-blue-50 dark:bg-blue-950 rounded-md border border-blue-200 dark:border-blue-800">
|
|
<p className="text-xs font-medium mb-2">소스 테이블에서 복사</p>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
모달 검색에서 선택한 항목(소스 테이블)의 컬럼 값을 가져옴
|
|
</p>
|
|
</div>
|
|
|
|
{/* 소스 테이블 선택 */}
|
|
<div className="space-y-1.5">
|
|
<Label className="text-[11px]">소스 테이블 선택 *</Label>
|
|
<Select
|
|
value={col.mapping?.referenceTable || localConfig.sourceTable || ""}
|
|
onValueChange={(value) => updateRepeaterColumn(index, {
|
|
mapping: {
|
|
...col.mapping,
|
|
type: "source",
|
|
referenceTable: value,
|
|
}
|
|
})}
|
|
>
|
|
<SelectTrigger className="h-10 text-sm w-full">
|
|
<SelectValue placeholder="테이블 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{allTables.map((table) => (
|
|
<SelectItem key={table.tableName} value={table.tableName}>
|
|
{table.displayName || table.tableName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
값을 가져올 소스 테이블
|
|
</p>
|
|
</div>
|
|
|
|
{/* 소스 컬럼 선택 */}
|
|
<div className="space-y-1.5">
|
|
<Label className="text-[11px]">소스 컬럼 선택 *</Label>
|
|
<SourceColumnSelector
|
|
sourceTable={col.mapping?.referenceTable || localConfig.sourceTable || ""}
|
|
value={col.mapping?.sourceField || ""}
|
|
onChange={(value) => updateRepeaterColumn(index, {
|
|
mapping: {
|
|
...col.mapping,
|
|
type: "source",
|
|
sourceField: value
|
|
} as ColumnMapping
|
|
})}
|
|
/>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
가져올 컬럼명
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{col.mapping?.type === "reference" && (
|
|
<div className="space-y-3">
|
|
<div className="p-3 bg-purple-50 dark:bg-purple-950 rounded-md border border-purple-200 dark:border-purple-800">
|
|
<p className="text-xs font-medium mb-2">외부 테이블 참조</p>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
다른 테이블의 컬럼 값을 조인하여 가져옴 (조인 조건 필요)
|
|
</p>
|
|
</div>
|
|
|
|
{/* 참조 테이블 선택 */}
|
|
<div className="space-y-1.5">
|
|
<Label className="text-[11px]">참조 테이블 선택 *</Label>
|
|
<Select
|
|
value={col.mapping?.referenceTable || ""}
|
|
onValueChange={(value) => updateRepeaterColumn(index, {
|
|
mapping: {
|
|
...col.mapping,
|
|
type: "reference",
|
|
referenceTable: value,
|
|
}
|
|
})}
|
|
>
|
|
<SelectTrigger className="h-10 text-sm w-full">
|
|
<SelectValue placeholder="테이블 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{allTables.map((table) => (
|
|
<SelectItem key={table.tableName} value={table.tableName}>
|
|
{table.displayName || table.tableName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
참조할 외부 테이블
|
|
</p>
|
|
</div>
|
|
|
|
{/* 참조 컬럼 선택 */}
|
|
<div className="space-y-1.5">
|
|
<Label className="text-[11px]">참조 컬럼 선택 *</Label>
|
|
<ReferenceColumnSelector
|
|
referenceTable={col.mapping?.referenceTable || ""}
|
|
value={col.mapping?.referenceField || ""}
|
|
onChange={(value) => updateRepeaterColumn(index, {
|
|
mapping: {
|
|
...col.mapping,
|
|
type: "reference",
|
|
referenceField: value
|
|
} as ColumnMapping
|
|
})}
|
|
/>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
가져올 컬럼명
|
|
</p>
|
|
</div>
|
|
|
|
{/* 조인 조건 설정 */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-[11px]">조인 조건 *</Label>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => {
|
|
const currentConditions = col.mapping?.joinCondition || [];
|
|
updateRepeaterColumn(index, {
|
|
mapping: {
|
|
...col.mapping,
|
|
type: "reference",
|
|
joinCondition: [
|
|
...currentConditions,
|
|
{ sourceField: "", targetField: "", operator: "=" }
|
|
]
|
|
} as ColumnMapping
|
|
});
|
|
}}
|
|
className="h-6 text-[10px] px-2"
|
|
>
|
|
<Plus className="h-3 w-3 mr-1" />
|
|
조인 조건 추가
|
|
</Button>
|
|
</div>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
소스 테이블과 참조 테이블을 어떻게 매칭할지 설정
|
|
</p>
|
|
|
|
{/* 조인 조건 목록 */}
|
|
<div className="space-y-2">
|
|
{(col.mapping?.joinCondition || []).map((condition, condIndex) => (
|
|
<div key={condIndex} className="p-3 border rounded-md bg-background space-y-2">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-[10px] font-medium text-muted-foreground">
|
|
조인 조건 {condIndex + 1}
|
|
</span>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => {
|
|
const currentConditions = [...(col.mapping?.joinCondition || [])];
|
|
currentConditions.splice(condIndex, 1);
|
|
updateRepeaterColumn(index, {
|
|
mapping: {
|
|
...col.mapping,
|
|
type: "reference",
|
|
joinCondition: currentConditions
|
|
} as ColumnMapping
|
|
});
|
|
}}
|
|
className="h-5 w-5 p-0"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 소스 테이블 선택 */}
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px] text-muted-foreground">소스 테이블 (어느 테이블)</Label>
|
|
<Select
|
|
value={condition.sourceTable || "target"}
|
|
onValueChange={(value) => {
|
|
const currentConditions = [...(col.mapping?.joinCondition || [])];
|
|
currentConditions[condIndex] = {
|
|
...currentConditions[condIndex],
|
|
sourceTable: value as "source" | "target"
|
|
};
|
|
updateRepeaterColumn(index, {
|
|
mapping: {
|
|
...col.mapping,
|
|
type: "reference",
|
|
joinCondition: currentConditions
|
|
} as ColumnMapping
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-9 text-xs w-full">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="target">
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">저장 테이블 (권장)</span>
|
|
<span className="text-[10px] text-muted-foreground">
|
|
{localConfig.targetTable || "sales_order_mng"}
|
|
</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="source">
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">소스 테이블</span>
|
|
<span className="text-[10px] text-muted-foreground">
|
|
{localConfig.sourceTable || "item_info"}
|
|
</span>
|
|
</div>
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
반복 테이블 = 저장 테이블 컬럼 사용
|
|
</p>
|
|
</div>
|
|
|
|
{/* 소스 필드 */}
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px] text-muted-foreground">소스 필드 (어느 컬럼)</Label>
|
|
<Select
|
|
value={condition.sourceField || ""}
|
|
onValueChange={(value) => {
|
|
const currentConditions = [...(col.mapping?.joinCondition || [])];
|
|
currentConditions[condIndex] = {
|
|
...currentConditions[condIndex],
|
|
sourceField: value
|
|
};
|
|
updateRepeaterColumn(index, {
|
|
mapping: {
|
|
...col.mapping,
|
|
type: "reference",
|
|
joinCondition: currentConditions
|
|
} as ColumnMapping
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-9 text-xs w-full">
|
|
<SelectValue placeholder="컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{/* 저장 테이블 선택 시 */}
|
|
{(!condition.sourceTable || condition.sourceTable === "target") && (
|
|
<>
|
|
{/* 반복 테이블에 추가된 컬럼 */}
|
|
{(localConfig.columns || []).length > 0 && (
|
|
<>
|
|
<div className="px-2 py-1.5 text-[10px] font-semibold text-muted-foreground">
|
|
반복 테이블 컬럼 (권장)
|
|
</div>
|
|
{localConfig.columns
|
|
.filter((c) => c.field !== col.field)
|
|
.map((repeaterCol) => (
|
|
<SelectItem key={`repeater-${repeaterCol.field}`} value={repeaterCol.field}>
|
|
<span className="font-medium">{repeaterCol.label}</span>
|
|
<span className="text-[10px] text-muted-foreground ml-1">({repeaterCol.field})</span>
|
|
</SelectItem>
|
|
))}
|
|
</>
|
|
)}
|
|
|
|
{/* 저장 테이블의 모든 컬럼 */}
|
|
{targetTableColumns.length > 0 && (
|
|
<>
|
|
<div className="px-2 py-1.5 text-[10px] font-semibold text-muted-foreground border-t mt-1 pt-2">
|
|
저장 테이블 전체 컬럼 ({localConfig.targetTable || "sales_order_mng"})
|
|
</div>
|
|
{targetTableColumns.map((targetCol) => (
|
|
<SelectItem key={`target-${targetCol.columnName}`} value={targetCol.columnName}>
|
|
{targetCol.displayName || targetCol.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* 소스 테이블 선택 시 */}
|
|
{condition.sourceTable === "source" && (
|
|
<>
|
|
{tableColumns.length > 0 && (
|
|
<>
|
|
<div className="px-2 py-1.5 text-[10px] font-semibold text-muted-foreground">
|
|
소스 테이블 컬럼 ({localConfig.sourceTable || "item_info"})
|
|
</div>
|
|
{tableColumns.map((sourceCol) => (
|
|
<SelectItem key={`source-${sourceCol.columnName}`} value={sourceCol.columnName}>
|
|
{sourceCol.displayName || sourceCol.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
{(!condition.sourceTable || condition.sourceTable === "target")
|
|
? "반복 테이블에 이미 추가된 컬럼"
|
|
: "모달에서 선택한 원본 데이터의 컬럼"}
|
|
</p>
|
|
</div>
|
|
|
|
{/* 연산자 */}
|
|
<div className="flex items-center justify-center">
|
|
<Select
|
|
value={condition.operator || "="}
|
|
onValueChange={(value) => {
|
|
const currentConditions = [...(col.mapping?.joinCondition || [])];
|
|
currentConditions[condIndex] = {
|
|
...currentConditions[condIndex],
|
|
operator: value as "=" | "!=" | ">" | "<" | ">=" | "<="
|
|
};
|
|
updateRepeaterColumn(index, {
|
|
mapping: {
|
|
...col.mapping,
|
|
type: "reference",
|
|
joinCondition: currentConditions
|
|
} as ColumnMapping
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-9 text-xs w-20">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="=">=</SelectItem>
|
|
<SelectItem value="!=">!=</SelectItem>
|
|
<SelectItem value=">">></SelectItem>
|
|
<SelectItem value="<"><</SelectItem>
|
|
<SelectItem value=">=">>=</SelectItem>
|
|
<SelectItem value="<="><=</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 대상 필드 */}
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px] text-muted-foreground">대상 필드 (참조 테이블)</Label>
|
|
<ReferenceColumnSelector
|
|
referenceTable={col.mapping?.referenceTable || ""}
|
|
value={condition.targetField || ""}
|
|
onChange={(value) => {
|
|
const currentConditions = [...(col.mapping?.joinCondition || [])];
|
|
currentConditions[condIndex] = {
|
|
...currentConditions[condIndex],
|
|
targetField: value
|
|
};
|
|
updateRepeaterColumn(index, {
|
|
mapping: {
|
|
...col.mapping,
|
|
type: "reference",
|
|
joinCondition: currentConditions
|
|
} as ColumnMapping
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* 조인 조건 미리보기 */}
|
|
{condition.sourceField && condition.targetField && (
|
|
<div className="mt-2 p-2 bg-muted/50 rounded text-[10px] font-mono">
|
|
<span className="text-blue-600 dark:text-blue-400">
|
|
{condition.sourceTable === "source"
|
|
? localConfig.sourceTable
|
|
: localConfig.targetTable || "저장테이블"}
|
|
</span>
|
|
<span className="text-muted-foreground">.{condition.sourceField}</span>
|
|
<span className="mx-2 text-purple-600 dark:text-purple-400">{condition.operator || "="}</span>
|
|
<span className="text-purple-600 dark:text-purple-400">{col.mapping?.referenceTable}</span>
|
|
<span className="text-muted-foreground">.{condition.targetField}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
{/* 조인 조건 없을 때 안내 */}
|
|
{(!col.mapping?.joinCondition || col.mapping.joinCondition.length === 0) && (
|
|
<div className="p-4 border-2 border-dashed rounded-lg text-center">
|
|
<p className="text-xs text-muted-foreground mb-2">
|
|
조인 조건이 없습니다
|
|
</p>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
"조인 조건 추가" 버튼을 클릭하여 매칭 조건을 설정하세요
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 조인 조건 예시 */}
|
|
{col.mapping?.referenceTable && (
|
|
<div className="p-3 bg-blue-50 dark:bg-blue-950 rounded-md border border-blue-200 dark:border-blue-800">
|
|
<p className="text-xs font-medium mb-2">조인 조건 예시</p>
|
|
<div className="space-y-1 text-[10px] text-muted-foreground">
|
|
<p>예) 거래처별 품목 단가 조회:</p>
|
|
<p className="ml-2 font-mono">• item_code = item_code</p>
|
|
<p className="ml-2 font-mono">• customer_code = customer_code</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{col.mapping?.type === "manual" && (
|
|
<div className="p-3 bg-green-50 dark:bg-green-950 rounded-md border border-green-200 dark:border-green-800">
|
|
<p className="text-xs font-medium mb-2">화면에서 입력 (수동)</p>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
사용자가 반복 테이블에서 직접 입력
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 계산 규칙 (자동 계산) */}
|
|
<div className="space-y-4 border rounded-lg p-4 bg-card">
|
|
<div>
|
|
<h3 className="text-sm font-semibold mb-1">4. 계산 규칙 (자동 계산)</h3>
|
|
<p className="text-xs text-muted-foreground">
|
|
반복 테이블 컬럼들을 조합하여 자동 계산 (예: 수량 x 단가 = 금액)
|
|
</p>
|
|
</div>
|
|
|
|
<div className="p-3 bg-amber-50 dark:bg-amber-950 rounded-md border border-amber-200 dark:border-amber-800">
|
|
<p className="text-xs font-medium mb-1">중요 안내</p>
|
|
<ul className="text-[10px] text-muted-foreground space-y-1">
|
|
<li>• <strong className="text-primary">결과 필드는 수정 불가</strong> (자동 계산됨)</li>
|
|
<li>• 계산 공식에 사용된 컬럼은 수정 가능</li>
|
|
<li>• 예시: order_qty * unit_price = total_amount</li>
|
|
</ul>
|
|
</div>
|
|
|
|
{localConfig.calculationRules && localConfig.calculationRules.length > 0 ? (
|
|
<div className="space-y-4">
|
|
{localConfig.calculationRules.map((rule, index) => (
|
|
<div key={index} className="border rounded-lg p-4 space-y-4 bg-gradient-to-br from-card to-muted/20">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-6 h-6 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold">
|
|
{index + 1}
|
|
</div>
|
|
<h4 className="text-sm font-semibold">계산 규칙 {index + 1}</h4>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => removeCalculationRule(index)}
|
|
className="h-7 w-7 p-0 hover:bg-destructive/10 hover:text-destructive"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-4">
|
|
{/* 결과 필드 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium text-muted-foreground">
|
|
결과 필드 (계산된 값이 저장될 컬럼) *
|
|
</Label>
|
|
<Select
|
|
value={rule.result || ""}
|
|
onValueChange={(value) => updateCalculationRule(index, { result: value })}
|
|
>
|
|
<SelectTrigger className="h-10 text-sm w-full">
|
|
<SelectValue placeholder="결과 컬럼 선택">
|
|
{rule.result
|
|
? localConfig.columns?.find((c) => c.field === rule.result)?.label || rule.result
|
|
: "결과 컬럼 선택"}
|
|
</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{(localConfig.columns || []).map((col) => (
|
|
<SelectItem key={col.field} value={col.field}>
|
|
{col.label} ({col.field})
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
이 컬럼은 자동 계산되어 수정 불가능합니다
|
|
</p>
|
|
</div>
|
|
|
|
{/* 계산 공식 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium text-muted-foreground">
|
|
계산 공식 (JavaScript 표현식) *
|
|
</Label>
|
|
<Input
|
|
value={rule.formula || ""}
|
|
onChange={(e) => updateCalculationRule(index, { formula: e.target.value })}
|
|
placeholder="예: order_qty * unit_price"
|
|
className="h-10 text-sm w-full font-mono"
|
|
/>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
사용 가능한 연산자: +, -, *, /, (), 필드명 사용
|
|
</p>
|
|
</div>
|
|
|
|
{/* 현재 설정 표시 */}
|
|
{rule.result && rule.formula && (
|
|
<div className="p-3 bg-primary/5 border border-primary/20 rounded-md">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<div className="w-1 h-4 bg-primary rounded-full"></div>
|
|
<p className="text-xs font-semibold text-primary">현재 설정</p>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<span className="font-bold text-primary">
|
|
{localConfig.columns?.find((c) => c.field === rule.result)?.label || rule.result}
|
|
</span>
|
|
<span className="text-muted-foreground">=</span>
|
|
<code className="px-2 py-1 bg-muted rounded text-xs font-mono">
|
|
{rule.formula}
|
|
</code>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8 border-2 border-dashed rounded-lg">
|
|
<div className="w-12 h-12 mx-auto mb-3 rounded-full bg-muted flex items-center justify-center">
|
|
<Plus className="h-6 w-6 text-muted-foreground" />
|
|
</div>
|
|
<p className="text-sm font-medium mb-1">계산 규칙이 없습니다</p>
|
|
<p className="text-xs text-muted-foreground mb-4">
|
|
자동 계산이 필요한 컬럼이 있다면 규칙을 추가하세요
|
|
</p>
|
|
<Button size="sm" variant="outline" onClick={addCalculationRule}>
|
|
<Plus className="h-3 w-3 mr-1" />
|
|
첫 번째 규칙 추가
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{localConfig.calculationRules && localConfig.calculationRules.length > 0 && (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={addCalculationRule}
|
|
className="w-full"
|
|
>
|
|
<Plus className="h-3 w-3 mr-1" />
|
|
계산 규칙 추가
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* 설정 순서 안내 */}
|
|
<div className="p-4 bg-muted rounded-md text-xs text-muted-foreground">
|
|
<p className="font-medium mb-2">설정 순서:</p>
|
|
<ol className="space-y-1 list-decimal list-inside">
|
|
<li><strong>소스 테이블</strong> (검색용): 모달에서 검색할 데이터 테이블</li>
|
|
<li><strong>저장 테이블</strong> (저장용): 선택한 항목을 저장할 테이블</li>
|
|
<li><strong>반복 테이블 컬럼</strong>: 저장 테이블의 컬럼 선택 → 화면에 표시</li>
|
|
<li><strong>컬럼 매핑 설정</strong>: 각 컬럼의 초기값을 어디서 가져올지 설정</li>
|
|
<li><strong>계산 규칙</strong>: 자동 계산이 필요한 컬럼 설정 (선택사항)</li>
|
|
</ol>
|
|
|
|
<div className="mt-4 pt-4 border-t border-border">
|
|
<p className="font-medium mb-2">실제 사용 예시:</p>
|
|
<ul className="space-y-1.5 text-[10px]">
|
|
<li><strong>수주 등록 화면</strong></li>
|
|
<li className="ml-4">- 소스 테이블: <code className="bg-background px-1 py-0.5 rounded">item_info</code> (품목 검색)</li>
|
|
<li className="ml-4">- 저장 테이블: <code className="bg-background px-1 py-0.5 rounded">sales_order_mng</code> (수주 저장)</li>
|
|
<li className="ml-4">- 컬럼: part_name, quantity, unit_price, amount...</li>
|
|
<li className="ml-4">- 매핑: item_name → part_name (소스에서 복사)</li>
|
|
<li className="ml-4">- 계산: <code className="bg-background px-1 py-0.5 rounded">amount = quantity * unit_price</code></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|