2025-09-19 18:43:55 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 🖥️ 화면관리 시스템 전용 타입 정의
|
|
|
|
|
|
*
|
|
|
|
|
|
* 화면 설계, 컴포넌트 관리, 레이아웃 등 화면관리 시스템에서만 사용하는 타입들
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
|
ComponentType,
|
|
|
|
|
|
WebType,
|
|
|
|
|
|
DynamicWebType,
|
|
|
|
|
|
Position,
|
|
|
|
|
|
Size,
|
|
|
|
|
|
CommonStyle,
|
|
|
|
|
|
ValidationRule,
|
|
|
|
|
|
TimestampFields,
|
|
|
|
|
|
CompanyCode,
|
|
|
|
|
|
ActiveStatus,
|
|
|
|
|
|
isWebType,
|
|
|
|
|
|
} from "./unified-core";
|
|
|
|
|
|
|
|
|
|
|
|
// ===== 기본 컴포넌트 인터페이스 =====
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 모든 컴포넌트의 기본 인터페이스
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface BaseComponent {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
type: ComponentType;
|
|
|
|
|
|
position: Position;
|
|
|
|
|
|
size: Size;
|
|
|
|
|
|
parentId?: string;
|
|
|
|
|
|
label?: string;
|
|
|
|
|
|
required?: boolean;
|
|
|
|
|
|
readonly?: boolean;
|
|
|
|
|
|
style?: ComponentStyle;
|
|
|
|
|
|
className?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 화면관리용 확장 스타일 (CommonStyle 기반)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface ComponentStyle extends CommonStyle {
|
|
|
|
|
|
// 화면관리 전용 스타일 확장 가능
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 위젯 컴포넌트 (입력 요소)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface WidgetComponent extends BaseComponent {
|
|
|
|
|
|
type: "widget";
|
|
|
|
|
|
widgetType: DynamicWebType;
|
|
|
|
|
|
placeholder?: string;
|
|
|
|
|
|
columnName?: string;
|
|
|
|
|
|
webTypeConfig?: WebTypeConfig;
|
|
|
|
|
|
validationRules?: ValidationRule[];
|
|
|
|
|
|
|
|
|
|
|
|
// 웹타입별 추가 설정
|
|
|
|
|
|
dateConfig?: DateTypeConfig;
|
|
|
|
|
|
numberConfig?: NumberTypeConfig;
|
|
|
|
|
|
selectConfig?: SelectTypeConfig;
|
|
|
|
|
|
textConfig?: TextTypeConfig;
|
|
|
|
|
|
fileConfig?: FileTypeConfig;
|
|
|
|
|
|
entityConfig?: EntityTypeConfig;
|
|
|
|
|
|
buttonConfig?: ButtonTypeConfig;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 컨테이너 컴포넌트 (레이아웃)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface ContainerComponent extends BaseComponent {
|
|
|
|
|
|
type: "container" | "row" | "column" | "area";
|
|
|
|
|
|
children?: string[]; // 자식 컴포넌트 ID 배열
|
|
|
|
|
|
layoutDirection?: "horizontal" | "vertical";
|
|
|
|
|
|
justifyContent?: "start" | "center" | "end" | "space-between" | "space-around";
|
|
|
|
|
|
alignItems?: "start" | "center" | "end" | "stretch";
|
|
|
|
|
|
gap?: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 그룹 컴포넌트 (논리적 그룹핑)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface GroupComponent extends BaseComponent {
|
|
|
|
|
|
type: "group";
|
|
|
|
|
|
groupName: string;
|
|
|
|
|
|
children: string[]; // 그룹에 속한 컴포넌트 ID 배열
|
|
|
|
|
|
isCollapsible?: boolean;
|
|
|
|
|
|
isCollapsed?: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 데이터 테이블 컴포넌트
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface DataTableComponent extends BaseComponent {
|
|
|
|
|
|
type: "datatable";
|
|
|
|
|
|
tableName?: string;
|
|
|
|
|
|
columns: DataTableColumn[];
|
|
|
|
|
|
pagination?: boolean;
|
|
|
|
|
|
pageSize?: number;
|
|
|
|
|
|
searchable?: boolean;
|
|
|
|
|
|
sortable?: boolean;
|
|
|
|
|
|
filters?: DataTableFilter[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 파일 업로드 컴포넌트
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface FileComponent extends BaseComponent {
|
|
|
|
|
|
type: "file";
|
|
|
|
|
|
fileConfig: FileTypeConfig;
|
|
|
|
|
|
uploadedFiles?: UploadedFile[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 통합 컴포넌트 데이터 타입
|
|
|
|
|
|
*/
|
|
|
|
|
|
export type ComponentData = WidgetComponent | ContainerComponent | GroupComponent | DataTableComponent | FileComponent;
|
|
|
|
|
|
|
|
|
|
|
|
// ===== 웹타입별 설정 인터페이스 =====
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 기본 웹타입 설정
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface WebTypeConfig {
|
|
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 날짜/시간 타입 설정
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface DateTypeConfig {
|
|
|
|
|
|
format: "YYYY-MM-DD" | "YYYY-MM-DD HH:mm" | "YYYY-MM-DD HH:mm:ss";
|
|
|
|
|
|
showTime: boolean;
|
|
|
|
|
|
minDate?: string;
|
|
|
|
|
|
maxDate?: string;
|
|
|
|
|
|
defaultValue?: string;
|
|
|
|
|
|
placeholder?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 숫자 타입 설정
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface NumberTypeConfig {
|
|
|
|
|
|
min?: number;
|
|
|
|
|
|
max?: number;
|
|
|
|
|
|
step?: number;
|
|
|
|
|
|
format?: "integer" | "decimal" | "currency" | "percentage";
|
|
|
|
|
|
decimalPlaces?: number;
|
|
|
|
|
|
thousandSeparator?: boolean;
|
|
|
|
|
|
placeholder?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 선택박스 타입 설정
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface SelectTypeConfig {
|
|
|
|
|
|
options: Array<{ label: string; value: string }>;
|
|
|
|
|
|
multiple?: boolean;
|
|
|
|
|
|
searchable?: boolean;
|
|
|
|
|
|
placeholder?: string;
|
|
|
|
|
|
allowCustomValue?: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 텍스트 타입 설정
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface TextTypeConfig {
|
|
|
|
|
|
minLength?: number;
|
|
|
|
|
|
maxLength?: number;
|
|
|
|
|
|
pattern?: string;
|
|
|
|
|
|
format?: "none" | "email" | "phone" | "url" | "korean" | "english";
|
|
|
|
|
|
placeholder?: string;
|
|
|
|
|
|
multiline?: boolean;
|
|
|
|
|
|
rows?: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 파일 타입 설정
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface FileTypeConfig {
|
|
|
|
|
|
accept?: string;
|
|
|
|
|
|
multiple?: boolean;
|
|
|
|
|
|
maxSize?: number; // bytes
|
|
|
|
|
|
maxFiles?: number;
|
|
|
|
|
|
preview?: boolean;
|
|
|
|
|
|
docType?: string;
|
|
|
|
|
|
companyCode?: CompanyCode;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 엔티티 타입 설정
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface EntityTypeConfig {
|
|
|
|
|
|
referenceTable: string;
|
|
|
|
|
|
referenceColumn: string;
|
2025-09-23 15:58:54 +09:00
|
|
|
|
displayColumns: string[]; // 여러 표시 컬럼을 배열로 변경
|
|
|
|
|
|
displayColumn?: string; // 하위 호환성을 위해 유지 (deprecated)
|
2025-09-19 18:43:55 +09:00
|
|
|
|
searchColumns?: string[];
|
|
|
|
|
|
filters?: Record<string, unknown>;
|
|
|
|
|
|
placeholder?: string;
|
2025-09-23 15:58:54 +09:00
|
|
|
|
displayFormat?: 'simple' | 'detailed' | 'custom'; // 표시 형식
|
|
|
|
|
|
separator?: string; // 여러 컬럼 표시 시 구분자 (기본: ' - ')
|
2025-09-19 18:43:55 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 버튼 타입 설정
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface ButtonTypeConfig {
|
|
|
|
|
|
text?: string;
|
|
|
|
|
|
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
|
|
|
|
|
|
size?: "sm" | "md" | "lg";
|
|
|
|
|
|
icon?: string;
|
|
|
|
|
|
// ButtonActionType과 관련된 설정은 control-management.ts에서 정의
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ===== 데이터 테이블 관련 =====
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 데이터 테이블 컬럼
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface DataTableColumn {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
columnName: string;
|
|
|
|
|
|
label: string;
|
|
|
|
|
|
dataType?: string;
|
|
|
|
|
|
widgetType?: DynamicWebType;
|
|
|
|
|
|
width?: number;
|
|
|
|
|
|
sortable?: boolean;
|
|
|
|
|
|
searchable?: boolean;
|
|
|
|
|
|
visible: boolean;
|
|
|
|
|
|
frozen?: boolean;
|
|
|
|
|
|
align?: "left" | "center" | "right";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 데이터 테이블 필터
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface DataTableFilter {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
columnName: string;
|
|
|
|
|
|
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN";
|
|
|
|
|
|
value: unknown;
|
|
|
|
|
|
logicalOperator?: "AND" | "OR";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ===== 파일 업로드 관련 =====
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 업로드된 파일 정보
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface UploadedFile {
|
|
|
|
|
|
objid: string;
|
|
|
|
|
|
realFileName: string;
|
|
|
|
|
|
savedFileName: string;
|
|
|
|
|
|
fileSize: number;
|
|
|
|
|
|
fileExt: string;
|
|
|
|
|
|
filePath: string;
|
|
|
|
|
|
docType?: string;
|
|
|
|
|
|
docTypeName?: string;
|
|
|
|
|
|
writer?: string;
|
|
|
|
|
|
regdate?: string;
|
|
|
|
|
|
status?: "uploading" | "completed" | "error";
|
|
|
|
|
|
companyCode?: CompanyCode;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ===== 화면 정의 관련 =====
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 화면 정의
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface ScreenDefinition {
|
|
|
|
|
|
screenId: number;
|
|
|
|
|
|
screenName: string;
|
|
|
|
|
|
screenCode: string;
|
|
|
|
|
|
tableName: string;
|
|
|
|
|
|
tableLabel?: string;
|
|
|
|
|
|
companyCode: CompanyCode;
|
|
|
|
|
|
description?: string;
|
|
|
|
|
|
isActive: ActiveStatus;
|
|
|
|
|
|
createdDate: Date;
|
|
|
|
|
|
updatedDate: Date;
|
|
|
|
|
|
createdBy?: string;
|
|
|
|
|
|
updatedBy?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 화면 생성 요청
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface CreateScreenRequest {
|
|
|
|
|
|
screenName: string;
|
|
|
|
|
|
screenCode?: string;
|
|
|
|
|
|
tableName: string;
|
|
|
|
|
|
tableLabel?: string;
|
|
|
|
|
|
companyCode: CompanyCode;
|
|
|
|
|
|
description?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 화면 수정 요청
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface UpdateScreenRequest {
|
|
|
|
|
|
screenName?: string;
|
|
|
|
|
|
screenCode?: string;
|
|
|
|
|
|
tableName?: string;
|
|
|
|
|
|
tableLabel?: string;
|
|
|
|
|
|
description?: string;
|
|
|
|
|
|
isActive?: 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;
|
|
|
|
|
|
selectedComponents: string[];
|
|
|
|
|
|
groupTarget?: string | null;
|
|
|
|
|
|
groupMode?: "create" | "add" | "remove" | "ungroup";
|
|
|
|
|
|
groupTitle?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 레이아웃 데이터
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface LayoutData {
|
|
|
|
|
|
screenId: number;
|
|
|
|
|
|
components: ComponentData[];
|
|
|
|
|
|
gridSettings?: GridSettings;
|
|
|
|
|
|
metadata?: LayoutMetadata;
|
|
|
|
|
|
screenResolution?: ScreenResolution;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 격자 설정
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface GridSettings {
|
|
|
|
|
|
enabled: boolean;
|
|
|
|
|
|
size: number;
|
|
|
|
|
|
color: string;
|
|
|
|
|
|
opacity: number;
|
|
|
|
|
|
snapToGrid: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 레이아웃 메타데이터
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface LayoutMetadata {
|
|
|
|
|
|
version: string;
|
|
|
|
|
|
lastModified: Date;
|
|
|
|
|
|
modifiedBy: string;
|
|
|
|
|
|
description?: string;
|
|
|
|
|
|
tags?: string[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ===== 템플릿 관련 =====
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 화면 템플릿
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface ScreenTemplate {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
description?: string;
|
|
|
|
|
|
category: string;
|
|
|
|
|
|
components: ComponentData[];
|
|
|
|
|
|
previewImage?: string;
|
|
|
|
|
|
isActive: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 템플릿 컴포넌트 (템플릿 패널에서 사용)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface TemplateComponent {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
description?: string;
|
|
|
|
|
|
icon?: string;
|
|
|
|
|
|
category: string;
|
|
|
|
|
|
defaultProps: Partial<ComponentData>;
|
|
|
|
|
|
children?: Array<{
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
defaultProps: 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// widgetType이 유효한 WebType인지 체크
|
|
|
|
|
|
if (!component.widgetType || !isWebType(component.widgetType)) {
|
|
|
|
|
|
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";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ===== 안전한 타입 캐스팅 유틸리티 =====
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 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;
|
|
|
|
|
|
};
|