feat: Enhance entity options retrieval with additional fields support
- Updated the `getEntityOptions` function to accept an optional `fields` parameter, allowing clients to specify additional columns to be retrieved. - Implemented logic to dynamically include extra columns in the SQL query based on the provided `fields`, improving flexibility in data retrieval. - Enhanced the response to indicate whether extra fields were included, facilitating better client-side handling of the data. - Added logging for authentication failures in the `AuthGuard` component to improve debugging and user experience. - Integrated auto-fill functionality in the `V2Select` component to automatically populate fields based on selected entity references, enhancing user interaction. - Updated the `ItemSearchModal` to support multi-selection of items, improving usability in item management scenarios.
This commit is contained in:
@@ -8,6 +8,76 @@ import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||
|
||||
// 통합 폼 시스템 import
|
||||
import { useV2FormOptional } from "@/components/v2/V2FormContext";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
|
||||
// 컬럼 메타데이터 캐시 (테이블명 → 컬럼 설정 맵)
|
||||
const columnMetaCache: Record<string, Record<string, any>> = {};
|
||||
const columnMetaLoading: Record<string, Promise<void>> = {};
|
||||
|
||||
async function loadColumnMeta(tableName: string): Promise<void> {
|
||||
if (columnMetaCache[tableName] || columnMetaLoading[tableName]) return;
|
||||
|
||||
columnMetaLoading[tableName] = (async () => {
|
||||
try {
|
||||
const response = await apiClient.get(`/table-management/tables/${tableName}/columns?size=1000`);
|
||||
const data = response.data.data || response.data;
|
||||
const columns = data.columns || data || [];
|
||||
const map: Record<string, any> = {};
|
||||
for (const col of columns) {
|
||||
const name = col.column_name || col.columnName;
|
||||
if (name) map[name] = col;
|
||||
}
|
||||
columnMetaCache[tableName] = map;
|
||||
} catch {
|
||||
columnMetaCache[tableName] = {};
|
||||
} finally {
|
||||
delete columnMetaLoading[tableName];
|
||||
}
|
||||
})();
|
||||
|
||||
await columnMetaLoading[tableName];
|
||||
}
|
||||
|
||||
// table_type_columns 기반 componentConfig 병합 (기존 설정이 없을 때만 DB 메타데이터로 보완)
|
||||
function mergeColumnMeta(tableName: string | undefined, columnName: string | undefined, componentConfig: any): any {
|
||||
if (!tableName || !columnName) return componentConfig;
|
||||
|
||||
const meta = columnMetaCache[tableName]?.[columnName];
|
||||
if (!meta) return componentConfig;
|
||||
|
||||
const inputType = meta.input_type || meta.inputType;
|
||||
if (!inputType) return componentConfig;
|
||||
|
||||
// 이미 source가 올바르게 설정된 경우 건드리지 않음
|
||||
const existingSource = componentConfig?.source;
|
||||
if (existingSource && existingSource !== "static" && existingSource !== "distinct" && existingSource !== "select") {
|
||||
return componentConfig;
|
||||
}
|
||||
|
||||
const merged = { ...componentConfig };
|
||||
|
||||
// source가 미설정/기본값일 때만 DB 메타데이터로 보완
|
||||
if (inputType === "entity") {
|
||||
const refTable = meta.reference_table || meta.referenceTable;
|
||||
const refColumn = meta.reference_column || meta.referenceColumn;
|
||||
const displayCol = meta.display_column || meta.displayColumn;
|
||||
if (refTable && !merged.entityTable) {
|
||||
merged.source = "entity";
|
||||
merged.entityTable = refTable;
|
||||
merged.entityValueColumn = refColumn || "id";
|
||||
merged.entityLabelColumn = displayCol || "name";
|
||||
}
|
||||
} else if (inputType === "category" && !existingSource) {
|
||||
merged.source = "category";
|
||||
} else if (inputType === "select" && !existingSource) {
|
||||
const detail = typeof meta.detail_settings === "string" ? JSON.parse(meta.detail_settings || "{}") : (meta.detail_settings || {});
|
||||
if (detail.options && !merged.options?.length) {
|
||||
merged.options = detail.options;
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
// 컴포넌트 렌더러 인터페이스
|
||||
export interface ComponentRenderer {
|
||||
@@ -175,6 +245,15 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
// 컬럼 메타데이터 로드 트리거 (테이블명이 있으면 비동기 로드)
|
||||
const screenTableName = props.tableName || (component as any).tableName;
|
||||
const [, forceUpdate] = React.useState(0);
|
||||
React.useEffect(() => {
|
||||
if (screenTableName && !columnMetaCache[screenTableName]) {
|
||||
loadColumnMeta(screenTableName).then(() => forceUpdate((v) => v + 1));
|
||||
}
|
||||
}, [screenTableName]);
|
||||
|
||||
// 컴포넌트 타입 추출 - 새 시스템에서는 componentType 속성 사용, 레거시는 type 사용
|
||||
// 🆕 V2 레이아웃의 경우 url에서 컴포넌트 타입 추출 (예: "@/lib/registry/components/v2-input" → "v2-input")
|
||||
const extractTypeFromUrl = (url: string | undefined): string | undefined => {
|
||||
@@ -550,24 +629,34 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||
height: finalStyle.height,
|
||||
};
|
||||
|
||||
// 컬럼 메타데이터 기반 componentConfig 병합 (DB 최신 설정 우선)
|
||||
const isEntityJoinColumn = fieldName?.includes(".");
|
||||
const baseColumnName = isEntityJoinColumn ? undefined : fieldName;
|
||||
const mergedComponentConfig = mergeColumnMeta(screenTableName, baseColumnName, component.componentConfig || {});
|
||||
|
||||
// 엔티티 조인 컬럼은 런타임에서 readonly/disabled 강제 해제
|
||||
const effectiveComponent = isEntityJoinColumn
|
||||
? { ...component, componentConfig: mergedComponentConfig, readonly: false }
|
||||
: { ...component, componentConfig: mergedComponentConfig };
|
||||
|
||||
const rendererProps = {
|
||||
component,
|
||||
component: effectiveComponent,
|
||||
isSelected,
|
||||
onClick,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
size: component.size || newComponent.defaultSize,
|
||||
position: component.position,
|
||||
config: component.componentConfig,
|
||||
componentConfig: component.componentConfig,
|
||||
config: mergedComponentConfig,
|
||||
componentConfig: mergedComponentConfig,
|
||||
// componentConfig의 모든 속성을 props로 spread (tableName, displayField 등)
|
||||
...(component.componentConfig || {}),
|
||||
...(mergedComponentConfig || {}),
|
||||
// 🔧 style은 맨 마지막에! (componentConfig.style이 있어도 mergedStyle이 우선)
|
||||
style: mergedStyle,
|
||||
// 🆕 라벨 표시 (labelDisplay가 true일 때만)
|
||||
label: effectiveLabel,
|
||||
// 🆕 V2 레이아웃에서 overrides에서 복원된 상위 레벨 속성들도 전달
|
||||
inputType: (component as any).inputType || component.componentConfig?.inputType,
|
||||
// 🆕 V2 레이아웃에서 overrides에서 복원된 상위 레벨 속성들도 전달 (DB 메타데이터 우선)
|
||||
inputType: (baseColumnName && columnMetaCache[screenTableName || ""]?.[baseColumnName]?.input_type) || (component as any).inputType || mergedComponentConfig?.inputType,
|
||||
columnName: (component as any).columnName || component.componentConfig?.columnName,
|
||||
value: currentValue, // formData에서 추출한 현재 값 전달
|
||||
// 새로운 기능들 전달
|
||||
@@ -607,9 +696,8 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||
// componentConfig.mode가 있으면 유지 (entity-search-input의 UI 모드)
|
||||
mode: component.componentConfig?.mode || mode,
|
||||
isInModal,
|
||||
readonly: component.readonly,
|
||||
// 🆕 disabledFields 체크 또는 기존 readonly
|
||||
disabled: disabledFields?.includes(fieldName) || component.readonly,
|
||||
readonly: isEntityJoinColumn ? false : component.readonly,
|
||||
disabled: isEntityJoinColumn ? false : (disabledFields?.includes(fieldName) || component.readonly),
|
||||
originalData,
|
||||
allComponents,
|
||||
onUpdateLayout,
|
||||
|
||||
Reference in New Issue
Block a user