feat: 다중 선택값 처리 로직 추가 및 개선

- 테이블 관리 서비스에서 파이프로 구분된 문자열을 처리하는 로직을 추가하여, 날짜 타입은 날짜 범위로, 그 외 타입은 다중 선택(IN 조건)으로 처리하도록 개선하였습니다.
- 엔티티 조인 검색 및 일반 컬럼 검색에서 다중 선택값을 처리하는 로직을 추가하여, 사용자 입력에 따른 필터링 기능을 강화하였습니다.
- 버튼 컴포넌트에서 기본 텍스트 결정 로직을 개선하여 다양한 소스에서 버튼 텍스트를 가져올 수 있도록 하였습니다.
- 테이블 리스트 컴포넌트에서 joinColumnMapping을 추가하여 필터링 기능을 개선하였습니다.
This commit is contained in:
kjs 2026-02-04 15:00:48 +09:00
parent 52fd370460
commit 80a7a8e455
4 changed files with 123 additions and 20 deletions

View File

@ -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}%'`
);
}
}
}
}

View File

@ -834,8 +834,10 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
{/* 이벤트 버스 */}
<SelectItem value="event"> </SelectItem>
{/* 🔒 - , UI
{/* 복사 */}
<SelectItem value="copy"> ( )</SelectItem>
{/* 🔒 - , UI
<SelectItem value="openRelatedModal"> </SelectItem>
<SelectItem value="openModalWithData">(deprecated) + </SelectItem>
<SelectItem value="view_table_history"> </SelectItem>

View File

@ -1324,7 +1324,31 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
...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<string, string> = {
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 (
<>

View File

@ -459,6 +459,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 🆕 Filter Builder (고급 필터) 관련 상태 - filteredData보다 먼저 정의해야 함
const [filterGroups, setFilterGroups] = useState<FilterGroup[]>([]);
// 🆕 joinColumnMapping - filteredData에서 사용하므로 먼저 정의해야 함
const [joinColumnMapping, setJoinColumnMapping] = useState<Record<string, string>>({});
// 🆕 분할 패널에서 우측에 이미 추가된 항목 필터링 (좌측 테이블에만 적용) + 헤더 필터
const filteredData = useMemo(() => {
@ -473,14 +476,17 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
});
}
// 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<TableListComponentProps> = ({
}
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<TableListComponentProps> = ({
const [tableLabel, setTableLabel] = useState<string>("");
const [localPageSize, setLocalPageSize] = useState<number>(tableConfig.pagination?.pageSize || 20);
const [displayColumns, setDisplayColumns] = useState<ColumnConfig[]>([]);
const [joinColumnMapping, setJoinColumnMapping] = useState<Record<string, string>>({});
const [columnMeta, setColumnMeta] = useState<
Record<string, { webType?: string; codeCategory?: string; inputType?: string }>
>({});