feature/screen-management #241
|
|
@ -403,18 +403,25 @@ export class EntityJoinService {
|
|||
const fromClause = `FROM ${tableName} main`;
|
||||
|
||||
// LEFT JOIN 절들 (위에서 생성한 별칭 매핑 사용, 각 sourceColumn마다 별도 JOIN)
|
||||
// 멀티테넌시: 모든 조인에 company_code 조건 추가 (다른 회사 데이터 혼합 방지)
|
||||
const joinClauses = uniqueReferenceTableConfigs
|
||||
.map((config) => {
|
||||
const aliasKey = `${config.referenceTable}:${config.sourceColumn}`;
|
||||
const alias = aliasMap.get(aliasKey);
|
||||
|
||||
// table_column_category_values는 특별한 조인 조건 필요 (회사별 필터링만)
|
||||
// table_column_category_values는 특별한 조인 조건 필요 (회사별 필터링)
|
||||
if (config.referenceTable === "table_column_category_values") {
|
||||
// 멀티테넌시: 회사 데이터만 사용 (공통 데이터 제외)
|
||||
return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn} AND ${alias}.table_name = '${tableName}' AND ${alias}.column_name = '${config.sourceColumn}' AND ${alias}.company_code = main.company_code AND ${alias}.is_active = true`;
|
||||
}
|
||||
|
||||
return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn}`;
|
||||
// user_info는 전역 테이블이므로 company_code 조건 없이 조인
|
||||
if (config.referenceTable === "user_info") {
|
||||
return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn}`;
|
||||
}
|
||||
|
||||
// 일반 테이블: company_code가 있으면 같은 회사 데이터만 조인 (멀티테넌시)
|
||||
// supplier_mng, customer_mng, item_info 등 회사별 데이터 테이블
|
||||
return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn} AND ${alias}.company_code = main.company_code`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
|
|
|
|||
|
|
@ -149,11 +149,12 @@ export const NumberingRuleDesigner: React.FC<NumberingRuleDesignerProps> = ({
|
|||
const existing = savedRules.find((r) => r.ruleId === currentRule.ruleId);
|
||||
|
||||
// 저장 전에 현재 화면의 테이블명과 menuObjid 자동 설정
|
||||
// 메뉴 기반으로 채번규칙 관리 (menuObjid로 필터링)
|
||||
const ruleToSave = {
|
||||
...currentRule,
|
||||
scopeType: "table" as const, // ⚠️ 임시: DB 제약 조건 때문에 table 유지
|
||||
tableName: currentTableName || currentRule.tableName || null, // 현재 테이블명 자동 설정 (빈 값은 null)
|
||||
menuObjid: menuObjid || currentRule.menuObjid || null, // 🆕 메뉴 OBJID 설정 (필터링용)
|
||||
scopeType: "menu" as const, // 메뉴 기반 채번규칙
|
||||
tableName: currentTableName || currentRule.tableName || null, // 현재 테이블명 (참고용)
|
||||
menuObjid: menuObjid || currentRule.menuObjid || null, // 메뉴 OBJID (필터링 기준)
|
||||
};
|
||||
|
||||
console.log("💾 채번 규칙 저장:", {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useState, useMemo, useCallback } from "react";
|
||||
import React, { useEffect, useState, useMemo, useCallback, useRef } from "react";
|
||||
import { ComponentRendererProps } from "@/types/component";
|
||||
import { CardDisplayConfig } from "./types";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
|
|
@ -13,6 +13,8 @@ import { Badge } from "@/components/ui/badge";
|
|||
import { useScreenContextOptional } from "@/contexts/ScreenContext";
|
||||
import { useSplitPanelContext } from "@/contexts/SplitPanelContext";
|
||||
import { useModalDataStore } from "@/stores/modalDataStore";
|
||||
import { useTableOptions } from "@/contexts/TableOptionsContext";
|
||||
import { TableFilter, ColumnVisibility, TableColumn } from "@/types/table-options";
|
||||
|
||||
export interface CardDisplayComponentProps extends ComponentRendererProps {
|
||||
config?: CardDisplayConfig;
|
||||
|
|
@ -48,11 +50,32 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
|||
const splitPanelContext = useSplitPanelContext();
|
||||
const splitPanelPosition = screenContext?.splitPanelPosition;
|
||||
|
||||
// TableOptions Context (검색 필터 위젯 연동용)
|
||||
let tableOptionsContext: ReturnType<typeof useTableOptions> | null = null;
|
||||
try {
|
||||
tableOptionsContext = useTableOptions();
|
||||
} catch (e) {
|
||||
// Context가 없으면 (디자이너 모드) 무시
|
||||
}
|
||||
|
||||
// 테이블 데이터 상태 관리
|
||||
const [loadedTableData, setLoadedTableData] = useState<any[]>([]);
|
||||
const [loadedTableColumns, setLoadedTableColumns] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 필터 상태 (검색 필터 위젯에서 전달받은 필터)
|
||||
const [filters, setFiltersInternal] = useState<TableFilter[]>([]);
|
||||
|
||||
// 필터 상태 변경 래퍼 (로깅용)
|
||||
const setFilters = useCallback((newFilters: TableFilter[]) => {
|
||||
console.log("🎴 [CardDisplay] setFilters 호출됨:", {
|
||||
componentId: component.id,
|
||||
filtersCount: newFilters.length,
|
||||
filters: newFilters,
|
||||
});
|
||||
setFiltersInternal(newFilters);
|
||||
}, [component.id]);
|
||||
|
||||
// 카테고리 매핑 상태 (카테고리 코드 -> 라벨/색상)
|
||||
const [columnMeta, setColumnMeta] = useState<
|
||||
Record<string, { webType?: string; codeCategory?: string; inputType?: string }>
|
||||
|
|
@ -380,6 +403,195 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
|||
}
|
||||
}, [screenContext, component.id, dataProvider]);
|
||||
|
||||
// TableOptionsContext에 테이블 등록 (검색 필터 위젯 연동용)
|
||||
const tableId = `card-display-${component.id}`;
|
||||
const tableNameToUse = tableName || component.componentConfig?.tableName || '';
|
||||
const tableLabel = component.componentConfig?.title || component.label || "카드 디스플레이";
|
||||
|
||||
// ref로 최신 데이터 참조 (useCallback 의존성 문제 해결)
|
||||
const loadedTableDataRef = useRef(loadedTableData);
|
||||
const categoryMappingsRef = useRef(categoryMappings);
|
||||
|
||||
useEffect(() => {
|
||||
loadedTableDataRef.current = loadedTableData;
|
||||
}, [loadedTableData]);
|
||||
|
||||
useEffect(() => {
|
||||
categoryMappingsRef.current = categoryMappings;
|
||||
}, [categoryMappings]);
|
||||
|
||||
// 필터가 변경되면 데이터 다시 로드 (테이블 리스트와 동일한 패턴)
|
||||
// 초기 로드 여부 추적
|
||||
const isInitialLoadRef = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!tableNameToUse || isDesignMode) return;
|
||||
|
||||
// 초기 로드는 별도 useEffect에서 처리하므로 스킵
|
||||
if (isInitialLoadRef.current) {
|
||||
isInitialLoadRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const loadFilteredData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// 필터 값을 검색 파라미터로 변환
|
||||
const searchParams: Record<string, any> = {};
|
||||
filters.forEach(filter => {
|
||||
if (filter.value !== undefined && filter.value !== null && filter.value !== '') {
|
||||
searchParams[filter.columnName] = filter.value;
|
||||
}
|
||||
});
|
||||
|
||||
console.log("🔍 [CardDisplay] 필터 적용 데이터 로드:", {
|
||||
tableName: tableNameToUse,
|
||||
filtersCount: filters.length,
|
||||
searchParams,
|
||||
});
|
||||
|
||||
// search 파라미터로 검색 조건 전달 (API 스펙에 맞게)
|
||||
const dataResponse = await tableTypeApi.getTableData(tableNameToUse, {
|
||||
page: 1,
|
||||
size: 50,
|
||||
search: searchParams,
|
||||
});
|
||||
|
||||
setLoadedTableData(dataResponse.data);
|
||||
|
||||
// 데이터 건수 업데이트
|
||||
if (tableOptionsContext) {
|
||||
tableOptionsContext.updateTableDataCount(tableId, dataResponse.data?.length || 0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ [CardDisplay] 필터 적용 실패:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 필터 변경 시 항상 데이터 다시 로드 (빈 필터 = 전체 데이터)
|
||||
loadFilteredData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [filters, tableNameToUse, isDesignMode, tableId]);
|
||||
|
||||
// 컬럼 고유 값 조회 함수 (select 타입 필터용)
|
||||
const getColumnUniqueValues = useCallback(async (columnName: string): Promise<Array<{ label: string; value: string }>> => {
|
||||
if (!tableNameToUse) return [];
|
||||
|
||||
try {
|
||||
// 현재 로드된 데이터에서 고유 값 추출
|
||||
const uniqueValues = new Set<string>();
|
||||
loadedTableDataRef.current.forEach(row => {
|
||||
const value = row[columnName];
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
uniqueValues.add(String(value));
|
||||
}
|
||||
});
|
||||
|
||||
// 카테고리 매핑이 있으면 라벨 적용
|
||||
const mapping = categoryMappingsRef.current[columnName];
|
||||
return Array.from(uniqueValues).map(value => ({
|
||||
value,
|
||||
label: mapping?.[value]?.label || value,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(`❌ [CardDisplay] 고유 값 조회 실패: ${columnName}`, error);
|
||||
return [];
|
||||
}
|
||||
}, [tableNameToUse]);
|
||||
|
||||
// TableOptionsContext에 등록
|
||||
// registerTable과 unregisterTable 함수 참조 저장 (의존성 안정화)
|
||||
const registerTableRef = useRef(tableOptionsContext?.registerTable);
|
||||
const unregisterTableRef = useRef(tableOptionsContext?.unregisterTable);
|
||||
|
||||
// setFiltersInternal을 ref로 저장 (등록 시 최신 함수 사용)
|
||||
const setFiltersRef = useRef(setFiltersInternal);
|
||||
const getColumnUniqueValuesRef = useRef(getColumnUniqueValues);
|
||||
|
||||
useEffect(() => {
|
||||
registerTableRef.current = tableOptionsContext?.registerTable;
|
||||
unregisterTableRef.current = tableOptionsContext?.unregisterTable;
|
||||
}, [tableOptionsContext]);
|
||||
|
||||
useEffect(() => {
|
||||
setFiltersRef.current = setFiltersInternal;
|
||||
}, [setFiltersInternal]);
|
||||
|
||||
useEffect(() => {
|
||||
getColumnUniqueValuesRef.current = getColumnUniqueValues;
|
||||
}, [getColumnUniqueValues]);
|
||||
|
||||
// 테이블 등록 (한 번만 실행, 컬럼 변경 시에만 재등록)
|
||||
const columnsKey = JSON.stringify(loadedTableColumns.map((col: any) => col.columnName || col.column_name));
|
||||
|
||||
useEffect(() => {
|
||||
if (!registerTableRef.current || !unregisterTableRef.current) return;
|
||||
if (isDesignMode || !tableNameToUse || loadedTableColumns.length === 0) return;
|
||||
|
||||
// 컬럼 정보를 TableColumn 형식으로 변환
|
||||
const columns: TableColumn[] = loadedTableColumns.map((col: any) => ({
|
||||
columnName: col.columnName || col.column_name,
|
||||
columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
|
||||
inputType: columnMeta[col.columnName || col.column_name]?.inputType || 'text',
|
||||
visible: true,
|
||||
width: 200,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
}));
|
||||
|
||||
// onFilterChange는 ref를 통해 최신 함수를 호출하는 래퍼 사용
|
||||
const onFilterChangeWrapper = (newFilters: TableFilter[]) => {
|
||||
console.log("🎴 [CardDisplay] onFilterChange 래퍼 호출:", {
|
||||
tableId,
|
||||
filtersCount: newFilters.length,
|
||||
});
|
||||
setFiltersRef.current(newFilters);
|
||||
};
|
||||
|
||||
const getColumnUniqueValuesWrapper = async (columnName: string) => {
|
||||
return getColumnUniqueValuesRef.current(columnName);
|
||||
};
|
||||
|
||||
const registration = {
|
||||
tableId,
|
||||
label: tableLabel,
|
||||
tableName: tableNameToUse,
|
||||
columns,
|
||||
dataCount: loadedTableData.length,
|
||||
onFilterChange: onFilterChangeWrapper,
|
||||
onGroupChange: () => {}, // 카드 디스플레이는 그룹핑 미지원
|
||||
onColumnVisibilityChange: () => {}, // 카드 디스플레이는 컬럼 가시성 미지원
|
||||
getColumnUniqueValues: getColumnUniqueValuesWrapper,
|
||||
};
|
||||
|
||||
console.log("📋 [CardDisplay] TableOptionsContext에 등록:", {
|
||||
tableId,
|
||||
tableName: tableNameToUse,
|
||||
columnsCount: columns.length,
|
||||
dataCount: loadedTableData.length,
|
||||
});
|
||||
|
||||
registerTableRef.current(registration);
|
||||
|
||||
const unregister = unregisterTableRef.current;
|
||||
const currentTableId = tableId;
|
||||
|
||||
return () => {
|
||||
console.log("📋 [CardDisplay] TableOptionsContext에서 해제:", currentTableId);
|
||||
unregister(currentTableId);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
isDesignMode,
|
||||
tableId,
|
||||
tableNameToUse,
|
||||
tableLabel,
|
||||
columnsKey, // 컬럼 변경 시에만 재등록
|
||||
]);
|
||||
|
||||
// 로딩 중인 경우 로딩 표시
|
||||
if (loading) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Settings, Filter, Layers, X, Check, ChevronsUpDown } from "lucide-react";
|
||||
|
|
@ -41,6 +41,7 @@ interface TableSearchWidgetProps {
|
|||
showTableSelector?: boolean; // 테이블 선택 드롭다운 표시 여부
|
||||
filterMode?: "dynamic" | "preset"; // 필터 모드
|
||||
presetFilters?: PresetFilter[]; // 고정 필터 목록
|
||||
targetPanelPosition?: "left" | "right" | "auto"; // 분할 패널에서 대상 패널 위치 (기본: "left")
|
||||
};
|
||||
};
|
||||
screenId?: number; // 화면 ID
|
||||
|
|
@ -82,19 +83,90 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
const showTableSelector = component.componentConfig?.showTableSelector ?? true;
|
||||
const filterMode = component.componentConfig?.filterMode ?? "dynamic";
|
||||
const presetFilters = component.componentConfig?.presetFilters ?? [];
|
||||
const targetPanelPosition = component.componentConfig?.targetPanelPosition ?? "left"; // 기본값: 좌측 패널
|
||||
|
||||
// Map을 배열로 변환
|
||||
const tableList = Array.from(registeredTables.values());
|
||||
const currentTable = selectedTableId ? getTable(selectedTableId) : undefined;
|
||||
|
||||
// 첫 번째 테이블 자동 선택
|
||||
useEffect(() => {
|
||||
const tables = Array.from(registeredTables.values());
|
||||
|
||||
if (autoSelectFirstTable && tables.length > 0 && !selectedTableId) {
|
||||
setSelectedTableId(tables[0].tableId);
|
||||
const allTableList = Array.from(registeredTables.values());
|
||||
|
||||
// 대상 패널 위치에 따라 테이블 필터링 (tableId 패턴 기반)
|
||||
const tableList = useMemo(() => {
|
||||
// "auto"면 모든 테이블 반환
|
||||
if (targetPanelPosition === "auto") {
|
||||
return allTableList;
|
||||
}
|
||||
}, [registeredTables, selectedTableId, autoSelectFirstTable, setSelectedTableId]);
|
||||
|
||||
// 테이블 ID 패턴으로 필터링
|
||||
// card-display-XXX: 좌측 패널 (카드 디스플레이)
|
||||
// datatable-XXX, table-list-XXX: 우측 패널 (테이블 리스트)
|
||||
const filteredTables = allTableList.filter(table => {
|
||||
const tableId = table.tableId.toLowerCase();
|
||||
|
||||
if (targetPanelPosition === "left") {
|
||||
// 좌측 패널 대상: card-display만
|
||||
return tableId.includes("card-display") || tableId.includes("card");
|
||||
} else if (targetPanelPosition === "right") {
|
||||
// 우측 패널 대상: datatable, table-list 등 (card-display 제외)
|
||||
const isCardDisplay = tableId.includes("card-display") || tableId.includes("card");
|
||||
return !isCardDisplay;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// 필터링된 결과가 없으면 모든 테이블 반환 (폴백)
|
||||
if (filteredTables.length === 0) {
|
||||
console.log("🔍 [TableSearchWidget] 대상 패널에 테이블 없음, 전체 테이블 사용:", {
|
||||
targetPanelPosition,
|
||||
allTablesCount: allTableList.length,
|
||||
allTableIds: allTableList.map(t => t.tableId),
|
||||
});
|
||||
return allTableList;
|
||||
}
|
||||
|
||||
console.log("🔍 [TableSearchWidget] 테이블 필터링:", {
|
||||
targetPanelPosition,
|
||||
allTablesCount: allTableList.length,
|
||||
filteredCount: filteredTables.length,
|
||||
filteredTableIds: filteredTables.map(t => t.tableId),
|
||||
});
|
||||
|
||||
return filteredTables;
|
||||
}, [allTableList, targetPanelPosition]);
|
||||
|
||||
// currentTable은 tableList(필터링된 목록)에서 가져와야 함
|
||||
const currentTable = useMemo(() => {
|
||||
if (!selectedTableId) return undefined;
|
||||
|
||||
// 먼저 tableList(필터링된 목록)에서 찾기
|
||||
const tableFromList = tableList.find(t => t.tableId === selectedTableId);
|
||||
if (tableFromList) {
|
||||
return tableFromList;
|
||||
}
|
||||
|
||||
// tableList에 없으면 전체에서 찾기 (폴백)
|
||||
return getTable(selectedTableId);
|
||||
}, [selectedTableId, tableList, getTable]);
|
||||
|
||||
// 대상 패널의 첫 번째 테이블 자동 선택
|
||||
useEffect(() => {
|
||||
if (!autoSelectFirstTable || tableList.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 현재 선택된 테이블이 대상 패널에 있는지 확인
|
||||
const isCurrentTableInTarget = selectedTableId && tableList.some(t => t.tableId === selectedTableId);
|
||||
|
||||
// 현재 선택된 테이블이 대상 패널에 없으면 대상 패널의 첫 번째 테이블 선택
|
||||
if (!selectedTableId || !isCurrentTableInTarget) {
|
||||
const targetTable = tableList[0];
|
||||
console.log("🔍 [TableSearchWidget] 대상 패널 테이블 자동 선택:", {
|
||||
targetPanelPosition,
|
||||
selectedTableId: targetTable.tableId,
|
||||
tableName: targetTable.tableName,
|
||||
});
|
||||
setSelectedTableId(targetTable.tableId);
|
||||
}
|
||||
}, [tableList, selectedTableId, autoSelectFirstTable, setSelectedTableId, targetPanelPosition]);
|
||||
|
||||
// 현재 테이블의 저장된 필터 불러오기 (동적 모드) 또는 고정 필터 적용 (고정 모드)
|
||||
useEffect(() => {
|
||||
|
|
@ -302,6 +374,12 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
return true;
|
||||
});
|
||||
|
||||
console.log("🔍 [TableSearchWidget] 필터 적용:", {
|
||||
currentTableId: currentTable?.tableId,
|
||||
currentTableName: currentTable?.tableName,
|
||||
filtersCount: filtersWithValues.length,
|
||||
filtersWithValues,
|
||||
});
|
||||
currentTable?.onFilterChange(filtersWithValues);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -76,12 +76,16 @@ export function TableSearchWidgetConfigPanel({
|
|||
const [localPresetFilters, setLocalPresetFilters] = useState<PresetFilter[]>(
|
||||
currentConfig.presetFilters ?? []
|
||||
);
|
||||
const [localTargetPanelPosition, setLocalTargetPanelPosition] = useState<"left" | "right" | "auto">(
|
||||
currentConfig.targetPanelPosition ?? "left"
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalAutoSelect(currentConfig.autoSelectFirstTable ?? true);
|
||||
setLocalShowSelector(currentConfig.showTableSelector ?? true);
|
||||
setLocalFilterMode(currentConfig.filterMode ?? "dynamic");
|
||||
setLocalPresetFilters(currentConfig.presetFilters ?? []);
|
||||
setLocalTargetPanelPosition(currentConfig.targetPanelPosition ?? "left");
|
||||
}, [currentConfig]);
|
||||
|
||||
// 설정 업데이트 헬퍼
|
||||
|
|
@ -164,6 +168,40 @@ export function TableSearchWidgetConfigPanel({
|
|||
</Label>
|
||||
</div>
|
||||
|
||||
{/* 대상 패널 위치 (분할 패널용) */}
|
||||
<div className="space-y-2 border-t pt-4">
|
||||
<Label className="text-xs sm:text-sm font-medium">대상 패널 위치 (분할 패널)</Label>
|
||||
<p className="text-[10px] text-muted-foreground mb-2">
|
||||
분할 패널이 있는 화면에서 검색 필터가 어떤 패널의 컴포넌트를 대상으로 할지 선택합니다.
|
||||
</p>
|
||||
<RadioGroup
|
||||
value={localTargetPanelPosition}
|
||||
onValueChange={(value: "left" | "right" | "auto") => {
|
||||
setLocalTargetPanelPosition(value);
|
||||
handleUpdate("targetPanelPosition", value);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="left" id="target-left" />
|
||||
<Label htmlFor="target-left" className="text-xs sm:text-sm cursor-pointer font-normal">
|
||||
좌측 패널 (카드 디스플레이 등)
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="right" id="target-right" />
|
||||
<Label htmlFor="target-right" className="text-xs sm:text-sm cursor-pointer font-normal">
|
||||
우측 패널 (테이블 리스트 등)
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="auto" id="target-auto" />
|
||||
<Label htmlFor="target-auto" className="text-xs sm:text-sm cursor-pointer font-normal">
|
||||
자동 (모든 테이블 대상)
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
{/* 필터 모드 선택 */}
|
||||
<div className="space-y-2 border-t pt-4">
|
||||
<Label className="text-xs sm:text-sm font-medium">필터 모드</Label>
|
||||
|
|
|
|||
Loading…
Reference in New Issue