diff --git a/backend-node/src/controllers/categoryValueCascadingController.ts b/backend-node/src/controllers/categoryValueCascadingController.ts index 41ac330e..66250bf9 100644 --- a/backend-node/src/controllers/categoryValueCascadingController.ts +++ b/backend-node/src/controllers/categoryValueCascadingController.ts @@ -76,7 +76,9 @@ export const getCategoryValueCascadingGroups = async ( data: result.rows, }); } catch (error: any) { - logger.error("카테고리 값 연쇄관계 그룹 목록 조회 실패", { error: error.message }); + logger.error("카테고리 값 연쇄관계 그룹 목록 조회 실패", { + error: error.message, + }); return res.status(500).json({ success: false, message: "카테고리 값 연쇄관계 그룹 목록 조회에 실패했습니다.", @@ -175,7 +177,9 @@ export const getCategoryValueCascadingGroupById = async ( }, }); } catch (error: any) { - logger.error("카테고리 값 연쇄관계 그룹 상세 조회 실패", { error: error.message }); + logger.error("카테고리 값 연쇄관계 그룹 상세 조회 실패", { + error: error.message, + }); return res.status(500).json({ success: false, message: "카테고리 값 연쇄관계 그룹 조회에 실패했습니다.", @@ -240,7 +244,9 @@ export const getCategoryValueCascadingByCode = async ( data: result.rows[0], }); } catch (error: any) { - logger.error("카테고리 값 연쇄관계 코드 조회 실패", { error: error.message }); + logger.error("카테고리 값 연쇄관계 코드 조회 실패", { + error: error.message, + }); return res.status(500).json({ success: false, message: "카테고리 값 연쇄관계 조회에 실패했습니다.", @@ -277,7 +283,14 @@ export const createCategoryValueCascadingGroup = async ( } = req.body; // 필수 필드 검증 - if (!relationCode || !relationName || !parentTableName || !parentColumnName || !childTableName || !childColumnName) { + if ( + !relationCode || + !relationName || + !parentTableName || + !parentColumnName || + !childTableName || + !childColumnName + ) { return res.status(400).json({ success: false, message: "필수 필드가 누락되었습니다.", @@ -352,7 +365,9 @@ export const createCategoryValueCascadingGroup = async ( message: "카테고리 값 연쇄관계 그룹이 생성되었습니다.", }); } catch (error: any) { - logger.error("카테고리 값 연쇄관계 그룹 생성 실패", { error: error.message }); + logger.error("카테고리 값 연쇄관계 그룹 생성 실패", { + error: error.message, + }); return res.status(500).json({ success: false, message: "카테고리 값 연쇄관계 그룹 생성에 실패했습니다.", @@ -403,7 +418,11 @@ export const updateCategoryValueCascadingGroup = async ( } const existingCompanyCode = existingCheck.rows[0].company_code; - if (companyCode !== "*" && existingCompanyCode !== companyCode && existingCompanyCode !== "*") { + if ( + companyCode !== "*" && + existingCompanyCode !== companyCode && + existingCompanyCode !== "*" + ) { return res.status(403).json({ success: false, message: "수정 권한이 없습니다.", @@ -440,7 +459,11 @@ export const updateCategoryValueCascadingGroup = async ( childTableName, childColumnName, childMenuObjid, - clearOnParentChange !== undefined ? (clearOnParentChange ? "Y" : "N") : null, + clearOnParentChange !== undefined + ? clearOnParentChange + ? "Y" + : "N" + : null, showGroupLabel !== undefined ? (showGroupLabel ? "Y" : "N") : null, emptyParentMessage, noOptionsMessage, @@ -461,7 +484,9 @@ export const updateCategoryValueCascadingGroup = async ( message: "카테고리 값 연쇄관계 그룹이 수정되었습니다.", }); } catch (error: any) { - logger.error("카테고리 값 연쇄관계 그룹 수정 실패", { error: error.message }); + logger.error("카테고리 값 연쇄관계 그룹 수정 실패", { + error: error.message, + }); return res.status(500).json({ success: false, message: "카테고리 값 연쇄관계 그룹 수정에 실패했습니다.", @@ -496,7 +521,11 @@ export const deleteCategoryValueCascadingGroup = async ( } const existingCompanyCode = existingCheck.rows[0].company_code; - if (companyCode !== "*" && existingCompanyCode !== companyCode && existingCompanyCode !== "*") { + if ( + companyCode !== "*" && + existingCompanyCode !== companyCode && + existingCompanyCode !== "*" + ) { return res.status(403).json({ success: false, message: "삭제 권한이 없습니다.", @@ -522,7 +551,9 @@ export const deleteCategoryValueCascadingGroup = async ( message: "카테고리 값 연쇄관계 그룹이 삭제되었습니다.", }); } catch (error: any) { - logger.error("카테고리 값 연쇄관계 그룹 삭제 실패", { error: error.message }); + logger.error("카테고리 값 연쇄관계 그룹 삭제 실패", { + error: error.message, + }); return res.status(500).json({ success: false, message: "카테고리 값 연쇄관계 그룹 삭제에 실패했습니다.", @@ -620,7 +651,9 @@ export const saveCategoryValueCascadingMappings = async ( client.release(); } } catch (error: any) { - logger.error("카테고리 값 연쇄관계 매핑 저장 실패", { error: error.message }); + logger.error("카테고리 값 연쇄관계 매핑 저장 실패", { + error: error.message, + }); return res.status(500).json({ success: false, message: "카테고리 값 연쇄관계 매핑 저장에 실패했습니다.", @@ -649,12 +682,15 @@ export const getCategoryValueCascadingOptions = async ( // 다중 부모값 파싱 let parentValueArray: string[] = []; - + if (parentValues) { if (Array.isArray(parentValues)) { - parentValueArray = parentValues.map(v => String(v)); + parentValueArray = parentValues.map((v) => String(v)); } else { - parentValueArray = String(parentValues).split(',').map(v => v.trim()).filter(v => v); + parentValueArray = String(parentValues) + .split(",") + .map((v) => v.trim()) + .filter((v) => v); } } else if (parentValue) { parentValueArray = [String(parentValue)]; @@ -696,8 +732,10 @@ export const getCategoryValueCascadingOptions = async ( const group = groupResult.rows[0]; // 매핑된 자식 값 조회 (다중 부모값에 대해 IN 절 사용) - const placeholders = parentValueArray.map((_, idx) => `$${idx + 2}`).join(', '); - + const placeholders = parentValueArray + .map((_, idx) => `$${idx + 2}`) + .join(", "); + const optionsQuery = ` SELECT DISTINCT child_value_code as value, @@ -712,7 +750,10 @@ export const getCategoryValueCascadingOptions = async ( ORDER BY parent_value_code, display_order, child_value_label `; - const optionsResult = await pool.query(optionsQuery, [group.group_id, ...parentValueArray]); + const optionsResult = await pool.query(optionsQuery, [ + group.group_id, + ...parentValueArray, + ]); logger.info("카테고리 값 연쇄 옵션 조회", { relationCode: code, @@ -723,7 +764,7 @@ export const getCategoryValueCascadingOptions = async ( return res.json({ success: true, data: optionsResult.rows, - showGroupLabel: group.show_group_label === 'Y', + showGroupLabel: group.show_group_label === "Y", }); } catch (error: any) { logger.error("카테고리 값 연쇄 옵션 조회 실패", { error: error.message }); @@ -789,7 +830,10 @@ export const getCategoryValueCascadingParentOptions = async ( AND is_active = true `; - const optionsParams: any[] = [group.parent_table_name, group.parent_column_name]; + const optionsParams: any[] = [ + group.parent_table_name, + group.parent_column_name, + ]; let paramIndex = 3; // 메뉴 스코프 적용 @@ -884,7 +928,10 @@ export const getCategoryValueCascadingChildOptions = async ( AND is_active = true `; - const optionsParams: any[] = [group.child_table_name, group.child_column_name]; + const optionsParams: any[] = [ + group.child_table_name, + group.child_column_name, + ]; let paramIndex = 3; // 메뉴 스코프 적용 @@ -925,3 +972,91 @@ export const getCategoryValueCascadingChildOptions = async ( } }; +/** + * 테이블명으로 해당 테이블의 모든 연쇄관계 매핑 조회 + * (테이블 목록에서 코드→라벨 변환에 사용) + */ +export const getCategoryValueCascadingMappingsByTable = async ( + req: AuthenticatedRequest, + res: Response +) => { + try { + const { tableName } = req.params; + const companyCode = req.user?.companyCode || "*"; + + if (!tableName) { + return res.status(400).json({ + success: false, + message: "테이블명이 필요합니다.", + }); + } + + // 해당 테이블이 자식 테이블인 연쇄관계 그룹 찾기 + let groupQuery = ` + SELECT + group_id, + relation_code, + child_column_name + FROM category_value_cascading_group + WHERE child_table_name = $1 + AND is_active = 'Y' + `; + const groupParams: any[] = [tableName]; + let paramIndex = 2; + + // 멀티테넌시 적용 + if (companyCode !== "*") { + groupQuery += ` AND (company_code = $${paramIndex} OR company_code = '*')`; + groupParams.push(companyCode); + } + + const groupResult = await pool.query(groupQuery, groupParams); + + if (groupResult.rowCount === 0) { + // 연쇄관계가 없으면 빈 객체 반환 + return res.json({ + success: true, + data: {}, + }); + } + + // 각 그룹의 매핑 조회 + const mappings: Record> = {}; + + for (const group of groupResult.rows) { + const mappingQuery = ` + SELECT DISTINCT + child_value_code as code, + child_value_label as label + FROM category_value_cascading_mapping + WHERE group_id = $1 + AND is_active = 'Y' + ORDER BY child_value_label + `; + + const mappingResult = await pool.query(mappingQuery, [group.group_id]); + + if (mappingResult.rowCount && mappingResult.rowCount > 0) { + mappings[group.child_column_name] = mappingResult.rows; + } + } + + logger.info("테이블별 연쇄관계 매핑 조회", { + tableName, + groupCount: groupResult.rowCount, + columnMappings: Object.keys(mappings), + }); + + return res.json({ + success: true, + data: mappings, + }); + } catch (error: any) { + logger.error("테이블별 연쇄관계 매핑 조회 실패", { error: error.message }); + return res.status(500).json({ + success: false, + message: "연쇄관계 매핑 조회에 실패했습니다.", + error: error.message, + }); + } +}; diff --git a/backend-node/src/routes/categoryValueCascadingRoutes.ts b/backend-node/src/routes/categoryValueCascadingRoutes.ts index d8919627..894da819 100644 --- a/backend-node/src/routes/categoryValueCascadingRoutes.ts +++ b/backend-node/src/routes/categoryValueCascadingRoutes.ts @@ -10,6 +10,7 @@ import { getCategoryValueCascadingOptions, getCategoryValueCascadingParentOptions, getCategoryValueCascadingChildOptions, + getCategoryValueCascadingMappingsByTable, } from "../controllers/categoryValueCascadingController"; import { authenticateToken } from "../middleware/authMiddleware"; @@ -60,5 +61,14 @@ router.get("/child-options/:code", getCategoryValueCascadingChildOptions); // 연쇄 옵션 조회 (부모 값 기반 자식 옵션) router.get("/options/:code", getCategoryValueCascadingOptions); -export default router; +// ============================================ +// 테이블별 매핑 조회 (테이블 목록 표시용) +// ============================================ +// 테이블명으로 해당 테이블의 모든 연쇄관계 매핑 조회 +router.get( + "/table/:tableName/mappings", + getCategoryValueCascadingMappingsByTable +); + +export default router; diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 171c65bb..7ac521af 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -209,7 +209,7 @@ export interface TableListComponentProps { onConfigChange?: (config: any) => void; refreshKey?: number; // 탭 관련 정보 (탭 내부의 테이블에서 사용) - parentTabId?: string; // 부모 탭 ID + parentTabId?: string; // 부모 탭 ID parentTabsComponentId?: string; // 부모 탭 컴포넌트 ID } @@ -689,7 +689,7 @@ export const TableListComponent: React.FC = ({ const [viewMode, setViewMode] = useState<"table" | "card" | "grouped-card">("table"); // 체크박스 컬럼은 항상 기본 틀고정 const [frozenColumns, setFrozenColumns] = useState( - (tableConfig.checkbox?.enabled ?? true) ? ["__checkbox__"] : [] + (tableConfig.checkbox?.enabled ?? true) ? ["__checkbox__"] : [], ); const [frozenColumnCount, setFrozenColumnCount] = useState(0); @@ -1311,17 +1311,15 @@ export const TableListComponent: React.FC = ({ const parts = columnName.split("."); targetTable = parts[0]; // 조인된 테이블명 (예: item_info) targetColumn = parts[1]; // 실제 컬럼명 (예: material) - console.log(`🔗 [TableList] 엔티티 조인 컬럼 감지:`, { + console.log("🔗 [TableList] 엔티티 조인 컬럼 감지:", { originalColumn: columnName, targetTable, targetColumn, }); } - const response = await apiClient.get(`/table-categories/${targetTable}/${targetColumn}/values`); - if (response.data.success && response.data.data && Array.isArray(response.data.data)) { const mapping: Record = {}; @@ -1376,7 +1374,6 @@ export const TableListComponent: React.FC = ({ col.columnName, })) || []; - // 조인 테이블별로 그룹화 const joinedTableColumns: Record = {}; @@ -1408,7 +1405,6 @@ export const TableListComponent: React.FC = ({ }); } - // 조인된 테이블별로 inputType 정보 가져오기 const newJoinedColumnMeta: Record = {}; @@ -1471,6 +1467,41 @@ export const TableListComponent: React.FC = ({ console.log("✅ [TableList] 조인 컬럼 메타데이터 설정:", newJoinedColumnMeta); } + // 🆕 카테고리 연쇄관계 매핑 로드 (category_value_cascading_mapping) + try { + const cascadingResponse = await apiClient.get( + `/category-value-cascading/table/${tableConfig.selectedTable}/mappings`, + ); + if (cascadingResponse.data.success && cascadingResponse.data.data) { + const cascadingMappings = cascadingResponse.data.data; + + // 각 자식 컬럼에 대한 매핑 추가 + for (const [columnName, columnMappings] of Object.entries( + cascadingMappings as Record>, + )) { + if (!mappings[columnName]) { + mappings[columnName] = {}; + } + // 연쇄관계 매핑 추가 + for (const item of columnMappings) { + mappings[columnName][item.code] = { + label: item.label, + color: undefined, // 연쇄관계는 색상 없음 + }; + } + } + console.log("✅ [TableList] 카테고리 연쇄관계 매핑 로드 완료:", { + tableName: tableConfig.selectedTable, + cascadingColumns: Object.keys(cascadingMappings), + }); + } + } catch (cascadingError: any) { + // 연쇄관계 매핑이 없는 경우 무시 (404 등) + if (cascadingError?.response?.status !== 404) { + console.warn("⚠️ [TableList] 카테고리 연쇄관계 매핑 로드 실패:", cascadingError?.message); + } + } + if (Object.keys(mappings).length > 0) { setCategoryMappings(mappings); setCategoryMappingsKey((prev) => prev + 1); @@ -1495,7 +1526,6 @@ export const TableListComponent: React.FC = ({ // ======================================== const fetchTableDataInternal = useCallback(async () => { - if (!tableConfig.selectedTable || isDesignMode) { setData([]); setTotalPages(0); @@ -1514,11 +1544,10 @@ export const TableListComponent: React.FC = ({ const search = searchTerm || undefined; // 🆕 연결 필터 값 가져오기 (분할 패널 내부일 때) - let linkedFilterValues: Record = {}; + const linkedFilterValues: Record = {}; let hasLinkedFiltersConfigured = false; // 연결 필터가 설정되어 있는지 여부 let hasSelectedLeftData = false; // 좌측에서 데이터가 선택되었는지 여부 - if (splitPanelContext) { // 연결 필터 설정 여부 확인 (현재 테이블에 해당하는 필터가 있는지) const linkedFiltersConfig = splitPanelContext.linkedFilters || []; @@ -1609,7 +1638,7 @@ export const TableListComponent: React.FC = ({ } // 🆕 RelatedDataButtons 필터 값 준비 - let relatedButtonFilterValues: Record = {}; + const relatedButtonFilterValues: Record = {}; if (relatedButtonFilter) { relatedButtonFilterValues[relatedButtonFilter.filterColumn] = { value: relatedButtonFilter.filterValue, @@ -1685,7 +1714,6 @@ export const TableListComponent: React.FC = ({ }; }); - // 🆕 제외 필터 처리 (다른 테이블에 이미 존재하는 데이터 제외) let excludeFilterParam: any = undefined; if (tableConfig.excludeFilter?.enabled) { @@ -2427,7 +2455,7 @@ export const TableListComponent: React.FC = ({ try { const { apiClient } = await import("@/lib/api/client"); - await apiClient.put(`/dynamic-form/update-field`, { + await apiClient.put("/dynamic-form/update-field", { tableName: tableConfig.selectedTable, keyField: primaryKeyField, keyValue: primaryKeyValue, @@ -2468,7 +2496,7 @@ export const TableListComponent: React.FC = ({ // 모든 변경사항 저장 const savePromises = Array.from(pendingChanges.values()).map((change) => - apiClient.put(`/dynamic-form/update-field`, { + apiClient.put("/dynamic-form/update-field", { tableName: tableConfig.selectedTable, keyField: primaryKeyField, keyValue: change.primaryKeyValue, @@ -2942,9 +2970,10 @@ export const TableListComponent: React.FC = ({ if (state.frozenColumns) { // 체크박스 컬럼이 항상 포함되도록 보장 const checkboxColumn = (tableConfig.checkbox?.enabled ?? true) ? "__checkbox__" : null; - const restoredFrozenColumns = checkboxColumn && !state.frozenColumns.includes(checkboxColumn) - ? [checkboxColumn, ...state.frozenColumns] - : state.frozenColumns; + const restoredFrozenColumns = + checkboxColumn && !state.frozenColumns.includes(checkboxColumn) + ? [checkboxColumn, ...state.frozenColumns] + : state.frozenColumns; setFrozenColumns(restoredFrozenColumns); } if (state.frozenColumnCount !== undefined) setFrozenColumnCount(state.frozenColumnCount); // 틀고정 컬럼 수 복원 @@ -2956,7 +2985,6 @@ export const TableListComponent: React.FC = ({ }); setHeaderFilters(filters); } - } catch (error) { console.error("❌ 테이블 상태 복원 실패:", error); } @@ -3576,7 +3604,7 @@ export const TableListComponent: React.FC = ({ })); // 배치 업데이트 - await Promise.all(updates.map((update) => apiClient.put(`/dynamic-form/update-field`, update))); + await Promise.all(updates.map((update) => apiClient.put("/dynamic-form/update-field", update))); toast.success("순서가 변경되었습니다."); setRefreshTrigger((prev) => prev + 1); @@ -4894,7 +4922,7 @@ export const TableListComponent: React.FC = ({ useEffect(() => { const handleRelatedButtonSelect = (event: CustomEvent) => { const { targetTable, filterColumn, filterValue } = event.detail || {}; - + // 이 테이블이 대상 테이블인지 확인 if (targetTable === tableConfig.selectedTable) { // filterValue가 null이면 선택 해제 (빈 상태) @@ -4925,9 +4953,9 @@ export const TableListComponent: React.FC = ({ useEffect(() => { if (!isDesignMode) { // relatedButtonFilter가 있으면 데이터 로드, null이면 빈 상태 (setRefreshTrigger로 트리거) - console.log("🔄 [TableList] RelatedDataButtons 상태 변경:", { - relatedButtonFilter, - isRelatedButtonTarget + console.log("🔄 [TableList] RelatedDataButtons 상태 변경:", { + relatedButtonFilter, + isRelatedButtonTarget, }); setRefreshTrigger((prev) => prev + 1); } @@ -5618,7 +5646,7 @@ export const TableListComponent: React.FC = ({ for (let i = 0; i < frozenIndex; i++) { const frozenCol = frozenColumns[i]; // 체크박스 컬럼은 48px 고정 - const frozenColWidth = frozenCol === "__checkbox__" ? 48 : (columnWidths[frozenCol] || 150); + const frozenColWidth = frozenCol === "__checkbox__" ? 48 : columnWidths[frozenCol] || 150; leftPosition += frozenColWidth; } } @@ -5930,7 +5958,8 @@ export const TableListComponent: React.FC = ({ for (let i = 0; i < frozenIndex; i++) { const frozenCol = frozenColumns[i]; // 체크박스 컬럼은 48px 고정 - const frozenColWidth = frozenCol === "__checkbox__" ? 48 : (columnWidths[frozenCol] || 150); + const frozenColWidth = + frozenCol === "__checkbox__" ? 48 : columnWidths[frozenCol] || 150; leftPosition += frozenColWidth; } } @@ -5958,7 +5987,7 @@ export const TableListComponent: React.FC = ({ : `${100 / visibleColumns.length}%`, minWidth: column.columnName === "__checkbox__" ? "48px" : undefined, maxWidth: column.columnName === "__checkbox__" ? "48px" : undefined, - ...(isFrozen && { + ...(isFrozen && { left: `${leftPosition}px`, backgroundColor: "hsl(var(--background))", }), @@ -6094,7 +6123,8 @@ export const TableListComponent: React.FC = ({ for (let i = 0; i < frozenIndex; i++) { const frozenCol = frozenColumns[i]; // 체크박스 컬럼은 48px 고정 - const frozenColWidth = frozenCol === "__checkbox__" ? 48 : (columnWidths[frozenCol] || 150); + const frozenColWidth = + frozenCol === "__checkbox__" ? 48 : columnWidths[frozenCol] || 150; leftPosition += frozenColWidth; } } @@ -6134,7 +6164,7 @@ export const TableListComponent: React.FC = ({ column.columnName === "__checkbox__" ? "48px" : `${100 / visibleColumns.length}%`, minWidth: column.columnName === "__checkbox__" ? "48px" : undefined, maxWidth: column.columnName === "__checkbox__" ? "48px" : undefined, - ...(isFrozen && { + ...(isFrozen && { left: `${leftPosition}px`, backgroundColor: "hsl(var(--background))", }), @@ -6259,7 +6289,7 @@ export const TableListComponent: React.FC = ({ for (let i = 0; i < frozenIndex; i++) { const frozenCol = frozenColumns[i]; // 체크박스 컬럼은 48px 고정 - const frozenColWidth = frozenCol === "__checkbox__" ? 48 : (columnWidths[frozenCol] || 150); + const frozenColWidth = frozenCol === "__checkbox__" ? 48 : columnWidths[frozenCol] || 150; leftPosition += frozenColWidth; } } @@ -6284,7 +6314,7 @@ export const TableListComponent: React.FC = ({ : columnWidth ? `${columnWidth}px` : undefined, - ...(isFrozen && { + ...(isFrozen && { left: `${leftPosition}px`, backgroundColor: "hsl(var(--muted) / 0.8)", }),