diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 6823e2d5..be16f68d 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -217,6 +217,18 @@ select:focus-visible { outline-offset: 2px; } +/* TableSearchWidget의 SelectTrigger 포커스 스타일 제거 */ +[role="combobox"]:focus-visible { + outline: none !important; + box-shadow: none !important; +} + +button[role="combobox"]:focus-visible { + outline: none !important; + box-shadow: none !important; + border-color: hsl(var(--input)) !important; +} + /* ===== Scrollbar Styles (Optional) ===== */ /* Webkit 기반 브라우저 (Chrome, Safari, Edge) */ ::-webkit-scrollbar { diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index 1fb10716..639ffa0a 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -683,6 +683,9 @@ export const InteractiveScreenViewerDynamic: React.FC; } @@ -188,6 +189,7 @@ export const FilterPanel: React.FC = ({ isOpen, onClose, onFiltersApplied operator: "contains", // 기본 연산자 value: "", filterType: cf.filterType, + width: cf.width || 200, // 너비 포함 (기본 200px) })); // localStorage에 저장 @@ -285,6 +287,28 @@ export const FilterPanel: React.FC = ({ isOpen, onClose, onFiltersApplied 선택 + + {/* 너비 입력 */} + { + const newWidth = parseInt(e.target.value) || 200; + setColumnFilters((prev) => + prev.map((f) => + f.columnName === filter.columnName + ? { ...f, width: newWidth } + : f + ) + ); + }} + disabled={!filter.enabled} + placeholder="너비" + className="h-8 w-[80px] text-xs sm:h-9 sm:text-sm" + min={50} + max={500} + /> + px ))} diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 4ad40826..6344f3e8 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -376,11 +376,53 @@ export const TableListComponent: React.FC = ({ const meta = columnMeta[columnName]; const inputType = meta?.inputType || "text"; - // 카테고리, 엔티티, 코드 타입인 경우 _name 필드 사용 (백엔드 조인 결과) + // 카테고리 타입인 경우 전체 정의된 값 조회 (백엔드 API) + if (inputType === "category") { + try { + console.log("🔍 [getColumnUniqueValues] 카테고리 전체 값 조회:", { + tableName: tableConfig.selectedTable, + columnName, + }); + + // API 클라이언트 사용 (쿠키 인증 자동 처리) + const { apiClient } = await import("@/lib/api/client"); + const response = await apiClient.get( + `/table-categories/${tableConfig.selectedTable}/${columnName}/values` + ); + + if (response.data.success && response.data.data) { + const categoryOptions = response.data.data.map((item: any) => ({ + value: item.valueCode, // 카멜케이스 + label: item.valueLabel, // 카멜케이스 + })); + + console.log("✅ [getColumnUniqueValues] 카테고리 전체 값:", { + columnName, + count: categoryOptions.length, + options: categoryOptions, + }); + + return categoryOptions; + } else { + console.warn("⚠️ [getColumnUniqueValues] 응답 형식 오류:", response.data); + } + } catch (error: any) { + console.error("❌ [getColumnUniqueValues] 카테고리 조회 실패:", { + error: error.message, + response: error.response?.data, + status: error.response?.status, + columnName, + tableName: tableConfig.selectedTable, + }); + // 에러 시 현재 데이터 기반으로 fallback + } + } + + // 일반 타입 또는 카테고리 조회 실패 시: 현재 데이터 기반 const isLabelType = ["category", "entity", "code"].includes(inputType); const labelField = isLabelType ? `${columnName}_name` : columnName; - console.log("🔍 [getColumnUniqueValues] 필드 선택:", { + console.log("🔍 [getColumnUniqueValues] 데이터 기반 조회:", { columnName, inputType, isLabelType, @@ -409,14 +451,13 @@ export const TableListComponent: React.FC = ({ })) .sort((a, b) => a.label.localeCompare(b.label)); - console.log("✅ [getColumnUniqueValues] 결과:", { + console.log("✅ [getColumnUniqueValues] 데이터 기반 결과:", { columnName, inputType, isLabelType, labelField, uniqueCount: result.length, - values: result, // 전체 값 출력 - allKeys: data[0] ? Object.keys(data[0]) : [], // 모든 키 출력 + values: result, }); return result; diff --git a/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx b/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx index 44ea5ae9..ad67080a 100644 --- a/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx +++ b/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx @@ -78,6 +78,7 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) { inputType: string; enabled: boolean; filterType: "text" | "number" | "date" | "select"; + width?: number; }>; // enabled된 필터들만 activeFilters로 설정 @@ -88,6 +89,7 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) { operator: "contains", value: "", filterType: f.filterType, + width: f.width || 200, // 저장된 너비 포함 })); setActiveFilters(activeFiltersList); @@ -212,6 +214,7 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) { const renderFilterInput = (filter: TableFilter) => { const column = currentTable?.columns.find((c) => c.columnName === filter.columnName); const value = filterValues[filter.columnName] || ""; + const width = filter.width || 200; // 기본 너비 200px switch (filter.filterType) { case "date": @@ -220,8 +223,8 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) { type="date" value={value} onChange={(e) => handleFilterChange(filter.columnName, e.target.value)} - className="h-9 text-xs sm:text-sm" - style={{ height: '36px', minHeight: '36px' }} + className="h-9 text-xs sm:text-sm focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0" + style={{ width: `${width}px`, height: '36px', minHeight: '36px', outline: 'none', boxShadow: 'none' }} placeholder={column?.columnLabel} /> ); @@ -232,8 +235,8 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) { type="number" value={value} onChange={(e) => handleFilterChange(filter.columnName, e.target.value)} - className="h-9 text-xs sm:text-sm" - style={{ height: '36px', minHeight: '36px' }} + className="h-9 text-xs sm:text-sm focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0" + style={{ width: `${width}px`, height: '36px', minHeight: '36px', outline: 'none', boxShadow: 'none' }} placeholder={column?.columnLabel} /> ); @@ -241,18 +244,40 @@ export function TableSearchWidget({ component }: TableSearchWidgetProps) { case "select": { let options = selectOptions[filter.columnName] || []; + console.log("🔍 [renderFilterInput] select 렌더링:", { + columnName: filter.columnName, + selectOptions: selectOptions[filter.columnName], + optionsLength: options.length, + }); + // 현재 선택된 값이 옵션 목록에 없으면 추가 (데이터 없을 때도 선택값 유지) if (value && !options.find(opt => opt.value === value)) { const savedLabel = selectedLabels[filter.columnName] || value; options = [{ value, label: savedLabel }, ...options]; } + // 중복 제거 (value 기준) + const uniqueOptions = options.reduce((acc, option) => { + if (!acc.find(opt => opt.value === option.value)) { + acc.push(option); + } + return acc; + }, [] as Array<{ value: string; label: string }>); + + console.log("✅ [renderFilterInput] uniqueOptions:", { + columnName: filter.columnName, + originalOptionsLength: options.length, + uniqueOptionsLength: uniqueOptions.length, + originalOptions: options, + uniqueOptions: uniqueOptions, + }); + return (