feat(universal-form-modal): 연쇄 드롭다운(Cascading Dropdown) 기능 구현

- SelectOptionConfig에 cascading 타입 및 설정 객체 추가
- FieldDetailSettingsModal에 연쇄 드롭다운 설정 UI 구현
  - 부모 필드 선택 (섹션별 그룹핑 콤보박스)
  - 관계 코드 선택 시 상세 설정 자동 채움
  - 소스 테이블, 부모 키 컬럼, 값/라벨 컬럼 설정
- UniversalFormModalComponent에 자식 필드 초기화 로직 추가
- selectOptions.cascading 방식 CascadingSelectField 렌더링 지원
This commit is contained in:
SeongHyun Kim 2026-01-13 18:26:41 +09:00
parent d7d7dabe84
commit ef27e0e38f
4 changed files with 653 additions and 12 deletions

View File

@ -84,7 +84,7 @@ const CascadingSelectField: React.FC<CascadingSelectFieldProps> = ({
return ( return (
<Select value={value || ""} onValueChange={onChange} disabled={isDisabled}> <Select value={value || ""} onValueChange={onChange} disabled={isDisabled}>
<SelectTrigger id={fieldId} className="w-full"> <SelectTrigger id={fieldId} className="w-full" size="default">
{loading ? ( {loading ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
@ -958,6 +958,26 @@ export function UniversalFormModalComponent({
if (fieldConfig) break; if (fieldConfig) break;
} }
// 🆕 연쇄 드롭다운: 부모 필드 변경 시 자식 필드 초기화
const childFieldsToReset: string[] = [];
for (const section of config.sections) {
if (section.type === "table" || section.repeatable) continue;
for (const field of section.fields || []) {
// field.cascading 방식 체크
if (field.cascading?.enabled && field.cascading?.parentField === columnName) {
if (field.cascading.clearOnParentChange !== false) {
childFieldsToReset.push(field.columnName);
}
}
// selectOptions.cascading 방식 체크
if (field.selectOptions?.type === "cascading" && field.selectOptions?.cascading?.parentField === columnName) {
if (field.selectOptions.cascading.clearOnParentChange !== false) {
childFieldsToReset.push(field.columnName);
}
}
}
}
setFormData((prev) => { setFormData((prev) => {
const newData = { ...prev, [columnName]: value }; const newData = { ...prev, [columnName]: value };
@ -976,6 +996,12 @@ export function UniversalFormModalComponent({
} }
} }
// 🆕 연쇄 드롭다운 자식 필드 초기화
for (const childField of childFieldsToReset) {
newData[childField] = "";
console.log(`[연쇄 드롭다운] 부모 ${columnName} 변경 → 자식 ${childField} 초기화`);
}
// onChange는 렌더링 외부에서 호출해야 함 (setTimeout 사용) // onChange는 렌더링 외부에서 호출해야 함 (setTimeout 사용)
if (onChange) { if (onChange) {
setTimeout(() => onChange(newData), 0); setTimeout(() => onChange(newData), 0);
@ -1463,7 +1489,7 @@ export function UniversalFormModalComponent({
); );
case "select": { case "select": {
// 🆕 연쇄 드롭다운 처리 // 🆕 연쇄 드롭다운 처리 (기존 field.cascading 방식)
if (field.cascading?.enabled) { if (field.cascading?.enabled) {
const cascadingConfig = field.cascading; const cascadingConfig = field.cascading;
const parentValue = formData[cascadingConfig.parentField]; const parentValue = formData[cascadingConfig.parentField];
@ -1480,6 +1506,37 @@ export function UniversalFormModalComponent({
/> />
); );
} }
// 🆕 연쇄 드롭다운 처리 (selectOptions.type === "cascading" 방식)
if (field.selectOptions?.type === "cascading" && field.selectOptions?.cascading?.parentField) {
const cascadingOpts = field.selectOptions.cascading;
const parentValue = formData[cascadingOpts.parentField];
// selectOptions 기반 cascading config를 CascadingDropdownConfig 형태로 변환
const cascadingConfig: CascadingDropdownConfig = {
enabled: true,
parentField: cascadingOpts.parentField,
sourceTable: cascadingOpts.sourceTable || field.selectOptions.tableName || "",
parentKeyColumn: cascadingOpts.parentKeyColumn || "",
valueColumn: field.selectOptions.valueColumn || "",
labelColumn: field.selectOptions.labelColumn || "",
emptyParentMessage: cascadingOpts.emptyParentMessage,
noOptionsMessage: cascadingOpts.noOptionsMessage,
clearOnParentChange: cascadingOpts.clearOnParentChange !== false,
};
return (
<CascadingSelectField
fieldId={fieldKey}
config={cascadingConfig}
parentValue={parentValue}
value={value}
onChange={onChangeHandler}
placeholder={field.placeholder || "선택하세요"}
disabled={isDisabled}
/>
);
}
// 다중 컬럼 저장이 활성화된 경우 // 다중 컬럼 저장이 활성화된 경우
const lfgMappings = field.linkedFieldGroup?.mappings; const lfgMappings = field.linkedFieldGroup?.mappings;

View File

@ -870,6 +870,14 @@ export function UniversalFormModalConfigPanel({
onLoadTableColumns={loadTableColumns} onLoadTableColumns={loadTableColumns}
targetTableName={config.saveConfig?.tableName} targetTableName={config.saveConfig?.tableName}
targetTableColumns={config.saveConfig?.tableName ? tableColumns[config.saveConfig.tableName] || [] : []} targetTableColumns={config.saveConfig?.tableName ? tableColumns[config.saveConfig.tableName] || [] : []}
allFieldsWithSections={config.sections
.filter(s => s.type !== "table" && !s.repeatable)
.map(s => ({
sectionId: s.id,
sectionTitle: s.title,
fields: s.fields || []
}))
}
/> />
)} )}

View File

@ -29,6 +29,7 @@ import {
LINKED_FIELD_DISPLAY_FORMAT_OPTIONS, LINKED_FIELD_DISPLAY_FORMAT_OPTIONS,
} from "../types"; } from "../types";
import { apiClient } from "@/lib/api/client"; import { apiClient } from "@/lib/api/client";
import { getCascadingRelations, getCascadingRelationByCode, CascadingRelation } from "@/lib/api/cascadingRelation";
// 카테고리 컬럼 타입 (table_column_category_values 용) // 카테고리 컬럼 타입 (table_column_category_values 용)
interface CategoryColumnOption { interface CategoryColumnOption {
@ -56,6 +57,13 @@ export interface AvailableParentField {
sourceTable?: string; // 출처 테이블명 sourceTable?: string; // 출처 테이블명
} }
// 섹션별 필드 그룹
interface SectionFieldGroup {
sectionId: string;
sectionTitle: string;
fields: FormFieldConfig[];
}
interface FieldDetailSettingsModalProps { interface FieldDetailSettingsModalProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
@ -68,6 +76,8 @@ interface FieldDetailSettingsModalProps {
// 저장 테이블 정보 (타겟 컬럼 선택용) // 저장 테이블 정보 (타겟 컬럼 선택용)
targetTableName?: string; targetTableName?: string;
targetTableColumns?: { name: string; type: string; label: string }[]; targetTableColumns?: { name: string; type: string; label: string }[];
// 연쇄 드롭다운 부모 필드 선택용 - 모든 섹션의 필드 목록 (섹션별 그룹핑)
allFieldsWithSections?: SectionFieldGroup[];
} }
export function FieldDetailSettingsModal({ export function FieldDetailSettingsModal({
@ -82,6 +92,7 @@ export function FieldDetailSettingsModal({
// targetTableName은 타겟 컬럼 선택 시 참고용으로 전달됨 (현재 targetTableColumns만 사용) // targetTableName은 타겟 컬럼 선택 시 참고용으로 전달됨 (현재 targetTableColumns만 사용)
targetTableName: _targetTableName, targetTableName: _targetTableName,
targetTableColumns = [], targetTableColumns = [],
allFieldsWithSections = [],
}: FieldDetailSettingsModalProps) { }: FieldDetailSettingsModalProps) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
void _targetTableName; // 향후 사용 가능성을 위해 유지 void _targetTableName; // 향후 사용 가능성을 위해 유지
@ -92,6 +103,12 @@ export function FieldDetailSettingsModal({
const [categoryColumns, setCategoryColumns] = useState<CategoryColumnOption[]>([]); const [categoryColumns, setCategoryColumns] = useState<CategoryColumnOption[]>([]);
const [loadingCategoryColumns, setLoadingCategoryColumns] = useState(false); const [loadingCategoryColumns, setLoadingCategoryColumns] = useState(false);
// 연쇄 관계 목록 상태
const [cascadingRelations, setCascadingRelations] = useState<CascadingRelation[]>([]);
const [loadingCascadingRelations, setLoadingCascadingRelations] = useState(false);
const [cascadingRelationOpen, setCascadingRelationOpen] = useState(false);
const [parentFieldOpen, setParentFieldOpen] = useState(false);
// Combobox 열림 상태 // Combobox 열림 상태
const [sourceTableOpen, setSourceTableOpen] = useState(false); const [sourceTableOpen, setSourceTableOpen] = useState(false);
const [targetColumnOpenMap, setTargetColumnOpenMap] = useState<Record<number, boolean>>({}); const [targetColumnOpenMap, setTargetColumnOpenMap] = useState<Record<number, boolean>>({});
@ -169,6 +186,66 @@ export function FieldDetailSettingsModal({
loadAllCategoryColumns(); loadAllCategoryColumns();
}, [open]); }, [open]);
// 연쇄 관계 목록 로드 (모달 열릴 때)
useEffect(() => {
const loadCascadingRelations = async () => {
if (!open) return;
setLoadingCascadingRelations(true);
try {
const result = await getCascadingRelations("Y"); // 활성화된 것만
if (result?.success && result?.data) {
setCascadingRelations(result.data);
} else {
setCascadingRelations([]);
}
} catch (error) {
setCascadingRelations([]);
} finally {
setLoadingCascadingRelations(false);
}
};
loadCascadingRelations();
}, [open]);
// 관계 코드 선택 시 상세 설정 자동 채움
const handleRelationCodeSelect = async (relationCode: string) => {
if (!relationCode) return;
try {
const result = await getCascadingRelationByCode(relationCode);
if (result?.success && result?.data) {
const relation = result.data as CascadingRelation;
updateField({
selectOptions: {
...localField.selectOptions,
type: "cascading",
tableName: relation.child_table,
valueColumn: relation.child_value_column,
labelColumn: relation.child_label_column,
cascading: {
...localField.selectOptions?.cascading,
relationCode: relation.relation_code,
sourceTable: relation.child_table,
parentKeyColumn: relation.child_filter_column,
emptyParentMessage: relation.empty_parent_message,
noOptionsMessage: relation.no_options_message,
clearOnParentChange: relation.clear_on_parent_change === "Y",
},
},
});
// 소스 테이블 컬럼 로드
if (relation.child_table) {
onLoadTableColumns(relation.child_table);
}
}
} catch (error) {
console.error("관계 코드 조회 실패:", error);
}
};
// 필드 업데이트 함수 // 필드 업데이트 함수
const updateField = (updates: Partial<FormFieldConfig>) => { const updateField = (updates: Partial<FormFieldConfig>) => {
setLocalField((prev) => ({ ...prev, ...updates })); setLocalField((prev) => ({ ...prev, ...updates }));
@ -362,14 +439,28 @@ export function FieldDetailSettingsModal({
<Label className="text-[10px]"> </Label> <Label className="text-[10px]"> </Label>
<Select <Select
value={localField.selectOptions?.type || "static"} value={localField.selectOptions?.type || "static"}
onValueChange={(value) => onValueChange={(value) => {
updateField({ // 타입 변경 시 관련 설정 초기화
selectOptions: { if (value === "cascading") {
...localField.selectOptions, updateField({
type: value as "static" | "code", selectOptions: {
}, type: "cascading",
}) cascading: {
} parentField: "",
clearOnParentChange: true,
},
},
});
} else {
updateField({
selectOptions: {
...localField.selectOptions,
type: value as "static" | "table" | "code",
cascading: undefined,
},
});
}
}}
> >
<SelectTrigger className="h-7 text-xs mt-1"> <SelectTrigger className="h-7 text-xs mt-1">
<SelectValue /> <SelectValue />
@ -382,6 +473,11 @@ export function FieldDetailSettingsModal({
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
<HelpText>
{localField.selectOptions?.type === "cascading"
? "연쇄 드롭다운: 부모 필드 선택에 따라 옵션이 동적으로 변경됩니다"
: "테이블 참조: DB 테이블에서 옵션 목록을 가져옵니다."}
</HelpText>
</div> </div>
{localField.selectOptions?.type === "table" && ( {localField.selectOptions?.type === "table" && (
@ -594,6 +690,472 @@ export function FieldDetailSettingsModal({
</div> </div>
</div> </div>
)} )}
{localField.selectOptions?.type === "cascading" && (
<div className="space-y-3 pt-2 border-t">
<HelpText>
드롭다운: 부모 .
<br />
: 거래처
</HelpText>
{/* 부모 필드 선택 - 콤보박스 (섹션별 그룹핑) */}
<div>
<Label className="text-[10px]"> *</Label>
{allFieldsWithSections.length > 0 ? (
<Popover open={parentFieldOpen} onOpenChange={setParentFieldOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={parentFieldOpen}
className="h-7 w-full justify-between text-xs mt-1 font-normal"
>
{localField.selectOptions?.cascading?.parentField
? (() => {
// 모든 섹션에서 선택된 필드 찾기
for (const section of allFieldsWithSections) {
const selectedField = section.fields.find(
(f) => f.columnName === localField.selectOptions?.cascading?.parentField
);
if (selectedField) {
return `${selectedField.label} (${selectedField.columnName})`;
}
}
return localField.selectOptions?.cascading?.parentField;
})()
: "부모 필드 선택..."}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[350px] p-0" align="start">
<Command>
<CommandInput placeholder="필드 검색..." className="h-8 text-xs" />
<CommandList className="max-h-[300px]">
<CommandEmpty className="py-2 text-xs text-center">
.
</CommandEmpty>
{allFieldsWithSections.map((section) => {
// 자기 자신 제외한 필드 목록
const availableFields = section.fields.filter(
(f) => f.columnName !== field.columnName
);
if (availableFields.length === 0) return null;
return (
<CommandGroup
key={section.sectionId}
heading={section.sectionTitle}
className="[&_[cmdk-group-heading]]:text-[10px] [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:text-primary [&_[cmdk-group-heading]]:bg-muted/50 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1"
>
{availableFields.map((f) => (
<CommandItem
key={f.id}
value={`${section.sectionTitle} ${f.columnName} ${f.label}`}
onSelect={() => {
updateField({
selectOptions: {
...localField.selectOptions,
cascading: {
...localField.selectOptions?.cascading,
parentField: f.columnName,
},
},
});
setParentFieldOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
localField.selectOptions?.cascading?.parentField === f.columnName
? "opacity-100"
: "opacity-0"
)}
/>
<div className="flex flex-col">
<span className="font-medium">{f.label}</span>
<span className="text-[9px] text-muted-foreground">
{f.columnName} ({f.fieldType})
</span>
</div>
</CommandItem>
))}
</CommandGroup>
);
})}
</CommandList>
</Command>
</PopoverContent>
</Popover>
) : (
<Input
value={localField.selectOptions?.cascading?.parentField || ""}
onChange={(e) =>
updateField({
selectOptions: {
...localField.selectOptions,
cascading: {
...localField.selectOptions?.cascading,
parentField: e.target.value,
},
},
})
}
placeholder="customer_code"
className="h-7 text-xs mt-1"
/>
)}
<HelpText>
<br />
: 거래처
</HelpText>
</div>
{/* 관계 코드 선택 */}
<div>
<Label className="text-[10px]"> ()</Label>
<Popover open={cascadingRelationOpen} onOpenChange={setCascadingRelationOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={cascadingRelationOpen}
className="h-7 w-full justify-between text-xs mt-1 font-normal"
>
{localField.selectOptions?.cascading?.relationCode
? (() => {
const selectedRelation = cascadingRelations.find(
(r) => r.relation_code === localField.selectOptions?.cascading?.relationCode
);
return selectedRelation
? `${selectedRelation.relation_name} (${selectedRelation.relation_code})`
: localField.selectOptions?.cascading?.relationCode;
})()
: loadingCascadingRelations
? "로딩 중..."
: "관계 선택 (또는 직접 설정)"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[350px] p-0" align="start">
<Command>
<CommandInput placeholder="관계 검색..." className="h-8 text-xs" />
<CommandList>
<CommandEmpty className="py-2 text-xs text-center">
.
</CommandEmpty>
<CommandGroup>
{/* 직접 설정 옵션 */}
<CommandItem
value="__direct__"
onSelect={() => {
updateField({
selectOptions: {
...localField.selectOptions,
cascading: {
...localField.selectOptions?.cascading,
relationCode: undefined,
},
},
});
setCascadingRelationOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
!localField.selectOptions?.cascading?.relationCode
? "opacity-100"
: "opacity-0"
)}
/>
<span className="text-muted-foreground"> </span>
</CommandItem>
<Separator className="my-1" />
{cascadingRelations.map((relation) => (
<CommandItem
key={relation.relation_id}
value={`${relation.relation_code} ${relation.relation_name}`}
onSelect={() => {
handleRelationCodeSelect(relation.relation_code);
setCascadingRelationOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
localField.selectOptions?.cascading?.relationCode === relation.relation_code
? "opacity-100"
: "opacity-0"
)}
/>
<div className="flex flex-col">
<span className="font-medium">{relation.relation_name}</span>
<span className="text-[9px] text-muted-foreground">
{relation.parent_table} {relation.child_table}
</span>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<HelpText>
.
<br />
.
</HelpText>
</div>
<Separator />
{/* 상세 설정 (수정 가능) */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<SettingsIcon className="h-3 w-3 text-muted-foreground" />
<span className="text-[10px] font-medium"> ( )</span>
</div>
<div>
<Label className="text-[10px]"> </Label>
<Select
value={localField.selectOptions?.cascading?.sourceTable || localField.selectOptions?.tableName || ""}
onValueChange={(value) => {
updateField({
selectOptions: {
...localField.selectOptions,
tableName: value,
cascading: {
...localField.selectOptions?.cascading,
sourceTable: 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> (: delivery_destination)</HelpText>
</div>
<div>
<Label className="text-[10px]"> </Label>
{selectTableColumns.length > 0 ? (
<Select
value={localField.selectOptions?.cascading?.parentKeyColumn || ""}
onValueChange={(value) =>
updateField({
selectOptions: {
...localField.selectOptions,
cascading: {
...localField.selectOptions?.cascading,
parentKeyColumn: 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?.cascading?.parentKeyColumn || ""}
onChange={(e) =>
updateField({
selectOptions: {
...localField.selectOptions,
cascading: {
...localField.selectOptions?.cascading,
parentKeyColumn: e.target.value,
},
},
})
}
placeholder="customer_code"
className="h-7 text-xs mt-1"
/>
)}
<HelpText> (: customer_code)</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="destination_code"
className="h-7 text-xs mt-1"
/>
)}
<HelpText> value로 </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="destination_name"
className="h-7 text-xs mt-1"
/>
)}
<HelpText> </HelpText>
</div>
<Separator />
<div>
<Label className="text-[10px]"> </Label>
<Input
value={localField.selectOptions?.cascading?.emptyParentMessage || ""}
onChange={(e) =>
updateField({
selectOptions: {
...localField.selectOptions,
cascading: {
...localField.selectOptions?.cascading,
emptyParentMessage: e.target.value,
},
},
})
}
placeholder="상위 항목을 먼저 선택하세요"
className="h-7 text-xs mt-1"
/>
</div>
<div>
<Label className="text-[10px]"> </Label>
<Input
value={localField.selectOptions?.cascading?.noOptionsMessage || ""}
onChange={(e) =>
updateField({
selectOptions: {
...localField.selectOptions,
cascading: {
...localField.selectOptions?.cascading,
noOptionsMessage: e.target.value,
},
},
})
}
placeholder="선택 가능한 항목이 없습니다"
className="h-7 text-xs mt-1"
/>
</div>
<div className="flex items-center justify-between">
<span className="text-[10px]"> </span>
<Switch
checked={localField.selectOptions?.cascading?.clearOnParentChange !== false}
onCheckedChange={(checked) =>
updateField({
selectOptions: {
...localField.selectOptions,
cascading: {
...localField.selectOptions?.cascading,
clearOnParentChange: checked,
},
},
})
}
/>
</div>
<HelpText> </HelpText>
</div>
</div>
)}
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
)} )}
@ -929,7 +1491,7 @@ export function FieldDetailSettingsModal({
preview = `${subLabel} - ${mainLabel}`; preview = `${subLabel} - ${mainLabel}`;
} else if (format === "name_code" && subCol) { } else if (format === "name_code" && subCol) {
preview = `${mainLabel} (${subLabel})`; preview = `${mainLabel} (${subLabel})`;
} else if (format !== "name_only" && !subCol) { } else if (!subCol) {
preview = `${mainLabel} (서브 컬럼을 선택하세요)`; preview = `${mainLabel} (서브 컬럼을 선택하세요)`;
} else { } else {
preview = mainLabel; preview = mainLabel;

View File

@ -7,7 +7,7 @@
// Select 옵션 설정 // Select 옵션 설정
export interface SelectOptionConfig { export interface SelectOptionConfig {
type?: "static" | "table" | "code"; // 옵션 타입 (기본: static) type?: "static" | "table" | "code" | "cascading"; // 옵션 타입 (기본: static)
// 정적 옵션 // 정적 옵션
staticOptions?: { value: string; label: string }[]; staticOptions?: { value: string; label: string }[];
// 테이블 기반 옵션 // 테이블 기반 옵션
@ -19,6 +19,19 @@ export interface SelectOptionConfig {
// 카테고리 컬럼 기반 옵션 (table_column_category_values 테이블) // 카테고리 컬럼 기반 옵션 (table_column_category_values 테이블)
// 형식: "tableName.columnName" (예: "sales_order_mng.incoterms") // 형식: "tableName.columnName" (예: "sales_order_mng.incoterms")
categoryKey?: string; categoryKey?: string;
// 연쇄 드롭다운 설정 (type이 "cascading"일 때 사용)
cascading?: {
parentField?: string; // 부모 필드명 (같은 폼 내)
relationCode?: string; // 관계 코드 (cascading_relation 테이블)
// 직접 설정 또는 관계 코드에서 가져온 값 수정 시 사용
sourceTable?: string; // 옵션을 조회할 테이블
parentKeyColumn?: string; // 부모 값과 매칭할 컬럼
// valueColumn, labelColumn은 상위 속성 사용
emptyParentMessage?: string; // 부모 미선택 시 메시지
noOptionsMessage?: string; // 옵션 없음 메시지
clearOnParentChange?: boolean; // 부모 변경 시 값 초기화 (기본: true)
};
} }
// 채번규칙 설정 // 채번규칙 설정
@ -873,6 +886,7 @@ export const SELECT_OPTION_TYPE_OPTIONS = [
{ value: "static", label: "직접 입력" }, { value: "static", label: "직접 입력" },
{ value: "table", label: "테이블 참조" }, { value: "table", label: "테이블 참조" },
{ value: "code", label: "공통코드" }, { value: "code", label: "공통코드" },
{ value: "cascading", label: "연쇄 드롭다운" },
] as const; ] as const;
// 연동 필드 표시 형식 옵션 // 연동 필드 표시 형식 옵션