ERP-node/popdocs/PLAN.md

21 KiB

POP 개발 계획


현재 상태 (2026-02-12)

대시보드 집계 함수 설정 유효성 검증 강화 (v2 구현 완료, 브라우저 확인 대기)


작업 순서

[Phase 1~3]      [Phase 5]           [정의서]        [Phase 0~6]
v4 Flexbox    →  v5 CSS Grid      →  컴포넌트 설계  →  실제 구현
   완료            완료 (v5.2)         완료 (v8.0)      다음

완료된 Phase

Phase 1~3: v4 Flexbox 시스템 (완료, 레거시 삭제됨)

v4 Flexbox 기반 시스템은 v5 CSS Grid로 완전히 대체되었습니다. v4 관련 파일은 모두 삭제되었습니다.

  • v4 기본 구조, 렌더러, 디자이너 통합
  • Undo/Redo, 드래그 리사이즈, Flexbox 가로 배치
  • 비율 스케일링 시스템
  • 오버라이드 기능 (모드별 배치 고정)
  • 컴포넌트 표시/숨김, 줄바꿈

Phase 5: v5 CSS Grid 시스템 (완료)

Phase 5.1: 타입 정의 (완료)

  • PopLayoutDataV5 인터페이스
  • PopGridConfig, PopGridPosition 타입
  • GridMode, GRID_BREAKPOINTS 상수
  • createEmptyPopLayoutV5(), isV5Layout(), detectGridMode()

Phase 5.2: 그리드 렌더러 (완료)

  • PopRenderer.tsx - CSS Grid 기반 렌더링
  • 격자 셀 렌더링 (CSS Grid 동일 좌표계)
  • 위치 변환 (12칸 -> 4/6/8칸)

Phase 5.3: 디자이너 UI (완료)

  • PopCanvas.tsx - 그리드 캔버스 + 행/열 라벨
  • 드래그 스냅 (칸에 맞춤)
  • ComponentEditorPanel.tsx - 위치 편집

Phase 5.4: 반응형 자동화 (완료)

  • 자동 변환 알고리즘 (12칸 -> 4칸)
  • 겹침 감지 및 재배치
  • 모드별 오버라이드 저장

v5.1 추가 기능 (완료)

  • 자동 줄바꿈 (col > maxCol -> 맨 아래 배치)
  • "검토 필요" 알림 시스템
  • Gap 프리셋 (좁게/보통/넓게)
  • 숨김 기능 (모드별)

v5.2 브레이크포인트 재설계 + 세로 자동 확장 (완료)

  • 기기 기반 브레이크포인트 (479/767/1023px)
  • 세로 자동 확장 (dynamicCanvasHeight)
  • 뷰어 반응형 일관성 (detectGridMode 사용)
  • VIEWPORT_PRESETS에서 height 제거

컴포넌트 설계 (완료)

  • 9개 컴포넌트 정의 (POPUPDATE_2.md v8.0)
  • POP 헌법 9조 작성
  • 공통 인프라 설계 (DataSourceConfig, ColumnBinding, JoinConfig, useDataSource, usePopEvent, PopActionConfig)
  • 모달 화면 설계 방식 확정 (인라인 + 외부 참조)
  • 기존 시스템 호환성 검증 (DB/백엔드/프론트 변경 불필요 확인)

다음 작업

Phase 0: 공통 인프라

모든 데이터 연동 컴포넌트가 공유하는 기반 시스템:

  • ColumnBinding 타입 정의 (read/write/readwrite/hidden) -- types.ts에 추가 완료
  • JoinConfig 타입 정의 (테이블 조인) -- types.ts에 추가 완료
  • DataSourceConfig 타입 정의 (데이터 소스 설정) -- types.ts에 추가 완료
  • PopActionConfig 타입 정의 (액션 설정) -- types.ts에 추가 완료
  • usePopEvent 훅 구현 (이벤트 버스, 데이터 전달, 화면 단위 격리) -- 완료
  • useDataSource 훅 구현 (CRUD 포함, 기존 dataApi 활용) -- 완료

