ERP-node/popdocs/PLAN.md

23 KiB

POP 개발 계획


현재 상태 (2026-02-11)

Phase 0 공통 인프라 (usePopEvent + useDataSource) 구현 완료, ksh-v2-work 병합 + 원격 push 완료. Phase 2 pop-button 설계 진행 중.


작업 순서

[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)


현재 구현 계획

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

대상: Phase 0 공통 인프라 (usePopEvent + useDataSource 훅)

배경 (2026-02-11)

모든 데이터 연동 POP 컴포넌트(pop-button, pop-table, pop-search 등)가 공유하는 2개 핵심 훅을 구현한다.

  • usePopEvent: 같은 화면(screenId) 안에서 컴포넌트 간 이벤트 통신 (publish/subscribe)
  • useDataSource: DB 테이블 CRUD 통합 (조회/생성/수정/삭제)

핵심 원칙: 새로 만드는 것이 아니라, 기존 코드를 공통화하는 작업이다.

  • useDataSource는 대시보드의 dataFetcher.ts 조회 로직 + 기존 dataApi CRUD를 훅으로 래핑
  • usePopEvent는 신규 구현 (Map 기반 이벤트 버스)
  • 대시보드는 이번에 교체하지 않는다 (훅 안정화 후 별도 교체)

결정사항 (2026-02-11)

항목 결정 이유
usePopEvent 범위 같은 screenId 안에서만 통신 화면 간 의존성 방지
usePopEvent 모달 같은 screenId면 모달 안 컴포넌트도 통신 가능 모달은 별도 화면이 아님
useDataSource 조회 분기 집계/조인이면 SQL 빌더 + executeQuery, 단순이면 dataApi.getTableData 대시보드 dataFetcher.ts와 동일 전략
useDataSource CRUD dataApi.createRecord/updateRecord/deleteRecord 래핑 백엔드 API 이미 완성됨
대시보드 교체 시점 이번에 하지 않음, 훅 안정화 후 별도 작업 안정성 우선
SQL 빌더 위치 dataFetcher.ts에서 추출하여 별도 유틸로 분리 훅과 대시보드 모두 사용
이벤트 버스 저장소 전역 Map (screenId -> EventEmitter) React 외부에서도 접근 가능, GC 관리 용이

구현 순서 (의존성 기반)

순서 파일 작업 의존성 상태
1 hooks/pop/usePopEvent.ts 이벤트 버스 훅 (신규) 없음 완료
2 hooks/pop/popSqlBuilder.ts SQL 빌더 유틸 분리 (dataFetcher.ts에서 추출) 없음 완료
3 hooks/pop/useDataSource.ts 데이터 CRUD 훅 (신규) 2 완료
4 hooks/pop/index.ts 배럴 파일 (re-export) 1, 3 완료

STEP 1: usePopEvent.ts (신규 생성)

파일: frontend/hooks/pop/usePopEvent.ts

역할: 같은 화면(screenId) 안에서 컴포넌트 간 이벤트 통신

핵심 구조:

전역 저장소 (React 외부)
screenBuses: Map<string, Map<string, Set<callback>>>
   │
   └── screenId: "S001"
        ├── "supplier-selected" → [콜백A, 콜백B]
        ├── "data-saved" → [콜백C]
        └── sharedData: Map<string, unknown>

sharedDataStore: Map<string, Map<string, unknown>>
   │
   └── screenId: "S001"
        ├── "selectedSupplier" → { id: "SUP-001", name: "삼성" }
        └── "inputQuantity" → 50

외부 API (훅이 반환하는 것):

function usePopEvent(screenId: string) {
  return {
    publish,       // (eventName, payload) => void
    subscribe,     // (eventName, callback) => unsubscribe 함수
    getSharedData, // (key) => unknown
    setSharedData, // (key, value) => void
  };
}

상세 구현 명세:

  1. 전역 Map 저장소 (모듈 스코프, React 외부)

    • screenBuses: Map<string, Map<string, Set<Function>>> - 이벤트 리스너
    • sharedDataStore: Map<string, Map<string, unknown>> - 공유 데이터
  2. publish(eventName, payload)

    • 해당 screenId의 eventName에 등록된 모든 콜백을 순회하며 payload 전달
    • 등록된 리스너가 없으면 아무 일도 안 함 (에러 아님)
  3. subscribe(eventName, callback)

    • 해당 screenId의 eventName에 콜백 등록
    • 반환값: unsubscribe 함수
    • useEffect 내부에서 호출되어야 함 (cleanup으로 unsubscribe)
  4. getSharedData(key) / setSharedData(key, value)

    • screenId별 격리된 key-value 저장소
    • publish/subscribe는 "이벤트"(일회성), sharedData는 "상태"(지속)
    • 용도: 버튼이 저장할 때 다른 컴포넌트들의 현재 값을 수집
  5. cleanupScreen(screenId) (내부 유틸)

    • 화면 언마운트 시 해당 screenId의 모든 리스너 + sharedData 정리
    • 메모리 누수 방지

