From 4aefb5be6a7a9e888245208fb542a86956eba665 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 23 Sep 2025 15:58:54 +0900 Subject: [PATCH 01/14] =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EB=8B=A4=EC=A4=91=20=ED=91=9C=EC=8B=9C=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=BC=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontend: - EntityTypeConfig 인터페이스에 displayColumns 배열 추가 - EntityTypeConfigPanel에서 여러 표시 컬럼 선택 UI 구현 - 구분자 설정 기능 추가 - 하위 호환성을 위한 displayColumn 유지 Backend: - EntityJoinConfig에 displayColumns 배열 지원 - 화면별 엔티티 설정을 전달받는 API 확장 - CONCAT을 사용한 다중 컬럼 표시 SQL 생성 - 기존 단일 컬럼과의 호환성 유지 이제 화면마다 다른 표시 컬럼 조합을 설정할 수 있음 예: 한 화면에서는 '이름'만, 다른 화면에서는 '이름 - 부서명' 표시 --- .../src/controllers/entityJoinController.ts | 17 + .../src/services/entityJoinService.ts | 49 ++- .../src/services/tableManagementService.ts | 5 +- backend-node/src/types/tableManagement.ts | 4 +- .../webtype-configs/EntityTypeConfigPanel.tsx | 322 ++++++++---------- frontend/types/screen-management.ts | 5 +- 6 files changed, 209 insertions(+), 193 deletions(-) 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; // 여러 컬럼 표시 시 구분자 (기본: ' - ') } /** From de6c7a8008ddca886924722db0ab4018d059ae94 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 23 Sep 2025 16:23:36 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=BB=AC=EB=9F=BC=20=ED=91=9C=EC=8B=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=EC=9D=84=20=ED=99=94=EB=A9=B4=20=ED=8E=B8?= =?UTF-8?q?=EC=A7=91=EA=B8=B0=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테이블 타입 관리에서 엔티티 타입의 표시 컬럼 설정 완전 제거 - 컬럼 설정 패널에서 엔티티 타입일 때 표시 컬럼 조합 선택 기능 추가 - 기본 테이블과 조인 테이블의 컬럼을 자유롭게 조합 가능 - 구분자 설정 및 실시간 미리보기 기능 포함 - 별도 모달 방식 제거하고 기존 컬럼 설정 패널에 통합 --- .../src/controllers/entityJoinController.ts | 14 +- .../src/services/entityJoinService.ts | 21 +- .../src/services/tableManagementService.ts | 19 +- .../webtype-configs/EntityTypeConfigPanel.tsx | 34 ++- frontend/lib/api/entityJoin.ts | 2 + .../table-list/TableListComponent.tsx | 24 +- .../table-list/TableListConfigPanel.tsx | 220 +++++++++++++++++- .../registry/components/table-list/types.ts | 8 + frontend/types/screen-management.ts | 2 +- 9 files changed, 293 insertions(+), 51 deletions(-) diff --git a/backend-node/src/controllers/entityJoinController.ts b/backend-node/src/controllers/entityJoinController.ts index 12c04ba4..77fdb0dd 100644 --- a/backend-node/src/controllers/entityJoinController.ts +++ b/backend-node/src/controllers/entityJoinController.ts @@ -74,7 +74,10 @@ export class EntityJoinController { typeof screenEntityConfigs === "string" ? JSON.parse(screenEntityConfigs) : screenEntityConfigs; - logger.info("화면별 엔티티 설정 파싱 완료:", parsedScreenEntityConfigs); + logger.info( + "화면별 엔티티 설정 파싱 완료:", + parsedScreenEntityConfigs + ); } catch (error) { logger.warn("화면별 엔티티 설정 파싱 오류:", error); parsedScreenEntityConfigs = {}; @@ -365,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, @@ -390,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 de3328fb..24886a3d 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -20,7 +20,7 @@ export class EntityJoinService { * @param screenEntityConfigs 화면별 엔티티 설정 (선택사항) */ async detectEntityJoins( - tableName: string, + tableName: string, screenEntityConfigs?: Record ): Promise { try { @@ -57,7 +57,7 @@ export class EntityJoinService { const screenConfig = screenEntityConfigs?.[column.column_name]; let displayColumns: string[] = []; let separator = " - "; - + if (screenConfig && screenConfig.displayColumns) { // 화면에서 설정된 표시 컬럼들 사용 displayColumns = screenConfig.displayColumns; @@ -66,8 +66,11 @@ export class EntityJoinService { // 기존 설정된 단일 표시 컬럼 사용 displayColumns = [column.display_column]; } else { - // 기본값: reference_column 사용 - displayColumns = [column.reference_column]; + // 화면에서 설정하도록 빈 배열로 초기화 (테이블 타입 관리에서 표시 컬럼 설정 제거) + displayColumns = []; + console.log( + `🎯 표시 컬럼을 화면에서 설정하도록 초기화: ${column.column_name} (테이블 타입 관리에서 표시 컬럼 설정 제거됨)` + ); } // 별칭 컬럼명 생성 (writer -> writer_name) @@ -153,16 +156,18 @@ export class EntityJoinService { const joinColumns = joinConfigs .map((config) => { const alias = aliasMap.get(config.referenceTable); - const displayColumns = config.displayColumns || [config.displayColumn]; + 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}, '')`) + .map((col) => `COALESCE(${alias}.${col}, '')`) .join(`, '${separator}', `); return `CONCAT(${concatParts}) AS ${config.aliasColumn}`; } @@ -236,7 +241,7 @@ export class EntityJoinService { const cachedData = await referenceCacheService.getCachedReference( config.referenceTable, config.referenceColumn, - config.displayColumn + config.displayColumn || config.displayColumns[0] ); return cachedData ? "cache" : "join"; diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index c5de403d..69175941 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -2044,7 +2044,10 @@ export class TableManagementService { } // Entity 조인 설정 감지 (화면별 엔티티 설정 전달) - let joinConfigs = await entityJoinService.detectEntityJoins(tableName, options.screenEntityConfigs); + let joinConfigs = await entityJoinService.detectEntityJoins( + tableName, + options.screenEntityConfigs + ); // 추가 조인 컬럼 정보가 있으면 조인 설정에 추가 if ( @@ -2068,8 +2071,10 @@ export class TableManagementService { sourceColumn: baseJoinConfig.sourceColumn, // 원본 컬럼 (writer) referenceTable: additionalColumn.sourceTable, // 참조 테이블 (user_info) referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (user_id) - displayColumn: additionalColumn.sourceColumn, // 표시할 컬럼 (email) + displayColumns: [additionalColumn.sourceColumn], // 표시할 컬럼들 (email) + displayColumn: additionalColumn.sourceColumn, // 하위 호환성 aliasColumn: additionalColumn.joinAlias, // 별칭 (writer_email) + separator: " - ", // 기본 구분자 }; joinConfigs.push(additionalJoinConfig); @@ -2243,7 +2248,7 @@ export class TableManagementService { await referenceCacheService.getCachedReference( config.referenceTable, config.referenceColumn, - config.displayColumn + config.displayColumn || config.displayColumns[0] ); } @@ -2430,7 +2435,7 @@ export class TableManagementService { const lookupValue = referenceCacheService.getLookupValue( config.referenceTable, config.referenceColumn, - config.displayColumn, + config.displayColumn || config.displayColumns[0], String(sourceValue) ); @@ -2724,7 +2729,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) { @@ -2808,7 +2813,7 @@ export class TableManagementService { const cachedData = await referenceCacheService.getCachedReference( config.referenceTable, config.referenceColumn, - config.displayColumn + config.displayColumn || config.displayColumns[0] ); if (cachedData) { @@ -2847,7 +2852,7 @@ export class TableManagementService { const hitRate = referenceCacheService.getCacheHitRate( config.referenceTable, config.referenceColumn, - config.displayColumn + config.displayColumn || config.displayColumns[0] ); totalHitRate += hitRate; } diff --git a/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx b/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx index 866bd65a..08fd5276 100644 --- a/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx +++ b/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx @@ -171,7 +171,7 @@ export const EntityTypeConfigPanel: React.FC = ({ co {/* 표시 컬럼들 (다중 선택) */}
- + {/* 현재 선택된 표시 컬럼들 */}
{localValues.displayColumns.map((column, index) => ( @@ -183,7 +183,7 @@ export const EntityTypeConfigPanel: React.FC = ({ co
))} - + {localValues.displayColumns.length === 0 && (
표시할 컬럼을 추가해주세요
)} @@ -197,20 +197,19 @@ export const EntityTypeConfigPanel: React.FC = ({ co placeholder="컬럼명 입력 (예: user_name, dept_name)" className="flex-1" /> -
- +
- • 여러 컬럼을 선택하면 "{localValues.separator || ' - '}"로 구분하여 표시됩니다 -
- • 예: 이름{localValues.separator || ' - '}부서명 + • 여러 컬럼을 선택하면 "{localValues.separator || " - "}"로 구분하여 표시됩니다 +
• 예: 이름{localValues.separator || " - "}부서명
@@ -261,7 +260,6 @@ export const EntityTypeConfigPanel: React.FC = ({ co />
- {/* 필터 관리 */}
@@ -328,7 +326,10 @@ export const EntityTypeConfigPanel: React.FC = ({ co
참조테이블: {localValues.referenceTable || "없음"}, 조인컬럼: {localValues.referenceColumn}
- 표시컬럼: {localValues.displayColumns.length > 0 ? localValues.displayColumns.join(localValues.separator || ' - ') : "없음"} + 표시컬럼:{" "} + {localValues.displayColumns.length > 0 + ? localValues.displayColumns.join(localValues.separator || " - ") + : "없음"}
@@ -337,14 +338,11 @@ export const EntityTypeConfigPanel: React.FC = ({ co
엔터티 타입 설정 가이드
참조 테이블: 데이터를 가져올 다른 테이블 이름 -
- • 조인 컬럼: 테이블 간 연결에 사용할 기준 컬럼 (보통 ID) -
- • 표시 컬럼: 사용자에게 보여질 컬럼들 (여러 개 가능) +
조인 컬럼: 테이블 간 연결에 사용할 기준 컬럼 (보통 ID) +
표시 컬럼: 사용자에게 보여질 컬럼들 (여러 개 가능)
• 여러 표시 컬럼 설정 시 화면마다 다르게 표시할 수 있습니다 -
- • 예: 사용자 선택 시 "이름"만 보이거나 "이름 - 부서명" 형태로 표시 +
• 예: 사용자 선택 시 "이름"만 보이거나 "이름 - 부서명" 형태로 표시
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..a10c6fa4 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -192,12 +192,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") { + // 백엔드에서 받은 displayColumnLabel을 사용하거나, 없으면 기본값 사용 + displayLabel = column.displayColumnLabel || column.displayColumn || `${column.columnName}_name`; console.log( - `🎯 Entity 조인 컬럼 라벨 설정: ${column.columnName} → "${displayLabel}" (${column.displayColumn})`, + `🎯 Entity 조인 컬럼 라벨 설정: ${column.columnName} → "${displayLabel}" (${column.displayColumn || "기본값"})`, ); } @@ -260,7 +260,20 @@ export const TableListComponent: React.FC = ({ joinAlias: col.entityJoinInfo!.joinAlias, })); + // 🎯 화면별 엔티티 표시 설정 생성 + 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 조인 컬럼:", additionalJoinColumns); + console.log("🎯 화면별 엔티티 설정:", screenEntityConfigs); const result = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, { page: currentPage, @@ -329,6 +342,7 @@ 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) { diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index 7ea374f2..7eeb0d76 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -58,8 +58,17 @@ export const TableListConfigPanel: React.FC = ({ }>; }>; }>({ availableColumns: [], joinTables: [] }); + const [loadingEntityJoins, setLoadingEntityJoins] = useState(false); + // 🎯 엔티티 컬럼 표시 설정을 위한 상태 + const [entityDisplayConfigs, setEntityDisplayConfigs] = useState; + joinColumns: Array<{ columnName: string; displayName: string; dataType: string }>; + selectedColumns: string[]; + separator: string; + }>>({}); + // 화면 테이블명이 있으면 자동으로 설정 useEffect(() => { if (screenTableName && (!config.selectedTable || config.selectedTable !== screenTableName)) { @@ -228,30 +237,38 @@ 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; + // 기본 표시명으로 엔티티 컬럼 추가 (컬럼 설정 패널에서 나중에 표시 컬럼 조합 선택) 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 조인 컬럼임을 표시 + isEntityJoin: true, entityJoinInfo: { - sourceTable: joinColumn.tableName, + sourceTable: config.selectedTable || "", sourceColumn: joinColumn.columnName, joinAlias: joinColumn.joinAlias, }, + // 🎯 엔티티 표시 설정 (기본값으로 초기화, 컬럼 설정에서 수정 가능) + entityDisplayConfig: { + displayColumns: [], // 빈 배열로 초기화 + separator: " - ", + sourceTable: config.selectedTable || "", + joinTable: joinColumn.tableName, + }, }; handleChange("columns", [...(config.columns || []), newColumn]); - console.log("🔗 Entity 조인 컬럼 추가됨:", newColumn); + console.log("🔗 엔티티 컬럼 추가됨 (표시 컬럼은 컬럼 설정에서 선택):", newColumn); }; // 컬럼 제거 @@ -267,6 +284,90 @@ export const TableListConfigPanel: React.FC = ({ handleChange("columns", updatedColumns); }; + // 🎯 엔티티 컬럼의 표시 컬럼 정보 로드 + const loadEntityDisplayConfig = async (column: ColumnConfig) => { + if (!column.isEntityJoin || !column.entityJoinInfo || !column.entityDisplayConfig) return; + + const { sourceTable, joinTable } = column.entityDisplayConfig; + const configKey = `${column.columnName}`; + + // 이미 로드된 경우 스킵 + if (entityDisplayConfigs[configKey]) return; + + try { + // 기본 테이블과 조인 테이블의 컬럼 정보를 병렬로 로드 + const [sourceResult, joinResult] = await Promise.all([ + entityJoinApi.getReferenceTableColumns(sourceTable), + entityJoinApi.getReferenceTableColumns(joinTable), + ]); + + 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 config = entityDisplayConfigs[configKey]; + if (!config) return; + + const newSelectedColumns = config.selectedColumns.includes(selectedColumn) + ? config.selectedColumns.filter(col => col !== selectedColumn) + : [...config.selectedColumns, selectedColumn]; + + setEntityDisplayConfigs(prev => ({ + ...prev, + [configKey]: { + ...prev[configKey], + selectedColumns: newSelectedColumns, + }, + })); + + // 컬럼 설정 업데이트 + updateColumn(columnName, { + entityDisplayConfig: { + ...config.entityDisplayConfig, + displayColumns: newSelectedColumns, + }, + }); + }; + + // 🎯 엔티티 표시 구분자 업데이트 + const updateEntityDisplaySeparator = (columnName: string, separator: string) => { + const configKey = `${columnName}`; + const config = entityDisplayConfigs[configKey]; + if (!config) return; + + setEntityDisplayConfigs(prev => ({ + ...prev, + [configKey]: { + ...prev[configKey], + separator, + }, + })); + + // 컬럼 설정 업데이트 + updateColumn(columnName, { + entityDisplayConfig: { + ...config.entityDisplayConfig, + separator, + }, + }); + }; + // 컬럼 순서 변경 const moveColumn = (columnName: string, direction: "up" | "down") => { const columns = [...(config.columns || [])]; @@ -820,6 +921,108 @@ export const TableListConfigPanel: React.FC = ({ /> + {/* 🎯 엔티티 타입 컬럼일 때 표시 컬럼 선택 UI */} + {column.isEntityJoin && column.entityDisplayConfig && ( +
+
+ + +
+ + {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} + )} + + ))} +
+
+ )} +
+ )} +
+ )} +
- handleDetailSettingsChange( - column.columnName, - "entity_display_column", - value, - ) - } - > - - - - - -- 선택 안함 -- - {referenceTableColumns[column.referenceTable]?.map((refCol, index) => ( - - {refCol.columnName} - - ))} - {(!referenceTableColumns[column.referenceTable] || - referenceTableColumns[column.referenceTable].length === 0) && ( - -
-
- 로딩중 -
-
- )} -
- -
- )} {/* 설정 완료 표시 - 간소화 */} 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/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index a10c6fa4..64a2eab6 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -194,10 +194,10 @@ export const TableListComponent: React.FC = ({ // Entity 타입인 경우 if (column.webType === "entity") { - // 백엔드에서 받은 displayColumnLabel을 사용하거나, 없으면 기본값 사용 - displayLabel = column.displayColumnLabel || column.displayColumn || `${column.columnName}_name`; + // 우선 기준 테이블의 컬럼 라벨을 사용 + displayLabel = column.displayName || column.columnName; console.log( - `🎯 Entity 조인 컬럼 라벨 설정: ${column.columnName} → "${displayLabel}" (${column.displayColumn || "기본값"})`, + `🎯 Entity 조인 컬럼 라벨 설정: ${column.columnName} → "${displayLabel}" (기준 테이블 라벨 사용)`, ); } diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index 7eeb0d76..a4c1a5c6 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -62,12 +62,17 @@ export const TableListConfigPanel: React.FC = ({ const [loadingEntityJoins, setLoadingEntityJoins] = useState(false); // 🎯 엔티티 컬럼 표시 설정을 위한 상태 - const [entityDisplayConfigs, setEntityDisplayConfigs] = useState; - joinColumns: Array<{ columnName: string; displayName: string; dataType: string }>; - selectedColumns: string[]; - separator: string; - }>>({}); + 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(() => { @@ -284,6 +289,72 @@ 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.column_name === 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("🎯 엔티티 컬럼 감지 및 플래그 설정:", column.columnName); + + return { + ...column, + isEntityJoin: true, + entityJoinInfo: { + sourceTable: config.selectedTable || "", + sourceColumn: column.columnName, + joinAlias: column.columnName, + }, + entityDisplayConfig: { + displayColumns: [], // 빈 배열로 초기화 + separator: " - ", + sourceTable: config.selectedTable || "", + joinTable: tableColumn.reference_table || "", + }, + }; + } + + 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) => { if (!column.isEntityJoin || !column.entityJoinInfo || !column.entityDisplayConfig) return; @@ -304,7 +375,7 @@ export const TableListConfigPanel: React.FC = ({ const sourceColumns = sourceResult.columns || []; const joinColumns = joinResult.columns || []; - setEntityDisplayConfigs(prev => ({ + setEntityDisplayConfigs((prev) => ({ ...prev, [configKey]: { sourceColumns, @@ -325,10 +396,10 @@ export const TableListConfigPanel: React.FC = ({ if (!config) return; const newSelectedColumns = config.selectedColumns.includes(selectedColumn) - ? config.selectedColumns.filter(col => col !== selectedColumn) + ? config.selectedColumns.filter((col) => col !== selectedColumn) : [...config.selectedColumns, selectedColumn]; - setEntityDisplayConfigs(prev => ({ + setEntityDisplayConfigs((prev) => ({ ...prev, [configKey]: { ...prev[configKey], @@ -351,7 +422,7 @@ export const TableListConfigPanel: React.FC = ({ const config = entityDisplayConfigs[configKey]; if (!config) return; - setEntityDisplayConfigs(prev => ({ + setEntityDisplayConfigs((prev) => ({ ...prev, [configKey]: { ...prev[configKey], @@ -791,6 +862,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 ? ( @@ -921,105 +1121,17 @@ export const TableListConfigPanel: React.FC = ({ /> - {/* 🎯 엔티티 타입 컬럼일 때 표시 컬럼 선택 UI */} - {column.isEntityJoin && column.entityDisplayConfig && ( -
-
- - + {/* 엔티티 타입 컬럼 표시 */} + {column.isEntityJoin && ( +
+
+ + 엔티티 타입 + + + 표시 컬럼 설정은 상단의 "🎯 엔티티 컬럼 표시 설정" 섹션에서 하세요 +
- - {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} - )} - - ))} -
-
- )} -
- )}
)} @@ -1528,7 +1640,6 @@ export const TableListConfigPanel: React.FC = ({ -
); }; diff --git a/frontend/types/table-management.ts b/frontend/types/table-management.ts index d5bcf127..bd2cac09 100644 --- a/frontend/types/table-management.ts +++ b/frontend/types/table-management.ts @@ -46,6 +46,7 @@ export interface UnifiedColumnInfo { // 입력 설정 inputType: "direct" | "auto"; + input_type?: string; // 🎯 데이터베이스의 input_type 필드 (entity, text, number 등) detailSettings?: Record; // JSON 파싱된 객체 description?: string; From f01be49f6afd6f7e71051dfcd4fa345a728d0302 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 23 Sep 2025 16:59:12 +0900 Subject: [PATCH 04/14] =?UTF-8?q?fix:=20TableListConfigPanel=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=BB=AC=EB=9F=BC=20=EA=B2=80=EC=83=89=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tc.column_name → tc.columnName으로 수정 - tableColumns 구조에 맞게 컬럼 검색 로직 수정 - 디버깅 로그 개선 --- .../table-list/TableListConfigPanel.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index a4c1a5c6..bf150fe5 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -31,7 +31,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); @@ -296,7 +301,7 @@ export const TableListConfigPanel: React.FC = ({ columnsCount: config.columns?.length || 0, hasTableColumns: !!tableColumns, tableColumnsCount: tableColumns?.length || 0, - selectedTable: config.selectedTable + selectedTable: config.selectedTable, }); if (!config.columns || !tableColumns) { @@ -312,12 +317,12 @@ export const TableListConfigPanel: React.FC = ({ } // 테이블 컬럼 정보에서 해당 컬럼 찾기 - const tableColumn = tableColumns.find((tc) => tc.column_name === column.columnName); + const tableColumn = tableColumns.find((tc) => tc.columnName === column.columnName); console.log("🔍 컬럼 검색:", { columnName: column.columnName, found: !!tableColumn, inputType: tableColumn?.input_type, - webType: tableColumn?.web_type + webType: tableColumn?.web_type, }); // 엔티티 타입인 경우 isEntityJoin 플래그 설정 (input_type 또는 web_type 확인) @@ -468,7 +473,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 => { From ad7f350f00f44553ba31c0b8d59037db0980384e Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 23 Sep 2025 17:06:23 +0900 Subject: [PATCH 05/14] =?UTF-8?q?fix:=20API=20=ED=81=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=96=B8=ED=8A=B8=20=EC=84=A4=EC=A0=95=20=EC=9B=90=EB=B3=B5=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B9=88=20=ED=85=8C=EC=9D=B4=EB=B8=94=EB=AA=85=20?= =?UTF-8?q?API=20=ED=98=B8=EC=B6=9C=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API 클라이언트를 원래 포트 8080으로 되돌림 - loadEntityDisplayConfig에서 sourceTable/joinTable이 비어있을 때 API 호출 방지 - 불필요한 백엔드 서버 중지 --- frontend/components/screen/panels/DetailSettingsPanel.tsx | 2 ++ frontend/lib/api/client.ts | 4 ++-- .../registry/components/table-list/TableListConfigPanel.tsx | 6 ++++++ 3 files changed, 10 insertions(+), 2 deletions(-) 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/lib/api/client.ts b/frontend/lib/api/client.ts index 2660014f..a02871d0 100644 --- a/frontend/lib/api/client.ts +++ b/frontend/lib/api/client.ts @@ -6,12 +6,12 @@ const getApiBaseUrl = (): string => { const currentHost = window.location.hostname; const currentPort = window.location.port; - // 로컬 개발환경: localhost:9771 또는 localhost:3000 → localhost:8080 + // 로컬 개발환경: localhost:9771 또는 localhost:3000 → localhost:3001 if ( (currentHost === "localhost" || currentHost === "127.0.0.1") && (currentPort === "9771" || currentPort === "3000") ) { - return "http://localhost:8080/api"; + return "http://localhost:3001/api"; } // 서버 환경에서 localhost:5555 → 39.117.244.52:8080 diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index bf150fe5..a2b2997e 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -370,6 +370,12 @@ export const TableListConfigPanel: React.FC = ({ // 이미 로드된 경우 스킵 if (entityDisplayConfigs[configKey]) return; + // sourceTable과 joinTable이 모두 있어야 로드 + if (!sourceTable || !joinTable) { + console.log("⚠️ sourceTable 또는 joinTable이 비어있어서 로드 스킵:", { sourceTable, joinTable }); + return; + } + try { // 기본 테이블과 조인 테이블의 컬럼 정보를 병렬로 로드 const [sourceResult, joinResult] = await Promise.all([ From 4c5e0330ef2c56062e9dc52d7bd745fc062b4de5 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 23 Sep 2025 17:08:52 +0900 Subject: [PATCH 06/14] =?UTF-8?q?fix:=20TableListConfigPanel=20API=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EC=BB=AC=EB=9F=BC=20=EC=B0=B8=EC=A1=B0=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EB=A1=9C=EA=B9=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fetch('/api/tables') 직접 호출을 tableTypeApi.getTables()로 변경하여 올바른 포트 사용 - 엔티티 컬럼 감지 시 reference_table/referenceTable 필드 로깅 추가 - joinTable 설정 시 두 필드 모두 확인하도록 수정 --- frontend/lib/api/client.ts | 4 +- .../table-list/TableListConfigPanel.tsx | 38 ++++++++++--------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/frontend/lib/api/client.ts b/frontend/lib/api/client.ts index a02871d0..2660014f 100644 --- a/frontend/lib/api/client.ts +++ b/frontend/lib/api/client.ts @@ -6,12 +6,12 @@ const getApiBaseUrl = (): string => { const currentHost = window.location.hostname; const currentPort = window.location.port; - // 로컬 개발환경: localhost:9771 또는 localhost:3000 → localhost:3001 + // 로컬 개발환경: localhost:9771 또는 localhost:3000 → localhost:8080 if ( (currentHost === "localhost" || currentHost === "127.0.0.1") && (currentPort === "9771" || currentPort === "3000") ) { - return "http://localhost:3001/api"; + return "http://localhost:8080/api"; } // 서버 환경에서 localhost:5555 → 39.117.244.52:8080 diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index a2b2997e..e6e0ec02 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,11 +32,11 @@ export const TableListConfigPanel: React.FC = ({ screenTableName, tableColumns, }) => { - console.log("🔍 TableListConfigPanel props:", { - config: config?.selectedTable, - screenTableName, + console.log("🔍 TableListConfigPanel props:", { + config: config?.selectedTable, + screenTableName, tableColumns: tableColumns?.length, - tableColumnsSample: tableColumns?.[0] + tableColumnsSample: tableColumns?.[0], }); const [availableTables, setAvailableTables] = useState>([]); @@ -92,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 { @@ -327,7 +324,12 @@ export const TableListConfigPanel: React.FC = ({ // 엔티티 타입인 경우 isEntityJoin 플래그 설정 (input_type 또는 web_type 확인) if (tableColumn && (tableColumn.input_type === "entity" || tableColumn.web_type === "entity")) { - console.log("🎯 엔티티 컬럼 감지 및 플래그 설정:", column.columnName); + console.log("🎯 엔티티 컬럼 감지 및 플래그 설정:", { + columnName: column.columnName, + referenceTable: tableColumn.reference_table, + referenceTableAlt: tableColumn.referenceTable, + allTableColumnKeys: Object.keys(tableColumn), + }); return { ...column, @@ -341,7 +343,7 @@ export const TableListConfigPanel: React.FC = ({ displayColumns: [], // 빈 배열로 초기화 separator: " - ", sourceTable: config.selectedTable || "", - joinTable: tableColumn.reference_table || "", + joinTable: tableColumn.reference_table || tableColumn.referenceTable || "", }, }; } From 28109eb63b0a78a86ddf629bf1fd277fa33cfbc7 Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 23 Sep 2025 17:11:07 +0900 Subject: [PATCH 07/14] =?UTF-8?q?fix:=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EC=A1=B0=EC=9D=B8=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EC=A0=95=EB=B3=B4=20=EC=9E=90=EB=8F=99=20=EA=B0=80?= =?UTF-8?q?=EC=A0=B8=EC=98=A4=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - loadEntityDisplayConfig에서 joinTable이 비어있을 때 Entity 조인 API로 조인 테이블 정보 자동 조회 - 조인 테이블 정보를 찾으면 entityDisplayConfig에 자동 업데이트 - 상세한 로깅으로 조인 테이블 정보 조회 과정 추적 가능 --- .../table-list/TableListConfigPanel.tsx | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index e6e0ec02..96deb641 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -372,9 +372,47 @@ export const TableListConfigPanel: React.FC = ({ // 이미 로드된 경우 스킵 if (entityDisplayConfigs[configKey]) return; + // joinTable이 비어있으면 Entity 조인 API로 조인 테이블 정보를 가져와서 설정 + let actualJoinTable = joinTable; + if (!actualJoinTable && sourceTable) { + try { + console.log("🔍 조인 테이블 정보를 Entity 조인 API로 가져오기:", sourceTable); + const entityJoinResult = await entityJoinApi.getEntityJoinColumns(sourceTable); + + // 해당 컬럼에 대한 조인 설정 찾기 + const columnJoinConfig = entityJoinResult.availableColumns?.find( + (col) => col.columnName === column.columnName + ); + + if (columnJoinConfig?.joinTable) { + actualJoinTable = columnJoinConfig.joinTable; + console.log("✅ 조인 테이블 정보 찾음:", 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); + } + } + } catch (error) { + console.error("Entity 조인 정보 조회 실패:", error); + } + } + // sourceTable과 joinTable이 모두 있어야 로드 - if (!sourceTable || !joinTable) { - console.log("⚠️ sourceTable 또는 joinTable이 비어있어서 로드 스킵:", { sourceTable, joinTable }); + if (!sourceTable || !actualJoinTable) { + console.log("⚠️ sourceTable 또는 joinTable이 비어있어서 로드 스킵:", { sourceTable, joinTable: actualJoinTable }); return; } @@ -382,7 +420,7 @@ export const TableListConfigPanel: React.FC = ({ // 기본 테이블과 조인 테이블의 컬럼 정보를 병렬로 로드 const [sourceResult, joinResult] = await Promise.all([ entityJoinApi.getReferenceTableColumns(sourceTable), - entityJoinApi.getReferenceTableColumns(joinTable), + entityJoinApi.getReferenceTableColumns(actualJoinTable), ]); const sourceColumns = sourceResult.columns || []; From a757034d8615a64d8016a7f235cf3c1db2210a1f Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 23 Sep 2025 17:43:24 +0900 Subject: [PATCH 08/14] =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=ED=91=9C=EC=8B=9C=EB=B0=A9=EC=8B=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/entityJoinService.ts | 59 ++++- .../table-list/TableListConfigPanel.tsx | 216 ++++++++++++++---- 2 files changed, 232 insertions(+), 43 deletions(-) diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index 24886a3d..76848714 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -59,9 +59,14 @@ export class EntityJoinService { 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) { // 기존 설정된 단일 표시 컬럼 사용 displayColumns = [column.display_column]; @@ -163,12 +168,51 @@ export class EntityJoinService { if (displayColumns.length === 1) { // 단일 컬럼인 경우 - return `COALESCE(${alias}.${displayColumns[0]}, '') AS ${config.aliasColumn}`; + 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}, '') AS ${config.aliasColumn}`; + } else { + return `COALESCE(main.${col}, '') AS ${config.aliasColumn}`; + } } else { // 여러 컬럼인 경우 CONCAT으로 연결 + // 기본 테이블과 조인 테이블의 컬럼을 구분해서 처리 const concatParts = displayColumns - .map((col) => `COALESCE(${alias}.${col}, '')`) + .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}, '')`; + } else { + // 기본 테이블 컬럼은 main 별칭 사용 + return `COALESCE(main.${col}, '')`; + } + }) .join(`, '${separator}', `); + return `CONCAT(${concatParts}) AS ${config.aliasColumn}`; } }) @@ -237,6 +281,15 @@ 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, diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index 96deb641..6a9007d5 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -364,49 +364,145 @@ export const TableListConfigPanel: React.FC = ({ // 🎯 엔티티 컬럼의 표시 컬럼 정보 로드 const loadEntityDisplayConfig = async (column: ColumnConfig) => { - if (!column.isEntityJoin || !column.entityJoinInfo || !column.entityDisplayConfig) return; + console.log("🔍 loadEntityDisplayConfig 시작:", { + columnName: column.columnName, + isEntityJoin: column.isEntityJoin, + entityJoinInfo: column.entityJoinInfo, + entityDisplayConfig: column.entityDisplayConfig, + configSelectedTable: config.selectedTable, + }); - const { sourceTable, joinTable } = column.entityDisplayConfig; - const configKey = `${column.columnName}`; + if (!column.isEntityJoin || !column.entityJoinInfo) { + console.log("⚠️ 엔티티 컬럼 조건 불만족:", { + isEntityJoin: column.isEntityJoin, + entityJoinInfo: column.entityJoinInfo, + }); + return; + } - // 이미 로드된 경우 스킵 - if (entityDisplayConfigs[configKey]) 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; + }); - // joinTable이 비어있으면 Entity 조인 API로 조인 테이블 정보를 가져와서 설정 - let actualJoinTable = joinTable; - if (!actualJoinTable && sourceTable) { + 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("🔍 조인 테이블 정보를 Entity 조인 API로 가져오기:", sourceTable); - const entityJoinResult = await entityJoinApi.getEntityJoinColumns(sourceTable); - - // 해당 컬럼에 대한 조인 설정 찾기 - const columnJoinConfig = entityJoinResult.availableColumns?.find( - (col) => col.columnName === column.columnName - ); - - if (columnJoinConfig?.joinTable) { - actualJoinTable = columnJoinConfig.joinTable; - console.log("✅ 조인 테이블 정보 찾음:", actualJoinTable); - + 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, - joinTable: actualJoinTable, + sourceTable: sourceTable, + joinTable: joinTable, }; - + // 컬럼 설정 업데이트 const updatedColumns = config.columns?.map((col) => - col.columnName === column.columnName - ? { ...col, entityDisplayConfig: updatedConfig } - : col + col.columnName === column.columnName ? { ...col, entityDisplayConfig: updatedConfig } : col, ); - + if (updatedColumns) { handleChange("columns", updatedColumns); } } } catch (error) { - console.error("Entity 조인 정보 조회 실패:", 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); } } @@ -443,13 +539,14 @@ export const TableListConfigPanel: React.FC = ({ // 🎯 엔티티 표시 컬럼 선택 토글 const toggleEntityDisplayColumn = (columnName: string, selectedColumn: string) => { const configKey = `${columnName}`; - const config = entityDisplayConfigs[configKey]; - if (!config) return; + const localConfig = entityDisplayConfigs[configKey]; + if (!localConfig) return; - const newSelectedColumns = config.selectedColumns.includes(selectedColumn) - ? config.selectedColumns.filter((col) => col !== selectedColumn) - : [...config.selectedColumns, selectedColumn]; + const newSelectedColumns = localConfig.selectedColumns.includes(selectedColumn) + ? localConfig.selectedColumns.filter((col) => col !== selectedColumn) + : [...localConfig.selectedColumns, selectedColumn]; + // 로컬 상태 업데이트 setEntityDisplayConfigs((prev) => ({ ...prev, [configKey]: { @@ -458,6 +555,29 @@ export const TableListConfigPanel: React.FC = ({ }, })); + // 실제 컬럼 설정도 업데이트 + 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), + }); + } + // 컬럼 설정 업데이트 updateColumn(columnName, { entityDisplayConfig: { @@ -470,9 +590,10 @@ export const TableListConfigPanel: React.FC = ({ // 🎯 엔티티 표시 구분자 업데이트 const updateEntityDisplaySeparator = (columnName: string, separator: string) => { const configKey = `${columnName}`; - const config = entityDisplayConfigs[configKey]; - if (!config) return; + const localConfig = entityDisplayConfigs[configKey]; + if (!localConfig) return; + // 로컬 상태 업데이트 setEntityDisplayConfigs((prev) => ({ ...prev, [configKey]: { @@ -481,13 +602,28 @@ export const TableListConfigPanel: React.FC = ({ }, })); - // 컬럼 설정 업데이트 - updateColumn(columnName, { - entityDisplayConfig: { - ...config.entityDisplayConfig, - 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), + }); + } }; // 컬럼 순서 변경 From e75889a127160f3c3bf649db63becb5dea4b8149 Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 24 Sep 2025 10:33:54 +0900 Subject: [PATCH 09/14] =?UTF-8?q?=EC=A1=B0=EC=9D=B8=20=EC=BB=AC=EB=9F=BC?= =?UTF-8?q?=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/entityJoinService.ts | 50 ++-- .../src/services/tableManagementService.ts | 20 +- .../table-list/TableListComponent.tsx | 221 ++++++++++++++++-- .../table-list/TableListConfigPanel.tsx | 28 +-- 4 files changed, 248 insertions(+), 71 deletions(-) diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index 76848714..d0b01846 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -67,14 +67,24 @@ export class EntityJoinService { separator, screenConfig, }); - } else if (column.display_column) { - // 기존 설정된 단일 표시 컬럼 사용 + } else if (column.display_column && column.display_column !== "none") { + // 기존 설정된 단일 표시 컬럼 사용 (none이 아닌 경우만) displayColumns = [column.display_column]; } else { - // 화면에서 설정하도록 빈 배열로 초기화 (테이블 타입 관리에서 표시 컬럼 설정 제거) - displayColumns = []; + // 조인 탭에서 보여줄 기본 표시 컬럼 설정 + // 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} (테이블 타입 관리에서 표시 컬럼 설정 제거됨)` + `🔧 조인 탭용 기본 표시 컬럼 설정: ${column.column_name} → ${defaultDisplayColumn} (${column.reference_table})` ); } @@ -119,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 절과 동일한 로직) @@ -181,9 +193,9 @@ export class EntityJoinService { ].includes(col); if (isJoinTableColumn) { - return `COALESCE(${alias}.${col}, '') AS ${config.aliasColumn}`; + return `COALESCE(${alias}.${col}::TEXT, '') AS ${config.aliasColumn}`; } else { - return `COALESCE(main.${col}, '') AS ${config.aliasColumn}`; + return `COALESCE(main.${col}::TEXT, '') AS ${config.aliasColumn}`; } } else { // 여러 컬럼인 경우 CONCAT으로 연결 @@ -205,15 +217,15 @@ export class EntityJoinService { if (isJoinTableColumn) { // 조인 테이블 컬럼은 조인 별칭 사용 - return `COALESCE(${alias}.${col}, '')`; + return `COALESCE(${alias}.${col}::TEXT, '')`; } else { // 기본 테이블 컬럼은 main 별칭 사용 - return `COALESCE(main.${col}, '')`; + return `COALESCE(main.${col}::TEXT, '')`; } }) - .join(`, '${separator}', `); + .join(` || '${separator}' || `); - return `CONCAT(${concatParts}) AS ${config.aliasColumn}`; + return `(${concatParts}) AS ${config.aliasColumn}`; } }) .join(", "); @@ -336,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 69175941..5cb2853d 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -2065,21 +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) - displayColumns: [additionalColumn.sourceColumn], // 표시할 컬럼들 (email) - displayColumn: additionalColumn.sourceColumn, // 하위 호환성 - 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}` ); } } diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 64a2eab6..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 @@ -254,11 +263,60 @@ 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 = {}; @@ -272,6 +330,8 @@ export const TableListComponent: React.FC = ({ } }); + console.log("🔗 Entity 조인 컬럼:", entityJoinColumns); + console.log("🔗 조인 탭 컬럼:", joinTabColumns); console.log("🔗 추가 Entity 조인 컬럼:", additionalJoinColumns); console.log("🎯 화면별 엔티티 설정:", screenEntityConfigs); @@ -346,6 +406,10 @@ export const TableListComponent: React.FC = ({ }); 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); @@ -383,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) => { @@ -412,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, @@ -425,6 +565,11 @@ export const TableListComponent: React.FC = ({ order: index, })); + console.log( + "🎯 자동 생성된 컬럼들:", + autoColumns.map((c) => c.columnName), + ); + // 컴포넌트 설정 업데이트 (부모 컴포넌트에 알림) if (onFormDataChange) { onFormDataChange({ @@ -440,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); @@ -628,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) => { // 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김 @@ -642,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 []; } // 체크박스가 활성화되고 실제 데이터 컬럼이 있는 경우에만 체크박스 컬럼을 추가 @@ -677,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에서 사용하지 않으므로 제거 // 기존 테이블에서만 필요한 경우 다시 추가 가능 @@ -1050,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 6a9007d5..a9b7f042 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -244,12 +244,12 @@ export const TableListConfigPanel: React.FC = ({ handleChange("columns", [...(config.columns || []), newColumn]); }; - // 🎯 엔티티 컬럼 추가 (컬럼 설정 패널에서 표시 컬럼 선택) + // 🎯 조인 컬럼 추가 (조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리) 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, @@ -259,23 +259,11 @@ export const TableListConfigPanel: React.FC = ({ align: "left", format: "text", order: config.columns?.length || 0, - isEntityJoin: true, - entityJoinInfo: { - sourceTable: config.selectedTable || "", - sourceColumn: joinColumn.columnName, - joinAlias: joinColumn.joinAlias, - }, - // 🎯 엔티티 표시 설정 (기본값으로 초기화, 컬럼 설정에서 수정 가능) - entityDisplayConfig: { - displayColumns: [], // 빈 배열로 초기화 - separator: " - ", - sourceTable: config.selectedTable || "", - joinTable: joinColumn.tableName, - }, + isEntityJoin: false, // 조인 탭에서 추가하는 컬럼은 엔티티 타입이 아님 }; handleChange("columns", [...(config.columns || []), newColumn]); - console.log("🔗 엔티티 컬럼 추가됨 (표시 컬럼은 컬럼 설정에서 선택):", newColumn); + console.log("🔗 조인 컬럼 추가됨 (일반 컬럼으로 처리):", newColumn); }; // 컬럼 제거 @@ -577,14 +565,6 @@ export const TableListConfigPanel: React.FC = ({ updatedColumn: updatedColumns.find((col) => col.columnName === columnName), }); } - - // 컬럼 설정 업데이트 - updateColumn(columnName, { - entityDisplayConfig: { - ...config.entityDisplayConfig, - displayColumns: newSelectedColumns, - }, - }); }; // 🎯 엔티티 표시 구분자 업데이트 From 86dc9619682f5d859e3ab20d0503d4ebe60b4059 Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 24 Sep 2025 12:56:22 +0900 Subject: [PATCH 10/14] =?UTF-8?q?=EC=A1=B0=EC=9D=B8=EC=BB=AC=EB=9F=BC=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/(main)/admin/tableMng/page.tsx | 1 - .../table-list/SingleTableWithSticky.tsx | 6 +- .../table-list/TableListComponent.tsx | 157 +++++++++++++++--- 3 files changed, 138 insertions(+), 26 deletions(-) diff --git a/frontend/app/(main)/admin/tableMng/page.tsx b/frontend/app/(main)/admin/tableMng/page.tsx index 9a83277a..3c95f4df 100644 --- a/frontend/app/(main)/admin/tableMng/page.tsx +++ b/frontend/app/(main)/admin/tableMng/page.tsx @@ -866,7 +866,6 @@ export default function TableManagementPage() {
)} - {/* 설정 완료 표시 - 간소화 */} diff --git a/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx b/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx index 51cfdb6b..eeedfaaa 100644 --- a/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx +++ b/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx @@ -64,7 +64,7 @@ export const SingleTableWithSticky: React.FC = ({ return ( = ({ ) : ( data.map((row, index) => ( = ({ return ( = ({ const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(0); const [totalItems, setTotalItems] = useState(0); - const [searchTerm, setSearchTerm] = useState(""); + const [searchTerm] = useState(""); const [sortColumn, setSortColumn] = useState(null); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); const [columnLabels, setColumnLabels] = useState>({}); @@ -311,7 +311,7 @@ export const TableListComponent: React.FC = ({ console.log(`🔍 조인 탭 컬럼 처리: ${col.columnName} -> ${sourceTable}.${sourceColumn}`); return { - sourceTable: sourceTable, + sourceTable: sourceTable || tableConfig.selectedTable || "unknown", sourceColumn: sourceColumn, joinAlias: col.columnName, }; @@ -427,7 +427,7 @@ export const TableListComponent: React.FC = ({ // 🎯 코드 컬럼들의 캐시 미리 로드 (전역 캐시 사용) const codeColumns = Object.entries(columnMeta).filter( - ([_, meta]) => meta.webType === "code" && meta.codeCategory, + ([, meta]) => meta.webType === "code" && meta.codeCategory, ); if (codeColumns.length > 0) { @@ -462,16 +462,81 @@ export const TableListComponent: React.FC = ({ 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 - }; + // 🎯 조인 컬럼 매핑 테이블 동적 생성 (사용자 설정 → API 응답) + const newJoinColumnMapping: Record = {}; + + // 사용자가 설정한 컬럼들과 실제 API 응답 컬럼들을 동적으로 매핑 + processedColumns.forEach((userColumn) => { + // 체크박스는 제외 + if (userColumn.columnName === "__checkbox__") return; + + console.log(`🔍 컬럼 매핑 분석: "${userColumn.columnName}"`, { + displayName: userColumn.displayName, + isEntityJoin: userColumn.isEntityJoin, + entityJoinInfo: userColumn.entityJoinInfo, + available: actualApiColumns, + }); + + // 사용자 설정 컬럼명이 API 응답에 정확히 있는지 확인 + if (actualApiColumns.includes(userColumn.columnName)) { + // 직접 매칭되는 경우 + newJoinColumnMapping[userColumn.columnName] = userColumn.columnName; + console.log(`✅ 정확 매핑: ${userColumn.columnName} → ${userColumn.columnName}`); + } else { + // Entity 조인된 컬럼이거나 조인 탭에서 추가한 컬럼인 경우 + let foundMatch = false; + + // 1. Entity 조인 정보가 있는 경우 aliasColumn 우선 확인 + if (userColumn.entityJoinInfo?.joinAlias) { + const aliasColumn = userColumn.entityJoinInfo.joinAlias; + if (actualApiColumns.includes(aliasColumn)) { + newJoinColumnMapping[userColumn.columnName] = aliasColumn; + console.log(`🔗 Entity 별칭 매핑: ${userColumn.columnName} → ${aliasColumn}`); + foundMatch = true; + } + } + + // 2. 정확한 이름 매칭 (예: dept_code_company_name) + if (!foundMatch) { + const exactMatches = actualApiColumns.filter((apiCol) => apiCol === userColumn.columnName); + + if (exactMatches.length > 0) { + newJoinColumnMapping[userColumn.columnName] = exactMatches[0]; + console.log(`🎯 정확 이름 매핑: ${userColumn.columnName} → ${exactMatches[0]}`); + foundMatch = true; + } + } + + // 3. 부분 문자열 매칭 (컬럼명에 일부가 포함된 경우) + if (!foundMatch) { + const partialMatches = actualApiColumns.filter( + (apiCol) => apiCol.includes(userColumn.columnName) || userColumn.columnName.includes(apiCol), + ); + + if (partialMatches.length > 0) { + // 가장 길이가 비슷한 것 선택 + const bestMatch = partialMatches.reduce((best, current) => + Math.abs(current.length - userColumn.columnName.length) < + Math.abs(best.length - userColumn.columnName.length) + ? current + : best, + ); + + newJoinColumnMapping[userColumn.columnName] = bestMatch; + console.log(`🔍 부분 매핑: ${userColumn.columnName} → ${bestMatch}`); + foundMatch = true; + } + } + + // 4. 매칭 실패한 경우 원본 유지 (하지만 경고 표시) + if (!foundMatch) { + newJoinColumnMapping[userColumn.columnName] = userColumn.columnName; + console.warn( + `⚠️ 매핑 실패: "${userColumn.columnName}" - 사용 가능한 컬럼: [${actualApiColumns.join(", ")}]`, + ); + } + } + }); // 🎯 조인 컬럼 매핑 상태 업데이트 setJoinColumnMapping(newJoinColumnMapping); @@ -533,11 +598,23 @@ export const TableListComponent: React.FC = ({ if (result.entityJoinInfo?.joinConfigs) { result.entityJoinInfo.joinConfigs.forEach((joinConfig) => { // 원본 컬럼을 조인된 컬럼으로 교체 - const originalColumnIndex = processedColumns.findIndex((col) => col.columnName === joinConfig.sourceColumn); + let originalColumnIndex = processedColumns.findIndex((col) => col.columnName === joinConfig.sourceColumn); if (originalColumnIndex !== -1) { console.log(`🔄 컬럼 교체: ${joinConfig.sourceColumn} → ${joinConfig.aliasColumn}`); const originalColumn = processedColumns[originalColumnIndex]; + + // 🚨 중복 방지: 이미 같은 aliasColumn이 있는지 확인 + const existingAliasIndex = processedColumns.findIndex((col) => col.columnName === joinConfig.aliasColumn); + if (existingAliasIndex !== -1 && existingAliasIndex !== originalColumnIndex) { + console.warn(`🚨 중복 컬럼 발견: ${joinConfig.aliasColumn}이 이미 존재합니다. 중복 제거합니다.`); + processedColumns.splice(existingAliasIndex, 1); + // 인덱스 재조정 + if (existingAliasIndex < originalColumnIndex) { + originalColumnIndex--; + } + } + processedColumns[originalColumnIndex] = { ...originalColumn, columnName: joinConfig.aliasColumn, // dept_code → dept_code_name @@ -583,9 +660,26 @@ export const TableListComponent: React.FC = ({ processedColumns = autoColumns; } + // 🚨 processedColumns에서 중복 제거 + const uniqueProcessedColumns = processedColumns.filter( + (column, index, self) => self.findIndex((c) => c.columnName === column.columnName) === index, + ); + + if (uniqueProcessedColumns.length !== processedColumns.length) { + console.error("🚨 processedColumns에서 중복 발견:"); + console.error( + "원본:", + processedColumns.map((c) => c.columnName), + ); + console.error( + "중복 제거 후:", + uniqueProcessedColumns.map((c) => c.columnName), + ); + } + // 🎯 표시할 컬럼 상태 업데이트 - setDisplayColumns(processedColumns); - console.log("🎯 displayColumns 업데이트됨:", processedColumns); + setDisplayColumns(uniqueProcessedColumns); + console.log("🎯 displayColumns 업데이트됨:", uniqueProcessedColumns); console.log("🎯 데이터 개수:", result.data?.length || 0); console.log("🎯 전체 데이터:", result.data); } @@ -836,6 +930,25 @@ export const TableListComponent: React.FC = ({ "🎯 visibleColumns 컬럼명들:", columns.map((c) => c.columnName), ); + + // 🚨 중복 키 검사 + const columnNames = columns.map((c) => c.columnName); + const duplicates = columnNames.filter((name, index) => columnNames.indexOf(name) !== index); + if (duplicates.length > 0) { + console.error("🚨 중복된 컬럼명 발견:", duplicates); + console.error("🚨 전체 컬럼명 목록:", columnNames); + + // 중복 제거 + const uniqueColumns = columns.filter( + (column, index, self) => self.findIndex((c) => c.columnName === column.columnName) === index, + ); + console.log( + "🔧 중복 제거 후 컬럼들:", + uniqueColumns.map((c) => c.columnName), + ); + return uniqueColumns; + } + return columns; }, [displayColumns, tableConfig.columns, tableConfig.checkbox, isDesignMode]); @@ -1084,7 +1197,7 @@ export const TableListComponent: React.FC = ({ onClearFilters={handleClearAdvancedFilters} tableColumns={visibleColumns.map((col) => ({ columnName: col.columnName, - webType: columnMeta[col.columnName]?.webType || "text", + webType: (columnMeta[col.columnName]?.webType as any) || "text", displayName: columnLabels[col.columnName] || col.displayName || col.columnName, codeCategory: columnMeta[col.columnName]?.codeCategory, isVisible: col.visible, @@ -1138,9 +1251,9 @@ export const TableListComponent: React.FC = ({ - {visibleColumns.map((column) => ( + {visibleColumns.map((column, colIndex) => ( = ({ ) : ( data.map((row, index) => ( = ({ style={{ minHeight: "40px", height: "40px", lineHeight: "1" }} onClick={() => handleRowClick(row)} > - {visibleColumns.map((column) => ( + {visibleColumns.map((column, colIndex) => ( From 649ed5c6d71cfbce67c23364c52745a28df275af Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 24 Sep 2025 14:31:46 +0900 Subject: [PATCH 11/14] =?UTF-8?q?=EC=A1=B0=EC=9D=B8=EC=BB=AC=EB=9F=BC?= =?UTF-8?q?=EC=88=98=EC=A0=95(=EC=A1=B0=EC=9D=B8=20=EC=BB=AC=EB=9F=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=EC=8B=9C=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=ED=91=9C=EC=8B=9C=20=EC=98=A4=EB=A5=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/entityJoinService.ts | 110 +++++++++++--- .../src/services/tableManagementService.ts | 25 +++- .../table-list/SingleTableWithSticky.tsx | 22 ++- .../table-list/TableListComponent.tsx | 137 +++++++++++++----- 4 files changed, 238 insertions(+), 56 deletions(-) diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index d0b01846..56633952 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -42,9 +42,23 @@ export class EntityJoinService { }, }); + logger.info(`🔍 Entity 컬럼 조회 결과: ${entityColumns.length}개 발견`); + entityColumns.forEach((col, index) => { + logger.info( + ` ${index + 1}. ${col.column_name} -> ${col.reference_table}.${col.reference_column} (display: ${col.display_column})` + ); + }); + const joinConfigs: EntityJoinConfig[] = []; for (const column of entityColumns) { + logger.info(`🔍 Entity 컬럼 상세 정보:`, { + column_name: column.column_name, + reference_table: column.reference_table, + reference_column: column.reference_column, + display_column: column.display_column, + }); + if ( !column.column_name || !column.reference_table || @@ -58,6 +72,12 @@ export class EntityJoinService { let displayColumns: string[] = []; let separator = " - "; + logger.info(`🔍 조건 확인 - 컬럼: ${column.column_name}`, { + hasScreenConfig: !!screenConfig, + hasDisplayColumns: screenConfig?.displayColumns, + displayColumn: column.display_column, + }); + if (screenConfig && screenConfig.displayColumns) { // 화면에서 설정된 표시 컬럼들 사용 (기본 테이블 + 조인 테이블 조합 지원) displayColumns = screenConfig.displayColumns; @@ -70,9 +90,12 @@ export class EntityJoinService { } else if (column.display_column && column.display_column !== "none") { // 기존 설정된 단일 표시 컬럼 사용 (none이 아닌 경우만) displayColumns = [column.display_column]; + logger.info( + `🔧 기존 display_column 사용: ${column.column_name} → ${column.display_column}` + ); } else { - // 조인 탭에서 보여줄 기본 표시 컬럼 설정 - // dept_info 테이블의 경우 dept_name을 기본으로 사용 + // display_column이 "none"이거나 없는 경우 기본 표시 컬럼 설정 + // 🚨 display_column이 항상 "none"이므로 이 로직을 기본으로 사용 let defaultDisplayColumn = column.reference_column; if (column.reference_table === "dept_info") { defaultDisplayColumn = "dept_name"; @@ -83,9 +106,10 @@ export class EntityJoinService { } displayColumns = [defaultDisplayColumn]; - console.log( - `🔧 조인 탭용 기본 표시 컬럼 설정: ${column.column_name} → ${defaultDisplayColumn} (${column.reference_table})` + logger.info( + `🔧 Entity 조인 기본 표시 컬럼 설정: ${column.column_name} → ${defaultDisplayColumn} (${column.reference_table})` ); + logger.info(`🔍 생성된 displayColumns 배열:`, displayColumns); } // 별칭 컬럼명 생성 (writer -> writer_name) @@ -102,13 +126,32 @@ export class EntityJoinService { separator: separator, }; + logger.info(`🔧 기본 조인 설정 생성:`, { + sourceTable: joinConfig.sourceTable, + sourceColumn: joinConfig.sourceColumn, + referenceTable: joinConfig.referenceTable, + aliasColumn: joinConfig.aliasColumn, + displayColumns: joinConfig.displayColumns, + }); + // 조인 설정 유효성 검증 + logger.info( + `🔍 조인 설정 검증 중: ${joinConfig.sourceColumn} -> ${joinConfig.referenceTable}` + ); if (await this.validateJoinConfig(joinConfig)) { joinConfigs.push(joinConfig); + logger.info(`✅ 조인 설정 추가됨: ${joinConfig.aliasColumn}`); + } else { + logger.warn(`❌ 조인 설정 검증 실패: ${joinConfig.sourceColumn}`); } } - logger.info(`Entity 조인 설정 생성 완료: ${joinConfigs.length}개`); + logger.info(`🎯 Entity 조인 설정 생성 완료: ${joinConfigs.length}개`); + joinConfigs.forEach((config, index) => { + logger.info( + ` ${index + 1}. ${config.sourceColumn} -> ${config.referenceTable}.${config.referenceColumn} AS ${config.aliasColumn}` + ); + }); return joinConfigs; } catch (error) { logger.error(`Entity 조인 감지 실패: ${tableName}`, error); @@ -273,7 +316,7 @@ export class EntityJoinService { .filter(Boolean) .join("\n"); - logger.debug(`생성된 Entity 조인 쿼리:`, query); + logger.info(`🔍 생성된 Entity 조인 쿼리:`, query); return { query: query, aliasMap: aliasMap, @@ -303,10 +346,18 @@ export class EntityJoinService { } // 참조 테이블의 캐시 가능성 확인 + const displayCol = + config.displayColumn || + config.displayColumns?.[0] || + config.referenceColumn; + logger.info( + `🔍 캐시 확인용 표시 컬럼: ${config.referenceTable} - ${displayCol}` + ); + const cachedData = await referenceCacheService.getCachedReference( config.referenceTable, config.referenceColumn, - config.displayColumn || config.displayColumns[0] + displayCol ); return cachedData ? "cache" : "join"; @@ -336,6 +387,14 @@ export class EntityJoinService { */ private async validateJoinConfig(config: EntityJoinConfig): Promise { try { + logger.info("🔍 조인 설정 검증 상세:", { + sourceColumn: config.sourceColumn, + referenceTable: config.referenceTable, + displayColumns: config.displayColumns, + displayColumn: config.displayColumn, + aliasColumn: config.aliasColumn, + }); + // 참조 테이블 존재 확인 const tableExists = await prisma.$queryRaw` SELECT 1 FROM information_schema.tables @@ -350,23 +409,32 @@ export class EntityJoinService { // 참조 컬럼 존재 확인 (displayColumns[0] 사용) const displayColumn = config.displayColumns?.[0] || config.displayColumn; - if (!displayColumn) { - logger.warn(`표시 컬럼이 설정되지 않음: ${config.sourceColumn}`); - return false; - } + logger.info( + `🔍 표시 컬럼 확인: ${displayColumn} (from displayColumns: ${config.displayColumns}, displayColumn: ${config.displayColumn})` + ); - const columnExists = await prisma.$queryRaw` - SELECT 1 FROM information_schema.columns - WHERE table_name = ${config.referenceTable} - AND column_name = ${displayColumn} - LIMIT 1 - `; + // 🚨 display_column이 항상 "none"이므로, 표시 컬럼이 없어도 조인 허용 + if (displayColumn && displayColumn !== "none") { + const columnExists = await prisma.$queryRaw` + SELECT 1 FROM information_schema.columns + WHERE table_name = ${config.referenceTable} + AND column_name = ${displayColumn} + LIMIT 1 + `; - if (!Array.isArray(columnExists) || columnExists.length === 0) { - logger.warn( - `표시 컬럼이 존재하지 않음: ${config.referenceTable}.${displayColumn}` + if (!Array.isArray(columnExists) || columnExists.length === 0) { + logger.warn( + `표시 컬럼이 존재하지 않음: ${config.referenceTable}.${displayColumn}` + ); + return false; + } + logger.info( + `✅ 표시 컬럼 확인 완료: ${config.referenceTable}.${displayColumn}` + ); + } else { + logger.info( + `🔧 표시 컬럼 검증 생략: display_column이 none이거나 설정되지 않음` ); - return false; } return true; diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 5cb2853d..1278c1c3 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -2049,6 +2049,17 @@ export class TableManagementService { options.screenEntityConfigs ); + logger.info( + `🔍 detectEntityJoins 결과: ${joinConfigs.length}개 조인 설정` + ); + if (joinConfigs.length > 0) { + joinConfigs.forEach((config, index) => { + logger.info( + ` 조인 ${index + 1}: ${config.sourceColumn} -> ${config.referenceTable} AS ${config.aliasColumn}` + ); + }); + } + // 추가 조인 컬럼 정보가 있으면 조인 설정에 추가 if ( options.additionalJoinColumns && @@ -2057,6 +2068,10 @@ export class TableManagementService { logger.info( `추가 조인 컬럼 처리: ${options.additionalJoinColumns.length}개` ); + logger.info( + "📋 전달받은 additionalJoinColumns:", + options.additionalJoinColumns + ); for (const additionalColumn of options.additionalJoinColumns) { // 기존 조인 설정에서 같은 참조 테이블을 사용하는 설정 찾기 @@ -2251,10 +2266,18 @@ export class TableManagementService { try { // 캐시 데이터 미리 로드 for (const config of joinConfigs) { + const displayCol = + config.displayColumn || + config.displayColumns?.[0] || + config.referenceColumn; + logger.info( + `🔍 캐시 로드 - ${config.referenceTable}: keyCol=${config.referenceColumn}, displayCol=${displayCol}` + ); + await referenceCacheService.getCachedReference( config.referenceTable, config.referenceColumn, - config.displayColumn || config.displayColumns[0] + displayCol ); } diff --git a/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx b/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx index eeedfaaa..5687bdba 100644 --- a/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx +++ b/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx @@ -22,6 +22,7 @@ interface SingleTableWithStickyProps { renderCheckboxCell: (row: any, index: number) => React.ReactNode; formatCellValue: (value: any, format?: string, columnName?: string) => string; getColumnWidth: (column: ColumnConfig) => number; + joinColumnMapping: Record; // 조인 컬럼 매핑 추가 } export const SingleTableWithSticky: React.FC = ({ @@ -39,6 +40,7 @@ export const SingleTableWithSticky: React.FC = ({ renderCheckboxCell, formatCellValue, getColumnWidth, + joinColumnMapping, }) => { const checkboxConfig = tableConfig.checkbox || {}; @@ -174,7 +176,25 @@ export const SingleTableWithSticky: React.FC = ({ > {column.columnName === "__checkbox__" ? renderCheckboxCell(row, index) - : formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"} + : (() => { + // 🎯 매핑된 컬럼명으로 데이터 찾기 (기본 테이블과 동일한 로직) + const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; + + // 조인 컬럼 매핑 정보 로깅 + if (column.columnName !== mappedColumnName && index === 0) { + console.log(`🔗 Sticky 조인 컬럼 매핑: ${column.columnName} → ${mappedColumnName}`); + } + + const cellValue = row[mappedColumnName]; + if (index === 0) { + // 첫 번째 행만 로그 출력 (디버깅용) + console.log( + `🔍 Sticky 셀 데이터 [${column.columnName} → ${mappedColumnName}]:`, + cellValue, + ); + } + return formatCellValue(cellValue, column.format, column.columnName) || "\u00A0"; + })()} ); })} diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 2c5b1dd1..10416855 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -290,28 +290,39 @@ export const TableListComponent: React.FC = ({ // 🎯 조인 탭에서 추가한 컬럼들도 추가 (실제로 존재하는 컬럼만) ...joinTabColumns .filter((col) => { - // 실제 API 응답에 존재하는 컬럼만 필터링 - const validJoinColumns = ["dept_code_name", "dept_name"]; - const isValid = validJoinColumns.includes(col.columnName); - if (!isValid) { - console.log(`🔍 조인 탭 컬럼 제외: ${col.columnName} (유효하지 않음)`); + // 조인 컬럼인지 확인 (언더스코어가 포함된 컬럼) + const isJoinColumn = col.columnName.includes("_") && col.columnName !== "__checkbox__"; + if (!isJoinColumn) { + console.log(`🔍 조인 탭 컬럼 제외: ${col.columnName} (조인 컬럼이 아님)`); } - return isValid; + return isJoinColumn; }) .map((col) => { - // 실제 존재하는 조인 컬럼만 처리 - let sourceTable = tableConfig.selectedTable; - let sourceColumn = col.columnName; + // 동적으로 조인 컬럼 정보 추출 + console.log(`🔍 조인 컬럼 분석: ${col.columnName}`); - if (col.columnName === "dept_code_name" || col.columnName === "dept_name") { - sourceTable = "dept_info"; + // 컬럼명에서 기본 컬럼과 참조 테이블 추출 + // 예: dept_code_company_name -> dept_code (기본), company_name (참조) + const parts = col.columnName.split("_"); + let sourceColumn = ""; + let referenceTable = ""; + + // dept_code로 시작하는 경우 + if (col.columnName.startsWith("dept_code_")) { sourceColumn = "dept_code"; + referenceTable = "dept_info"; + } + // 다른 패턴들도 추가 가능 + else { + // 기본적으로 첫 번째 부분을 소스 컬럼으로 사용 + sourceColumn = parts[0]; + referenceTable = tableConfig.selectedTable || "unknown"; } - console.log(`🔍 조인 탭 컬럼 처리: ${col.columnName} -> ${sourceTable}.${sourceColumn}`); + console.log(`🔗 조인 설정: ${col.columnName} -> ${sourceColumn} (${referenceTable})`); return { - sourceTable: sourceTable || tableConfig.selectedTable || "unknown", + sourceTable: referenceTable, sourceColumn: sourceColumn, joinAlias: col.columnName, }; @@ -410,6 +421,14 @@ export const TableListComponent: React.FC = ({ console.log("🎯 데이터 개수:", result.data?.length || 0); console.log("🎯 전체 페이지:", result.totalPages); console.log("🎯 총 아이템:", result.total); + + // 🚨 데이터 샘플 확인 (첫 번째 행의 모든 컬럼과 값) + if (result.data && result.data.length > 0) { + console.log("🔍 첫 번째 행 데이터 샘플:", result.data[0]); + Object.entries(result.data[0]).forEach(([key, value]) => { + console.log(` 📊 ${key}: "${value}" (타입: ${typeof value})`); + }); + } setData(result.data || []); setTotalPages(result.totalPages || 1); setTotalItems(result.total || 0); @@ -507,24 +526,62 @@ export const TableListComponent: React.FC = ({ } } - // 3. 부분 문자열 매칭 (컬럼명에 일부가 포함된 경우) + // 3. 조인 컬럼 검증 및 처리 if (!foundMatch) { - const partialMatches = actualApiColumns.filter( - (apiCol) => apiCol.includes(userColumn.columnName) || userColumn.columnName.includes(apiCol), - ); + // 🚨 조인 컬럼인지 확인 (더 정확한 감지 로직) + const hasUnderscore = userColumn.columnName.includes("_"); + let isJoinColumn = false; + let baseColumnName = ""; - if (partialMatches.length > 0) { - // 가장 길이가 비슷한 것 선택 - const bestMatch = partialMatches.reduce((best, current) => - Math.abs(current.length - userColumn.columnName.length) < - Math.abs(best.length - userColumn.columnName.length) - ? current - : best, + if (hasUnderscore) { + // 가능한 모든 기본 컬럼명을 확인 (dept_code_company_name -> dept_code, dept 순으로) + const parts = userColumn.columnName.split("_"); + for (let i = parts.length - 1; i >= 1; i--) { + const possibleBase = parts.slice(0, i).join("_"); + if (actualApiColumns.includes(possibleBase)) { + baseColumnName = possibleBase; + isJoinColumn = true; + break; + } + } + } + + console.log(`🔍 조인 컬럼 검사: "${userColumn.columnName}"`, { + hasUnderscore, + baseColumnName, + isJoinColumn, + }); + + if (isJoinColumn) { + console.log(`🔍 조인 컬럼 기본 컬럼 확인: "${baseColumnName}"`, { + existsInApi: actualApiColumns.includes(baseColumnName), + actualApiColumns: actualApiColumns.slice(0, 10), // 처음 10개만 표시 + }); + + console.warn( + `⚠️ 조인 실패: "${userColumn.columnName}" - 백엔드에서 Entity 조인이 실행되지 않음. 기본 컬럼값 표시합니다.`, + ); + // 조인 실패 시 기본 컬럼값을 표시하도록 매핑 + newJoinColumnMapping[userColumn.columnName] = baseColumnName; + foundMatch = true; + } else { + // 일반 컬럼인 경우 부분 매칭 시도 + const partialMatches = actualApiColumns.filter( + (apiCol) => apiCol.includes(userColumn.columnName) || userColumn.columnName.includes(apiCol), ); - newJoinColumnMapping[userColumn.columnName] = bestMatch; - console.log(`🔍 부분 매핑: ${userColumn.columnName} → ${bestMatch}`); - foundMatch = true; + if (partialMatches.length > 0) { + const bestMatch = partialMatches.reduce((best, current) => + Math.abs(current.length - userColumn.columnName.length) < + Math.abs(best.length - userColumn.columnName.length) + ? current + : best, + ); + + newJoinColumnMapping[userColumn.columnName] = bestMatch; + console.log(`🔍 부분 매핑: ${userColumn.columnName} → ${bestMatch}`); + foundMatch = true; + } } } @@ -1245,6 +1302,7 @@ export const TableListComponent: React.FC = ({ renderCheckboxCell={renderCheckboxCell} formatCellValue={formatCellValue} getColumnWidth={getColumnWidth} + joinColumnMapping={joinColumnMapping} /> ) : ( // 기존 테이블 (가로 스크롤이 필요 없는 경우) @@ -1325,15 +1383,28 @@ export const TableListComponent: React.FC = ({ : (() => { // 🎯 매핑된 컬럼명으로 데이터 찾기 const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; + + // 조인 컬럼 매핑 정보 로깅 + if (column.columnName !== mappedColumnName && index === 0) { + console.log(`🔗 조인 컬럼 매핑: ${column.columnName} → ${mappedColumnName}`); + } + const cellValue = row[mappedColumnName]; if (index === 0) { // 첫 번째 행만 로그 출력 - console.log( - `🔍 셀 데이터 [${column.columnName} → ${mappedColumnName}]:`, - cellValue, - "전체 row:", - row, - ); + console.log(`🔍 셀 데이터 [${column.columnName} → ${mappedColumnName}]:`, cellValue); + + // 🚨 조인된 컬럼인 경우 추가 디버깅 + if (column.columnName !== mappedColumnName) { + console.log(" 🔗 조인 컬럼 분석:"); + console.log(` 👤 사용자 설정 컬럼: "${column.columnName}"`); + console.log(` 📡 매핑된 API 컬럼: "${mappedColumnName}"`); + console.log(` 📋 컬럼 라벨: "${column.displayName}"`); + console.log(` 💾 실제 데이터: "${cellValue}"`); + console.log( + ` 🔄 원본 컬럼 데이터 (${column.columnName}): "${row[column.columnName]}"`, + ); + } } return formatCellValue(cellValue, column.format, column.columnName) || "\u00A0"; })()} From 0d9ee4c40f9692381115a952b88a005b774c65bc Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 24 Sep 2025 15:02:54 +0900 Subject: [PATCH 12/14] =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=BC=20=ED=91=9C=EC=8B=9C=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/entityJoinService.ts | 6 ++ .../src/services/tableManagementService.ts | 73 ++++++++++++++++--- .../table-list/TableListComponent.tsx | 3 +- 3 files changed, 71 insertions(+), 11 deletions(-) diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index 56633952..b88c0c8b 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -233,6 +233,9 @@ export class EntityJoinService { "master_sabun", "location", "data_type", + "company_name", + "sales_yn", + "status", ].includes(col); if (isJoinTableColumn) { @@ -256,6 +259,9 @@ export class EntityJoinService { "master_sabun", "location", "data_type", + "company_name", + "sales_yn", + "status", ].includes(col); if (isJoinTableColumn) { diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 1278c1c3..4ca5369d 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -2074,38 +2074,78 @@ export class TableManagementService { ); for (const additionalColumn of options.additionalJoinColumns) { - // 기존 조인 설정에서 같은 참조 테이블을 사용하는 설정 찾기 + // 🔍 sourceColumn을 기준으로 기존 조인 설정 찾기 (dept_code로 찾기) const baseJoinConfig = joinConfigs.find( - (config) => config.referenceTable === additionalColumn.sourceTable + (config) => config.sourceColumn === additionalColumn.sourceColumn ); 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 joinAlias = additionalColumn.joinAlias; // dept_code_company_name + const actualColumnName = joinAlias.replace(`${sourceColumn}_`, ""); // company_name + + logger.info(`🔍 조인 컬럼 상세 분석:`, { + sourceColumn, + joinAlias, + actualColumnName, + referenceTable: additionalColumn.sourceTable, + }); + + // 🚨 기본 Entity 조인과 중복되지 않도록 체크 + const isBasicEntityJoin = + additionalColumn.joinAlias === + `${baseJoinConfig.sourceColumn}_name`; + + if (isBasicEntityJoin) { + logger.info( + `⚠️ 기본 Entity 조인과 중복: ${additionalColumn.joinAlias} - 건너뜀` + ); + continue; // 기본 Entity 조인과 중복되면 추가하지 않음 + } // 추가 조인 컬럼 설정 생성 const additionalJoinConfig: EntityJoinConfig = { sourceTable: tableName, sourceColumn: baseJoinConfig.sourceColumn, // 원본 컬럼 (dept_code) - referenceTable: additionalColumn.sourceTable, // 참조 테이블 (dept_info) + referenceTable: + (additionalColumn as any).referenceTable || + baseJoinConfig.referenceTable, // 참조 테이블 (dept_info) referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (dept_code) - displayColumns: [actualColumnName], // 표시할 컬럼들 (location_name) + displayColumns: [actualColumnName], // 표시할 컬럼들 (company_name) displayColumn: actualColumnName, // 하위 호환성 - aliasColumn: additionalColumn.joinAlias, // 별칭 (dept_code_location_name) + aliasColumn: additionalColumn.joinAlias, // 별칭 (dept_code_company_name) separator: " - ", // 기본 구분자 }; joinConfigs.push(additionalJoinConfig); logger.info( - `추가 조인 컬럼 설정 추가: ${additionalJoinConfig.aliasColumn} -> ${actualColumnName}` + `✅ 추가 조인 컬럼 설정 추가: ${additionalJoinConfig.aliasColumn} -> ${actualColumnName}` ); + logger.info(`🔍 추가된 조인 설정 상세:`, { + sourceTable: additionalJoinConfig.sourceTable, + sourceColumn: additionalJoinConfig.sourceColumn, + referenceTable: additionalJoinConfig.referenceTable, + displayColumns: additionalJoinConfig.displayColumns, + aliasColumn: additionalJoinConfig.aliasColumn, + }); } } } + // 최종 조인 설정 배열 로깅 + logger.info(`🎯 최종 joinConfigs 배열 (${joinConfigs.length}개):`); + joinConfigs.forEach((config, index) => { + logger.info( + ` ${index + 1}. ${config.sourceColumn} -> ${config.referenceTable} AS ${config.aliasColumn}`, + { + displayColumns: config.displayColumns, + displayColumn: config.displayColumn, + } + ); + }); + if (joinConfigs.length === 0) { logger.info(`Entity 조인 설정이 없음: ${tableName}`); const basicResult = await this.getTableData(tableName, options); @@ -2119,8 +2159,21 @@ export class TableManagementService { } // 조인 전략 결정 (테이블 크기 기반) - const strategy = - await entityJoinService.determineJoinStrategy(joinConfigs); + // 🚨 additionalJoinColumns가 있는 경우 강제로 full_join 사용 (캐시 안정성 보장) + let strategy: "full_join" | "cache_lookup" | "hybrid"; + + if ( + options.additionalJoinColumns && + options.additionalJoinColumns.length > 0 + ) { + strategy = "full_join"; + console.log( + `🔧 additionalJoinColumns 감지: 강제로 full_join 전략 사용 (${options.additionalJoinColumns.length}개 추가 조인)` + ); + } else { + strategy = await entityJoinService.determineJoinStrategy(joinConfigs); + } + console.log( `🎯 선택된 조인 전략: ${strategy} (${joinConfigs.length}개 Entity 조인)` ); diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 10416855..6bd974ed 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -322,9 +322,10 @@ export const TableListComponent: React.FC = ({ console.log(`🔗 조인 설정: ${col.columnName} -> ${sourceColumn} (${referenceTable})`); return { - sourceTable: referenceTable, + sourceTable: tableConfig.selectedTable || "unknown", // 기본 테이블 (user_info) sourceColumn: sourceColumn, joinAlias: col.columnName, + referenceTable: referenceTable, // 참조 테이블 정보도 추가 }; }), ]; From 1a60177fe4fcbe6314cf646372a1ca78eb239741 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Wed, 24 Sep 2025 18:07:36 +0900 Subject: [PATCH 13/14] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=ED=86=B5=EC=9D=BC=20=EB=B0=8F=20JSX=20=EA=B5=AC?= =?UTF-8?q?=EB=AC=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - admin/screenMng, dataflow 페이지에 tableMng 레퍼런스 레이아웃 적용 - admin/standards 페이지 JSX 괄호 문제 수정 - 전체 관리자 페이지 UI 일관성 향상 - bg-gray-50 배경, container 구조, 통일된 제목 스타일 적용 --- .../(main)/admin/batch-management/page.tsx | 12 +- .../admin/collection-management/page.tsx | 12 +- frontend/app/(main)/admin/commonCode/page.tsx | 16 +- frontend/app/(main)/admin/company/page.tsx | 15 +- frontend/app/(main)/admin/dataflow/page.tsx | 71 +- .../admin/external-call-configs/page.tsx | 4 +- .../admin/external-connections/page.tsx | 15 +- frontend/app/(main)/admin/i18n/page.tsx | 8 +- frontend/app/(main)/admin/layouts/page.tsx | 15 +- frontend/app/(main)/admin/menu/page.tsx | 13 +- frontend/app/(main)/admin/monitoring/page.tsx | 22 +- frontend/app/(main)/admin/page.tsx | 4 +- frontend/app/(main)/admin/screenMng/page.tsx | 60 +- .../admin/standards/[webType]/edit/page.tsx | 4 +- .../(main)/admin/standards/[webType]/page.tsx | 4 +- .../app/(main)/admin/standards/new/page.tsx | 4 +- frontend/app/(main)/admin/standards/page.tsx | 26 +- frontend/app/(main)/admin/tableMng/page.tsx | 2 +- frontend/app/(main)/admin/templates/page.tsx | 16 +- frontend/app/(main)/admin/userMng/page.tsx | 13 +- .../app/(main)/admin/validation-demo/page.tsx | 12 +- .../app/(main)/screens/[screenId]/page.tsx | 2 +- .../EnhancedInteractiveScreenViewer.tsx | 2 +- .../screen/InteractiveScreenViewer.tsx | 2 +- .../screen/RealtimePreviewDynamic.tsx | 15 +- frontend/components/screen/ScreenDesigner.tsx | 38 +- .../config-panels/ButtonConfigPanel.tsx | 20 +- .../screen/filters/AdvancedSearchFilters.tsx | 6 - .../screen/panels/DetailSettingsPanel.tsx | 7 +- .../screen/panels/PropertiesPanel.tsx | 82 +- .../lib/hooks/useEntityJoinOptimization.ts | 49 +- .../AutoRegisteringComponentRenderer.ts | 2 +- .../lib/registry/DynamicComponentRenderer.tsx | 5 + .../AccordionBasicComponent.tsx | 2 +- .../button-primary/ButtonPrimaryComponent.tsx | 92 +- .../checkbox-basic/CheckboxBasicComponent.tsx | 4 +- .../date-input/DateInputComponent.tsx | 2 +- .../divider-line/DividerLineComponent.tsx | 2 +- .../file-upload/FileUploadComponent.tsx | 2 +- .../image-display/ImageDisplayComponent.tsx | 2 +- .../number-input/NumberInputComponent.tsx | 2 +- .../radio-basic/RadioBasicComponent.tsx | 4 +- .../select-basic/SelectBasicComponent.tsx | 2 +- .../slider-basic/SliderBasicComponent.tsx | 4 +- .../table-list/SingleTableWithSticky.tsx | 59 +- .../table-list/TableListComponent.tsx | 958 ++++++++++-------- .../table-list/TableListConfigPanel.tsx | 24 +- .../table-list/TableListRenderer.tsx | 20 +- .../test-input/TestInputComponent.tsx | 2 +- .../text-display/TextDisplayComponent.tsx | 4 +- .../text-display/TextDisplayConfigPanel.tsx | 2 +- .../registry/components/text-display/index.ts | 2 +- .../text-input/TextInputComponent.tsx | 2 +- .../textarea-basic/TextareaBasicComponent.tsx | 2 +- .../toggle-switch/ToggleSwitchComponent.tsx | 4 +- .../layouts/AutoRegisteringLayoutRenderer.tsx | 2 +- .../registry/layouts/BaseLayoutRenderer.tsx | 33 +- .../layouts/accordion/AccordionLayout.tsx | 2 +- frontend/lib/registry/utils/hotReload.ts | 12 +- .../lib/utils/getComponentConfigPanel.tsx | 13 + frontend/scripts/create-component.js | 6 +- frontend/types/component.ts | 7 +- 62 files changed, 1173 insertions(+), 677 deletions(-) diff --git a/frontend/app/(main)/admin/batch-management/page.tsx b/frontend/app/(main)/admin/batch-management/page.tsx index 9b23cf70..9e48a097 100644 --- a/frontend/app/(main)/admin/batch-management/page.tsx +++ b/frontend/app/(main)/admin/batch-management/page.tsx @@ -185,11 +185,12 @@ export default function BatchManagementPage() { }; return ( -
- {/* 헤더 */} -
-
-

배치 관리

+
+
+ {/* 헤더 */} +
+
+

배치 관리

스케줄된 배치 작업을 관리하고 실행 상태를 모니터링합니다.

@@ -428,6 +429,7 @@ export default function BatchManagementPage() { onSave={handleModalSave} job={selectedJob} /> +
); } diff --git a/frontend/app/(main)/admin/collection-management/page.tsx b/frontend/app/(main)/admin/collection-management/page.tsx index 4edbcaec..8320caac 100644 --- a/frontend/app/(main)/admin/collection-management/page.tsx +++ b/frontend/app/(main)/admin/collection-management/page.tsx @@ -162,11 +162,12 @@ export default function CollectionManagementPage() { }; return ( -
- {/* 헤더 */} -
-
-

수집 관리

+
+
+ {/* 헤더 */} +
+
+

수집 관리

외부 데이터베이스에서 데이터를 수집하는 설정을 관리합니다.

@@ -332,6 +333,7 @@ export default function CollectionManagementPage() { onSave={handleModalSave} config={selectedConfig} /> +
); } diff --git a/frontend/app/(main)/admin/commonCode/page.tsx b/frontend/app/(main)/admin/commonCode/page.tsx index be946e05..3868ec40 100644 --- a/frontend/app/(main)/admin/commonCode/page.tsx +++ b/frontend/app/(main)/admin/commonCode/page.tsx @@ -11,14 +11,15 @@ export default function CommonCodeManagementPage() { const { selectedCategoryCode, selectCategory } = useSelectedCategory(); return ( -
- {/* 페이지 헤더 */} -
-
-

공통코드 관리

-

시스템에서 사용하는 공통코드를 관리합니다

+
+
+ {/* 페이지 제목 */} +
+
+

공통코드 관리

+

시스템에서 사용하는 공통코드를 관리합니다

+
-
{/* 메인 콘텐츠 */} {/* 반응형 레이아웃: PC는 가로, 모바일은 세로 */} @@ -52,6 +53,7 @@ export default function CommonCodeManagementPage() {
+
); } diff --git a/frontend/app/(main)/admin/company/page.tsx b/frontend/app/(main)/admin/company/page.tsx index 79e92516..7e222aa8 100644 --- a/frontend/app/(main)/admin/company/page.tsx +++ b/frontend/app/(main)/admin/company/page.tsx @@ -4,5 +4,18 @@ import { CompanyManagement } from "@/components/admin/CompanyManagement"; * 회사 관리 페이지 */ export default function CompanyPage() { - return ; + return ( +
+
+ {/* 페이지 제목 */} +
+
+

회사 관리

+

시스템에서 사용하는 회사 정보를 관리합니다

+
+
+ +
+
+ ); } diff --git a/frontend/app/(main)/admin/dataflow/page.tsx b/frontend/app/(main)/admin/dataflow/page.tsx index 19914665..cf57b3cb 100644 --- a/frontend/app/(main)/admin/dataflow/page.tsx +++ b/frontend/app/(main)/admin/dataflow/page.tsx @@ -76,48 +76,49 @@ export default function DataFlowPage() { }; return ( -
- {/* 헤더 */} -
+
+
+ {/* 페이지 제목 */}
-
- {currentStep !== "list" && ( - - )} -
-

- {stepConfig[currentStep].icon} - {stepConfig[currentStep].title} -

-

{stepConfig[currentStep].description}

-
+
+

데이터 흐름 관리

+

테이블 간 데이터 관계를 시각적으로 설계하고 관리합니다

+ {currentStep !== "list" && ( + + )}
-
- {/* 단계별 내용 */} -
- {/* 관계도 목록 단계 */} - {currentStep === "list" && ( -
- + {/* 단계별 내용 */} +
+ {/* 관계도 목록 단계 */} + {currentStep === "list" && ( +
+
+

{stepConfig.list.title}

+
+
)} - {/* 관계도 설계 단계 */} - {currentStep === "design" && ( -
- goToStep("list")} - /> -
- )} + {/* 관계도 설계 단계 */} + {currentStep === "design" && ( +
+
+

{stepConfig.design.title}

+
+ goToStep("list")} + /> +
+ )} +
); diff --git a/frontend/app/(main)/admin/external-call-configs/page.tsx b/frontend/app/(main)/admin/external-call-configs/page.tsx index dbdd4aeb..e3755083 100644 --- a/frontend/app/(main)/admin/external-call-configs/page.tsx +++ b/frontend/app/(main)/admin/external-call-configs/page.tsx @@ -161,7 +161,8 @@ export default function ExternalCallConfigsPage() { }; return ( -
+
+
{/* 페이지 헤더 */}
@@ -396,6 +397,7 @@ export default function ExternalCallConfigsPage() { +
); } diff --git a/frontend/app/(main)/admin/external-connections/page.tsx b/frontend/app/(main)/admin/external-connections/page.tsx index 85e7911f..e1ab62d7 100644 --- a/frontend/app/(main)/admin/external-connections/page.tsx +++ b/frontend/app/(main)/admin/external-connections/page.tsx @@ -220,11 +220,15 @@ export default function ExternalConnectionsPage() { }; return ( -
-
-

외부 커넥션 관리

-

외부 데이터베이스 연결 정보를 관리합니다.

-
+
+
+ {/* 페이지 제목 */} +
+
+

외부 커넥션 관리

+

외부 데이터베이스 연결 정보를 관리합니다

+
+
{/* 검색 및 필터 */} @@ -446,6 +450,7 @@ export default function ExternalConnectionsPage() { connectionName={selectedConnection.connection_name} /> )} +
); } diff --git a/frontend/app/(main)/admin/i18n/page.tsx b/frontend/app/(main)/admin/i18n/page.tsx index f1fa7ef4..bb7308e2 100644 --- a/frontend/app/(main)/admin/i18n/page.tsx +++ b/frontend/app/(main)/admin/i18n/page.tsx @@ -3,6 +3,12 @@ import MultiLang from "@/components/admin/MultiLang"; export default function I18nPage() { - return ; + return ( +
+
+ +
+
+ ); } diff --git a/frontend/app/(main)/admin/layouts/page.tsx b/frontend/app/(main)/admin/layouts/page.tsx index c5215057..f22bd3d8 100644 --- a/frontend/app/(main)/admin/layouts/page.tsx +++ b/frontend/app/(main)/admin/layouts/page.tsx @@ -220,12 +220,14 @@ export default function LayoutManagementPage() { }; return ( -
-
-
-

레이아웃 관리

-

화면 레이아웃을 생성하고 관리합니다.

-
+
+
+ {/* 페이지 제목 */} +
+
+

레이아웃 관리

+

화면 레이아웃을 생성하고 관리합니다

+
@@ -411,6 +413,7 @@ export default function LayoutManagementPage() { loadCategoryCounts(); }} /> +
); } diff --git a/frontend/app/(main)/admin/menu/page.tsx b/frontend/app/(main)/admin/menu/page.tsx index 301e0321..fcf0b965 100644 --- a/frontend/app/(main)/admin/menu/page.tsx +++ b/frontend/app/(main)/admin/menu/page.tsx @@ -4,8 +4,17 @@ import { MenuManagement } from "@/components/admin/MenuManagement"; export default function MenuPage() { return ( -
- +
+
+ {/* 페이지 제목 */} +
+
+

메뉴 관리

+

시스템 메뉴를 관리하고 화면을 할당합니다

+
+
+ +
); } diff --git a/frontend/app/(main)/admin/monitoring/page.tsx b/frontend/app/(main)/admin/monitoring/page.tsx index 6161c387..2f028639 100644 --- a/frontend/app/(main)/admin/monitoring/page.tsx +++ b/frontend/app/(main)/admin/monitoring/page.tsx @@ -5,17 +5,19 @@ import MonitoringDashboard from "@/components/admin/MonitoringDashboard"; export default function MonitoringPage() { return ( -
- {/* 헤더 */} -
-

모니터링

-

- 배치 작업 실행 상태를 실시간으로 모니터링합니다. -

-
+
+
+ {/* 헤더 */} +
+

모니터링

+

+ 배치 작업 실행 상태를 실시간으로 모니터링합니다. +

+
- {/* 모니터링 대시보드 */} - + {/* 모니터링 대시보드 */} + +
); } diff --git a/frontend/app/(main)/admin/page.tsx b/frontend/app/(main)/admin/page.tsx index b320ab45..8735d7f6 100644 --- a/frontend/app/(main)/admin/page.tsx +++ b/frontend/app/(main)/admin/page.tsx @@ -5,7 +5,8 @@ import Link from "next/link"; */ export default function AdminPage() { return ( -
+
+
{/* 관리자 기능 카드들 */}
@@ -162,6 +163,7 @@ export default function AdminPage() {
+
); } diff --git a/frontend/app/(main)/admin/screenMng/page.tsx b/frontend/app/(main)/admin/screenMng/page.tsx index bf90f2d7..8918e936 100644 --- a/frontend/app/(main)/admin/screenMng/page.tsx +++ b/frontend/app/(main)/admin/screenMng/page.tsx @@ -66,14 +66,23 @@ export default function ScreenManagementPage() { const isLastStep = currentStep === "template"; return ( -
- {/* 단계별 내용 */} -
- {/* 화면 목록 단계 */} - {currentStep === "list" && ( -
-
-

{stepConfig.list.title}

+
+
+ {/* 페이지 제목 */} +
+
+

화면 관리

+

화면을 설계하고 템플릿을 관리합니다

+
+
+ + {/* 단계별 내용 */} +
+ {/* 화면 목록 단계 */} + {currentStep === "list" && ( +
+
+

{stepConfig.list.title}

@@ -89,18 +98,24 @@ export default function ScreenManagementPage() {
)} - {/* 화면 설계 단계 */} - {currentStep === "design" && ( -
- goToStep("list")} /> -
- )} + {/* 화면 설계 단계 */} + {currentStep === "design" && ( +
+
+

{stepConfig.design.title}

+ +
+ goToStep("list")} /> +
+ )} - {/* 템플릿 관리 단계 */} - {currentStep === "template" && ( -
-
-

{stepConfig.template.title}

+ {/* 템플릿 관리 단계 */} + {currentStep === "template" && ( +
+
+

{stepConfig.template.title}

- goToStep("list")} /> -
- )} + goToStep("list")} /> +
+ )} +
); diff --git a/frontend/app/(main)/admin/standards/[webType]/edit/page.tsx b/frontend/app/(main)/admin/standards/[webType]/edit/page.tsx index be7dd3f5..ff24db7f 100644 --- a/frontend/app/(main)/admin/standards/[webType]/edit/page.tsx +++ b/frontend/app/(main)/admin/standards/[webType]/edit/page.tsx @@ -203,7 +203,8 @@ export default function EditWebTypePage() { } return ( -
+
+
{/* 헤더 */}
@@ -502,6 +503,7 @@ export default function EditWebTypePage() {

)} +
); } diff --git a/frontend/app/(main)/admin/standards/[webType]/page.tsx b/frontend/app/(main)/admin/standards/[webType]/page.tsx index c11999ff..f44a8447 100644 --- a/frontend/app/(main)/admin/standards/[webType]/page.tsx +++ b/frontend/app/(main)/admin/standards/[webType]/page.tsx @@ -80,7 +80,8 @@ export default function WebTypeDetailPage() { } return ( -
+
+
{/* 헤더 */}
@@ -280,6 +281,7 @@ export default function WebTypeDetailPage() { +
); } diff --git a/frontend/app/(main)/admin/standards/new/page.tsx b/frontend/app/(main)/admin/standards/new/page.tsx index 77df8a74..aa60ed45 100644 --- a/frontend/app/(main)/admin/standards/new/page.tsx +++ b/frontend/app/(main)/admin/standards/new/page.tsx @@ -159,7 +159,8 @@ export default function NewWebTypePage() { }; return ( -
+
+
{/* 헤더 */}
@@ -453,6 +454,7 @@ export default function NewWebTypePage() {

)} +
); } diff --git a/frontend/app/(main)/admin/standards/page.tsx b/frontend/app/(main)/admin/standards/page.tsx index c21266ab..e00ddfa1 100644 --- a/frontend/app/(main)/admin/standards/page.tsx +++ b/frontend/app/(main)/admin/standards/page.tsx @@ -127,19 +127,20 @@ export default function WebTypesManagePage() { } return ( -
- {/* 헤더 */} -
-
-

웹타입 관리

-

화면관리에서 사용할 웹타입들을 관리합니다.

+
+
+ {/* 페이지 제목 */} +
+
+

웹타입 관리

+

화면관리에서 사용할 웹타입들을 관리합니다

+
+ + +
- - - -
{/* 필터 및 검색 */} @@ -364,6 +365,7 @@ export default function WebTypesManagePage() {

)} +
); } diff --git a/frontend/app/(main)/admin/tableMng/page.tsx b/frontend/app/(main)/admin/tableMng/page.tsx index 3c95f4df..311e20db 100644 --- a/frontend/app/(main)/admin/tableMng/page.tsx +++ b/frontend/app/(main)/admin/tableMng/page.tsx @@ -541,7 +541,7 @@ export default function TableManagementPage() { }, [selectedTable, columns.length, totalColumns, columnsLoading, pageSize, loadColumnTypes]); return ( -
+
{/* 페이지 제목 */}
diff --git a/frontend/app/(main)/admin/templates/page.tsx b/frontend/app/(main)/admin/templates/page.tsx index 800c84ac..e964d85c 100644 --- a/frontend/app/(main)/admin/templates/page.tsx +++ b/frontend/app/(main)/admin/templates/page.tsx @@ -145,13 +145,14 @@ export default function TemplatesManagePage() { } return ( -
- {/* 헤더 */} -
-
-

템플릿 관리

-

화면 디자이너에서 사용할 템플릿을 관리합니다.

-
+
+
+ {/* 페이지 제목 */} +
+
+

템플릿 관리

+

화면 디자이너에서 사용할 템플릿을 관리합니다

+
+
); } diff --git a/frontend/app/(main)/admin/userMng/page.tsx b/frontend/app/(main)/admin/userMng/page.tsx index 94f861cc..0d0df171 100644 --- a/frontend/app/(main)/admin/userMng/page.tsx +++ b/frontend/app/(main)/admin/userMng/page.tsx @@ -8,8 +8,17 @@ import { UserManagement } from "@/components/admin/UserManagement"; */ export default function UserMngPage() { return ( -
- +
+
+ {/* 페이지 제목 */} +
+
+

사용자 관리

+

시스템 사용자 계정 및 권한을 관리합니다

+
+
+ +
); } diff --git a/frontend/app/(main)/admin/validation-demo/page.tsx b/frontend/app/(main)/admin/validation-demo/page.tsx index bb567d63..e903bb4e 100644 --- a/frontend/app/(main)/admin/validation-demo/page.tsx +++ b/frontend/app/(main)/admin/validation-demo/page.tsx @@ -54,7 +54,7 @@ const TEST_COMPONENTS: ComponentData[] = [ required: true, style: { labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", }, } as WidgetComponent, @@ -72,7 +72,7 @@ const TEST_COMPONENTS: ComponentData[] = [ required: true, style: { labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", }, } as WidgetComponent, @@ -94,7 +94,7 @@ const TEST_COMPONENTS: ComponentData[] = [ }, style: { labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", }, } as WidgetComponent, @@ -112,7 +112,7 @@ const TEST_COMPONENTS: ComponentData[] = [ required: false, style: { labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", }, } as WidgetComponent, @@ -130,7 +130,7 @@ const TEST_COMPONENTS: ComponentData[] = [ required: false, style: { labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", }, } as WidgetComponent, @@ -152,7 +152,7 @@ const TEST_COMPONENTS: ComponentData[] = [ }, style: { labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", }, } as WidgetComponent, diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index 90195801..e5b622a6 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -237,7 +237,7 @@ export default function ScreenViewPage() { const labelText = component.style?.labelText || component.label || ""; const labelStyle = { fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: component.style?.labelFontWeight || "500", backgroundColor: component.style?.labelBackgroundColor || "transparent", padding: component.style?.labelPadding || "0", diff --git a/frontend/components/screen/EnhancedInteractiveScreenViewer.tsx b/frontend/components/screen/EnhancedInteractiveScreenViewer.tsx index 93475d3f..8c553b3c 100644 --- a/frontend/components/screen/EnhancedInteractiveScreenViewer.tsx +++ b/frontend/components/screen/EnhancedInteractiveScreenViewer.tsx @@ -232,7 +232,7 @@ export const EnhancedInteractiveScreenViewer: React.FC = ( // 라벨 스타일 적용 const labelStyle = { fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: component.style?.labelFontWeight || "500", backgroundColor: component.style?.labelBackgroundColor || "transparent", padding: component.style?.labelPadding || "0", diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index 409c6056..5260b3e5 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -32,6 +32,7 @@ interface RealtimePreviewProps { selectedScreen?: any; onZoneComponentDrop?: (e: React.DragEvent, zoneId: string, layoutId: string) => void; // 존별 드롭 핸들러 onZoneClick?: (zoneId: string) => void; // 존 클릭 핸들러 + onConfigChange?: (config: any) => void; // 설정 변경 핸들러 } // 동적 위젯 타입 아이콘 (레지스트리에서 조회) @@ -73,6 +74,7 @@ export const RealtimePreviewDynamic: React.FC = ({ selectedScreen, onZoneComponentDrop, onZoneClick, + onConfigChange, }) => { const { id, type, position, size, style: componentStyle } = component; @@ -89,8 +91,12 @@ export const RealtimePreviewDynamic: React.FC = ({ const baseStyle = { left: `${position.x}px`, top: `${position.y}px`, - width: `${size?.width || 100}px`, - height: `${size?.height || 36}px`, + width: component.componentConfig?.type === "table-list" + ? `${Math.max(size?.width || 400, 400)}px` // table-list는 최소 400px + : `${size?.width || 100}px`, + height: component.componentConfig?.type === "table-list" + ? `${Math.max(size?.height || 300, 300)}px` // table-list는 최소 300px + : `${size?.height || 36}px`, zIndex: component.type === "layout" ? 1 : position.z || 2, // 레이아웃은 z-index 1, 다른 컴포넌트는 2 이상 ...componentStyle, }; @@ -120,7 +126,9 @@ export const RealtimePreviewDynamic: React.FC = ({ onDragEnd={handleDragEnd} > {/* 동적 컴포넌트 렌더링 */} -
+
= ({ selectedScreen={selectedScreen} onZoneComponentDrop={onZoneComponentDrop} onZoneClick={onZoneClick} + onConfigChange={onConfigChange} />
diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 5e89166d..000ebc44 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -1004,7 +1004,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "600", labelMarginBottom: "8px", ...templateComp.style, @@ -1083,7 +1083,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "600", labelMarginBottom: "8px", ...templateComp.style, @@ -1134,7 +1134,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "600", labelMarginBottom: "8px", ...templateComp.style, @@ -1185,7 +1185,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "600", labelMarginBottom: "8px", ...templateComp.style, @@ -1274,7 +1274,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "600", labelMarginBottom: "8px", ...templateComp.style, @@ -1564,7 +1564,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: false, // 모든 컴포넌트의 기본 라벨 표시를 false로 설정 labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", labelMarginBottom: "4px", }, @@ -1653,7 +1653,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, labelFontSize: "14px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "600", labelMarginBottom: "8px", }, @@ -1844,7 +1844,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: false, // 모든 컴포넌트의 기본 라벨 표시를 false로 설정 labelFontSize: "12px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", labelMarginBottom: "6px", }, @@ -1887,7 +1887,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD style: { labelDisplay: true, // 테이블 패널에서 드래그한 컴포넌트는 라벨을 기본적으로 표시 labelFontSize: "12px", - labelColor: "#374151", + labelColor: "#3b83f6", labelFontWeight: "500", labelMarginBottom: "6px", }, @@ -3158,11 +3158,15 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD {/* 실제 작업 캔버스 (해상도 크기) */}
{ if (e.target === e.currentTarget && !selectionDrag.wasSelecting) { setSelectedComponent(null); @@ -3271,6 +3275,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD selectedScreen={selectedScreen} // onZoneComponentDrop 제거 onZoneClick={handleZoneClick} + // 설정 변경 핸들러 (테이블 페이지 크기 등 설정을 상세설정에 반영) + onConfigChange={(config) => { + console.log("📤 테이블 설정 변경을 상세설정에 알림:", config); + // 여기서 DetailSettingsPanel의 상태를 업데이트하거나 + // 컴포넌트의 componentConfig를 업데이트할 수 있습니다 + // TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요 + }} > {/* 컨테이너, 그룹, 영역의 자식 컴포넌트들 렌더링 (레이아웃은 독립적으로 렌더링) */} {(component.type === "group" || component.type === "container" || component.type === "area") && @@ -3351,6 +3362,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD selectedScreen={selectedScreen} // onZoneComponentDrop 제거 onZoneClick={handleZoneClick} + // 설정 변경 핸들러 (자식 컴포넌트용) + onConfigChange={(config) => { + console.log("📤 자식 컴포넌트 설정 변경을 상세설정에 알림:", config); + // TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요 + }} /> ); })} diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index 7c819eb2..10cabc52 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -134,7 +134,25 @@ export const ButtonConfigPanel: React.FC = ({ component,
+
+
{visibleColumns.map((column, colIndex) => { @@ -66,7 +81,7 @@ export const SingleTableWithSticky: React.FC = ({ return ( = ({ width: getColumnWidth(column), minWidth: getColumnWidth(column), maxWidth: getColumnWidth(column), + boxSizing: "border-box", + overflow: "hidden", + textOverflow: "ellipsis", // sticky 위치 설정 ...(column.fixed === "left" && { left: leftFixedWidth }), ...(column.fixed === "right" && { right: rightFixedWidth }), @@ -92,7 +110,7 @@ export const SingleTableWithSticky: React.FC = ({
{column.columnName === "__checkbox__" ? ( checkboxConfig.selectAll && ( - + ) ) : ( <> @@ -131,7 +149,7 @@ export const SingleTableWithSticky: React.FC = ({ ) : ( data.map((row, index) => ( = ({ return ( = ({ minHeight: "40px", height: "40px", verticalAlign: "middle", + width: getColumnWidth(column), + boxSizing: "border-box", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", // sticky 위치 설정 ...(column.fixed === "left" && { left: leftFixedWidth }), ...(column.fixed === "right" && { right: rightFixedWidth }), @@ -176,25 +199,7 @@ export const SingleTableWithSticky: React.FC = ({ > {column.columnName === "__checkbox__" ? renderCheckboxCell(row, index) - : (() => { - // 🎯 매핑된 컬럼명으로 데이터 찾기 (기본 테이블과 동일한 로직) - const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; - - // 조인 컬럼 매핑 정보 로깅 - if (column.columnName !== mappedColumnName && index === 0) { - console.log(`🔗 Sticky 조인 컬럼 매핑: ${column.columnName} → ${mappedColumnName}`); - } - - const cellValue = row[mappedColumnName]; - if (index === 0) { - // 첫 번째 행만 로그 출력 (디버깅용) - console.log( - `🔍 Sticky 셀 데이터 [${column.columnName} → ${mappedColumnName}]:`, - cellValue, - ); - } - return formatCellValue(cellValue, column.format, column.columnName) || "\u00A0"; - })()} + : formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"} ); })} diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 6bd974ed..a3d63041 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useMemo } from "react"; import { TableListConfig, ColumnConfig } from "./types"; +import { WebType } from "@/types/common"; import { tableTypeApi } from "@/lib/api/screen"; import { entityJoinApi } from "@/lib/api/entityJoin"; import { codeCache } from "@/lib/caching/codeCache"; @@ -22,7 +23,6 @@ import { import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/lib/utils"; import { AdvancedSearchFilters } from "@/components/screen/filters/AdvancedSearchFilters"; -import { Separator } from "@/components/ui/separator"; import { SingleTableWithSticky } from "./SingleTableWithSticky"; export interface TableListComponentProps { @@ -54,6 +54,9 @@ export interface TableListComponentProps { // 선택된 행 정보 전달 핸들러 onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void; + // 설정 변경 핸들러 (상세설정과 연동) + onConfigChange?: (config: any) => void; + // 테이블 새로고침 키 refreshKey?: number; } @@ -75,6 +78,7 @@ export const TableListComponent: React.FC = ({ onFormDataChange, componentConfig, onSelectedRowsChange, + onConfigChange, refreshKey, }) => { // 컴포넌트 설정 @@ -84,11 +88,16 @@ export const TableListComponent: React.FC = ({ ...componentConfig, } as TableListConfig; - // 🎯 디버깅: 초기 컬럼 설정 확인 - console.log( - "🔍 초기 tableConfig.columns:", - tableConfig.columns?.map((c) => c.columnName), - ); + // 🎨 동적 색상 설정 (속성편집 모달의 "색상" 필드와 연동) + const buttonColor = component.style?.labelColor || '#3b83f6'; // 기본 파란색 + const buttonTextColor = component.config?.buttonTextColor || '#ffffff'; + const buttonStyle = { + backgroundColor: buttonColor, + color: buttonTextColor, + borderColor: buttonColor + }; + + // 디버깅 로그 제거 (성능상 이유로) // 상태 관리 const [data, setData] = useState[]>([]); @@ -97,7 +106,7 @@ export const TableListComponent: React.FC = ({ const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(0); const [totalItems, setTotalItems] = useState(0); - const [searchTerm] = useState(""); + const [searchTerm, setSearchTerm] = useState(""); const [sortColumn, setSortColumn] = useState(null); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); const [columnLabels, setColumnLabels] = useState>({}); @@ -113,7 +122,11 @@ export const TableListComponent: React.FC = ({ const [searchValues, setSearchValues] = useState>({}); // 체크박스 상태 관리 - const [selectedRows, setSelectedRows] = useState>(new Set()); // 선택된 행들의 키 집합 + const [selectedRows, setSelectedRows] = useState>(new Set()); + + // 드래그 상태 관리 + const [isDragging, setIsDragging] = useState(false); + const [draggedRowIndex, setDraggedRowIndex] = useState(null); // 선택된 행들의 키 집합 const [isAllSelected, setIsAllSelected] = useState(false); // 전체 선택 상태 // 🎯 Entity 조인 최적화 훅 사용 @@ -125,10 +138,9 @@ export const TableListComponent: React.FC = ({ // 높이 계산 함수 (메모이제이션) const optimalHeight = useMemo(() => { - // 50개 이상일 때는 20개 기준으로 높이 고정하고 스크롤 생성 - // 50개 미만일 때는 실제 데이터 개수에 맞춰서 스크롤 없이 표시 - const actualDataCount = Math.min(data.length, localPageSize); - const displayPageSize = localPageSize >= 50 ? 20 : Math.max(actualDataCount, 5); + // 실제 데이터 개수에 맞춰서 높이 계산 (최소 5개, 최대 20개) + const actualDataCount = data.length; + const displayPageSize = Math.min(Math.max(actualDataCount, 5), 20); const headerHeight = 50; // 테이블 헤더 const rowHeight = 42; // 각 행 높이 @@ -144,7 +156,7 @@ export const TableListComponent: React.FC = ({ actualDataCount, localPageSize, displayPageSize, - willHaveScroll: localPageSize >= 50, + isDesignMode, titleHeight, searchHeight, headerHeight, @@ -165,25 +177,70 @@ export const TableListComponent: React.FC = ({ }); return calculatedHeight; - }, [data.length, localPageSize, tableConfig.filter?.enabled, tableConfig.showFooter, tableConfig.showHeader]); + }, []); - // 스타일 계산 + // 🎯 강제로 그리드 컬럼수에 맞는 크기 적용 (디자인 모드에서는 더 큰 크기 허용) + const gridColumns = component.gridColumns || 1; + let calculatedWidth: string; + + if (isDesignMode) { + // 디자인 모드에서는 더 큰 최소 크기 적용 + if (gridColumns === 1) { + calculatedWidth = "400px"; // 1컬럼일 때 400px (디자인 모드) + } else if (gridColumns === 2) { + calculatedWidth = "600px"; // 2컬럼일 때 600px (디자인 모드) + } else if (gridColumns <= 6) { + calculatedWidth = `${gridColumns * 250}px`; // 컬럼당 250px (디자인 모드) + } else { + calculatedWidth = "100%"; // 7컬럼 이상은 전체 + } + } else { + // 일반 모드는 기존 크기 유지 + if (gridColumns === 1) { + calculatedWidth = "200px"; // 1컬럼일 때 200px 고정 + } else if (gridColumns === 2) { + calculatedWidth = "400px"; // 2컬럼일 때 400px + } else if (gridColumns <= 6) { + calculatedWidth = `${gridColumns * 200}px`; // 컬럼당 200px + } else { + calculatedWidth = "100%"; // 7컬럼 이상은 전체 + } + } + + // 디버깅 로그 제거 (성능상 이유로) + + + // 스타일 계산 (컨테이너에 맞춤) const componentStyle: React.CSSProperties = { - width: "100%", - height: `${optimalHeight}px`, // 20개 데이터를 모두 보여주는 높이 - minHeight: `${optimalHeight}px`, // 최소 높이 보장 + width: "100%", // 컨테이너 전체 너비 사용 + maxWidth: "100%", // 최대 너비 제한 + height: "auto", // 항상 자동 높이로 테이블 크기에 맞춤 + minHeight: isDesignMode ? `${Math.min(optimalHeight, 400)}px` : `${optimalHeight}px`, // 최소 높이 보장 ...component.style, ...style, display: "flex", flexDirection: "column", boxSizing: "border-box", // 패딩/보더 포함한 크기 계산 + // overflow는 CSS 클래스로 처리 }; + // 🎯 tableContainerStyle 제거 - componentStyle만 사용 + // 디자인 모드 스타일 if (isDesignMode) { - componentStyle.border = "1px dashed #cbd5e1"; + componentStyle.border = "2px dashed #cbd5e1"; componentStyle.borderColor = isSelected ? "#3b82f6" : "#cbd5e1"; - // minHeight 제거 - 실제 데이터에 맞는 높이 사용 + componentStyle.borderRadius = "8px"; + componentStyle.padding = "4px"; // 약간의 패딩으로 구분감 확보 + componentStyle.margin = "2px"; // 외부 여백으로 레이아웃과 구분 + // 🎯 컨테이너에 맞춤 + componentStyle.width = "calc(100% - 12px)"; // margin + padding 보정 + componentStyle.maxWidth = "calc(100% - 12px)"; + componentStyle.minWidth = "calc(100% - 12px)"; + componentStyle.overflow = "hidden !important"; // 넘치는 부분 숨김 (강제) + componentStyle.boxSizing = "border-box"; // 패딩 포함 크기 계산 + componentStyle.position = "relative"; // 위치 고정 + // 자동 높이로 테이블 전체를 감쌈 } // 컬럼 라벨 정보 가져오기 @@ -290,42 +347,30 @@ export const TableListComponent: React.FC = ({ // 🎯 조인 탭에서 추가한 컬럼들도 추가 (실제로 존재하는 컬럼만) ...joinTabColumns .filter((col) => { - // 조인 컬럼인지 확인 (언더스코어가 포함된 컬럼) - const isJoinColumn = col.columnName.includes("_") && col.columnName !== "__checkbox__"; - if (!isJoinColumn) { - console.log(`🔍 조인 탭 컬럼 제외: ${col.columnName} (조인 컬럼이 아님)`); + // 실제 API 응답에 존재하는 컬럼만 필터링 + const validJoinColumns = ["dept_code_name", "dept_name"]; + const isValid = validJoinColumns.includes(col.columnName); + if (!isValid) { + console.log(`🔍 조인 탭 컬럼 제외: ${col.columnName} (유효하지 않음)`); } - return isJoinColumn; + return isValid; }) .map((col) => { - // 동적으로 조인 컬럼 정보 추출 - console.log(`🔍 조인 컬럼 분석: ${col.columnName}`); + // 실제 존재하는 조인 컬럼만 처리 + let sourceTable = tableConfig.selectedTable; + let sourceColumn = col.columnName; - // 컬럼명에서 기본 컬럼과 참조 테이블 추출 - // 예: dept_code_company_name -> dept_code (기본), company_name (참조) - const parts = col.columnName.split("_"); - let sourceColumn = ""; - let referenceTable = ""; - - // dept_code로 시작하는 경우 - if (col.columnName.startsWith("dept_code_")) { + if (col.columnName === "dept_code_name" || col.columnName === "dept_name") { + sourceTable = "dept_info"; sourceColumn = "dept_code"; - referenceTable = "dept_info"; - } - // 다른 패턴들도 추가 가능 - else { - // 기본적으로 첫 번째 부분을 소스 컬럼으로 사용 - sourceColumn = parts[0]; - referenceTable = tableConfig.selectedTable || "unknown"; } - console.log(`🔗 조인 설정: ${col.columnName} -> ${sourceColumn} (${referenceTable})`); + console.log(`🔍 조인 탭 컬럼 처리: ${col.columnName} -> ${sourceTable}.${sourceColumn}`); return { - sourceTable: tableConfig.selectedTable || "unknown", // 기본 테이블 (user_info) + sourceTable: sourceTable || tableConfig.selectedTable || "", sourceColumn: sourceColumn, joinAlias: col.columnName, - referenceTable: referenceTable, // 참조 테이블 정보도 추가 }; }), ]; @@ -422,14 +467,6 @@ export const TableListComponent: React.FC = ({ console.log("🎯 데이터 개수:", result.data?.length || 0); console.log("🎯 전체 페이지:", result.totalPages); console.log("🎯 총 아이템:", result.total); - - // 🚨 데이터 샘플 확인 (첫 번째 행의 모든 컬럼과 값) - if (result.data && result.data.length > 0) { - console.log("🔍 첫 번째 행 데이터 샘플:", result.data[0]); - Object.entries(result.data[0]).forEach(([key, value]) => { - console.log(` 📊 ${key}: "${value}" (타입: ${typeof value})`); - }); - } setData(result.data || []); setTotalPages(result.totalPages || 1); setTotalItems(result.total || 0); @@ -447,7 +484,7 @@ export const TableListComponent: React.FC = ({ // 🎯 코드 컬럼들의 캐시 미리 로드 (전역 캐시 사용) const codeColumns = Object.entries(columnMeta).filter( - ([, meta]) => meta.webType === "code" && meta.codeCategory, + ([_, meta]) => meta.webType === "code" && meta.codeCategory, ); if (codeColumns.length > 0) { @@ -482,119 +519,16 @@ export const TableListComponent: React.FC = ({ const actualApiColumns = Object.keys(result.data[0]); console.log("🔍 API 응답의 실제 컬럼들:", actualApiColumns); - // 🎯 조인 컬럼 매핑 테이블 동적 생성 (사용자 설정 → API 응답) - const newJoinColumnMapping: Record = {}; - - // 사용자가 설정한 컬럼들과 실제 API 응답 컬럼들을 동적으로 매핑 - processedColumns.forEach((userColumn) => { - // 체크박스는 제외 - if (userColumn.columnName === "__checkbox__") return; - - console.log(`🔍 컬럼 매핑 분석: "${userColumn.columnName}"`, { - displayName: userColumn.displayName, - isEntityJoin: userColumn.isEntityJoin, - entityJoinInfo: userColumn.entityJoinInfo, - available: actualApiColumns, - }); - - // 사용자 설정 컬럼명이 API 응답에 정확히 있는지 확인 - if (actualApiColumns.includes(userColumn.columnName)) { - // 직접 매칭되는 경우 - newJoinColumnMapping[userColumn.columnName] = userColumn.columnName; - console.log(`✅ 정확 매핑: ${userColumn.columnName} → ${userColumn.columnName}`); - } else { - // Entity 조인된 컬럼이거나 조인 탭에서 추가한 컬럼인 경우 - let foundMatch = false; - - // 1. Entity 조인 정보가 있는 경우 aliasColumn 우선 확인 - if (userColumn.entityJoinInfo?.joinAlias) { - const aliasColumn = userColumn.entityJoinInfo.joinAlias; - if (actualApiColumns.includes(aliasColumn)) { - newJoinColumnMapping[userColumn.columnName] = aliasColumn; - console.log(`🔗 Entity 별칭 매핑: ${userColumn.columnName} → ${aliasColumn}`); - foundMatch = true; - } - } - - // 2. 정확한 이름 매칭 (예: dept_code_company_name) - if (!foundMatch) { - const exactMatches = actualApiColumns.filter((apiCol) => apiCol === userColumn.columnName); - - if (exactMatches.length > 0) { - newJoinColumnMapping[userColumn.columnName] = exactMatches[0]; - console.log(`🎯 정확 이름 매핑: ${userColumn.columnName} → ${exactMatches[0]}`); - foundMatch = true; - } - } - - // 3. 조인 컬럼 검증 및 처리 - if (!foundMatch) { - // 🚨 조인 컬럼인지 확인 (더 정확한 감지 로직) - const hasUnderscore = userColumn.columnName.includes("_"); - let isJoinColumn = false; - let baseColumnName = ""; - - if (hasUnderscore) { - // 가능한 모든 기본 컬럼명을 확인 (dept_code_company_name -> dept_code, dept 순으로) - const parts = userColumn.columnName.split("_"); - for (let i = parts.length - 1; i >= 1; i--) { - const possibleBase = parts.slice(0, i).join("_"); - if (actualApiColumns.includes(possibleBase)) { - baseColumnName = possibleBase; - isJoinColumn = true; - break; - } - } - } - - console.log(`🔍 조인 컬럼 검사: "${userColumn.columnName}"`, { - hasUnderscore, - baseColumnName, - isJoinColumn, - }); - - if (isJoinColumn) { - console.log(`🔍 조인 컬럼 기본 컬럼 확인: "${baseColumnName}"`, { - existsInApi: actualApiColumns.includes(baseColumnName), - actualApiColumns: actualApiColumns.slice(0, 10), // 처음 10개만 표시 - }); - - console.warn( - `⚠️ 조인 실패: "${userColumn.columnName}" - 백엔드에서 Entity 조인이 실행되지 않음. 기본 컬럼값 표시합니다.`, - ); - // 조인 실패 시 기본 컬럼값을 표시하도록 매핑 - newJoinColumnMapping[userColumn.columnName] = baseColumnName; - foundMatch = true; - } else { - // 일반 컬럼인 경우 부분 매칭 시도 - const partialMatches = actualApiColumns.filter( - (apiCol) => apiCol.includes(userColumn.columnName) || userColumn.columnName.includes(apiCol), - ); - - if (partialMatches.length > 0) { - const bestMatch = partialMatches.reduce((best, current) => - Math.abs(current.length - userColumn.columnName.length) < - Math.abs(best.length - userColumn.columnName.length) - ? current - : best, - ); - - newJoinColumnMapping[userColumn.columnName] = bestMatch; - console.log(`🔍 부분 매핑: ${userColumn.columnName} → ${bestMatch}`); - foundMatch = true; - } - } - } - - // 4. 매칭 실패한 경우 원본 유지 (하지만 경고 표시) - if (!foundMatch) { - newJoinColumnMapping[userColumn.columnName] = userColumn.columnName; - console.warn( - `⚠️ 매핑 실패: "${userColumn.columnName}" - 사용 가능한 컬럼: [${actualApiColumns.join(", ")}]`, - ); - } - } - }); + // 🎯 조인 컬럼 매핑 테이블 (사용자 설정 → 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); @@ -656,23 +590,11 @@ export const TableListComponent: React.FC = ({ if (result.entityJoinInfo?.joinConfigs) { result.entityJoinInfo.joinConfigs.forEach((joinConfig) => { // 원본 컬럼을 조인된 컬럼으로 교체 - let originalColumnIndex = processedColumns.findIndex((col) => col.columnName === joinConfig.sourceColumn); + const originalColumnIndex = processedColumns.findIndex((col) => col.columnName === joinConfig.sourceColumn); if (originalColumnIndex !== -1) { console.log(`🔄 컬럼 교체: ${joinConfig.sourceColumn} → ${joinConfig.aliasColumn}`); const originalColumn = processedColumns[originalColumnIndex]; - - // 🚨 중복 방지: 이미 같은 aliasColumn이 있는지 확인 - const existingAliasIndex = processedColumns.findIndex((col) => col.columnName === joinConfig.aliasColumn); - if (existingAliasIndex !== -1 && existingAliasIndex !== originalColumnIndex) { - console.warn(`🚨 중복 컬럼 발견: ${joinConfig.aliasColumn}이 이미 존재합니다. 중복 제거합니다.`); - processedColumns.splice(existingAliasIndex, 1); - // 인덱스 재조정 - if (existingAliasIndex < originalColumnIndex) { - originalColumnIndex--; - } - } - processedColumns[originalColumnIndex] = { ...originalColumn, columnName: joinConfig.aliasColumn, // dept_code → dept_code_name @@ -718,26 +640,9 @@ export const TableListComponent: React.FC = ({ processedColumns = autoColumns; } - // 🚨 processedColumns에서 중복 제거 - const uniqueProcessedColumns = processedColumns.filter( - (column, index, self) => self.findIndex((c) => c.columnName === column.columnName) === index, - ); - - if (uniqueProcessedColumns.length !== processedColumns.length) { - console.error("🚨 processedColumns에서 중복 발견:"); - console.error( - "원본:", - processedColumns.map((c) => c.columnName), - ); - console.error( - "중복 제거 후:", - uniqueProcessedColumns.map((c) => c.columnName), - ); - } - // 🎯 표시할 컬럼 상태 업데이트 - setDisplayColumns(uniqueProcessedColumns); - console.log("🎯 displayColumns 업데이트됨:", uniqueProcessedColumns); + setDisplayColumns(processedColumns); + console.log("🎯 displayColumns 업데이트됨:", processedColumns); console.log("🎯 데이터 개수:", result.data?.length || 0); console.log("🎯 전체 데이터:", result.data); } @@ -753,6 +658,20 @@ export const TableListComponent: React.FC = ({ // 페이지 변경 const handlePageChange = (newPage: number) => { setCurrentPage(newPage); + + // 상세설정에 현재 페이지 정보 알림 (필요한 경우) + if (onConfigChange && tableConfig.pagination) { + console.log("📤 테이블에서 페이지 변경을 상세설정에 알림:", newPage); + onConfigChange({ + ...tableConfig, + pagination: { + ...tableConfig.pagination, + currentPage: newPage, // 현재 페이지 정보 추가 + }, + }); + } else if (!onConfigChange) { + console.warn("⚠️ onConfigChange가 정의되지 않음 - 페이지 변경 상세설정과 연동 불가"); + } }; // 정렬 변경 @@ -916,6 +835,22 @@ export const TableListComponent: React.FC = ({ } }, [refreshKey]); + // 상세설정에서 페이지네이션 설정 변경 시 로컬 상태 동기화 + useEffect(() => { + // 페이지 크기 동기화 + if (tableConfig.pagination?.pageSize && tableConfig.pagination.pageSize !== localPageSize) { + console.log("🔄 상세설정에서 페이지 크기 변경 감지:", tableConfig.pagination.pageSize); + setLocalPageSize(tableConfig.pagination.pageSize); + setCurrentPage(1); // 페이지를 1로 리셋 + } + + // 현재 페이지 동기화 (상세설정에서 페이지를 직접 변경한 경우) + if (tableConfig.pagination?.currentPage && tableConfig.pagination.currentPage !== currentPage) { + console.log("🔄 상세설정에서 현재 페이지 변경 감지:", tableConfig.pagination.currentPage); + setCurrentPage(tableConfig.pagination.currentPage); + } + }, [tableConfig.pagination?.pageSize, tableConfig.pagination?.currentPage]); + // 표시할 컬럼 계산 (Entity 조인 적용됨 + 체크박스 컬럼 추가 + 숨김 기능) const visibleColumns = useMemo(() => { // 기본값 처리: checkbox 설정이 없으면 기본값 사용 @@ -930,7 +865,7 @@ export const TableListComponent: React.FC = ({ // displayColumns가 있으면 우선 사용 (Entity 조인 적용된 컬럼들) if (displayColumns && displayColumns.length > 0) { - console.log("🎯 displayColumns 사용:", displayColumns); + // 디버깅 로그 제거 (성능상 이유로) const filteredColumns = displayColumns.filter((col) => { // 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김 if (isDesignMode) { @@ -939,11 +874,11 @@ export const TableListComponent: React.FC = ({ 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) => { // 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김 @@ -982,31 +917,7 @@ export const TableListComponent: React.FC = ({ } } - console.log("🎯 최종 visibleColumns:", columns); - console.log("🎯 visibleColumns 개수:", columns.length); - console.log( - "🎯 visibleColumns 컬럼명들:", - columns.map((c) => c.columnName), - ); - - // 🚨 중복 키 검사 - const columnNames = columns.map((c) => c.columnName); - const duplicates = columnNames.filter((name, index) => columnNames.indexOf(name) !== index); - if (duplicates.length > 0) { - console.error("🚨 중복된 컬럼명 발견:", duplicates); - console.error("🚨 전체 컬럼명 목록:", columnNames); - - // 중복 제거 - const uniqueColumns = columns.filter( - (column, index, self) => self.findIndex((c) => c.columnName === column.columnName) === index, - ); - console.log( - "🔧 중복 제거 후 컬럼들:", - uniqueColumns.map((c) => c.columnName), - ); - return uniqueColumns; - } - + // 디버깅 로그 제거 (성능상 이유로) return columns; }, [displayColumns, tableConfig.columns, tableConfig.checkbox, isDesignMode]); @@ -1075,7 +986,7 @@ export const TableListComponent: React.FC = ({ return null; } - return ; + return ; }; // 체크박스 셀 렌더링 @@ -1100,6 +1011,7 @@ export const TableListComponent: React.FC = ({ checked={isSelected} onCheckedChange={(checked) => handleRowSelection(rowKey, checked as boolean)} aria-label={`행 ${index + 1} 선택`} + style={{ zIndex: 1 }} /> ); }; @@ -1109,30 +1021,14 @@ export const TableListComponent: React.FC = ({ return (value: any, format?: string, columnName?: string) => { if (value === null || value === undefined) return ""; - // 디버깅: 모든 값 변환 시도를 로깅 - if ( - columnName && - (columnName === "contract_type" || columnName === "domestic_foreign" || columnName === "status") - ) { - console.log(`🔍 값 변환 시도: ${columnName}="${value}"`, { - columnMeta: columnMeta[columnName], - hasColumnMeta: !!columnMeta[columnName], - webType: columnMeta[columnName]?.webType, - codeCategory: columnMeta[columnName]?.codeCategory, - globalColumnMeta: Object.keys(columnMeta), - }); - } + // 디버깅 로그 제거 (성능상 이유로) // 🎯 코드 컬럼인 경우 최적화된 코드명 변환 사용 if (columnName && columnMeta[columnName]?.webType === "code" && columnMeta[columnName]?.codeCategory) { const categoryCode = columnMeta[columnName].codeCategory!; const convertedValue = optimizedConvertCode(categoryCode, String(value)); - if (convertedValue !== String(value)) { - console.log(`🔄 코드 변환 성공: ${columnName}[${categoryCode}] ${value} → ${convertedValue}`); - } else { - console.log(`⚠️ 코드 변환 실패: ${columnName}[${categoryCode}] ${value} → ${convertedValue} (값 동일)`); - } + // 코드 변환 로그 제거 (성능상 이유로) value = convertedValue; } @@ -1165,6 +1061,82 @@ export const TableListComponent: React.FC = ({ } }; + // 드래그 핸들러 (그리드 스냅 지원) + const handleRowDragStart = (e: React.DragEvent, row: any, index: number) => { + setIsDragging(true); + setDraggedRowIndex(index); + + // 드래그 데이터에 그리드 정보 포함 + const dragData = { + ...row, + _dragType: 'table-row', + _gridSize: { width: 4, height: 1 }, // 기본 그리드 크기 (4칸 너비, 1칸 높이) + _snapToGrid: true + }; + + e.dataTransfer.setData('application/json', JSON.stringify(dragData)); + e.dataTransfer.effectAllowed = 'copy'; // move 대신 copy로 변경 + + // 드래그 이미지를 더 깔끔하게 + const dragElement = e.currentTarget as HTMLElement; + + // 커스텀 드래그 이미지 생성 (저장 버튼과 어울리는 스타일) + const dragImage = document.createElement('div'); + dragImage.style.position = 'absolute'; + dragImage.style.top = '-1000px'; + dragImage.style.left = '-1000px'; + dragImage.style.background = 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)'; + dragImage.style.color = 'white'; + dragImage.style.padding = '12px 16px'; + dragImage.style.borderRadius = '8px'; + dragImage.style.fontSize = '14px'; + dragImage.style.fontWeight = '600'; + dragImage.style.boxShadow = '0 4px 12px rgba(59, 130, 246, 0.4)'; + dragImage.style.display = 'flex'; + dragImage.style.alignItems = 'center'; + dragImage.style.gap = '8px'; + dragImage.style.minWidth = '200px'; + dragImage.style.whiteSpace = 'nowrap'; + + // 아이콘과 텍스트 추가 + const firstValue = Object.values(row)[0] || 'Row'; + dragImage.innerHTML = ` +
📋
+ ${firstValue} +
4×1
+ `; + + document.body.appendChild(dragImage); + e.dataTransfer.setDragImage(dragImage, 20, 20); + + // 정리 + setTimeout(() => { + if (document.body.contains(dragImage)) { + document.body.removeChild(dragImage); + } + }, 0); + }; + + const handleRowDragEnd = (e: React.DragEvent) => { + setIsDragging(false); + setDraggedRowIndex(null); + }; + // DOM에 전달할 수 있는 기본 props만 정의 const domProps = { onClick: handleClick, @@ -1176,11 +1148,15 @@ export const TableListComponent: React.FC = ({ if (isDesignMode && !tableConfig.selectedTable) { return (
-
-
- -
테이블 리스트
-
설정 패널에서 테이블을 선택해주세요
+
+
+
+ +
+
테이블 리스트
+
+ 설정 패널에서 테이블을 선택해주세요 +
@@ -1188,56 +1164,64 @@ export const TableListComponent: React.FC = ({ } return ( -
+
{/* 헤더 */} {tableConfig.showHeader && ( -
+
{(tableConfig.title || tableLabel) && ( -

{tableConfig.title || tableLabel}

+

+ {tableConfig.title || tableLabel} +

)}
-
+
{/* 선택된 항목 정보 표시 */} {selectedRows.size > 0 && ( -
- {selectedRows.size}개 선택됨 +
+ {selectedRows.size}개 선택됨
)} - {/* 검색 - 기존 방식은 주석처리 */} - {/* {tableConfig.filter?.enabled && tableConfig.filter?.quickSearch && ( + {/* 새로고침 */} +
@@ -1246,168 +1230,255 @@ export const TableListComponent: React.FC = ({ {/* 고급 검색 필터 - 항상 표시 (컬럼 정보 기반 자동 생성) */} {tableConfig.filter?.enabled && visibleColumns && visibleColumns.length > 0 && ( <> - -
+
+ ({ columnName: col.columnName, - webType: (columnMeta[col.columnName]?.webType as any) || "text", + widgetType: (columnMeta[col.columnName]?.webType || "text") as WebType, displayName: columnLabels[col.columnName] || col.displayName || col.columnName, codeCategory: columnMeta[col.columnName]?.codeCategory, isVisible: col.visible, // 추가 메타데이터 전달 (필터 자동 생성용) - web_type: columnMeta[col.columnName]?.webType || "text", + web_type: (columnMeta[col.columnName]?.webType || "text") as WebType, column_name: col.columnName, column_label: columnLabels[col.columnName] || col.displayName || col.columnName, code_category: columnMeta[col.columnName]?.codeCategory, }))} - tableName={tableConfig.selectedTable} - /> + tableName={tableConfig.selectedTable} + /> +
)} {/* 테이블 컨텐츠 */} -
= 50 ? "flex-1 overflow-auto" : ""}`}> +
= 50 ? "flex-1" : ""}`} + style={{ + width: "100%", + maxWidth: "100%", + boxSizing: "border-box" + }} + > {loading ? ( -
-
- -
데이터를 불러오는 중...
+
+
+
+
+ +
+
+
+
데이터를 불러오는 중...
+
잠시만 기다려주세요
) : error ? ( -
-
-
오류가 발생했습니다
-
{error}
+
+
+
+
+ ! +
+
+
오류가 발생했습니다
+
{error}
) : needsHorizontalScroll ? ( // 가로 스크롤이 필요한 경우 - 단일 테이블에서 sticky 컬럼 사용 - +
+ +
) : ( // 기존 테이블 (가로 스크롤이 필요 없는 경우) -
- - - {visibleColumns.map((column, colIndex) => ( - column.sortable && handleSort(column.columnName)} - > - {column.columnName === "__checkbox__" ? ( - renderCheckboxHeader() - ) : ( -
- {columnLabels[column.columnName] || column.displayName} - {column.sortable && ( -
- {sortColumn === column.columnName ? ( - sortDirection === "asc" ? ( - +
+
+ + + {visibleColumns.map((column, colIndex) => ( + column.sortable && handleSort(column.columnName)} + > + {column.columnName === "__checkbox__" ? ( + renderCheckboxHeader() + ) : ( +
+ + {columnLabels[column.columnName] || column.displayName} + + {column.sortable && ( +
+ {sortColumn === column.columnName ? ( + sortDirection === "asc" ? ( + + ) : ( + + ) ) : ( - - ) - ) : ( - - )} -
- )} -
- )} -
- ))} -
-
+ + )} + + )} + + )} + + ))} + + {data.length === 0 ? ( - - 데이터가 없습니다 + +
+
+ +
+
데이터가 없습니다
+
조건을 변경하거나 새로운 데이터를 추가해보세요
+
) : ( data.map((row, index) => ( handleRowDragStart(e, row, index)} + onDragEnd={handleRowDragEnd} className={cn( - "h-10 cursor-pointer leading-none", - tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50", - tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/50", + "group relative h-12 cursor-pointer transition-all duration-200 border-b border-gray-100", + // 기본 스타일 + tableConfig.tableStyle?.hoverEffect && "hover:bg-gradient-to-r hover:from-orange-200 hover:to-orange-300/90 hover:shadow-sm", + tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-100/80", + // 드래그 상태 스타일 (미묘하게) + draggedRowIndex === index && "bg-gradient-to-r from-blue-50 to-blue-100/40 shadow-sm border-blue-200", + isDragging && draggedRowIndex !== index && "opacity-70", + // 드래그 가능 표시 + !isDesignMode && "hover:cursor-grab active:cursor-grabbing" )} - style={{ minHeight: "40px", height: "40px", lineHeight: "1" }} + style={{ + minHeight: "48px", + height: "48px", + lineHeight: "1", + width: "100%", + maxWidth: "100%" + }} onClick={() => handleRowClick(row)} > {visibleColumns.map((column, colIndex) => ( {column.columnName === "__checkbox__" ? renderCheckboxCell(row, index) : (() => { // 🎯 매핑된 컬럼명으로 데이터 찾기 const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; - - // 조인 컬럼 매핑 정보 로깅 - if (column.columnName !== mappedColumnName && index === 0) { - console.log(`🔗 조인 컬럼 매핑: ${column.columnName} → ${mappedColumnName}`); - } - const cellValue = row[mappedColumnName]; if (index === 0) { - // 첫 번째 행만 로그 출력 - console.log(`🔍 셀 데이터 [${column.columnName} → ${mappedColumnName}]:`, cellValue); - - // 🚨 조인된 컬럼인 경우 추가 디버깅 - if (column.columnName !== mappedColumnName) { - console.log(" 🔗 조인 컬럼 분석:"); - console.log(` 👤 사용자 설정 컬럼: "${column.columnName}"`); - console.log(` 📡 매핑된 API 컬럼: "${mappedColumnName}"`); - console.log(` 📋 컬럼 라벨: "${column.displayName}"`); - console.log(` 💾 실제 데이터: "${cellValue}"`); - console.log( - ` 🔄 원본 컬럼 데이터 (${column.columnName}): "${row[column.columnName]}"`, - ); - } + // 디버깅 로그 제거 (성능상 이유로) } - return formatCellValue(cellValue, column.format, column.columnName) || "\u00A0"; + const formattedValue = formatCellValue(cellValue, column.format, column.columnName) || "\u00A0"; + + // 첫 번째 컬럼에 드래그 핸들과 아바타 추가 + const isFirstColumn = colIndex === (visibleColumns[0]?.columnName === "__checkbox__" ? 1 : 0); + + return ( +
+ {isFirstColumn && !isDesignMode && ( +
+ {/* 그리드 스냅 가이드 아이콘 */} +
+
+
+
+
+
+
+
+
+
+
+ )} + + {formattedValue} + +
+ ); })()}
))} @@ -1416,40 +1487,76 @@ export const TableListComponent: React.FC = ({ )}
+ )} {/* 푸터/페이지네이션 */} {tableConfig.showFooter && tableConfig.pagination?.enabled && ( -
-
- {tableConfig.pagination?.showPageInfo && ( - - 전체 {totalItems.toLocaleString()}건 중 {(currentPage - 1) * localPageSize + 1}- - {Math.min(currentPage * localPageSize, totalItems)} 표시 +
+ {/* 페이지 정보 - 가운데 정렬 */} + {tableConfig.pagination?.showPageInfo && ( +
+
+ + 전체 {totalItems.toLocaleString()}건 중{" "} + + {(currentPage - 1) * localPageSize + 1}-{Math.min(currentPage * localPageSize, totalItems)} + {" "} + 표시 - )} -
+
+ )} -
- {/* 페이지 크기 선택 */} - {tableConfig.pagination?.showSizeSelector && ( + {/* 페이지 크기 선택과 페이지네이션 버튼 - 가운데 정렬 */} +
+ {/* 페이지 크기 선택 - 임시로 항상 표시 (테스트용) */} + {true && ( handleChange("color", e.target.value)} />
diff --git a/frontend/lib/registry/components/text-display/index.ts b/frontend/lib/registry/components/text-display/index.ts index c86255f1..9280aa0b 100644 --- a/frontend/lib/registry/components/text-display/index.ts +++ b/frontend/lib/registry/components/text-display/index.ts @@ -24,7 +24,7 @@ export const TextDisplayDefinition = createComponentDefinition({ text: "텍스트를 입력하세요", fontSize: "14px", fontWeight: "normal", - color: "#374151", + color: "#3b83f6", textAlign: "left", }, defaultSize: { width: 150, height: 24 }, diff --git a/frontend/lib/registry/components/text-input/TextInputComponent.tsx b/frontend/lib/registry/components/text-input/TextInputComponent.tsx index 4a5aabf6..f4fe7a9e 100644 --- a/frontend/lib/registry/components/text-input/TextInputComponent.tsx +++ b/frontend/lib/registry/components/text-input/TextInputComponent.tsx @@ -190,7 +190,7 @@ export const TextInputComponent: React.FC = ({ top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", }} > diff --git a/frontend/lib/registry/components/textarea-basic/TextareaBasicComponent.tsx b/frontend/lib/registry/components/textarea-basic/TextareaBasicComponent.tsx index 04128d74..482280b0 100644 --- a/frontend/lib/registry/components/textarea-basic/TextareaBasicComponent.tsx +++ b/frontend/lib/registry/components/textarea-basic/TextareaBasicComponent.tsx @@ -84,7 +84,7 @@ export const TextareaBasicComponent: React.FC = ({ top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", // isInteractive 모드에서는 사용자 스타일 우선 적용 ...(isInteractive && component.style ? component.style : {}), diff --git a/frontend/lib/registry/components/toggle-switch/ToggleSwitchComponent.tsx b/frontend/lib/registry/components/toggle-switch/ToggleSwitchComponent.tsx index 8183e1c0..f71a4127 100644 --- a/frontend/lib/registry/components/toggle-switch/ToggleSwitchComponent.tsx +++ b/frontend/lib/registry/components/toggle-switch/ToggleSwitchComponent.tsx @@ -84,7 +84,7 @@ export const ToggleSwitchComponent: React.FC = ({ top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", // isInteractive 모드에서는 사용자 스타일 우선 적용 ...(isInteractive && component.style ? component.style : {}), @@ -173,7 +173,7 @@ export const ToggleSwitchComponent: React.FC = ({
{ const element = e.currentTarget; - element.style.borderColor = "#3b82f6"; - element.style.backgroundColor = "rgba(59, 130, 246, 0.02)"; - element.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.1)"; + // 🎯 컴포넌트가 있는 존은 호버 효과 최소화 + if (zoneChildren.length > 0) { + element.style.backgroundColor = "rgba(59, 130, 246, 0.01)"; + } else { + element.style.borderColor = "#3b82f6"; + element.style.backgroundColor = "rgba(59, 130, 246, 0.02)"; + element.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.1)"; + } }} onMouseLeave={(e) => { const element = e.currentTarget; - element.style.borderColor = isDesignMode ? "#cbd5e1" : "#e2e8f0"; - element.style.backgroundColor = isDesignMode ? "rgba(241, 245, 249, 0.8)" : "rgba(248, 250, 252, 0.5)"; + if (zoneChildren.length > 0) { + // 컴포넌트가 있는 존 복원 + element.style.borderColor = "transparent"; + element.style.backgroundColor = isDesignMode ? "rgba(248, 250, 252, 0.3)" : "rgba(248, 250, 252, 0.5)"; + } else { + // 빈 존 복원 + element.style.borderColor = isDesignMode ? "#cbd5e1" : "#e2e8f0"; + element.style.backgroundColor = isDesignMode ? "rgba(241, 245, 249, 0.8)" : "rgba(248, 250, 252, 0.5)"; + } element.style.boxShadow = "none"; }} onDrop={this.handleDrop(zone.id)} diff --git a/frontend/lib/registry/layouts/accordion/AccordionLayout.tsx b/frontend/lib/registry/layouts/accordion/AccordionLayout.tsx index 43488c51..e108b7c7 100644 --- a/frontend/lib/registry/layouts/accordion/AccordionLayout.tsx +++ b/frontend/lib/registry/layouts/accordion/AccordionLayout.tsx @@ -148,7 +148,7 @@ const AccordionSection: React.FC<{ const headerStyle: React.CSSProperties = { padding: "12px 16px", backgroundColor: isDesignMode ? "#3b82f6" : "#f8fafc", - color: isDesignMode ? "white" : "#374151", + color: isDesignMode ? "white" : "#3b83f6", border: "1px solid #e2e8f0", borderBottom: isExpanded ? "none" : "1px solid #e2e8f0", cursor: "pointer", diff --git a/frontend/lib/registry/utils/hotReload.ts b/frontend/lib/registry/utils/hotReload.ts index c4688100..a2f61a29 100644 --- a/frontend/lib/registry/utils/hotReload.ts +++ b/frontend/lib/registry/utils/hotReload.ts @@ -14,6 +14,10 @@ let hotReloadListeners: Array<() => void> = []; * Hot Reload 시스템 초기화 */ export function initializeHotReload(): void { + // 핫 리로드 시스템 임시 비활성화 (디버깅 목적) + console.log("🔥 컴포넌트 Hot Reload 시스템 비활성화됨 (디버깅 모드)"); + return; + if (process.env.NODE_ENV !== "development" || typeof window === "undefined") { return; } @@ -55,11 +59,15 @@ function setupDevServerEventListener(): void { const originalLog = console.log; let reloadPending = false; - // console.log 메시지를 감지하여 Hot Reload 트리거 + // console.log 메시지를 감지하여 Hot Reload 트리거 (특정 메시지만) console.log = (...args: any[]) => { const message = args.join(" "); - if (message.includes("compiled") || message.includes("Fast Refresh") || message.includes("component")) { + // 핫 리로드를 트리거할 특정 메시지만 감지 (디버깅 로그는 제외) + if ((message.includes("compiled") || message.includes("Fast Refresh")) && + !message.includes("🔍") && !message.includes("🎯") && !message.includes("📤") && + !message.includes("📥") && !message.includes("⚠️") && !message.includes("🔄") && + !message.includes("✅") && !message.includes("🔧") && !message.includes("📋")) { if (!reloadPending) { reloadPending = true; setTimeout(() => { diff --git a/frontend/lib/utils/getComponentConfigPanel.tsx b/frontend/lib/utils/getComponentConfigPanel.tsx index 8ff6fd55..fa464377 100644 --- a/frontend/lib/utils/getComponentConfigPanel.tsx +++ b/frontend/lib/utils/getComponentConfigPanel.tsx @@ -110,6 +110,8 @@ export const DynamicComponentConfigPanel: React.FC = screenTableName, tableColumns, }) => { + console.log(`🔥 DynamicComponentConfigPanel 렌더링 시작: ${componentId}`); + const [ConfigPanelComponent, setConfigPanelComponent] = React.useState | null>(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); @@ -180,10 +182,21 @@ export const DynamicComponentConfigPanel: React.FC = ); } + console.log(`🔧 DynamicComponentConfigPanel 렌더링:`, { + componentId, + ConfigPanelComponent: ConfigPanelComponent?.name, + config, + configType: typeof config, + configKeys: typeof config === 'object' ? Object.keys(config || {}) : 'not object', + screenTableName, + tableColumns: Array.isArray(tableColumns) ? tableColumns.length : tableColumns + }); + return ( diff --git a/frontend/scripts/create-component.js b/frontend/scripts/create-component.js index 674f48d7..83d5c852 100755 --- a/frontend/scripts/create-component.js +++ b/frontend/scripts/create-component.js @@ -661,7 +661,7 @@ function getComponentJSXByWebType(webType) { top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", }} > @@ -709,7 +709,7 @@ function getComponentJSXByWebType(webType) { top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", }} > @@ -785,7 +785,7 @@ function getComponentJSXByWebType(webType) { top: "-25px", left: "0px", fontSize: component.style?.labelFontSize || "14px", - color: component.style?.labelColor || "#374151", + color: component.style?.labelColor || "#3b83f6", fontWeight: "500", }} > diff --git a/frontend/types/component.ts b/frontend/types/component.ts index 34a8cd92..ea29cc5f 100644 --- a/frontend/types/component.ts +++ b/frontend/types/component.ts @@ -68,6 +68,9 @@ export interface ComponentRendererProps { // 새로운 기능들 autoGeneration?: AutoGenerationConfig; // 자동생성 설정 hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김) + + // 설정 변경 핸들러 + onConfigChange?: (config: any) => void; [key: string]: any; } @@ -317,7 +320,7 @@ export const COMPONENT_CATEGORIES_INFO = { [ComponentCategory.CHART]: { name: "차트", description: "데이터 시각화 컴포넌트", - color: "#06b6d4", + color: "#3b83f6", }, [ComponentCategory.FORM]: { name: "폼", @@ -347,7 +350,7 @@ export const COMPONENT_CATEGORIES_INFO = { [ComponentCategory.CONTAINER]: { name: "컨테이너", description: "다른 컴포넌트를 담는 컨테이너", - color: "#374151", + color: "#3b83f6", }, [ComponentCategory.SYSTEM]: { name: "시스템", From e3cd6dc3a0c5beccfd48ba3236136800f5c06476 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Thu, 25 Sep 2025 09:29:56 +0900 Subject: [PATCH 14/14] =?UTF-8?q?UI/UX=20=EA=B0=9C=EC=84=A0:=20=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=EB=B0=94=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=95=88=EC=A0=95=ED=99=94=20=EB=B0=8F=20=EB=A9=94?= =?UTF-8?q?=EB=89=B4=20hover=20=ED=9A=A8=EA=B3=BC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사이드바 고정 너비 설정으로 메뉴 클릭 시 너비 변화 방지 - 메뉴 항목 hover 효과 일관성 개선 (고정 높이, 부드러운 색상 전환) - 디버깅 로그 제거로 성능 최적화 - 관리자 페이지 카드 디자인 개선 (그라데이션 배경, 아이콘 색상 조정) --- frontend/app/(main)/admin/commonCode/page.tsx | 10 +- frontend/app/(main)/admin/company/page.tsx | 2 +- frontend/app/(main)/admin/dataflow/page.tsx | 8 +- .../admin/external-connections/page.tsx | 8 +- frontend/app/(main)/admin/layouts/page.tsx | 14 +- frontend/app/(main)/admin/menu/page.tsx | 2 +- frontend/app/(main)/admin/screenMng/page.tsx | 36 +-- frontend/app/(main)/admin/standards/page.tsx | 258 +++++++++--------- frontend/app/(main)/admin/tableMng/page.tsx | 14 +- frontend/app/(main)/admin/templates/page.tsx | 26 +- frontend/app/(main)/admin/userMng/page.tsx | 2 +- frontend/components/admin/MenuManagement.tsx | 126 +++++---- frontend/components/layout/AppLayout.tsx | 70 +++-- 13 files changed, 307 insertions(+), 269 deletions(-) diff --git a/frontend/app/(main)/admin/commonCode/page.tsx b/frontend/app/(main)/admin/commonCode/page.tsx index 3868ec40..6d5eba31 100644 --- a/frontend/app/(main)/admin/commonCode/page.tsx +++ b/frontend/app/(main)/admin/commonCode/page.tsx @@ -14,7 +14,7 @@ export default function CommonCodeManagementPage() {
{/* 페이지 제목 */} -
+

공통코드 관리

시스템에서 사용하는 공통코드를 관리합니다

@@ -26,8 +26,8 @@ export default function CommonCodeManagementPage() {
{/* 카테고리 패널 - PC에서 좌측 고정 너비, 모바일에서 전체 너비 */}
- - + + 📂 코드 카테고리 @@ -38,8 +38,8 @@ export default function CommonCodeManagementPage() { {/* 코드 상세 패널 - PC에서 나머지 공간, 모바일에서 전체 너비 */}
- - + + 📋 코드 상세 정보 {selectedCategoryCode && ( diff --git a/frontend/app/(main)/admin/company/page.tsx b/frontend/app/(main)/admin/company/page.tsx index 7e222aa8..645470eb 100644 --- a/frontend/app/(main)/admin/company/page.tsx +++ b/frontend/app/(main)/admin/company/page.tsx @@ -8,7 +8,7 @@ export default function CompanyPage() {
{/* 페이지 제목 */} -
+

회사 관리

시스템에서 사용하는 회사 정보를 관리합니다

diff --git a/frontend/app/(main)/admin/dataflow/page.tsx b/frontend/app/(main)/admin/dataflow/page.tsx index cf57b3cb..de70ff1a 100644 --- a/frontend/app/(main)/admin/dataflow/page.tsx +++ b/frontend/app/(main)/admin/dataflow/page.tsx @@ -79,13 +79,13 @@ export default function DataFlowPage() {
{/* 페이지 제목 */} -
+

데이터 흐름 관리

테이블 간 데이터 관계를 시각적으로 설계하고 관리합니다

{currentStep !== "list" && ( - @@ -97,7 +97,7 @@ export default function DataFlowPage() { {/* 관계도 목록 단계 */} {currentStep === "list" && (
-
+

{stepConfig.list.title}

@@ -107,7 +107,7 @@ export default function DataFlowPage() { {/* 관계도 설계 단계 */} {currentStep === "design" && (
-
+

{stepConfig.design.title}

{/* 페이지 제목 */} -
+

외부 커넥션 관리

외부 데이터베이스 연결 정보를 관리합니다

@@ -231,7 +231,7 @@ export default function ExternalConnectionsPage() {
{/* 검색 및 필터 */} - +
@@ -289,7 +289,7 @@ export default function ExternalConnectionsPage() {
로딩 중...
) : connections.length === 0 ? ( - +
@@ -302,7 +302,7 @@ export default function ExternalConnectionsPage() { ) : ( - + diff --git a/frontend/app/(main)/admin/layouts/page.tsx b/frontend/app/(main)/admin/layouts/page.tsx index f22bd3d8..eb5b2aff 100644 --- a/frontend/app/(main)/admin/layouts/page.tsx +++ b/frontend/app/(main)/admin/layouts/page.tsx @@ -223,18 +223,18 @@ export default function LayoutManagementPage() {
{/* 페이지 제목 */} -
+

레이아웃 관리

화면 레이아웃을 생성하고 관리합니다

- -
+ +
{/* 검색 및 필터 */} - +
@@ -284,7 +284,7 @@ export default function LayoutManagementPage() { {layouts.map((layout) => { const CategoryIcon = CATEGORY_ICONS[layout.category as keyof typeof CATEGORY_ICONS]; return ( - +
diff --git a/frontend/app/(main)/admin/menu/page.tsx b/frontend/app/(main)/admin/menu/page.tsx index fcf0b965..3d5548cc 100644 --- a/frontend/app/(main)/admin/menu/page.tsx +++ b/frontend/app/(main)/admin/menu/page.tsx @@ -7,7 +7,7 @@ export default function MenuPage() {
{/* 페이지 제목 */} -
+

메뉴 관리

시스템 메뉴를 관리하고 화면을 할당합니다

diff --git a/frontend/app/(main)/admin/screenMng/page.tsx b/frontend/app/(main)/admin/screenMng/page.tsx index 8918e936..2002d364 100644 --- a/frontend/app/(main)/admin/screenMng/page.tsx +++ b/frontend/app/(main)/admin/screenMng/page.tsx @@ -69,7 +69,7 @@ export default function ScreenManagementPage() {
{/* 페이지 제목 */} -
+

화면 관리

화면을 설계하고 템플릿을 관리합니다

@@ -81,12 +81,12 @@ export default function ScreenManagementPage() { {/* 화면 목록 단계 */} {currentStep === "list" && (
-
+

{stepConfig.list.title}

- -
+ +
-
+

{stepConfig.design.title}

-
@@ -114,18 +114,18 @@ export default function ScreenManagementPage() { {/* 템플릿 관리 단계 */} {currentStep === "template" && (
-
+

{stepConfig.template.title}

-
- - +
+ + +
-
goToStep("list")} />
)} diff --git a/frontend/app/(main)/admin/standards/page.tsx b/frontend/app/(main)/admin/standards/page.tsx index e00ddfa1..ce1170e9 100644 --- a/frontend/app/(main)/admin/standards/page.tsx +++ b/frontend/app/(main)/admin/standards/page.tsx @@ -130,44 +130,44 @@ export default function WebTypesManagePage() {
{/* 페이지 제목 */} -
+

웹타입 관리

화면관리에서 사용할 웹타입들을 관리합니다

-
- {/* 필터 및 검색 */} - - - - - 필터 및 검색 - - - -
- {/* 검색 */} -
- - setSearchTerm(e.target.value)} - className="pl-10" - /> -
+ {/* 필터 및 검색 */} + + + + + 필터 및 검색 + + + +
+ {/* 검색 */} +
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
- {/* 카테고리 필터 */} - + + + 전체 카테고리 {categories.map((category) => ( @@ -178,96 +178,96 @@ export default function WebTypesManagePage() { - {/* 활성화 상태 필터 */} - + {/* 활성화 상태 필터 */} + - {/* 초기화 버튼 */} - + {/* 초기화 버튼 */} +
- {/* 결과 통계 */} -
-

총 {filteredAndSortedWebTypes.length}개의 웹타입이 있습니다.

-
+ {/* 결과 통계 */} +
+

총 {filteredAndSortedWebTypes.length}개의 웹타입이 있습니다.

+
- {/* 웹타입 목록 테이블 */} - - -
- - - handleSort("sort_order")}> -
- 순서 - {sortField === "sort_order" && - (sortDirection === "asc" ? : )} -
-
- handleSort("web_type")}> -
- 웹타입 코드 - {sortField === "web_type" && - (sortDirection === "asc" ? : )} -
-
- handleSort("type_name")}> -
- 웹타입명 - {sortField === "type_name" && - (sortDirection === "asc" ? : )} -
-
- handleSort("category")}> -
- 카테고리 - {sortField === "category" && - (sortDirection === "asc" ? : )} -
-
- 설명 - handleSort("component_name")}> -
- 연결된 컴포넌트 - {sortField === "component_name" && - (sortDirection === "asc" ? : )} -
-
- handleSort("config_panel")}> -
- 설정 패널 - {sortField === "config_panel" && - (sortDirection === "asc" ? : )} -
-
- handleSort("is_active")}> -
- 상태 - {sortField === "is_active" && - (sortDirection === "asc" ? : )} -
-
- handleSort("updated_date")}> -
- 최종 수정일 - {sortField === "updated_date" && - (sortDirection === "asc" ? : )} -
-
- 작업 + {/* 웹타입 목록 테이블 */} + + +
+ + + handleSort("sort_order")}> +
+ 순서 + {sortField === "sort_order" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("web_type")}> +
+ 웹타입 코드 + {sortField === "web_type" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("type_name")}> +
+ 웹타입명 + {sortField === "type_name" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("category")}> +
+ 카테고리 + {sortField === "category" && + (sortDirection === "asc" ? : )} +
+
+ 설명 + handleSort("component_name")}> +
+ 연결된 컴포넌트 + {sortField === "component_name" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("config_panel")}> +
+ 설정 패널 + {sortField === "config_panel" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("is_active")}> +
+ 상태 + {sortField === "is_active" && + (sortDirection === "asc" ? : )} +
+
+ handleSort("updated_date")}> +
+ 최종 수정일 + {sortField === "updated_date" && + (sortDirection === "asc" ? : )} +
+
+ 작업
@@ -310,24 +310,24 @@ export default function WebTypesManagePage() { {webType.updated_date ? new Date(webType.updated_date).toLocaleDateString("ko-KR") : "-"} - -
- - - - - - - - - - + +
+ + + + + + + + + + 웹타입 삭제 diff --git a/frontend/app/(main)/admin/tableMng/page.tsx b/frontend/app/(main)/admin/tableMng/page.tsx index 311e20db..9fbaaed5 100644 --- a/frontend/app/(main)/admin/tableMng/page.tsx +++ b/frontend/app/(main)/admin/tableMng/page.tsx @@ -543,7 +543,7 @@ export default function TableManagementPage() { return (
{/* 페이지 제목 */} -
+

{getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_TITLE, "테이블 타입 관리")} @@ -593,10 +593,10 @@ export default function TableManagementPage() {
{/* 테이블 목록 */} - - + + - + {getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_NAME, "테이블 목록")} @@ -663,10 +663,10 @@ export default function TableManagementPage() { {/* 컬럼 타입 관리 */} - - + + - + {selectedTable ? <>테이블 설정 - {selectedTable} : "테이블 타입 관리"} diff --git a/frontend/app/(main)/admin/templates/page.tsx b/frontend/app/(main)/admin/templates/page.tsx index e964d85c..c06fda1d 100644 --- a/frontend/app/(main)/admin/templates/page.tsx +++ b/frontend/app/(main)/admin/templates/page.tsx @@ -148,25 +148,25 @@ export default function TemplatesManagePage() {
{/* 페이지 제목 */} -
+

템플릿 관리

화면 디자이너에서 사용할 템플릿을 관리합니다

-
- +
+ +
-
{/* 필터 및 검색 */} - - + + - + 필터 및 검색 @@ -231,8 +231,8 @@ export default function TemplatesManagePage() { {/* 템플릿 목록 테이블 */} - - + + 템플릿 목록 ({filteredAndSortedTemplates.length}개) diff --git a/frontend/app/(main)/admin/userMng/page.tsx b/frontend/app/(main)/admin/userMng/page.tsx index 0d0df171..3348148a 100644 --- a/frontend/app/(main)/admin/userMng/page.tsx +++ b/frontend/app/(main)/admin/userMng/page.tsx @@ -11,7 +11,7 @@ export default function UserMngPage() {
{/* 페이지 제목 */} -
+

사용자 관리

시스템 사용자 계정 및 권한을 관리합니다

diff --git a/frontend/components/admin/MenuManagement.tsx b/frontend/components/admin/MenuManagement.tsx index eb6d72de..c92f0a2d 100644 --- a/frontend/components/admin/MenuManagement.tsx +++ b/frontend/components/admin/MenuManagement.tsx @@ -821,8 +821,11 @@ export const MenuManagement: React.FC = () => { {/* 좌측 사이드바 - 메뉴 타입 선택 (20%) */}
-

{getUITextSync("menu.type.title")}

-
+ + + {getUITextSync("menu.type.title")} + + {
-
+ +
{/* 우측 메인 영역 - 메뉴 목록 (80%) */}
-
-

- {getMenuTypeString()} {getUITextSync("menu.list.title")} -

-
- - {/* 검색 및 필터 영역 */} -
+ + + + {getMenuTypeString()} {getUITextSync("menu.list.title")} + + + + {/* 검색 및 필터 영역 */} +
@@ -997,52 +1002,54 @@ export const MenuManagement: React.FC = () => {
-
+
-
-
-
- {getUITextSync("menu.list.total", { count: getCurrentMenus().length })} -
-
- - {selectedMenus.size > 0 && ( - + {selectedMenus.size > 0 && ( + )} - - )} +
+
+
-
- -
+ +
@@ -1050,8 +1057,15 @@ export const MenuManagement: React.FC = () => { {/* 화면 할당 탭 */} - - + + + + 화면 할당 + + + + + diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index 7d537098..5ac4c6cb 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, Suspense } from "react"; +import { useState, Suspense, useEffect } from "react"; import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { Button } from "@/components/ui/button"; import { @@ -197,8 +197,27 @@ function AppLayoutInner({ children }: AppLayoutProps) { const searchParams = useSearchParams(); const { user, logout, refreshUserData } = useAuth(); const { userMenus, adminMenus, loading, refreshMenus } = useMenu(); - const [sidebarOpen, setSidebarOpen] = useState(false); + const [sidebarOpen, setSidebarOpen] = useState(true); const [expandedMenus, setExpandedMenus] = useState>(new Set()); + const [isMobile, setIsMobile] = useState(false); + + // 화면 크기 감지 및 사이드바 초기 상태 설정 + useEffect(() => { + const checkIsMobile = () => { + const mobile = window.innerWidth < 1024; // lg 브레이크포인트 + setIsMobile(mobile); + // 모바일에서만 사이드바를 닫음 + if (mobile) { + setSidebarOpen(false); + } else { + setSidebarOpen(true); + } + }; + + checkIsMobile(); + window.addEventListener('resize', checkIsMobile); + return () => window.removeEventListener('resize', checkIsMobile); + }, []); // 프로필 관련 로직 const { @@ -253,15 +272,10 @@ function AppLayoutInner({ children }: AppLayoutProps) { ? `/screens/${firstScreen.screenId}?mode=admin` : `/screens/${firstScreen.screenId}`; - console.log("🎯 메뉴에서 화면으로 이동:", { - menuName: menu.name, - screenId: firstScreen.screenId, - isAdminMode, - targetPath: screenPath, - }); - router.push(screenPath); - setSidebarOpen(false); + if (isMobile) { + setSidebarOpen(false); + } return; } } catch (error) { @@ -271,10 +285,11 @@ function AppLayoutInner({ children }: AppLayoutProps) { // 할당된 화면이 없고 URL이 있으면 기존 URL로 이동 if (menu.url && menu.url !== "#") { router.push(menu.url); - setSidebarOpen(false); + if (isMobile) { + setSidebarOpen(false); + } } else { // URL도 없고 할당된 화면도 없으면 경고 메시지 - console.warn("메뉴에 URL이나 할당된 화면이 없습니다:", menu); toast.warning("이 메뉴에는 연결된 페이지나 화면이 없습니다."); } } @@ -295,7 +310,7 @@ function AppLayoutInner({ children }: AppLayoutProps) { await logout(); router.push("/login"); } catch (error) { - console.error("로그아웃 실패:", error); + // 로그아웃 실패 시 처리 } }; @@ -306,7 +321,7 @@ function AppLayoutInner({ children }: AppLayoutProps) { return (
0 ? "ml-6" : ""}`} onClick={() => handleMenuClick(menu)} > -
+
{menu.icon} - {menu.name} + {menu.name}
{menu.hasChildren && (
@@ -339,8 +354,10 @@ function AppLayoutInner({ children }: AppLayoutProps) { }`} onClick={() => handleMenuClick(child)} > - {child.icon} - {child.name} +
+ {child.icon} + {child.name} +
))}
@@ -369,22 +386,29 @@ function AppLayoutInner({ children }: AppLayoutProps) { {/* MainHeader 컴포넌트 사용 */} setSidebarOpen(!sidebarOpen)} + onSidebarToggle={() => { + // 모바일에서만 토글 동작 + if (isMobile) { + setSidebarOpen(!sidebarOpen); + } + }} onProfileClick={openProfileModal} onLogout={handleLogout} />
{/* 모바일 사이드바 오버레이 */} - {sidebarOpen && ( + {sidebarOpen && isMobile && (
setSidebarOpen(false)} /> )} {/* 왼쪽 사이드바 */} {/* 가운데 컨텐츠 영역 */} -
{children}
+
{children}
{/* 프로필 수정 모달 */}