Phase 1: pop-dashboard -- 완료

2026-02-10: 17단계 코딩 + 검수 + 팔레트 등록 완료

  • PopDashboardConfig, DashboardItem 타입 정의
  • 멀티 아이템 컨테이너 구현 (여러 아이템 묶음)
  • 4개 서브타입: kpi-card, chart, gauge, stat-card
  • 4개 표시 모드: arrows, auto-slide, grid, scroll
  • 계산식(formula) 지원: "생산량/총재고량" 같은 복합 표현
  • 설정 패널: 드롭다운 기반 쉬운 집계 설정 (SQL 불필요)
  • PopComponentRegistry 등록 + 디자이너 팔레트 등록
  • 이벤트: filter_changed 수신, kpi_clicked 발행 -- usePopEvent 완성 후
  • 기존 components/pop/dashboard/ 폴더 폐기 (모든 기능 대체 확인 후)

Phase 2: pop-button, pop-icon

  • pop-button: 저장/삭제/API 호출 액션
  • pop-icon: 화면 이동/URL/새로고침

Phase 3: pop-table (테이블형 우선)

  • table-list 서브타입: 행/열 장부형
  • ColumnBinding 기반 컬럼별 read/write
  • card-list 서브타입: 카드 템플릿 (후순위)

Phase 4: pop-search, pop-field, pop-lookup

  • pop-search: 필터 조건 입력 (text/date/select/combo)
  • pop-field: 저장용 입력 (text/number/date/select/numpad)
  • pop-lookup: 모달 값 선택 (인라인 + 외부 참조)

Phase 5: 고도화

  • pop-table 카드 템플릿 디자이너

Phase 6: pop-system

  • 프로필, 테마, 대시보드 보이기/숨기기 통합

후속 작업

  • 워크플로우 연동 (버튼 액션, 화면 전환)
  • 실기기 테스트 (아이폰 SE, iPad Mini 등)

참고 문서: POPUPDATE_2.md (컴포넌트 정의서 v8.0)


현재 구현 계획

용도: 이 섹션은 "지금 바로 실행할 구체적 계획"입니다. 새 세션에서 이 섹션만 읽으면 코딩을 시작할 수 있어야 합니다. 완료되면 다음 기능의 계획으로 교체합니다.

대상: 대시보드 집계 함수 설정 유효성 검증 강화 (v2 - 시뮬레이션 검증 완료)

배경 (2026-02-12)

팀원이 브랜치를 pull 받은 뒤 대시보드에서 500 에러가 발생. 원인: batch_mappings 테이블의 to_table_name(문자열) 컬럼에 SUM 집계를 설정한 대시보드 아이템이 존재. PostgreSQL이 SELECT SUM(to_table_name) 실행 시 function sum(character varying) does not exist 에러 반환.

근본 원인: 설정 UI(PopDashboardConfig.tsx)에서 아이템 타입(subType)에 관계없이 동일한 5개 집계 함수를 모두 보여주고, 컬럼 선택에서도 타입 구분 없이 모든 컬럼이 표시됨.

시뮬레이션 결과 (2026-02-12)

8개 시나리오로 가상 코딩 → 데이터 흐름 추적 수행. 원래 7단계 계획에서 1건의 심각한 결함 발견:

chart(sum) → stat-card로 subType 변경 시, Select UI에서 "sum"이 목록에 없어도 내부 value는 "sum"으로 유지되어 SQL 생성 시 그대로 적용됨.

이를 해결하기 위해 STEP 7.5 추가: ItemEditor의 subType 변경 핸들러에서 비호환 aggregation 자동 전환.


수정 대상

파일 경로 변경 유형
PopDashboardConfig.tsx frontend/lib/registry/pop-components/pop-dashboard/PopDashboardConfig.tsx 수정 (신규 파일 없음)

구현 순서 (의존성 기반, 8단계)

