ERP-node/frontend/lib/registry/components/universal-form-modal/modals/SaveSettingsModal.tsx

797 lines
38 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { Plus, Trash2, Database, Layers } from "lucide-react";
import { cn } from "@/lib/utils";
import { SaveConfig, SubTableSaveConfig, SubTableFieldMapping, FormSectionConfig, FormFieldConfig } from "../types";
// 도움말 텍스트 컴포넌트
const HelpText = ({ children }: { children: React.ReactNode }) => (
<p className="text-[10px] text-muted-foreground mt-0.5">{children}</p>
);
interface SaveSettingsModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
saveConfig: SaveConfig;
sections: FormSectionConfig[];
onSave: (updates: SaveConfig) => void;
tables: { name: string; label: string }[];
tableColumns: { [tableName: string]: { name: string; type: string; label: string }[] };
onLoadTableColumns: (tableName: string) => void;
}
export function SaveSettingsModal({
open,
onOpenChange,
saveConfig,
sections,
onSave,
tables,
tableColumns,
onLoadTableColumns,
}: SaveSettingsModalProps) {
// 로컬 상태로 저장 설정 관리
const [localSaveConfig, setLocalSaveConfig] = useState<SaveConfig>(saveConfig);
// 저장 모드 (단일 테이블 vs 다중 테이블)
const [saveMode, setSaveMode] = useState<"single" | "multi">(
saveConfig.customApiSave?.enabled && saveConfig.customApiSave?.multiTable?.enabled ? "multi" : "single"
);
// open이 변경될 때마다 데이터 동기화
useEffect(() => {
if (open) {
setLocalSaveConfig(saveConfig);
setSaveMode(saveConfig.customApiSave?.enabled && saveConfig.customApiSave?.multiTable?.enabled ? "multi" : "single");
}
}, [open, saveConfig]);
// 저장 설정 업데이트 함수
const updateSaveConfig = (updates: Partial<SaveConfig>) => {
setLocalSaveConfig((prev) => ({ ...prev, ...updates }));
};
// 저장 함수
const handleSave = () => {
// 저장 모드에 따라 설정 조정
let finalConfig = { ...localSaveConfig };
if (saveMode === "single") {
// 단일 테이블 모드: customApiSave 비활성화
finalConfig = {
...finalConfig,
customApiSave: {
enabled: false,
apiType: "custom",
},
};
} else {
// 다중 테이블 모드: customApiSave 활성화
finalConfig = {
...finalConfig,
customApiSave: {
...finalConfig.customApiSave,
enabled: true,
apiType: "multi-table",
multiTable: {
...finalConfig.customApiSave?.multiTable,
enabled: true,
},
},
};
}
onSave(finalConfig);
onOpenChange(false);
};
// 서브 테이블 추가
const addSubTable = () => {
const newSubTable: SubTableSaveConfig = {
enabled: true,
tableName: "",
repeatSectionId: "",
linkColumn: {
mainField: "",
subColumn: "",
},
fieldMappings: [],
};
const subTables = [...(localSaveConfig.customApiSave?.multiTable?.subTables || []), newSubTable];
updateSaveConfig({
customApiSave: {
...localSaveConfig.customApiSave,
apiType: "multi-table",
multiTable: {
...localSaveConfig.customApiSave?.multiTable,
enabled: true,
subTables,
},
},
});
};
// 서브 테이블 삭제
const removeSubTable = (index: number) => {
const subTables = [...(localSaveConfig.customApiSave?.multiTable?.subTables || [])];
subTables.splice(index, 1);
updateSaveConfig({
customApiSave: {
...localSaveConfig.customApiSave,
multiTable: {
...localSaveConfig.customApiSave?.multiTable,
subTables,
},
},
});
};
// 서브 테이블 업데이트
const updateSubTable = (index: number, updates: Partial<SubTableSaveConfig>) => {
const subTables = [...(localSaveConfig.customApiSave?.multiTable?.subTables || [])];
subTables[index] = { ...subTables[index], ...updates };
updateSaveConfig({
customApiSave: {
...localSaveConfig.customApiSave,
multiTable: {
...localSaveConfig.customApiSave?.multiTable,
subTables,
},
},
});
};
// 필드 매핑 추가
const addFieldMapping = (subTableIndex: number) => {
const newMapping: SubTableFieldMapping = {
formField: "",
targetColumn: "",
};
const subTables = [...(localSaveConfig.customApiSave?.multiTable?.subTables || [])];
const fieldMappings = [...(subTables[subTableIndex].fieldMappings || []), newMapping];
subTables[subTableIndex] = { ...subTables[subTableIndex], fieldMappings };
updateSaveConfig({
customApiSave: {
...localSaveConfig.customApiSave,
multiTable: {
...localSaveConfig.customApiSave?.multiTable,
subTables,
},
},
});
};
// 필드 매핑 삭제
const removeFieldMapping = (subTableIndex: number, mappingIndex: number) => {
const subTables = [...(localSaveConfig.customApiSave?.multiTable?.subTables || [])];
const fieldMappings = [...(subTables[subTableIndex].fieldMappings || [])];
fieldMappings.splice(mappingIndex, 1);
subTables[subTableIndex] = { ...subTables[subTableIndex], fieldMappings };
updateSaveConfig({
customApiSave: {
...localSaveConfig.customApiSave,
multiTable: {
...localSaveConfig.customApiSave?.multiTable,
subTables,
},
},
});
};
// 필드 매핑 업데이트
const updateFieldMapping = (subTableIndex: number, mappingIndex: number, updates: Partial<SubTableFieldMapping>) => {
const subTables = [...(localSaveConfig.customApiSave?.multiTable?.subTables || [])];
const fieldMappings = [...(subTables[subTableIndex].fieldMappings || [])];
fieldMappings[mappingIndex] = { ...fieldMappings[mappingIndex], ...updates };
subTables[subTableIndex] = { ...subTables[subTableIndex], fieldMappings };
updateSaveConfig({
customApiSave: {
...localSaveConfig.customApiSave,
multiTable: {
...localSaveConfig.customApiSave?.multiTable,
subTables,
},
},
});
};
// 메인 테이블 컬럼 목록
const mainTableColumns = localSaveConfig.customApiSave?.multiTable?.mainTable?.tableName
? tableColumns[localSaveConfig.customApiSave.multiTable.mainTable.tableName] || []
: [];
// 반복 섹션 목록
const repeatSections = sections.filter((s) => s.repeatable);
// 모든 필드 목록 (반복 섹션 포함)
const getAllFields = (): { columnName: string; label: string; sectionTitle: string }[] => {
const fields: { columnName: string; label: string; sectionTitle: string }[] = [];
sections.forEach((section) => {
section.fields.forEach((field) => {
fields.push({
columnName: field.columnName,
label: field.label,
sectionTitle: section.title,
});
});
});
return fields;
};
const allFields = getAllFields();
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[95vw] sm:max-w-[800px] max-h-[90vh] flex flex-col p-0">
<DialogHeader className="px-4 pt-4 pb-2 border-b shrink-0">
<DialogTitle className="text-base"> </DialogTitle>
<DialogDescription className="text-xs">
.
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-hidden px-4">
<ScrollArea className="h-[calc(90vh-180px)]">
<div className="space-y-4 py-3 pr-3">
{/* 저장 모드 선택 */}
<div className="space-y-3 border rounded-lg p-3 bg-card">
<Label className="text-xs font-semibold"> </Label>
<RadioGroup value={saveMode} onValueChange={(value) => setSaveMode(value as "single" | "multi")}>
<div className="flex items-center space-x-2">
<RadioGroupItem value="single" id="mode-single" />
<Label htmlFor="mode-single" className="text-[10px] cursor-pointer">
</Label>
</div>
<HelpText> ( )</HelpText>
<div className="flex items-center space-x-2 pt-2">
<RadioGroupItem value="multi" id="mode-multi" />
<Label htmlFor="mode-multi" className="text-[10px] cursor-pointer">
</Label>
</div>
<HelpText>
+
<br />
: 주문(orders) + (order_items), (user_info) + (user_dept)
</HelpText>
</RadioGroup>
</div>
{/* 단일 테이블 저장 설정 */}
{saveMode === "single" && (
<div className="space-y-3 border rounded-lg p-3 bg-card">
<div className="flex items-center gap-2">
<Database className="h-4 w-4 text-blue-600" />
<h3 className="text-xs font-semibold"> </h3>
</div>
<div>
<Label className="text-[10px]"> </Label>
<Select
value={localSaveConfig.tableName || ""}
onValueChange={(value) => {
updateSaveConfig({ tableName: value });
onLoadTableColumns(value);
}}
>
<SelectTrigger className="h-7 text-xs mt-1">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{tables.map((t) => (
<SelectItem key={t.name} value={t.name}>
{t.label || t.name}
</SelectItem>
))}
</SelectContent>
</Select>
<HelpText> </HelpText>
</div>
<div>
<Label className="text-[10px]"> (Primary Key)</Label>
<Input
value={localSaveConfig.primaryKeyColumn || ""}
onChange={(e) => updateSaveConfig({ primaryKeyColumn: e.target.value })}
placeholder="id"
className="h-7 text-xs mt-1"
/>
<HelpText>
<br />
: id, user_id, order_id
</HelpText>
</div>
</div>
)}
{/* 다중 테이블 저장 설정 */}
{saveMode === "multi" && (
<div className="space-y-3">
{/* 메인 테이블 설정 */}
<div className="border rounded-lg p-3 bg-card space-y-3">
<div className="flex items-center gap-2">
<Database className="h-4 w-4 text-blue-600" />
<h3 className="text-xs font-semibold"> </h3>
</div>
<div>
<Label className="text-[10px]"> </Label>
<Select
value={localSaveConfig.customApiSave?.multiTable?.mainTable?.tableName || ""}
onValueChange={(value) => {
updateSaveConfig({
customApiSave: {
...localSaveConfig.customApiSave,
apiType: "multi-table",
multiTable: {
...localSaveConfig.customApiSave?.multiTable,
enabled: true,
mainTable: {
...localSaveConfig.customApiSave?.multiTable?.mainTable,
tableName: value,
},
},
},
});
onLoadTableColumns(value);
}}
>
<SelectTrigger className="h-7 text-xs mt-1">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{tables.map((t) => (
<SelectItem key={t.name} value={t.name}>
{t.label || t.name}
</SelectItem>
))}
</SelectContent>
</Select>
<HelpText> (: orders, user_info)</HelpText>
</div>
<div>
<Label className="text-[10px]"> </Label>
{mainTableColumns.length > 0 ? (
<Select
value={localSaveConfig.customApiSave?.multiTable?.mainTable?.primaryKeyColumn || ""}
onValueChange={(value) =>
updateSaveConfig({
customApiSave: {
...localSaveConfig.customApiSave,
multiTable: {
...localSaveConfig.customApiSave?.multiTable,
mainTable: {
...localSaveConfig.customApiSave?.multiTable?.mainTable,
primaryKeyColumn: value,
},
},
},
})
}
>
<SelectTrigger className="h-7 text-xs mt-1">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{mainTableColumns.map((col) => (
<SelectItem key={col.name} value={col.name}>
{col.name}
{col.label !== col.name && ` (${col.label})`}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={localSaveConfig.customApiSave?.multiTable?.mainTable?.primaryKeyColumn || ""}
onChange={(e) =>
updateSaveConfig({
customApiSave: {
...localSaveConfig.customApiSave,
multiTable: {
...localSaveConfig.customApiSave?.multiTable,
mainTable: {
...localSaveConfig.customApiSave?.multiTable?.mainTable,
primaryKeyColumn: e.target.value,
},
},
},
})
}
placeholder="id"
className="h-7 text-xs mt-1"
/>
)}
<HelpText> (: order_id, user_id)</HelpText>
</div>
</div>
{/* 서브 테이블 목록 */}
<div className="border rounded-lg p-3 bg-card space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Layers className="h-4 w-4 text-orange-600" />
<h3 className="text-xs font-semibold"> </h3>
<span className="text-[9px] text-muted-foreground">
({(localSaveConfig.customApiSave?.multiTable?.subTables || []).length})
</span>
</div>
<Button size="sm" variant="outline" onClick={addSubTable} className="h-6 text-[9px] px-2">
<Plus className="h-3 w-3 mr-1" />
</Button>
</div>
<HelpText>
.
<br />
: 주문상세(order_items), (user_dept)
</HelpText>
{(localSaveConfig.customApiSave?.multiTable?.subTables || []).length === 0 ? (
<div className="text-center py-6 border border-dashed rounded-lg">
<p className="text-[10px] text-muted-foreground mb-1"> </p>
<p className="text-[9px] text-muted-foreground"> "서브 테이블 추가" </p>
</div>
) : (
<div className="space-y-3 pt-2">
{(localSaveConfig.customApiSave?.multiTable?.subTables || []).map((subTable, subIndex) => {
const subTableColumns = subTable.tableName ? tableColumns[subTable.tableName] || [] : [];
return (
<Accordion key={subIndex} type="single" collapsible>
<AccordionItem value={`sub-${subIndex}`} className="border rounded-lg bg-orange-50/30">
<AccordionTrigger className="px-3 py-2 text-xs hover:no-underline">
<div className="flex items-center justify-between flex-1">
<div className="flex items-center gap-2">
<span className="font-medium">
{subIndex + 1}: {subTable.tableName || "(미설정)"}
</span>
<span className="text-[9px] text-muted-foreground">
({subTable.fieldMappings?.length || 0} )
</span>
</div>
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation();
removeSubTable(subIndex);
}}
className="h-5 w-5 p-0 text-destructive hover:text-destructive mr-2"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</AccordionTrigger>
<AccordionContent className="px-3 pb-3 space-y-3">
<div>
<Label className="text-[10px]"> </Label>
<Select
value={subTable.tableName || ""}
onValueChange={(value) => {
updateSubTable(subIndex, { tableName: value });
onLoadTableColumns(value);
}}
>
<SelectTrigger className="h-7 text-xs mt-1">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{tables.map((t) => (
<SelectItem key={t.name} value={t.name}>
{t.label || t.name}
</SelectItem>
))}
</SelectContent>
</Select>
<HelpText> </HelpText>
</div>
<div>
<Label className="text-[10px]"> </Label>
<Select
value={subTable.repeatSectionId || ""}
onValueChange={(value) => updateSubTable(subIndex, { repeatSectionId: value })}
>
<SelectTrigger className="h-7 text-xs mt-1">
<SelectValue placeholder="섹션 선택" />
</SelectTrigger>
<SelectContent>
{repeatSections.length === 0 ? (
<div className="px-2 py-1.5 text-xs text-muted-foreground">
</div>
) : (
repeatSections.map((section) => (
<SelectItem key={section.id} value={section.id}>
{section.title}
</SelectItem>
))
)}
</SelectContent>
</Select>
<HelpText> </HelpText>
</div>
<Separator />
<div className="space-y-2">
<Label className="text-[10px] font-medium"> </Label>
<HelpText> </HelpText>
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="text-[9px]"> </Label>
{mainTableColumns.length > 0 ? (
<Select
value={subTable.linkColumn?.mainField || ""}
onValueChange={(value) =>
updateSubTable(subIndex, {
linkColumn: { ...subTable.linkColumn, mainField: value },
})
}
>
<SelectTrigger className="h-6 text-[9px] mt-0.5">
<SelectValue placeholder="필드 선택" />
</SelectTrigger>
<SelectContent>
{mainTableColumns.map((col) => (
<SelectItem key={col.name} value={col.name}>
{col.name}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={subTable.linkColumn?.mainField || ""}
onChange={(e) =>
updateSubTable(subIndex, {
linkColumn: { ...subTable.linkColumn, mainField: e.target.value },
})
}
placeholder="order_id"
className="h-6 text-[9px] mt-0.5"
/>
)}
</div>
<div>
<Label className="text-[9px]"> </Label>
{subTableColumns.length > 0 ? (
<Select
value={subTable.linkColumn?.subColumn || ""}
onValueChange={(value) =>
updateSubTable(subIndex, {
linkColumn: { ...subTable.linkColumn, subColumn: value },
})
}
>
<SelectTrigger className="h-6 text-[9px] mt-0.5">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{subTableColumns.map((col) => (
<SelectItem key={col.name} value={col.name}>
{col.name}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={subTable.linkColumn?.subColumn || ""}
onChange={(e) =>
updateSubTable(subIndex, {
linkColumn: { ...subTable.linkColumn, subColumn: e.target.value },
})
}
placeholder="order_id"
className="h-6 text-[9px] mt-0.5"
/>
)}
</div>
</div>
</div>
<Separator />
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-[10px] font-medium"> </Label>
<Button
size="sm"
variant="outline"
onClick={() => addFieldMapping(subIndex)}
className="h-5 text-[8px] px-1.5"
>
<Plus className="h-2.5 w-2.5 mr-0.5" />
</Button>
</div>
<HelpText> </HelpText>
{(subTable.fieldMappings || []).length === 0 ? (
<div className="text-center py-3 border border-dashed rounded-lg">
<p className="text-[9px] text-muted-foreground"> </p>
</div>
) : (
<div className="space-y-2">
{(subTable.fieldMappings || []).map((mapping, mapIndex) => (
<div key={mapIndex} className="border rounded-lg p-2 bg-white space-y-1.5">
<div className="flex items-center justify-between">
<span className="text-[8px] font-medium text-muted-foreground">
{mapIndex + 1}
</span>
<Button
size="sm"
variant="ghost"
onClick={() => removeFieldMapping(subIndex, mapIndex)}
className="h-4 w-4 p-0 text-destructive"
>
<Trash2 className="h-2.5 w-2.5" />
</Button>
</div>
<div>
<Label className="text-[8px]"> </Label>
<Select
value={mapping.formField || ""}
onValueChange={(value) =>
updateFieldMapping(subIndex, mapIndex, { formField: value })
}
>
<SelectTrigger className="h-5 text-[8px] mt-0.5">
<SelectValue placeholder="필드 선택" />
</SelectTrigger>
<SelectContent>
{allFields.map((field) => (
<SelectItem key={field.columnName} value={field.columnName}>
{field.label} ({field.sectionTitle})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="text-center text-[8px] text-muted-foreground"></div>
<div>
<Label className="text-[8px]"> </Label>
{subTableColumns.length > 0 ? (
<Select
value={mapping.targetColumn || ""}
onValueChange={(value) =>
updateFieldMapping(subIndex, mapIndex, { targetColumn: value })
}
>
<SelectTrigger className="h-5 text-[8px] mt-0.5">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{subTableColumns.map((col) => (
<SelectItem key={col.name} value={col.name}>
{col.name}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={mapping.targetColumn || ""}
onChange={(e) =>
updateFieldMapping(subIndex, mapIndex, {
targetColumn: e.target.value,
})
}
placeholder="item_name"
className="h-5 text-[8px] mt-0.5"
/>
)}
</div>
</div>
))}
</div>
)}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
);
})}
</div>
)}
</div>
</div>
)}
{/* 저장 후 동작 */}
<div className="space-y-2 border rounded-lg p-3 bg-card">
<h3 className="text-xs font-semibold"> </h3>
<div className="flex items-center justify-between">
<span className="text-[10px]"> </span>
<Switch
checked={localSaveConfig.afterSave?.showToast !== false}
onCheckedChange={(checked) =>
updateSaveConfig({
afterSave: {
...localSaveConfig.afterSave,
showToast: checked,
},
})
}
/>
</div>
<HelpText> "저장되었습니다" </HelpText>
<Separator className="my-2" />
<div className="flex items-center justify-between">
<span className="text-[10px]"> </span>
<Switch
checked={localSaveConfig.afterSave?.closeModal !== false}
onCheckedChange={(checked) =>
updateSaveConfig({
afterSave: {
...localSaveConfig.afterSave,
closeModal: checked,
},
})
}
/>
</div>
<HelpText> </HelpText>
<Separator className="my-2" />
<div className="flex items-center justify-between">
<span className="text-[10px]"> </span>
<Switch
checked={localSaveConfig.afterSave?.refreshParent !== false}
onCheckedChange={(checked) =>
updateSaveConfig({
afterSave: {
...localSaveConfig.afterSave,
refreshParent: checked,
},
})
}
/>
</div>
<HelpText> </HelpText>
</div>
</div>
</ScrollArea>
</div>
<DialogFooter className="px-4 py-3 border-t shrink-0">
<Button variant="outline" onClick={() => onOpenChange(false)} className="h-9 text-sm">
</Button>
<Button onClick={handleSave} className="h-9 text-sm">
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}