feat: Implement advanced filtering capabilities in entity search
- Added a new helper function `applyFilters` to handle dynamic filter conditions for entity search queries. - Enhanced the `getDistinctColumnValues` and `getEntityOptions` endpoints to support JSON array filters, allowing for more flexible data retrieval based on specified conditions. - Updated the frontend components to integrate filter conditions, improving user interaction and data management in selection components. - Introduced new filter options in the V2Select component, enabling users to define and apply various filter criteria dynamically.
This commit is contained in:
@@ -162,6 +162,79 @@ export function RepeaterTable({
|
||||
// 초기 균등 분배 실행 여부 (마운트 시 한 번만 실행)
|
||||
const initializedRef = useRef(false);
|
||||
|
||||
// 편집 가능한 컬럼 인덱스 목록 (방향키 네비게이션용)
|
||||
const editableColIndices = useMemo(
|
||||
() => visibleColumns.reduce<number[]>((acc, col, idx) => {
|
||||
if (col.editable && !col.calculated) acc.push(idx);
|
||||
return acc;
|
||||
}, []),
|
||||
[visibleColumns],
|
||||
);
|
||||
|
||||
// 방향키로 리피터 셀 간 이동
|
||||
const handleArrowNavigation = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
const key = e.key;
|
||||
if (!["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(key)) return;
|
||||
|
||||
const target = e.target as HTMLElement;
|
||||
const cell = target.closest("[data-repeater-row]") as HTMLElement | null;
|
||||
if (!cell) return;
|
||||
|
||||
const row = Number(cell.dataset.repeaterRow);
|
||||
const col = Number(cell.dataset.repeaterCol);
|
||||
if (isNaN(row) || isNaN(col)) return;
|
||||
|
||||
// 텍스트 입력 중 좌/우 방향키는 커서 이동에 사용하므로 무시
|
||||
if ((key === "ArrowLeft" || key === "ArrowRight") && target.tagName === "INPUT") {
|
||||
const input = target as HTMLInputElement;
|
||||
const len = input.value?.length ?? 0;
|
||||
const pos = input.selectionStart ?? 0;
|
||||
// 커서가 끝에 있을 때만 오른쪽 이동, 처음에 있을 때만 왼쪽 이동
|
||||
if (key === "ArrowRight" && pos < len) return;
|
||||
if (key === "ArrowLeft" && pos > 0) return;
|
||||
}
|
||||
|
||||
let nextRow = row;
|
||||
let nextColPos = editableColIndices.indexOf(col);
|
||||
|
||||
switch (key) {
|
||||
case "ArrowUp":
|
||||
nextRow = Math.max(0, row - 1);
|
||||
break;
|
||||
case "ArrowDown":
|
||||
nextRow = Math.min(data.length - 1, row + 1);
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
nextColPos = Math.max(0, nextColPos - 1);
|
||||
break;
|
||||
case "ArrowRight":
|
||||
nextColPos = Math.min(editableColIndices.length - 1, nextColPos + 1);
|
||||
break;
|
||||
}
|
||||
|
||||
const nextCol = editableColIndices[nextColPos];
|
||||
if (nextRow === row && nextCol === col) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const selector = `[data-repeater-row="${nextRow}"][data-repeater-col="${nextCol}"]`;
|
||||
const nextCell = containerRef.current?.querySelector(selector) as HTMLElement | null;
|
||||
if (!nextCell) return;
|
||||
|
||||
const focusable = nextCell.querySelector<HTMLElement>(
|
||||
'input:not([disabled]), select:not([disabled]), [role="combobox"]:not([disabled]), button:not([disabled])',
|
||||
);
|
||||
if (focusable) {
|
||||
focusable.focus();
|
||||
if (focusable.tagName === "INPUT") {
|
||||
(focusable as HTMLInputElement).select();
|
||||
}
|
||||
}
|
||||
},
|
||||
[editableColIndices, data.length],
|
||||
);
|
||||
|
||||
// DnD 센서 설정
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
@@ -648,7 +721,7 @@ export function RepeaterTable({
|
||||
|
||||
return (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<div ref={containerRef} className="flex h-full flex-col border border-gray-200 bg-white">
|
||||
<div ref={containerRef} className="flex h-full flex-col border border-gray-200 bg-white" onKeyDown={handleArrowNavigation}>
|
||||
<div className="min-h-0 flex-1 overflow-x-auto overflow-y-auto">
|
||||
<table
|
||||
className="border-collapse text-xs"
|
||||
@@ -840,6 +913,8 @@ export function RepeaterTable({
|
||||
width: `${columnWidths[col.field]}px`,
|
||||
maxWidth: `${columnWidths[col.field]}px`,
|
||||
}}
|
||||
data-repeater-row={rowIndex}
|
||||
data-repeater-col={colIndex}
|
||||
>
|
||||
{renderCell(row, col, rowIndex)}
|
||||
</td>
|
||||
|
||||
@@ -5777,12 +5777,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
renderCheckboxHeader()
|
||||
) : (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "4px", justifyContent: "center" }}>
|
||||
{/* 🆕 편집 불가 컬럼 표시 */}
|
||||
{column.editable === false && (
|
||||
<span title="편집 불가">
|
||||
<Lock className="text-muted-foreground h-3 w-3" />
|
||||
</span>
|
||||
)}
|
||||
<span>{columnLabels[column.columnName] || column.displayName}</span>
|
||||
{column.sortable !== false && sortColumn === column.columnName && (
|
||||
<span>{sortDirection === "asc" ? "↑" : "↓"}</span>
|
||||
|
||||
@@ -1333,7 +1333,38 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
||||
/>
|
||||
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
|
||||
<span className="truncate text-xs">{column.label || column.columnName}</span>
|
||||
<span className="ml-auto text-[10px] text-gray-400">
|
||||
{isAdded && (
|
||||
<button
|
||||
type="button"
|
||||
title={
|
||||
config.columns?.find((c) => c.columnName === column.columnName)?.editable === false
|
||||
? "편집 잠금 (클릭하여 해제)"
|
||||
: "편집 가능 (클릭하여 잠금)"
|
||||
}
|
||||
className={cn(
|
||||
"ml-auto flex-shrink-0 rounded p-0.5 transition-colors",
|
||||
config.columns?.find((c) => c.columnName === column.columnName)?.editable === false
|
||||
? "text-destructive hover:bg-destructive/10"
|
||||
: "text-muted-foreground hover:bg-muted",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const currentCol = config.columns?.find((c) => c.columnName === column.columnName);
|
||||
if (currentCol) {
|
||||
updateColumn(column.columnName, {
|
||||
editable: currentCol.editable === false ? undefined : false,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{config.columns?.find((c) => c.columnName === column.columnName)?.editable === false ? (
|
||||
<Lock className="h-3 w-3" />
|
||||
) : (
|
||||
<Unlock className="h-3 w-3" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<span className={cn("text-[10px] text-gray-400", !isAdded && "ml-auto")}>
|
||||
{column.input_type || column.dataType}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user