diff --git a/PLAN.MD b/PLAN.MD index e4f4e424..45468fa4 100644 --- a/PLAN.MD +++ b/PLAN.MD @@ -1,135 +1,527 @@ -# 현재 구현 계획: POP 뷰어 스크롤 수정 +# 현재 구현 계획: pop-dashboard 4가지 아이템 모드 완성 -> **작성일**: 2026-02-09 -> **상태**: 계획 완료, 코딩 대기 -> **목적**: 뷰어에서 화면 높이를 초과하는 컴포넌트가 잘리지 않고 스크롤 가능하도록 수정 +> **작성일**: 2026-02-10 +> **상태**: 코딩 완료 (방어 로직 패치 포함) +> **목적**: 대시보드 설정 패널의 미구현/버그 4건을 수정하여 KPI카드, 차트, 게이지, 통계카드 모두 실제 데이터로 동작하도록 완성 --- ## 1. 문제 요약 -설계(디자이너)에서 컴포넌트를 아래로 배치하면 캔버스가 늘어나고 스크롤이 되지만, -뷰어(`/pop/screens/4114`)에서는 화면 높이를 초과하는 컴포넌트가 잘려서 안 보임. +pop-dashboard 컴포넌트의 4가지 아이템 모드 중 **설정 UI가 누락**되거나 **데이터 처리 로직에 버그**가 있어 실제 테스트 불가. -**근본 원인**: CSS 컨테이너 구조가 스크롤을 차단 - -| # | 컨테이너 (라인) | 현재 클래스 | 문제 | -|---|----------------|-------------|------| -| 1 | 최외곽 (185) | `h-screen ... overflow-hidden` | 넘치는 콘텐츠를 잘라냄 | -| 2 | 컨텐츠 영역 (266) | 일반 모드에 `overflow-auto` 없음 | 스크롤 불가 | -| 3 | 백색 배경 (275) | 일반 모드에 `min-h-full` 없음 | 짧은 콘텐츠 시 배경 불완전 | +| # | 문제 | 심각도 | 영향 | +|---|------|--------|------| +| BUG-1 | 차트: groupBy 설정 UI 없음 | 높음 | 차트가 단일 값만 표시, X축 카테고리 분류 불가 | +| BUG-2 | 차트: xAxisColumn 설정 UI 없음 | 높음 | groupBy 결과의 컬럼명과 xKey 불일치로 차트 빈 화면 | +| BUG-3 | 통계 카드: 카테고리 설정 UI 없음 | 높음 | statConfig.categories를 설정할 방법 없음 | +| BUG-4 | 통계 카드: 카테고리별 필터 미적용 | 높음 | 모든 카테고리에 동일 값(rows.length) 입력되는 버그 | --- -## 2. 수정 대상 파일 (1개) +## 2. 수정 대상 파일 (2개) -### `frontend/app/(pop)/pop/screens/[screenId]/page.tsx` +### 파일 A: `frontend/lib/registry/pop-components/pop-dashboard/PopDashboardConfig.tsx` -**변경 유형**: CSS 클래스 문자열 수정 3곳 (새 변수/함수/타입 추가 없음) +**변경 유형**: 설정 UI 추가 3건 -#### 변경 1: 라인 185 - 최외곽 컨테이너 +#### 변경 A-1: DataSourceEditor에 groupBy 설정 추가 (라인 253~362 부근, 집계 함수 아래) -**현재 코드**: +집계 함수가 선택된 상태에서 "그룹핑 컬럼"을 선택할 수 있는 드롭다운 추가. + +**추가할 위치**: `{/* 집계 함수 + 대상 컬럼 */}` 블록 다음, `{/* 자동 새로고침 */}` 블록 이전 + +**추가할 코드** (약 50줄): + +```tsx +{/* 그룹핑 (차트용 X축 분류) */} +{dataSource.aggregation && ( +
+ 차트에서 X축 카테고리로 사용됩니다 +
++ 그룹핑 컬럼명과 동일하게 입력. 비우면 첫 번째 groupBy 컬럼 사용 +
++ 카테고리를 추가하면 각 조건에 맞는 건수가 표시됩니다 +
+ )} ++ 차트에서 X축 카테고리로 사용됩니다 +
++ 그룹핑 컬럼명과 동일하게 입력. 비우면 첫 번째 groupBy 컬럼 사용 +
++ 집계 결과 컬럼. 비우면 집계 함수 결과를 자동 사용 +
++ 카테고리를 추가하면 각 조건에 맞는 건수가 표시됩니다 +
+ )} ++
{item.label}
)} diff --git a/frontend/lib/registry/pop-components/pop-dashboard/items/GaugeItem.tsx b/frontend/lib/registry/pop-components/pop-dashboard/items/GaugeItem.tsx index e2b5dd30..c7313a85 100644 --- a/frontend/lib/registry/pop-components/pop-dashboard/items/GaugeItem.tsx +++ b/frontend/lib/registry/pop-components/pop-dashboard/items/GaugeItem.tsx @@ -69,10 +69,10 @@ export function GaugeItemComponent({ const largeArcFlag = percentage > 50 ? 1 : 0; return ( -+
{item.label}
)} @@ -128,7 +128,7 @@ export function GaugeItemComponent({ {/* 목표값 */} {visibility.showTarget && ( -+
목표: {abbreviateNumber(target)}
)} diff --git a/frontend/lib/registry/pop-components/pop-dashboard/items/KpiCard.tsx b/frontend/lib/registry/pop-components/pop-dashboard/items/KpiCard.tsx index 1cb09e74..29db2791 100644 --- a/frontend/lib/registry/pop-components/pop-dashboard/items/KpiCard.tsx +++ b/frontend/lib/registry/pop-components/pop-dashboard/items/KpiCard.tsx @@ -66,10 +66,10 @@ export function KpiCardComponent({ const valueColor = getColorForValue(displayValue, kpiConfig?.colorRanges); return ( -+
{item.label}
)} @@ -78,7 +78,7 @@ export function KpiCardComponent({ {visibility.showValue && (+
{item.formula?.values.map((v) => v.label).join(" / ")}
)} diff --git a/frontend/lib/registry/pop-components/pop-dashboard/items/StatCard.tsx b/frontend/lib/registry/pop-components/pop-dashboard/items/StatCard.tsx index f12e4e05..c3c02e7b 100644 --- a/frontend/lib/registry/pop-components/pop-dashboard/items/StatCard.tsx +++ b/frontend/lib/registry/pop-components/pop-dashboard/items/StatCard.tsx @@ -37,10 +37,10 @@ export function StatCardComponent({ item, categoryData }: StatCardProps) { const total = Object.values(categoryData).reduce((sum, v) => sum + v, 0); return ( -+
{item.label}
)} @@ -80,7 +80,7 @@ export function StatCardComponent({ item, categoryData }: StatCardProps) { {/* 보조 라벨 (단위 등) */} {visibility.showSubLabel && ( -+
{visibility.showUnit && item.kpiConfig?.unit
? `단위: ${item.kpiConfig.unit}`
: ""}
diff --git a/frontend/lib/registry/pop-components/pop-dashboard/modes/GridMode.tsx b/frontend/lib/registry/pop-components/pop-dashboard/modes/GridMode.tsx
index 66c4f5e9..5e339fc5 100644
--- a/frontend/lib/registry/pop-components/pop-dashboard/modes/GridMode.tsx
+++ b/frontend/lib/registry/pop-components/pop-dashboard/modes/GridMode.tsx
@@ -18,7 +18,7 @@ import type { DashboardCell } from "../../types";
// ===== 상수 =====
/** 셀 하나의 최소 너비 (px). 이보다 좁아지면 열 수 축소 */
-const MIN_CELL_WIDTH = 160;
+const MIN_CELL_WIDTH = 80;
// ===== Props =====
diff --git a/frontend/lib/registry/pop-components/pop-dashboard/utils/dataFetcher.ts b/frontend/lib/registry/pop-components/pop-dashboard/utils/dataFetcher.ts
index 64860699..4746b69b 100644
--- a/frontend/lib/registry/pop-components/pop-dashboard/utils/dataFetcher.ts
+++ b/frontend/lib/registry/pop-components/pop-dashboard/utils/dataFetcher.ts
@@ -46,13 +46,55 @@ function escapeSQL(value: unknown): string {
return `'${str}'`;
}
+// ===== 설정 완료 여부 검증 =====
+
+/**
+ * DataSourceConfig의 필수값이 모두 채워졌는지 검증
+ * 설정 중간 상태(테이블 미선택, 컬럼 미선택 등)에서는
+ * SQL을 생성하지 않도록 사전 차단
+ *
+ * @returns null이면 유효, 문자열이면 미완료 사유
+ */
+function validateDataSourceConfig(config: DataSourceConfig): string | null {
+ // 테이블명 필수
+ if (!config.tableName || !config.tableName.trim()) {
+ return "테이블이 선택되지 않았습니다";
+ }
+
+ // 집계 함수가 설정되었으면 대상 컬럼도 필수
+ // (단, COUNT는 컬럼 없이도 COUNT(*)로 처리 가능)
+ if (config.aggregation) {
+ const aggType = config.aggregation.type?.toLowerCase();
+ const aggCol = config.aggregation.column?.trim();
+ if (aggType !== "count" && !aggCol) {
+ return `${config.aggregation.type.toUpperCase()} 집계에 대상 컬럼이 필요합니다`;
+ }
+ }
+
+ // 조인이 있으면 조인 조건 필수
+ if (config.joins?.length) {
+ for (const join of config.joins) {
+ if (!join.targetTable?.trim()) {
+ return "조인 대상 테이블이 선택되지 않았습니다";
+ }
+ if (!join.on.sourceColumn?.trim() || !join.on.targetColumn?.trim()) {
+ return "조인 조건 컬럼이 설정되지 않았습니다";
+ }
+ }
+ }
+
+ return null;
+}
+
// ===== 필터 조건 SQL 생성 =====
/** DataSourceFilter 배열을 WHERE 절 조건문으로 변환 */
function buildWhereClause(filters: DataSourceFilter[]): string {
- if (!filters.length) return "";
+ // 컬럼명이 빈 필터는 무시 (설정 중간 상태 방어)
+ const validFilters = filters.filter((f) => f.column?.trim());
+ if (!validFilters.length) return "";
- const conditions = filters.map((f) => {
+ const conditions = validFilters.map((f) => {
const col = sanitizeIdentifier(f.column);
switch (f.operator) {
@@ -98,8 +140,18 @@ export function buildAggregationSQL(config: DataSourceConfig): string {
let selectClause: string;
if (config.aggregation) {
const aggType = config.aggregation.type.toUpperCase();
- const aggCol = sanitizeIdentifier(config.aggregation.column);
- selectClause = `${aggType}(${aggCol}) as value`;
+ const aggCol = config.aggregation.column?.trim()
+ ? sanitizeIdentifier(config.aggregation.column)
+ : "";
+
+ // COUNT는 컬럼 없으면 COUNT(*), 나머지는 컬럼 필수
+ if (!aggCol) {
+ selectClause = aggType === "COUNT"
+ ? "COUNT(*) as value"
+ : `${aggType}(${tableName}.*) as value`;
+ } else {
+ selectClause = `${aggType}(${aggCol}) as value`;
+ }
// GROUP BY가 있으면 해당 컬럼도 SELECT에 포함
if (config.aggregation.groupBy?.length) {
@@ -110,10 +162,14 @@ export function buildAggregationSQL(config: DataSourceConfig): string {
selectClause = "*";
}
- // FROM 절 (조인 포함)
+ // FROM 절 (조인 포함 - 조건이 완전한 조인만 적용)
let fromClause = tableName;
if (config.joins?.length) {
for (const join of config.joins) {
+ // 조인 설정이 불완전하면 건너뜀 (설정 중간 상태 방어)
+ if (!join.targetTable?.trim() || !join.on.sourceColumn?.trim() || !join.on.targetColumn?.trim()) {
+ continue;
+ }
const joinTable = sanitizeIdentifier(join.targetTable);
const joinType = join.joinType.toUpperCase();
const srcCol = sanitizeIdentifier(join.on.sourceColumn);
@@ -173,6 +229,12 @@ export async function fetchAggregatedData(
config: DataSourceConfig
): Promise