From 6d1743c5244dc15d03e93da23462643ac57a4d28 Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 12 Nov 2025 14:50:06 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=ED=95=84=ED=84=B0=20=EA=B0=9C=EC=84=A0=20-=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EB=84=88=EB=B9=84=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9E=90=EB=8F=99=20wrap=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FilterPanel: 필터별 너비(width) 설정 기능 추가 (50-500px) - TableSearchWidget: 필터가 여러 줄로 자동 wrap되도록 flex-wrap 적용 - TableSearchWidget: 필터 너비 설정을 localStorage에 저장/복원 - InteractiveScreenViewerDynamic: TableSearchWidget의 높이를 auto로 설정하여 콘텐츠에 맞게 자동 조정 - globals.css: 입력 필드 포커스 시 검정 테두리 제거 (combobox, input) 주요 변경사항: 1. 필터 설정에서 각 필터의 표시 너비를 개별 설정 가능 2. 필터가 많을 때 자동으로 여러 줄로 배치 (overflow 방지) 3. 설정된 필터 너비가 새로고침 후에도 유지됨 4. TableSearchWidget 높이가 콘텐츠에 맞게 자동 조정 TODO: TableSearchWidget 높이 변화 시 아래 컴포넌트 자동 재배치 기능 구현 예정 --- frontend/app/globals.css | 12 ++++ .../screen/InteractiveScreenViewerDynamic.tsx | 6 +- .../screen/table-options/FilterPanel.tsx | 24 ++++++++ .../table-list/TableListComponent.tsx | 51 +++++++++++++++-- .../table-search-widget/TableSearchWidget.tsx | 56 ++++++++++++++----- frontend/types/table-options.ts | 1 + 6 files changed, 131 insertions(+), 19 deletions(-) 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 (