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

1814 lines
103 KiB
TypeScript
Raw Normal View History

"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 { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Plus, Trash2, Database, Layers, Info, Check, ChevronsUpDown } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import {
SaveConfig,
SubTableSaveConfig,
SubTableFieldMapping,
FormSectionConfig,
FormFieldConfig,
SectionSaveMode,
} from "../types";
// 도움말 텍스트 컴포넌트
const HelpText = ({ children }: { children: React.ReactNode }) => (
<p className="text-muted-foreground mt-0.5 text-[10px]">{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",
);
// 테이블 검색 Popover 상태
const [singleTableSearchOpen, setSingleTableSearchOpen] = useState(false);
const [mainTableSearchOpen, setMainTableSearchOpen] = useState(false);
const [subTableSearchOpen, setSubTableSearchOpen] = useState<Record<number, boolean>>({});
// 컬럼 검색 Popover 상태
const [mainKeyColumnSearchOpen, setMainKeyColumnSearchOpen] = useState(false);
const [mainFieldSearchOpen, setMainFieldSearchOpen] = useState<Record<number, boolean>>({});
const [subColumnSearchOpen, setSubColumnSearchOpen] = useState<Record<number, boolean>>({});
const [subTableColumnSearchOpen, setSubTableColumnSearchOpen] = useState<Record<string, boolean>>({});
const [markerColumnSearchOpen, setMarkerColumnSearchOpen] = useState<Record<number, boolean>>({});
// open이 변경될 때마다 데이터 동기화
useEffect(() => {
if (open) {
setLocalSaveConfig(saveConfig);
setSaveMode(
saveConfig.customApiSave?.enabled && saveConfig.customApiSave?.multiTable?.enabled ? "multi" : "single",
);
// 모달이 열릴 때 기존에 설정된 테이블들의 컬럼 정보 로드
const mainTableName = saveConfig.customApiSave?.multiTable?.mainTable?.tableName;
if (mainTableName && !tableColumns[mainTableName]) {
onLoadTableColumns(mainTableName);
}
// 서브 테이블들의 컬럼 정보도 로드
const subTables = saveConfig.customApiSave?.multiTable?.subTables || [];
subTables.forEach((subTable) => {
if (subTable.tableName && !tableColumns[subTable.tableName]) {
onLoadTableColumns(subTable.tableName);
}
});
}
}, [open, saveConfig, tableColumns, onLoadTableColumns]);
// 저장 설정 업데이트 함수
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; sectionId: string }[] => {
const fields: { columnName: string; label: string; sectionTitle: string; sectionId: string }[] = [];
sections.forEach((section) => {
// 필드 타입 섹션만 처리 (테이블 타입은 fields가 undefined)
if (section.fields && Array.isArray(section.fields)) {
section.fields.forEach((field) => {
fields.push({
columnName: field.columnName,
label: field.label,
sectionTitle: section.title,
sectionId: section.id,
});
});
}
});
return fields;
};
const allFields = getAllFields();
// 섹션별 저장 방식 조회 (없으면 기본값 반환)
const getSectionSaveMode = (sectionId: string, sectionType: "fields" | "table"): "common" | "individual" => {
const sectionMode = localSaveConfig.sectionSaveModes?.find((s) => s.sectionId === sectionId);
if (sectionMode) {
return sectionMode.saveMode;
}
// 기본값: fields 타입은 공통 저장, table 타입은 개별 저장
return sectionType === "fields" ? "common" : "individual";
};
// 필드별 저장 방식 조회 (오버라이드 확인)
const getFieldSaveMode = (
sectionId: string,
fieldName: string,
sectionType: "fields" | "table",
): "common" | "individual" => {
const sectionMode = localSaveConfig.sectionSaveModes?.find((s) => s.sectionId === sectionId);
if (sectionMode) {
// 필드별 오버라이드 확인
const fieldOverride = sectionMode.fieldOverrides?.find((f) => f.fieldName === fieldName);
if (fieldOverride) {
return fieldOverride.saveMode;
}
return sectionMode.saveMode;
}
// 기본값
return sectionType === "fields" ? "common" : "individual";
};
// 섹션별 저장 방식 업데이트
const updateSectionSaveMode = (sectionId: string, mode: "common" | "individual") => {
const currentModes = localSaveConfig.sectionSaveModes || [];
const existingIndex = currentModes.findIndex((s) => s.sectionId === sectionId);
let newModes: SectionSaveMode[];
if (existingIndex >= 0) {
newModes = [...currentModes];
newModes[existingIndex] = { ...newModes[existingIndex], saveMode: mode };
} else {
newModes = [...currentModes, { sectionId, saveMode: mode }];
}
updateSaveConfig({ sectionSaveModes: newModes });
};
// 필드별 오버라이드 토글
const toggleFieldOverride = (sectionId: string, fieldName: string, sectionType: "fields" | "table") => {
const currentModes = localSaveConfig.sectionSaveModes || [];
const sectionIndex = currentModes.findIndex((s) => s.sectionId === sectionId);
// 섹션 설정이 없으면 먼저 생성
let newModes = [...currentModes];
if (sectionIndex < 0) {
const defaultMode = sectionType === "fields" ? "common" : "individual";
newModes.push({ sectionId, saveMode: defaultMode, fieldOverrides: [] });
}
const targetIndex = newModes.findIndex((s) => s.sectionId === sectionId);
const sectionMode = newModes[targetIndex];
const currentFieldOverrides = sectionMode.fieldOverrides || [];
const fieldOverrideIndex = currentFieldOverrides.findIndex((f) => f.fieldName === fieldName);
let newFieldOverrides;
if (fieldOverrideIndex >= 0) {
// 이미 오버라이드가 있으면 제거 (섹션 기본값으로 돌아감)
newFieldOverrides = currentFieldOverrides.filter((f) => f.fieldName !== fieldName);
} else {
// 오버라이드 추가 (섹션 기본값의 반대)
const oppositeMode = sectionMode.saveMode === "common" ? "individual" : "common";
newFieldOverrides = [...currentFieldOverrides, { fieldName, saveMode: oppositeMode }];
}
newModes[targetIndex] = { ...sectionMode, fieldOverrides: newFieldOverrides };
updateSaveConfig({ sectionSaveModes: newModes });
};
// 섹션의 필드 목록 가져오기
const getSectionFields = (section: FormSectionConfig): { fieldName: string; label: string }[] => {
if (section.type === "table" && section.tableConfig) {
// 테이블 타입: tableConfig.columns에서 필드 목록 가져오기
return (section.tableConfig.columns || []).map((col) => ({
fieldName: col.field,
label: col.label,
}));
} else if (section.fields) {
// 필드 타입: fields에서 목록 가져오기
return section.fields.map((field) => ({
fieldName: field.columnName,
label: field.label,
}));
}
return [];
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="flex max-h-[90vh] max-w-[95vw] flex-col p-0 sm:max-w-[800px]">
<DialogHeader className="shrink-0 border-b px-4 pt-4 pb-2">
<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="bg-card space-y-3 rounded-lg border p-3">
<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="cursor-pointer text-[10px]">
</Label>
</div>
<HelpText>
1 .
<br />
: 사원 , ,
</HelpText>
<div className="flex items-center space-x-2 pt-2">
<RadioGroupItem value="multi" id="mode-multi" />
<Label htmlFor="mode-multi" className="cursor-pointer text-[10px]">
</Label>
</div>
<HelpText>
. ( )
<br />
테이블: 폼의
<br />
테이블: 필드 ( )
<br />
: 사원+(user_info+user_dept), +(orders+order_items)
</HelpText>
</RadioGroup>
</div>
{/* 단일 테이블 저장 설정 */}
{saveMode === "single" && (
<div className="bg-card space-y-3 rounded-lg border p-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>
<Popover open={singleTableSearchOpen} onOpenChange={setSingleTableSearchOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={singleTableSearchOpen}
className="mt-1 h-8 w-full justify-between text-xs font-normal"
>
{localSaveConfig.tableName ? (
<div className="flex flex-col items-start text-left">
<span className="font-medium">{localSaveConfig.tableName}</span>
{(() => {
const tableLabel = tables.find((t) => t.name === localSaveConfig.tableName)?.label;
return tableLabel && tableLabel !== localSaveConfig.tableName ? (
<span className="text-muted-foreground text-[10px]">{tableLabel}</span>
) : null;
})()}
</div>
) : (
<span className="text-muted-foreground"> ...</span>
)}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0" align="start">
<Command>
<CommandInput placeholder="테이블 검색..." className="text-xs" />
<CommandList className="max-h-[250px]">
<CommandEmpty className="py-4 text-center text-xs"> .</CommandEmpty>
<CommandGroup>
{tables.map((t) => (
<CommandItem
key={t.name}
value={`${t.name} ${t.label}`}
onSelect={() => {
updateSaveConfig({ tableName: t.name });
onLoadTableColumns(t.name);
setSingleTableSearchOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
localSaveConfig.tableName === t.name ? "opacity-100" : "opacity-0",
)}
/>
<div className="flex flex-col">
<span className="font-medium">{t.name}</span>
{t.label && <span className="text-muted-foreground text-[10px]">{t.label}</span>}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<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="mt-1 h-7 text-xs"
/>
<HelpText>
<br />
: id, user_id, order_id
</HelpText>
</div>
</div>
)}
{/* 다중 테이블 저장 설정 */}
{saveMode === "multi" && (
<div className="space-y-3">
{/* 메인 테이블 설정 */}
<div className="bg-card space-y-3 rounded-lg border p-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>
<Popover open={mainTableSearchOpen} onOpenChange={setMainTableSearchOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={mainTableSearchOpen}
className="mt-1 h-8 w-full justify-between text-xs font-normal"
>
{localSaveConfig.customApiSave?.multiTable?.mainTable?.tableName ? (
<div className="flex flex-col items-start text-left">
<span className="font-medium">
{localSaveConfig.customApiSave.multiTable.mainTable.tableName}
</span>
{(() => {
const tableLabel = tables.find(
(t) => t.name === localSaveConfig.customApiSave?.multiTable?.mainTable?.tableName,
)?.label;
return tableLabel &&
tableLabel !== localSaveConfig.customApiSave?.multiTable?.mainTable?.tableName ? (
<span className="text-muted-foreground text-[10px]">{tableLabel}</span>
) : null;
})()}
</div>
) : (
<span className="text-muted-foreground"> ...</span>
)}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0" align="start">
<Command>
<CommandInput placeholder="테이블 검색..." className="text-xs" />
<CommandList className="max-h-[250px]">
<CommandEmpty className="py-4 text-center text-xs">
.
</CommandEmpty>
<CommandGroup>
{tables.map((t) => (
<CommandItem
key={t.name}
value={`${t.name} ${t.label}`}
onSelect={() => {
updateSaveConfig({
customApiSave: {
...localSaveConfig.customApiSave,
apiType: "multi-table",
multiTable: {
...localSaveConfig.customApiSave?.multiTable,
enabled: true,
mainTable: {
...localSaveConfig.customApiSave?.multiTable?.mainTable,
tableName: t.name,
},
},
},
});
onLoadTableColumns(t.name);
setMainTableSearchOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
localSaveConfig.customApiSave?.multiTable?.mainTable?.tableName === t.name
? "opacity-100"
: "opacity-0",
)}
/>
<div className="flex flex-col">
<span className="font-medium">{t.name}</span>
{t.label && <span className="text-muted-foreground text-[10px]">{t.label}</span>}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<HelpText> (: orders, user_info)</HelpText>
</div>
<div>
<Label className="text-[10px]"> </Label>
{mainTableColumns.length > 0 ? (
<Popover open={mainKeyColumnSearchOpen} onOpenChange={setMainKeyColumnSearchOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={mainKeyColumnSearchOpen}
className="mt-1 h-7 w-full justify-between text-xs font-normal"
>
{localSaveConfig.customApiSave?.multiTable?.mainTable?.primaryKeyColumn ? (
<>
{localSaveConfig.customApiSave.multiTable.mainTable.primaryKeyColumn}
{(() => {
const col = mainTableColumns.find(
(c) =>
c.name ===
localSaveConfig.customApiSave?.multiTable?.mainTable?.primaryKeyColumn,
);
return col?.label && col.label !== col.name ? ` (${col.label})` : "";
})()}
</>
) : (
<span className="text-muted-foreground"> </span>
)}
<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-3 text-center text-xs">
.
</CommandEmpty>
<CommandGroup>
{mainTableColumns.map((col) => (
<CommandItem
key={col.name}
value={`${col.name} ${col.label || ""}`}
onSelect={() => {
updateSaveConfig({
customApiSave: {
...localSaveConfig.customApiSave,
multiTable: {
...localSaveConfig.customApiSave?.multiTable,
mainTable: {
...localSaveConfig.customApiSave?.multiTable?.mainTable,
primaryKeyColumn: col.name,
},
},
},
});
setMainKeyColumnSearchOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
localSaveConfig.customApiSave?.multiTable?.mainTable?.primaryKeyColumn ===
col.name
? "opacity-100"
: "opacity-0",
)}
/>
<div className="flex flex-col">
<span className="font-medium">{col.name}</span>
{col.label && col.label !== col.name && (
<span className="text-muted-foreground text-[10px]">{col.label}</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
) : (
<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="mt-1 h-7 text-xs"
/>
)}
<HelpText> (: order_id, user_id)</HelpText>
</div>
</div>
{/* 서브 테이블 목록 */}
<div className="bg-card space-y-3 rounded-lg border p-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-muted-foreground text-[9px]">
({(localSaveConfig.customApiSave?.multiTable?.subTables || []).length})
</span>
</div>
<Button size="sm" variant="outline" onClick={addSubTable} className="h-6 px-2 text-[9px]">
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
<HelpText>
.
<br />
(: user_id) .
<br />
.
</HelpText>
{(localSaveConfig.customApiSave?.multiTable?.subTables || []).length === 0 ? (
<div className="rounded-lg border border-dashed py-6 text-center">
<p className="text-muted-foreground mb-1 text-[10px]"> </p>
<p className="text-muted-foreground text-[9px]"> "서브 테이블 추가" </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="rounded-lg border bg-orange-50/30">
<div className="flex items-center justify-between px-3 py-2">
<AccordionTrigger className="flex-1 p-0 text-xs hover:no-underline">
<div className="flex items-center gap-2">
<span className="font-medium">
{subIndex + 1}: {subTable.tableName || "(미설정)"}
</span>
<span className="text-muted-foreground text-[9px]">
({subTable.fieldMappings?.length || 0} )
</span>
</div>
</AccordionTrigger>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
removeSubTable(subIndex);
}}
className="text-destructive hover:text-destructive/80 ml-2 inline-flex h-5 w-5 items-center justify-center p-0"
>
<Trash2 className="h-3 w-3" />
</button>
</div>
<AccordionContent className="space-y-3 px-3 pb-3">
<div>
<Label className="text-[10px]"> </Label>
<Popover
open={subTableSearchOpen[subIndex] || false}
onOpenChange={(open) =>
setSubTableSearchOpen((prev) => ({ ...prev, [subIndex]: open }))
}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={subTableSearchOpen[subIndex] || false}
className="mt-1 h-8 w-full justify-between text-xs font-normal"
>
{subTable.tableName ? (
<div className="flex flex-col items-start text-left">
<span className="font-medium">{subTable.tableName}</span>
{(() => {
const tableLabel = tables.find(
(t) => t.name === subTable.tableName,
)?.label;
return tableLabel && tableLabel !== subTable.tableName ? (
<span className="text-muted-foreground text-[10px]">
{tableLabel}
</span>
) : null;
})()}
</div>
) : (
<span className="text-muted-foreground"> ...</span>
)}
<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="text-xs" />
<CommandList className="max-h-[200px]">
<CommandEmpty className="py-4 text-center text-xs">
.
</CommandEmpty>
<CommandGroup>
{tables.map((t) => (
<CommandItem
key={t.name}
value={`${t.name} ${t.label}`}
onSelect={() => {
updateSubTable(subIndex, { tableName: t.name });
onLoadTableColumns(t.name);
setSubTableSearchOpen((prev) => ({ ...prev, [subIndex]: false }));
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
subTable.tableName === t.name ? "opacity-100" : "opacity-0",
)}
/>
<div className="flex flex-col">
<span className="font-medium">{t.name}</span>
{t.label && (
<span className="text-muted-foreground text-[10px]">
{t.label}
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<HelpText> </HelpText>
</div>
<div>
<Label className="text-[10px]"> ()</Label>
<Select
value={subTable.repeatSectionId || ""}
onValueChange={(value) => updateSubTable(subIndex, { repeatSectionId: value })}
>
<SelectTrigger className="mt-1 h-7 text-xs">
<SelectValue placeholder="섹션 선택 (없으면 필드 매핑만 사용)" />
</SelectTrigger>
<SelectContent>
{repeatSections.length === 0 ? (
<div className="text-muted-foreground px-2 py-1.5 text-xs">
</div>
) : (
repeatSections.map((section) => (
<SelectItem key={section.id} value={section.id}>
{section.title}
</SelectItem>
))
)}
</SelectContent>
</Select>
<HelpText>
섹션: / (: 주문 )
<br />
.
<br />
1 .
</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 ? (
<Popover
open={mainFieldSearchOpen[subIndex] || false}
onOpenChange={(open) =>
setMainFieldSearchOpen((prev) => ({ ...prev, [subIndex]: open }))
}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={mainFieldSearchOpen[subIndex] || false}
className="mt-0.5 h-6 w-full justify-between text-[9px] font-normal"
>
{subTable.linkColumn?.mainField ? (
<>
{subTable.linkColumn.mainField}
{(() => {
const col = mainTableColumns.find(
(c) => c.name === subTable.linkColumn?.mainField,
);
return col?.label && col.label !== col.name
? ` (${col.label})`
: "";
})()}
</>
) : (
<span className="text-muted-foreground"> </span>
)}
<ChevronsUpDown className="ml-1 h-2.5 w-2.5 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[220px] p-0" align="start">
<Command>
<CommandInput
placeholder="컬럼명/라벨 검색..."
className="h-7 text-xs"
/>
<CommandList className="max-h-[180px]">
<CommandEmpty className="py-2 text-center text-[10px]">
.
</CommandEmpty>
<CommandGroup>
{mainTableColumns.map((col) => (
<CommandItem
key={col.name}
value={`${col.name} ${col.label || ""}`}
onSelect={() => {
updateSubTable(subIndex, {
linkColumn: { ...subTable.linkColumn, mainField: col.name },
});
setMainFieldSearchOpen((prev) => ({
...prev,
[subIndex]: false,
}));
}}
className="text-[10px]"
>
<Check
className={cn(
"mr-2 h-3 w-3",
subTable.linkColumn?.mainField === col.name
? "opacity-100"
: "opacity-0",
)}
/>
<div className="flex flex-col">
<span className="font-medium">{col.name}</span>
{col.label && col.label !== col.name && (
<span className="text-muted-foreground text-[9px]">
{col.label}
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
) : (
<Input
value={subTable.linkColumn?.mainField || ""}
onChange={(e) =>
updateSubTable(subIndex, {
linkColumn: { ...subTable.linkColumn, mainField: e.target.value },
})
}
placeholder="order_id"
className="mt-0.5 h-6 text-[9px]"
/>
)}
</div>
<div>
<Label className="text-[9px]"> </Label>
{subTableColumns.length > 0 ? (
<Popover
open={subColumnSearchOpen[subIndex] || false}
onOpenChange={(open) =>
setSubColumnSearchOpen((prev) => ({ ...prev, [subIndex]: open }))
}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={subColumnSearchOpen[subIndex] || false}
className="mt-0.5 h-6 w-full justify-between text-[9px] font-normal"
>
{subTable.linkColumn?.subColumn ? (
<>
{subTable.linkColumn.subColumn}
{(() => {
const col = subTableColumns.find(
(c) => c.name === subTable.linkColumn?.subColumn,
);
return col?.label && col.label !== col.name
? ` (${col.label})`
: "";
})()}
</>
) : (
<span className="text-muted-foreground"> </span>
)}
<ChevronsUpDown className="ml-1 h-2.5 w-2.5 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[220px] p-0" align="start">
<Command>
<CommandInput
placeholder="컬럼명/라벨 검색..."
className="h-7 text-xs"
/>
<CommandList className="max-h-[180px]">
<CommandEmpty className="py-2 text-center text-[10px]">
.
</CommandEmpty>
<CommandGroup>
{subTableColumns.map((col) => (
<CommandItem
key={col.name}
value={`${col.name} ${col.label || ""}`}
onSelect={() => {
updateSubTable(subIndex, {
linkColumn: { ...subTable.linkColumn, subColumn: col.name },
});
setSubColumnSearchOpen((prev) => ({
...prev,
[subIndex]: false,
}));
}}
className="text-[10px]"
>
<Check
className={cn(
"mr-2 h-3 w-3",
subTable.linkColumn?.subColumn === col.name
? "opacity-100"
: "opacity-0",
)}
/>
<div className="flex flex-col">
<span className="font-medium">{col.name}</span>
{col.label && col.label !== col.name && (
<span className="text-muted-foreground text-[9px]">
{col.label}
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
) : (
<Input
value={subTable.linkColumn?.subColumn || ""}
onChange={(e) =>
updateSubTable(subIndex, {
linkColumn: { ...subTable.linkColumn, subColumn: e.target.value },
})
}
placeholder="order_id"
className="mt-0.5 h-6 text-[9px]"
/>
)}
</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 px-1.5 text-[8px]"
>
<Plus className="mr-0.5 h-2.5 w-2.5" />
</Button>
</div>
<HelpText> </HelpText>
{(subTable.fieldMappings || []).length === 0 ? (
<div className="rounded-lg border border-dashed py-3 text-center">
<p className="text-muted-foreground text-[9px]"> </p>
</div>
) : (
<div className="space-y-2">
{(subTable.fieldMappings || []).map((mapping, mapIndex) => (
<div key={mapIndex} className="space-y-1.5 rounded-lg border bg-white p-2">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-[8px] font-medium">
{mapIndex + 1}
</span>
<Button
size="sm"
variant="ghost"
onClick={() => removeFieldMapping(subIndex, mapIndex)}
className="text-destructive h-4 w-4 p-0"
>
<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="mt-0.5 h-5 text-[8px]">
<SelectValue placeholder="필드 선택" />
</SelectTrigger>
<SelectContent>
{allFields.map((field, fieldIndex) => (
<SelectItem
key={`${field.sectionId}-${field.columnName}-${fieldIndex}`}
value={field.columnName}
>
{field.label} ({field.sectionTitle})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="text-muted-foreground text-center text-[8px]"></div>
<div>
<Label className="text-[8px]"> </Label>
{subTableColumns.length > 0 ? (
<Popover
open={subTableColumnSearchOpen[`${subIndex}-${mapIndex}`] || false}
onOpenChange={(open) =>
setSubTableColumnSearchOpen((prev) => ({
...prev,
[`${subIndex}-${mapIndex}`]: open,
}))
}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={
subTableColumnSearchOpen[`${subIndex}-${mapIndex}`] || false
}
className="mt-0.5 h-5 w-full justify-between text-[8px] font-normal"
>
{mapping.targetColumn ? (
<>
{mapping.targetColumn}
{(() => {
const col = subTableColumns.find(
(c) => c.name === mapping.targetColumn,
);
return col?.label && col.label !== col.name
? ` (${col.label})`
: "";
})()}
</>
) : (
<span className="text-muted-foreground"> </span>
)}
<ChevronsUpDown className="ml-1 h-2 w-2 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandInput
placeholder="컬럼명/라벨 검색..."
className="h-7 text-xs"
/>
<CommandList className="max-h-[180px]">
<CommandEmpty className="py-2 text-center text-[10px]">
.
</CommandEmpty>
<CommandGroup>
{subTableColumns.map((col) => (
<CommandItem
key={col.name}
value={`${col.name} ${col.label || ""}`}
onSelect={() => {
updateFieldMapping(subIndex, mapIndex, {
targetColumn: col.name,
});
setSubTableColumnSearchOpen((prev) => ({
...prev,
[`${subIndex}-${mapIndex}`]: false,
}));
}}
className="text-[10px]"
>
<Check
className={cn(
"mr-2 h-3 w-3",
mapping.targetColumn === col.name
? "opacity-100"
: "opacity-0",
)}
/>
<div className="flex flex-col">
<span className="font-medium">{col.name}</span>
{col.label && col.label !== col.name && (
<span className="text-muted-foreground text-[9px]">
{col.label}
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
) : (
<Input
value={mapping.targetColumn || ""}
onChange={(e) =>
updateFieldMapping(subIndex, mapIndex, {
targetColumn: e.target.value,
})
}
placeholder="item_name"
className="mt-0.5 h-5 text-[8px]"
/>
)}
</div>
</div>
))}
</div>
)}
</div>
<Separator />
{/* 대표 데이터 구분 저장 옵션 */}
<div className="space-y-2">
{!subTable.options?.saveMainAsFirst ? (
// 비활성화 상태: 추가 버튼 표시
<div className="border-muted hover:border-primary/50 hover:bg-muted/30 rounded-lg border-2 border-dashed p-3 transition-colors">
<div className="flex items-center justify-between">
<div>
<p className="text-muted-foreground text-[10px] font-medium">
/
</p>
<p className="text-muted-foreground/70 mt-0.5 text-[9px]">
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() =>
updateSubTable(subIndex, {
options: {
...subTable.options,
saveMainAsFirst: true,
mainMarkerColumn: "",
mainMarkerValue: true,
subMarkerValue: false,
},
})
}
className="h-6 shrink-0 px-2 text-[9px]"
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
</div>
) : (
// 활성화 상태: 설정 필드 표시
<div className="space-y-3 rounded-lg border bg-amber-50/50 p-3">
<div className="flex items-center justify-between">
<div>
<p className="text-[10px] font-medium">/ </p>
<p className="text-muted-foreground text-[9px]">
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() =>
updateSubTable(subIndex, {
options: {
...subTable.options,
saveMainAsFirst: false,
mainMarkerColumn: undefined,
mainMarkerValue: undefined,
subMarkerValue: undefined,
},
})
}
className="text-destructive hover:text-destructive h-6 px-2 text-[9px]"
>
<Trash2 className="mr-1 h-3 w-3" />
</Button>
</div>
<div className="space-y-2">
<div>
<Label className="text-[9px]"> </Label>
{subTableColumns.length > 0 ? (
<Popover
open={markerColumnSearchOpen[subIndex] || false}
onOpenChange={(open) =>
setMarkerColumnSearchOpen((prev) => ({ ...prev, [subIndex]: open }))
}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={markerColumnSearchOpen[subIndex] || false}
className="mt-0.5 h-6 w-full justify-between text-[9px] font-normal"
>
{subTable.options?.mainMarkerColumn ? (
<>
{subTable.options.mainMarkerColumn}
{(() => {
const col = subTableColumns.find(
(c) => c.name === subTable.options?.mainMarkerColumn,
);
return col?.label && col.label !== col.name
? ` (${col.label})`
: "";
})()}
</>
) : (
<span className="text-muted-foreground"> </span>
)}
<ChevronsUpDown className="ml-1 h-2.5 w-2.5 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[220px] p-0" align="start">
<Command>
<CommandInput
placeholder="컬럼명/라벨 검색..."
className="h-7 text-xs"
/>
<CommandList className="max-h-[180px]">
<CommandEmpty className="py-2 text-center text-[10px]">
.
</CommandEmpty>
<CommandGroup>
{subTableColumns.map((col) => (
<CommandItem
key={col.name}
value={`${col.name} ${col.label || ""}`}
onSelect={() => {
updateSubTable(subIndex, {
options: {
...subTable.options,
mainMarkerColumn: col.name,
},
});
setMarkerColumnSearchOpen((prev) => ({
...prev,
[subIndex]: false,
}));
}}
className="text-[10px]"
>
<Check
className={cn(
"mr-2 h-3 w-3",
subTable.options?.mainMarkerColumn === col.name
? "opacity-100"
: "opacity-0",
)}
/>
<div className="flex flex-col">
<span className="font-medium">{col.name}</span>
{col.label && col.label !== col.name && (
<span className="text-muted-foreground text-[9px]">
{col.label}
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
) : (
<Input
value={subTable.options?.mainMarkerColumn || ""}
onChange={(e) =>
updateSubTable(subIndex, {
options: {
...subTable.options,
mainMarkerColumn: e.target.value,
},
})
}
placeholder="is_primary"
className="mt-0.5 h-6 text-[9px]"
/>
)}
<HelpText>/ </HelpText>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="text-[9px]"> ()</Label>
<Input
value={String(subTable.options?.mainMarkerValue ?? "true")}
onChange={(e) => {
const val = e.target.value;
// true/false 문자열은 boolean으로 변환
let parsedValue: any = val;
if (val === "true") parsedValue = true;
else if (val === "false") parsedValue = false;
else if (!isNaN(Number(val)) && val !== "") parsedValue = Number(val);
updateSubTable(subIndex, {
options: {
...subTable.options,
mainMarkerValue: parsedValue,
},
});
}}
placeholder="true, Y, 1 등"
className="mt-0.5 h-6 text-[9px]"
/>
<HelpText> </HelpText>
</div>
<div>
<Label className="text-[9px]"> ()</Label>
<Input
value={String(subTable.options?.subMarkerValue ?? "false")}
onChange={(e) => {
const val = e.target.value;
let parsedValue: any = val;
if (val === "true") parsedValue = true;
else if (val === "false") parsedValue = false;
else if (!isNaN(Number(val)) && val !== "") parsedValue = Number(val);
updateSubTable(subIndex, {
options: {
...subTable.options,
subMarkerValue: parsedValue,
},
});
}}
placeholder="false, N, 0 등"
className="mt-0.5 h-6 text-[9px]"
/>
<HelpText> </HelpText>
</div>
</div>
</div>
</div>
)}
</div>
<Separator />
{/* 수정 시 데이터 로드 옵션 */}
<div className="space-y-2">
{!subTable.options?.loadOnEdit ? (
// 비활성화 상태: 추가 버튼 표시
<div className="border-muted hover:border-primary/50 hover:bg-muted/30 rounded-lg border-2 border-dashed p-3 transition-colors">
<div className="flex items-center justify-between">
<div>
<p className="text-muted-foreground text-[10px] font-medium">
</p>
<p className="text-muted-foreground/70 mt-0.5 text-[9px]">
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() =>
updateSubTable(subIndex, {
options: {
...subTable.options,
loadOnEdit: true,
loadOnlySubItems: true,
},
})
}
className="h-6 shrink-0 px-2 text-[9px]"
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
</div>
) : (
// 활성화 상태: 설정 필드 표시
<div className="space-y-3 rounded-lg border bg-blue-50/50 p-3">
<div className="flex items-center justify-between">
<div>
<p className="text-[10px] font-medium"> </p>
<p className="text-muted-foreground text-[9px]">
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() =>
updateSubTable(subIndex, {
options: {
...subTable.options,
loadOnEdit: false,
loadOnlySubItems: undefined,
},
})
}
className="text-destructive hover:text-destructive h-6 px-2 text-[9px]"
>
<Trash2 className="mr-1 h-3 w-3" />
</Button>
</div>
<div className="flex items-center gap-2">
<Switch
id={`loadOnlySubItems-${subIndex}`}
checked={subTable.options?.loadOnlySubItems ?? true}
onCheckedChange={(checked) =>
updateSubTable(subIndex, {
options: {
...subTable.options,
loadOnlySubItems: checked,
},
})
}
/>
<Label htmlFor={`loadOnlySubItems-${subIndex}`} className="text-[9px]">
( )
</Label>
</div>
<HelpText>
,
</HelpText>
</div>
)}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
);
})}
</div>
)}
</div>
</div>
)}
{/* 섹션별 저장 방식 */}
<div className="bg-card space-y-3 rounded-lg border p-3">
<div className="flex items-center gap-2">
<Layers className="h-4 w-4 text-green-600" />
<h3 className="text-xs font-semibold"> </h3>
</div>
{/* 설명 */}
<div className="bg-muted/50 space-y-1.5 rounded-lg p-2.5">
<div className="flex items-start gap-2">
<Info className="mt-0.5 h-3.5 w-3.5 shrink-0 text-blue-500" />
<div className="space-y-1">
<p className="text-muted-foreground text-[10px]">
<span className="text-foreground font-medium"> :</span>
<span className="font-medium"></span>
<br />
<span className="text-muted-foreground/80 text-[9px]">
: 수주번호, , - 3 3
</span>
</p>
<p className="text-muted-foreground text-[10px]">
<span className="text-foreground font-medium"> :</span> {" "}
<span className="font-medium"></span>
<br />
<span className="text-muted-foreground/80 text-[9px]">
: 품목코드, , -
</span>
</p>
</div>
</div>
</div>
{/* 섹션 목록 */}
{sections.length === 0 ? (
<div className="rounded-lg border border-dashed py-4 text-center">
<p className="text-muted-foreground text-[10px]"> </p>
</div>
) : (
<Accordion type="multiple" className="space-y-2">
{sections.map((section) => {
const sectionType = section.type || "fields";
const currentMode = getSectionSaveMode(section.id, sectionType);
const sectionFields = getSectionFields(section);
return (
<AccordionItem
key={section.id}
value={section.id}
className={cn(
"rounded-lg border",
currentMode === "common" ? "bg-blue-50/30" : "bg-orange-50/30",
)}
>
<AccordionTrigger className="px-3 py-2 text-xs hover:no-underline">
<div className="mr-2 flex flex-1 items-center justify-between">
<div className="flex items-center gap-2">
<span className="font-medium">{section.title}</span>
<Badge
variant="outline"
className={cn(
"h-4 text-[8px]",
sectionType === "table"
? "border-orange-300 text-orange-600"
: "border-blue-300 text-blue-600",
)}
>
{sectionType === "table" ? "테이블" : "필드"}
</Badge>
</div>
<Badge
variant={currentMode === "common" ? "default" : "secondary"}
className="h-4 text-[8px]"
>
{currentMode === "common" ? "공통 저장" : "개별 저장"}
</Badge>
</div>
</AccordionTrigger>
<AccordionContent className="space-y-3 px-3 pb-3">
{/* 저장 방식 선택 */}
<div className="space-y-2">
<Label className="text-[10px] font-medium"> </Label>
<RadioGroup
value={currentMode}
onValueChange={(value) =>
updateSectionSaveMode(section.id, value as "common" | "individual")
}
className="flex gap-4"
>
<div className="flex items-center space-x-1.5">
<RadioGroupItem value="common" id={`${section.id}-common`} className="h-3 w-3" />
<Label htmlFor={`${section.id}-common`} className="cursor-pointer text-[10px]">
</Label>
</div>
<div className="flex items-center space-x-1.5">
<RadioGroupItem
value="individual"
id={`${section.id}-individual`}
className="h-3 w-3"
/>
<Label htmlFor={`${section.id}-individual`} className="cursor-pointer text-[10px]">
</Label>
</div>
</RadioGroup>
</div>
{/* 필드 목록 */}
{sectionFields.length > 0 && (
<>
<Separator />
<div className="space-y-2">
<Label className="text-[10px] font-medium">
({sectionFields.length})
</Label>
<HelpText> </HelpText>
<div className="grid grid-cols-2 gap-1.5">
{sectionFields.map((field) => {
const fieldMode = getFieldSaveMode(section.id, field.fieldName, sectionType);
const isOverridden = fieldMode !== currentMode;
return (
<button
key={field.fieldName}
onClick={() => toggleFieldOverride(section.id, field.fieldName, sectionType)}
className={cn(
"flex items-center justify-between rounded border px-2 py-1.5 text-left transition-colors",
isOverridden
? "border-amber-300 bg-amber-50"
: "border-gray-200 bg-white hover:bg-gray-50",
)}
>
<span className="flex-1 truncate text-[9px]">
{field.label}
<span className="text-muted-foreground ml-1">({field.fieldName})</span>
</span>
<Badge
variant={fieldMode === "common" ? "default" : "secondary"}
className={cn(
"ml-1 h-3.5 shrink-0 text-[7px]",
isOverridden && "ring-1 ring-amber-400",
)}
>
{fieldMode === "common" ? "공통" : "개별"}
</Badge>
</button>
);
})}
</div>
</div>
</>
)}
</AccordionContent>
</AccordionItem>
);
})}
</Accordion>
)}
</div>
{/* 저장 후 동작 */}
<div className="bg-card space-y-2 rounded-lg border p-3">
<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="shrink-0 border-t px-4 py-3">
<Button variant="outline" onClick={() => onOpenChange(false)} className="h-9 text-sm">
</Button>
<Button onClick={handleSave} className="h-9 text-sm">
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}