2c0a97f2ba
- components/builder/* 폐기 (12-grid 미완성 빌더 14개 파일)
- components/template-builder/TemplateBuilder.tsx 신규
(자유배치 + 3뷰 + Zustand 스토어 + 드래그/리사이즈/히스토리/격자)
- admin/builder/page.tsx 진입점 전환 (BuilderLayout → TemplateBuilder)
- 타입 정리: FreePosition / TemplateComponent / ViewConfig / Card /
Dashboard / CardConnection 추가, 레거시(GridPosition/TemplateKind/
DEFAULT_COMPONENT_LAYOUTS/CANVAS_KEYWORDS) @deprecated 표기
- v2-* 마이그레이션 1차:
· 완전: v2-table-list (ResizeObserver), v2-table-search-widget (@container)
· 경량: button/input/select/date/text-display/card-display/aggregation-widget
(withContainerQuery HOC)
- 다크 모드 대응: Tailwind dark: variant 21패턴 71곳 치환
- /test-card-responsive PoC 검증 페이지
세션 후반 버그 픽스 (phase1-log §7):
- test-card-responsive (main) 그룹 밖 이동 (AppLayout 탭 시스템 회피)
- useRegistryPalette default_size {width,height}/{w,h} 포맷 정규화
- dark: variant 중복 체인 정리
검증: (A) 반응형 메커니즘, (B) TemplateBuilder UI 통과
(C) 기존 VEX 화면은 마이그레이션 미완 상태라 Phase 2 이후 개별 진행
스펙: notes/gbpark/2026-04-10-card-engine-final-spec.md
로그: notes/gbpark/2026-04-10-card-engine-phase1-log.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1034 lines
32 KiB
TypeScript
1034 lines
32 KiB
TypeScript
// ============================================================================
|
|
// INVYONE 컴포넌트 규격 v1.0 — TypeScript 타입 정의
|
|
// ============================================================================
|
|
//
|
|
// 설계 원칙:
|
|
// 1. 하나의 FieldConfig가 모든 곳(테이블/폼/검색)에서 쓰인다
|
|
// 2. 컴포넌트는 DataPort로 통신한다
|
|
// 3. 설정은 BaseConfig → TypeConfig → InstanceConfig 3단계
|
|
//
|
|
// 기존 vex 타입(types/component.ts, types/screen.ts)과 의존성 없음 — 완전 독립
|
|
// ============================================================================
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 1. FieldConfig — 유일한 필드 규격
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* 필드 렌더링 방식을 결정하는 타입.
|
|
*
|
|
* - 테이블에서는 셀 포맷, 폼에서는 입력 위젯, 검색에서는 필터 UI로 매핑된다.
|
|
* - 새 타입 추가 시 이 union에 리터럴만 추가하면 된다.
|
|
*/
|
|
export type FieldType =
|
|
| 'text' // 일반 문자열
|
|
| 'number' // 숫자 (포맷팅)
|
|
| 'date' // 날짜
|
|
| 'datetime' // 날짜+시간
|
|
| 'select' // 드롭다운 (options 배열)
|
|
| 'entity' // FK 참조 (팝업 검색)
|
|
| 'checkbox' // 체크박스
|
|
| 'textarea' // 장문
|
|
| 'file' // 파일 첨부
|
|
| 'code'; // 자동채번 (readonly)
|
|
|
|
/**
|
|
* entity 타입 필드의 FK 참조 정보.
|
|
* 팝업 검색 시 대상 테이블·표시 컬럼·검색 컬럼을 지정한다.
|
|
*/
|
|
export interface FieldRef {
|
|
/** 참조 대상 테이블 */
|
|
table: string;
|
|
/** 값으로 사용할 컬럼 (PK 등) */
|
|
valueColumn: string;
|
|
/** 화면에 표시할 컬럼 */
|
|
displayColumn: string;
|
|
/** 팝업 검색 대상 컬럼 목록 */
|
|
searchColumns?: string[];
|
|
}
|
|
|
|
/**
|
|
* select 타입의 선택지.
|
|
* - string이면 value=label로 해석 (단순 목록)
|
|
* - { value, label } 객체면 value를 저장, label을 표시 (코드값/표시값 분리)
|
|
*/
|
|
export type FieldOption = string | { value: string; label: string };
|
|
|
|
/**
|
|
* 모든 컴포넌트가 공유하는 유일한 필드 정의.
|
|
*
|
|
* 테이블 컬럼, 폼 입력, 검색 조건 전부 이 하나의 규격으로 표현한다.
|
|
* 렌더러가 `type`을 보고 적절한 UI를 그린다.
|
|
*
|
|
* vex의 ColumnConfig(354줄) + FilterConfig + FormField → 이 ~30줄로 통합.
|
|
*/
|
|
export interface FieldConfig {
|
|
// ─── 식별 ───
|
|
|
|
/** DB 컬럼명 (필드의 유일 키) */
|
|
column: string;
|
|
/** 화면에 표시되는 라벨 */
|
|
label: string;
|
|
|
|
// ─── 타입 ───
|
|
|
|
/** 렌더링 방식을 결정하는 필드 타입 */
|
|
type: FieldType;
|
|
|
|
// ─── 표시 ───
|
|
|
|
/** 화면에 보이는지 여부 */
|
|
visible: boolean;
|
|
/** 표시 순서 (작을수록 먼저) */
|
|
order: number;
|
|
/** 컬럼 너비 (px, 테이블에서 사용) */
|
|
width?: number;
|
|
/** 텍스트 정렬 */
|
|
align?: 'left' | 'center' | 'right';
|
|
|
|
// ─── 입력 ───
|
|
|
|
/** 필수 입력 여부 */
|
|
required: boolean;
|
|
/** 편집 가능 여부 */
|
|
editable: boolean;
|
|
/** 기본값 */
|
|
defaultValue?: unknown;
|
|
/** 입력 힌트 텍스트 */
|
|
placeholder?: string;
|
|
|
|
// ─── 타입별 확장 ───
|
|
|
|
/** select 타입: 선택지 목록
|
|
* - string이면 value=label로 해석
|
|
* - { value, label } 객체면 value를 저장, label을 표시
|
|
* - select 렌더러/검색 파라미터는 항상 value를 저장/전송
|
|
* - 테이블 셀은 label을 표시
|
|
*/
|
|
options?: FieldOption[];
|
|
/** entity 타입: FK 참조 정보 */
|
|
ref?: FieldRef;
|
|
/** 포맷 문자열 (number: '#,##0', date: 'YYYY-MM-DD' 등) */
|
|
format?: string;
|
|
/** 자동 계산 수식 (예: 'quantity * unit_price') */
|
|
computed?: string;
|
|
|
|
// ─── 메타 ───
|
|
|
|
/** PK 여부 */
|
|
pk?: boolean;
|
|
/** 시스템 필드 여부 (company_code 등, 폼에서 숨김) */
|
|
system?: boolean;
|
|
/** 검색 대상 여부 */
|
|
searchable?: boolean;
|
|
/** 정렬 가능 여부 */
|
|
sortable?: boolean;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 2. Component — 유일한 컴포넌트 규격
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* 지원하는 컴포넌트 종류.
|
|
*
|
|
* 새 컴포넌트 추가 시 이 union에 리터럴을 추가하고,
|
|
* ComponentTypeConfigMap에 대응하는 설정 타입을 등록한다.
|
|
*/
|
|
export type ComponentType =
|
|
| 'table' // 데이터 테이블 (목록)
|
|
| 'form' // 입력 폼
|
|
| 'search' // 검색 필터
|
|
| 'button' // 버튼 (단일)
|
|
| 'button-bar' // 버튼 그룹
|
|
| 'tabs' // 탭
|
|
| 'split-panel' // 분할 패널
|
|
| 'title' // 텍스트/제목
|
|
| 'stats' // 통계 카드
|
|
| 'divider' // 구분선
|
|
| 'pagination'; // 페이지네이션
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Position — ⚠️ DEPRECATED 블록 (2026-04-10 폐기 결정)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
//
|
|
// 아래 타입/함수/상수는 12-grid + business/canvas 분기 모델 잔재이다.
|
|
// 진실의 원천: notes/gbpark/2026-04-10-card-engine-final-spec.md
|
|
// 대체 타입: §7 의 FreePosition / TemplateComponent / TemplateViewConfig.
|
|
//
|
|
// 현재 DashboardCard.tsx 가 유일한 내부 사용처이며, Phase 2 에서 해당 파일을
|
|
// Card = Template 인스턴스 모델로 재작성할 때 일괄 제거 예정이다.
|
|
// 새 코드는 이 블록의 타입을 사용하지 말 것.
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* @deprecated Phase 2 DashboardCard 재작성 시 제거 예정.
|
|
* FreePosition { left, top, width, height } 를 사용할 것.
|
|
* 현재 DashboardCard.tsx 만 이 타입을 사용 중.
|
|
*/
|
|
export interface GridPosition {
|
|
/** 시작 컬럼 (1~12) */
|
|
col: number;
|
|
/** 차지 컬럼 수 (1~12) */
|
|
colSpan: number;
|
|
/** 명시적 행 번호 (생략 시 auto placement) */
|
|
row?: number;
|
|
/** 행 높이 배수 (기본 1) */
|
|
rowSpan?: number;
|
|
/** 카드 너비별 반응형 오버라이드 */
|
|
responsive?: ResponsiveGridOverride;
|
|
}
|
|
|
|
/**
|
|
* @deprecated Phase 2 DashboardCard 재작성 시 제거 예정.
|
|
* FreePosition { left, top, width, height } 로 통합됐다.
|
|
* 현재 DashboardCard.tsx 만 이 타입을 사용 중.
|
|
*/
|
|
export interface AbsolutePosition {
|
|
/** 가로 위치 (px) */
|
|
x: number;
|
|
/** 세로 위치 (px) */
|
|
y: number;
|
|
/** 너비 (px) */
|
|
w: number;
|
|
/** 높이 (px) */
|
|
h: number;
|
|
}
|
|
|
|
/**
|
|
* @deprecated Phase 2 DashboardCard 재작성 시 제거 예정.
|
|
* 단일 자유배치 모델로 union 분기가 사라졌다. FreePosition 을 사용할 것.
|
|
*/
|
|
export type ComponentPosition = GridPosition | AbsolutePosition;
|
|
|
|
/**
|
|
* @deprecated Phase 2 DashboardCard 재작성 시 제거 예정.
|
|
* 반응형은 컴포넌트 내부 @container 쿼리가 담당한다(스펙 §2).
|
|
* 외부에서 그리드 오버라이드를 주입하는 모델은 폐기됐다.
|
|
*/
|
|
export interface ResponsiveGridOverride {
|
|
narrow?: Partial<Omit<GridPosition, 'responsive'>>;
|
|
normal?: Partial<Omit<GridPosition, 'responsive'>>;
|
|
wide?: Partial<Omit<GridPosition, 'responsive'>>;
|
|
}
|
|
|
|
/**
|
|
* @deprecated Phase 2 DashboardCard 재작성 시 제거 예정.
|
|
* FreePosition 이 단일 위치 모델이므로 판정 가드가 필요 없다.
|
|
*/
|
|
export function isGridPosition(pos: ComponentPosition): pos is GridPosition {
|
|
return pos != null && typeof pos === 'object' && 'col' in pos && 'colSpan' in pos;
|
|
}
|
|
|
|
/**
|
|
* @deprecated Phase 2 DashboardCard 재작성 시 제거 예정.
|
|
* FreePosition 이 단일 위치 모델이므로 판정 가드가 필요 없다.
|
|
*/
|
|
export function isAbsolutePosition(pos: ComponentPosition): pos is AbsolutePosition {
|
|
return pos != null && typeof pos === 'object' && 'x' in pos && 'y' in pos && 'w' in pos && 'h' in pos;
|
|
}
|
|
|
|
/**
|
|
* @deprecated Phase 2 DashboardCard 재작성 시 제거 예정.
|
|
* business/canvas 분기는 폐기됐다. 모든 Template 은 단일 자유배치 모델이다.
|
|
* 현재 DashboardCard.tsx 만 이 타입을 사용 중.
|
|
*/
|
|
export type TemplateKind = 'business' | 'canvas';
|
|
|
|
/**
|
|
* 컴포넌트의 데이터 소스 바인딩.
|
|
*/
|
|
export interface DataSource {
|
|
/** 바인딩 대상 테이블명 */
|
|
table: string;
|
|
/** 읽기/쓰기 모드 */
|
|
mode: 'read' | 'write' | 'readwrite';
|
|
}
|
|
|
|
/**
|
|
* 모든 컴포넌트의 기본 구조.
|
|
*
|
|
* 타입이 무엇이든 이 구조를 따른다.
|
|
* 타입별 고유 설정은 `config` 필드에 ComponentTypeConfig로 들어간다.
|
|
*/
|
|
export interface Component {
|
|
// ─── 식별 ───
|
|
|
|
/** 컴포넌트 고유 ID */
|
|
id: string;
|
|
/** 컴포넌트 종류 */
|
|
type: ComponentType;
|
|
/** 빌더에서 표시되는 이름 */
|
|
label: string;
|
|
|
|
// ─── 위치 (빌더가 관리) ───
|
|
|
|
/**
|
|
* Template.kind에 따라 해석이 달라짐.
|
|
* - business → GridPosition (col / colSpan / row / responsive)
|
|
* - canvas → AbsolutePosition (x / y / w / h)
|
|
*/
|
|
position: ComponentPosition;
|
|
|
|
// ─── 데이터 바인딩 ───
|
|
|
|
/** 데이터 소스 설정 */
|
|
dataSource?: DataSource;
|
|
/** 이 컴포넌트가 사용하는 필드 목록 (유일한 필드 규격) */
|
|
fields?: FieldConfig[];
|
|
|
|
// ─── 데이터 포트 (컴포넌트 간 통신) ───
|
|
|
|
/** 받는 데이터 포트 */
|
|
inputs?: DataPort[];
|
|
/** 내보내는 데이터 포트 */
|
|
outputs?: DataPort[];
|
|
|
|
// ─── 타입별 설정 ───
|
|
|
|
/** 컴포넌트 타입에 따른 고유 설정 */
|
|
config: ComponentTypeConfig;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 3. DataPort — 컴포넌트 간 통신 규격
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* DataPort가 전달하는 데이터의 형태.
|
|
*
|
|
* | 타입 | 실제 데이터 |
|
|
* |------|------------|
|
|
* | row | Record<string, any> (단일 행) |
|
|
* | rows | Record<string, any>[] (복수 행) |
|
|
* | value | any (단일 값) |
|
|
* | params | Record<string, any> (검색 파라미터) |
|
|
*/
|
|
export type DataPortType =
|
|
| 'row' // 단일 행: Record<string, any>
|
|
| 'rows' // 복수 행: Record<string, any>[]
|
|
| 'value' // 단일 값: any
|
|
| 'params'; // 검색 파라미터: Record<string, any>
|
|
|
|
/**
|
|
* 컴포넌트가 데이터를 주고받는 포트.
|
|
*
|
|
* output.name → input.name 매칭으로 연결된다.
|
|
* 빌더에서 시각적으로 연결을 설정하고, 실행 시 이벤트 버스가 자동 전달한다.
|
|
*/
|
|
export interface DataPort {
|
|
/** 포트 이름 (예: 'selectedRow', 'searchParams') */
|
|
name: string;
|
|
/** 데이터 형태 */
|
|
type: DataPortType;
|
|
/** 연결된 컴포넌트 ID (빌더에서 설정) */
|
|
connectedTo?: string;
|
|
}
|
|
|
|
/**
|
|
* 두 컴포넌트 간 DataPort 연결을 나타낸다.
|
|
*
|
|
* Template.connections 배열에 저장되어 화면 전체의 데이터 흐름을 정의한다.
|
|
*/
|
|
export interface Connection {
|
|
/** 연결 고유 ID */
|
|
id: string;
|
|
/** 출발점 (데이터를 보내는 쪽) */
|
|
from: {
|
|
/** 출발 컴포넌트 ID */
|
|
componentId: string;
|
|
/** 출발 output 포트 이름 */
|
|
port: string;
|
|
};
|
|
/** 도착점 (데이터를 받는 쪽) */
|
|
to: {
|
|
/** 도착 컴포넌트 ID */
|
|
componentId: string;
|
|
/** 도착 input 포트 이름 */
|
|
port: string;
|
|
};
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 4. ComponentTypeConfig — 타입별 설정
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
// --- 4.1 table ---
|
|
|
|
/** 테이블 기본 정렬 설정 */
|
|
export interface DefaultSort {
|
|
/** 정렬 대상 컬럼명 */
|
|
column: string;
|
|
/** 정렬 방향 */
|
|
direction: 'asc' | 'desc';
|
|
}
|
|
|
|
/** 테이블 상단 툴바 표시 설정 */
|
|
export interface TableToolbar {
|
|
/** 엑셀 내보내기 버튼 표시 */
|
|
showExcel: boolean;
|
|
/** 새로고침 버튼 표시 */
|
|
showRefresh: boolean;
|
|
/** 필터 버튼 표시 */
|
|
showFilter: boolean;
|
|
}
|
|
|
|
/** 테이블 스타일 종류 */
|
|
export type TableStyle = 'default' | 'striped' | 'bordered' | 'compact';
|
|
|
|
/**
|
|
* 테이블(목록) 컴포넌트 설정.
|
|
*
|
|
* 기본 포트:
|
|
* - outputs: selectedRow(row), selectedRows(rows)
|
|
* - inputs: searchParams(params), refreshTrigger(value)
|
|
*/
|
|
export interface TableConfig {
|
|
/** 페이지당 행 수 (기본: 20) */
|
|
pageSize: number;
|
|
/** 행 선택 모드 */
|
|
selectionMode: 'none' | 'single' | 'multiple';
|
|
/** 체크박스 컬럼 표시 여부 */
|
|
showCheckbox: boolean;
|
|
/** 인라인 편집 활성화 여부 */
|
|
inlineEdit: boolean;
|
|
/** 화면 로드 시 자동으로 데이터 조회 */
|
|
autoLoad: boolean;
|
|
/** 기본 정렬 (미지정 시 서버 기본) */
|
|
defaultSort?: DefaultSort;
|
|
/** 상단 툴바 설정 */
|
|
toolbar: TableToolbar;
|
|
/** 테이블 스타일 */
|
|
style: TableStyle;
|
|
}
|
|
|
|
// --- 4.2 form ---
|
|
|
|
/** 폼 섹션 구분 (선택) */
|
|
export interface FormSection {
|
|
/** 섹션 라벨 */
|
|
label: string;
|
|
/** 이 섹션에 포함되는 필드의 column 목록 (FieldConfig.column 참조) */
|
|
fields: string[];
|
|
}
|
|
|
|
/** 폼 저장 액션 설정 */
|
|
export interface FormSaveAction {
|
|
/** 저장 방식 */
|
|
method: 'INSERT' | 'UPDATE' | 'UPSERT';
|
|
/** 저장 성공 후 표시 메시지 */
|
|
successMessage?: string;
|
|
/** 저장 후 목록 자동 새로고침 여부 */
|
|
refreshAfterSave: boolean;
|
|
}
|
|
|
|
/**
|
|
* 입력 폼 컴포넌트 설정.
|
|
*
|
|
* 기본 포트:
|
|
* - outputs: formData(row), savedRow(row)
|
|
* - inputs: loadRow(row)
|
|
*/
|
|
export interface FormConfig {
|
|
/** 폼 레이아웃 컬럼 수 */
|
|
columns: 1 | 2 | 3;
|
|
/** 섹션 구분 (미지정 시 단일 섹션) */
|
|
sections?: FormSection[];
|
|
/** 저장 액션 설정 */
|
|
saveAction: FormSaveAction;
|
|
}
|
|
|
|
// --- 4.3 search ---
|
|
|
|
/**
|
|
* 검색 필터 컴포넌트 설정.
|
|
*
|
|
* 기본 포트:
|
|
* - outputs: searchParams(params)
|
|
* - inputs: 없음
|
|
*/
|
|
export interface SearchConfig {
|
|
/** 날짜 범위 검색 활성화 (date 타입 필드를 자동으로 범위 입력으로) */
|
|
dateRangeEnabled: boolean;
|
|
/** 초기화 버튼 표시 */
|
|
showResetButton: boolean;
|
|
/** 입력 시 자동 검색 (디바운스) */
|
|
autoSearch: boolean;
|
|
/** 검색 필드 배치 방식 */
|
|
layout: 'inline' | 'stacked';
|
|
}
|
|
|
|
// --- 4.4 button / button-bar ---
|
|
|
|
/**
|
|
* 버튼 액션 종류 (12종).
|
|
*
|
|
* 새 액션 추가 시 이 union에 리터럴만 추가하고 핸들러를 구현하면 된다.
|
|
*/
|
|
export type ActionType =
|
|
| 'save' // 저장
|
|
| 'edit' // 수정 모드 전환
|
|
| 'delete' // 삭제
|
|
| 'add' // 신규 추가
|
|
| 'cancel' // 취소
|
|
| 'close' // 닫기
|
|
| 'navigate' // 페이지 이동
|
|
| 'popup' // 팝업 열기
|
|
| 'search' // 검색 실행
|
|
| 'reset' // 초기화
|
|
| 'submit' // 제출 (폼)
|
|
| 'approval'; // 승인
|
|
|
|
/** 버튼 스타일 종류 */
|
|
export type ButtonVariant = 'primary' | 'default' | 'destructive' | 'outline' | 'ghost';
|
|
|
|
/** 버튼의 제어 플로우 연결 설정 */
|
|
export interface ButtonFlow {
|
|
/** 연결할 플로우 ID */
|
|
flowId: string;
|
|
/** 플로우 실행 타이밍 (액션 전/후) */
|
|
timing: 'before' | 'after';
|
|
}
|
|
|
|
/**
|
|
* 단일 버튼 컴포넌트 설정.
|
|
*
|
|
* 기본 포트:
|
|
* - outputs: clicked(value)
|
|
* - inputs: 없음
|
|
*/
|
|
export interface ButtonConfig {
|
|
/** 버튼 텍스트 */
|
|
text: string;
|
|
/** 실행할 액션 종류 */
|
|
actionType: ActionType;
|
|
/** 버튼 스타일 */
|
|
variant: ButtonVariant;
|
|
/** 확인 메시지 (있으면 실행 전 확인 팝업 표시) */
|
|
confirm?: string;
|
|
/** 제어 플로우 연결 (선택) */
|
|
flow?: ButtonFlow;
|
|
}
|
|
|
|
/**
|
|
* 버튼 그룹 컴포넌트 설정.
|
|
* 여러 버튼을 한 줄로 묶어 배치한다.
|
|
*/
|
|
export interface ButtonBarConfig {
|
|
/** 포함된 버튼 목록 */
|
|
buttons: ButtonConfig[];
|
|
}
|
|
|
|
// --- 4.5 tabs ---
|
|
|
|
/** 개별 탭 항목 */
|
|
export interface TabItem {
|
|
/** 탭에 표시되는 라벨 */
|
|
label: string;
|
|
/** 탭 고유 ID */
|
|
id: string;
|
|
}
|
|
|
|
/**
|
|
* 탭 컴포넌트 설정.
|
|
*/
|
|
export interface TabsConfig {
|
|
/** 탭 목록 */
|
|
tabs: TabItem[];
|
|
/** 기본 선택 탭 ID */
|
|
defaultTab: string;
|
|
}
|
|
|
|
// --- 4.6 기타 (title, stats, divider, pagination, split-panel) ---
|
|
|
|
/**
|
|
* 제목/텍스트 컴포넌트 설정.
|
|
*/
|
|
export interface TitleConfig {
|
|
/** 표시 텍스트 */
|
|
text: string;
|
|
/** 글꼴 크기 (CSS 값, 예: '1.2rem') */
|
|
fontSize: string;
|
|
/** 글꼴 두께 (CSS 값, 예: '600') */
|
|
fontWeight: string;
|
|
/** 텍스트 정렬 */
|
|
align: 'left' | 'center' | 'right';
|
|
}
|
|
|
|
/** 통계 항목 하나 */
|
|
export interface StatsItem {
|
|
/** 항목 라벨 */
|
|
label: string;
|
|
/** 집계 대상 컬럼명 */
|
|
column: string;
|
|
/** 집계 방식 */
|
|
aggregation: 'count' | 'sum' | 'avg';
|
|
}
|
|
|
|
/**
|
|
* 통계 카드 컴포넌트 설정.
|
|
*
|
|
* 기본 포트:
|
|
* - outputs: 없음
|
|
* - inputs: data(rows)
|
|
*/
|
|
export interface StatsConfig {
|
|
/** 통계 항목 목록 */
|
|
items: StatsItem[];
|
|
}
|
|
|
|
/**
|
|
* 구분선 컴포넌트 설정.
|
|
*/
|
|
export interface DividerConfig {
|
|
/** 선 스타일 */
|
|
style: 'solid' | 'dashed' | 'dotted';
|
|
}
|
|
|
|
/**
|
|
* 페이지네이션 컴포넌트 설정.
|
|
*
|
|
* 기본 포트:
|
|
* - outputs: pageChange(params)
|
|
* - inputs: totalCount(value)
|
|
*/
|
|
export interface PaginationConfig {
|
|
/** 페이지당 행 수 */
|
|
pageSize: number;
|
|
/** 페이지 크기 선택기 표시 여부 */
|
|
showSizeSelector: boolean;
|
|
/** 선택 가능한 페이지 크기 목록 */
|
|
sizeOptions: number[];
|
|
}
|
|
|
|
/**
|
|
* 분할 패널 컴포넌트 설정.
|
|
* 추후 확장 예정 — 현재는 빈 구조로 예약.
|
|
*/
|
|
export interface SplitPanelConfig {
|
|
/** 분할 방향 */
|
|
direction?: 'horizontal' | 'vertical';
|
|
/** 초기 분할 비율 (0~1, 왼쪽/위쪽 비율) */
|
|
ratio?: number;
|
|
}
|
|
|
|
// --- ComponentTypeConfig 유니온 ---
|
|
|
|
/**
|
|
* 모든 컴포넌트 타입별 설정의 유니온 타입.
|
|
*
|
|
* Component.config 필드에 사용되며,
|
|
* Component.type에 따라 실제로 들어가는 타입이 결정된다.
|
|
*/
|
|
export type ComponentTypeConfig =
|
|
| TableConfig
|
|
| FormConfig
|
|
| SearchConfig
|
|
| ButtonConfig
|
|
| ButtonBarConfig
|
|
| TabsConfig
|
|
| TitleConfig
|
|
| StatsConfig
|
|
| DividerConfig
|
|
| PaginationConfig
|
|
| SplitPanelConfig;
|
|
|
|
/**
|
|
* ComponentType → ComponentTypeConfig 매핑.
|
|
*
|
|
* 타입 안전한 config 접근이 필요할 때 사용한다.
|
|
* 예: ComponentTypeConfigMap['table'] → TableConfig
|
|
*/
|
|
export interface ComponentTypeConfigMap {
|
|
table: TableConfig;
|
|
form: FormConfig;
|
|
search: SearchConfig;
|
|
button: ButtonConfig;
|
|
'button-bar': ButtonBarConfig;
|
|
tabs: TabsConfig;
|
|
'split-panel': SplitPanelConfig;
|
|
title: TitleConfig;
|
|
stats: StatsConfig;
|
|
divider: DividerConfig;
|
|
pagination: PaginationConfig;
|
|
}
|
|
|
|
/**
|
|
* 타입 안전한 컴포넌트.
|
|
*
|
|
* Component.type과 Component.config의 타입을 연동시킨다.
|
|
* 예: TypedComponent<'table'>이면 config는 반드시 TableConfig.
|
|
*/
|
|
export interface TypedComponent<T extends ComponentType> extends Omit<Component, 'type' | 'config'> {
|
|
type: T;
|
|
config: ComponentTypeConfigMap[T];
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 5. Template — 화면 단위
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* 뷰 하나의 설정 (목록, 등록 팝업, 수정 팝업).
|
|
*
|
|
* edit 뷰가 create를 상속하면 extends: 'create'로 지정하고,
|
|
* 다른 부분만 오버라이드한다.
|
|
*/
|
|
export interface ViewConfig {
|
|
/** 이 뷰에 배치된 컴포넌트 목록 */
|
|
components: Component[];
|
|
/** 상속 대상 뷰 (edit가 create를 상속할 때) */
|
|
extends?: 'create';
|
|
/** 팝업 크기 (create/edit 뷰용) */
|
|
size?: {
|
|
/** 팝업 너비 (px) */
|
|
w: number;
|
|
/** 팝업 높이 (px) */
|
|
h: number;
|
|
};
|
|
}
|
|
|
|
/** 템플릿 상태 */
|
|
export type TemplateStatus = 'draft' | 'published';
|
|
|
|
/**
|
|
* 한 화면 = 한 템플릿.
|
|
*
|
|
* 목록 + 등록 팝업 + 수정 팝업이 하나의 Template에 포함된다.
|
|
* Template.fields가 유일한 진실의 원천이며, 모든 뷰가 이를 공유한다.
|
|
*/
|
|
export interface Template {
|
|
// ─── 식별 ───
|
|
|
|
/** 템플릿 고유 ID */
|
|
templateId: string;
|
|
/** 화면 이름 */
|
|
name: string;
|
|
/**
|
|
* @deprecated Phase 2 에서 제거. 단일 자유배치 모델로 kind 분기 없음.
|
|
* 기존 데이터 호환용으로만 optional 유지. 새 Template 은 이 필드를 쓰지 않는다.
|
|
*/
|
|
kind?: TemplateKind;
|
|
/** 분류 (예: sales, production, purchase) */
|
|
category: string;
|
|
/** 화면 설명 */
|
|
description?: string;
|
|
|
|
// ─── 데이터 ───
|
|
|
|
/** 메인 바인딩 테이블 */
|
|
primaryTable: string;
|
|
/** 필드 정의 목록 — 모든 뷰가 이 하나를 공유한다 */
|
|
fields: FieldConfig[];
|
|
|
|
// ─── 3뷰 ───
|
|
|
|
/** 목록 / 등록 / 수정 세 가지 뷰 */
|
|
views: {
|
|
/** 목록 화면 */
|
|
list: ViewConfig;
|
|
/** 등록 팝업 */
|
|
create: ViewConfig;
|
|
/** 수정 팝업 (create 상속 가능) */
|
|
edit: ViewConfig;
|
|
};
|
|
|
|
// ─── 연결 ───
|
|
|
|
/** 컴포넌트 간 DataPort 연결 목록 */
|
|
connections: Connection[];
|
|
|
|
// ─── 메타 ───
|
|
|
|
/** 회사 코드 */
|
|
companyCode: string;
|
|
/** 템플릿 버전 */
|
|
version: number;
|
|
/** 상태 (초안 / 게시됨) */
|
|
status: TemplateStatus;
|
|
/** 생성 일시 (ISO 8601) */
|
|
createdAt: string;
|
|
/** 수정 일시 (ISO 8601) */
|
|
updatedAt: string;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 6. 컴포넌트 기본 grid 배치 (섹션 10)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* @deprecated Phase 2 DashboardCard 재작성 시 제거 예정.
|
|
* 12-grid 기본 배치는 폐기됐다. 팔레트 드롭 시 기본 크기는
|
|
* ComponentRegistry 의 default_size 를 사용하고, 위치는 FreePosition 으로
|
|
* 마우스 좌표에서 계산한다(components/template-builder/TemplateBuilder.tsx).
|
|
*/
|
|
export const DEFAULT_COMPONENT_LAYOUTS: Record<ComponentType, GridPosition> = {
|
|
search: { col: 1, colSpan: 12 },
|
|
table: {
|
|
col: 1,
|
|
colSpan: 8,
|
|
responsive: {
|
|
narrow: { colSpan: 12 },
|
|
normal: { colSpan: 12 },
|
|
wide: { colSpan: 8 },
|
|
},
|
|
},
|
|
form: {
|
|
col: 1,
|
|
colSpan: 4,
|
|
responsive: {
|
|
narrow: { col: 1, colSpan: 12 },
|
|
normal: { col: 1, colSpan: 12 },
|
|
wide: { col: 9, colSpan: 4 },
|
|
},
|
|
},
|
|
'button-bar': { col: 1, colSpan: 12 },
|
|
button: {
|
|
col: 1,
|
|
colSpan: 2,
|
|
responsive: {
|
|
narrow: { colSpan: 12 },
|
|
normal: { colSpan: 3 },
|
|
wide: { colSpan: 2 },
|
|
},
|
|
},
|
|
stats: {
|
|
col: 1,
|
|
colSpan: 4,
|
|
responsive: {
|
|
narrow: { colSpan: 12 },
|
|
normal: { colSpan: 6 },
|
|
wide: { colSpan: 4 },
|
|
},
|
|
},
|
|
title: { col: 1, colSpan: 12 },
|
|
divider: { col: 1, colSpan: 12 },
|
|
pagination: { col: 1, colSpan: 12 },
|
|
tabs: { col: 1, colSpan: 12 },
|
|
'split-panel': { col: 1, colSpan: 12 },
|
|
};
|
|
|
|
/**
|
|
* @deprecated Phase 2 DashboardCard 재작성 시 제거 예정.
|
|
* 모든 Template 이 단일 자유배치 모델이므로 kind 휴리스틱이 필요 없다.
|
|
* 현재 DashboardCard.tsx 만 이 상수를 사용 중.
|
|
*/
|
|
export const CANVAS_KEYWORDS = [
|
|
'control',
|
|
'flow',
|
|
'workflow',
|
|
'bpm',
|
|
'canvas',
|
|
'node',
|
|
'diagram',
|
|
'graph',
|
|
] as const;
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 7. 카드 엔진 v2 — 자유배치 단일 모델 (2026-04-10 확정)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
//
|
|
// 진실의 원천: notes/gbpark/2026-04-10-card-engine-final-spec.md
|
|
//
|
|
// 이 섹션의 타입들이 Phase 1 이후 INVYONE 의 유일한 템플릿/대시보드 모델이다.
|
|
// 위쪽 §6 까지의 Component/GridPosition/TemplateKind/DEFAULT_COMPONENT_LAYOUTS
|
|
// 등은 Phase 1 Step 5 에서 제거 예정(현재는 폐기 예정 코드와 호환용으로만 남음).
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* 자유배치 위치 — 단일 위치 모델.
|
|
*
|
|
* 빌더 캔버스(Template 안의 컴포넌트) 와 대시보드 캔버스(Card) 가
|
|
* 모두 이 하나의 타입으로 배치된다. 12-grid / 48-col 분기 없음.
|
|
*/
|
|
export interface FreePosition {
|
|
/** 좌상단 X (px) */
|
|
left: number;
|
|
/** 좌상단 Y (px) */
|
|
top: number;
|
|
/** 너비 (px) */
|
|
width: number;
|
|
/** 높이 (px) */
|
|
height: number;
|
|
}
|
|
|
|
/**
|
|
* 뷰가 다른 뷰를 여는 방식.
|
|
*
|
|
* 빌더가 "등록" 버튼을 감지하면 viewTrigger 를 자동 세팅하고
|
|
* 대응되는 create 뷰 placeholder 를 만든다.
|
|
*/
|
|
export interface ViewTrigger {
|
|
/** 어느 뷰를 여는가 */
|
|
targetView: 'create' | 'edit' | 'detail';
|
|
/** 뷰 표시 방식 */
|
|
action: 'open-modal' | 'navigate';
|
|
}
|
|
|
|
/**
|
|
* Template 안에 자유배치된 컴포넌트 하나.
|
|
*
|
|
* 컴포넌트 종류(componentId) 는 ComponentRegistry 의 v2-* ID 를 참조한다.
|
|
* 위치는 FreePosition 단일 모델. 타입별 옵션은 config 에 자유 형태로 들어간다.
|
|
*/
|
|
export interface TemplateComponent {
|
|
/** 인스턴스 ID */
|
|
id: string;
|
|
|
|
/**
|
|
* 컴포넌트 종류 — ComponentRegistry 의 ID 참조.
|
|
* 예: 'v2-table-list', 'v2-button-primary', 'v2-bom-tree'
|
|
*/
|
|
componentId: string;
|
|
|
|
/** 빌더에서 표시되는 라벨 (선택) */
|
|
label?: string;
|
|
|
|
/** 캔버스 안에서의 위치 (px 자유배치) */
|
|
position: FreePosition;
|
|
|
|
/**
|
|
* 컴포넌트별 설정 — 모드/옵션/컬럼 정의 등.
|
|
* ComponentRegistry 의 default_config 를 인스턴스마다 오버라이드한다.
|
|
* 로우코드 플랫폼 원칙상 느슨한 Record 형태.
|
|
*/
|
|
config: Record<string, any>;
|
|
|
|
/** 컨테이너 컴포넌트의 자식 (탭/아코디언 등) */
|
|
children?: TemplateComponent[];
|
|
|
|
/** 받는 데이터 포트 */
|
|
inputs?: DataPort[];
|
|
/** 내보내는 데이터 포트 */
|
|
outputs?: DataPort[];
|
|
|
|
/** 이 컴포넌트가 다른 뷰를 여는지 여부 (예: 등록 버튼 → create 뷰) */
|
|
viewTrigger?: ViewTrigger;
|
|
|
|
/** 그룹핑용 — 여러 컴포넌트를 하나의 group 으로 묶을 때 부모 id */
|
|
parentId?: string;
|
|
|
|
/**
|
|
* group 전용 — 이 컴포넌트가 type='group' 일 때 자식 id 목록.
|
|
* 빌더의 그룹 박스 UI 용이며, 런타임 렌더러는 parentId 로 부모를 찾는다.
|
|
*/
|
|
groupChildren?: string[];
|
|
}
|
|
|
|
/**
|
|
* 한 뷰의 자유배치 캔버스 설정.
|
|
*
|
|
* Template 의 views.list / create / edit 각각이 이 타입이다.
|
|
* 기존 §5 의 ViewConfig 와 이름이 겹치므로 Phase 1 Step 5 에서
|
|
* 기존 ViewConfig 를 이 타입으로 교체한다(그 사이에는 별칭으로 사용).
|
|
*/
|
|
export interface TemplateViewConfig {
|
|
/** 이 뷰에 배치된 컴포넌트들 (자유배치) */
|
|
components: TemplateComponent[];
|
|
|
|
/** 뷰 표시 방식 (list 는 카드 본체, create/edit 는 모달) */
|
|
layout?: 'card' | 'modal';
|
|
|
|
/** 모달 사이즈 (layout === 'modal' 일 때) */
|
|
modalSize?: { w: number; h: number };
|
|
|
|
/** 뷰 캔버스의 기본 사이즈 (디자인 시의 작업 영역 크기) */
|
|
designSize?: { w: number; h: number };
|
|
}
|
|
|
|
/** Template 의 3뷰 묶음. list 는 필수, create/edit 는 선택(자동 생성 가능). */
|
|
export interface TemplateViews {
|
|
/** 목록 뷰 — 카드 본체 (필수) */
|
|
list: TemplateViewConfig;
|
|
/** 등록 뷰 — 모달 (자동 생성 가능) */
|
|
create?: TemplateViewConfig;
|
|
/** 수정 뷰 — 모달 (자동 생성 가능) */
|
|
edit?: TemplateViewConfig;
|
|
}
|
|
|
|
/**
|
|
* Template 단위 데이터 포트 정의 (카드 간 통신용).
|
|
*
|
|
* 개별 TemplateComponent 의 DataPort 와 달리, 이건 Template 전체가
|
|
* 대시보드에서 어떤 입출력을 외부에 노출하는지를 기술한다.
|
|
*/
|
|
export interface DataPortDef {
|
|
/** 포트 이름 */
|
|
name: string;
|
|
/** 포트 데이터 형태 */
|
|
type: DataPortType;
|
|
/** 설명 (대시보드 연결 UI 에서 표시) */
|
|
description?: string;
|
|
}
|
|
|
|
/**
|
|
* 대시보드에 배치된 카드 한 개 — Template 의 인스턴스.
|
|
*
|
|
* 1 Template : N Card 관계. 한 Template 을 여러 대시보드에 배치할 수 있고,
|
|
* 각 인스턴스마다 위치/크기/설정 오버라이드를 가진다.
|
|
*/
|
|
export interface Card {
|
|
/** 인스턴스 ID */
|
|
id: string;
|
|
|
|
/** 참조하는 Template ID */
|
|
templateId: string;
|
|
|
|
/** 대시보드 안에서의 위치 (px 자유배치) */
|
|
position: FreePosition;
|
|
|
|
/** 접힘 상태 (mini 모드) */
|
|
collapsed: boolean;
|
|
|
|
/** Template 의 config 를 이 인스턴스에서만 덮어쓸 값 */
|
|
configOverride?: Record<string, any>;
|
|
|
|
/** 인스턴스별 DataPort 연결 상태 */
|
|
inputs?: DataPort[];
|
|
outputs?: DataPort[];
|
|
}
|
|
|
|
/**
|
|
* 대시보드 레벨 카드 간 연결.
|
|
*
|
|
* 예: 수주 카드의 selectedRow 를 BOM 카드의 masterRow 로 연결.
|
|
*/
|
|
export interface CardConnection {
|
|
/** 연결 ID */
|
|
id: string;
|
|
/** 출발 카드 + 포트 */
|
|
from: { cardId: string; port: string };
|
|
/** 도착 카드 + 포트 */
|
|
to: { cardId: string; port: string };
|
|
}
|
|
|
|
/**
|
|
* 사이드바 메뉴 항목 = 카드 컬렉션.
|
|
*
|
|
* INVYONE 의 "메뉴 = 대시보드" 원칙에 따라, 사이드바 엔트리와 대시보드가
|
|
* 1:1 대응한다. 별도 대시보드 UI 없음.
|
|
*/
|
|
export interface Dashboard {
|
|
/** 대시보드 ID */
|
|
id: string;
|
|
/** 메뉴 표시 이름 */
|
|
name: string;
|
|
/** 사이드바 아이콘 */
|
|
icon: string;
|
|
|
|
/** 카드 자유배치 목록 */
|
|
cards: Card[];
|
|
|
|
/** 카드 간 데이터 연결 */
|
|
connections: CardConnection[];
|
|
|
|
// ─── 메타 ───
|
|
companyCode: string;
|
|
/** 사용자별 대시보드일 때 소유자 ID (공유 대시보드는 비움) */
|
|
ownerId?: string;
|
|
/** 공유 대시보드 여부 */
|
|
isShared: boolean;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|