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:
DDD1542
2026-02-25 11:45:28 +09:00
parent 72068d003a
commit 2b175a21f4
15 changed files with 828 additions and 129 deletions
@@ -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,