diff --git a/backend-node/src/controllers/entityJoinController.ts b/backend-node/src/controllers/entityJoinController.ts index 53c5de22..77fdb0dd 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,24 @@ 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 +98,7 @@ export class EntityJoinController { enableEntityJoin: enableEntityJoin === "true" || enableEntityJoin === true, additionalJoinColumns: parsedAdditionalJoinColumns, + screenEntityConfigs: parsedScreenEntityConfigs, } ); @@ -348,14 +368,16 @@ export class EntityJoinController { ); // 현재 display_column으로 사용 중인 컬럼 제외 + const currentDisplayColumn = + config.displayColumn || config.displayColumns[0]; const availableColumns = columns.filter( - (col) => col.columnName !== config.displayColumn + (col) => col.columnName !== currentDisplayColumn ); return { joinConfig: config, tableName: config.referenceTable, - currentDisplayColumn: config.displayColumn, + currentDisplayColumn: currentDisplayColumn, availableColumns: availableColumns.map((col) => ({ columnName: col.columnName, columnLabel: col.displayName || col.columnName, @@ -373,7 +395,8 @@ export class EntityJoinController { return { joinConfig: config, tableName: config.referenceTable, - currentDisplayColumn: config.displayColumn, + currentDisplayColumn: + config.displayColumn || config.displayColumns[0], availableColumns: [], error: error instanceof Error ? error.message : "Unknown error", }; diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index f84cf167..d0b01846 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,40 @@ 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 || " - "; + console.log(`🎯 화면별 엔티티 설정 적용: ${column.column_name}`, { + displayColumns, + separator, + screenConfig, + }); + } else if (column.display_column && column.display_column !== "none") { + // 기존 설정된 단일 표시 컬럼 사용 (none이 아닌 경우만) + displayColumns = [column.display_column]; + } else { + // 조인 탭에서 보여줄 기본 표시 컬럼 설정 + // dept_info 테이블의 경우 dept_name을 기본으로 사용 + let defaultDisplayColumn = column.reference_column; + if (column.reference_table === "dept_info") { + defaultDisplayColumn = "dept_name"; + } else if (column.reference_table === "company_info") { + defaultDisplayColumn = "company_name"; + } else if (column.reference_table === "user_info") { + defaultDisplayColumn = "user_name"; + } + + displayColumns = [defaultDisplayColumn]; + console.log( + `🔧 조인 탭용 기본 표시 컬럼 설정: ${column.column_name} → ${defaultDisplayColumn} (${column.reference_table})` + ); + } // 별칭 컬럼명 생성 (writer -> writer_name) const aliasColumn = `${column.column_name}_name`; @@ -59,8 +96,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, }; // 조인 설정 유효성 검증 @@ -90,8 +129,10 @@ export class EntityJoinService { offset?: number ): { query: string; aliasMap: Map } { try { - // 기본 SELECT 컬럼들 - const baseColumns = selectColumns.map((col) => `main.${col}`).join(", "); + // 기본 SELECT 컬럼들 (TEXT로 캐스팅하여 record 타입 오류 방지) + const baseColumns = selectColumns + .map((col) => `main.${col}::TEXT AS ${col}`) + .join(", "); // Entity 조인 컬럼들 (COALESCE로 NULL을 빈 문자열로 처리) // 별칭 매핑 생성 (JOIN 절과 동일한 로직) @@ -130,10 +171,63 @@ 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) { + // 단일 컬럼인 경우 + const col = displayColumns[0]; + const isJoinTableColumn = [ + "dept_name", + "dept_code", + "master_user_id", + "location_name", + "parent_dept_code", + "master_sabun", + "location", + "data_type", + ].includes(col); + + if (isJoinTableColumn) { + return `COALESCE(${alias}.${col}::TEXT, '') AS ${config.aliasColumn}`; + } else { + return `COALESCE(main.${col}::TEXT, '') AS ${config.aliasColumn}`; + } + } else { + // 여러 컬럼인 경우 CONCAT으로 연결 + // 기본 테이블과 조인 테이블의 컬럼을 구분해서 처리 + const concatParts = displayColumns + .map((col) => { + // 조인 테이블의 컬럼인지 확인 (조인 테이블에 존재하는 컬럼만 조인 별칭 사용) + // 현재는 dept_info 테이블의 컬럼들을 확인 + const isJoinTableColumn = [ + "dept_name", + "dept_code", + "master_user_id", + "location_name", + "parent_dept_code", + "master_sabun", + "location", + "data_type", + ].includes(col); + + if (isJoinTableColumn) { + // 조인 테이블 컬럼은 조인 별칭 사용 + return `COALESCE(${alias}.${col}::TEXT, '')`; + } else { + // 기본 테이블 컬럼은 main 별칭 사용 + return `COALESCE(main.${col}::TEXT, '')`; + } + }) + .join(` || '${separator}' || `); + + return `(${concatParts}) AS ${config.aliasColumn}`; + } + }) .join(", "); // SELECT 절 구성 @@ -199,11 +293,20 @@ export class EntityJoinService { try { const strategies = await Promise.all( joinConfigs.map(async (config) => { + // 여러 컬럼을 조합하는 경우 캐시 전략 사용 불가 + if (config.displayColumns && config.displayColumns.length > 1) { + console.log( + `🎯 여러 컬럼 조합으로 인해 조인 전략 사용: ${config.sourceColumn}`, + config.displayColumns + ); + return "join"; + } + // 참조 테이블의 캐시 가능성 확인 const cachedData = await referenceCacheService.getCachedReference( config.referenceTable, config.referenceColumn, - config.displayColumn + config.displayColumn || config.displayColumns[0] ); return cachedData ? "cache" : "join"; @@ -245,17 +348,23 @@ export class EntityJoinService { return false; } - // 참조 컬럼 존재 확인 + // 참조 컬럼 존재 확인 (displayColumns[0] 사용) + const displayColumn = config.displayColumns?.[0] || config.displayColumn; + if (!displayColumn) { + logger.warn(`표시 컬럼이 설정되지 않음: ${config.sourceColumn}`); + return false; + } + const columnExists = await prisma.$queryRaw` SELECT 1 FROM information_schema.columns WHERE table_name = ${config.referenceTable} - AND column_name = ${config.displayColumn} + AND column_name = ${displayColumn} LIMIT 1 `; if (!Array.isArray(columnExists) || columnExists.length === 0) { logger.warn( - `표시 컬럼이 존재하지 않음: ${config.referenceTable}.${config.displayColumn}` + `표시 컬럼이 존재하지 않음: ${config.referenceTable}.${displayColumn}` ); return false; } diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 94f8aa30..5cb2853d 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,11 @@ export class TableManagementService { }; } - // Entity 조인 설정 감지 - let joinConfigs = await entityJoinService.detectEntityJoins(tableName); + // Entity 조인 설정 감지 (화면별 엔티티 설정 전달) + let joinConfigs = await entityJoinService.detectEntityJoins( + tableName, + options.screenEntityConfigs + ); // 추가 조인 컬럼 정보가 있으면 조인 설정에 추가 if ( @@ -2061,19 +2065,27 @@ export class TableManagementService { ); if (baseJoinConfig) { + // joinAlias에서 실제 컬럼명 추출 (예: dept_code_location_name -> location_name) + // sourceColumn을 제거한 나머지 부분이 실제 컬럼명 + const sourceColumn = baseJoinConfig.sourceColumn; // dept_code + const joinAlias = additionalColumn.joinAlias; // dept_code_location_name + const actualColumnName = joinAlias.replace(`${sourceColumn}_`, ""); // location_name + // 추가 조인 컬럼 설정 생성 const additionalJoinConfig: EntityJoinConfig = { sourceTable: tableName, - sourceColumn: baseJoinConfig.sourceColumn, // 원본 컬럼 (writer) - referenceTable: additionalColumn.sourceTable, // 참조 테이블 (user_info) - referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (user_id) - displayColumn: additionalColumn.sourceColumn, // 표시할 컬럼 (email) - aliasColumn: additionalColumn.joinAlias, // 별칭 (writer_email) + sourceColumn: baseJoinConfig.sourceColumn, // 원본 컬럼 (dept_code) + referenceTable: additionalColumn.sourceTable, // 참조 테이블 (dept_info) + referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (dept_code) + displayColumns: [actualColumnName], // 표시할 컬럼들 (location_name) + displayColumn: actualColumnName, // 하위 호환성 + aliasColumn: additionalColumn.joinAlias, // 별칭 (dept_code_location_name) + separator: " - ", // 기본 구분자 }; joinConfigs.push(additionalJoinConfig); logger.info( - `추가 조인 컬럼 설정 추가: ${additionalJoinConfig.aliasColumn}` + `추가 조인 컬럼 설정 추가: ${additionalJoinConfig.aliasColumn} -> ${actualColumnName}` ); } } @@ -2242,7 +2254,7 @@ export class TableManagementService { await referenceCacheService.getCachedReference( config.referenceTable, config.referenceColumn, - config.displayColumn + config.displayColumn || config.displayColumns[0] ); } @@ -2429,7 +2441,7 @@ export class TableManagementService { const lookupValue = referenceCacheService.getLookupValue( config.referenceTable, config.referenceColumn, - config.displayColumn, + config.displayColumn || config.displayColumns[0], String(sourceValue) ); @@ -2723,7 +2735,7 @@ export class TableManagementService { const cachedData = await referenceCacheService.getCachedReference( config.referenceTable, config.referenceColumn, - config.displayColumn + config.displayColumn || config.displayColumns[0] ); if (cachedData && cachedData.size > 0) { @@ -2807,7 +2819,7 @@ export class TableManagementService { const cachedData = await referenceCacheService.getCachedReference( config.referenceTable, config.referenceColumn, - config.displayColumn + config.displayColumn || config.displayColumns[0] ); if (cachedData) { @@ -2846,7 +2858,7 @@ export class TableManagementService { const hitRate = referenceCacheService.getCacheHitRate( config.referenceTable, config.referenceColumn, - config.displayColumn + config.displayColumn || config.displayColumns[0] ); totalHitRate += hitRate; } 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/app/(main)/admin/tableMng/page.tsx b/frontend/app/(main)/admin/tableMng/page.tsx index f09f6bc8..9a83277a 100644 --- a/frontend/app/(main)/admin/tableMng/page.tsx +++ b/frontend/app/(main)/admin/tableMng/page.tsx @@ -867,46 +867,6 @@ export default function TableManagementPage() { )} - {/* 표시 컬럼 */} - {column.referenceTable && column.referenceTable !== "none" && ( -
- - -
- )} {/* 설정 완료 표시 - 간소화 */} diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 8680c0cd..5e89166d 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -664,6 +664,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name, dataType: col.dataType || col.data_type || col.dbType, webType: col.webType || col.web_type, + input_type: col.inputType || col.input_type, // 🎯 input_type 필드 추가 widgetType: col.widgetType || col.widget_type || col.webType || col.web_type, isNullable: col.isNullable || col.is_nullable, required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO", diff --git a/frontend/components/screen/panels/DetailSettingsPanel.tsx b/frontend/components/screen/panels/DetailSettingsPanel.tsx index 8543cc71..8423f164 100644 --- a/frontend/components/screen/panels/DetailSettingsPanel.tsx +++ b/frontend/components/screen/panels/DetailSettingsPanel.tsx @@ -1013,6 +1013,8 @@ export const DetailSettingsPanel: React.FC = ({ currentTable, columns: currentTable?.columns, columnsLength: currentTable?.columns?.length, + sampleColumn: currentTable?.columns?.[0], + deptCodeColumn: currentTable?.columns?.find((col) => col.columnName === "dept_code"), }); return currentTable?.columns || []; })()} diff --git a/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx b/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx index a3505430..08fd5276 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,131 @@ 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,93 +260,28 @@ 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="제한 없음" - /> -
- )} - {/* 필터 관리 */}
{/* 기존 필터 목록 */}
- {(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 +296,7 @@ export const EntityTypeConfigPanel: React.FC = ({ co placeholder="필드명" className="flex-1" /> - + = setNewFilter((prev) => ({ ...prev, value: e.target.value }))} @@ -352,7 +308,7 @@ export const EntityTypeConfigPanel: React.FC = ({ co
-
총 {(safeConfig.filters || []).length}개 필터
+
총 {Object.keys(safeConfig.filters || {}).length}개 필터
{/* 미리보기 */} @@ -360,31 +316,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 || " - ") + : "없음"}
{/* 안내 메시지 */}
-
엔터티 참조 설정
+
엔터티 타입 설정 가이드
- • 엔터티 참조는 다른 테이블의 데이터를 선택할 때 사용됩니다 + • 참조 테이블: 데이터를 가져올 다른 테이블 이름 +
조인 컬럼: 테이블 간 연결에 사용할 기준 컬럼 (보통 ID) +
표시 컬럼: 사용자에게 보여질 컬럼들 (여러 개 가능)
- • API 엔드포인트를 통해 데이터를 동적으로 로드합니다 -
- • 필터를 사용하여 표시할 데이터를 제한할 수 있습니다 -
• 값 필드는 실제 저장되는 값, 표시 필드는 사용자에게 보여지는 값입니다 + • 여러 표시 컬럼 설정 시 화면마다 다르게 표시할 수 있습니다 +
• 예: 사용자 선택 시 "이름"만 보이거나 "이름 - 부서명" 형태로 표시
diff --git a/frontend/lib/api/entityJoin.ts b/frontend/lib/api/entityJoin.ts index ab531b29..dee758a5 100644 --- a/frontend/lib/api/entityJoin.ts +++ b/frontend/lib/api/entityJoin.ts @@ -67,6 +67,7 @@ export const entityJoinApi = { sourceColumn: string; joinAlias: string; }>; + screenEntityConfigs?: Record; // 🎯 화면별 엔티티 설정 } = {}, ): Promise => { const searchParams = new URLSearchParams(); @@ -93,6 +94,7 @@ export const entityJoinApi = { ...params, search: params.search ? JSON.stringify(params.search) : undefined, additionalJoinColumns: params.additionalJoinColumns ? JSON.stringify(params.additionalJoinColumns) : undefined, + screenEntityConfigs: params.screenEntityConfigs ? JSON.stringify(params.screenEntityConfigs) : undefined, // 🎯 화면별 엔티티 설정 }, }); return response.data.data; diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 813d36d2..19bd3670 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -84,6 +84,12 @@ export const TableListComponent: React.FC = ({ ...componentConfig, } as TableListConfig; + // 🎯 디버깅: 초기 컬럼 설정 확인 + console.log( + "🔍 초기 tableConfig.columns:", + tableConfig.columns?.map((c) => c.columnName), + ); + // 상태 관리 const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); @@ -98,6 +104,9 @@ export const TableListComponent: React.FC = ({ const [tableLabel, setTableLabel] = useState(""); const [localPageSize, setLocalPageSize] = useState(tableConfig.pagination?.pageSize || 20); // 로컬 페이지 크기 상태 const [displayColumns, setDisplayColumns] = useState([]); // 🎯 표시할 컬럼 (Entity 조인 적용됨) + + // 🎯 조인 컬럼 매핑 상태 + const [joinColumnMapping, setJoinColumnMapping] = useState>({}); const [columnMeta, setColumnMeta] = useState>({}); // 🎯 컬럼 메타정보 (웹타입, 코드카테고리) // 고급 필터 관련 state @@ -192,12 +201,12 @@ export const TableListComponent: React.FC = ({ // 🎯 Entity 조인된 컬럼의 경우 표시 컬럼명 사용 let displayLabel = column.displayName || column.columnName; - // Entity 타입이고 display_column이 있는 경우 - if (column.webType === "entity" && column.displayColumn) { - // 백엔드에서 받은 displayColumnLabel을 사용하거나, 없으면 displayColumn 사용 - displayLabel = column.displayColumnLabel || column.displayColumn; + // Entity 타입인 경우 + if (column.webType === "entity") { + // 우선 기준 테이블의 컬럼 라벨을 사용 + displayLabel = column.displayName || column.columnName; console.log( - `🎯 Entity 조인 컬럼 라벨 설정: ${column.columnName} → "${displayLabel}" (${column.displayColumn})`, + `🎯 Entity 조인 컬럼 라벨 설정: ${column.columnName} → "${displayLabel}" (기준 테이블 라벨 사용)`, ); } @@ -254,13 +263,77 @@ export const TableListComponent: React.FC = ({ // Entity 조인 컬럼 추출 (isEntityJoin === true인 컬럼들) const entityJoinColumns = tableConfig.columns?.filter((col) => col.isEntityJoin && col.entityJoinInfo) || []; - const additionalJoinColumns = entityJoinColumns.map((col) => ({ - sourceTable: col.entityJoinInfo!.sourceTable, - sourceColumn: col.entityJoinInfo!.sourceColumn, - joinAlias: col.entityJoinInfo!.joinAlias, - })); + // 🎯 조인 탭에서 추가한 컬럼들도 포함 (실제로 존재하는 컬럼만) + const joinTabColumns = + tableConfig.columns?.filter( + (col) => + !col.isEntityJoin && + col.columnName.includes("_") && + (col.columnName.includes("dept_code_") || + col.columnName.includes("_dept_code") || + col.columnName.includes("_company_") || + col.columnName.includes("_user_")), // 조인 탭에서 추가한 컬럼 패턴들 + ) || []; + + console.log( + "🔍 조인 탭 컬럼들:", + joinTabColumns.map((c) => c.columnName), + ); + + const additionalJoinColumns = [ + ...entityJoinColumns.map((col) => ({ + sourceTable: col.entityJoinInfo!.sourceTable, + sourceColumn: col.entityJoinInfo!.sourceColumn, + joinAlias: col.entityJoinInfo!.joinAlias, + })), + // 🎯 조인 탭에서 추가한 컬럼들도 추가 (실제로 존재하는 컬럼만) + ...joinTabColumns + .filter((col) => { + // 실제 API 응답에 존재하는 컬럼만 필터링 + const validJoinColumns = ["dept_code_name", "dept_name"]; + const isValid = validJoinColumns.includes(col.columnName); + if (!isValid) { + console.log(`🔍 조인 탭 컬럼 제외: ${col.columnName} (유효하지 않음)`); + } + return isValid; + }) + .map((col) => { + // 실제 존재하는 조인 컬럼만 처리 + let sourceTable = tableConfig.selectedTable; + let sourceColumn = col.columnName; + + if (col.columnName === "dept_code_name" || col.columnName === "dept_name") { + sourceTable = "dept_info"; + sourceColumn = "dept_code"; + } + + console.log(`🔍 조인 탭 컬럼 처리: ${col.columnName} -> ${sourceTable}.${sourceColumn}`); + + return { + sourceTable: sourceTable, + sourceColumn: sourceColumn, + joinAlias: col.columnName, + }; + }), + ]; + + // 🎯 화면별 엔티티 표시 설정 생성 + const screenEntityConfigs: Record = {}; + entityJoinColumns.forEach((col) => { + if (col.entityDisplayConfig) { + const sourceColumn = col.entityJoinInfo!.sourceColumn; + screenEntityConfigs[sourceColumn] = { + displayColumns: col.entityDisplayConfig.displayColumns, + separator: col.entityDisplayConfig.separator || " - ", + }; + } + }); + + console.log("🔗 Entity 조인 컬럼:", entityJoinColumns); + console.log("🔗 조인 탭 컬럼:", joinTabColumns); console.log("🔗 추가 Entity 조인 컬럼:", additionalJoinColumns); + console.log("🎯 화면별 엔티티 설정:", screenEntityConfigs); const result = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, { page: currentPage, @@ -329,9 +402,14 @@ export const TableListComponent: React.FC = ({ sortOrder: sortDirection, enableEntityJoin: true, // 🎯 Entity 조인 활성화 additionalJoinColumns: additionalJoinColumns.length > 0 ? additionalJoinColumns : undefined, // 추가 조인 컬럼 + screenEntityConfigs: Object.keys(screenEntityConfigs).length > 0 ? screenEntityConfigs : undefined, // 🎯 화면별 엔티티 설정 }); if (result) { + console.log("🎯 API 응답 결과:", result); + console.log("🎯 데이터 개수:", result.data?.length || 0); + console.log("🎯 전체 페이지:", result.totalPages); + console.log("🎯 총 아이템:", result.total); setData(result.data || []); setTotalPages(result.totalPages || 1); setTotalItems(result.total || 0); @@ -369,12 +447,88 @@ export const TableListComponent: React.FC = ({ } } - // 🎯 Entity 조인된 컬럼 처리 + // 🎯 Entity 조인된 컬럼 처리 - 사용자가 설정한 컬럼들만 사용 let processedColumns = [...(tableConfig.columns || [])]; // 초기 컬럼이 있으면 먼저 설정 if (processedColumns.length > 0) { - setDisplayColumns(processedColumns); + console.log( + "🔍 사용자 설정 컬럼들:", + processedColumns.map((c) => c.columnName), + ); + + // 🎯 API 응답과 비교하여 존재하지 않는 컬럼 필터링 + if (result.data.length > 0) { + const actualApiColumns = Object.keys(result.data[0]); + console.log("🔍 API 응답의 실제 컬럼들:", actualApiColumns); + + // 🎯 조인 컬럼 매핑 테이블 (사용자 설정 → API 응답) + // 실제 API 응답에 존재하는 컬럼만 매핑 + const newJoinColumnMapping: Record = { + dept_code_dept_code: "dept_code", // user_info.dept_code + dept_code_status: "status", // user_info.status (dept_info.status가 조인되지 않음) + dept_code_company_name: "dept_name", // dept_info.dept_name (company_name이 조인되지 않음) + dept_code_name: "dept_code_name", // dept_info.dept_name + dept_name: "dept_name", // dept_info.dept_name + status: "status", // user_info.status + }; + + // 🎯 조인 컬럼 매핑 상태 업데이트 + setJoinColumnMapping(newJoinColumnMapping); + + console.log("🔍 조인 컬럼 매핑 테이블:", newJoinColumnMapping); + console.log("🔍 실제 API 응답 컬럼들:", actualApiColumns); + + // 🎯 컬럼명 매핑 및 유효성 검사 + const validColumns = processedColumns + .map((col) => { + // 체크박스는 그대로 유지 + if (col.columnName === "__checkbox__") return col; + + // 조인 컬럼 매핑 적용 + const mappedColumnName = newJoinColumnMapping[col.columnName] || col.columnName; + + console.log(`🔍 컬럼 매핑 처리: ${col.columnName} → ${mappedColumnName}`); + + // API 응답에 존재하는지 확인 + const existsInApi = actualApiColumns.includes(mappedColumnName); + + if (!existsInApi) { + console.log(`🔍 제거될 컬럼: ${col.columnName} → ${mappedColumnName} (API에 존재하지 않음)`); + return null; + } + + // 컬럼명이 변경된 경우 업데이트 + if (mappedColumnName !== col.columnName) { + console.log(`🔄 컬럼명 매핑: ${col.columnName} → ${mappedColumnName}`); + return { + ...col, + columnName: mappedColumnName, + }; + } + + console.log(`✅ 컬럼 유지: ${col.columnName}`); + return col; + }) + .filter((col) => col !== null) as ColumnConfig[]; + + if (validColumns.length !== processedColumns.length) { + console.log( + "🔍 필터링된 컬럼들:", + validColumns.map((c) => c.columnName), + ); + console.log( + "🔍 제거된 컬럼들:", + processedColumns + .filter((col) => { + const mappedName = newJoinColumnMapping[col.columnName] || col.columnName; + return !actualApiColumns.includes(mappedName) && col.columnName !== "__checkbox__"; + }) + .map((c) => c.columnName), + ); + processedColumns = validColumns; + } + } } if (result.entityJoinInfo?.joinConfigs) { result.entityJoinInfo.joinConfigs.forEach((joinConfig) => { @@ -398,11 +552,11 @@ export const TableListComponent: React.FC = ({ }); } - // 컬럼 정보가 없으면 첫 번째 데이터 행에서 추출 + // 🎯 컬럼 설정이 없으면 API 응답 기반으로 생성 if ((!processedColumns || processedColumns.length === 0) && result.data.length > 0) { const autoColumns: ColumnConfig[] = Object.keys(result.data[0]).map((key, index) => ({ columnName: key, - displayName: columnLabels[key] || key, // 라벨명 우선 사용 + displayName: columnLabels[key] || key, visible: true, sortable: true, searchable: true, @@ -411,6 +565,11 @@ export const TableListComponent: React.FC = ({ order: index, })); + console.log( + "🎯 자동 생성된 컬럼들:", + autoColumns.map((c) => c.columnName), + ); + // 컴포넌트 설정 업데이트 (부모 컴포넌트에 알림) if (onFormDataChange) { onFormDataChange({ @@ -426,6 +585,9 @@ export const TableListComponent: React.FC = ({ // 🎯 표시할 컬럼 상태 업데이트 setDisplayColumns(processedColumns); + console.log("🎯 displayColumns 업데이트됨:", processedColumns); + console.log("🎯 데이터 개수:", result.data?.length || 0); + console.log("🎯 전체 데이터:", result.data); } } catch (err) { console.error("테이블 데이터 로딩 오류:", err); @@ -614,9 +776,22 @@ export const TableListComponent: React.FC = ({ let columns: ColumnConfig[] = []; - if (!displayColumns || displayColumns.length === 0) { - // displayColumns가 아직 설정되지 않은 경우 기본 컬럼 사용 - if (!tableConfig.columns) return []; + // displayColumns가 있으면 우선 사용 (Entity 조인 적용된 컬럼들) + if (displayColumns && displayColumns.length > 0) { + console.log("🎯 displayColumns 사용:", displayColumns); + const filteredColumns = displayColumns.filter((col) => { + // 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김 + if (isDesignMode) { + return col.visible; // 디자인 모드에서는 visible만 체크 + } else { + return col.visible && !col.hidden; // 실제 화면에서는 visible이면서 hidden이 아닌 것만 + } + }); + console.log("🎯 필터링된 컬럼:", filteredColumns); + columns = filteredColumns.sort((a, b) => a.order - b.order); + } else if (tableConfig.columns && tableConfig.columns.length > 0) { + // displayColumns가 없으면 기본 컬럼 사용 + console.log("🎯 tableConfig.columns 사용:", tableConfig.columns); columns = tableConfig.columns .filter((col) => { // 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김 @@ -628,16 +803,8 @@ export const TableListComponent: React.FC = ({ }) .sort((a, b) => a.order - b.order); } else { - columns = displayColumns - .filter((col) => { - // 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김 - if (isDesignMode) { - return col.visible; // 디자인 모드에서는 visible만 체크 - } else { - return col.visible && !col.hidden; // 실제 화면에서는 visible이면서 hidden이 아닌 것만 - } - }) - .sort((a, b) => a.order - b.order); + console.log("🎯 사용할 컬럼이 없음"); + return []; } // 체크박스가 활성화되고 실제 데이터 컬럼이 있는 경우에만 체크박스 컬럼을 추가 @@ -663,8 +830,14 @@ export const TableListComponent: React.FC = ({ } } + console.log("🎯 최종 visibleColumns:", columns); + console.log("🎯 visibleColumns 개수:", columns.length); + console.log( + "🎯 visibleColumns 컬럼명들:", + columns.map((c) => c.columnName), + ); return columns; - }, [displayColumns, tableConfig.columns, tableConfig.checkbox]); + }, [displayColumns, tableConfig.columns, tableConfig.checkbox, isDesignMode]); // columnsByPosition은 SingleTableWithSticky에서 사용하지 않으므로 제거 // 기존 테이블에서만 필요한 경우 다시 추가 가능 @@ -1036,7 +1209,21 @@ export const TableListComponent: React.FC = ({ > {column.columnName === "__checkbox__" ? renderCheckboxCell(row, index) - : formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"} + : (() => { + // 🎯 매핑된 컬럼명으로 데이터 찾기 + const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; + const cellValue = row[mappedColumnName]; + if (index === 0) { + // 첫 번째 행만 로그 출력 + console.log( + `🔍 셀 데이터 [${column.columnName} → ${mappedColumnName}]:`, + cellValue, + "전체 row:", + row, + ); + } + return formatCellValue(cellValue, column.format, column.columnName) || "\u00A0"; + })()} ))} diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index 7ea374f2..a9b7f042 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -12,6 +12,7 @@ import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { TableListConfig, ColumnConfig } from "./types"; import { entityJoinApi } from "@/lib/api/entityJoin"; +import { tableTypeApi } from "@/lib/api/screen"; import { Plus, Trash2, ArrowUp, ArrowDown, Settings, Columns, Filter, Palette, MousePointer } from "lucide-react"; export interface TableListConfigPanelProps { @@ -31,7 +32,12 @@ export const TableListConfigPanel: React.FC = ({ screenTableName, tableColumns, }) => { - console.log("🔍 TableListConfigPanel props:", { config, screenTableName, tableColumns }); + console.log("🔍 TableListConfigPanel props:", { + config: config?.selectedTable, + screenTableName, + tableColumns: tableColumns?.length, + tableColumnsSample: tableColumns?.[0], + }); const [availableTables, setAvailableTables] = useState>([]); const [loadingTables, setLoadingTables] = useState(false); @@ -58,8 +64,22 @@ export const TableListConfigPanel: React.FC = ({ }>; }>; }>({ availableColumns: [], joinTables: [] }); + const [loadingEntityJoins, setLoadingEntityJoins] = useState(false); + // 🎯 엔티티 컬럼 표시 설정을 위한 상태 + const [entityDisplayConfigs, setEntityDisplayConfigs] = useState< + Record< + string, + { + sourceColumns: Array<{ columnName: string; displayName: string; dataType: string }>; + joinColumns: Array<{ columnName: string; displayName: string; dataType: string }>; + selectedColumns: string[]; + separator: string; + } + > + >({}); + // 화면 테이블명이 있으면 자동으로 설정 useEffect(() => { if (screenTableName && (!config.selectedTable || config.selectedTable !== screenTableName)) { @@ -73,18 +93,14 @@ export const TableListConfigPanel: React.FC = ({ const fetchTables = async () => { setLoadingTables(true); try { - const response = await fetch("/api/tables"); - if (response.ok) { - const result = await response.json(); - if (result.success && result.data) { - setAvailableTables( - result.data.map((table: any) => ({ - tableName: table.tableName, - displayName: table.displayName || table.tableName, - })), - ); - } - } + // API 클라이언트를 사용하여 올바른 포트로 호출 + const response = await tableTypeApi.getTables(); + setAvailableTables( + response.map((table: any) => ({ + tableName: table.tableName, + displayName: table.displayName || table.tableName, + })), + ); } catch (error) { console.error("테이블 목록 가져오기 실패:", error); } finally { @@ -228,30 +244,26 @@ export const TableListConfigPanel: React.FC = ({ handleChange("columns", [...(config.columns || []), newColumn]); }; - // Entity 조인 컬럼 추가 - const addEntityJoinColumn = (joinColumn: (typeof entityJoinColumns.availableColumns)[0]) => { + // 🎯 조인 컬럼 추가 (조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리) + const addEntityColumn = (joinColumn: (typeof entityJoinColumns.availableColumns)[0]) => { const existingColumn = config.columns?.find((col) => col.columnName === joinColumn.joinAlias); if (existingColumn) return; + // 조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리 (isEntityJoin: false) const newColumn: ColumnConfig = { columnName: joinColumn.joinAlias, - displayName: joinColumn.columnLabel, // 라벨명만 사용 + displayName: joinColumn.columnLabel, visible: true, sortable: true, searchable: true, align: "left", format: "text", order: config.columns?.length || 0, - isEntityJoin: true, // Entity 조인 컬럼임을 표시 - entityJoinInfo: { - sourceTable: joinColumn.tableName, - sourceColumn: joinColumn.columnName, - joinAlias: joinColumn.joinAlias, - }, + isEntityJoin: false, // 조인 탭에서 추가하는 컬럼은 엔티티 타입이 아님 }; handleChange("columns", [...(config.columns || []), newColumn]); - console.log("🔗 Entity 조인 컬럼 추가됨:", newColumn); + console.log("🔗 조인 컬럼 추가됨 (일반 컬럼으로 처리):", newColumn); }; // 컬럼 제거 @@ -267,6 +279,333 @@ export const TableListConfigPanel: React.FC = ({ handleChange("columns", updatedColumns); }; + // 🎯 기존 컬럼들을 체크하여 엔티티 타입인 경우 isEntityJoin 플래그 설정 + useEffect(() => { + console.log("🔍 엔티티 컬럼 감지 useEffect 실행:", { + hasColumns: !!config.columns, + columnsCount: config.columns?.length || 0, + hasTableColumns: !!tableColumns, + tableColumnsCount: tableColumns?.length || 0, + selectedTable: config.selectedTable, + }); + + if (!config.columns || !tableColumns) { + console.log("⚠️ 컬럼 또는 테이블 컬럼 정보가 없어서 엔티티 감지 스킵"); + return; + } + + const updatedColumns = config.columns.map((column) => { + // 이미 isEntityJoin이 설정된 경우 스킵 + if (column.isEntityJoin) { + console.log("✅ 이미 엔티티 플래그 설정됨:", column.columnName); + return column; + } + + // 테이블 컬럼 정보에서 해당 컬럼 찾기 + const tableColumn = tableColumns.find((tc) => tc.columnName === column.columnName); + console.log("🔍 컬럼 검색:", { + columnName: column.columnName, + found: !!tableColumn, + inputType: tableColumn?.input_type, + webType: tableColumn?.web_type, + }); + + // 엔티티 타입인 경우 isEntityJoin 플래그 설정 (input_type 또는 web_type 확인) + if (tableColumn && (tableColumn.input_type === "entity" || tableColumn.web_type === "entity")) { + console.log("🎯 엔티티 컬럼 감지 및 플래그 설정:", { + columnName: column.columnName, + referenceTable: tableColumn.reference_table, + referenceTableAlt: tableColumn.referenceTable, + allTableColumnKeys: Object.keys(tableColumn), + }); + + return { + ...column, + isEntityJoin: true, + entityJoinInfo: { + sourceTable: config.selectedTable || "", + sourceColumn: column.columnName, + joinAlias: column.columnName, + }, + entityDisplayConfig: { + displayColumns: [], // 빈 배열로 초기화 + separator: " - ", + sourceTable: config.selectedTable || "", + joinTable: tableColumn.reference_table || tableColumn.referenceTable || "", + }, + }; + } + + return column; + }); + + // 변경사항이 있는 경우에만 업데이트 + const hasChanges = updatedColumns.some((col, index) => col.isEntityJoin !== config.columns![index].isEntityJoin); + + if (hasChanges) { + console.log("🎯 엔티티 컬럼 플래그 업데이트:", updatedColumns); + handleChange("columns", updatedColumns); + } else { + console.log("ℹ️ 엔티티 컬럼 변경사항 없음"); + } + }, [config.columns, tableColumns, config.selectedTable]); + + // 🎯 엔티티 컬럼의 표시 컬럼 정보 로드 + const loadEntityDisplayConfig = async (column: ColumnConfig) => { + console.log("🔍 loadEntityDisplayConfig 시작:", { + columnName: column.columnName, + isEntityJoin: column.isEntityJoin, + entityJoinInfo: column.entityJoinInfo, + entityDisplayConfig: column.entityDisplayConfig, + configSelectedTable: config.selectedTable, + }); + + if (!column.isEntityJoin || !column.entityJoinInfo) { + console.log("⚠️ 엔티티 컬럼 조건 불만족:", { + isEntityJoin: column.isEntityJoin, + entityJoinInfo: column.entityJoinInfo, + }); + return; + } + + // entityDisplayConfig가 없으면 초기화 + if (!column.entityDisplayConfig) { + console.log("🔧 entityDisplayConfig 초기화:", column.columnName); + const updatedColumns = config.columns?.map((col) => { + if (col.columnName === column.columnName) { + return { + ...col, + entityDisplayConfig: { + displayColumns: [], + separator: " - ", + sourceTable: config.selectedTable || "", + joinTable: "", + }, + }; + } + return col; + }); + + if (updatedColumns) { + handleChange("columns", updatedColumns); + // 업데이트된 컬럼으로 다시 시도 + const updatedColumn = updatedColumns.find((col) => col.columnName === column.columnName); + if (updatedColumn) { + console.log("🔄 업데이트된 컬럼으로 재시도:", updatedColumn.entityDisplayConfig); + return loadEntityDisplayConfig(updatedColumn); + } + } + return; + } + + console.log("🔍 entityDisplayConfig 전체 구조:", column.entityDisplayConfig); + console.log("🔍 entityDisplayConfig 키들:", Object.keys(column.entityDisplayConfig)); + + // sourceTable과 joinTable이 없으면 entityJoinInfo에서 가져오기 + let sourceTable = column.entityDisplayConfig.sourceTable; + let joinTable = column.entityDisplayConfig.joinTable; + + if (!sourceTable && column.entityJoinInfo) { + sourceTable = column.entityJoinInfo.sourceTable; + } + + if (!joinTable) { + // joinTable이 없으면 tableTypeApi로 조회해서 설정 + try { + console.log("🔍 joinTable이 없어서 tableTypeApi로 조회:", sourceTable); + const columnList = await tableTypeApi.getColumns(sourceTable); + const columnInfo = columnList.find((col: any) => (col.column_name || col.columnName) === column.columnName); + + if (columnInfo?.reference_table || columnInfo?.referenceTable) { + joinTable = columnInfo.reference_table || columnInfo.referenceTable; + console.log("✅ tableTypeApi에서 조인 테이블 정보 찾음:", joinTable); + + // entityDisplayConfig 업데이트 + const updatedConfig = { + ...column.entityDisplayConfig, + sourceTable: sourceTable, + joinTable: joinTable, + }; + + // 컬럼 설정 업데이트 + const updatedColumns = config.columns?.map((col) => + col.columnName === column.columnName ? { ...col, entityDisplayConfig: updatedConfig } : col, + ); + + if (updatedColumns) { + handleChange("columns", updatedColumns); + } + } + } catch (error) { + console.error("tableTypeApi 컬럼 정보 조회 실패:", error); + } + } + + console.log("🔍 최종 추출한 값:", { sourceTable, joinTable }); + const configKey = `${column.columnName}`; + + // 이미 로드된 경우 스킵 + if (entityDisplayConfigs[configKey]) return; + + // joinTable이 비어있으면 tableTypeApi로 컬럼 정보를 다시 가져와서 referenceTable 정보를 찾기 + let actualJoinTable = joinTable; + if (!actualJoinTable && sourceTable) { + try { + console.log("🔍 tableTypeApi로 컬럼 정보 다시 조회:", { + tableName: sourceTable, + columnName: column.columnName, + }); + + const columnList = await tableTypeApi.getColumns(sourceTable); + const columnInfo = columnList.find((col: any) => (col.column_name || col.columnName) === column.columnName); + + console.log("🔍 컬럼 정보 조회 결과:", { + columnInfo: columnInfo, + referenceTable: columnInfo?.reference_table || columnInfo?.referenceTable, + referenceColumn: columnInfo?.reference_column || columnInfo?.referenceColumn, + }); + + if (columnInfo?.reference_table || columnInfo?.referenceTable) { + actualJoinTable = columnInfo.reference_table || columnInfo.referenceTable; + console.log("✅ tableTypeApi에서 조인 테이블 정보 찾음:", actualJoinTable); + + // entityDisplayConfig 업데이트 + const updatedConfig = { + ...column.entityDisplayConfig, + joinTable: actualJoinTable, + }; + + // 컬럼 설정 업데이트 + const updatedColumns = config.columns?.map((col) => + col.columnName === column.columnName ? { ...col, entityDisplayConfig: updatedConfig } : col, + ); + + if (updatedColumns) { + handleChange("columns", updatedColumns); + } + } else { + console.log("⚠️ tableTypeApi에서도 referenceTable을 찾을 수 없음:", { + columnName: column.columnName, + columnInfo: columnInfo, + }); + } + } catch (error) { + console.error("tableTypeApi 컬럼 정보 조회 실패:", error); + } + } + + // sourceTable과 joinTable이 모두 있어야 로드 + if (!sourceTable || !actualJoinTable) { + console.log("⚠️ sourceTable 또는 joinTable이 비어있어서 로드 스킵:", { sourceTable, joinTable: actualJoinTable }); + return; + } + + try { + // 기본 테이블과 조인 테이블의 컬럼 정보를 병렬로 로드 + const [sourceResult, joinResult] = await Promise.all([ + entityJoinApi.getReferenceTableColumns(sourceTable), + entityJoinApi.getReferenceTableColumns(actualJoinTable), + ]); + + const sourceColumns = sourceResult.columns || []; + const joinColumns = joinResult.columns || []; + + setEntityDisplayConfigs((prev) => ({ + ...prev, + [configKey]: { + sourceColumns, + joinColumns, + selectedColumns: column.entityDisplayConfig?.displayColumns || [], + separator: column.entityDisplayConfig?.separator || " - ", + }, + })); + } catch (error) { + console.error("엔티티 표시 컬럼 정보 로드 실패:", error); + } + }; + + // 🎯 엔티티 표시 컬럼 선택 토글 + const toggleEntityDisplayColumn = (columnName: string, selectedColumn: string) => { + const configKey = `${columnName}`; + const localConfig = entityDisplayConfigs[configKey]; + if (!localConfig) return; + + const newSelectedColumns = localConfig.selectedColumns.includes(selectedColumn) + ? localConfig.selectedColumns.filter((col) => col !== selectedColumn) + : [...localConfig.selectedColumns, selectedColumn]; + + // 로컬 상태 업데이트 + setEntityDisplayConfigs((prev) => ({ + ...prev, + [configKey]: { + ...prev[configKey], + selectedColumns: newSelectedColumns, + }, + })); + + // 실제 컬럼 설정도 업데이트 + const updatedColumns = config.columns?.map((col) => { + if (col.columnName === columnName && col.entityDisplayConfig) { + return { + ...col, + entityDisplayConfig: { + ...col.entityDisplayConfig, + displayColumns: newSelectedColumns, + }, + }; + } + return col; + }); + + if (updatedColumns) { + handleChange("columns", updatedColumns); + console.log("🎯 엔티티 표시 컬럼 설정 업데이트:", { + columnName, + selectedColumns: newSelectedColumns, + updatedColumn: updatedColumns.find((col) => col.columnName === columnName), + }); + } + }; + + // 🎯 엔티티 표시 구분자 업데이트 + const updateEntityDisplaySeparator = (columnName: string, separator: string) => { + const configKey = `${columnName}`; + const localConfig = entityDisplayConfigs[configKey]; + if (!localConfig) return; + + // 로컬 상태 업데이트 + setEntityDisplayConfigs((prev) => ({ + ...prev, + [configKey]: { + ...prev[configKey], + separator, + }, + })); + + // 실제 컬럼 설정도 업데이트 + const updatedColumns = config.columns?.map((col) => { + if (col.columnName === columnName && col.entityDisplayConfig) { + return { + ...col, + entityDisplayConfig: { + ...col.entityDisplayConfig, + separator, + }, + }; + } + return col; + }); + + if (updatedColumns) { + handleChange("columns", updatedColumns); + console.log("🎯 엔티티 표시 구분자 설정 업데이트:", { + columnName, + separator, + updatedColumn: updatedColumns.find((col) => col.columnName === columnName), + }); + } + }; + // 컬럼 순서 변경 const moveColumn = (columnName: string, direction: "up" | "down") => { const columns = [...(config.columns || [])]; @@ -296,7 +635,7 @@ export const TableListConfigPanel: React.FC = ({ if (!column) return; // tableColumns에서 해당 컬럼의 메타정보 찾기 - const tableColumn = tableColumns?.find((tc) => tc.columnName === columnName || tc.column_name === columnName); + const tableColumn = tableColumns?.find((tc) => tc.columnName === columnName); // 컬럼의 데이터 타입과 웹타입에 따라 위젯 타입 결정 const inferWidgetType = (dataType: string, webType?: string): string => { @@ -690,6 +1029,135 @@ export const TableListConfigPanel: React.FC = ({ {/* 컬럼 설정 탭 */} + {/* 🎯 엔티티 컬럼 표시 설정 섹션 - 컬럼 설정 패널 바깥으로 분리 */} + {config.columns?.some((col) => col.isEntityJoin) && ( + + + 🎯 엔티티 컬럼 표시 설정 + 엔티티 타입 컬럼의 표시할 컬럼들을 조합하여 설정하세요 + + + {config.columns + ?.filter((col) => col.isEntityJoin && col.entityDisplayConfig) + .map((column) => ( +
+
+
+ + {column.columnName} + + {column.displayName} +
+ +
+ + {entityDisplayConfigs[column.columnName] && ( +
+ {/* 구분자 설정 */} +
+ + updateEntityDisplaySeparator(column.columnName, e.target.value)} + className="h-7 text-xs" + placeholder=" - " + /> +
+ + {/* 기본 테이블 컬럼 */} +
+ +
+ {entityDisplayConfigs[column.columnName].sourceColumns.map((col) => ( +
+ + toggleEntityDisplayColumn(column.columnName, col.columnName) + } + className="h-3 w-3" + /> + +
+ ))} +
+
+ + {/* 조인 테이블 컬럼 */} +
+ +
+ {entityDisplayConfigs[column.columnName].joinColumns.map((col) => ( +
+ + toggleEntityDisplayColumn(column.columnName, col.columnName) + } + className="h-3 w-3" + /> + +
+ ))} +
+
+ + {/* 선택된 컬럼 미리보기 */} + {entityDisplayConfigs[column.columnName].selectedColumns.length > 0 && ( +
+ +
+ {entityDisplayConfigs[column.columnName].selectedColumns.map((colName, idx) => ( + + + {colName} + + {idx < entityDisplayConfigs[column.columnName].selectedColumns.length - 1 && ( + + {entityDisplayConfigs[column.columnName].separator} + + )} + + ))} +
+
+ )} +
+ )} +
+ ))} +
+
+ )} + {!screenTableName ? ( @@ -820,6 +1288,20 @@ export const TableListConfigPanel: React.FC = ({ />
+ {/* 엔티티 타입 컬럼 표시 */} + {column.isEntityJoin && ( +
+
+ + 엔티티 타입 + + + 표시 컬럼 설정은 상단의 "🎯 엔티티 컬럼 표시 설정" 섹션에서 하세요 + +
+
+ )} +