순서 작업 수정 위치 (원본 라인) 의존성 상태
1 import에 AggregationType 추가 + 유틸 상수/함수 6개 추가 라인 48~63, 라인 133 뒤 없음 완료
2 DataSourceEditor props에 subType 추가 라인 149~155 없음 완료
3 집계 함수 Select를 동적 생성으로 교체 라인 288~294 1, 2 완료
4 집계 함수 변경 시 컬럼 자동 초기화 + groupBy 보존 라인 271~283 1 완료
5 대상 컬럼 Combobox 숫자 필터링 + 안내 메시지 라인 333~366 1 완료
6 chart 모드 groupBy 미설정 경고 라인 445~447 2 완료
7 DataSourceEditor 호출부에 subType 전달 (라인 1212) 라인 1212~1215 2 완료
7.5 ItemEditor subType 변경 시 비호환 aggregation 자동 전환 라인 1147~1148 1 완료

STEP 1: import 추가 + 유틸 상수/함수

1-A. import 추가 (라인 48~63)

현재:

import type {
  PopDashboardConfig,
  DashboardItem,
  DashboardSubType,
  DashboardDisplayMode,
  DataSourceConfig,
  DataSourceFilter,
  FilterOperator,
  FormulaConfig,
  ItemVisibility,
  DashboardCell,
  DashboardPage,
  JoinConfig,
  JoinType,
  ItemStyleConfig,
} from "../types";

변경: ItemStyleConfig, 뒤에 AggregationType, 추가:

import type {
  PopDashboardConfig,
  DashboardItem,
  DashboardSubType,
  DashboardDisplayMode,
  DataSourceConfig,
  DataSourceFilter,
  FilterOperator,
  FormulaConfig,
  ItemVisibility,
  DashboardCell,
  DashboardPage,
  JoinConfig,
  JoinType,
  ItemStyleConfig,
  AggregationType,
} from "../types";

1-B. 유틸 상수/함수 추가 (라인 133 }; 직후, 라인 135 const FILTER_OPERATOR_LABELS 직전)

// ===== 집계 함수 유효성 검증 유틸 =====

// 아이템 타입별 사용 가능한 집계 함수
const SUBTYPE_AGGREGATION_MAP: Record<DashboardSubType, AggregationType[]> = {
  "kpi-card": ["count", "sum", "avg", "min", "max"],
  chart: ["count", "sum", "avg", "min", "max"],
  gauge: ["count", "sum", "avg", "min", "max"],
  "stat-card": ["count"],
};

// 집계 함수 라벨
const AGGREGATION_LABELS: Record<AggregationType, string> = {
  count: "건수 (COUNT)",
  sum: "합계 (SUM)",
  avg: "평균 (AVG)",
  min: "최소 (MIN)",
  max: "최대 (MAX)",
};

// 숫자 전용 집계 함수 (숫자 컬럼에만 사용 가능)
const NUMERIC_ONLY_AGGREGATIONS: AggregationType[] = ["sum", "avg"];

// PostgreSQL 숫자 타입 판별용 패턴
const NUMERIC_TYPE_PATTERNS = [
  "int", "integer", "bigint", "smallint",
  "numeric", "decimal", "real", "double",
  "float", "serial", "bigserial", "smallserial",
  "money", "number",
];

/** 컬럼이 숫자 타입인지 판별 */
function isNumericColumn(col: ColumnInfo): boolean {
  const t = (col.type || "").toLowerCase();
  const u = (col.udtName || "").toLowerCase();
  return NUMERIC_TYPE_PATTERNS.some(
    (pattern) => t.includes(pattern) || u.includes(pattern)
  );
}

/** 현재 집계 함수가 숫자 전용(sum/avg)인지 판별 */
function isNumericOnlyAggregation(aggType: string | undefined): boolean {
  return !!aggType && NUMERIC_ONLY_AGGREGATIONS.includes(aggType as AggregationType);
}

STEP 2: DataSourceEditor props에 subType 추가

라인 149~155 교체:

