From ef27e0e38f52c95a8c34a73d3274ecc6f9697aa5 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 13 Jan 2026 18:26:41 +0900 Subject: [PATCH] =?UTF-8?q?feat(universal-form-modal):=20=EC=97=B0?= =?UTF-8?q?=EC=87=84=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4(Cascading=20Dr?= =?UTF-8?q?opdown)=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SelectOptionConfig에 cascading 타입 및 설정 객체 추가 - FieldDetailSettingsModal에 연쇄 드롭다운 설정 UI 구현 - 부모 필드 선택 (섹션별 그룹핑 콤보박스) - 관계 코드 선택 시 상세 설정 자동 채움 - 소스 테이블, 부모 키 컬럼, 값/라벨 컬럼 설정 - UniversalFormModalComponent에 자식 필드 초기화 로직 추가 - selectOptions.cascading 방식 CascadingSelectField 렌더링 지원 --- .../UniversalFormModalComponent.tsx | 61 +- .../UniversalFormModalConfigPanel.tsx | 8 + .../modals/FieldDetailSettingsModal.tsx | 580 +++++++++++++++++- .../components/universal-form-modal/types.ts | 16 +- 4 files changed, 653 insertions(+), 12 deletions(-) diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx index 3e043331..e6976844 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx @@ -84,7 +84,7 @@ const CascadingSelectField: React.FC = ({ return ( - updateField({ - selectOptions: { - ...localField.selectOptions, - type: value as "static" | "code", - }, - }) - } + onValueChange={(value) => { + // 타입 변경 시 관련 설정 초기화 + if (value === "cascading") { + updateField({ + selectOptions: { + type: "cascading", + cascading: { + parentField: "", + clearOnParentChange: true, + }, + }, + }); + } else { + updateField({ + selectOptions: { + ...localField.selectOptions, + type: value as "static" | "table" | "code", + cascading: undefined, + }, + }); + } + }} > @@ -382,6 +473,11 @@ export function FieldDetailSettingsModal({ ))} + + {localField.selectOptions?.type === "cascading" + ? "연쇄 드롭다운: 부모 필드 선택에 따라 옵션이 동적으로 변경됩니다" + : "테이블 참조: DB 테이블에서 옵션 목록을 가져옵니다."} + {localField.selectOptions?.type === "table" && ( @@ -594,6 +690,472 @@ export function FieldDetailSettingsModal({ )} + + {localField.selectOptions?.type === "cascading" && ( +
+ + 연쇄 드롭다운: 부모 필드의 값에 따라 옵션이 동적으로 필터링됩니다. +
+ 예: 거래처 선택 → 해당 거래처의 납품처만 표시 +
+ + {/* 부모 필드 선택 - 콤보박스 (섹션별 그룹핑) */} +
+ + {allFieldsWithSections.length > 0 ? ( + + + + + + + + + + 선택 가능한 필드가 없습니다. + + {allFieldsWithSections.map((section) => { + // 자기 자신 제외한 필드 목록 + const availableFields = section.fields.filter( + (f) => f.columnName !== field.columnName + ); + if (availableFields.length === 0) return null; + + return ( + + {availableFields.map((f) => ( + { + updateField({ + selectOptions: { + ...localField.selectOptions, + cascading: { + ...localField.selectOptions?.cascading, + parentField: f.columnName, + }, + }, + }); + setParentFieldOpen(false); + }} + className="text-xs" + > + +
+ {f.label} + + {f.columnName} ({f.fieldType}) + +
+
+ ))} +
+ ); + })} +
+
+
+
+ ) : ( + + updateField({ + selectOptions: { + ...localField.selectOptions, + cascading: { + ...localField.selectOptions?.cascading, + parentField: e.target.value, + }, + }, + }) + } + placeholder="customer_code" + className="h-7 text-xs mt-1" + /> + )} + + 이 드롭다운의 옵션을 결정할 부모 필드를 선택하세요 +
+ 예: 거래처 선택 → 납품처 필터링 +
+
+ + {/* 관계 코드 선택 */} +
+ + + + + + + + + + + 등록된 연쇄 관계가 없습니다. + + + {/* 직접 설정 옵션 */} + { + updateField({ + selectOptions: { + ...localField.selectOptions, + cascading: { + ...localField.selectOptions?.cascading, + relationCode: undefined, + }, + }, + }); + setCascadingRelationOpen(false); + }} + className="text-xs" + > + + 직접 설정 + + + {cascadingRelations.map((relation) => ( + { + handleRelationCodeSelect(relation.relation_code); + setCascadingRelationOpen(false); + }} + className="text-xs" + > + +
+ {relation.relation_name} + + {relation.parent_table} → {relation.child_table} + +
+
+ ))} +
+
+
+
+
+ + 미리 등록된 관계를 선택하면 설정이 자동으로 채워집니다. +
+ 직접 설정을 선택하면 아래에서 수동으로 입력할 수 있습니다. +
+
+ + + + {/* 상세 설정 (수정 가능) */} +
+
+ + 상세 설정 (수정 가능) +
+ +
+ + + 옵션을 가져올 테이블 (예: delivery_destination) +
+ +
+ + {selectTableColumns.length > 0 ? ( + + ) : ( + + updateField({ + selectOptions: { + ...localField.selectOptions, + cascading: { + ...localField.selectOptions?.cascading, + parentKeyColumn: e.target.value, + }, + }, + }) + } + placeholder="customer_code" + className="h-7 text-xs mt-1" + /> + )} + 부모 값과 매칭할 컬럼 (예: customer_code) +
+ +
+ + {selectTableColumns.length > 0 ? ( + + ) : ( + + updateField({ + selectOptions: { + ...localField.selectOptions, + valueColumn: e.target.value, + }, + }) + } + placeholder="destination_code" + className="h-7 text-xs mt-1" + /> + )} + 드롭다운 value로 사용할 컬럼 +
+ +
+ + {selectTableColumns.length > 0 ? ( + + ) : ( + + updateField({ + selectOptions: { + ...localField.selectOptions, + labelColumn: e.target.value, + }, + }) + } + placeholder="destination_name" + className="h-7 text-xs mt-1" + /> + )} + 드롭다운에 표시할 컬럼 +
+ + + +
+ + + updateField({ + selectOptions: { + ...localField.selectOptions, + cascading: { + ...localField.selectOptions?.cascading, + emptyParentMessage: e.target.value, + }, + }, + }) + } + placeholder="상위 항목을 먼저 선택하세요" + className="h-7 text-xs mt-1" + /> +
+ +
+ + + updateField({ + selectOptions: { + ...localField.selectOptions, + cascading: { + ...localField.selectOptions?.cascading, + noOptionsMessage: e.target.value, + }, + }, + }) + } + placeholder="선택 가능한 항목이 없습니다" + className="h-7 text-xs mt-1" + /> +
+ +
+ 부모 변경 시 값 초기화 + + updateField({ + selectOptions: { + ...localField.selectOptions, + cascading: { + ...localField.selectOptions?.cascading, + clearOnParentChange: checked, + }, + }, + }) + } + /> +
+ 부모 필드 값이 변경되면 이 필드의 값을 자동으로 초기화합니다 +
+
+ )} )} @@ -929,7 +1491,7 @@ export function FieldDetailSettingsModal({ preview = `${subLabel} - ${mainLabel}`; } else if (format === "name_code" && subCol) { preview = `${mainLabel} (${subLabel})`; - } else if (format !== "name_only" && !subCol) { + } else if (!subCol) { preview = `${mainLabel} (서브 컬럼을 선택하세요)`; } else { preview = mainLabel; diff --git a/frontend/lib/registry/components/universal-form-modal/types.ts b/frontend/lib/registry/components/universal-form-modal/types.ts index c5ecb1cc..72e29d58 100644 --- a/frontend/lib/registry/components/universal-form-modal/types.ts +++ b/frontend/lib/registry/components/universal-form-modal/types.ts @@ -7,7 +7,7 @@ // Select 옵션 설정 export interface SelectOptionConfig { - type?: "static" | "table" | "code"; // 옵션 타입 (기본: static) + type?: "static" | "table" | "code" | "cascading"; // 옵션 타입 (기본: static) // 정적 옵션 staticOptions?: { value: string; label: string }[]; // 테이블 기반 옵션 @@ -19,6 +19,19 @@ export interface SelectOptionConfig { // 카테고리 컬럼 기반 옵션 (table_column_category_values 테이블) // 형식: "tableName.columnName" (예: "sales_order_mng.incoterms") 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: "table", label: "테이블 참조" }, { value: "code", label: "공통코드" }, + { value: "cascading", label: "연쇄 드롭다운" }, ] as const; // 연동 필드 표시 형식 옵션