# 현재 구현 계획: pop-dashboard 4가지 아이템 모드 완성 > **작성일**: 2026-02-10 > **상태**: 코딩 완료 (방어 로직 패치 포함) > **목적**: 대시보드 설정 패널의 미구현/버그 4건을 수정하여 KPI카드, 차트, 게이지, 통계카드 모두 실제 데이터로 동작하도록 완성 --- ## 1. 문제 요약 pop-dashboard 컴포넌트의 4가지 아이템 모드 중 **설정 UI가 누락**되거나 **데이터 처리 로직에 버그**가 있어 실제 테스트 불가. | # | 문제 | 심각도 | 영향 | |---|------|--------|------| | BUG-1 | 차트: groupBy 설정 UI 없음 | 높음 | 차트가 단일 값만 표시, X축 카테고리 분류 불가 | | BUG-2 | 차트: xAxisColumn 설정 UI 없음 | 높음 | groupBy 결과의 컬럼명과 xKey 불일치로 차트 빈 화면 | | BUG-3 | 통계 카드: 카테고리 설정 UI 없음 | 높음 | statConfig.categories를 설정할 방법 없음 | | BUG-4 | 통계 카드: 카테고리별 필터 미적용 | 높음 | 모든 카테고리에 동일 값(rows.length) 입력되는 버그 | --- ## 2. 수정 대상 파일 (2개) ### 파일 A: `frontend/lib/registry/pop-components/pop-dashboard/PopDashboardConfig.tsx` **변경 유형**: 설정 UI 추가 3건 #### 변경 A-1: DataSourceEditor에 groupBy 설정 추가 (라인 253~362 부근, 집계 함수 아래) 집계 함수가 선택된 상태에서 "그룹핑 컬럼"을 선택할 수 있는 드롭다운 추가. **추가할 위치**: `{/* 집계 함수 + 대상 컬럼 */}` 블록 다음, `{/* 자동 새로고침 */}` 블록 이전 **추가할 코드** (약 50줄): ```tsx {/* 그룹핑 (차트용 X축 분류) */} {dataSource.aggregation && (
컬럼을 찾을 수 없습니다. {columns.map((col) => ( { const current = dataSource.aggregation?.groupBy ?? []; const isSelected = current.includes(col.name); const newGroupBy = isSelected ? current.filter((g) => g !== col.name) : [...current, col.name]; onChange({ ...dataSource, aggregation: { ...dataSource.aggregation!, groupBy: newGroupBy.length > 0 ? newGroupBy : undefined, }, }); setGroupByOpen(false); }} className="text-xs" > {col.name} ({col.type}) ))}

