# INVYONE 컴포넌트 규격 (v1.0)

> **상태**: DRAFT
> **작성일**: 2026-04-08
> **원칙**: 범용성, 호환성, 확장성
> **핵심 문제**: vex에서 ColumnConfig(354줄), FilterConfig, FormField가 전부 다른 규격 → 컴포넌트 간 데이터 전달 시 변환 필요 → 에러 폭발

---

## 1. 설계 원칙

### 1.1 하나의 FieldConfig가 모든 곳에서 쓰인다

vex의 문제:
```
table-list  →  ColumnConfig      (354줄 types.ts)
form        →  자체 필드 구조
search      →  FilterConfig
button      →  ButtonActionConfig

→ 같은 "수주일" 필드를 3군데서 3가지 다른 규격으로 정의
→ 테이블에서 폼으로 데이터 넘길 때 변환 필요
→ 변환 코드에서 에러 발생
```

invyone의 해법:
```
table  →  FieldConfig[]  (컬럼으로 렌더)
form   →  FieldConfig[]  (입력 필드로 렌더)
search →  FieldConfig[]  (검색 조건으로 렌더)

→ 같은 FieldConfig를 공유
→ 테이블 행 클릭 → 그대로 폼에 전달 → 변환 없음
```

### 1.2 컴포넌트는 DataPort로 통신한다

컴포넌트끼리 직접 참조하지 않는다. 대신 표준화된 DataPort로 데이터를 주고받는다.

```
[검색] --searchParams-→ [테이블] --selectedRow-→ [폼]
                                  --selectedRows-→ [버튼(삭제)]
```

모든 컴포넌트가 같은 형식(`Record<string, any>`)의 데이터를 주고받으므로 변환 불필요.

### 1.3 설정은 3단계로 나뉜다

```
1. BaseConfig      — 모든 컴포넌트 공통 (id, type, position, label)
2. TypeConfig      — 타입별 고유 설정 (table의 pageSize, form의 columns)
3. InstanceConfig  — 인스턴스별 오버라이드 (이 화면에서만 다르게)
```

---

## 2. FieldConfig — 유일한 필드 규격

```typescript
/**
 * 모든 컴포넌트가 공유하는 필드 정의.
 * 테이블 컬럼, 폼 입력, 검색 조건, 전부 이 하나로.
 */
interface FieldConfig {
  // ─── 식별 ───
  column: string;          // DB 컬럼명 (유일 키)
  label: string;           // 표시 라벨
  
  // ─── 타입 ───
  type: FieldType;         // 렌더링 방식 결정
  
  // ─── 표시 ───
  visible: boolean;        // 보이는지
  order: number;           // 표시 순서
  width?: number;          // 컬럼 너비 (px, 테이블용)
  align?: 'left' | 'center' | 'right';
  
  // ─── 입력 ───
  required: boolean;       // 필수 여부
  editable: boolean;       // 편집 가능 여부
  defaultValue?: any;      // 기본값
  placeholder?: string;    // 플레이스홀더
  
  // ─── 타입별 확장 (★ 이것만으로 모든 특수 케이스 커버) ───
  options?: string[];      // select/category: 선택지
  ref?: {                  // entity: FK 참조
    table: string;
    valueColumn: string;
    displayColumn: string;
    searchColumns?: string[];
  };
  format?: string;         // number: '#,##0', date: 'YYYY-MM-DD'
  computed?: string;       // 자동 계산 수식: 'quantity * unit_price'
  
  // ─── 메타 ───
  pk?: boolean;            // PK 여부
  system?: boolean;        // 시스템 필드 (company_code 등, 폼에서 숨김)
  searchable?: boolean;    // 검색 대상 여부
  sortable?: boolean;      // 정렬 가능 여부
}

type FieldType = 
  | 'text'       // 일반 문자열
  | 'number'     // 숫자 (포맷팅)
  | 'date'       // 날짜
  | 'datetime'   // 날짜+시간
  | 'select'     // 드롭다운 (options 배열)
  | 'entity'     // FK 참조 (팝업 검색)
  | 'checkbox'   // 체크박스
  | 'textarea'   // 장문
  | 'file'       // 파일 첨부
  | 'code';      // 자동채번 (readonly)
```

