655 lines
16 KiB
TypeScript
655 lines
16 KiB
TypeScript
/**
|
||
* 🖥️ 화면관리 시스템 전용 타입 정의
|
||
*
|
||
* 화면 설계, 컴포넌트 관리, 레이아웃 등 화면관리 시스템에서만 사용하는 타입들
|
||
*/
|
||
|
||
import {
|
||
ComponentType,
|
||
WebType,
|
||
DynamicWebType,
|
||
Position,
|
||
Size,
|
||
CommonStyle,
|
||
ValidationRule,
|
||
TimestampFields,
|
||
CompanyCode,
|
||
ActiveStatus,
|
||
isWebType,
|
||
} from "./unified-core";
|
||
import { ColumnSpanPreset } from "@/lib/constants/columnSpans";
|
||
|
||
// ===== 기본 컴포넌트 인터페이스 =====
|
||
|
||
/**
|
||
* 모든 컴포넌트의 기본 인터페이스
|
||
*/
|
||
export interface BaseComponent {
|
||
id: string;
|
||
type: ComponentType;
|
||
|
||
// 🔄 레거시 위치/크기 (단계적 제거 예정)
|
||
position: Position; // y 좌표는 유지 (행 정렬용)
|
||
size: Size; // height만 사용
|
||
|
||
// 🆕 그리드 시스템 속성
|
||
gridColumnSpan?: ColumnSpanPreset; // 컬럼 너비
|
||
gridColumnStart?: number; // 시작 컬럼 (1-12)
|
||
gridRowIndex?: number; // 행 인덱스
|
||
|
||
parentId?: string;
|
||
label?: string;
|
||
required?: boolean;
|
||
readonly?: boolean;
|
||
style?: ComponentStyle;
|
||
className?: string;
|
||
|
||
// 새 컴포넌트 시스템에서 필요한 속성들
|
||
gridColumns?: number; // 🔄 deprecated - gridColumnSpan 사용
|
||
zoneId?: string; // 레이아웃 존 ID
|
||
componentConfig?: any; // 컴포넌트별 설정
|
||
componentType?: string; // 새 컴포넌트 시스템의 ID
|
||
webTypeConfig?: WebTypeConfig; // 웹타입별 설정
|
||
}
|
||
|
||
/**
|
||
* 화면관리용 확장 스타일 (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;
|
||
arrayConfig?: ArrayTypeConfig;
|
||
}
|
||
|
||
/**
|
||
* 컨테이너 컴포넌트 (레이아웃)
|
||
*/
|
||
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[];
|
||
columnName?: string;
|
||
tableName?: string;
|
||
lastFileUpdate?: number;
|
||
}
|
||
|
||
/**
|
||
* 새로운 컴포넌트 시스템 컴포넌트
|
||
*/
|
||
export interface ComponentComponent extends BaseComponent {
|
||
type: "component";
|
||
widgetType: WebType; // 웹타입 (기존 호환성)
|
||
componentType: string; // 새 컴포넌트 시스템의 ID
|
||
componentConfig: any; // 컴포넌트별 설정
|
||
}
|
||
|
||
/**
|
||
* 통합 컴포넌트 데이터 타입
|
||
*/
|
||
export type ComponentData =
|
||
| WidgetComponent
|
||
| ContainerComponent
|
||
| GroupComponent
|
||
| DataTableComponent
|
||
| FileComponent
|
||
| ComponentComponent;
|
||
|
||
// ===== 웹타입별 설정 인터페이스 =====
|
||
|
||
/**
|
||
* 기본 웹타입 설정
|
||
*/
|
||
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 ArrayTypeConfig {
|
||
itemType?: "text" | "number" | "email" | "tel"; // 각 항목의 입력 타입
|
||
minItems?: number; // 최소 항목 수
|
||
maxItems?: number; // 최대 항목 수
|
||
placeholder?: string; // 입력 필드 placeholder
|
||
addButtonText?: string; // + 버튼 텍스트
|
||
removeButtonText?: string; // - 버튼 텍스트 (보통 아이콘)
|
||
allowReorder?: boolean; // 순서 변경 가능 여부
|
||
showIndex?: boolean; // 인덱스 번호 표시 여부
|
||
}
|
||
|
||
/**
|
||
* 파일 타입 설정
|
||
*/
|
||
export interface FileTypeConfig {
|
||
accept?: string[];
|
||
multiple?: boolean;
|
||
maxSize?: number; // MB
|
||
maxFiles?: number;
|
||
showPreview?: boolean;
|
||
showProgress?: boolean;
|
||
docType?: string;
|
||
docTypeName?: string;
|
||
dragDropText?: string;
|
||
uploadButtonText?: string;
|
||
autoUpload?: boolean;
|
||
chunkedUpload?: boolean;
|
||
linkedTable?: string;
|
||
linkedField?: string;
|
||
autoLink?: boolean;
|
||
companyCode?: CompanyCode;
|
||
}
|
||
|
||
/**
|
||
* 엔티티 타입 설정
|
||
*/
|
||
export interface EntityTypeConfig {
|
||
referenceTable: string;
|
||
referenceColumn: string;
|
||
displayColumns: string[]; // 여러 표시 컬럼을 배열로 변경
|
||
displayColumn?: string; // 하위 호환성을 위해 유지 (deprecated)
|
||
searchColumns?: string[];
|
||
filters?: Record<string, unknown>;
|
||
placeholder?: string;
|
||
displayFormat?: "simple" | "detailed" | "custom"; // 표시 형식
|
||
separator?: string; // 여러 컬럼 표시 시 구분자 (기본: ' - ')
|
||
}
|
||
|
||
/**
|
||
* 버튼 타입 설정
|
||
*/
|
||
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;
|
||
targetObjid: string;
|
||
parentTargetObjid?: 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;
|
||
// gridUtils에서 필요한 속성들 추가
|
||
columns: number;
|
||
gap: number;
|
||
padding: number;
|
||
showGrid?: boolean;
|
||
gridColor?: string;
|
||
gridOpacity?: number;
|
||
}
|
||
|
||
/**
|
||
* 레이아웃 메타데이터
|
||
*/
|
||
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;
|
||
};
|