From 963e0c2d247a980d9c6335fc7e056ef020c889ce Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 16 Dec 2025 14:56:31 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EB=94=94=EC=8A=A4?= =?UTF-8?q?=ED=94=8C=EB=A0=88=EC=9D=B4=20=EC=84=A0=ED=83=9D=EC=95=88?= =?UTF-8?q?=ED=95=A8=20=EC=98=B5=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/entityJoinController.ts | 10 +- .../card-display/CardDisplayComponent.tsx | 30 +- .../card-display/CardDisplayConfigPanel.tsx | 492 ++++++++++++------ 3 files changed, 362 insertions(+), 170 deletions(-) diff --git a/backend-node/src/controllers/entityJoinController.ts b/backend-node/src/controllers/entityJoinController.ts index 00727f1d..fbb88750 100644 --- a/backend-node/src/controllers/entityJoinController.ts +++ b/backend-node/src/controllers/entityJoinController.ts @@ -424,18 +424,16 @@ export class EntityJoinController { config.referenceTable ); - // 현재 display_column으로 사용 중인 컬럼 제외 + // 현재 display_column 정보 (참고용으로만 사용, 필터링하지 않음) const currentDisplayColumn = config.displayColumn || config.displayColumns[0]; - const availableColumns = columns.filter( - (col) => col.columnName !== currentDisplayColumn - ); - + + // 모든 컬럼 표시 (기본 표시 컬럼도 포함) return { joinConfig: config, tableName: config.referenceTable, currentDisplayColumn: currentDisplayColumn, - availableColumns: availableColumns.map((col) => ({ + availableColumns: columns.map((col) => ({ columnName: col.columnName, columnLabel: col.displayName || col.columnName, dataType: col.dataType, diff --git a/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx b/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx index c3414677..5594d266 100644 --- a/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx +++ b/frontend/lib/registry/components/card-display/CardDisplayComponent.tsx @@ -4,6 +4,7 @@ import React, { useEffect, useState, useMemo, useCallback, useRef } from "react" import { ComponentRendererProps } from "@/types/component"; import { CardDisplayConfig } from "./types"; import { tableTypeApi } from "@/lib/api/screen"; +import { entityJoinApi } from "@/lib/api/entityJoin"; import { getFullImageUrl, apiClient } from "@/lib/api/client"; import { filterDOMProps } from "@/lib/utils/domPropsFilter"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; @@ -308,10 +309,35 @@ export const CardDisplayComponent: React.FC = ({ search: Object.keys(linkedFilterValues).length > 0 ? linkedFilterValues : undefined, }; + // 조인 컬럼 설정 가져오기 (componentConfig에서) + const joinColumnsConfig = component.componentConfig?.joinColumns || []; + const entityJoinColumns = joinColumnsConfig + .filter((col: any) => col.isJoinColumn) + .map((col: any) => ({ + columnName: col.columnName, + sourceColumn: col.sourceColumn, + referenceTable: col.referenceTable, + referenceColumn: col.referenceColumn, + displayColumn: col.referenceColumn, + label: col.label, + joinAlias: col.columnName, // 백엔드에서 필요한 joinAlias 추가 + sourceTable: tableNameToUse, // 기준 테이블 + })); // 테이블 데이터, 컬럼 정보, 입력 타입 정보를 병렬로 로드 - const [dataResponse, columnsResponse, inputTypesResponse] = await Promise.all([ - tableTypeApi.getTableData(tableNameToUse, apiParams), + // 조인 컬럼이 있으면 entityJoinApi 사용 + let dataResponse; + if (entityJoinColumns.length > 0) { + console.log("🔗 [CardDisplay] 엔티티 조인 API 사용:", entityJoinColumns); + dataResponse = await entityJoinApi.getTableDataWithJoins(tableNameToUse, { + ...apiParams, + additionalJoinColumns: entityJoinColumns, + }); + } else { + dataResponse = await tableTypeApi.getTableData(tableNameToUse, apiParams); + } + + const [columnsResponse, inputTypesResponse] = await Promise.all([ tableTypeApi.getColumns(tableNameToUse), tableTypeApi.getColumnInputTypes(tableNameToUse), ]); diff --git a/frontend/lib/registry/components/card-display/CardDisplayConfigPanel.tsx b/frontend/lib/registry/components/card-display/CardDisplayConfigPanel.tsx index 52889865..73bb79a9 100644 --- a/frontend/lib/registry/components/card-display/CardDisplayConfigPanel.tsx +++ b/frontend/lib/registry/components/card-display/CardDisplayConfigPanel.tsx @@ -1,6 +1,21 @@ "use client"; -import React from "react"; +import React, { useState, useEffect } from "react"; +import { entityJoinApi } from "@/lib/api/entityJoin"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Trash2 } from "lucide-react"; interface CardDisplayConfigPanelProps { config: any; @@ -9,9 +24,32 @@ interface CardDisplayConfigPanelProps { tableColumns?: any[]; } +interface EntityJoinColumn { + tableName: string; + columnName: string; + columnLabel: string; + dataType: string; + joinAlias: string; + suggestedLabel: string; +} + +interface JoinTable { + tableName: string; + currentDisplayColumn: string; + joinConfig?: { + sourceColumn: string; + }; + availableColumns: Array<{ + columnName: string; + columnLabel: string; + dataType: string; + description?: string; + }>; +} + /** * CardDisplay 설정 패널 - * 카드 레이아웃과 동일한 설정 UI 제공 + * 카드 레이아웃과 동일한 설정 UI 제공 + 엔티티 조인 컬럼 지원 */ export const CardDisplayConfigPanel: React.FC = ({ config, @@ -19,6 +57,40 @@ export const CardDisplayConfigPanel: React.FC = ({ screenTableName, tableColumns = [], }) => { + // 엔티티 조인 컬럼 상태 + const [entityJoinColumns, setEntityJoinColumns] = useState<{ + availableColumns: EntityJoinColumn[]; + joinTables: JoinTable[]; + }>({ availableColumns: [], joinTables: [] }); + const [loadingEntityJoins, setLoadingEntityJoins] = useState(false); + + // 엔티티 조인 컬럼 정보 가져오기 + useEffect(() => { + const fetchEntityJoinColumns = async () => { + const tableName = config.tableName || screenTableName; + if (!tableName) { + setEntityJoinColumns({ availableColumns: [], joinTables: [] }); + return; + } + + setLoadingEntityJoins(true); + try { + const result = await entityJoinApi.getEntityJoinColumns(tableName); + setEntityJoinColumns({ + availableColumns: result.availableColumns || [], + joinTables: result.joinTables || [], + }); + } catch (error) { + console.error("Entity 조인 컬럼 조회 오류:", error); + setEntityJoinColumns({ availableColumns: [], joinTables: [] }); + } finally { + setLoadingEntityJoins(false); + } + }; + + fetchEntityJoinColumns(); + }, [config.tableName, screenTableName]); + const handleChange = (key: string, value: any) => { onChange({ ...config, [key]: value }); }; @@ -28,7 +100,6 @@ export const CardDisplayConfigPanel: React.FC = ({ let newConfig = { ...config }; let current = newConfig; - // 중첩 객체 생성 for (let i = 0; i < keys.length - 1; i++) { if (!current[keys[i]]) { current[keys[i]] = {}; @@ -40,6 +111,47 @@ export const CardDisplayConfigPanel: React.FC = ({ onChange(newConfig); }; + // 컬럼 선택 시 조인 컬럼이면 joinColumns 설정도 함께 업데이트 + const handleColumnSelect = (path: string, columnName: string) => { + const joinColumn = entityJoinColumns.availableColumns.find( + (col) => col.joinAlias === columnName + ); + + if (joinColumn) { + const joinColumnsConfig = config.joinColumns || []; + const existingJoinColumn = joinColumnsConfig.find( + (jc: any) => jc.columnName === columnName + ); + + if (!existingJoinColumn) { + const joinTableInfo = entityJoinColumns.joinTables?.find( + (jt) => jt.tableName === joinColumn.tableName + ); + + const newJoinColumnConfig = { + columnName: joinColumn.joinAlias, + label: joinColumn.suggestedLabel || joinColumn.columnLabel, + sourceColumn: joinTableInfo?.joinConfig?.sourceColumn || "", + referenceTable: joinColumn.tableName, + referenceColumn: joinColumn.columnName, + isJoinColumn: true, + }; + + onChange({ + ...config, + columnMapping: { + ...config.columnMapping, + [path.split(".")[1]]: columnName, + }, + joinColumns: [...joinColumnsConfig, newJoinColumnConfig], + }); + return; + } + } + + handleNestedChange(path, columnName); + }; + // 표시 컬럼 추가 const addDisplayColumn = () => { const currentColumns = config.columnMapping?.displayColumns || []; @@ -58,122 +170,198 @@ export const CardDisplayConfigPanel: React.FC = ({ const updateDisplayColumn = (index: number, value: string) => { const currentColumns = [...(config.columnMapping?.displayColumns || [])]; currentColumns[index] = value; + + const joinColumn = entityJoinColumns.availableColumns.find( + (col) => col.joinAlias === value + ); + + if (joinColumn) { + const joinColumnsConfig = config.joinColumns || []; + const existingJoinColumn = joinColumnsConfig.find( + (jc: any) => jc.columnName === value + ); + + if (!existingJoinColumn) { + const joinTableInfo = entityJoinColumns.joinTables?.find( + (jt) => jt.tableName === joinColumn.tableName + ); + + const newJoinColumnConfig = { + columnName: joinColumn.joinAlias, + label: joinColumn.suggestedLabel || joinColumn.columnLabel, + sourceColumn: joinTableInfo?.joinConfig?.sourceColumn || "", + referenceTable: joinColumn.tableName, + referenceColumn: joinColumn.columnName, + isJoinColumn: true, + }; + + onChange({ + ...config, + columnMapping: { + ...config.columnMapping, + displayColumns: currentColumns, + }, + joinColumns: [...joinColumnsConfig, newJoinColumnConfig], + }); + return; + } + } + handleNestedChange("columnMapping.displayColumns", currentColumns); }; + // 테이블별로 조인 컬럼 그룹화 + const joinColumnsByTable: Record = {}; + entityJoinColumns.availableColumns.forEach((col) => { + if (!joinColumnsByTable[col.tableName]) { + joinColumnsByTable[col.tableName] = []; + } + joinColumnsByTable[col.tableName].push(col); + }); + + // 컬럼 선택 셀렉트 박스 렌더링 (Shadcn UI) + const renderColumnSelect = ( + value: string, + onChangeHandler: (value: string) => void, + placeholder: string = "컬럼을 선택하세요" + ) => { + return ( + + ); + }; + return (
-
카드 디스플레이 설정
+
카드 디스플레이 설정
{/* 테이블이 선택된 경우 컬럼 매핑 설정 */} {tableColumns && tableColumns.length > 0 && (
-
컬럼 매핑
+
컬럼 매핑
-
- - + {loadingEntityJoins && ( +
조인 컬럼 로딩 중...
+ )} + +
+ + {renderColumnSelect( + config.columnMapping?.titleColumn || "", + (value) => handleColumnSelect("columnMapping.titleColumn", value) + )}
-
- - +
+ + {renderColumnSelect( + config.columnMapping?.subtitleColumn || "", + (value) => handleColumnSelect("columnMapping.subtitleColumn", value) + )}
-
- - +
+ + {renderColumnSelect( + config.columnMapping?.descriptionColumn || "", + (value) => handleColumnSelect("columnMapping.descriptionColumn", value) + )}
-
- - +
+ + {renderColumnSelect( + config.columnMapping?.imageColumn || "", + (value) => handleColumnSelect("columnMapping.imageColumn", value) + )}
{/* 동적 표시 컬럼 추가 */} -
-
- - +
{(config.columnMapping?.displayColumns || []).map((column: string, index: number) => ( -
- - + +
))} {(!config.columnMapping?.displayColumns || config.columnMapping.displayColumns.length === 0) && ( -
+
"컬럼 추가" 버튼을 클릭하여 표시할 컬럼을 추가하세요
)} @@ -184,186 +372,166 @@ export const CardDisplayConfigPanel: React.FC = ({ {/* 카드 스타일 설정 */}
-
카드 스타일
+
카드 스타일
-
-
- - +
+ + handleChange("cardsPerRow", parseInt(e.target.value))} - className="w-full rounded border border-gray-300 px-2 py-1 text-sm" + className="h-8 text-xs" />
-
- - + + handleChange("cardSpacing", parseInt(e.target.value))} - className="w-full rounded border border-gray-300 px-2 py-1 text-sm" + className="h-8 text-xs" />
- handleNestedChange("cardStyle.showTitle", e.target.checked)} - className="rounded border-gray-300" + onCheckedChange={(checked) => handleNestedChange("cardStyle.showTitle", checked)} /> -
- handleNestedChange("cardStyle.showSubtitle", e.target.checked)} - className="rounded border-gray-300" + onCheckedChange={(checked) => handleNestedChange("cardStyle.showSubtitle", checked)} /> -
- handleNestedChange("cardStyle.showDescription", e.target.checked)} - className="rounded border-gray-300" + onCheckedChange={(checked) => handleNestedChange("cardStyle.showDescription", checked)} /> -
- handleNestedChange("cardStyle.showImage", e.target.checked)} - className="rounded border-gray-300" + onCheckedChange={(checked) => handleNestedChange("cardStyle.showImage", checked)} /> -
- handleNestedChange("cardStyle.showActions", e.target.checked)} - className="rounded border-gray-300" + onCheckedChange={(checked) => handleNestedChange("cardStyle.showActions", checked)} /> -
- {/* 개별 버튼 설정 (액션 버튼이 활성화된 경우에만 표시) */} + {/* 개별 버튼 설정 */} {(config.cardStyle?.showActions ?? true) && ( -
+
- handleNestedChange("cardStyle.showViewButton", e.target.checked)} - className="rounded border-gray-300" + onCheckedChange={(checked) => handleNestedChange("cardStyle.showViewButton", checked)} /> -
- handleNestedChange("cardStyle.showEditButton", e.target.checked)} - className="rounded border-gray-300" + onCheckedChange={(checked) => handleNestedChange("cardStyle.showEditButton", checked)} /> -
- handleNestedChange("cardStyle.showDeleteButton", e.target.checked)} - className="rounded border-gray-300" + onCheckedChange={(checked) => handleNestedChange("cardStyle.showDeleteButton", checked)} /> -
)}
-
- - + + handleNestedChange("cardStyle.maxDescriptionLength", parseInt(e.target.value))} - className="w-full rounded border border-gray-300 px-2 py-1 text-sm" + className="h-8 text-xs" />
{/* 공통 설정 */}
-
공통 설정
+
공통 설정
- handleChange("disabled", e.target.checked)} - className="rounded border-gray-300" + onCheckedChange={(checked) => handleChange("disabled", checked)} /> -
- handleChange("readonly", e.target.checked)} - className="rounded border-gray-300" + onCheckedChange={(checked) => handleChange("readonly", checked)} /> -