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

1094 lines
53 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 { 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, Settings as SettingsIcon, Check, ChevronsUpDown } from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { cn } from "@/lib/utils";
import {
FormFieldConfig,
LinkedFieldMapping,
FIELD_TYPE_OPTIONS,
SELECT_OPTION_TYPE_OPTIONS,
LINKED_FIELD_DISPLAY_FORMAT_OPTIONS,
} from "../types";
import { apiClient } from "@/lib/api/client";
// 카테고리 컬럼 타입 (table_column_category_values 용)
interface CategoryColumnOption {
tableName: string;
columnName: string;
columnLabel: string;
valueCount: number;
// 조합키: tableName.columnName
key: string;
}
// 도움말 텍스트 컴포넌트
const HelpText = ({ children }: { children: React.ReactNode }) => (
<p className="text-[10px] text-muted-foreground mt-0.5">{children}</p>
);
/**
* 부모 화면에서 전달 가능한 필드 타입
* 유니버셜 폼 모달에서 "부모에서 값 받기" 설정 시 선택 가능한 필드 목록
*/
export interface AvailableParentField {
name: string; // 필드명 (columnName)
label: string; // 표시 라벨
sourceComponent?: string; // 출처 컴포넌트 (예: "TableList", "SplitPanelLayout2")
sourceTable?: string; // 출처 테이블명
}
interface FieldDetailSettingsModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
field: FormFieldConfig;
onSave: (updates: Partial<FormFieldConfig>) => void;
tables: { name: string; label: string }[];
tableColumns: { [tableName: string]: { name: string; type: string; label: string }[] };
numberingRules: { id: string; name: string }[];
onLoadTableColumns: (tableName: string) => void;
// 부모 화면에서 전달 가능한 필드 목록 (선택사항)
availableParentFields?: AvailableParentField[];
// 저장 테이블 정보 (타겟 컬럼 선택용)
targetTableName?: string;
targetTableColumns?: { name: string; type: string; label: string }[];
}
export function FieldDetailSettingsModal({
open,
onOpenChange,
field,
onSave,
tables,
tableColumns,
numberingRules,
onLoadTableColumns,
availableParentFields = [],
// targetTableName은 타겟 컬럼 선택 시 참고용으로 전달됨 (현재 targetTableColumns만 사용)
targetTableName: _targetTableName,
targetTableColumns = [],
}: FieldDetailSettingsModalProps) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
void _targetTableName; // 향후 사용 가능성을 위해 유지
// 로컬 상태로 필드 설정 관리
const [localField, setLocalField] = useState<FormFieldConfig>(field);
// 전체 카테고리 컬럼 목록 상태
const [categoryColumns, setCategoryColumns] = useState<CategoryColumnOption[]>([]);
const [loadingCategoryColumns, setLoadingCategoryColumns] = useState(false);
// Combobox 열림 상태
const [sourceTableOpen, setSourceTableOpen] = useState(false);
const [targetColumnOpenMap, setTargetColumnOpenMap] = useState<Record<number, boolean>>({});
// open이 변경될 때마다 필드 데이터 동기화
useEffect(() => {
if (open) {
setLocalField(field);
}
}, [open, field]);
// 모든 카테고리 컬럼 목록 로드 (모달 열릴 때)
useEffect(() => {
const loadAllCategoryColumns = async () => {
if (!open) return;
setLoadingCategoryColumns(true);
try {
// /api/table-categories/all-columns API 호출
const response = await apiClient.get("/table-categories/all-columns");
if (response.data?.success && response.data?.data) {
// 중복 제거를 위해 Map 사용
const uniqueMap = new Map<string, CategoryColumnOption>();
response.data.data.forEach((col: any) => {
const tableName = col.tableName || col.table_name;
const columnName = col.columnName || col.column_name;
const key = `${tableName}.${columnName}`;
// 이미 존재하는 경우 valueCount가 더 큰 것을 유지
if (!uniqueMap.has(key)) {
uniqueMap.set(key, {
tableName,
columnName,
columnLabel: col.columnLabel || col.column_label || columnName,
valueCount: parseInt(col.valueCount || col.value_count || "0"),
key,
});
}
});
setCategoryColumns(Array.from(uniqueMap.values()));
} else {
setCategoryColumns([]);
}
} catch (error) {
setCategoryColumns([]);
} finally {
setLoadingCategoryColumns(false);
}
};
loadAllCategoryColumns();
}, [open]);
// 필드 업데이트 함수
const updateField = (updates: Partial<FormFieldConfig>) => {
setLocalField((prev) => ({ ...prev, ...updates }));
};
// 저장 함수
const handleSave = () => {
onSave(localField);
onOpenChange(false);
};
// 연결 필드 매핑 추가
const addLinkedFieldMapping = () => {
const newMapping: LinkedFieldMapping = {
sourceColumn: "",
targetColumn: "",
};
const mappings = [...(localField.linkedFieldGroup?.mappings || []), newMapping];
updateField({
linkedFieldGroup: {
...localField.linkedFieldGroup,
enabled: true,
mappings,
},
});
};
// 연결 필드 매핑 삭제
const removeLinkedFieldMapping = (index: number) => {
const mappings = [...(localField.linkedFieldGroup?.mappings || [])];
mappings.splice(index, 1);
updateField({
linkedFieldGroup: {
...localField.linkedFieldGroup,
mappings,
},
});
};
// 연결 필드 매핑 업데이트
const updateLinkedFieldMapping = (index: number, updates: Partial<LinkedFieldMapping>) => {
const mappings = [...(localField.linkedFieldGroup?.mappings || [])];
mappings[index] = { ...mappings[index], ...updates };
updateField({
linkedFieldGroup: {
...localField.linkedFieldGroup,
mappings,
},
});
};
// 소스 테이블 컬럼 목록 (연결 필드용)
const sourceTableColumns = localField.linkedFieldGroup?.sourceTable
? tableColumns[localField.linkedFieldGroup.sourceTable] || []
: [];
// Select 옵션의 참조 테이블 컬럼 목록
const selectTableColumns = localField.selectOptions?.tableName
? tableColumns[localField.selectOptions.tableName] || []
: [];
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[95vw] sm:max-w-[700px] max-h-[85vh] flex flex-col p-0">
<DialogHeader className="px-4 pt-4 pb-2 border-b shrink-0">
<DialogTitle className="text-base"> : {localField.label}</DialogTitle>
<DialogDescription className="text-xs">
, , .
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-hidden px-4">
<ScrollArea className="h-[calc(85vh-180px)]">
<div className="space-y-4 py-3 pr-3">
{/* 기본 정보 섹션 */}
<div className="space-y-3 border rounded-lg p-3 bg-card">
<h3 className="text-xs font-semibold"> </h3>
<div>
<Label className="text-[10px]"> </Label>
<Select
value={localField.fieldType}
onValueChange={(value) =>
updateField({
fieldType: value as FormFieldConfig["fieldType"],
})
}
>
<SelectTrigger className="h-7 text-xs mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
{FIELD_TYPE_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
<HelpText> (, , )</HelpText>
</div>
<div>
<Label className="text-[10px]"> </Label>
<Select
value={String(localField.gridSpan || 6)}
onValueChange={(value) => updateField({ gridSpan: parseInt(value) })}
>
<SelectTrigger className="h-7 text-xs mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="3">1/4 </SelectItem>
<SelectItem value="4">1/3 </SelectItem>
<SelectItem value="6">1/2 </SelectItem>
<SelectItem value="8">2/3 </SelectItem>
<SelectItem value="12"> </SelectItem>
</SelectContent>
</Select>
<HelpText> (12 )</HelpText>
</div>
<div>
<Label className="text-[10px]"></Label>
<Input
value={localField.placeholder || ""}
onChange={(e) => updateField({ placeholder: e.target.value })}
placeholder="입력 힌트"
className="h-7 text-xs mt-1"
/>
<HelpText> </HelpText>
</div>
</div>
{/* 옵션 토글 */}
<div className="space-y-2 border rounded-lg p-3 bg-card">
<h3 className="text-xs font-semibold mb-2"> </h3>
<div className="flex items-center justify-between">
<span className="text-[10px]"> </span>
<Switch
checked={localField.required || false}
onCheckedChange={(checked) => updateField({ required: checked })}
/>
</div>
<HelpText> </HelpText>
<Separator className="my-2" />
<div className="flex items-center justify-between">
<span className="text-[10px]"> ()</span>
<Switch
checked={localField.disabled || false}
onCheckedChange={(checked) => updateField({ disabled: checked })}
/>
</div>
<HelpText> </HelpText>
<Separator className="my-2" />
<div className="flex items-center justify-between">
<span className="text-[10px]"> ( )</span>
<Switch
checked={localField.hidden || false}
onCheckedChange={(checked) => updateField({ hidden: checked })}
/>
</div>
<HelpText> </HelpText>
<Separator className="my-2" />
<div className="flex items-center justify-between">
<span className="text-[10px]"> </span>
<Switch
checked={localField.receiveFromParent || false}
onCheckedChange={(checked) => updateField({ receiveFromParent: checked })}
/>
</div>
<HelpText> </HelpText>
{/* 부모에서 값 받기 활성화 시 필드 선택 */}
{localField.receiveFromParent && (
<div className="mt-3 space-y-2 p-3 rounded-md bg-blue-50 border border-blue-200">
<Label className="text-xs font-medium text-blue-700"> </Label>
{availableParentFields.length > 0 ? (
<Select
value={localField.parentFieldName || localField.columnName}
onValueChange={(value) => updateField({ parentFieldName: value })}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="필드 선택" />
</SelectTrigger>
<SelectContent>
{availableParentFields.map((pf) => (
<SelectItem key={pf.name} value={pf.name}>
<div className="flex flex-col">
<span>{pf.label || pf.name}</span>
{pf.sourceComponent && (
<span className="text-[9px] text-muted-foreground">
{pf.sourceComponent}{pf.sourceTable && ` (${pf.sourceTable})`}
</span>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<div className="space-y-1">
<Input
value={localField.parentFieldName || ""}
onChange={(e) => updateField({ parentFieldName: e.target.value })}
placeholder={`예: ${localField.columnName || "parent_field_name"}`}
className="h-8 text-xs"
/>
<p className="text-[10px] text-muted-foreground">
. "{localField.columnName}" .
</p>
</div>
)}
</div>
)}
</div>
{/* Accordion으로 고급 설정 */}
<Accordion type="single" collapsible className="space-y-2">
{/* Select 옵션 설정 */}
{localField.fieldType === "select" && (
<AccordionItem value="select-options" className="border rounded-lg">
<AccordionTrigger className="px-3 py-2 text-xs font-medium hover:no-underline bg-green-50/50">
<div className="flex items-center gap-2">
<SettingsIcon className="h-3.5 w-3.5 text-green-600" />
<span>Select </span>
{localField.selectOptions?.type && (
<span className="text-[9px] text-muted-foreground">
({localField.selectOptions.type === "code" ? "공통코드" : "직접 입력"})
</span>
)}
</div>
</AccordionTrigger>
<AccordionContent className="px-3 pb-3 space-y-3">
<HelpText> .</HelpText>
<div>
<Label className="text-[10px]"> </Label>
<Select
value={localField.selectOptions?.type || "static"}
onValueChange={(value) =>
updateField({
selectOptions: {
...localField.selectOptions,
type: value as "static" | "code",
},
})
}
>
<SelectTrigger className="h-7 text-xs mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
{SELECT_OPTION_TYPE_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{localField.selectOptions?.type === "table" && (
<div className="space-y-3 pt-2 border-t">
<HelpText> 참조: DB .</HelpText>
<div>
<Label className="text-[10px]"> </Label>
<Select
value={localField.selectOptions?.tableName || ""}
onValueChange={(value) => {
updateField({
selectOptions: {
...localField.selectOptions,
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>
{selectTableColumns.length > 0 ? (
<Select
value={localField.selectOptions?.valueColumn || ""}
onValueChange={(value) =>
updateField({
selectOptions: {
...localField.selectOptions,
valueColumn: value,
},
})
}
>
<SelectTrigger className="h-7 text-xs mt-1">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{selectTableColumns.map((col) => (
<SelectItem key={col.name} value={col.name}>
{col.name}
{col.label !== col.name && ` (${col.label})`}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={localField.selectOptions?.valueColumn || ""}
onChange={(e) =>
updateField({
selectOptions: {
...localField.selectOptions,
valueColumn: e.target.value,
},
})
}
placeholder="customer_code"
className="h-7 text-xs mt-1"
/>
)}
<HelpText>
()
<br />
: customer_code, customer_id
</HelpText>
</div>
<div>
<Label className="text-[10px]"> ()</Label>
{selectTableColumns.length > 0 ? (
<Select
value={localField.selectOptions?.labelColumn || ""}
onValueChange={(value) =>
updateField({
selectOptions: {
...localField.selectOptions,
labelColumn: value,
},
})
}
>
<SelectTrigger className="h-7 text-xs mt-1">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{selectTableColumns.map((col) => (
<SelectItem key={col.name} value={col.name}>
{col.name}
{col.label !== col.name && ` (${col.label})`}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={localField.selectOptions?.labelColumn || ""}
onChange={(e) =>
updateField({
selectOptions: {
...localField.selectOptions,
labelColumn: e.target.value,
},
})
}
placeholder="customer_name"
className="h-7 text-xs mt-1"
/>
)}
<HelpText>
()
<br />
: customer_name, dept_name
</HelpText>
</div>
<div>
<Label className="text-[10px]"> </Label>
{selectTableColumns.length > 0 ? (
<Select
value={localField.selectOptions?.saveColumn || "__default__"}
onValueChange={(value) =>
updateField({
selectOptions: {
...localField.selectOptions,
saveColumn: value === "__default__" ? "" : value,
},
})
}
>
<SelectTrigger className="h-7 text-xs mt-1">
<SelectValue placeholder="컬럼 선택 (미선택 시 조인 컬럼 저장)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__default__"> ()</SelectItem>
{selectTableColumns.map((col) => (
<SelectItem key={col.name} value={col.name}>
{col.name}
{col.label !== col.name && ` (${col.label})`}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={localField.selectOptions?.saveColumn || ""}
onChange={(e) =>
updateField({
selectOptions: {
...localField.selectOptions,
saveColumn: e.target.value,
},
})
}
placeholder="비워두면 조인 컬럼 저장"
className="h-7 text-xs mt-1"
/>
)}
<HelpText>
DB에
<br />
: customer_name ( customer_code )
</HelpText>
</div>
</div>
)}
{localField.selectOptions?.type === "code" && (
<div className="space-y-2 pt-2 border-t">
<HelpText>공통코드: 코드설정에서 .</HelpText>
<div>
<Label className="text-[10px]"> </Label>
<Select
value={localField.selectOptions?.categoryKey || ""}
onValueChange={(value) =>
updateField({
selectOptions: {
...localField.selectOptions,
categoryKey: value,
},
})
}
>
<SelectTrigger className="h-7 text-xs mt-1">
<SelectValue placeholder={loadingCategoryColumns ? "로딩 중..." : "카테고리 선택"} />
</SelectTrigger>
<SelectContent>
{categoryColumns.map((col, idx) => (
<SelectItem key={`${col.key}-${idx}`} value={col.key}>
{col.columnLabel} - {col.tableName} ({col.valueCount})
</SelectItem>
))}
</SelectContent>
</Select>
<HelpText> </HelpText>
</div>
</div>
)}
</AccordionContent>
</AccordionItem>
)}
{/* 연결 필드 설정 */}
<AccordionItem value="linked-fields" className="border rounded-lg">
<AccordionTrigger className="px-3 py-2 text-xs font-medium hover:no-underline bg-orange-50/50">
<div className="flex items-center gap-2">
<SettingsIcon className="h-3.5 w-3.5 text-orange-600" />
<span> ( )</span>
{localField.linkedFieldGroup?.enabled && (
<span className="text-[9px] text-muted-foreground">
({(localField.linkedFieldGroup?.mappings || []).length})
</span>
)}
</div>
</AccordionTrigger>
<AccordionContent className="px-3 pb-3 space-y-3">
<div className="flex items-center justify-between">
<span className="text-[10px] font-medium"> </span>
<Switch
checked={localField.linkedFieldGroup?.enabled || false}
onCheckedChange={(checked) =>
updateField({
linkedFieldGroup: {
...localField.linkedFieldGroup,
enabled: checked,
},
})
}
/>
</div>
<HelpText>
.
<br />
: 고객 , ,
</HelpText>
{localField.linkedFieldGroup?.enabled && (
<div className="space-y-3 pt-2 border-t">
<div>
<Label className="text-[10px]"> </Label>
<Popover open={sourceTableOpen} onOpenChange={setSourceTableOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={sourceTableOpen}
className="h-7 w-full justify-between text-xs mt-1 font-normal"
>
{localField.linkedFieldGroup?.sourceTable
? (() => {
const selectedTable = tables.find(
(t) => t.name === localField.linkedFieldGroup?.sourceTable
);
return selectedTable
? `${selectedTable.label || selectedTable.name} (${selectedTable.name})`
: localField.linkedFieldGroup?.sourceTable;
})()
: "테이블 선택..."}
<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="h-8 text-xs" />
<CommandList>
<CommandEmpty className="py-2 text-xs text-center">
.
</CommandEmpty>
<CommandGroup>
{tables.map((t) => (
<CommandItem
key={t.name}
value={`${t.name} ${t.label || ""}`}
onSelect={() => {
updateField({
linkedFieldGroup: {
...localField.linkedFieldGroup,
sourceTable: t.name,
},
});
onLoadTableColumns(t.name);
setSourceTableOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
localField.linkedFieldGroup?.sourceTable === t.name
? "opacity-100"
: "opacity-0"
)}
/>
<span className="font-medium">{t.label || t.name}</span>
<span className="ml-1 text-muted-foreground">({t.name})</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<HelpText> (: customer_mng)</HelpText>
</div>
<div>
<Label className="text-[10px]"> </Label>
{sourceTableColumns.length > 0 ? (
<Select
value={localField.linkedFieldGroup?.displayColumn || ""}
onValueChange={(value) =>
updateField({
linkedFieldGroup: {
...localField.linkedFieldGroup,
displayColumn: value,
},
})
}
>
<SelectTrigger className="h-7 text-xs mt-1">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{sourceTableColumns.map((col) => (
<SelectItem key={col.name} value={col.name}>
{col.name}
{col.label !== col.name && ` (${col.label})`}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={localField.linkedFieldGroup?.displayColumn || ""}
onChange={(e) =>
updateField({
linkedFieldGroup: {
...localField.linkedFieldGroup,
displayColumn: e.target.value,
},
})
}
placeholder="customer_name"
className="h-7 text-xs mt-1"
/>
)}
<HelpText> (: customer_name)</HelpText>
</div>
<div>
<Label className="text-[10px]"> </Label>
<Select
value={localField.linkedFieldGroup?.displayFormat || "name_only"}
onValueChange={(value) =>
updateField({
linkedFieldGroup: {
...localField.linkedFieldGroup,
displayFormat: value as "name_only" | "code_name" | "name_code",
},
})
}
>
<SelectTrigger className="h-7 text-xs mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
{LINKED_FIELD_DISPLAY_FORMAT_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
<HelpText> </HelpText>
</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={addLinkedFieldMapping} className="h-6 text-[9px] px-2">
<Plus className="h-3 w-3 mr-1" />
</Button>
</div>
<HelpText>
.
<br />
: customer_code partner_id, customer_name partner_name
</HelpText>
{(localField.linkedFieldGroup?.mappings || []).length === 0 ? (
<div className="text-center py-4 border border-dashed rounded-lg">
<p className="text-[10px] text-muted-foreground"> </p>
<p className="text-[9px] text-muted-foreground"> "매핑 추가" </p>
</div>
) : (
<div className="space-y-2">
{(localField.linkedFieldGroup?.mappings || []).map((mapping, index) => (
<div key={index} className="border rounded-lg p-2 space-y-2 bg-muted/30">
<div className="flex items-center justify-between">
<span className="text-[9px] font-medium text-muted-foreground"> {index + 1}</span>
<Button
size="sm"
variant="ghost"
onClick={() => removeLinkedFieldMapping(index)}
className="h-5 w-5 p-0 text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
<div>
<Label className="text-[9px]"> ( )</Label>
{sourceTableColumns.length > 0 ? (
<Select
value={mapping.sourceColumn || ""}
onValueChange={(value) =>
updateLinkedFieldMapping(index, { sourceColumn: value })
}
>
<SelectTrigger className="h-6 text-[9px] mt-0.5">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{sourceTableColumns.map((col) => (
<SelectItem key={col.name} value={col.name}>
{col.name}
{col.label !== col.name && ` (${col.label})`}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={mapping.sourceColumn || ""}
onChange={(e) =>
updateLinkedFieldMapping(index, { sourceColumn: e.target.value })
}
placeholder="customer_code"
className="h-6 text-[9px] mt-0.5"
/>
)}
</div>
<div className="text-center text-[9px] text-muted-foreground"></div>
<div>
<Label className="text-[9px]"> ( )</Label>
{targetTableColumns.length > 0 ? (
<Popover
open={targetColumnOpenMap[index] || false}
onOpenChange={(open) =>
setTargetColumnOpenMap((prev) => ({ ...prev, [index]: open }))
}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={targetColumnOpenMap[index] || false}
className="h-6 w-full justify-between text-[9px] mt-0.5 font-normal"
>
{mapping.targetColumn
? (() => {
const selectedCol = targetTableColumns.find(
(c) => c.name === mapping.targetColumn
);
return selectedCol
? `${selectedCol.name} (${selectedCol.label})`
: mapping.targetColumn;
})()
: "컬럼 선택..."}
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[250px] p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="h-7 text-[9px]" />
<CommandList>
<CommandEmpty className="py-2 text-[9px] text-center">
.
</CommandEmpty>
<CommandGroup>
{targetTableColumns.map((col) => (
<CommandItem
key={col.name}
value={`${col.name} ${col.label}`}
onSelect={() => {
updateLinkedFieldMapping(index, { targetColumn: col.name });
setTargetColumnOpenMap((prev) => ({ ...prev, [index]: false }));
}}
className="text-[9px]"
>
<Check
className={cn(
"mr-2 h-3 w-3",
mapping.targetColumn === col.name
? "opacity-100"
: "opacity-0"
)}
/>
<span className="font-medium">{col.name}</span>
<span className="ml-1 text-muted-foreground">({col.label})</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
) : (
<Input
value={mapping.targetColumn || ""}
onChange={(e) =>
updateLinkedFieldMapping(index, { targetColumn: e.target.value })
}
placeholder="partner_id"
className="h-6 text-[9px] mt-0.5"
/>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
)}
</AccordionContent>
</AccordionItem>
{/* 채번규칙 설정 */}
<AccordionItem value="numbering-rule" className="border rounded-lg">
<AccordionTrigger className="px-3 py-2 text-xs font-medium hover:no-underline bg-blue-50/50">
<div className="flex items-center gap-2">
<SettingsIcon className="h-3.5 w-3.5 text-blue-600" />
<span> </span>
{localField.numberingRule?.enabled && (
<span className="text-[9px] text-muted-foreground">()</span>
)}
</div>
</AccordionTrigger>
<AccordionContent className="px-3 pb-3 space-y-3">
<div className="flex items-center justify-between">
<span className="text-[10px] font-medium"> </span>
<Switch
checked={localField.numberingRule?.enabled || false}
onCheckedChange={(checked) =>
updateField({
numberingRule: {
...localField.numberingRule,
enabled: checked,
},
})
}
/>
</div>
<HelpText>
/ .
<br />
: EMP-001, ORD-20240101-001
</HelpText>
{localField.numberingRule?.enabled && (
<div className="space-y-2 pt-2 border-t">
<div>
<Label className="text-[10px]"> </Label>
<Select
value={localField.numberingRule?.ruleId || ""}
onValueChange={(value) =>
updateField({
numberingRule: {
...localField.numberingRule,
ruleId: value,
},
})
}
>
<SelectTrigger className="h-7 text-xs mt-1">
<SelectValue placeholder="규칙 선택" />
</SelectTrigger>
<SelectContent>
{numberingRules.length === 0 ? (
<div className="px-2 py-1.5 text-xs text-muted-foreground">
</div>
) : (
numberingRules.map((rule) => (
<SelectItem key={rule.id} value={rule.id}>
{rule.name}
</SelectItem>
))
)}
</SelectContent>
</Select>
<HelpText> </HelpText>
</div>
<Separator className="my-2" />
<div className="flex items-center justify-between">
<span className="text-[10px]"> </span>
<Switch
checked={localField.numberingRule?.editable || false}
onCheckedChange={(checked) =>
updateField({
numberingRule: {
...localField.numberingRule,
editable: checked,
},
})
}
/>
</div>
<HelpText> </HelpText>
<Separator className="my-2" />
<div className="flex items-center justify-between">
<span className="text-[10px]"> </span>
<Switch
checked={localField.numberingRule?.generateOnSave || false}
onCheckedChange={(checked) =>
updateField({
numberingRule: {
...localField.numberingRule,
generateOnSave: checked,
generateOnOpen: !checked,
},
})
}
/>
</div>
<HelpText>OFF: 모달 / ON: 저장 </HelpText>
</div>
)}
</AccordionContent>
</AccordionItem>
</Accordion>
</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>
);
}