사용 예시:

// 거래처 선택 버튼
const { publish, setSharedData } = usePopEvent("S001");

const onSelect = (supplier) => {
  setSharedData("selectedSupplier", supplier);           // 상태 저장
  publish("supplier-selected", { supplierId: supplier.id }); // 이벤트 발행
};

// 발주 테이블 (다른 컴포넌트)
const { subscribe } = usePopEvent("S001");

useEffect(() => {
  const unsub = subscribe("supplier-selected", (payload) => {
    refetch({ filters: { supplier_id: payload.supplierId } });
  });
  return unsub; // cleanup
}, []);

// 저장 버튼 (다른 컴포넌트)
const { getSharedData } = usePopEvent("S001");

const handleSave = () => {
  const supplier = getSharedData("selectedSupplier"); // 다른 컴포넌트가 저장한 값 수집
  const quantity = getSharedData("inputQuantity");
  save({ supplier_id: supplier.id, quantity });
};

STEP 2: popSqlBuilder.ts (신규 생성 - dataFetcher.ts에서 추출)

파일: frontend/hooks/pop/popSqlBuilder.ts

역할: DataSourceConfig를 SQL 문자열로 변환하는 순수 유틸리티

기존 dataFetcher.ts에서 그대로 추출할 함수 5개 (로직 변경 없음):

함수 원본 위치 (dataFetcher.ts) 역할
escapeSQL(value) 라인 41~48 SQL 값 이스케이프
sanitizeIdentifier(name) 라인 124~127 테이블/컬럼명 위험 문자 제거
validateDataSourceConfig(config) 라인 59~88 설정 완료 여부 검증
buildWhereClause(filters) 라인 93~118 필터 -> WHERE 절 변환
buildAggregationSQL(config) 라인 137~215 DataSourceConfig -> SELECT SQL 변환

export 대상: validateDataSourceConfig, buildAggregationSQL (나머지는 내부 함수)

주의: dataFetcher.ts는 수정하지 않는다. 대시보드가 계속 사용 중이므로, 복사만 한다. 대시보드 교체 시점에 dataFetcher.ts에서 이 파일을 import하도록 변경할 예정.


STEP 3: useDataSource.ts (신규 생성)

파일: frontend/hooks/pop/useDataSource.ts

역할: DataSourceConfig 기반 DB 테이블 CRUD 통합 훅

내부 의존성:

  • popSqlBuilder.ts - SQL 빌더 (STEP 2)
  • @/lib/api/data - dataApi (기존, 조회/생성/수정/삭제)
  • @/lib/api/dashboard - dashboardApi (기존, SQL 직접 실행)
  • @/lib/api/client - apiClient (기존, axios 기반)

외부 API (훅이 반환하는 것):

function useDataSource(config: DataSourceConfig) {
  return {
    // 상태
    data: { rows: [], value: 0, total: 0 },
    loading: boolean,
    error: string | null,

    // 조회
    refetch: (overrideFilters?) => Promise<void>,

    // 쓰기
    save: (record) => Promise<MutationResult>,
    update: (id, record) => Promise<MutationResult>,
    remove: (id) => Promise<MutationResult>,
  };
}

MutationResult 타입 (신규):

interface MutationResult {
  success: boolean;
  data?: any;
  error?: string;
}

조회 분기 로직 (핵심):

config에 aggregation 또는 joins가 있는가?
├── YES → buildAggregationSQL(config) → apiClient.post("/dashboards/execute-query")
│         (대시보드와 동일한 경로, SQL 직접 실행)
│         실패 시 → dashboardApi.executeQuery() 폴백
│
└── NO  → dataApi.getTableData(tableName, { page, size, filters, sortBy, sortOrder })
          (단순 테이블 조회)

CRUD 메서드 구현:

메서드 내부 호출 비고
save(record) dataApi.createRecord(config.tableName, record) company_code 자동 추가는 백엔드가 처리
update(id, record) dataApi.updateRecord(config.tableName, id, record)
remove(id) dataApi.deleteRecord(config.tableName, id) 복합키 객체도 지원

