연쇄관계 자식 라벨표시

This commit is contained in:
kjs 2025-12-18 15:16:34 +09:00
parent bca6de9811
commit cf8a5a3d93
3 changed files with 226 additions and 51 deletions

View File

@ -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<string, Array<{ code: string; label: string }>> = {};
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,
});
}
};

View File

@ -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;

View File

@ -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<TableListComponentProps> = ({
const [viewMode, setViewMode] = useState<"table" | "card" | "grouped-card">("table");
// 체크박스 컬럼은 항상 기본 틀고정
const [frozenColumns, setFrozenColumns] = useState<string[]>(
(tableConfig.checkbox?.enabled ?? true) ? ["__checkbox__"] : []
(tableConfig.checkbox?.enabled ?? true) ? ["__checkbox__"] : [],
);
const [frozenColumnCount, setFrozenColumnCount] = useState<number>(0);
@ -1311,17 +1311,15 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
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<string, { label: string; color?: string }> = {};
@ -1376,7 +1374,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
col.columnName,
})) || [];
// 조인 테이블별로 그룹화
const joinedTableColumns: Record<string, { columnName: string; actualColumn: string }[]> = {};
@ -1408,7 +1405,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
});
}
// 조인된 테이블별로 inputType 정보 가져오기
const newJoinedColumnMeta: Record<string, { webType?: string; codeCategory?: string; inputType?: string }> = {};
@ -1471,6 +1467,41 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
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<string, Array<{ code: string; label: string }>>,
)) {
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<TableListComponentProps> = ({
// ========================================
const fetchTableDataInternal = useCallback(async () => {
if (!tableConfig.selectedTable || isDesignMode) {
setData([]);
setTotalPages(0);
@ -1514,11 +1544,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const search = searchTerm || undefined;
// 🆕 연결 필터 값 가져오기 (분할 패널 내부일 때)
let linkedFilterValues: Record<string, any> = {};
const linkedFilterValues: Record<string, any> = {};
let hasLinkedFiltersConfigured = false; // 연결 필터가 설정되어 있는지 여부
let hasSelectedLeftData = false; // 좌측에서 데이터가 선택되었는지 여부
if (splitPanelContext) {
// 연결 필터 설정 여부 확인 (현재 테이블에 해당하는 필터가 있는지)
const linkedFiltersConfig = splitPanelContext.linkedFilters || [];
@ -1609,7 +1638,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}
// 🆕 RelatedDataButtons 필터 값 준비
let relatedButtonFilterValues: Record<string, any> = {};
const relatedButtonFilterValues: Record<string, any> = {};
if (relatedButtonFilter) {
relatedButtonFilterValues[relatedButtonFilter.filterColumn] = {
value: relatedButtonFilter.filterValue,
@ -1685,7 +1714,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
};
});
// 🆕 제외 필터 처리 (다른 테이블에 이미 존재하는 데이터 제외)
let excludeFilterParam: any = undefined;
if (tableConfig.excludeFilter?.enabled) {
@ -2427,7 +2455,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
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<TableListComponentProps> = ({
// 모든 변경사항 저장
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<TableListComponentProps> = ({
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<TableListComponentProps> = ({
});
setHeaderFilters(filters);
}
} catch (error) {
console.error("❌ 테이블 상태 복원 실패:", error);
}
@ -3576,7 +3604,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}));
// 배치 업데이트
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<TableListComponentProps> = ({
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<TableListComponentProps> = ({
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<TableListComponentProps> = ({
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<TableListComponentProps> = ({
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<TableListComponentProps> = ({
: `${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<TableListComponentProps> = ({
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<TableListComponentProps> = ({
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<TableListComponentProps> = ({
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<TableListComponentProps> = ({
: columnWidth
? `${columnWidth}px`
: undefined,
...(isFrozen && {
...(isFrozen && {
left: `${leftPosition}px`,
backgroundColor: "hsl(var(--muted) / 0.8)",
}),