diff --git a/backend-node/src/controllers/entityJoinController.ts b/backend-node/src/controllers/entityJoinController.ts index 53c5de22..12c04ba4 100644 --- a/backend-node/src/controllers/entityJoinController.ts +++ b/backend-node/src/controllers/entityJoinController.ts @@ -26,6 +26,7 @@ export class EntityJoinController { sortOrder = "asc", enableEntityJoin = true, additionalJoinColumns, // 추가 조인 컬럼 정보 (JSON 문자열) + screenEntityConfigs, // 화면별 엔티티 설정 (JSON 문자열) userLang, // userLang은 별도로 분리하여 search에 포함되지 않도록 함 ...otherParams } = req.query; @@ -65,6 +66,21 @@ export class EntityJoinController { } } + // 화면별 엔티티 설정 처리 + let parsedScreenEntityConfigs: Record = {}; + 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( tableName, { @@ -79,6 +95,7 @@ export class EntityJoinController { enableEntityJoin: enableEntityJoin === "true" || enableEntityJoin === true, additionalJoinColumns: parsedAdditionalJoinColumns, + screenEntityConfigs: parsedScreenEntityConfigs, } ); diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index f84cf167..de3328fb 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -16,8 +16,13 @@ const prisma = new PrismaClient(); export class EntityJoinService { /** * 테이블의 Entity 컬럼들을 감지하여 조인 설정 생성 + * @param tableName 테이블명 + * @param screenEntityConfigs 화면별 엔티티 설정 (선택사항) */ - async detectEntityJoins(tableName: string): Promise { + async detectEntityJoins( + tableName: string, + screenEntityConfigs?: Record + ): Promise { try { logger.info(`Entity 컬럼 감지 시작: ${tableName}`); @@ -48,8 +53,22 @@ export class EntityJoinService { 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) const aliasColumn = `${column.column_name}_name`; @@ -59,8 +78,10 @@ export class EntityJoinService { sourceColumn: column.column_name, referenceTable: column.reference_table, referenceColumn: column.reference_column, - displayColumn: displayColumn, + displayColumns: displayColumns, + displayColumn: displayColumns[0], // 하위 호환성 aliasColumn: aliasColumn, + separator: separator, }; // 조인 설정 유효성 검증 @@ -130,10 +151,22 @@ export class EntityJoinService { }); const joinColumns = joinConfigs - .map( - (config) => - `COALESCE(${aliasMap.get(config.referenceTable)}.${config.displayColumn}, '') AS ${config.aliasColumn}` - ) + .map((config) => { + const alias = aliasMap.get(config.referenceTable); + 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(", "); // SELECT 절 구성 diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 94f8aa30..c5de403d 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -2023,6 +2023,7 @@ export class TableManagementService { sourceColumn: string; joinAlias: string; }>; + screenEntityConfigs?: Record; // 화면별 엔티티 설정 } ): Promise { const startTime = Date.now(); @@ -2042,8 +2043,8 @@ export class TableManagementService { }; } - // Entity 조인 설정 감지 - let joinConfigs = await entityJoinService.detectEntityJoins(tableName); + // Entity 조인 설정 감지 (화면별 엔티티 설정 전달) + let joinConfigs = await entityJoinService.detectEntityJoins(tableName, options.screenEntityConfigs); // 추가 조인 컬럼 정보가 있으면 조인 설정에 추가 if ( diff --git a/backend-node/src/types/tableManagement.ts b/backend-node/src/types/tableManagement.ts index 52dca092..ee5e97b1 100644 --- a/backend-node/src/types/tableManagement.ts +++ b/backend-node/src/types/tableManagement.ts @@ -77,8 +77,10 @@ export interface EntityJoinConfig { sourceColumn: string; // writer referenceTable: string; // user_info referenceColumn: string; // user_id (조인 키) - displayColumn: string; // user_name (표시할 값) + displayColumns: string[]; // ['user_name', 'dept_name'] (표시할 값들) + displayColumn?: string; // user_name (하위 호환성용, deprecated) aliasColumn: string; // writer_name (결과 컬럼명) + separator?: string; // ' - ' (여러 컬럼 연결 시 구분자) } export interface EntityJoinResponse { diff --git a/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx b/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx index a3505430..866bd65a 100644 --- a/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx +++ b/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx @@ -18,40 +18,36 @@ interface EntityTypeConfigPanelProps { export const EntityTypeConfigPanel: React.FC = ({ config, onConfigChange }) => { // 기본값이 설정된 config 사용 const safeConfig = { - entityName: "", - displayField: "name", - valueField: "id", - searchable: true, - multiple: false, - allowClear: true, + referenceTable: "", + referenceColumn: "id", + displayColumns: config.displayColumns || (config.displayColumn ? [config.displayColumn] : ["name"]), // 호환성 처리 + searchColumns: [], + filters: {}, placeholder: "", - apiEndpoint: "", - filters: [], displayFormat: "simple", - maxSelections: undefined, + separator: " - ", ...config, }; // 로컬 상태로 실시간 입력 관리 const [localValues, setLocalValues] = useState({ - entityName: safeConfig.entityName, - displayField: safeConfig.displayField, - valueField: safeConfig.valueField, - searchable: safeConfig.searchable, - multiple: safeConfig.multiple, - allowClear: safeConfig.allowClear, + referenceTable: safeConfig.referenceTable, + referenceColumn: safeConfig.referenceColumn, + displayColumns: [...safeConfig.displayColumns], + searchColumns: [...(safeConfig.searchColumns || [])], placeholder: safeConfig.placeholder, - apiEndpoint: safeConfig.apiEndpoint, displayFormat: safeConfig.displayFormat, - maxSelections: safeConfig.maxSelections?.toString() || "", + separator: safeConfig.separator, }); const [newFilter, setNewFilter] = useState({ field: "", operator: "=", value: "" }); + const [newDisplayColumn, setNewDisplayColumn] = useState(""); + const [availableColumns, setAvailableColumns] = useState([]); // 표시 형식 옵션 const displayFormats = [ - { value: "simple", label: "단순 (이름만)" }, - { value: "detailed", label: "상세 (이름 + 설명)" }, + { value: "simple", label: "단순 (첫 번째 컬럼만)" }, + { value: "detailed", label: "상세 (모든 컬럼 표시)" }, { value: "custom", label: "사용자 정의" }, ]; @@ -71,37 +67,27 @@ export const EntityTypeConfigPanel: React.FC = ({ co // config가 변경될 때 로컬 상태 동기화 useEffect(() => { setLocalValues({ - entityName: safeConfig.entityName, - displayField: safeConfig.displayField, - valueField: safeConfig.valueField, - searchable: safeConfig.searchable, - multiple: safeConfig.multiple, - allowClear: safeConfig.allowClear, + referenceTable: safeConfig.referenceTable, + referenceColumn: safeConfig.referenceColumn, + displayColumns: [...safeConfig.displayColumns], + searchColumns: [...(safeConfig.searchColumns || [])], placeholder: safeConfig.placeholder, - apiEndpoint: safeConfig.apiEndpoint, displayFormat: safeConfig.displayFormat, - maxSelections: safeConfig.maxSelections?.toString() || "", + separator: safeConfig.separator, }); }, [ - safeConfig.entityName, - safeConfig.displayField, - safeConfig.valueField, - safeConfig.searchable, - safeConfig.multiple, - safeConfig.allowClear, + safeConfig.referenceTable, + safeConfig.referenceColumn, + safeConfig.displayColumns, + safeConfig.searchColumns, safeConfig.placeholder, - safeConfig.apiEndpoint, safeConfig.displayFormat, - safeConfig.maxSelections, + safeConfig.separator, ]); 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 업데이트 const newConfig = { ...safeConfig, [key]: value }; @@ -114,82 +100,132 @@ export const EntityTypeConfigPanel: React.FC = ({ co 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 = () => { if (newFilter.field.trim() && newFilter.value.trim()) { - const updatedFilters = [...(safeConfig.filters || []), { ...newFilter }]; + const updatedFilters = { ...safeConfig.filters, [newFilter.field]: newFilter.value }; updateConfig("filters", updatedFilters); setNewFilter({ field: "", operator: "=", value: "" }); } }; - const removeFilter = (index: number) => { - const updatedFilters = (safeConfig.filters || []).filter((_, i) => i !== index); + const removeFilter = (field: string) => { + const updatedFilters = { ...safeConfig.filters }; + delete updatedFilters[field]; updateConfig("filters", updatedFilters); }; - const updateFilter = (index: number, field: keyof typeof newFilter, value: string) => { - const updatedFilters = [...(safeConfig.filters || [])]; - updatedFilters[index] = { ...updatedFilters[index], [field]: value }; + const updateFilter = (oldField: string, field: string, value: string) => { + const updatedFilters = { ...safeConfig.filters }; + if (oldField !== field) { + delete updatedFilters[oldField]; + } + updatedFilters[field] = value; updateConfig("filters", updatedFilters); }; return (
- {/* 엔터티 이름 */} + {/* 참조 테이블 */}
-
- {/* API 엔드포인트 */} + {/* 조인 컬럼 (값 필드) */}
-
- {/* 필드 설정 */} -
-
- - updateConfig("valueField", e.target.value)} - placeholder="id" - className="mt-1" - /> + {/* 표시 컬럼들 (다중 선택) */} +
+ + + {/* 현재 선택된 표시 컬럼들 */} +
+ {localValues.displayColumns.map((column, index) => ( +
+ + {column} + +
+ ))} + + {localValues.displayColumns.length === 0 && ( +
표시할 컬럼을 추가해주세요
+ )}
-
- + {/* 새 표시 컬럼 추가 */} +
updateConfig("displayField", e.target.value)} - placeholder="name" - className="mt-1" + value={newDisplayColumn} + onChange={(e) => setNewDisplayColumn(e.target.value)} + placeholder="컬럼명 입력 (예: user_name, dept_name)" + className="flex-1" /> +
+ +
+ • 여러 컬럼을 선택하면 "{localValues.separator || ' - '}"로 구분하여 표시됩니다 +
+ • 예: 이름{localValues.separator || ' - '}부서명 +
+
+ + {/* 구분자 설정 */} +
+ + updateConfig("separator", e.target.value)} + placeholder=" - " + className="mt-1" + />
{/* 표시 형식 */} @@ -225,59 +261,6 @@ export const EntityTypeConfigPanel: React.FC = ({ co />
- {/* 옵션들 */} -
-
- - updateConfig("searchable", !!checked)} - /> -
- -
- - updateConfig("multiple", !!checked)} - /> -
- -
- - updateConfig("allowClear", !!checked)} - /> -
-
- - {/* 최대 선택 개수 (다중 선택 시) */} - {localValues.multiple && ( -
- - updateConfig("maxSelections", e.target.value ? Number(e.target.value) : undefined)} - className="mt-1" - placeholder="제한 없음" - /> -
- )} {/* 필터 관리 */}
@@ -285,33 +268,22 @@ export const EntityTypeConfigPanel: React.FC = ({ co {/* 기존 필터 목록 */}
- {(safeConfig.filters || []).map((filter, index) => ( -
+ {Object.entries(safeConfig.filters || {}).map(([field, value]) => ( +
updateFilter(index, "field", e.target.value)} + value={field} + onChange={(e) => updateFilter(field, e.target.value, value as string)} placeholder="필드명" className="flex-1" /> - + = updateFilter(index, "value", e.target.value)} + value={value as string} + onChange={(e) => updateFilter(field, field, e.target.value)} placeholder="값" className="flex-1" /> -
@@ -326,21 +298,7 @@ export const EntityTypeConfigPanel: React.FC = ({ co placeholder="필드명" className="flex-1" /> - + = setNewFilter((prev) => ({ ...prev, value: e.target.value }))} @@ -352,7 +310,7 @@ export const EntityTypeConfigPanel: React.FC = ({ co
-
총 {(safeConfig.filters || []).length}개 필터
+
총 {Object.keys(safeConfig.filters || {}).length}개 필터
{/* 미리보기 */} @@ -360,31 +318,33 @@ export const EntityTypeConfigPanel: React.FC = ({ co
- {localValues.searchable && } +
- {localValues.placeholder || `${localValues.entityName || "엔터티"}를 선택하세요`} + {localValues.placeholder || `${localValues.referenceTable || "엔터티"}를 선택하세요`}
- 엔터티: {localValues.entityName || "없음"}, API: {localValues.apiEndpoint || "없음"}, 값필드:{" "} - {localValues.valueField}, 표시필드: {localValues.displayField} - {localValues.multiple && `, 다중선택`} - {localValues.searchable && `, 검색가능`} + 참조테이블: {localValues.referenceTable || "없음"}, 조인컬럼: {localValues.referenceColumn} +
+ 표시컬럼: {localValues.displayColumns.length > 0 ? localValues.displayColumns.join(localValues.separator || ' - ') : "없음"}
{/* 안내 메시지 */}
-
엔터티 참조 설정
+
엔터티 타입 설정 가이드
- • 엔터티 참조는 다른 테이블의 데이터를 선택할 때 사용됩니다 + • 참조 테이블: 데이터를 가져올 다른 테이블 이름
- • API 엔드포인트를 통해 데이터를 동적으로 로드합니다 + • 조인 컬럼: 테이블 간 연결에 사용할 기준 컬럼 (보통 ID)
- • 필터를 사용하여 표시할 데이터를 제한할 수 있습니다 -
• 값 필드는 실제 저장되는 값, 표시 필드는 사용자에게 보여지는 값입니다 + • 표시 컬럼: 사용자에게 보여질 컬럼들 (여러 개 가능) +
+ • 여러 표시 컬럼 설정 시 화면마다 다르게 표시할 수 있습니다 +
+ • 예: 사용자 선택 시 "이름"만 보이거나 "이름 - 부서명" 형태로 표시
diff --git a/frontend/types/screen-management.ts b/frontend/types/screen-management.ts index 0d8649a2..40dbd965 100644 --- a/frontend/types/screen-management.ts +++ b/frontend/types/screen-management.ts @@ -192,10 +192,13 @@ export interface FileTypeConfig { export interface EntityTypeConfig { referenceTable: string; referenceColumn: string; - displayColumn: string; + displayColumns: string[]; // 여러 표시 컬럼을 배열로 변경 + displayColumn?: string; // 하위 호환성을 위해 유지 (deprecated) searchColumns?: string[]; filters?: Record; placeholder?: string; + displayFormat?: 'simple' | 'detailed' | 'custom'; // 표시 형식 + separator?: string; // 여러 컬럼 표시 시 구분자 (기본: ' - ') } /**