엔티티 타입 다중 표시 컬럼 기능 구현

Frontend:
- EntityTypeConfig 인터페이스에 displayColumns 배열 추가
- EntityTypeConfigPanel에서 여러 표시 컬럼 선택 UI 구현
- 구분자 설정 기능 추가
- 하위 호환성을 위한 displayColumn 유지

Backend:
- EntityJoinConfig에 displayColumns 배열 지원
- 화면별 엔티티 설정을 전달받는 API 확장
- CONCAT을 사용한 다중 컬럼 표시 SQL 생성
- 기존 단일 컬럼과의 호환성 유지

이제 화면마다 다른 표시 컬럼 조합을 설정할 수 있음
예: 한 화면에서는 '이름'만, 다른 화면에서는 '이름 - 부서명' 표시
This commit is contained in:
kjs 2025-09-23 15:58:54 +09:00
parent 699efd25a2
commit 4aefb5be6a
6 changed files with 209 additions and 193 deletions

View File

@ -26,6 +26,7 @@ export class EntityJoinController {
sortOrder = "asc", sortOrder = "asc",
enableEntityJoin = true, enableEntityJoin = true,
additionalJoinColumns, // 추가 조인 컬럼 정보 (JSON 문자열) additionalJoinColumns, // 추가 조인 컬럼 정보 (JSON 문자열)
screenEntityConfigs, // 화면별 엔티티 설정 (JSON 문자열)
userLang, // userLang은 별도로 분리하여 search에 포함되지 않도록 함 userLang, // userLang은 별도로 분리하여 search에 포함되지 않도록 함
...otherParams ...otherParams
} = req.query; } = req.query;
@ -65,6 +66,21 @@ export class EntityJoinController {
} }
} }
// 화면별 엔티티 설정 처리
let parsedScreenEntityConfigs: Record<string, any> = {};
if (screenEntityConfigs) {
try {
parsedScreenEntityConfigs =
typeof screenEntityConfigs === "string"
? JSON.parse(screenEntityConfigs)
: screenEntityConfigs;
logger.info("화면별 엔티티 설정 파싱 완료:", parsedScreenEntityConfigs);
} catch (error) {
logger.warn("화면별 엔티티 설정 파싱 오류:", error);
parsedScreenEntityConfigs = {};
}
}
const result = await tableManagementService.getTableDataWithEntityJoins( const result = await tableManagementService.getTableDataWithEntityJoins(
tableName, tableName,
{ {
@ -79,6 +95,7 @@ export class EntityJoinController {
enableEntityJoin: enableEntityJoin:
enableEntityJoin === "true" || enableEntityJoin === true, enableEntityJoin === "true" || enableEntityJoin === true,
additionalJoinColumns: parsedAdditionalJoinColumns, additionalJoinColumns: parsedAdditionalJoinColumns,
screenEntityConfigs: parsedScreenEntityConfigs,
} }
); );

View File

@ -16,8 +16,13 @@ const prisma = new PrismaClient();
export class EntityJoinService { export class EntityJoinService {
/** /**
* Entity * Entity
* @param tableName
* @param screenEntityConfigs ()
*/ */
async detectEntityJoins(tableName: string): Promise<EntityJoinConfig[]> { async detectEntityJoins(
tableName: string,
screenEntityConfigs?: Record<string, any>
): Promise<EntityJoinConfig[]> {
try { try {
logger.info(`Entity 컬럼 감지 시작: ${tableName}`); logger.info(`Entity 컬럼 감지 시작: ${tableName}`);
@ -48,8 +53,22 @@ export class EntityJoinService {
continue; continue;
} }
// display_column이 없으면 reference_column 사용 // 화면별 엔티티 설정이 있으면 우선 사용, 없으면 기본값 사용
const displayColumn = column.display_column || column.reference_column; const screenConfig = screenEntityConfigs?.[column.column_name];
let displayColumns: string[] = [];
let separator = " - ";
if (screenConfig && screenConfig.displayColumns) {
// 화면에서 설정된 표시 컬럼들 사용
displayColumns = screenConfig.displayColumns;
separator = screenConfig.separator || " - ";
} else if (column.display_column) {
// 기존 설정된 단일 표시 컬럼 사용
displayColumns = [column.display_column];
} else {
// 기본값: reference_column 사용
displayColumns = [column.reference_column];
}
// 별칭 컬럼명 생성 (writer -> writer_name) // 별칭 컬럼명 생성 (writer -> writer_name)
const aliasColumn = `${column.column_name}_name`; const aliasColumn = `${column.column_name}_name`;
@ -59,8 +78,10 @@ export class EntityJoinService {
sourceColumn: column.column_name, sourceColumn: column.column_name,
referenceTable: column.reference_table, referenceTable: column.reference_table,
referenceColumn: column.reference_column, referenceColumn: column.reference_column,
displayColumn: displayColumn, displayColumns: displayColumns,
displayColumn: displayColumns[0], // 하위 호환성
aliasColumn: aliasColumn, aliasColumn: aliasColumn,
separator: separator,
}; };
// 조인 설정 유효성 검증 // 조인 설정 유효성 검증
@ -130,10 +151,22 @@ export class EntityJoinService {
}); });
const joinColumns = joinConfigs const joinColumns = joinConfigs
.map( .map((config) => {
(config) => const alias = aliasMap.get(config.referenceTable);
`COALESCE(${aliasMap.get(config.referenceTable)}.${config.displayColumn}, '') AS ${config.aliasColumn}` const displayColumns = config.displayColumns || [config.displayColumn];
) const separator = config.separator || " - ";
if (displayColumns.length === 1) {
// 단일 컬럼인 경우
return `COALESCE(${alias}.${displayColumns[0]}, '') AS ${config.aliasColumn}`;
} else {
// 여러 컬럼인 경우 CONCAT으로 연결
const concatParts = displayColumns
.map(col => `COALESCE(${alias}.${col}, '')`)
.join(`, '${separator}', `);
return `CONCAT(${concatParts}) AS ${config.aliasColumn}`;
}
})
.join(", "); .join(", ");
// SELECT 절 구성 // SELECT 절 구성

View File

@ -2023,6 +2023,7 @@ export class TableManagementService {
sourceColumn: string; sourceColumn: string;
joinAlias: string; joinAlias: string;
}>; }>;
screenEntityConfigs?: Record<string, any>; // 화면별 엔티티 설정
} }
): Promise<EntityJoinResponse> { ): Promise<EntityJoinResponse> {
const startTime = Date.now(); const startTime = Date.now();
@ -2042,8 +2043,8 @@ export class TableManagementService {
}; };
} }
// Entity 조인 설정 감지 // Entity 조인 설정 감지 (화면별 엔티티 설정 전달)
let joinConfigs = await entityJoinService.detectEntityJoins(tableName); let joinConfigs = await entityJoinService.detectEntityJoins(tableName, options.screenEntityConfigs);
// 추가 조인 컬럼 정보가 있으면 조인 설정에 추가 // 추가 조인 컬럼 정보가 있으면 조인 설정에 추가
if ( if (

View File

@ -77,8 +77,10 @@ export interface EntityJoinConfig {
sourceColumn: string; // writer sourceColumn: string; // writer
referenceTable: string; // user_info referenceTable: string; // user_info
referenceColumn: string; // user_id (조인 키) referenceColumn: string; // user_id (조인 키)
displayColumn: string; // user_name (표시할 값) displayColumns: string[]; // ['user_name', 'dept_name'] (표시할 값들)
displayColumn?: string; // user_name (하위 호환성용, deprecated)
aliasColumn: string; // writer_name (결과 컬럼명) aliasColumn: string; // writer_name (결과 컬럼명)
separator?: string; // ' - ' (여러 컬럼 연결 시 구분자)
} }
export interface EntityJoinResponse { export interface EntityJoinResponse {

View File

@ -18,40 +18,36 @@ interface EntityTypeConfigPanelProps {
export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ config, onConfigChange }) => { export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ config, onConfigChange }) => {
// 기본값이 설정된 config 사용 // 기본값이 설정된 config 사용
const safeConfig = { const safeConfig = {
entityName: "", referenceTable: "",
displayField: "name", referenceColumn: "id",
valueField: "id", displayColumns: config.displayColumns || (config.displayColumn ? [config.displayColumn] : ["name"]), // 호환성 처리
searchable: true, searchColumns: [],
multiple: false, filters: {},
allowClear: true,
placeholder: "", placeholder: "",
apiEndpoint: "",
filters: [],
displayFormat: "simple", displayFormat: "simple",
maxSelections: undefined, separator: " - ",
...config, ...config,
}; };
// 로컬 상태로 실시간 입력 관리 // 로컬 상태로 실시간 입력 관리
const [localValues, setLocalValues] = useState({ const [localValues, setLocalValues] = useState({
entityName: safeConfig.entityName, referenceTable: safeConfig.referenceTable,
displayField: safeConfig.displayField, referenceColumn: safeConfig.referenceColumn,
valueField: safeConfig.valueField, displayColumns: [...safeConfig.displayColumns],
searchable: safeConfig.searchable, searchColumns: [...(safeConfig.searchColumns || [])],
multiple: safeConfig.multiple,
allowClear: safeConfig.allowClear,
placeholder: safeConfig.placeholder, placeholder: safeConfig.placeholder,
apiEndpoint: safeConfig.apiEndpoint,
displayFormat: safeConfig.displayFormat, displayFormat: safeConfig.displayFormat,
maxSelections: safeConfig.maxSelections?.toString() || "", separator: safeConfig.separator,
}); });
const [newFilter, setNewFilter] = useState({ field: "", operator: "=", value: "" }); const [newFilter, setNewFilter] = useState({ field: "", operator: "=", value: "" });
const [newDisplayColumn, setNewDisplayColumn] = useState("");
const [availableColumns, setAvailableColumns] = useState<string[]>([]);
// 표시 형식 옵션 // 표시 형식 옵션
const displayFormats = [ const displayFormats = [
{ value: "simple", label: "단순 (이름만)" }, { value: "simple", label: "단순 (첫 번째 컬럼만)" },
{ value: "detailed", label: "상세 (이름 + 설명)" }, { value: "detailed", label: "상세 (모든 컬럼 표시)" },
{ value: "custom", label: "사용자 정의" }, { value: "custom", label: "사용자 정의" },
]; ];
@ -71,37 +67,27 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
// config가 변경될 때 로컬 상태 동기화 // config가 변경될 때 로컬 상태 동기화
useEffect(() => { useEffect(() => {
setLocalValues({ setLocalValues({
entityName: safeConfig.entityName, referenceTable: safeConfig.referenceTable,
displayField: safeConfig.displayField, referenceColumn: safeConfig.referenceColumn,
valueField: safeConfig.valueField, displayColumns: [...safeConfig.displayColumns],
searchable: safeConfig.searchable, searchColumns: [...(safeConfig.searchColumns || [])],
multiple: safeConfig.multiple,
allowClear: safeConfig.allowClear,
placeholder: safeConfig.placeholder, placeholder: safeConfig.placeholder,
apiEndpoint: safeConfig.apiEndpoint,
displayFormat: safeConfig.displayFormat, displayFormat: safeConfig.displayFormat,
maxSelections: safeConfig.maxSelections?.toString() || "", separator: safeConfig.separator,
}); });
}, [ }, [
safeConfig.entityName, safeConfig.referenceTable,
safeConfig.displayField, safeConfig.referenceColumn,
safeConfig.valueField, safeConfig.displayColumns,
safeConfig.searchable, safeConfig.searchColumns,
safeConfig.multiple,
safeConfig.allowClear,
safeConfig.placeholder, safeConfig.placeholder,
safeConfig.apiEndpoint,
safeConfig.displayFormat, safeConfig.displayFormat,
safeConfig.maxSelections, safeConfig.separator,
]); ]);
const updateConfig = (key: keyof EntityTypeConfig, value: any) => { const updateConfig = (key: keyof EntityTypeConfig, value: any) => {
// 로컬 상태 즉시 업데이트 // 로컬 상태 즉시 업데이트
if (key === "maxSelections") { setLocalValues((prev) => ({ ...prev, [key]: value }));
setLocalValues((prev) => ({ ...prev, [key]: value?.toString() || "" }));
} else {
setLocalValues((prev) => ({ ...prev, [key]: value }));
}
// 실제 config 업데이트 // 실제 config 업데이트
const newConfig = { ...safeConfig, [key]: value }; const newConfig = { ...safeConfig, [key]: value };
@ -114,82 +100,132 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
onConfigChange(newConfig); onConfigChange(newConfig);
}; };
// 표시 컬럼 추가
const addDisplayColumn = () => {
if (newDisplayColumn.trim() && !localValues.displayColumns.includes(newDisplayColumn.trim())) {
const updatedColumns = [...localValues.displayColumns, newDisplayColumn.trim()];
updateConfig("displayColumns", updatedColumns);
setNewDisplayColumn("");
}
};
// 표시 컬럼 제거
const removeDisplayColumn = (index: number) => {
const updatedColumns = localValues.displayColumns.filter((_, i) => i !== index);
updateConfig("displayColumns", updatedColumns);
};
const addFilter = () => { const addFilter = () => {
if (newFilter.field.trim() && newFilter.value.trim()) { if (newFilter.field.trim() && newFilter.value.trim()) {
const updatedFilters = [...(safeConfig.filters || []), { ...newFilter }]; const updatedFilters = { ...safeConfig.filters, [newFilter.field]: newFilter.value };
updateConfig("filters", updatedFilters); updateConfig("filters", updatedFilters);
setNewFilter({ field: "", operator: "=", value: "" }); setNewFilter({ field: "", operator: "=", value: "" });
} }
}; };
const removeFilter = (index: number) => { const removeFilter = (field: string) => {
const updatedFilters = (safeConfig.filters || []).filter((_, i) => i !== index); const updatedFilters = { ...safeConfig.filters };
delete updatedFilters[field];
updateConfig("filters", updatedFilters); updateConfig("filters", updatedFilters);
}; };
const updateFilter = (index: number, field: keyof typeof newFilter, value: string) => { const updateFilter = (oldField: string, field: string, value: string) => {
const updatedFilters = [...(safeConfig.filters || [])]; const updatedFilters = { ...safeConfig.filters };
updatedFilters[index] = { ...updatedFilters[index], [field]: value }; if (oldField !== field) {
delete updatedFilters[oldField];
}
updatedFilters[field] = value;
updateConfig("filters", updatedFilters); updateConfig("filters", updatedFilters);
}; };
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{/* 엔터티 이름 */} {/* 참조 테이블 */}
<div> <div>
<Label htmlFor="entityName" className="text-sm font-medium"> <Label htmlFor="referenceTable" className="text-sm font-medium">
</Label> </Label>
<Input <Input
id="entityName" id="referenceTable"
value={localValues.entityName} value={localValues.referenceTable}
onChange={(e) => updateConfig("entityName", e.target.value)} onChange={(e) => updateConfig("referenceTable", e.target.value)}
placeholder="예: User, Company, Product" placeholder="예: user_info, company_info"
className="mt-1" className="mt-1"
/> />
</div> </div>
{/* API 엔드포인트 */} {/* 조인 컬럼 (값 필드) */}
<div> <div>
<Label htmlFor="apiEndpoint" className="text-sm font-medium"> <Label htmlFor="referenceColumn" className="text-sm font-medium">
API ( )
</Label> </Label>
<Input <Input
id="apiEndpoint" id="referenceColumn"
value={localValues.apiEndpoint} value={localValues.referenceColumn}
onChange={(e) => updateConfig("apiEndpoint", e.target.value)} onChange={(e) => updateConfig("referenceColumn", e.target.value)}
placeholder="예: /api/users" placeholder="id, user_id, company_code"
className="mt-1" className="mt-1"
/> />
</div> </div>
{/* 필드 설정 */} {/* 표시 컬럼들 (다중 선택) */}
<div className="grid grid-cols-2 gap-3"> <div className="space-y-3">
<div> <Label className="text-sm font-medium"> </Label>
<Label htmlFor="valueField" className="text-sm font-medium">
{/* 현재 선택된 표시 컬럼들 */}
</Label> <div className="space-y-2">
<Input {localValues.displayColumns.map((column, index) => (
id="valueField" <div key={index} className="flex items-center space-x-2 rounded border bg-gray-50 p-2">
value={localValues.valueField} <Database className="h-4 w-4 text-gray-500" />
onChange={(e) => updateConfig("valueField", e.target.value)} <span className="flex-1 text-sm font-medium">{column}</span>
placeholder="id" <Button size="sm" variant="outline" onClick={() => removeDisplayColumn(index)}>
className="mt-1" <X className="h-3 w-3" />
/> </Button>
</div>
))}
{localValues.displayColumns.length === 0 && (
<div className="text-sm text-gray-500 italic"> </div>
)}
</div> </div>
<div> {/* 새 표시 컬럼 추가 */}
<Label htmlFor="displayField" className="text-sm font-medium"> <div className="flex items-center space-x-2">
</Label>
<Input <Input
id="displayField" value={newDisplayColumn}
value={localValues.displayField} onChange={(e) => setNewDisplayColumn(e.target.value)}
onChange={(e) => updateConfig("displayField", e.target.value)} placeholder="컬럼명 입력 (예: user_name, dept_name)"
placeholder="name" className="flex-1"
className="mt-1"
/> />
<Button
size="sm"
onClick={addDisplayColumn}
disabled={!newDisplayColumn.trim() || localValues.displayColumns.includes(newDisplayColumn.trim())}
>
<Plus className="h-3 w-3 mr-1" />
</Button>
</div> </div>
<div className="text-xs text-gray-500">
"{localValues.separator || ' - '}"
<br />
: 이름{localValues.separator || ' - '}
</div>
</div>
{/* 구분자 설정 */}
<div>
<Label htmlFor="separator" className="text-sm font-medium">
</Label>
<Input
id="separator"
value={localValues.separator}
onChange={(e) => updateConfig("separator", e.target.value)}
placeholder=" - "
className="mt-1"
/>
</div> </div>
{/* 표시 형식 */} {/* 표시 형식 */}
@ -225,59 +261,6 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
/> />
</div> </div>
{/* 옵션들 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="searchable" className="text-sm font-medium">
</Label>
<Checkbox
id="searchable"
checked={localValues.searchable}
onCheckedChange={(checked) => updateConfig("searchable", !!checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="multiple" className="text-sm font-medium">
</Label>
<Checkbox
id="multiple"
checked={localValues.multiple}
onCheckedChange={(checked) => updateConfig("multiple", !!checked)}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="allowClear" className="text-sm font-medium">
</Label>
<Checkbox
id="allowClear"
checked={localValues.allowClear}
onCheckedChange={(checked) => updateConfig("allowClear", !!checked)}
/>
</div>
</div>
{/* 최대 선택 개수 (다중 선택 시) */}
{localValues.multiple && (
<div>
<Label htmlFor="maxSelections" className="text-sm font-medium">
</Label>
<Input
id="maxSelections"
type="number"
min="1"
value={localValues.maxSelections}
onChange={(e) => updateConfig("maxSelections", e.target.value ? Number(e.target.value) : undefined)}
className="mt-1"
placeholder="제한 없음"
/>
</div>
)}
{/* 필터 관리 */} {/* 필터 관리 */}
<div className="space-y-3"> <div className="space-y-3">
@ -285,33 +268,22 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
{/* 기존 필터 목록 */} {/* 기존 필터 목록 */}
<div className="max-h-40 space-y-2 overflow-y-auto"> <div className="max-h-40 space-y-2 overflow-y-auto">
{(safeConfig.filters || []).map((filter, index) => ( {Object.entries(safeConfig.filters || {}).map(([field, value]) => (
<div key={index} className="flex items-center space-x-2 rounded border p-2 text-sm"> <div key={field} className="flex items-center space-x-2 rounded border p-2 text-sm">
<Input <Input
value={filter.field} value={field}
onChange={(e) => updateFilter(index, "field", e.target.value)} onChange={(e) => updateFilter(field, e.target.value, value as string)}
placeholder="필드명" placeholder="필드명"
className="flex-1" className="flex-1"
/> />
<Select value={filter.operator} onValueChange={(value) => updateFilter(index, "operator", value)}> <span className="text-gray-500">=</span>
<SelectTrigger className="w-20">
<SelectValue />
</SelectTrigger>
<SelectContent>
{operators.map((op) => (
<SelectItem key={op.value} value={op.value}>
{op.value}
</SelectItem>
))}
</SelectContent>
</Select>
<Input <Input
value={filter.value} value={value as string}
onChange={(e) => updateFilter(index, "value", e.target.value)} onChange={(e) => updateFilter(field, field, e.target.value)}
placeholder="값" placeholder="값"
className="flex-1" className="flex-1"
/> />
<Button size="sm" variant="outline" onClick={() => removeFilter(index)}> <Button size="sm" variant="outline" onClick={() => removeFilter(field)}>
<X className="h-3 w-3" /> <X className="h-3 w-3" />
</Button> </Button>
</div> </div>
@ -326,21 +298,7 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
placeholder="필드명" placeholder="필드명"
className="flex-1" className="flex-1"
/> />
<Select <span className="text-gray-500">=</span>
value={newFilter.operator}
onValueChange={(value) => setNewFilter((prev) => ({ ...prev, operator: value }))}
>
<SelectTrigger className="w-20">
<SelectValue />
</SelectTrigger>
<SelectContent>
{operators.map((op) => (
<SelectItem key={op.value} value={op.value}>
{op.value}
</SelectItem>
))}
</SelectContent>
</Select>
<Input <Input
value={newFilter.value} value={newFilter.value}
onChange={(e) => setNewFilter((prev) => ({ ...prev, value: e.target.value }))} onChange={(e) => setNewFilter((prev) => ({ ...prev, value: e.target.value }))}
@ -352,7 +310,7 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
</Button> </Button>
</div> </div>
<div className="text-xs text-gray-500"> {(safeConfig.filters || []).length} </div> <div className="text-xs text-gray-500"> {Object.keys(safeConfig.filters || {}).length} </div>
</div> </div>
{/* 미리보기 */} {/* 미리보기 */}
@ -360,31 +318,33 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
<Label className="text-sm font-medium text-gray-700"></Label> <Label className="text-sm font-medium text-gray-700"></Label>
<div className="mt-2"> <div className="mt-2">
<div className="flex items-center space-x-2 rounded border bg-white p-2"> <div className="flex items-center space-x-2 rounded border bg-white p-2">
{localValues.searchable && <Search className="h-4 w-4 text-gray-400" />} <Search className="h-4 w-4 text-gray-400" />
<div className="flex-1 text-sm text-gray-600"> <div className="flex-1 text-sm text-gray-600">
{localValues.placeholder || `${localValues.entityName || "엔터티"}를 선택하세요`} {localValues.placeholder || `${localValues.referenceTable || "엔터티"}를 선택하세요`}
</div> </div>
<Database className="h-4 w-4 text-gray-400" /> <Database className="h-4 w-4 text-gray-400" />
</div> </div>
</div> </div>
<div className="mt-2 text-xs text-gray-500"> <div className="mt-2 text-xs text-gray-500">
: {localValues.entityName || "없음"}, API: {localValues.apiEndpoint || "없음"}, :{" "} : {localValues.referenceTable || "없음"}, : {localValues.referenceColumn}
{localValues.valueField}, : {localValues.displayField} <br />
{localValues.multiple && `, 다중선택`} : {localValues.displayColumns.length > 0 ? localValues.displayColumns.join(localValues.separator || ' - ') : "없음"}
{localValues.searchable && `, 검색가능`}
</div> </div>
</div> </div>
{/* 안내 메시지 */} {/* 안내 메시지 */}
<div className="rounded-md bg-blue-50 p-3"> <div className="rounded-md bg-blue-50 p-3">
<div className="text-sm font-medium text-blue-900"> </div> <div className="text-sm font-medium text-blue-900"> </div>
<div className="mt-1 text-xs text-blue-800"> <div className="mt-1 text-xs text-blue-800">
<strong> </strong>:
<br /> <br />
API <strong> </strong>: ( ID)
<br /> <br />
<strong> </strong>: ( )
<br /> , <br />
<br />
: 사용자 "이름" "이름 - 부서명"
</div> </div>
</div> </div>
</div> </div>

View File

@ -192,10 +192,13 @@ export interface FileTypeConfig {
export interface EntityTypeConfig { export interface EntityTypeConfig {
referenceTable: string; referenceTable: string;
referenceColumn: string; referenceColumn: string;
displayColumn: string; displayColumns: string[]; // 여러 표시 컬럼을 배열로 변경
displayColumn?: string; // 하위 호환성을 위해 유지 (deprecated)
searchColumns?: string[]; searchColumns?: string[];
filters?: Record<string, unknown>; filters?: Record<string, unknown>;
placeholder?: string; placeholder?: string;
displayFormat?: 'simple' | 'detailed' | 'custom'; // 표시 형식
separator?: string; // 여러 컬럼 표시 시 구분자 (기본: ' - ')
} }
/** /**