자동 새로고침:

useEffect(() => {
  if (config.tableName) refetch();

  if (config.refreshInterval && config.refreshInterval > 0) {
    const sec = Math.max(5, config.refreshInterval); // 최소 5초
    const timer = setInterval(refetch, sec * 1000);
    return () => clearInterval(timer);
  }
}, [config.tableName, config.refreshInterval]);

refetch 오버라이드 필터:

// 기본 조회
refetch();

// 필터 추가하여 조회 (usePopEvent와 연동 시)
refetch({ filters: { supplier_id: "SUP-001" } });

내부적으로 overrideFilters가 있으면 config.filters에 병합하여 조회한다.

사용 예시:

// 대시보드 스타일 (집계)
const { data, loading } = useDataSource({
  tableName: "sales_order",
  aggregation: { type: "sum", column: "amount", groupBy: ["category"] },
  refreshInterval: 30,
});
// data.rows → [{ category: "A", value: 1500 }, ...]
// data.value → 첫 번째 행의 value

// 테이블 스타일 (목록)
const { data, refetch } = useDataSource({
  tableName: "purchase_order",
  sort: [{ column: "created_at", direction: "desc" }],
  limit: 20,
});
// data.rows → [{ id: 1, item_name: "볼트", ... }, ...]
// data.total → 전체 행 수

// 버튼 스타일 (저장만)
const { save, remove, loading } = useDataSource({
  tableName: "inbound_record",
});
const result = await save({ supplier_id: "SUP-001", quantity: 50 });
// result.success → true/false

STEP 4: index.ts (신규 생성 - 배럴 파일)

파일: frontend/hooks/pop/index.ts

export { usePopEvent, cleanupScreen } from "./usePopEvent";
export { useDataSource } from "./useDataSource";
export type { MutationResult } from "./useDataSource";
export { buildAggregationSQL, validateDataSourceConfig } from "./popSqlBuilder";

외부에서 사용할 때: import { usePopEvent, useDataSource } from "@/hooks/pop";


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

이름 유형 검색 범위 검색 결과 판정
usePopEvent 훅 이름 frontend 전체 주석 2건 (PopDashboardComponent.tsx 라인 10, @INFRA-EXTRACT 교체 예정 주석) 충돌 없음 (실제 코드 아님)
useDataSource 훅 이름 frontend 전체 주석 4건 (dataFetcher.ts 라인 4,227,355 + PopDashboardComponent.tsx 라인 9, 모두 @INFRA-EXTRACT 주석) 충돌 없음 (실제 코드 아님)
PopEventBus 클래스명 frontend 전체 1건 (PopDashboardComponent.tsx 라인 10, @INFRA-EXTRACT 주석 내) 충돌 없음 (주석)
MutationResult 타입명 frontend 전체 0건 충돌 없음
popSqlBuilder 파일명 frontend 전체 0건 충돌 없음
cleanupScreen 함수명 frontend 전체 0건 충돌 없음
screenBuses 변수명 frontend 전체 0건 충돌 없음
sharedDataStore 변수명 frontend 전체 0건 충돌 없음
buildAggregationSQL 함수명 frontend 전체 2건 (dataFetcher.ts 정의 + PopDashboardComponent에서 import) 충돌 주의: 동일 이름을 popSqlBuilder.ts에서 재정의. 대시보드는 여전히 dataFetcher.ts 것을 사용하므로 런타임 충돌 없음. 향후 대시보드 교체 시 import 경로만 변경.
validateDataSourceConfig 함수명 frontend 전체 1건 (dataFetcher.ts 정의) 위와 동일
escapeSQL 함수명 frontend 전체 1건 (dataFetcher.ts 내부 함수) 충돌 없음 (export 안 됨)
sanitizeIdentifier 함수명 frontend 전체 1건 (dataFetcher.ts 내부 함수) 충돌 없음 (export 안 됨)
AggregatedResult 타입명 frontend 전체 1건 (dataFetcher.ts 정의) 충돌 주의: useDataSource 내부에서 동일 타입 사용. types.ts 정의를 공유하므로 별도 재정의 불필요.

정의-사용 매핑

