diff --git a/backend-node/src/controllers/screenGroupController.ts b/backend-node/src/controllers/screenGroupController.ts index 6d3564a6..518de7e8 100644 --- a/backend-node/src/controllers/screenGroupController.ts +++ b/backend-node/src/controllers/screenGroupController.ts @@ -603,7 +603,7 @@ export const deleteFieldJoin = async (req: Request, res: Response) => { export const getDataFlows = async (req: Request, res: Response) => { try { const companyCode = (req.user as any).companyCode; - const { group_id } = req.query; + const { group_id, source_screen_id } = req.query; let query = ` SELECT sdf.*, @@ -631,6 +631,13 @@ export const getDataFlows = async (req: Request, res: Response) => { paramIndex++; } + // 특정 화면에서 시작하는 데이터 흐름만 조회 + if (source_screen_id) { + query += ` AND sdf.source_screen_id = $${paramIndex}`; + params.push(source_screen_id); + paramIndex++; + } + query += " ORDER BY sdf.id ASC"; const result = await pool.query(query, params); diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index b49c78dc..c3947edd 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -22,7 +22,7 @@ type ViewMode = "tree" | "table"; export default function ScreenManagementPage() { const [currentStep, setCurrentStep] = useState("list"); const [selectedScreen, setSelectedScreen] = useState(null); - const [selectedGroup, setSelectedGroup] = useState<{ id: number; name: string } | null>(null); + const [selectedGroup, setSelectedGroup] = useState<{ id: number; name: string; company_code?: string } | null>(null); const [focusedScreenIdInGroup, setFocusedScreenIdInGroup] = useState(null); const [stepHistory, setStepHistory] = useState(["list"]); const [viewMode, setViewMode] = useState("tree"); diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index dffbd75b..99634357 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -32,9 +32,15 @@ function ScreenViewPage() { // URL 쿼리에서 menuObjid 가져오기 (메뉴 스코프) const menuObjid = searchParams.get("menuObjid") ? parseInt(searchParams.get("menuObjid")!) : undefined; + + // URL 쿼리에서 프리뷰용 company_code 가져오기 + const previewCompanyCode = searchParams.get("company_code"); // 🆕 현재 로그인한 사용자 정보 - const { user, userName, companyCode } = useAuth(); + const { user, userName, companyCode: authCompanyCode } = useAuth(); + + // 프리뷰 모드에서는 URL 파라미터의 company_code 우선 사용 + const companyCode = previewCompanyCode || authCompanyCode; // 🆕 모바일 환경 감지 const { isMobile } = useResponsive(); diff --git a/frontend/components/layout/AppLayout.tsx b/frontend/components/layout/AppLayout.tsx index 236071ac..e3e8d920 100644 --- a/frontend/components/layout/AppLayout.tsx +++ b/frontend/components/layout/AppLayout.tsx @@ -302,6 +302,9 @@ function AppLayoutInner({ children }: AppLayoutProps) { // 현재 경로에 따라 어드민 모드인지 판단 (쿼리 파라미터도 고려) const isAdminMode = pathname.startsWith("/admin") || searchParams.get("mode") === "admin"; + // 프리뷰 모드: 사이드바/헤더 없이 화면만 표시 (iframe 임베드용) + const isPreviewMode = searchParams.get("preview") === "true"; + // 현재 모드에 따라 표시할 메뉴 결정 // 관리자 모드에서는 관리자 메뉴만, 사용자 모드에서는 사용자 메뉴만 표시 const currentMenus = isAdminMode ? adminMenus : userMenus; @@ -458,6 +461,15 @@ function AppLayoutInner({ children }: AppLayoutProps) { ); } + // 프리뷰 모드: 사이드바/헤더 없이 화면만 표시 + if (isPreviewMode) { + return ( +
+ {children} +
+ ); + } + // UI 변환된 메뉴 데이터 const uiMenus = convertMenuToUI(currentMenus, user as ExtendedUserInfo); diff --git a/frontend/components/screen/ScreenGroupTreeView.tsx b/frontend/components/screen/ScreenGroupTreeView.tsx index 49fbc23f..6fa7314b 100644 --- a/frontend/components/screen/ScreenGroupTreeView.tsx +++ b/frontend/components/screen/ScreenGroupTreeView.tsx @@ -80,8 +80,8 @@ interface ScreenGroupTreeViewProps { selectedScreen: ScreenDefinition | null; onScreenSelect: (screen: ScreenDefinition) => void; onScreenDesign: (screen: ScreenDefinition) => void; - onGroupSelect?: (group: { id: number; name: string } | null) => void; - onScreenSelectInGroup?: (group: { id: number; name: string }, screenId: number) => void; + onGroupSelect?: (group: { id: number; name: string; company_code?: string } | null) => void; + onScreenSelectInGroup?: (group: { id: number; name: string; company_code?: string }, screenId: number) => void; companyCode?: string; } @@ -177,7 +177,7 @@ export function ScreenGroupTreeView({ if (onGroupSelect && groupId !== "ungrouped") { const group = groups.find((g) => String(g.id) === groupId); if (group) { - onGroupSelect({ id: group.id, name: group.group_name }); + onGroupSelect({ id: group.id, name: group.group_name, company_code: group.company_code }); } } } @@ -192,7 +192,7 @@ export function ScreenGroupTreeView({ const handleScreenClickInGroup = (screen: ScreenDefinition, group: ScreenGroup) => { if (onScreenSelectInGroup) { onScreenSelectInGroup( - { id: group.id, name: group.group_name }, + { id: group.id, name: group.group_name, company_code: group.company_code }, screen.screenId ); } else { diff --git a/frontend/components/screen/ScreenNode.tsx b/frontend/components/screen/ScreenNode.tsx index e49aa470..e49bf6d8 100644 --- a/frontend/components/screen/ScreenNode.tsx +++ b/frontend/components/screen/ScreenNode.tsx @@ -39,6 +39,7 @@ export interface FieldMappingDisplay { targetField: string; // 서브 테이블 컬럼 (예: user_id) sourceDisplayName?: string; // 메인 테이블 한글 컬럼명 (예: 담당자) targetDisplayName?: string; // 서브 테이블 한글 컬럼명 (예: 사용자ID) + sourceTable?: string; // 소스 테이블명 (필드 매핑에서 테이블 구분용) } // 참조 관계 정보 (다른 테이블에서 이 테이블을 참조하는 경우) diff --git a/frontend/components/screen/ScreenRelationFlow.tsx b/frontend/components/screen/ScreenRelationFlow.tsx index addebb35..f3b14d56 100644 --- a/frontend/components/screen/ScreenRelationFlow.tsx +++ b/frontend/components/screen/ScreenRelationFlow.tsx @@ -59,7 +59,7 @@ const NODE_GAP = 40; // 노드 간격 interface ScreenRelationFlowProps { screen: ScreenDefinition | null; - selectedGroup?: { id: number; name: string } | null; + selectedGroup?: { id: number; name: string; company_code?: string } | null; initialFocusedScreenId?: number | null; } @@ -97,6 +97,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId screenName: string; tableName?: string; tableLabel?: string; + companyCode?: string; // 프리뷰용 회사 코드 // 기존 설정 정보 (화면 디자이너에서 추출) existingConfig?: { joinColumnRefs?: Array<{ @@ -1114,6 +1115,32 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId }; }); + // 화면의 모든 서브 테이블에서 fieldMappings 추출 + const screenSubTablesData = subTablesDataMap[screenId]; + const allFieldMappings: Array<{ + targetField: string; + sourceField: string; + sourceTable?: string; + sourceDisplayName?: string; + componentType?: string; + }> = []; + + if (screenSubTablesData?.subTables) { + screenSubTablesData.subTables.forEach((subTable) => { + if (subTable.fieldMappings) { + subTable.fieldMappings.forEach((mapping) => { + allFieldMappings.push({ + targetField: mapping.targetField, + sourceField: mapping.sourceField, + sourceTable: mapping.sourceTable || subTable.tableName, + sourceDisplayName: mapping.sourceDisplayName, + componentType: subTable.relationType, + }); + }); + } + }); + } + setSettingModalNode({ nodeType: "screen", nodeId: node.id, @@ -1121,10 +1148,12 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId screenName: nodeData.label || `화면 ${screenId}`, tableName: mainTable, tableLabel: nodeData.subLabel, + companyCode: selectedGroup?.company_code, // 프리뷰용 회사 코드 // 화면의 테이블 정보 전달 existingConfig: { mainTable: mainTable, filterTables: filterTables, + fieldMappings: allFieldMappings, }, }); setIsSettingModalOpen(true); @@ -2232,6 +2261,7 @@ function ScreenRelationFlowInner({ screen, selectedGroup, initialFocusedScreenId screenId={settingModalNode.screenId} screenName={settingModalNode.screenName} groupId={selectedGroup?.id} + companyCode={settingModalNode.companyCode} mainTable={settingModalNode.existingConfig?.mainTable} mainTableLabel={settingModalNode.tableLabel} filterTables={settingModalNode.existingConfig?.filterTables} diff --git a/frontend/components/screen/ScreenSettingModal.tsx b/frontend/components/screen/ScreenSettingModal.tsx index 4e72a432..6b6dea5f 100644 --- a/frontend/components/screen/ScreenSettingModal.tsx +++ b/frontend/components/screen/ScreenSettingModal.tsx @@ -55,6 +55,9 @@ import { Table2, ArrowRight, Settings2, + ChevronDown, + ChevronRight, + Filter, } from "lucide-react"; import { getDataFlows, @@ -62,6 +65,8 @@ import { updateDataFlow, deleteDataFlow, DataFlow, + getMultipleScreenLayoutSummary, + LayoutItem, } from "@/lib/api/screenGroup"; import { tableManagementApi, ColumnTypeInfo, TableInfo } from "@/lib/api/tableManagement"; @@ -95,6 +100,7 @@ interface ScreenSettingModalProps { screenId: number; screenName: string; groupId?: number; + companyCode?: string; // 프리뷰용 회사 코드 mainTable?: string; mainTableLabel?: string; filterTables?: FilterTableInfo[]; @@ -199,6 +205,7 @@ export function ScreenSettingModal({ screenId, screenName, groupId, + companyCode, mainTable, mainTableLabel, filterTables = [], @@ -209,6 +216,7 @@ export function ScreenSettingModal({ const [activeTab, setActiveTab] = useState("overview"); const [loading, setLoading] = useState(false); const [dataFlows, setDataFlows] = useState([]); + const [layoutItems, setLayoutItems] = useState([]); // 데이터 로드 const loadData = useCallback(async () => { @@ -216,11 +224,18 @@ export function ScreenSettingModal({ setLoading(true); try { - // 데이터 흐름 로드 - const flowsResponse = await getDataFlows(screenId); + // 1. 해당 화면에서 시작하는 데이터 흐름 로드 + const flowsResponse = await getDataFlows({ sourceScreenId: screenId }); if (flowsResponse.success && flowsResponse.data) { setDataFlows(flowsResponse.data); } + + // 2. 화면 레이아웃 요약 정보 로드 (컴포넌트 컬럼 정보 포함) + const layoutResponse = await getMultipleScreenLayoutSummary([screenId]); + if (layoutResponse.success && layoutResponse.data) { + const screenLayout = layoutResponse.data[screenId]; + setLayoutItems(screenLayout?.layoutItems || []); + } } catch (error) { console.error("데이터 로드 실패:", error); } finally { @@ -299,6 +314,7 @@ export function ScreenSettingModal({ fieldMappings={fieldMappings} componentCount={componentCount} dataFlows={dataFlows} + layoutItems={layoutItems} loading={loading} /> @@ -309,6 +325,7 @@ export function ScreenSettingModal({ screenId={screenId} mainTable={mainTable} fieldMappings={fieldMappings} + layoutItems={layoutItems} loading={loading} /> @@ -327,7 +344,7 @@ export function ScreenSettingModal({ {/* 탭 4: 화면 프리뷰 */} - + @@ -335,6 +352,182 @@ export function ScreenSettingModal({ ); } +// ============================================================ +// 필터 테이블 아코디언 컴포넌트 +// ============================================================ + +interface FilterTableAccordionProps { + filterTable: FilterTableInfo; + mainTable?: string; +} + +function FilterTableAccordion({ filterTable: ft, mainTable }: FilterTableAccordionProps) { + const [isOpen, setIsOpen] = useState(false); + const [columns, setColumns] = useState([]); + const [loadingColumns, setLoadingColumns] = useState(false); + + const hasJoinRefs = ft.joinColumnRefs && ft.joinColumnRefs.length > 0; + const hasFilterColumns = ft.filterColumns && ft.filterColumns.length > 0; + + // 아코디언 열릴 때 테이블 컬럼 로드 + const handleToggle = async () => { + const newIsOpen = !isOpen; + setIsOpen(newIsOpen); + + // 처음 열릴 때 컬럼 로드 + if (newIsOpen && columns.length === 0 && ft.tableName) { + setLoadingColumns(true); + try { + const result = await tableManagementApi.getColumnList(ft.tableName); + if (result.success && result.data && result.data.columns) { + setColumns(result.data.columns); + } + } catch (error) { + console.error("테이블 컬럼 로드 실패:", error); + } finally { + setLoadingColumns(false); + } + } + }; + + return ( +
+ {/* 헤더 - 클릭하면 펼쳐짐 */} + + + {/* 펼쳐진 내용 */} + {isOpen && ( +
+ {/* 필터 키 설명 */} +
+ {ft.tableLabel || ft.tableName}의 데이터를 기준으로 필터링됩니다. +
+ + {/* 테이블 컬럼 정보 */} +
+
+ + 테이블 컬럼 ({loadingColumns ? "로딩중..." : `${columns.length}개`}) +
+ {loadingColumns ? ( +
+ +
+ ) : columns.length > 0 ? ( +
+ {columns.slice(0, 10).map((col, cIdx) => ( +
+ {col.displayName || col.columnName} + + ({col.dataType}) + +
+ ))} + {columns.length > 10 && ( +
+ +{columns.length - 10}개 더 +
+ )} +
+ ) : ( +
+ 컬럼 정보 없음 +
+ )} +
+ + {/* 필터 컬럼 매핑 */} + {hasFilterColumns && ( +
+
+ + 필터 키 매핑 +
+
+ {ft.filterColumns!.map((col, cIdx) => ( +
+ + {mainTable}.{col} + + + + {ft.tableLabel || ft.tableName}.{col} + +
+ ))} +
+
+ )} + + {/* 조인 관계 */} + {hasJoinRefs && ( +
+
+ + 조인 관계 ({ft.joinColumnRefs!.length}개) +
+
+ {ft.joinColumnRefs!.map((join, jIdx) => ( +
+ + {ft.tableLabel || ft.tableName}.{join.column} + + + + {join.refTableLabel || join.refTable}.{join.refColumn} + +
+ ))} +
+
+ )} +
+ )} +
+ ); +} + // ============================================================ // 탭 1: 화면 개요 // ============================================================ @@ -348,6 +541,7 @@ interface OverviewTabProps { fieldMappings: FieldMappingInfo[]; componentCount: number; dataFlows: DataFlow[]; + layoutItems: LayoutItem[]; // 컴포넌트 컬럼 정보 추가 loading: boolean; } @@ -360,9 +554,10 @@ function OverviewTab({ fieldMappings, componentCount, dataFlows, + layoutItems, loading, }: OverviewTabProps) { - // 통계 계산 + // 통계 계산 (layoutItems의 컬럼 수도 포함) const stats = useMemo(() => { const totalJoins = filterTables.reduce( (sum, ft) => sum + (ft.joinColumnRefs?.length || 0), @@ -373,14 +568,23 @@ function OverviewTab({ 0 ); + // layoutItems에서 사용하는 컬럼 수 계산 + const layoutColumnsSet = new Set(); + layoutItems.forEach((item) => { + if (item.usedColumns) { + item.usedColumns.forEach((col) => layoutColumnsSet.add(col)); + } + }); + const layoutColumnCount = layoutColumnsSet.size; + return { tableCount: 1 + filterTables.length, // 메인 + 필터 - fieldCount: fieldMappings.length, + fieldCount: layoutColumnCount > 0 ? layoutColumnCount : fieldMappings.length, joinCount: totalJoins, filterCount: totalFilters, flowCount: dataFlows.length, }; - }, [filterTables, fieldMappings, dataFlows]); + }, [filterTables, fieldMappings, dataFlows, layoutItems]); return (
@@ -434,7 +638,7 @@ function OverviewTab({ )}
- {/* 필터 테이블 */} + {/* 연결된 필터 테이블 (아코디언 형식) */}

@@ -443,63 +647,16 @@ function OverviewTab({ {filterTables.length > 0 ? (
{filterTables.map((ft, idx) => ( -
-
- -
-
{ft.tableLabel || ft.tableName}
- {ft.tableLabel && ft.tableName !== ft.tableLabel && ( -
{ft.tableName}
- )} -
- - 필터 - -
- - {/* 조인 정보 */} - {ft.joinColumnRefs && ft.joinColumnRefs.length > 0 && ( -
-
조인 설정:
- {ft.joinColumnRefs.map((join, jIdx) => ( -
- - {join.column} - - - - {join.refTableLabel || join.refTable}.{join.refColumn} - -
- ))} -
- )} - - {/* 필터 컬럼 */} - {ft.filterColumns && ft.filterColumns.length > 0 && ( -
-
필터 컬럼:
-
- {ft.filterColumns.map((col, cIdx) => ( - - {col} - - ))} -
-
- )} -
+ filterTable={ft} + mainTable={mainTable} + /> ))}
) : (
- 필터 테이블이 없습니다. + 연결된 필터 테이블이 없습니다.
)}

@@ -549,6 +706,7 @@ interface FieldMappingTabProps { screenId: number; mainTable?: string; fieldMappings: FieldMappingInfo[]; + layoutItems: LayoutItem[]; loading: boolean; } @@ -556,9 +714,42 @@ function FieldMappingTab({ screenId, mainTable, fieldMappings, + layoutItems, loading, }: FieldMappingTabProps) { - // 컴포넌트 타입별 그룹핑 + // 화면 컴포넌트에서 사용하는 컬럼 정보 추출 + const componentColumns = useMemo(() => { + const result: Array<{ + componentKind: string; + componentLabel?: string; + columns: string[]; + joinColumns: string[]; + }> = []; + + layoutItems.forEach((item) => { + if (item.usedColumns && item.usedColumns.length > 0) { + result.push({ + componentKind: item.componentKind, + componentLabel: item.label, + columns: item.usedColumns, + joinColumns: item.joinColumns || [], + }); + } + }); + + return result; + }, [layoutItems]); + + // 전체 컬럼 수 계산 + const totalColumns = useMemo(() => { + const allColumns = new Set(); + componentColumns.forEach((comp) => { + comp.columns.forEach((col) => allColumns.add(col)); + }); + return allColumns.size; + }, [componentColumns]); + + // 컴포넌트 타입별 그룹핑 (기존 fieldMappings용) const groupedMappings = useMemo(() => { const grouped: Record = {}; @@ -584,83 +775,155 @@ function FieldMappingTab({ } return ( -
-
-
-

필드-컬럼 매핑 현황

-

- 화면 필드가 어떤 테이블 컬럼과 연결되어 있는지 확인합니다. -

+
+ {/* 화면 컴포넌트별 컬럼 사용 현황 */} +
+
+
+

화면 컴포넌트별 컬럼 사용 현황

+

+ 각 컴포넌트에서 사용하는 테이블 컬럼을 확인합니다. +

+
+ + 총 {totalColumns}개 컬럼 +
- - 총 {fieldMappings.length}개 필드 - -
- {fieldMappings.length === 0 ? ( -
- -

설정된 필드 매핑이 없습니다.

-
- ) : ( -
- - - - # - 필드명 - 테이블 - 컬럼 - 컴포넌트 타입 - - - - {fieldMappings.map((mapping, idx) => ( - - - {idx + 1} - - - {mapping.targetField} - - - - {mapping.sourceTable || mainTable || "-"} - - - - - {mapping.sourceField} - - - - {mapping.componentType || "-"} - - - ))} - -
-
- )} - - {/* 컴포넌트 타입별 요약 */} - {componentTypes.length > 0 && ( -
-

컴포넌트 타입별 분류

-
- {componentTypes.map((type) => ( - + +

화면 컴포넌트에서 사용하는 컬럼 정보가 없습니다.

+
+ ) : ( +
+ {componentColumns.map((comp, idx) => ( +
- {type} - - {groupedMappings[type].length} - - +
+
+ + + {comp.componentLabel || comp.componentKind} + + + {comp.componentKind} + +
+ + {comp.columns.length}개 컬럼 + +
+
+ {comp.columns.map((col, cIdx) => { + const isJoinColumn = comp.joinColumns.includes(col); + return ( + + {col} + {isJoinColumn && ( + + )} + + ); + })} +
+
))}
+ )} +
+ + {/* 서브 테이블 연결 관계 (기존 fieldMappings) */} + {fieldMappings.length > 0 && ( +
+
+
+

서브 테이블 연결 관계

+

+ 메인 테이블과 서브 테이블 간의 필드 연결 관계입니다. +

+
+ + 총 {fieldMappings.length}개 연결 + +
+ +
+ + + + # + 메인 테이블 컬럼 + + 서브 테이블 + 서브 테이블 컬럼 + 연결 타입 + + + + {fieldMappings.map((mapping, idx) => ( + + + {idx + 1} + + + + {mainTable}.{mapping.targetField} + + + + + + + + {mapping.sourceTable || "-"} + + + + + {mapping.sourceField} + + + + {mapping.componentType || "-"} + + + ))} + +
+
+ + {/* 컴포넌트 타입별 요약 */} + {componentTypes.length > 0 && ( +
+

연결 타입별 분류

+
+ {componentTypes.map((type) => ( + + {type} + + {groupedMappings[type].length} + + + ))} +
+
+ )}
)}
@@ -954,21 +1217,27 @@ function DataFlowTab({ interface PreviewTabProps { screenId: number; screenName: string; + companyCode?: string; } -function PreviewTab({ screenId, screenName }: PreviewTabProps) { +function PreviewTab({ screenId, screenName, companyCode }: PreviewTabProps) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - // 화면 URL 생성 + // 화면 URL 생성 (preview=true로 사이드바 없이 화면만 표시, company_code 전달) const previewUrl = useMemo(() => { // 현재 호스트 기반으로 URL 생성 + const params = new URLSearchParams({ preview: "true" }); + // 프리뷰용 회사 코드 추가 (데이터 조회에 필요) + if (companyCode) { + params.set("company_code", companyCode); + } if (typeof window !== "undefined") { const baseUrl = window.location.origin; - return `${baseUrl}/screens/${screenId}`; + return `${baseUrl}/screens/${screenId}?${params.toString()}`; } - return `/screens/${screenId}`; - }, [screenId]); + return `/screens/${screenId}?${params.toString()}`; + }, [screenId, companyCode]); const handleIframeLoad = () => { setLoading(false); diff --git a/frontend/lib/api/screenGroup.ts b/frontend/lib/api/screenGroup.ts index 3c89d01c..65294444 100644 --- a/frontend/lib/api/screenGroup.ts +++ b/frontend/lib/api/screenGroup.ts @@ -254,10 +254,17 @@ export async function deleteFieldJoin(id: number): Promise> { // 데이터 흐름 (screen_data_flows) API // ============================================================ -export async function getDataFlows(groupId?: number): Promise> { +export async function getDataFlows(params?: { groupId?: number; sourceScreenId?: number }): Promise> { try { - const queryParams = groupId ? `?group_id=${groupId}` : ""; - const response = await apiClient.get(`/screen-groups/data-flows${queryParams}`); + const queryParts: string[] = []; + if (params?.groupId) { + queryParts.push(`group_id=${params.groupId}`); + } + if (params?.sourceScreenId) { + queryParts.push(`source_screen_id=${params.sourceScreenId}`); + } + const queryString = queryParts.length > 0 ? `?${queryParts.join("&")}` : ""; + const response = await apiClient.get(`/screen-groups/data-flows${queryString}`); return response.data; } catch (error: any) { return { success: false, error: error.message }; @@ -403,9 +410,11 @@ export interface FieldMappingInfo { // 서브 테이블 정보 타입 export interface SubTableInfo { tableName: string; + tableLabel?: string; // 테이블 한글명 componentType: string; relationType: 'lookup' | 'source' | 'join' | 'reference' | 'parentMapping' | 'rightPanelRelation'; fieldMappings?: FieldMappingInfo[]; + filterColumns?: string[]; // 필터링에 사용되는 컬럼 목록 // rightPanelRelation에서 추가 정보 (관계 유형 추론용) originalRelationType?: 'join' | 'detail'; // 원본 relation.type foreignKey?: string; // 디테일 테이블의 FK 컬럼