// 현재
function DataSourceEditor({
  dataSource,
  onChange,
}: {
  dataSource: DataSourceConfig;
  onChange: (ds: DataSourceConfig) => void;
}) {

// 변경
function DataSourceEditor({
  dataSource,
  onChange,
  subType,
}: {
  dataSource: DataSourceConfig;
  onChange: (ds: DataSourceConfig) => void;
  subType?: DashboardSubType;
}) {

subType은 optional. FormulaEditor 내부 호출(라인 969)에서는 미전달 → undefined → 5개 전부 표시.


STEP 3: 집계 함수 Select 동적 생성

라인 288~294 교체:

// 현재
<SelectContent>
  <SelectItem value="count">건수 (COUNT)</SelectItem>
  <SelectItem value="sum">합계 (SUM)</SelectItem>
  <SelectItem value="avg">평균 (AVG)</SelectItem>
  <SelectItem value="min">최소 (MIN)</SelectItem>
  <SelectItem value="max">최대 (MAX)</SelectItem>
</SelectContent>

// 변경
<SelectContent>
  {(subType
    ? SUBTYPE_AGGREGATION_MAP[subType]
    : (Object.keys(AGGREGATION_LABELS) as AggregationType[])
  ).map((aggType) => (
    <SelectItem key={aggType} value={aggType}>
      {AGGREGATION_LABELS[aggType]}
    </SelectItem>
  ))}
</SelectContent>

STEP 4: 집계 함수 변경 시 컬럼 자동 초기화 + groupBy 보존

라인 271~283 교체 (onValueChange 핸들러 전체):

// 현재
onValueChange={(val) =>
  onChange({
    ...dataSource,
    aggregation: val
      ? {
          type: val as NonNullable<
            DataSourceConfig["aggregation"]
          >["type"],
          column: dataSource.aggregation?.column ?? "",
        }
      : undefined,
  })
}

// 변경
onValueChange={(val) => {
  // 숫자 전용 집계로 변경 시, 기존 컬럼이 숫자가 아니면 초기화
  let currentColumn = dataSource.aggregation?.column ?? "";
  if (val && isNumericOnlyAggregation(val) && currentColumn) {
    const selectedCol = columns.find((c) => c.name === currentColumn);
    if (selectedCol && !isNumericColumn(selectedCol)) {
      currentColumn = "";
    }
  }
  onChange({
    ...dataSource,
    aggregation: val
      ? {
          type: val as NonNullable<
            DataSourceConfig["aggregation"]
          >["type"],
          column: currentColumn,
          groupBy: dataSource.aggregation?.groupBy,
        }
      : undefined,
  });
}}

기존 대비 변경점 2가지:

  1. 숫자 전용 집계 + 문자열 컬럼 → currentColumn = ""
  2. groupBy 보존 (기존 코드에서 누락되던 버그 수정)

STEP 5: 대상 컬럼 Combobox 필터링

5-A. CommandEmpty 메시지 교체 (라인 333~335):

// 현재
<CommandEmpty className="py-2 text-center text-xs">
  컬럼을 찾을  없습니다.
</CommandEmpty>

// 변경
<CommandEmpty className="py-2 text-center text-xs">
  {isNumericOnlyAggregation(dataSource.aggregation?.type)
    ? "숫자 타입 컬럼이 없습니다."
    : "컬럼을 찾을 수 없습니다."}
</CommandEmpty>

5-B. columns.map을 필터링 후 map으로 교체 (라인 337):

// 현재
{columns.map((col) => (

// 변경
{(isNumericOnlyAggregation(dataSource.aggregation?.type)
  ? columns.filter(isNumericColumn)
  : columns
).map((col) => (

나머지 CommandItem 내부는 변경 없음.


STEP 6: chart 모드 groupBy 경고

라인 445~447 뒤에 추가 (기존 <p> 태그는 유지):

// 기존 유지
<p className="mt-0.5 text-[10px] text-muted-foreground">
  차트에서 X축 카테고리로 사용됩니다
</p>

// 아래에 추가
{subType === "chart" && !dataSource.aggregation?.groupBy?.length && (
  <p className="mt-0.5 text-[10px] text-destructive">
    차트 모드에서는 그룹핑(X축) 설정해야 의미 있는 차트가 표시됩니다
  </p>
)}

STEP 7: DataSourceEditor 호출부에 subType 전달

라인 1212~1215 (ItemEditor 내부, 단일 집계 모드):

// 현재
<DataSourceEditor
  dataSource={item.dataSource}
  onChange={(ds) => onUpdate({ ...item, dataSource: ds })}
/>

// 변경
<DataSourceEditor
  dataSource={item.dataSource}
  onChange={(ds) => onUpdate({ ...item, dataSource: ds })}
  subType={item.subType}
/>

라인 969 (FormulaEditor 내부): 수정하지 않음 (subType=undefined → 모든 집계 표시).


STEP 7.5: ItemEditor subType 변경 시 비호환 aggregation 자동 전환

시뮬레이션에서 발견: chart(aggregation=sum) → stat-card로 subType 변경 시, Select 목록에 "sum"이 없지만 내부 value는 "sum" 유지 → SQL 생성 시 SUM 실행됨. radix Select는 목록에 없는 value를 자동으로 초기화하지 않음.

라인 1147~1148 (ItemEditor 내부, subType Select onValueChange):

// 현재
onValueChange={(val) =>
  onUpdate({ ...item, subType: val as DashboardSubType })
}

// 변경
onValueChange={(val) => {
  const newSubType = val as DashboardSubType;
  const allowedAggs = SUBTYPE_AGGREGATION_MAP[newSubType];
  const currentAggType = item.dataSource.aggregation?.type;
  // 새 subType에서 현재 집계 함수가 허용 안 되면 첫 번째 허용 함수로 전환
  let newDataSource = item.dataSource;
  if (currentAggType && !allowedAggs.includes(currentAggType)) {
    newDataSource = {
      ...item.dataSource,
      aggregation: item.dataSource.aggregation
        ? {
            ...item.dataSource.aggregation,
            type: allowedAggs[0],
            column: allowedAggs[0] === "count" ? "" : item.dataSource.aggregation.column,
          }
        : undefined,
    };
  }
  onUpdate({ ...item, subType: newSubType, dataSource: newDataSource });
}}

핵심: allowedAggs[0]이 "count"면 column도 빈 문자열로 (count는 column 불필요).


사전 충돌 검사 결과 (2026-02-12)

이름 유형 검색 범위 검색 결과 판정
SUBTYPE_AGGREGATION_MAP 상수 frontend 전체 0건 충돌 없음
AGGREGATION_LABELS 상수 frontend 전체 0건 충돌 없음
NUMERIC_ONLY_AGGREGATIONS 상수 frontend 전체 0건 충돌 없음
NUMERIC_TYPE_PATTERNS 상수 frontend 전체 0건 충돌 없음
isNumericColumn 함수 frontend 전체 0건 충돌 없음
isNumericOnlyAggregation 함수 frontend 전체 0건 충돌 없음
AggregationType type import PopDashboardConfig.tsx 0건 (현재 미import) 충돌 없음 (추가 필요)

정의-사용 매핑

정의 정의 위치 사용 위치
SUBTYPE_AGGREGATION_MAP STEP 1 (상수) STEP 3, STEP 7.5
AGGREGATION_LABELS STEP 1 (상수) STEP 3
NUMERIC_ONLY_AGGREGATIONS STEP 1 (상수) isNumericOnlyAggregation 내부
NUMERIC_TYPE_PATTERNS STEP 1 (상수) isNumericColumn 내부
isNumericColumn STEP 1 (함수) STEP 4, STEP 5
isNumericOnlyAggregation STEP 1 (함수) STEP 4, STEP 5
subType (prop) STEP 2 (DataSourceEditor) STEP 3, STEP 6
AggregationType types.ts L123 (기존) STEP 1 import 추가
ColumnInfo dataFetcher.ts (기존, L71에서 import) STEP 1 isNumericColumn 파라미터
DashboardSubType types.ts (기존, L48에서 이미 import) STEP 2 prop 타입, STEP 7.5

누락 검사: 모든 신규 정의에 사용처 있음. 모든 사용처에 정의 존재. 누락 없음.


함정 경고

# 심각도 위험 설명 해결 방안
W1 높음 import 누락 AggregationType을 import에 추가 안 하면 컴파일 에러 STEP 1-A에서 반드시 추가
W2 높음 subType 변경 시 비호환 aggregation 잔류 chart(sum) → stat-card 시 Select 목록에 없는 값이 내부에 남음 STEP 7.5에서 해결 (시뮬레이션 발견)
W3 중간 groupBy 소실 (기존 버그) 기존 코드에서 집계 함수 변경 시 groupBy가 누락됨 STEP 4에서 groupBy 명시적 보존
W4 낮음 FormulaEditor 호출 미수정 FormulaEditor 내부 DataSourceEditor에는 subType 미전달 의도적 결정 (수식 모드는 모든 집계 허용). 절대 수정하지 말 것 (연쇄 변경 발생)
W5 낮음 columns 비동기 로드 테이블 방금 선택 → columns=[] → STEP 4 초기화 안 됨 허용. 컬럼 로드 후 재변경 시 정상 동작
W6 정보 MIN/MAX 문자열 허용 PostgreSQL MIN/MAX는 문자열에도 작동 (사전순) 의도적. NUMERIC_ONLY에 포함 안 함
W7 정보 STEP 7.5에서 column 초기화 조건 allowedAggs[0] === "count"이면 column="" (count는 * 사용) stat-card → count 전환 시 올바른 동작

작업 완료 후 확인 체크리스트

코드 레벨
  • AggregationType import 추가 확인
  • stat-card: 집계 함수 Select에 "건수 (COUNT)"만 표시
  • kpi-card/chart/gauge: 5개 집계 함수 전부 표시
  • SUM 선택 시 컬럼 목록에 숫자 타입만 표시
  • COUNT 선택 시 컬럼 목록에 모든 타입 표시
  • COUNT→SUM 변경 시 문자열 컬럼 자동 초기화
  • SUM→COUNT 변경 시 숫자 컬럼 유지
  • 집계 함수 변경 시 groupBy 보존
  • chart→stat-card subType 변경 시 sum→count 자동 전환 (STEP 7.5)
  • chart 모드에서 groupBy 미설정 시 빨간 경고 표시
  • FormulaEditor 내부에서 5개 전부 표시 (subType 미전달 → 기본 전체 목록)
  • TypeScript 컴파일 에러 0건
  • 린트 에러 0건
  • 기존 저장된 대시보드 데이터 조회 동작에 영향 없음 (브라우저 확인 필요)
구조 레벨
  • 수정 파일 1개: PopDashboardConfig.tsx
  • 신규 파일 0개
  • 기존 파일 삭제 0개

이전 완료된 계획 (보관)

Phase 0 공통 인프라 (2026-02-11, 완료): usePopEvent + useDataSource + popSqlBuilder 구현.

대시보드 스타일 정리 (2026-02-11, 완료): 글자 크기 커스텀 제거, 라벨 정렬만 유지, stale closure 수정, .next 캐시 해결.

브라우저 확인 체크리스트 (대기):

  • 라벨 정렬, 페이지 미리보기, 차트 디자인, 게이지/통계카드 동작 확인

브레이크포인트 (v5.2 현재)

모드 화면 너비 칸 수 대상 기기
mobile_portrait ~479px 4칸 아이폰 SE ~ 갤럭시 S
mobile_landscape 480~767px 6칸 스마트폰 가로
tablet_portrait 768~1023px 8칸 8~10인치 태블릿 세로
tablet_landscape 1024px~ 12칸 10~14인치 태블릿 가로

관련 문서

문서 내용
STATUS.md 현재 진행 상태
SPEC.md 기술 스펙
ARCHITECTURE.md 코드 구조
POPUPDATE_2.md 컴포넌트 정의서 v8.0 (최신)
components-spec.md 컴포넌트 상세 설계 (v4 기준, 갱신 필요)
decisions/005 브레이크포인트 재설계 ADR

최종 업데이트: 2026-02-12 (대시보드 집계 함수 유효성 검증 v2 - 시뮬레이션 검증 완료, STEP 7.5 추가)