정의 정의 위치 사용 위치
usePopEvent hooks/pop/usePopEvent.ts (신규) pop-button (Phase 2), pop-table (Phase 3), pop-search (Phase 4) 등
useDataSource hooks/pop/useDataSource.ts (신규) pop-button (Phase 2), pop-table (Phase 3), pop-dashboard (향후 교체) 등
MutationResult hooks/pop/useDataSource.ts (신규) useDataSource 반환 타입으로 사용
buildAggregationSQL hooks/pop/popSqlBuilder.ts (신규) useDataSource 내부에서 호출
validateDataSourceConfig hooks/pop/popSqlBuilder.ts (신규) useDataSource 내부에서 호출
cleanupScreen hooks/pop/usePopEvent.ts (신규) 화면 언마운트 시 호출 (PopRenderer 또는 뷰어에서)
DataSourceConfig lib/registry/pop-components/types.ts (기존) useDataSource 파라미터 타입
DataSourceFilter lib/registry/pop-components/types.ts (기존) popSqlBuilder 내부
dataApi lib/api/data.ts (기존) useDataSource 내부에서 CRUD 호출
dashboardApi lib/api/dashboard.ts (기존) useDataSource 내부에서 SQL 실행 폴백
apiClient lib/api/client.ts (기존) useDataSource 내부에서 SQL 실행 1차

누락 검사: 모든 신규 정의에 사용처 있음. 모든 사용처에 정의 존재. 단, cleanupScreen의 호출 시점은 Phase 2 이후 뷰어 통합 시 결정 (이번에는 export만 해둠).


함정 경고

번호 위험 설명 해결 방안
W1 subscribe를 useEffect 밖에서 호출하면 메모리 누수 subscribe는 콜백을 등록하므로, 컴포넌트 언마운트 시 해제해야 함 subscribe의 반환값(unsubscribe)을 useEffect cleanup에서 호출. JSDoc에 사용 패턴 명시.
W2 DataSourceConfig의 import 경로 DataSourceConfiglib/registry/pop-components/types.ts에 정의됨. hooks 디렉토리에서 import 시 경로가 김 @/lib/registry/pop-components/types로 import. 별도 re-export 하지 않음 (타입 중복 방지).
W3 buildAggregationSQL 동일 이름 2곳 dataFetcher.ts와 popSqlBuilder.ts에 같은 이름의 함수가 존재 의도적 복사. 대시보드는 dataFetcher.ts, 새 컴포넌트는 popSqlBuilder.ts 사용. 향후 대시보드 교체 시 dataFetcher.ts를 popSqlBuilder.ts import로 변경.
W4 apiClient.post와 dashboardApi.executeQuery 이중 경로 대시보드 dataFetcher.ts에서 apiClient 우선 + dashboardApi 폴백 패턴을 그대로 복사함 동일 패턴 유지 (안정성 검증 완료). 향후 하나로 통합 가능.
W5 refetch overrideFilters와 config.filters 병합 순서 overrideFilters가 config.filters를 완전 대체하는지, 추가하는지 모호 추가(append) 방식: config.filters + overrideFilters를 합침. overrideFilters에 같은 column이 있으면 덮어씀. 이 동작을 JSDoc에 명시.
W6 SSR 환경에서 전역 Map Next.js SSR에서 전역 Map이 서버/클라이언트 간 공유될 수 있음 typeof window !== "undefined" 가드. 이벤트 버스는 클라이언트 전용.
W7 hooks/pop/ 디렉토리 신규 frontend/hooks/pop/ 디렉토리가 존재하지 않음 STEP 1에서 파일 생성 시 디렉토리 자동 생성됨. 수동으로 mkdir 불필요.

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

코드 레벨
  • usePopEvent - publish/subscribe 기본 동작 (같은 screenId)
  • usePopEvent - 다른 screenId 간 격리 확인
  • usePopEvent - subscribe cleanup (메모리 누수 없음)
  • usePopEvent - sharedData set/get
  • useDataSource - 단순 조회 (aggregation 없음 → dataApi.getTableData)
  • useDataSource - 집계 조회 (aggregation 있음 → SQL 빌더 → executeQuery)
  • useDataSource - save/update/remove
  • useDataSource - loading/error 상태 관리
  • useDataSource - refreshInterval 자동 새로고침
  • popSqlBuilder - buildAggregationSQL이 dataFetcher.ts와 동일 결과 생성
  • TypeScript 컴파일 에러 0건
  • 린트 에러 0건
  • 기존 대시보드 동작에 영향 없음 (dataFetcher.ts 미수정)
구조 레벨
  • frontend/hooks/pop/ 디렉토리 생성됨
  • index.ts 배럴 파일에서 모든 public API export 됨
  • @/hooks/pop으로 import 가능

이전 완료된 계획 (보관)

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

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

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

브레이크포인트 (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-11 (Phase 0 공통 인프라 완료, Phase 2 pop-button 설계 시작)