ERP-node/frontend/types/screen-management.ts

796 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 🖥️ 화면관리 시스템 전용 타입 정의
*
* 화면 설계, 컴포넌트 관리, 레이아웃 등 화면관리 시스템에서만 사용하는 타입들
*/
import {
ComponentType,
WebType,
DynamicWebType,
Position,
Size,
CommonStyle,
ValidationRule,
TimestampFields,
CompanyCode,
ActiveStatus,
isWebType,
} from "./unified-core";
import { ColumnSpanPreset } from "@/lib/constants/columnSpans";
import { ResponsiveComponentConfig } from "./responsive";
// ===== 기본 컴포넌트 인터페이스 =====
/**
* 모든 컴포넌트의 기본 인터페이스
*/
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; // 웹타입별 설정
// 반응형 설정
responsiveConfig?: ResponsiveComponentConfig;
responsiveDisplay?: any; // 런타임에 추가되는 임시 필드
}
/**
* 화면관리용 확장 스타일 (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;
// 🆕 자동 입력 설정 (테이블 조회 기반)
autoFill?: {
enabled: boolean; // 자동 입력 활성화
sourceTable: string; // 조회할 테이블 (예: company_mng)
filterColumn: string; // 필터링할 컬럼 (예: company_code)
userField: 'companyCode' | 'userId' | 'deptCode'; // 사용자 정보 필드
displayColumn: string; // 표시할 컬럼 (예: company_name)
};
}
/**
* 컨테이너 컴포넌트 (레이아웃)
*/
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[];
// 🆕 현재 사용자 정보로 자동 필터링
autoFilter?: {
enabled: boolean; // 자동 필터 활성화 여부
filterColumn: string; // 필터링할 테이블 컬럼 (예: company_code, dept_code)
userField: 'companyCode' | 'userId' | 'deptCode'; // 사용자 정보에서 가져올 필드
};
}
/**
* 파일 업로드 컴포넌트
*/
export interface FileComponent extends BaseComponent {
type: "file";
fileConfig: FileTypeConfig;
uploadedFiles?: UploadedFile[];
columnName?: string;
tableName?: string;
lastFileUpdate?: number;
}
/**
* 플로우 스텝별 컬럼 표시 설정
*/
export interface FlowStepColumnConfig {
selectedColumns: string[]; // 표시할 컬럼 목록
columnOrder?: string[]; // 컬럼 순서 (선택사항)
}
/**
* 플로우 컴포넌트
*/
export interface FlowComponent extends BaseComponent {
type: "flow";
flowId?: number; // 선택된 플로우 ID
flowName?: string; // 플로우 이름 (표시용)
showStepCount?: boolean; // 각 스텝의 데이터 건수 표시 여부
allowDataMove?: boolean; // 데이터 이동 허용 여부
displayMode?: "horizontal" | "vertical"; // 플로우 표시 방향
// 🆕 단계별 컬럼 오버라이드 설정 (화면관리에서 설정)
stepColumnConfig?: {
[stepId: number]: FlowStepColumnConfig;
};
}
/**
* 새로운 컴포넌트 시스템 컴포넌트
*/
export interface ComponentComponent extends BaseComponent {
type: "component";
widgetType: WebType; // 웹타입 (기존 호환성)
componentType: string; // 새 컴포넌트 시스템의 ID
componentConfig: any; // 컴포넌트별 설정
}
/**
* 통합 컴포넌트 데이터 타입
*/
export type ComponentData =
| WidgetComponent
| ContainerComponent
| GroupComponent
| DataTableComponent
| FileComponent
| FlowComponent
| 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 FlowVisibilityConfig {
/**
* 플로우 단계별 표시 제어 활성화 여부
*/
enabled: boolean;
/**
* 대상 플로우 컴포넌트 ID
* 화면에 여러 플로우 위젯이 있을 경우, 어떤 플로우에 반응할지 지정
*/
targetFlowComponentId: string;
/**
* 대상 플로우 정의 ID (선택사항, 검증용)
*/
targetFlowId?: number;
/**
* 대상 플로우 이름 (표시용)
*/
targetFlowName?: string;
/**
* 표시 조건 모드
* - whitelist: visibleSteps에 포함된 단계에서만 표시
* - blacklist: hiddenSteps에 포함된 단계에서 숨김
* - all: 모든 단계에서 표시 (기본값)
*/
mode: "whitelist" | "blacklist" | "all";
/**
* 표시할 단계 ID 목록 (mode="whitelist"일 때 사용)
*/
visibleSteps?: number[];
/**
* 숨길 단계 ID 목록 (mode="blacklist"일 때 사용)
*/
hiddenSteps?: number[];
/**
* 레이아웃 동작 방식
* - preserve-position: 원래 위치 유지 (display: none, 빈 공간 유지)
* - auto-compact: 빈 공간 자동 제거 (Flexbox 그룹으로 자동 정렬)
*/
layoutBehavior: "preserve-position" | "auto-compact";
/**
* 그룹 ID (auto-compact 모드일 때 사용)
* 같은 그룹 ID를 가진 버튼들이 하나의 FlowButtonGroup으로 묶임
*/
groupId?: string;
/**
* 그룹 정렬 방향 (auto-compact 모드일 때 사용)
*/
groupDirection?: "horizontal" | "vertical";
/**
* 그룹 내 버튼 간격 (px, auto-compact 모드일 때 사용)
*/
groupGap?: number;
/**
* 그룹 정렬 방식 (auto-compact 모드일 때 사용)
*/
groupAlign?: "start" | "center" | "end" | "space-between" | "space-around";
}
// ===== 데이터 테이블 관련 =====
/**
* 데이터 테이블 컬럼
*/
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;
dbSourceType?: "internal" | "external";
dbConnectionId?: number;
}
/**
* 화면 생성 요청
*/
export interface CreateScreenRequest {
screenName: string;
screenCode?: string;
tableName: string;
tableLabel?: string;
companyCode: CompanyCode;
description?: string;
dbSourceType?: "internal" | "external";
dbConnectionId?: number;
}
/**
* 화면 수정 요청
*/
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";
};
/**
* FlowComponent 타입 가드
*/
export const isFlowComponent = (component: ComponentData): component is FlowComponent => {
return component.type === "flow";
};
// ===== 안전한 타입 캐스팅 유틸리티 =====
/**
* 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;
};