3883031c0b
INV Studio 데이터 뷰 시리즈. 솔루션 개발 단계라 backward-compat alias 없이 깔끔하게. Backend: - TableManagementController + Service: /aggregate, /aggregate-group, /select-rows endpoint 추가 sanitize + hasColumn 검증 + buildAggregateWhere 공유 헬퍼 Frontend canonical view components (신규): - stats: DB-first KPI editor (CPSegment 메타 chip, 컬럼 dropdown, 디자인 모드 debounce 350ms preview) - chart: recharts (bar / horizontalBar / line / donut) - card-list: title/subtitles/metrics 카드 카탈로그 (list / grid 레이아웃) - grouped-table: 클라이언트 측 groupBy + 그룹 헤더 row Canonical container (Phase G.2 / G.2.5 / G.2.6): - containerType='tabs' 활성 탭만 mount, ChildSlot 으로 자식 렌더 - ScreenDesigner.handleComponentDrop 가 canonical container tabs 도 인식 - 우측 V2PropertiesPanel 4-way 분기: tab child / panel child / selected / empty nested path update + saveToHistory, delete handler 동기화 Shared utilities: - useDbColumns hook (모듈 캐시), ColumnPicker (CPSelect 기반) - OptionFilterRow 자연어 카드 형식 (컬럼 dropdown / 조건 select / 값 입력) - _shared/use-table-rows.ts (cardList + groupedTable 공용 fetch) - IconPicker: 한글 키워드 80+ alias, 휠 스크롤 fix, 360px 상한, 결과 80→300 stats DB-first UX (Phase G.4.x): - DB / 정적 모드 이분법 제거 — 항상 dataSource 시작 - collapsed: 라벨 input + KpiMetaSegment chip (테이블 · 집계 · 컬럼 · 필터수) - expanded: 데이터 / 필터 / 외형 / 고급 flat CP rows - useSlideToggle hook 으로 펼침/닫힘 양방향 애니메이션 - 변화량 (delta) 수동 입력 UI 제거 — 향후 DB 자동 계산 영역 - 카드 fetch state 명시: loading / error / 대기 중 / 테이블 미설정 기타: - ScreenDesigner.tsx → InvyoneStudio.tsx rename (활성 빌더 파일) - 모든 hardcoded #6c5ce7 fallback 제거, hsl(var(--primary)) 토큰만 사용 (light/dark/테마 자동 적응) - StatsDefinition default_config 도 DB-first placeholder (value: 0 박지 않음) Docs: - notes/gbpark/2026-05-14-studio-data-view-roadmap.md (G.0 ~ G.4.2 진행 기록) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
90 lines
2.5 KiB
TypeScript
90 lines
2.5 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* useDbColumns — DB 테이블의 컬럼 목록을 mount/tableName 변경 시 lazy 로드.
|
|
*
|
|
* 사용처: stats / chart / cardList / groupedTable 의 컬럼 선택 dropdown.
|
|
* 모듈 스코프 Map 캐시로 같은 테이블 중복 요청 방지.
|
|
*/
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
|
|
export interface DbColumnOption {
|
|
value: string;
|
|
label: string;
|
|
data_type?: string;
|
|
}
|
|
|
|
const cache = new Map<string, DbColumnOption[]>();
|
|
const inflight = new Map<string, Promise<DbColumnOption[]>>();
|
|
|
|
export interface UseDbColumnsResult {
|
|
columns: DbColumnOption[];
|
|
loading: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
export function useDbColumns(tableName?: string): UseDbColumnsResult {
|
|
const [columns, setColumns] = useState<DbColumnOption[]>(() =>
|
|
tableName ? cache.get(tableName) ?? [] : [],
|
|
);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | undefined>(undefined);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
setError(undefined);
|
|
if (!tableName) {
|
|
setColumns([]);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
const cached = cache.get(tableName);
|
|
if (cached) {
|
|
setColumns(cached);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
const promise =
|
|
inflight.get(tableName) ??
|
|
(async () => {
|
|
const { tableManagementApi } = await import("@/lib/api/tableManagement");
|
|
const res = await tableManagementApi.getColumnList(tableName, 1000);
|
|
const rows = (res?.success ? res.data?.columns ?? [] : []) as any[];
|
|
const opts: DbColumnOption[] = rows
|
|
.map((c) => ({
|
|
value: c?.column_name ?? c?.columnName ?? "",
|
|
label:
|
|
(c?.display_name ?? c?.displayName ?? "") ||
|
|
c?.column_name ||
|
|
c?.columnName ||
|
|
"",
|
|
data_type: c?.data_type ?? c?.dataType,
|
|
}))
|
|
.filter((o) => !!o.value);
|
|
cache.set(tableName, opts);
|
|
return opts;
|
|
})();
|
|
inflight.set(tableName, promise);
|
|
promise
|
|
.then((opts) => {
|
|
if (cancelled) return;
|
|
setColumns(opts);
|
|
})
|
|
.catch((e) => {
|
|
if (cancelled) return;
|
|
setError(e?.message ?? "컬럼 목록 로드 실패");
|
|
})
|
|
.finally(() => {
|
|
inflight.delete(tableName);
|
|
if (!cancelled) setLoading(false);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [tableName]);
|
|
|
|
return useMemo(() => ({ columns, loading, error }), [columns, loading, error]);
|
|
}
|