**이게 전부다.** vex의 ColumnConfig 354줄 → invyone의 FieldConfig ~30줄.

### 2.1 같은 FieldConfig가 다르게 렌더되는 방식

```
FieldConfig { column: 'order_date', label: '수주일', type: 'date' }

→ 테이블에서: <td>2026-04-08</td>  (format 적용)
→ 폼에서:     <DatePicker label="수주일" required />
→ 검색에서:   <DateRangePicker label="수주일" />
→ 속성에서:   [수주일] date  ☑visible  ☑required
```

렌더러가 FieldConfig의 `type`을 보고 알아서 적절한 UI를 그린다. 필드 규격은 하나.

### 2.2 렌더링 계약 (★ 스키마-런타임 바인딩 규칙)

**렌더러마다 FieldConfig를 해석하는 규칙이 다르다. 이 차이는 암묵적이면 안 되고 명시적이어야 한다.**

| FieldType | 테이블 렌더 | 폼 렌더 | 검색 렌더 |
|---|---|---|---|
| `text` | 텍스트 그대로 | `<input type="text">` | `<input type="text">` (부분 일치) |
| `number` | format 적용 (#,##0) | `<input type="number">` | `<input type="number">` (범위: min~max) |
| `date` | format 적용 (YYYY-MM-DD) | DatePicker (단일) | **DateRangePicker (범위)** |
| `datetime` | format 적용 | DateTimePicker | DateTimeRangePicker |
| `select` | 텍스트 그대로 | `<select>` (단일 선택) | **MultiSelect (다중 선택)** |
| `entity` | ref.displayColumn 표시 | 검색 팝업 | 검색 팝업 (단일) |
| `checkbox` | ✓/✗ | `<checkbox>` | `<select>` (전체/✓/✗) |
| `textarea` | 말줄임 (…) | `<textarea>` | `<input type="text">` (부분 일치) |
| `file` | 파일명 링크 | 파일 업로드 | — (검색 불가) |
| `code` | 텍스트 그대로 | readonly | `<input type="text">` (완전 일치) |

**핵심:** 검색에서 date→범위, select→다중 선택으로 바뀌는 것은 **FieldType의 암묵적 속성이 아니라 SearchConfig의 렌더링 규칙**이다. 이 매핑 테이블이 spec의 일부이며, 렌더러 구현체는 이 테이블을 정확히 따라야 한다.

향후 이 매핑을 오버라이드하고 싶으면(예: 날짜를 범위 대신 단일로 검색), SearchConfig에 필드별 오버라이드를 추가한다:

```typescript
interface SearchConfig {
  // ... 기존 속성
  fieldOverrides?: {
    [column: string]: {
      searchMode?: 'exact' | 'partial' | 'range' | 'multi';  // 검색 모드 강제
      searchWidget?: 'input' | 'datepicker' | 'daterange' | 'select' | 'multiselect';  // 위젯 강제
    };
  };
}
```

### 2.3 computed 실행 모델 (★ 보안 필수)

**`computed` 필드는 절대로 `eval()` 이나 `new Function()`으로 실행하지 않는다.**

```
❌ 금지: new Function(...keys, `return ${computed}`)
❌ 금지: eval(computed)
```

computed는 **선언적 수식 문자열**이다. 실행은 **안전한 수식 파서**만 허용한다.

**허용되는 computed 문법:**

```
// 사칙연산 + 필드 참조
'quantity * unit_price'
'order_qty - received_qty'
'(unit_price * quantity) * (1 - discount_rate)'

// 집계 함수 (detail 테이블 참조)
'SUM(order_items.amount)'
'COUNT(order_items)'
```

**허용 안 되는 것:**

```
// 함수 호출
'fetch("/api/something")'
'console.log("hack")'
'document.cookie'

// 할당/부수효과
'quantity = 0'
'window.location = "..."'
```

**구현 방식:** AST 기반 수식 파서 또는 화이트리스트 토큰 파서.
- 허용 토큰: 숫자, 필드명, +, -, *, /, (, ), SUM, COUNT, AVG, MIN, MAX
- 그 외 전부 거부
- 파서는 별도 유틸(`lib/formula-parser.ts`)로 분리

### 2.4 검증 규칙 (★ required의 정확한 의미)

**`required: true`는 "이 필드에 유효한 값이 있어야 한다"는 뜻이다.**

| 값 | required 통과? | 이유 |
|---|---|---|
| `'abc'` | ✅ | 유효한 문자열 |
| `0` | **✅** | 유효한 숫자 (수량 0은 의미 있음) |
| `false` | **✅** | 유효한 boolean |
| `''` (빈 문자열) | ❌ | 값 없음 |
| `null` | ❌ | 값 없음 |
| `undefined` | ❌ | 값 없음 |

**검증 로직 (정확한 구현):**

```typescript
function isFieldEmpty(value: any): boolean {
  return value === null || value === undefined || value === '';
}

// required 검증
if (field.required && isFieldEmpty(row[field.column])) {
  errors.push({ column: field.column, message: `${field.label}은(는) 필수입니다` });
}
```

**`0`, `false`, `[]`(빈 배열)은 empty가 아니다.** 이건 JavaScript의 falsy 함정과 다른 비즈니스 규칙이다.

향후 추가될 검증 규칙 (Phase 2):

```typescript
interface ValidationRule {
  type: 'required' | 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern' | 'custom';
  value?: any;           // min: 0, max: 999999, pattern: '^[A-Z]'
  message?: string;      // 커스텀 에러 메시지
}

// FieldConfig 확장 (Phase 2)
interface FieldConfig {
  // ... 기존 속성
  validations?: ValidationRule[];  // Phase 2에서 추가
}
```

---

## 3. Component — 유일한 컴포넌트 규격

```typescript
/**
 * 모든 컴포넌트의 기본 구조.
 * 타입이 뭐든 이 구조를 따른다.
 */
interface Component {
  // ─── 식별 ───
  id: string;              // 고유 ID
  type: ComponentType;     // 컴포넌트 종류
  label: string;           // 빌더에서 보이는 이름
  
  // ─── 위치 (빌더가 관리) ───
  position: {
    x: number;
    y: number;
    w: number;
    h: number;
  };
  
  // ─── 반응형 (lg/md/sm 브레이크포인트) ───
  responsive?: {
    lg?: { x?: number; y?: number; w?: number; h?: number; };  // ≥1200px
    md?: { x?: number; y?: number; w?: number; h?: number; };  // ≥768px
    sm?: { x?: number; y?: number; w?: number; h?: number; };  // <768px
  };
  
  // ─── 데이터 바인딩 ───
  dataSource?: {
    table: string;             // 바인딩 테이블
    mode: 'read' | 'write' | 'readwrite';
  };
  fields?: FieldConfig[];      // ★ 유일한 필드 규격
  
  // ─── 데이터 포트 (컴포넌트 간 통신) ───
  inputs?: DataPort[];         // 받는 데이터
  outputs?: DataPort[];        // 내보내는 데이터
  
  // ─── 타입별 설정 (간결하게) ───
  config: ComponentTypeConfig;
}

type ComponentType =
  | 'table'        // 데이터 테이블 (목록)
  | 'form'         // 입력 폼
  | 'search'       // 검색 필터
  | 'button'       // 버튼 (단일)
  | 'button-bar'   // 버튼 그룹
  | 'tabs'         // 탭
  | 'split-panel'  // 분할 패널
  | 'title'        // 텍스트/제목
  | 'stats'        // 통계 카드
  | 'divider'      // 구분선
  | 'pagination';  // 페이지네이션
```

---

## 4. DataPort — 컴포넌트 간 통신 규격

```typescript
/**
 * 컴포넌트가 데이터를 주고받는 포트.
 * 연결은 output.name → input.name 매칭.
 */
interface DataPort {
  name: string;            // 포트 이름
  type: DataPortType;      // 데이터 형태
  connectedTo?: string;    // 연결된 컴포넌트 ID (빌더에서 설정)
}

type DataPortType =
  | 'row'       // 단일 행: Record<string, any>
  | 'rows'      // 복수 행: Record<string, any>[]
  | 'value'     // 단일 값: any
  | 'params';   // 검색 파라미터: Record<string, any>
```

### 4.1 타입별 기본 포트

| 컴포넌트 | 기본 outputs | 기본 inputs |
|---|---|---|
| `table` | `selectedRow(row)`, `selectedRows(rows)` | `searchParams(params)`, `refreshTrigger(value)` |
| `form` | `formData(row)`, `savedRow(row)` | `loadRow(row)` |
| `search` | `searchParams(params)` | — |
| `button` | `clicked(value)` | — |
| `pagination` | `pageChange(params)` | `totalCount(value)` |
| `stats` | — | `data(rows)` |

### 4.2 연결 예시

```
[검색 필터]                    [데이터 테이블]              [입력 폼]
 outputs:                      inputs:                      inputs:
   searchParams ──────────→     searchParams                  loadRow
                               outputs:                     outputs:
                                 selectedRow ──────────→      loadRow
                                                              formData
```

빌더에서 연결을 시각적으로 설정. 실행 시 이벤트 버스로 자동 전달.

---

## 5. ComponentTypeConfig — 타입별 설정 (간결하게)

### 5.1 table

```typescript
interface TableConfig {
  pageSize: number;            // 페이지 크기 (기본: 20)
  selectionMode: 'none' | 'single' | 'multiple';
  showCheckbox: boolean;
  inlineEdit: boolean;         // 인라인 편집
  autoLoad: boolean;           // 자동 로드
  defaultSort?: { column: string; direction: 'asc' | 'desc'; };
  toolbar: {
    showExcel: boolean;
    showRefresh: boolean;
    showFilter: boolean;
  };
  style: 'default' | 'striped' | 'bordered' | 'compact';
}
```

### 5.2 form

```typescript
interface FormConfig {
  columns: 1 | 2 | 3;         // 폼 컬럼 수
  sections?: {                 // 섹션 구분 (선택)
    label: string;
    fields: string[];          // FieldConfig.column 참조
  }[];
  saveAction: {
    method: 'INSERT' | 'UPDATE' | 'UPSERT';
    successMessage?: string;
    refreshAfterSave: boolean;
  };
}
```

### 5.3 search

```typescript
interface SearchConfig {
  dateRangeEnabled: boolean;   // 날짜 범위 검색
  showResetButton: boolean;    // 초기화 버튼
  autoSearch: boolean;         // 입력 시 자동 검색
  layout: 'inline' | 'stacked';  // 검색 필드 배치
}
```

### 5.4 button / button-bar

```typescript
interface ButtonConfig {
  text: string;
  actionType: ActionType;      // 12종
  variant: 'primary' | 'default' | 'destructive' | 'outline' | 'ghost';
  confirm?: string;            // 확인 메시지 (있으면 확인 팝업)
  flow?: {                     // 제어 플로우 연결
    flowId: string;
    timing: 'before' | 'after';
  };
}

type ActionType = 
  'save' | 'edit' | 'delete' | 'add' | 'cancel' | 'close' |
  'navigate' | 'popup' | 'search' | 'reset' | 'submit' | 'approval';

interface ButtonBarConfig {
  buttons: ButtonConfig[];
}
```

### 5.5 tabs

```typescript
interface TabsConfig {
  tabs: { label: string; id: string; }[];
  defaultTab: string;          // 기본 선택 탭 ID
}
```

### 5.6 기타 (title, stats, divider, pagination)

```typescript
interface TitleConfig {
  text: string;
  fontSize: string;
  fontWeight: string;
  align: 'left' | 'center' | 'right';
}

interface StatsConfig {
  items: { label: string; column: string; aggregation: 'count' | 'sum' | 'avg'; }[];
}

interface DividerConfig {
  style: 'solid' | 'dashed' | 'dotted';
}

interface PaginationConfig {
  pageSize: number;
  showSizeSelector: boolean;
  sizeOptions: number[];
}
```

---

## 6. Template — 화면 단위

```typescript
/**
 * 한 화면 = 한 템플릿.
 * 목록 + 등록팝업 + 수정팝업이 한 덩어리.
 */
interface Template {
  // ─── 식별 ───
  templateId: string;
  name: string;
  category: string;          // sales, production, purchase, ...
  description?: string;
  
  // ─── 데이터 ───
  primaryTable: string;      // 메인 테이블
  fields: FieldConfig[];     // ★ 이 필드 목록을 모든 뷰가 공유
  
  // ─── 3뷰 ───
  views: {
    list: ViewConfig;        // 목록 화면
    create: ViewConfig;      // 등록 팝업
    edit: ViewConfig;        // 수정 팝업 (create 상속 가능)
  };
  
  // ─── 연결 ───
  connections: Connection[]; // 컴포넌트 간 DataPort 연결
  
  // ─── 메타 ───
  companyCode: string;
  version: number;
  status: 'draft' | 'published';
  createdAt: string;
  updatedAt: string;
}

interface ViewConfig {
  components: Component[];   // 이 뷰에 배치된 컴포넌트들
  extends?: 'create';       // edit가 create를 상속할 때
  size?: { w: number; h: number; };  // 팝업 크기 (create/edit용)
}

interface Connection {
  id: string;
  from: { componentId: string; port: string; };
  to: { componentId: string; port: string; };
}
```

---

## 7. vex 대비 비교

| | vex | invyone |
|---|---|---|
| 필드 규격 | ColumnConfig(354줄) + FilterConfig + FormField = 전부 다름 | **FieldConfig 하나 (~30줄)** |
| 컴포넌트 간 통신 | 자체 구현, 컴포넌트마다 다름 | **DataPort 표준 프로토콜** |
| 컴포넌트 타입 | 33종 (v2-*), 각각 다른 규격 | **11종, 공통 base + 간결한 TypeConfig** |
| 테이블 설정 | TableListConfig 354줄 | **TableConfig ~15줄** |
| 화면 구조 | 목록/등록/수정 = 3개 화면 따로 | **Template 1개에 3뷰 내장** |
| 필드 공유 | 화면마다 따로 정의 | **Template.fields를 모든 뷰가 공유** |
| 데이터 전달 | 컴포넌트별 커스텀 이벤트 | **output → input 자동 매칭** |

---

## 8. 확장 방법

새 필드 타입 추가:
```
1. FieldType에 새 타입 추가 (예: 'color')
2. 각 렌더러(table/form/search)에 해당 타입 렌더링 추가
→ 끝. FieldConfig 구조는 변경 없음.
```

새 컴포넌트 타입 추가:
```
1. ComponentType에 새 타입 추가 (예: 'chart')
2. 해당 TypeConfig 정의 (예: ChartConfig)
3. DataPort 기본 포트 정의
4. 렌더러 구현
→ 끝. 기존 컴포넌트/필드 규격에 영향 없음.
```

새 액션 타입 추가:
```
1. ActionType에 새 타입 추가 (예: 'email')
2. 액션 핸들러 구현
→ 끝.
```

---

## 9. 데이터 흐름 (전체 그림)

```
[table_type_columns DB]
        ↓ (자동 로드)
[Template.fields: FieldConfig[]]  ← 유일한 진실의 원천
        ↓ (공유)
   ┌────┼────┐
   ↓    ↓    ↓
 목록  등록  수정     ← 3뷰가 같은 fields 공유
   │    │    │
   ↓    ↓    ↓
[Component[]]         ← 각 뷰의 컴포넌트 배열
   │
   ↓
[DataPort 연결]       ← 검색→테이블→폼 자동 흐름
   │
   ↓
[렌더러]              ← FieldConfig.type 보고 적절한 UI
```

---

## 10. 다음 단계

1. FieldConfig 타입스크립트 정의 확정
2. 빌더 mockup에 이 규격 적용
3. 렌더러 프로토타입 (FieldConfig → React 컴포넌트)
4. DataPort 이벤트 버스 구현
