엔티티 타입 다중 표시 컬럼 기능 구현
Frontend: - EntityTypeConfig 인터페이스에 displayColumns 배열 추가 - EntityTypeConfigPanel에서 여러 표시 컬럼 선택 UI 구현 - 구분자 설정 기능 추가 - 하위 호환성을 위한 displayColumn 유지 Backend: - EntityJoinConfig에 displayColumns 배열 지원 - 화면별 엔티티 설정을 전달받는 API 확장 - CONCAT을 사용한 다중 컬럼 표시 SQL 생성 - 기존 단일 컬럼과의 호환성 유지 이제 화면마다 다른 표시 컬럼 조합을 설정할 수 있음 예: 한 화면에서는 '이름'만, 다른 화면에서는 '이름 - 부서명' 표시
This commit is contained in:
parent
699efd25a2
commit
4aefb5be6a
|
|
@ -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,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 절 구성
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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?.toString() || "" }));
|
|
||||||
} else {
|
|
||||||
setLocalValues((prev) => ({ ...prev, [key]: value }));
|
setLocalValues((prev) => ({ ...prev, [key]: value }));
|
||||||
}
|
|
||||||
|
|
||||||
// 실제 config 업데이트
|
// 실제 config 업데이트
|
||||||
const newConfig = { ...safeConfig, [key]: value };
|
const newConfig = { ...safeConfig, [key]: value };
|
||||||
|
|
@ -114,83 +100,133 @@ 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 className="flex items-center space-x-2">
|
||||||
|
<Input
|
||||||
|
value={newDisplayColumn}
|
||||||
|
onChange={(e) => setNewDisplayColumn(e.target.value)}
|
||||||
|
placeholder="컬럼명 입력 (예: user_name, dept_name)"
|
||||||
|
className="flex-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 className="text-xs text-gray-500">
|
||||||
|
• 여러 컬럼을 선택하면 "{localValues.separator || ' - '}"로 구분하여 표시됩니다
|
||||||
|
<br />
|
||||||
|
• 예: 이름{localValues.separator || ' - '}부서명
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 구분자 설정 */}
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="displayField" className="text-sm font-medium">
|
<Label htmlFor="separator" className="text-sm font-medium">
|
||||||
표시 필드
|
구분자
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="displayField"
|
id="separator"
|
||||||
value={localValues.displayField}
|
value={localValues.separator}
|
||||||
onChange={(e) => updateConfig("displayField", e.target.value)}
|
onChange={(e) => updateConfig("separator", e.target.value)}
|
||||||
placeholder="name"
|
placeholder=" - "
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 표시 형식 */}
|
{/* 표시 형식 */}
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -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; // 여러 컬럼 표시 시 구분자 (기본: ' - ')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue