1256 lines
33 KiB
TypeScript
1256 lines
33 KiB
TypeScript
/**
|
||
* 🖥️ 화면관리 시스템 전용 타입 정의
|
||
*
|
||
* 화면 설계, 컴포넌트 관리, 레이아웃 등 화면관리 시스템에서만 사용하는 타입들
|
||
*/
|
||
|
||
import {
|
||
ComponentType,
|
||
WebType,
|
||
DynamicWebType,
|
||
Position,
|
||
Size,
|
||
CommonStyle,
|
||
ValidationRule,
|
||
TimestampFields,
|
||
CompanyCode,
|
||
ActiveStatus,
|
||
isWebType,
|
||
} from "./v2-core";
|
||
import { ColumnSpanPreset } from "@/lib/constants/columnSpans";
|
||
import type { FieldConfig, Connection } from "./invyone-component";
|
||
import { ResponsiveComponentConfig } from "./responsive";
|
||
|
||
// ===== 기본 컴포넌트 인터페이스 =====
|
||
|
||
/**
|
||
* 모든 컴포넌트의 기본 인터페이스
|
||
*/
|
||
export interface BaseComponent {
|
||
id: string;
|
||
type: ComponentType;
|
||
|
||
// 🔄 레거시 위치/크기 (단계적 제거 예정)
|
||
position: Position; // y 좌표는 유지 (행 정렬용)
|
||
size: Size; // height만 사용
|
||
|
||
// 🆕 그리드 시스템 속성
|
||
grid_column_span?: ColumnSpanPreset; // 컬럼 너비
|
||
grid_column_start?: number; // 시작 컬럼 (1-12)
|
||
grid_row_index?: number; // 행 인덱스
|
||
|
||
// 🆕 레이어 시스템 (DB layer_id: 1=기본, 2+=조건부)
|
||
layer_id?: string | number; // 컴포넌트가 속한 레이어 ID
|
||
|
||
parent_id?: string;
|
||
label?: string;
|
||
required?: boolean;
|
||
readonly?: boolean;
|
||
style?: ComponentStyle;
|
||
className?: string;
|
||
|
||
// 새 컴포넌트 시스템에서 필요한 속성들
|
||
grid_columns?: number; // 🔄 deprecated - grid_column_span 사용
|
||
zone_id?: string; // 레이아웃 존 ID
|
||
component_config?: any; // 컴포넌트별 설정
|
||
component_type?: string; // 새 컴포넌트 시스템의 ID
|
||
web_type_config?: WebTypeConfig; // 웹타입별 설정
|
||
|
||
// 반응형 설정
|
||
responsive_config?: ResponsiveComponentConfig;
|
||
responsive_display?: any; // 런타임에 추가되는 임시 필드
|
||
|
||
// 조건부 표시 설정
|
||
conditional?: {
|
||
enabled: boolean;
|
||
field: string;
|
||
operator: "=" | "!=" | ">" | "<" | "in" | "notIn" | "isEmpty" | "isNotEmpty";
|
||
value: unknown;
|
||
action: "show" | "hide" | "enable" | "disable";
|
||
};
|
||
|
||
// 자동 입력 설정
|
||
auto_fill?: {
|
||
enabled: boolean;
|
||
source_table: string;
|
||
filter_column: string;
|
||
user_field: "companyCode" | "userId" | "deptCode";
|
||
display_column: string;
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 화면관리용 확장 스타일 (CommonStyle 기반)
|
||
*/
|
||
export interface ComponentStyle extends CommonStyle {
|
||
// 화면관리 전용 스타일 확장 가능
|
||
}
|
||
|
||
/**
|
||
* 위젯 컴포넌트 (입력 요소)
|
||
*/
|
||
export interface WidgetComponent extends BaseComponent {
|
||
type: "widget";
|
||
widget_type: DynamicWebType;
|
||
placeholder?: string;
|
||
column_name?: string;
|
||
web_type_config?: WebTypeConfig;
|
||
validation_rules?: ValidationRule[];
|
||
|
||
// 웹타입별 추가 설정
|
||
date_config?: DateTypeConfig;
|
||
number_config?: NumberTypeConfig;
|
||
select_config?: SelectTypeConfig;
|
||
text_config?: TextTypeConfig;
|
||
file_config?: FileTypeConfig;
|
||
entity_config?: EntityTypeConfig;
|
||
button_config?: ButtonTypeConfig;
|
||
array_config?: ArrayTypeConfig;
|
||
|
||
// 🆕 자동 입력 설정 (테이블 조회 기반)
|
||
auto_fill?: {
|
||
enabled: boolean; // 자동 입력 활성화
|
||
source_table: string; // 조회할 테이블 (예: company_mng)
|
||
filter_column: string; // 필터링할 컬럼 (예: company_code)
|
||
user_field: "companyCode" | "userId" | "deptCode"; // 사용자 정보 필드
|
||
display_column: string; // 표시할 컬럼 (예: company_name)
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 컨테이너 컴포넌트 (레이아웃)
|
||
*/
|
||
export interface ContainerComponent extends BaseComponent {
|
||
type: "container" | "row" | "column" | "area";
|
||
children?: string[]; // 자식 컴포넌트 ID 배열
|
||
layout_direction?: "horizontal" | "vertical";
|
||
justifyContent?: "start" | "center" | "end" | "space-between" | "space-around";
|
||
alignItems?: "start" | "center" | "end" | "stretch";
|
||
gap?: number;
|
||
}
|
||
|
||
/**
|
||
* 그룹 컴포넌트 (논리적 그룹핑)
|
||
*/
|
||
export interface GroupComponent extends BaseComponent {
|
||
type: "group";
|
||
group_name: string;
|
||
children: string[]; // 그룹에 속한 컴포넌트 ID 배열
|
||
isCollapsible?: boolean;
|
||
isCollapsed?: boolean;
|
||
}
|
||
|
||
/**
|
||
* 데이터 테이블 컴포넌트
|
||
*/
|
||
export interface DataTableComponent extends BaseComponent {
|
||
type: "datatable";
|
||
table_name?: string;
|
||
columns: DataTableColumn[];
|
||
pagination?: boolean;
|
||
page_size?: number;
|
||
searchable?: boolean;
|
||
sortable?: boolean;
|
||
filters?: DataTableFilter[];
|
||
|
||
// 🆕 현재 사용자 정보로 자동 필터링
|
||
auto_filter?: {
|
||
enabled: boolean; // 자동 필터 활성화 여부
|
||
filter_column: string; // 필터링할 테이블 컬럼 (예: company_code, dept_code)
|
||
user_field: "companyCode" | "userId" | "deptCode"; // 사용자 정보에서 가져올 필드
|
||
};
|
||
|
||
// 🆕 컬럼 값 기반 데이터 필터링
|
||
data_filter?: DataFilterConfig;
|
||
}
|
||
|
||
/**
|
||
* 파일 업로드 컴포넌트
|
||
*/
|
||
export interface FileComponent extends BaseComponent {
|
||
type: "file";
|
||
file_config: FileTypeConfig;
|
||
uploaded_files?: UploadedFile[];
|
||
column_name?: string;
|
||
table_name?: string;
|
||
last_file_update?: number;
|
||
}
|
||
|
||
/**
|
||
* 플로우 스텝별 컬럼 표시 설정
|
||
*/
|
||
export interface FlowStepColumnConfig {
|
||
selected_columns: string[]; // 표시할 컬럼 목록
|
||
column_order?: string[]; // 컬럼 순서 (선택사항)
|
||
}
|
||
|
||
/**
|
||
* 플로우 컴포넌트
|
||
*/
|
||
export interface FlowComponent extends BaseComponent {
|
||
type: "flow";
|
||
flow_id?: number; // 선택된 플로우 ID
|
||
flow_name?: string; // 플로우 이름 (표시용)
|
||
show_step_count?: boolean; // 각 스텝의 데이터 건수 표시 여부
|
||
allow_data_move?: boolean; // 데이터 이동 허용 여부
|
||
display_mode?: "horizontal" | "vertical"; // 플로우 표시 방향
|
||
// 🆕 단계별 컬럼 오버라이드 설정 (화면관리에서 설정)
|
||
step_column_config?: {
|
||
[stepId: number]: FlowStepColumnConfig;
|
||
};
|
||
// 🆕 컬럼 값 기반 데이터 필터링
|
||
data_filter?: DataFilterConfig;
|
||
}
|
||
|
||
/**
|
||
* 새로운 컴포넌트 시스템 컴포넌트
|
||
*/
|
||
export interface ComponentComponent extends BaseComponent {
|
||
type: "component";
|
||
widget_type: WebType; // 웹타입 (기존 호환성)
|
||
component_type: string; // 새 컴포넌트 시스템의 ID
|
||
component_config: any; // 컴포넌트별 설정
|
||
}
|
||
|
||
/**
|
||
* 탭 아이템 인터페이스
|
||
*/
|
||
/**
|
||
* 탭 내부 컴포넌트 (자유 배치)
|
||
*/
|
||
export interface TabInlineComponent {
|
||
id: string;
|
||
component_type: string; // 컴포넌트 타입 (예: "v2-text-display", "v2-table-list")
|
||
label?: string;
|
||
position: Position; // 탭 내부에서의 위치
|
||
size: Size; // 컴포넌트 크기
|
||
component_config?: any; // 컴포넌트별 설정
|
||
style?: ComponentStyle;
|
||
}
|
||
|
||
export interface TabItem {
|
||
id: string;
|
||
label: string;
|
||
icon?: string; // 아이콘 (선택사항)
|
||
disabled?: boolean; // 비활성화 여부
|
||
order: number; // 탭 순서
|
||
// 🆕 인라인 컴포넌트 배치
|
||
components?: TabInlineComponent[]; // 탭 내부 컴포넌트들
|
||
}
|
||
|
||
/**
|
||
* 탭 컴포넌트
|
||
*/
|
||
export interface TabsComponent extends BaseComponent {
|
||
type: "tabs";
|
||
tabs: TabItem[]; // 탭 목록
|
||
default_tab?: string; // 기본 선택 탭 ID
|
||
orientation?: "horizontal" | "vertical"; // 탭 방향
|
||
variant?: "default" | "pills" | "underline"; // 탭 스타일
|
||
allow_closeable?: boolean; // 탭 닫기 버튼 표시 여부
|
||
persist_selection?: boolean; // 선택 상태 유지 (localStorage)
|
||
}
|
||
|
||
/**
|
||
* 통합 컴포넌트 데이터 타입
|
||
*/
|
||
export type ComponentData =
|
||
| WidgetComponent
|
||
| ContainerComponent
|
||
| GroupComponent
|
||
| DataTableComponent
|
||
| FileComponent
|
||
| FlowComponent
|
||
| ComponentComponent
|
||
| TabsComponent;
|
||
|
||
// ===== 웹타입별 설정 인터페이스 =====
|
||
|
||
/**
|
||
* 기본 웹타입 설정
|
||
*/
|
||
export interface WebTypeConfig {
|
||
[key: string]: unknown;
|
||
}
|
||
|
||
/**
|
||
* 날짜/시간 타입 설정
|
||
*/
|
||
export interface DateTypeConfig {
|
||
format: "YYYY-MM-DD" | "YYYY-MM-DD HH:mm" | "YYYY-MM-DD HH:mm:ss";
|
||
show_time: boolean;
|
||
min_date?: string;
|
||
max_date?: string;
|
||
default_value?: string;
|
||
placeholder?: string;
|
||
}
|
||
|
||
/**
|
||
* 숫자 타입 설정
|
||
*/
|
||
export interface NumberTypeConfig {
|
||
min?: number;
|
||
max?: number;
|
||
step?: number;
|
||
format?: "integer" | "decimal" | "currency" | "percentage";
|
||
decimal_places?: number;
|
||
thousand_separator?: boolean;
|
||
placeholder?: string;
|
||
}
|
||
|
||
/**
|
||
* 선택박스 타입 설정
|
||
*/
|
||
export interface SelectTypeConfig {
|
||
options: Array<{ label: string; value: string; disabled?: boolean }>;
|
||
multiple?: boolean;
|
||
searchable?: boolean;
|
||
placeholder?: string;
|
||
allow_custom_value?: boolean;
|
||
default_value?: string;
|
||
required?: boolean;
|
||
readonly?: boolean;
|
||
empty_message?: string;
|
||
|
||
/** 🆕 연쇄 드롭다운 관계 코드 (관계 관리에서 정의한 코드) */
|
||
cascading_relation_code?: string;
|
||
|
||
/** 🆕 연쇄 드롭다운 부모 필드명 (화면 내 다른 필드의 column_name) */
|
||
cascading_parent_field?: string;
|
||
|
||
/** @deprecated 직접 설정 방식 - cascadingRelationCode 사용 권장 */
|
||
cascading?: CascadingDropdownConfig;
|
||
}
|
||
|
||
/**
|
||
* 텍스트 타입 설정
|
||
*/
|
||
export interface TextTypeConfig {
|
||
min_length?: number;
|
||
max_length?: number;
|
||
pattern?: string;
|
||
format?: "none" | "email" | "phone" | "url" | "korean" | "english";
|
||
placeholder?: string;
|
||
default_value?: string;
|
||
multiline?: boolean;
|
||
rows?: number;
|
||
// 자동입력 관련 설정
|
||
auto_input?: boolean;
|
||
auto_value_type?:
|
||
| "current_datetime"
|
||
| "current_date"
|
||
| "current_time"
|
||
| "current_user"
|
||
| "uuid"
|
||
| "sequence"
|
||
| "numbering_rule"
|
||
| "custom";
|
||
custom_value?: string;
|
||
numbering_rule_id?: string; // 채번 규칙 ID
|
||
}
|
||
|
||
/**
|
||
* 텍스트 영역 타입 설정
|
||
*/
|
||
export interface TextareaTypeConfig {
|
||
rows?: number;
|
||
max_length?: number;
|
||
min_length?: number;
|
||
placeholder?: string;
|
||
resizable?: boolean;
|
||
auto_resize?: boolean;
|
||
word_wrap?: boolean;
|
||
default_value?: string;
|
||
}
|
||
|
||
/**
|
||
* 라디오 버튼 타입 설정
|
||
*/
|
||
export interface RadioTypeConfig {
|
||
options: Array<{ label: string; value: string; disabled?: boolean }>;
|
||
layout?: "vertical" | "horizontal" | "grid";
|
||
default_value?: string;
|
||
allow_none?: boolean;
|
||
}
|
||
|
||
/**
|
||
* 배열(다중 입력) 타입 설정
|
||
*/
|
||
export interface ArrayTypeConfig {
|
||
item_type?: "text" | "number" | "email" | "tel"; // 각 항목의 입력 타입
|
||
min_items?: number; // 최소 항목 수
|
||
max_items?: number; // 최대 항목 수
|
||
placeholder?: string; // 입력 필드 placeholder
|
||
add_button_text?: string; // + 버튼 텍스트
|
||
remove_button_text?: string; // - 버튼 텍스트 (보통 아이콘)
|
||
allow_reorder?: boolean; // 순서 변경 가능 여부
|
||
show_index?: boolean; // 인덱스 번호 표시 여부
|
||
}
|
||
|
||
/**
|
||
* 파일 타입 설정
|
||
*/
|
||
export interface FileTypeConfig {
|
||
accept?: string[];
|
||
multiple?: boolean;
|
||
max_size?: number; // MB
|
||
max_files?: number;
|
||
show_preview?: boolean;
|
||
show_progress?: boolean;
|
||
doc_type?: string;
|
||
doc_type_name?: string;
|
||
drag_drop_text?: string;
|
||
upload_button_text?: string;
|
||
auto_upload?: boolean;
|
||
chunked_upload?: boolean;
|
||
linked_table?: string;
|
||
linked_field?: string;
|
||
auto_link?: boolean;
|
||
company_code?: CompanyCode;
|
||
}
|
||
|
||
/**
|
||
* 엔티티 타입 설정
|
||
*/
|
||
export interface EntityTypeConfig {
|
||
reference_table: string;
|
||
reference_column: string;
|
||
display_columns: string[]; // 여러 표시 컬럼을 배열로 변경
|
||
display_column?: string; // 하위 호환성을 위해 유지 (deprecated)
|
||
search_columns?: string[];
|
||
filters?: Record<string, unknown>;
|
||
placeholder?: string;
|
||
display_format?: "simple" | "detailed" | "custom"; // 표시 형식
|
||
separator?: string; // 여러 컬럼 표시 시 구분자 (기본: ' - ')
|
||
// UI 모드
|
||
ui_mode?: "select" | "modal" | "combo" | "autocomplete"; // 기본: "combo"
|
||
// 다중 선택
|
||
multiple?: boolean; // 여러 항목 선택 가능 여부 (기본: false)
|
||
}
|
||
|
||
/**
|
||
* 🆕 연쇄 드롭다운(Cascading Dropdown) 설정
|
||
*
|
||
* 부모 필드의 값에 따라 자식 드롭다운의 옵션이 동적으로 변경됩니다.
|
||
* 예: 창고 선택 → 해당 창고의 위치만 표시
|
||
*
|
||
* @example
|
||
* // 창고 → 위치 연쇄 드롭다운
|
||
* {
|
||
* enabled: true,
|
||
* parentField: "warehouse_code",
|
||
* sourceTable: "warehouse_location",
|
||
* parentKeyColumn: "warehouse_id",
|
||
* valueColumn: "location_code",
|
||
* labelColumn: "location_name",
|
||
* }
|
||
*/
|
||
export interface CascadingDropdownConfig {
|
||
/** 연쇄 드롭다운 활성화 여부 */
|
||
enabled: boolean;
|
||
|
||
/** 부모 필드명 (이 필드의 값에 따라 옵션이 필터링됨) */
|
||
parent_field: string;
|
||
|
||
/** 옵션을 조회할 테이블명 */
|
||
source_table: string;
|
||
|
||
/** 부모 값과 매칭할 컬럼명 (source_table의 컬럼) */
|
||
parent_key_column: string;
|
||
|
||
/** 드롭다운 value로 사용할 컬럼명 */
|
||
value_column: string;
|
||
|
||
/** 드롭다운 label로 표시할 컬럼명 */
|
||
label_column: string;
|
||
|
||
/** 추가 필터 조건 (선택사항) */
|
||
additional_filters?: Record<string, unknown>;
|
||
|
||
/** 부모 값이 없을 때 표시할 메시지 */
|
||
empty_parent_message?: string;
|
||
|
||
/** 옵션이 없을 때 표시할 메시지 */
|
||
no_options_message?: string;
|
||
|
||
/** 로딩 중 표시할 메시지 */
|
||
loading_message?: string;
|
||
|
||
/** 부모 값 변경 시 자동으로 값 초기화 */
|
||
clear_on_parent_change?: boolean;
|
||
}
|
||
|
||
/**
|
||
* 버튼 타입 설정
|
||
*/
|
||
export interface ButtonTypeConfig {
|
||
text?: string;
|
||
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
|
||
size?: "sm" | "md" | "lg";
|
||
icon?: string;
|
||
// ButtonActionType과 관련된 설정은 control-management.ts에서 정의
|
||
}
|
||
|
||
// ===== 즉시 저장(quickInsert) 설정 =====
|
||
|
||
/**
|
||
* 즉시 저장 컬럼 매핑 설정
|
||
* 저장할 테이블의 각 컬럼에 대해 값을 어디서 가져올지 정의
|
||
*/
|
||
export interface QuickInsertColumnMapping {
|
||
/** 저장할 테이블의 대상 컬럼명 */
|
||
target_column: string;
|
||
|
||
/** 값 소스 타입 */
|
||
source_type: "component" | "leftPanel" | "fixed" | "currentUser";
|
||
|
||
// source_type별 추가 설정
|
||
/** component: 값을 가져올 컴포넌트 ID */
|
||
source_component_id?: string;
|
||
|
||
/** component: 컴포넌트의 column_name (formData 접근용) */
|
||
source_column_name?: string;
|
||
|
||
/** leftPanel: 좌측 선택 데이터의 컬럼명 */
|
||
source_column?: string;
|
||
|
||
/** fixed: 고정값 */
|
||
fixed_value?: any;
|
||
|
||
/** currentUser: 사용자 정보 필드 */
|
||
user_field?: "userId" | "userName" | "companyCode" | "deptCode";
|
||
}
|
||
|
||
/**
|
||
* 즉시 저장 후 동작 설정
|
||
*/
|
||
export interface QuickInsertAfterAction {
|
||
/** 데이터 새로고침 (테이블리스트, 카드 디스플레이 컴포넌트) */
|
||
refresh_data?: boolean;
|
||
|
||
/** 초기화할 컴포넌트 ID 목록 */
|
||
clear_components?: string[];
|
||
|
||
/** 성공 메시지 표시 여부 */
|
||
show_success_message?: boolean;
|
||
|
||
/** 커스텀 성공 메시지 */
|
||
success_message?: string;
|
||
}
|
||
|
||
/**
|
||
* 중복 체크 설정
|
||
*/
|
||
export interface QuickInsertDuplicateCheck {
|
||
/** 중복 체크 활성화 */
|
||
enabled: boolean;
|
||
|
||
/** 중복 체크할 컬럼들 */
|
||
columns: string[];
|
||
|
||
/** 중복 시 에러 메시지 */
|
||
error_message?: string;
|
||
}
|
||
|
||
/**
|
||
* 즉시 저장(quickInsert) 버튼 액션 설정
|
||
*
|
||
* 화면에서 entity 타입 선택박스로 데이터를 선택한 후,
|
||
* 버튼 클릭 시 특정 테이블에 즉시 INSERT하는 기능
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const config: QuickInsertConfig = {
|
||
* targetTable: "process_equipment",
|
||
* columnMappings: [
|
||
* {
|
||
* targetColumn: "equipment_code",
|
||
* sourceType: "component",
|
||
* sourceComponentId: "equipment-select"
|
||
* },
|
||
* {
|
||
* targetColumn: "process_code",
|
||
* sourceType: "leftPanel",
|
||
* sourceColumn: "process_code"
|
||
* }
|
||
* ],
|
||
* afterInsert: {
|
||
* refreshData: true,
|
||
* clearComponents: ["equipment-select"],
|
||
* showSuccessMessage: true
|
||
* }
|
||
* };
|
||
* ```
|
||
*/
|
||
export interface QuickInsertConfig {
|
||
/** 저장할 대상 테이블명 */
|
||
target_table: string;
|
||
|
||
/** 컬럼 매핑 설정 */
|
||
column_mappings: QuickInsertColumnMapping[];
|
||
|
||
/** 저장 후 동작 설정 */
|
||
after_insert?: QuickInsertAfterAction;
|
||
|
||
/** 중복 체크 설정 (선택사항) */
|
||
duplicate_check?: QuickInsertDuplicateCheck;
|
||
}
|
||
|
||
/**
|
||
* 플로우 단계별 버튼 표시 설정
|
||
*
|
||
* 플로우 위젯과 버튼을 함께 사용할 때, 특정 플로우 단계에서만 버튼을 표시하거나 숨길 수 있습니다.
|
||
*/
|
||
export interface FlowVisibilityConfig {
|
||
/**
|
||
* 플로우 단계별 표시 제어 활성화 여부
|
||
*/
|
||
enabled: boolean;
|
||
|
||
/**
|
||
* 대상 플로우 컴포넌트 ID
|
||
* 화면에 여러 플로우 위젯이 있을 경우, 어떤 플로우에 반응할지 지정
|
||
*/
|
||
target_flow_component_id: string;
|
||
|
||
/**
|
||
* 대상 플로우 정의 ID (선택사항, 검증용)
|
||
*/
|
||
target_flow_id?: number;
|
||
|
||
/**
|
||
* 대상 플로우 이름 (표시용)
|
||
*/
|
||
target_flow_name?: string;
|
||
|
||
/**
|
||
* 표시 조건 모드
|
||
* - whitelist: visibleSteps에 포함된 단계에서만 표시
|
||
* - blacklist: hiddenSteps에 포함된 단계에서 숨김
|
||
* - all: 모든 단계에서 표시 (기본값)
|
||
*/
|
||
mode: "whitelist" | "blacklist" | "all";
|
||
|
||
/**
|
||
* 표시할 단계 ID 목록 (mode="whitelist"일 때 사용)
|
||
*/
|
||
visible_steps?: number[];
|
||
|
||
/**
|
||
* 숨길 단계 ID 목록 (mode="blacklist"일 때 사용)
|
||
*/
|
||
hidden_steps?: number[];
|
||
|
||
/**
|
||
* 레이아웃 동작 방식
|
||
* - preserve-position: 원래 위치 유지 (display: none, 빈 공간 유지)
|
||
* - auto-compact: 빈 공간 자동 제거 (Flexbox 그룹으로 자동 정렬)
|
||
*/
|
||
layout_behavior: "preserve-position" | "auto-compact";
|
||
|
||
/**
|
||
* 그룹 ID (auto-compact 모드일 때 사용)
|
||
* 같은 그룹 ID를 가진 버튼들이 하나의 FlowButtonGroup으로 묶임
|
||
*/
|
||
group_id?: string;
|
||
|
||
/**
|
||
* 그룹 정렬 방향 (auto-compact 모드일 때 사용)
|
||
*/
|
||
group_direction?: "horizontal" | "vertical";
|
||
|
||
/**
|
||
* 그룹 내 버튼 간격 (px, auto-compact 모드일 때 사용)
|
||
*/
|
||
group_gap?: number;
|
||
|
||
/**
|
||
* 그룹 정렬 방식 (auto-compact 모드일 때 사용)
|
||
*/
|
||
group_align?: "start" | "center" | "end" | "space-between" | "space-around";
|
||
}
|
||
|
||
// ===== 데이터 테이블 관련 =====
|
||
|
||
/**
|
||
* 데이터 테이블 컬럼
|
||
*/
|
||
export interface DataTableColumn {
|
||
id: string;
|
||
column_name: string;
|
||
label: string;
|
||
data_type?: string;
|
||
widget_type?: DynamicWebType;
|
||
width?: number;
|
||
sortable?: boolean;
|
||
searchable?: boolean;
|
||
visible: boolean;
|
||
frozen?: boolean;
|
||
align?: "left" | "center" | "right";
|
||
}
|
||
|
||
/**
|
||
* 데이터 테이블 필터
|
||
*/
|
||
export interface DataTableFilter {
|
||
id: string;
|
||
column_name: string;
|
||
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN";
|
||
value: unknown;
|
||
logical_operator?: "AND" | "OR";
|
||
}
|
||
|
||
/**
|
||
* 컬럼 필터 조건 (단일 필터)
|
||
*/
|
||
export interface ColumnFilter {
|
||
id: string;
|
||
column_name: string; // 필터링할 컬럼명
|
||
operator:
|
||
| "equals"
|
||
| "not_equals"
|
||
| "in"
|
||
| "not_in"
|
||
| "contains"
|
||
| "starts_with"
|
||
| "ends_with"
|
||
| "is_null"
|
||
| "is_not_null"
|
||
| "greater_than"
|
||
| "less_than"
|
||
| "greater_than_or_equal"
|
||
| "less_than_or_equal"
|
||
| "between"
|
||
| "date_range_contains"; // 날짜 범위 포함 (start_date <= value <= end_date)
|
||
value: string | string[]; // 필터 값 (in/not_in은 배열, date_range_contains는 비교할 날짜)
|
||
value_type: "static" | "category" | "code" | "dynamic"; // 값 타입 (dynamic: 현재 날짜 등)
|
||
// date_range_contains 전용 설정
|
||
range_config?: {
|
||
start_column: string; // 시작일 컬럼명
|
||
end_column: string; // 종료일 컬럼명
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 데이터 필터 설정 (여러 필터의 조합)
|
||
*/
|
||
export interface DataFilterConfig {
|
||
enabled: boolean; // 필터 활성화 여부
|
||
filters: ColumnFilter[]; // 필터 조건 목록
|
||
match_type: "all" | "any"; // AND(모두 만족) / OR(하나 이상 만족)
|
||
}
|
||
|
||
// ===== 파일 업로드 관련 =====
|
||
|
||
/**
|
||
* 업로드된 파일 정보
|
||
*/
|
||
export interface UploadedFile {
|
||
objid: string;
|
||
real_file_name: string;
|
||
saved_file_name: string;
|
||
file_size: number;
|
||
file_ext: string;
|
||
file_path: string;
|
||
doc_type?: string;
|
||
doc_type_name?: string;
|
||
target_objid: string;
|
||
parent_target_objid?: string;
|
||
writer?: string;
|
||
regdate?: string;
|
||
status?: "uploading" | "completed" | "error";
|
||
company_code?: CompanyCode;
|
||
}
|
||
|
||
// ===== 화면 정의 관련 =====
|
||
|
||
/**
|
||
* 화면 정의
|
||
*/
|
||
export interface ScreenDefinition {
|
||
screen_id: number;
|
||
screen_name: string;
|
||
screen_code: string;
|
||
table_name: string;
|
||
table_label?: string;
|
||
company_code: CompanyCode;
|
||
description?: string;
|
||
is_active: ActiveStatus;
|
||
created_date: Date;
|
||
updated_date: Date;
|
||
created_by?: string;
|
||
updated_by?: string;
|
||
db_source_type?: "internal" | "external";
|
||
db_connection_id?: number;
|
||
// REST API 관련 필드
|
||
data_source_type?: "database" | "restapi";
|
||
rest_api_connection_id?: number;
|
||
rest_api_endpoint?: string;
|
||
rest_api_json_path?: string;
|
||
|
||
// ─── INVYONE 확장 (Phase 1+) ───
|
||
/** 화면 수준 필드 규격 — 테이블/폼/검색 컴포넌트가 공유하는 단일 필드 정의 */
|
||
fields?: FieldConfig[];
|
||
/** 컴포넌트 간 DataPort 연결 목록 (런타임에 DataPortBus 브리지로 변환) */
|
||
connections?: Connection[];
|
||
|
||
// ─── INVYONE 스튜디오 (Templates 모드) ───
|
||
/** 템플릿 모드일 때 저장/로드 대상 TEMPLATE_ID. 세팅 시 screens 테이블이 아닌 templates 테이블로 입출력 */
|
||
template_id?: string;
|
||
/** 템플릿 모드 여부 — true 면 ScreenDesigner 가 templates API 로 동작 */
|
||
is_template_mode?: boolean;
|
||
/** 템플릿 카테고리 (예: sales/production/hr/inventory/finance/admin) */
|
||
template_category?: string;
|
||
/** 템플릿 게시 상태 (draft|published) */
|
||
template_status?: "draft" | "published";
|
||
}
|
||
|
||
/**
|
||
* 화면 생성 요청
|
||
*/
|
||
export interface CreateScreenRequest {
|
||
screen_name: string;
|
||
screen_code?: string;
|
||
table_name: string;
|
||
table_label?: string;
|
||
company_code: CompanyCode;
|
||
description?: string;
|
||
db_source_type?: "internal" | "external";
|
||
db_connection_id?: number;
|
||
// REST API 관련 필드
|
||
data_source_type?: "database" | "restapi";
|
||
rest_api_connection_id?: number;
|
||
rest_api_endpoint?: string;
|
||
rest_api_json_path?: string;
|
||
}
|
||
|
||
/**
|
||
* 화면 수정 요청
|
||
*/
|
||
export interface UpdateScreenRequest {
|
||
screen_name?: string;
|
||
screen_code?: string;
|
||
table_name?: string;
|
||
table_label?: string;
|
||
description?: string;
|
||
is_active?: ActiveStatus;
|
||
}
|
||
|
||
/**
|
||
* 화면 해상도 설정
|
||
*/
|
||
export interface ScreenResolution {
|
||
width: number;
|
||
height: number;
|
||
name: string;
|
||
category: "desktop" | "tablet" | "mobile" | "custom";
|
||
}
|
||
|
||
/**
|
||
* 미리 정의된 해상도 프리셋
|
||
*/
|
||
export const SCREEN_RESOLUTIONS: ScreenResolution[] = [
|
||
// Desktop
|
||
{ width: 1920, height: 1080, name: "Full HD (1920×1080)", category: "desktop" },
|
||
{ width: 1366, height: 768, name: "HD (1366×768)", category: "desktop" },
|
||
{ width: 1440, height: 900, name: "WXGA+ (1440×900)", category: "desktop" },
|
||
{ width: 1280, height: 1024, name: "SXGA (1280×1024)", category: "desktop" },
|
||
|
||
// Tablet
|
||
{ width: 1024, height: 768, name: "iPad Landscape (1024×768)", category: "tablet" },
|
||
{ width: 768, height: 1024, name: "iPad Portrait (768×1024)", category: "tablet" },
|
||
{ width: 1112, height: 834, name: 'iPad Pro 10.5" Landscape', category: "tablet" },
|
||
{ width: 834, height: 1112, name: 'iPad Pro 10.5" Portrait', category: "tablet" },
|
||
|
||
// Mobile
|
||
{ width: 375, height: 667, name: "iPhone 8 (375×667)", category: "mobile" },
|
||
{ width: 414, height: 896, name: "iPhone 11 (414×896)", category: "mobile" },
|
||
{ width: 390, height: 844, name: "iPhone 12/13 (390×844)", category: "mobile" },
|
||
{ width: 360, height: 640, name: "Android Medium (360×640)", category: "mobile" },
|
||
];
|
||
|
||
/**
|
||
* 그룹화 상태
|
||
*/
|
||
export interface GroupState {
|
||
isGrouping: boolean;
|
||
selected_components: string[];
|
||
group_target?: string | null;
|
||
group_mode?: "create" | "add" | "remove" | "ungroup";
|
||
group_title?: string;
|
||
}
|
||
|
||
// ============================================
|
||
// 레이어 시스템 타입 정의
|
||
// ============================================
|
||
|
||
/**
|
||
* 레이어 타입
|
||
* - base: 기본 레이어 (항상 표시)
|
||
* - conditional: 조건부 레이어 (특정 조건 만족 시 표시)
|
||
* - modal: 모달 레이어 (팝업 형태)
|
||
* - drawer: 드로어 레이어 (사이드 패널 형태)
|
||
*/
|
||
export type LayerType = "base" | "conditional" | "modal" | "drawer";
|
||
|
||
/**
|
||
* 레이어 조건부 표시 설정
|
||
*/
|
||
export interface LayerCondition {
|
||
target_component_id: string; // 트리거가 되는 컴포넌트 ID
|
||
operator: "eq" | "neq" | "in"; // 비교 연산자
|
||
value: any; // 비교할 값
|
||
}
|
||
|
||
/**
|
||
* 레이어 오버레이 설정 (모달/드로어용)
|
||
*/
|
||
export interface LayerOverlayConfig {
|
||
backdrop: boolean; // 배경 어둡게 처리 여부
|
||
close_on_backdrop_click: boolean; // 배경 클릭 시 닫기 여부
|
||
width?: string | number; // 너비
|
||
height?: string | number; // 높이
|
||
// 모달/드로어 스타일링
|
||
background_color?: string; // 컨텐츠 배경색
|
||
backdrop_blur?: number; // 배경 블러 (px)
|
||
// 드로어 전용
|
||
position?: "left" | "right" | "top" | "bottom"; // 드로어 위치
|
||
}
|
||
|
||
/**
|
||
* 조건부 레이어 표시 영역
|
||
* @deprecated Zone 기반으로 전환 - ConditionalZone.x/y/width/height 사용
|
||
*/
|
||
export interface DisplayRegion {
|
||
x: number;
|
||
y: number;
|
||
width: number;
|
||
height: number;
|
||
}
|
||
|
||
/**
|
||
* 조건부 영역(Zone)
|
||
* - 기본 레이어 캔버스에서 영역을 정의하고, 여러 레이어를 할당
|
||
* - Zone 내에서는 항상 1개 레이어만 활성 (exclusive)
|
||
* - Zone 단위로 접힘/펼침 판단 (Y 오프셋 계산 단순화)
|
||
*/
|
||
export interface ConditionalZone {
|
||
zone_id: number;
|
||
screen_id: number;
|
||
company_code: string;
|
||
zone_name: string;
|
||
x: number;
|
||
y: number;
|
||
width: number;
|
||
height: number;
|
||
trigger_component_id: string | null; // 기본 레이어의 트리거 컴포넌트 ID
|
||
trigger_operator: string; // eq, neq, in
|
||
created_at?: string;
|
||
updated_at?: string;
|
||
}
|
||
|
||
/**
|
||
* 레이어 정의
|
||
*/
|
||
export interface LayerDefinition {
|
||
id: string;
|
||
name: string;
|
||
type: LayerType;
|
||
z_index: number;
|
||
isVisible: boolean; // 초기 표시 여부
|
||
isLocked: boolean; // 편집 잠금 여부
|
||
|
||
// 조건부 표시 로직 (레거시 - Zone 미사용 레이어용)
|
||
condition?: LayerCondition;
|
||
|
||
// Zone 기반 조건부 설정 (신규)
|
||
zone_id?: number; // 소속 조건부 영역 ID
|
||
condition_value?: string; // Zone 트리거 매칭 값
|
||
|
||
// 조건부 레이어 표시 영역 (레거시 호환 - Zone으로 대체됨)
|
||
display_region?: DisplayRegion;
|
||
|
||
// 모달/드로어 전용 설정
|
||
overlay_config?: LayerOverlayConfig;
|
||
|
||
// 해당 레이어에 속한 컴포넌트들
|
||
components: ComponentData[];
|
||
}
|
||
|
||
/**
|
||
* 레이아웃 데이터
|
||
*/
|
||
export interface LayoutData {
|
||
screen_id: number;
|
||
components: ComponentData[]; // @deprecated - use layers instead (kept for backward compatibility)
|
||
layers?: LayerDefinition[]; // 🆕 레이어 목록
|
||
grid_settings?: GridSettings;
|
||
metadata?: LayoutMetadata;
|
||
screen_resolution?: ScreenResolution;
|
||
}
|
||
|
||
/**
|
||
* 격자 설정
|
||
*/
|
||
export interface GridSettings {
|
||
enabled: boolean;
|
||
size: number;
|
||
color: string;
|
||
opacity: number;
|
||
snap_to_grid: boolean;
|
||
// gridUtils에서 필요한 속성들 추가
|
||
columns: number;
|
||
gap: number;
|
||
padding: number;
|
||
show_grid?: boolean;
|
||
grid_color?: string;
|
||
grid_opacity?: number;
|
||
}
|
||
|
||
/**
|
||
* 레이아웃 메타데이터
|
||
*/
|
||
export interface LayoutMetadata {
|
||
version: string;
|
||
last_modified: Date;
|
||
modified_by: string;
|
||
description?: string;
|
||
tags?: string[];
|
||
}
|
||
|
||
// ===== 템플릿 관련 =====
|
||
|
||
/**
|
||
* 화면 템플릿
|
||
*/
|
||
export interface ScreenTemplate {
|
||
id: string;
|
||
name: string;
|
||
description?: string;
|
||
category: string;
|
||
components: ComponentData[];
|
||
preview_image?: string;
|
||
isActive: boolean;
|
||
}
|
||
|
||
/**
|
||
* 템플릿 컴포넌트 (템플릿 패널에서 사용)
|
||
*/
|
||
export interface TemplateComponent {
|
||
id: string;
|
||
name: string;
|
||
description?: string;
|
||
icon?: string;
|
||
category: string;
|
||
default_props: Partial<ComponentData>;
|
||
children?: Array<{
|
||
id: string;
|
||
name: string;
|
||
default_props: Partial<ComponentData>;
|
||
}>;
|
||
}
|
||
|
||
// ===== 타입 가드 함수들 =====
|
||
|
||
/**
|
||
* WidgetComponent 타입 가드 (강화된 검증)
|
||
*/
|
||
export const isWidgetComponent = (component: ComponentData): component is WidgetComponent => {
|
||
if (!component || typeof component !== "object") {
|
||
return false;
|
||
}
|
||
|
||
// 기본 타입 체크
|
||
if (component.type !== "widget") {
|
||
return false;
|
||
}
|
||
|
||
// 필수 필드 존재 여부 체크
|
||
if (!component.id || typeof component.id !== "string") {
|
||
return false;
|
||
}
|
||
|
||
// widget_type 이 유효한 WebType 인지 체크
|
||
if (!component.widget_type || !isWebType(component.widget_type)) {
|
||
return false;
|
||
}
|
||
|
||
// position 검증
|
||
if (
|
||
!component.position ||
|
||
typeof component.position.x !== "number" ||
|
||
typeof component.position.y !== "number" ||
|
||
!Number.isFinite(component.position.x) ||
|
||
!Number.isFinite(component.position.y)
|
||
) {
|
||
return false;
|
||
}
|
||
|
||
// size 검증
|
||
if (
|
||
!component.size ||
|
||
typeof component.size.width !== "number" ||
|
||
typeof component.size.height !== "number" ||
|
||
!Number.isFinite(component.size.width) ||
|
||
!Number.isFinite(component.size.height) ||
|
||
component.size.width <= 0 ||
|
||
component.size.height <= 0
|
||
) {
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
};
|
||
|
||
/**
|
||
* ContainerComponent 타입 가드 (강화된 검증)
|
||
*/
|
||
export const isContainerComponent = (component: ComponentData): component is ContainerComponent => {
|
||
if (!component || typeof component !== "object") {
|
||
return false;
|
||
}
|
||
|
||
// 기본 타입 체크
|
||
if (!["container", "row", "column", "area"].includes(component.type)) {
|
||
return false;
|
||
}
|
||
|
||
// 필수 필드 존재 여부 체크
|
||
if (!component.id || typeof component.id !== "string") {
|
||
return false;
|
||
}
|
||
|
||
// position 검증
|
||
if (
|
||
!component.position ||
|
||
typeof component.position.x !== "number" ||
|
||
typeof component.position.y !== "number" ||
|
||
!Number.isFinite(component.position.x) ||
|
||
!Number.isFinite(component.position.y)
|
||
) {
|
||
return false;
|
||
}
|
||
|
||
// size 검증
|
||
if (
|
||
!component.size ||
|
||
typeof component.size.width !== "number" ||
|
||
typeof component.size.height !== "number" ||
|
||
!Number.isFinite(component.size.width) ||
|
||
!Number.isFinite(component.size.height) ||
|
||
component.size.width <= 0 ||
|
||
component.size.height <= 0
|
||
) {
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
};
|
||
|
||
/**
|
||
* GroupComponent 타입 가드
|
||
*/
|
||
export const isGroupComponent = (component: ComponentData): component is GroupComponent => {
|
||
return component.type === "group";
|
||
};
|
||
|
||
/**
|
||
* DataTableComponent 타입 가드
|
||
*/
|
||
export const isDataTableComponent = (component: ComponentData): component is DataTableComponent => {
|
||
return component.type === "datatable";
|
||
};
|
||
|
||
/**
|
||
* FileComponent 타입 가드
|
||
*/
|
||
export const isFileComponent = (component: ComponentData): component is FileComponent => {
|
||
return component.type === "file";
|
||
};
|
||
|
||
/**
|
||
* FlowComponent 타입 가드
|
||
*/
|
||
export const isFlowComponent = (component: ComponentData): component is FlowComponent => {
|
||
return component.type === "flow";
|
||
};
|
||
|
||
/**
|
||
* TabsComponent 타입 가드
|
||
*/
|
||
export const isTabsComponent = (component: ComponentData): component is TabsComponent => {
|
||
return component.type === "tabs";
|
||
};
|
||
|
||
// ===== 안전한 타입 캐스팅 유틸리티 =====
|
||
|
||
/**
|
||
* ComponentData를 WidgetComponent로 안전하게 캐스팅
|
||
*/
|
||
export const asWidgetComponent = (component: ComponentData): WidgetComponent => {
|
||
if (!isWidgetComponent(component)) {
|
||
throw new Error(`Expected WidgetComponent, got ${component.type}`);
|
||
}
|
||
return component;
|
||
};
|
||
|
||
/**
|
||
* ComponentData를 ContainerComponent로 안전하게 캐스팅
|
||
*/
|
||
export const asContainerComponent = (component: ComponentData): ContainerComponent => {
|
||
if (!isContainerComponent(component)) {
|
||
throw new Error(`Expected ContainerComponent, got ${component.type}`);
|
||
}
|
||
return component;
|
||
};
|
||
|
||
/**
|
||
* ComponentData를 GroupComponent로 안전하게 캐스팅
|
||
*/
|
||
export const asGroupComponent = (component: ComponentData): GroupComponent => {
|
||
if (!isGroupComponent(component)) {
|
||
throw new Error(`Expected GroupComponent, got ${component.type}`);
|
||
}
|
||
return component;
|
||
};
|
||
|
||
/**
|
||
* ComponentData를 DataTableComponent로 안전하게 캐스팅
|
||
*/
|
||
export const asDataTableComponent = (component: ComponentData): DataTableComponent => {
|
||
if (!isDataTableComponent(component)) {
|
||
throw new Error(`Expected DataTableComponent, got ${component.type}`);
|
||
}
|
||
return component;
|
||
};
|
||
|
||
/**
|
||
* ComponentData를 FileComponent로 안전하게 캐스팅
|
||
*/
|
||
export const asFileComponent = (component: ComponentData): FileComponent => {
|
||
if (!isFileComponent(component)) {
|
||
throw new Error(`Expected FileComponent, got ${component.type}`);
|
||
}
|
||
return component;
|
||
};
|
||
|
||
/**
|
||
* ComponentData를 FlowComponent로 안전하게 캐스팅
|
||
*/
|
||
export const asFlowComponent = (component: ComponentData): FlowComponent => {
|
||
if (!isFlowComponent(component)) {
|
||
throw new Error(`Expected FlowComponent, got ${component.type}`);
|
||
}
|
||
return component;
|
||
};
|
||
|
||
/**
|
||
* ComponentData를 TabsComponent로 안전하게 캐스팅
|
||
*/
|
||
export const asTabsComponent = (component: ComponentData): TabsComponent => {
|
||
if (!isTabsComponent(component)) {
|
||
throw new Error(`Expected TabsComponent, got ${component.type}`);
|
||
}
|
||
return component;
|
||
};
|