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:
kjs
2026-02-26 16:07:15 +09:00
parent 63d8e17392
commit 20167ad359
10 changed files with 689 additions and 64 deletions

View File

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

View File

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

View File

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