차트에서 X축 카테고리로 사용됩니다

)} ``` **필요한 state 추가** (DataSourceEditor 내부, 기존 state 옆): ```tsx const [groupByOpen, setGroupByOpen] = useState(false); ``` #### 변경 A-2: 차트 타입 설정에 xAxisColumn/yAxisColumn 추가 (라인 1187~1212 부근) **추가할 위치**: `{item.subType === "chart" && (` 블록 내부, 차트 유형 Select 다음 **추가할 코드** (약 30줄): ```tsx {/* X축 컬럼 */}
onUpdate({ ...item, chartConfig: { ...item.chartConfig, chartType: item.chartConfig?.chartType ?? "bar", xAxisColumn: e.target.value || undefined, }, }) } placeholder="groupBy 컬럼명 (비우면 자동)" className="h-8 text-xs" />

그룹핑 컬럼명과 동일하게 입력. 비우면 첫 번째 groupBy 컬럼 사용

``` #### 변경 A-3: 통계 카드 타입에 카테고리 설정 UI 추가 (라인 1272 부근, 게이지 설정 다음) **추가할 위치**: `{item.subType === "gauge" && (` 블록 다음에 새 블록 추가 **추가할 코드** (약 100줄): `StatCategoryEditor` 인라인 블록 ```tsx {item.subType === "stat-card" && (
{(item.statConfig?.categories ?? []).map((cat, catIdx) => (
{ const newCats = [...(item.statConfig?.categories ?? [])]; newCats[catIdx] = { ...cat, label: e.target.value }; onUpdate({ ...item, statConfig: { ...item.statConfig, categories: newCats }, }); }} placeholder="라벨 (예: 수주)" className="h-6 flex-1 text-xs" /> { const newCats = [...(item.statConfig?.categories ?? [])]; newCats[catIdx] = { ...cat, color: e.target.value || undefined }; onUpdate({ ...item, statConfig: { ...item.statConfig, categories: newCats }, }); }} placeholder="#색상코드" className="h-6 w-20 text-xs" />
{/* 필터 조건: 컬럼 / 연산자 / 값 */}
{ const newCats = [...(item.statConfig?.categories ?? [])]; newCats[catIdx] = { ...cat, filter: { ...cat.filter, column: e.target.value }, }; onUpdate({ ...item, statConfig: { ...item.statConfig, categories: newCats }, }); }} placeholder="컬럼" className="h-6 w-20 text-[10px]" /> { const newCats = [...(item.statConfig?.categories ?? [])]; newCats[catIdx] = { ...cat, filter: { ...cat.filter, value: e.target.value }, }; onUpdate({ ...item, statConfig: { ...item.statConfig, categories: newCats }, }); }} placeholder="값" className="h-6 flex-1 text-[10px]" />
))} {(item.statConfig?.categories ?? []).length === 0 && (

카테고리를 추가하면 각 조건에 맞는 건수가 표시됩니다

)}
)} ``` --- ### 파일 B: `frontend/lib/registry/pop-components/pop-dashboard/PopDashboardComponent.tsx` **변경 유형**: 데이터 처리 로직 수정 2건 #### 변경 B-1: 차트 xAxisColumn 자동 설정 (라인 276~283 부근) 차트에 groupBy가 있지만 xAxisColumn이 설정되지 않은 경우, 첫 번째 groupBy 컬럼을 자동으로 xAxisColumn에 반영. **현재 코드** (라인 276~283): ```tsx case "chart": return ( ); ``` **변경 코드**: ```tsx case "chart": { // groupBy가 있지만 xAxisColumn이 미설정이면 자동 보정 const chartItem = { ...item }; if ( item.dataSource.aggregation?.groupBy?.length && !item.chartConfig?.xAxisColumn ) { chartItem.chartConfig = { ...chartItem.chartConfig, chartType: chartItem.chartConfig?.chartType ?? "bar", xAxisColumn: item.dataSource.aggregation.groupBy[0], }; } return ( ); } ``` #### 변경 B-2: 통계 카드 카테고리별 독립 데이터 조회 (라인 286~297) **현재 코드** (버그): ```tsx case "stat-card": { const categoryData: Record = {}; if (item.statConfig?.categories) { for (const cat of item.statConfig.categories) { categoryData[cat.label] = itemData.rows.length; // BUG: 모든 카테고리에 동일 값 } } return ( ); } ``` **변경 코드**: ```tsx case "stat-card": { const categoryData: Record = {}; if (item.statConfig?.categories) { for (const cat of item.statConfig.categories) { if (cat.filter.column && cat.filter.value) { // 카테고리 필터로 rows 필터링 const filtered = itemData.rows.filter((row) => { const cellValue = String(row[cat.filter.column] ?? ""); const filterValue = String(cat.filter.value ?? ""); switch (cat.filter.operator) { case "=": return cellValue === filterValue; case "!=": return cellValue !== filterValue; case "like": return cellValue.toLowerCase().includes(filterValue.toLowerCase()); default: return cellValue === filterValue; } }); categoryData[cat.label] = filtered.length; } else { categoryData[cat.label] = itemData.rows.length; } } } return ( ); } ``` **주의**: 이 방식은 rows에 전체 데이터가 있어야 작동함. 통계 카드의 dataSource에 집계 함수를 설정하지 않고(또는 groupBy를 사용하여) 원시 rows를 가져와야 한다. --- ## 3. 구현 순서 (의존성 기반) | 순서 | 작업 | 파일 | 의존성 | 상태 | |------|------|------|--------|------| | 1 | A-1: DataSourceEditor에 groupBy UI 추가 | PopDashboardConfig.tsx | 없음 | [x] | | 2 | A-2: 차트 xAxisColumn 입력 UI 추가 | PopDashboardConfig.tsx | 없음 | [x] | | 3 | A-3: 통계 카드 카테고리 설정 UI 추가 | PopDashboardConfig.tsx | 없음 | [x] | | 4 | B-1: 차트 xAxisColumn 자동 보정 로직 | PopDashboardComponent.tsx | 순서 1 | [x] | | 5 | B-2: 통계 카드 카테고리별 필터 적용 | PopDashboardComponent.tsx | 순서 3 | [x] | | 6 | 린트 검사 | 전체 | 순서 1~5 | [x] | | 7 | C-1: SQL 빌더 방어 로직 (빈 컬럼/테이블 차단) | dataFetcher.ts | 없음 | [x] | | 8 | C-2: refreshInterval 최소값 강제 (5초) | PopDashboardComponent.tsx | 없음 | [x] | | 9 | 브라우저 테스트 | - | 순서 1~8 | [ ] | 순서 1, 2, 3은 서로 독립이므로 병렬 가능. 순서 4는 순서 1의 groupBy 값이 있어야 의미 있음. 순서 5는 순서 3의 카테고리 설정이 있어야 의미 있음. 순서 7, 8은 백엔드 부하 방지를 위한 방어 패치. --- ## 4. 사전 충돌 검사 결과 ### 새로 추가할 식별자 목록 | 식별자 | 타입 | 정의 파일 | 사용 파일 | 충돌 여부 | |--------|------|-----------|-----------|-----------| | `groupByOpen` | state (boolean) | PopDashboardConfig.tsx DataSourceEditor 내부 | 동일 함수 내부 | 충돌 없음 | | `setGroupByOpen` | state setter | PopDashboardConfig.tsx DataSourceEditor 내부 | 동일 함수 내부 | 충돌 없음 | | `chartItem` | const (DashboardItem) | PopDashboardComponent.tsx renderSingleItem 내부 | 동일 함수 내부 | 충돌 없음 | **Grep 검색 결과** (전체 pop-dashboard 폴더): - `groupByOpen`: 0건 - 충돌 없음 - `setGroupByOpen`: 0건 - 충돌 없음 - `groupByColumns`: 0건 - 충돌 없음 - `chartItem`: 0건 - 충돌 없음 - `StatCategoryEditor`: 0건 - 충돌 없음 - `loadCategoryData`: 0건 - 충돌 없음 ### 기존 타입/함수 재사용 목록 | 기존 식별자 | 정의 위치 | 이번 수정에서 사용하는 곳 | |------------|-----------|------------------------| | `DataSourceConfig.aggregation.groupBy` | types.ts 라인 155 | A-1 UI에서 읽기/쓰기 | | `ChartItemConfig.xAxisColumn` | types.ts 라인 248 | A-2 UI, B-1 자동 보정 | | `StatCategory` | types.ts 라인 261 | A-3 카테고리 편집 | | `StatCardConfig.categories` | types.ts 라인 268 | A-3 UI에서 읽기/쓰기 | | `FilterOperator` | types.ts (import 이미 존재) | A-3 카테고리 필터 Select | | `columns` (state) | PopDashboardConfig.tsx DataSourceEditor 내부 | A-1 groupBy 컬럼 목록 | **사용처 있는데 정의 누락된 항목: 없음** --- ## 5. 에러 함정 경고 ### 함정 1: 차트에 groupBy만 설정하고 xAxisColumn을 비우면 ChartItem은 기본 xKey로 `"name"`을 사용하는데, groupBy 결과 행은 `{status: "수주", value: 79}` 형태. `name` 키가 없으므로 X축이 빈 채로 렌더링됨. **B-1의 자동 보정 로직이 필수**. 순서 4를 빠뜨리면 차트가 깨짐. ### 함정 2: 통계 카드에 집계 함수를 설정하면 집계(COUNT 등)가 설정되면 rows에 `[{value: 87}]` 하나만 들어옴. 카테고리별 필터링이 작동하려면 **집계 함수를 "없음"으로 두거나, groupBy를 설정**해야 개별 행이 rows에 포함됨. 통계 카드에서는 **집계를 사용하지 않는 것이 올바른 사용법**. 설정 가이드 문서에 이 점을 명시해야 함. ### 함정 3: PopDashboardConfig.tsx의 import 누락 현재 `FilterOperator`는 이미 import되어 있음 (라인 54). `StatCategory`는 직접 사용하지 않고 `item.statConfig.categories` 구조로 접근하므로 import 불필요. **새로운 import 추가 필요 없음.** ### 함정 4: 통계 카드 카테고리 필터에서 숫자 비교 `String(row[col])` vs `String(filter.value)` 비교이므로, 숫자 컬럼도 문자열로 비교됨. `"100" === "100"`은 정상 동작하지만, `"100.00" !== "100"`이 될 수 있음. 현재 대부분 컬럼이 varchar이므로 문제없지만, numeric 컬럼 사용 시 주의. ### 함정 5: DataSourceEditor의 columns state 타이밍 `groupByOpen` Popover에서 `columns` 배열을 사용하는데, 테이블 선택 직후 columns가 아직 로딩 중일 수 있음. 기존 코드에서 `loadingCols` 상태로 버튼을 disabled 처리하고 있으므로 문제없음. --- ## 6. 검증 방법 ### 차트 (BUG-1, BUG-2) 1. 아이템 추가 > "차트" 선택 2. 테이블: `sales_order_mng`, 집계: COUNT, 컬럼: `id`, 그룹핑: `status` 3. 차트 유형: 막대 차트 4. 기대 결과: X축에 "수주", "진행중", "완료" / Y축에 79, 7, 1 ### 통계 카드 (BUG-3, BUG-4) 1. 아이템 추가 > "통계 카드" 선택 2. 테이블: `sales_order_mng`, **집계: 없음** (중요!) 3. 카테고리 추가: - "수주" / status / = / 수주 - "진행중" / status / = / 진행중 - "완료" / status / = / 완료 4. 기대 결과: 수주 79, 진행중 7, 완료 1 --- ## 이전 완료 계획 (아카이브)
POP 뷰어 스크롤 수정 (완료) - [x] 라인 185: overflow-hidden 제거 - [x] 라인 266: overflow-auto 공통 적용 - [x] 라인 275: 일반 모드 min-h-full 추가 - [x] 린트 검사 통과
POP 뷰어 실제 컴포넌트 렌더링 (완료) - [x] 뷰어 페이지에 레지스트리 초기화 import 추가 - [x] renderActualComponent() 실제 컴포넌트 렌더링으로 교체 - [x] 린트 검사 통과
V2/V2 컴포넌트 설정 스키마 정비 (완료) - [x] 레거시 컴포넌트 스키마 제거 - [x] V2 컴포넌트 overrides 스키마 정의 (16개) - [x] V2 컴포넌트 overrides 스키마 정의 (9개) - [x] componentConfig.ts 한 파일에서 통합 관리
화면 복제 기능 개선 (진행 중) - [완료] DB 구조 개편 (menu_objid 의존성 제거) - [완료] 복제 옵션 정리 - [완료] 화면 간 연결 복제 버그 수정 - [대기] 화면 간 연결 복제 테스트 - [대기] 제어관리 복제 테스트 - [대기] 추가 옵션 복제 테스트