From 80a7a8e4556efd1f052dbd01ab491937b33af639 Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 4 Feb 2026 15:00:48 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=8B=A4=EC=A4=91=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=EA=B0=92=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테이블 관리 서비스에서 파이프로 구분된 문자열을 처리하는 로직을 추가하여, 날짜 타입은 날짜 범위로, 그 외 타입은 다중 선택(IN 조건)으로 처리하도록 개선하였습니다. - 엔티티 조인 검색 및 일반 컬럼 검색에서 다중 선택값을 처리하는 로직을 추가하여, 사용자 입력에 따른 필터링 기능을 강화하였습니다. - 버튼 컴포넌트에서 기본 텍스트 결정 로직을 개선하여 다양한 소스에서 버튼 텍스트를 가져올 수 있도록 하였습니다. - 테이블 리스트 컴포넌트에서 joinColumnMapping을 추가하여 필터링 기능을 개선하였습니다. --- .../src/services/tableManagementService.ts | 98 ++++++++++++++++--- .../config-panels/ButtonConfigPanel.tsx | 4 +- .../ButtonPrimaryComponent.tsx | 26 ++++- .../v2-table-list/TableListComponent.tsx | 15 ++- 4 files changed, 123 insertions(+), 20 deletions(-) diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 09a9691d..da7a3981 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -1461,6 +1461,40 @@ export class TableManagementService { }); } + // 🔧 파이프로 구분된 문자열 처리 (객체에서 추출한 actualValue도 처리) + if (typeof actualValue === "string" && actualValue.includes("|")) { + const columnInfo = await this.getColumnWebTypeInfo( + tableName, + columnName + ); + + // 날짜 타입이면 날짜 범위로 처리 + if ( + columnInfo && + (columnInfo.webType === "date" || columnInfo.webType === "datetime") + ) { + return this.buildDateRangeCondition(columnName, actualValue, paramIndex); + } + + // 그 외 타입이면 다중선택(IN 조건)으로 처리 + const multiValues = actualValue + .split("|") + .filter((v: string) => v.trim() !== ""); + if (multiValues.length > 0) { + const placeholders = multiValues + .map((_: string, idx: number) => `$${paramIndex + idx}`) + .join(", "); + logger.info( + `🔍 다중선택 필터 적용 (객체에서 추출): ${columnName} IN (${multiValues.join(", ")})` + ); + return { + whereClause: `${columnName}::text IN (${placeholders})`, + values: multiValues, + paramCount: multiValues.length, + }; + } + } + // "__ALL__" 값이거나 빈 값이면 필터 조건을 적용하지 않음 if ( actualValue === "__ALL__" || @@ -3428,15 +3462,37 @@ export class TableManagementService { // 기본 Entity 조인 컬럼인 경우: 조인된 테이블의 표시 컬럼에서 검색 const aliasKey = `${joinConfig.referenceTable}:${joinConfig.sourceColumn}`; const alias = aliasMap.get(aliasKey); - whereConditions.push( - `${alias}.${joinConfig.displayColumn} ILIKE '%${safeValue}%'` - ); - entitySearchColumns.push( - `${key} (${joinConfig.referenceTable}.${joinConfig.displayColumn})` - ); - logger.info( - `🎯 Entity 조인 검색: ${key} → ${joinConfig.referenceTable}.${joinConfig.displayColumn} LIKE '%${safeValue}%' (별칭: ${alias})` - ); + + // 🔧 파이프로 구분된 다중 선택값 처리 + if (safeValue.includes("|")) { + const multiValues = safeValue + .split("|") + .filter((v: string) => v.trim() !== ""); + if (multiValues.length > 0) { + const inClause = multiValues + .map((v: string) => `'${v}'`) + .join(", "); + whereConditions.push( + `${alias}.${joinConfig.displayColumn}::text IN (${inClause})` + ); + entitySearchColumns.push( + `${key} (${joinConfig.referenceTable}.${joinConfig.displayColumn})` + ); + logger.info( + `🎯 Entity 조인 다중선택 검색: ${key} → ${joinConfig.referenceTable}.${joinConfig.displayColumn} IN (${multiValues.join(", ")}) (별칭: ${alias})` + ); + } + } else { + whereConditions.push( + `${alias}.${joinConfig.displayColumn} ILIKE '%${safeValue}%'` + ); + entitySearchColumns.push( + `${key} (${joinConfig.referenceTable}.${joinConfig.displayColumn})` + ); + logger.info( + `🎯 Entity 조인 검색: ${key} → ${joinConfig.referenceTable}.${joinConfig.displayColumn} LIKE '%${safeValue}%' (별칭: ${alias})` + ); + } } else if (key === "writer_dept_code") { // writer_dept_code: user_info.dept_code에서 검색 const userAliasKey = Array.from(aliasMap.keys()).find((k) => @@ -3473,10 +3529,26 @@ export class TableManagementService { } } else { // 일반 컬럼인 경우: 메인 테이블에서 검색 - whereConditions.push(`main.${key} ILIKE '%${safeValue}%'`); - logger.info( - `🔍 일반 컬럼 검색: ${key} → main.${key} LIKE '%${safeValue}%'` - ); + // 🔧 파이프로 구분된 다중 선택값 처리 + if (safeValue.includes("|")) { + const multiValues = safeValue + .split("|") + .filter((v: string) => v.trim() !== ""); + if (multiValues.length > 0) { + const inClause = multiValues + .map((v: string) => `'${v}'`) + .join(", "); + whereConditions.push(`main.${key}::text IN (${inClause})`); + logger.info( + `🔍 다중선택 컬럼 검색: ${key} → main.${key} IN (${multiValues.join(", ")})` + ); + } + } else { + whereConditions.push(`main.${key} ILIKE '%${safeValue}%'`); + logger.info( + `🔍 일반 컬럼 검색: ${key} → main.${key} LIKE '%${safeValue}%'` + ); + } } } } diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index b822aeee..6ea347c2 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -834,8 +834,10 @@ export const ButtonConfigPanel: React.FC = ({ {/* 이벤트 버스 */} 이벤트 발송 - {/* 🔒 숨김 처리 - 기존 시스템 호환성 유지, UI에서만 숨김 + {/* 복사 */} 복사 (품목코드 초기화) + + {/* 🔒 숨김 처리 - 기존 시스템 호환성 유지, UI에서만 숨김 연관 데이터 버튼 모달 열기 (deprecated) 데이터 전달 + 모달 열기 테이블 이력 보기 diff --git a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx index 83a7771d..918d7560 100644 --- a/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/v2-button-primary/ButtonPrimaryComponent.tsx @@ -1324,7 +1324,31 @@ export const ButtonPrimaryComponent: React.FC = ({ ...userStyle, }; - const buttonContent = processedConfig.text !== undefined ? processedConfig.text : component.label || "버튼"; + // 버튼 텍스트 결정 (다양한 소스에서 가져옴) + // "기본 버튼"은 컴포넌트 생성 시 기본값이므로 무시 + const labelValue = component.label === "기본 버튼" ? undefined : component.label; + + // 액션 타입에 따른 기본 텍스트 (modal 액션과 동일하게) + const actionType = processedConfig.action?.type || component.componentConfig?.action?.type; + const actionDefaultText: Record = { + save: "저장", + delete: "삭제", + modal: "등록", + edit: "수정", + copy: "복사", + close: "닫기", + cancel: "취소", + }; + + const buttonContent = + processedConfig.text || + component.webTypeConfig?.text || + component.componentConfig?.text || + component.config?.text || + component.style?.labelText || + labelValue || + actionDefaultText[actionType as string] || + "버튼"; return ( <> diff --git a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx index c99f9876..02ef8643 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListComponent.tsx @@ -459,6 +459,9 @@ export const TableListComponent: React.FC = ({ // 🆕 Filter Builder (고급 필터) 관련 상태 - filteredData보다 먼저 정의해야 함 const [filterGroups, setFilterGroups] = useState([]); + + // 🆕 joinColumnMapping - filteredData에서 사용하므로 먼저 정의해야 함 + const [joinColumnMapping, setJoinColumnMapping] = useState>({}); // 🆕 분할 패널에서 우측에 이미 추가된 항목 필터링 (좌측 테이블에만 적용) + 헤더 필터 const filteredData = useMemo(() => { @@ -473,14 +476,17 @@ export const TableListComponent: React.FC = ({ }); } - // 2. 헤더 필터 적용 (joinColumnMapping 사용 안 함 - 직접 컬럼명 사용) + // 2. 헤더 필터 적용 (joinColumnMapping 사용 - 조인된 컬럼과 일치해야 함) if (Object.keys(headerFilters).length > 0) { result = result.filter((row) => { return Object.entries(headerFilters).every(([columnName, values]) => { if (values.size === 0) return true; - // 여러 가능한 컬럼명 시도 - const cellValue = row[columnName] ?? row[columnName.toLowerCase()] ?? row[columnName.toUpperCase()]; + // joinColumnMapping을 사용하여 조인된 컬럼명 확인 + const mappedColumnName = joinColumnMapping[columnName] || columnName; + + // 여러 가능한 컬럼명 시도 (mappedColumnName 우선) + const cellValue = row[mappedColumnName] ?? row[columnName] ?? row[columnName.toLowerCase()] ?? row[columnName.toUpperCase()]; const cellStr = cellValue !== null && cellValue !== undefined ? String(cellValue) : ""; return values.has(cellStr); @@ -541,7 +547,7 @@ export const TableListComponent: React.FC = ({ } return result; - }, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, filterGroups]); + }, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, filterGroups, joinColumnMapping]); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(0); @@ -554,7 +560,6 @@ export const TableListComponent: React.FC = ({ const [tableLabel, setTableLabel] = useState(""); const [localPageSize, setLocalPageSize] = useState(tableConfig.pagination?.pageSize || 20); const [displayColumns, setDisplayColumns] = useState([]); - const [joinColumnMapping, setJoinColumnMapping] = useState>({}); const [columnMeta, setColumnMeta] = useState< Record >({});