feat: 다중 선택값 처리 로직 추가 및 개선
- 테이블 관리 서비스에서 파이프로 구분된 문자열을 처리하는 로직을 추가하여, 날짜 타입은 날짜 범위로, 그 외 타입은 다중 선택(IN 조건)으로 처리하도록 개선하였습니다. - 엔티티 조인 검색 및 일반 컬럼 검색에서 다중 선택값을 처리하는 로직을 추가하여, 사용자 입력에 따른 필터링 기능을 강화하였습니다. - 버튼 컴포넌트에서 기본 텍스트 결정 로직을 개선하여 다양한 소스에서 버튼 텍스트를 가져올 수 있도록 하였습니다. - 테이블 리스트 컴포넌트에서 joinColumnMapping을 추가하여 필터링 기능을 개선하였습니다.
This commit is contained in:
parent
52fd370460
commit
80a7a8e455
|
|
@ -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__" 값이거나 빈 값이면 필터 조건을 적용하지 않음
|
// "__ALL__" 값이거나 빈 값이면 필터 조건을 적용하지 않음
|
||||||
if (
|
if (
|
||||||
actualValue === "__ALL__" ||
|
actualValue === "__ALL__" ||
|
||||||
|
|
@ -3428,15 +3462,37 @@ export class TableManagementService {
|
||||||
// 기본 Entity 조인 컬럼인 경우: 조인된 테이블의 표시 컬럼에서 검색
|
// 기본 Entity 조인 컬럼인 경우: 조인된 테이블의 표시 컬럼에서 검색
|
||||||
const aliasKey = `${joinConfig.referenceTable}:${joinConfig.sourceColumn}`;
|
const aliasKey = `${joinConfig.referenceTable}:${joinConfig.sourceColumn}`;
|
||||||
const alias = aliasMap.get(aliasKey);
|
const alias = aliasMap.get(aliasKey);
|
||||||
whereConditions.push(
|
|
||||||
`${alias}.${joinConfig.displayColumn} ILIKE '%${safeValue}%'`
|
// 🔧 파이프로 구분된 다중 선택값 처리
|
||||||
);
|
if (safeValue.includes("|")) {
|
||||||
entitySearchColumns.push(
|
const multiValues = safeValue
|
||||||
`${key} (${joinConfig.referenceTable}.${joinConfig.displayColumn})`
|
.split("|")
|
||||||
);
|
.filter((v: string) => v.trim() !== "");
|
||||||
logger.info(
|
if (multiValues.length > 0) {
|
||||||
`🎯 Entity 조인 검색: ${key} → ${joinConfig.referenceTable}.${joinConfig.displayColumn} LIKE '%${safeValue}%' (별칭: ${alias})`
|
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") {
|
} else if (key === "writer_dept_code") {
|
||||||
// writer_dept_code: user_info.dept_code에서 검색
|
// writer_dept_code: user_info.dept_code에서 검색
|
||||||
const userAliasKey = Array.from(aliasMap.keys()).find((k) =>
|
const userAliasKey = Array.from(aliasMap.keys()).find((k) =>
|
||||||
|
|
@ -3473,10 +3529,26 @@ export class TableManagementService {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 일반 컬럼인 경우: 메인 테이블에서 검색
|
// 일반 컬럼인 경우: 메인 테이블에서 검색
|
||||||
whereConditions.push(`main.${key} ILIKE '%${safeValue}%'`);
|
// 🔧 파이프로 구분된 다중 선택값 처리
|
||||||
logger.info(
|
if (safeValue.includes("|")) {
|
||||||
`🔍 일반 컬럼 검색: ${key} → main.${key} LIKE '%${safeValue}%'`
|
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}%'`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -834,8 +834,10 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||||
{/* 이벤트 버스 */}
|
{/* 이벤트 버스 */}
|
||||||
<SelectItem value="event">이벤트 발송</SelectItem>
|
<SelectItem value="event">이벤트 발송</SelectItem>
|
||||||
|
|
||||||
{/* 🔒 숨김 처리 - 기존 시스템 호환성 유지, UI에서만 숨김
|
{/* 복사 */}
|
||||||
<SelectItem value="copy">복사 (품목코드 초기화)</SelectItem>
|
<SelectItem value="copy">복사 (품목코드 초기화)</SelectItem>
|
||||||
|
|
||||||
|
{/* 🔒 숨김 처리 - 기존 시스템 호환성 유지, UI에서만 숨김
|
||||||
<SelectItem value="openRelatedModal">연관 데이터 버튼 모달 열기</SelectItem>
|
<SelectItem value="openRelatedModal">연관 데이터 버튼 모달 열기</SelectItem>
|
||||||
<SelectItem value="openModalWithData">(deprecated) 데이터 전달 + 모달 열기</SelectItem>
|
<SelectItem value="openModalWithData">(deprecated) 데이터 전달 + 모달 열기</SelectItem>
|
||||||
<SelectItem value="view_table_history">테이블 이력 보기</SelectItem>
|
<SelectItem value="view_table_history">테이블 이력 보기</SelectItem>
|
||||||
|
|
|
||||||
|
|
@ -1324,7 +1324,31 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||||
...userStyle,
|
...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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -459,6 +459,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
|
|
||||||
// 🆕 Filter Builder (고급 필터) 관련 상태 - filteredData보다 먼저 정의해야 함
|
// 🆕 Filter Builder (고급 필터) 관련 상태 - filteredData보다 먼저 정의해야 함
|
||||||
const [filterGroups, setFilterGroups] = useState<FilterGroup[]>([]);
|
const [filterGroups, setFilterGroups] = useState<FilterGroup[]>([]);
|
||||||
|
|
||||||
|
// 🆕 joinColumnMapping - filteredData에서 사용하므로 먼저 정의해야 함
|
||||||
|
const [joinColumnMapping, setJoinColumnMapping] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
// 🆕 분할 패널에서 우측에 이미 추가된 항목 필터링 (좌측 테이블에만 적용) + 헤더 필터
|
// 🆕 분할 패널에서 우측에 이미 추가된 항목 필터링 (좌측 테이블에만 적용) + 헤더 필터
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
|
|
@ -473,14 +476,17 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 헤더 필터 적용 (joinColumnMapping 사용 안 함 - 직접 컬럼명 사용)
|
// 2. 헤더 필터 적용 (joinColumnMapping 사용 - 조인된 컬럼과 일치해야 함)
|
||||||
if (Object.keys(headerFilters).length > 0) {
|
if (Object.keys(headerFilters).length > 0) {
|
||||||
result = result.filter((row) => {
|
result = result.filter((row) => {
|
||||||
return Object.entries(headerFilters).every(([columnName, values]) => {
|
return Object.entries(headerFilters).every(([columnName, values]) => {
|
||||||
if (values.size === 0) return true;
|
if (values.size === 0) return true;
|
||||||
|
|
||||||
// 여러 가능한 컬럼명 시도
|
// joinColumnMapping을 사용하여 조인된 컬럼명 확인
|
||||||
const cellValue = row[columnName] ?? row[columnName.toLowerCase()] ?? row[columnName.toUpperCase()];
|
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) : "";
|
const cellStr = cellValue !== null && cellValue !== undefined ? String(cellValue) : "";
|
||||||
|
|
||||||
return values.has(cellStr);
|
return values.has(cellStr);
|
||||||
|
|
@ -541,7 +547,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, filterGroups]);
|
}, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, filterGroups, joinColumnMapping]);
|
||||||
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [totalPages, setTotalPages] = useState(0);
|
const [totalPages, setTotalPages] = useState(0);
|
||||||
|
|
@ -554,7 +560,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
const [tableLabel, setTableLabel] = useState<string>("");
|
const [tableLabel, setTableLabel] = useState<string>("");
|
||||||
const [localPageSize, setLocalPageSize] = useState<number>(tableConfig.pagination?.pageSize || 20);
|
const [localPageSize, setLocalPageSize] = useState<number>(tableConfig.pagination?.pageSize || 20);
|
||||||
const [displayColumns, setDisplayColumns] = useState<ColumnConfig[]>([]);
|
const [displayColumns, setDisplayColumns] = useState<ColumnConfig[]>([]);
|
||||||
const [joinColumnMapping, setJoinColumnMapping] = useState<Record<string, string>>({});
|
|
||||||
const [columnMeta, setColumnMeta] = useState<
|
const [columnMeta, setColumnMeta] = useState<
|
||||||
Record<string, { webType?: string; codeCategory?: string; inputType?: string }>
|
Record<string, { webType?: string; codeCategory?: string; inputType?: string }>
|
||||||
>({});
|
>({});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue