349 lines
9.0 KiB
TypeScript
349 lines
9.0 KiB
TypeScript
/**
|
|
* 메타 컴포넌트 V3 API 클라이언트
|
|
* - 절대 fetch() 직접 사용 금지! apiClient 사용 필수
|
|
*/
|
|
|
|
import { apiClient, ApiResponse } from "./client";
|
|
|
|
// ============================================
|
|
// TypeScript 인터페이스 (백엔드 응답과 동일)
|
|
// ============================================
|
|
|
|
export interface FieldConfig {
|
|
webType: string;
|
|
label: string;
|
|
placeholder?: string;
|
|
defaultValue?: any;
|
|
required: boolean;
|
|
maxLength?: number;
|
|
validation?: {
|
|
pattern?: string;
|
|
min?: number;
|
|
max?: number;
|
|
message?: string;
|
|
};
|
|
options?: { value: string; label: string }[];
|
|
join?: {
|
|
referenceTable: string;
|
|
referenceColumn: string;
|
|
displayColumn: string;
|
|
};
|
|
}
|
|
|
|
export interface TableColumn {
|
|
columnName: string;
|
|
webType: string;
|
|
label: string;
|
|
required: boolean;
|
|
maxLength?: number;
|
|
join?: {
|
|
referenceTable: string;
|
|
referenceColumn: string;
|
|
displayColumn: string;
|
|
};
|
|
}
|
|
|
|
export interface LayoutData {
|
|
version: "3.0";
|
|
screenId: number;
|
|
layerId: number;
|
|
components: MetaComponent[];
|
|
layers?: Layer[];
|
|
metadata?: {
|
|
lastModified?: string;
|
|
description?: string;
|
|
};
|
|
}
|
|
|
|
export interface MetaComponent {
|
|
id: string;
|
|
type: "meta-field" | "meta-dataview" | "meta-action" | "meta-layout" | "meta-display" | "meta-search" | "meta-modal";
|
|
position?: { x: number; y: number; width: number; height: number };
|
|
config:
|
|
| FieldComponentConfig
|
|
| DataViewComponentConfig
|
|
| ActionComponentConfig
|
|
| LayoutComponentConfig
|
|
| DisplayComponentConfig
|
|
| SearchComponentConfig
|
|
| ModalComponentConfig;
|
|
}
|
|
|
|
export interface FieldComponentConfig {
|
|
webType: string;
|
|
label: string;
|
|
binding: string; // 바인딩될 컬럼명
|
|
tableName?: string;
|
|
placeholder?: string;
|
|
required?: boolean;
|
|
defaultValue?: any;
|
|
validation?: FieldConfig["validation"];
|
|
join?: FieldConfig["join"];
|
|
options?: FieldConfig["options"];
|
|
}
|
|
|
|
export interface DataViewComponentConfig {
|
|
viewMode: "table" | "card" | "list" | "tree";
|
|
tableName: string;
|
|
columns: string[];
|
|
defaultSort?: { column: string; direction: "asc" | "desc" };
|
|
pageSize?: number;
|
|
actions?: {
|
|
create?: boolean;
|
|
read?: boolean;
|
|
update?: boolean;
|
|
delete?: boolean;
|
|
};
|
|
}
|
|
|
|
export interface ActionComponentConfig {
|
|
label: string;
|
|
buttonType: "primary" | "secondary" | "destructive" | "ghost";
|
|
icon?: string;
|
|
steps: ActionStep[];
|
|
confirmDialog?: {
|
|
title: string;
|
|
message: string;
|
|
};
|
|
}
|
|
|
|
export type ActionStep =
|
|
| { type: "save"; target: string }
|
|
| { type: "delete"; target: string }
|
|
| { type: "refresh"; target: string }
|
|
| { type: "navigate"; screenId: number }
|
|
| { type: "openModal"; modalId: string }
|
|
| { type: "api"; method: string; endpoint: string; body?: any };
|
|
|
|
// ============================================
|
|
// 새로운 메타 컴포넌트 타입 (Phase B)
|
|
// ============================================
|
|
|
|
export interface LayoutComponentConfig {
|
|
mode: "columns" | "rows" | "tabs" | "accordion" | "card";
|
|
areas?: { id: string; label?: string; size?: string; collapsible?: boolean }[];
|
|
tabs?: { id: string; label: string; icon?: string }[];
|
|
gap?: number;
|
|
padding?: number;
|
|
bordered?: boolean;
|
|
title?: string;
|
|
}
|
|
|
|
export interface DisplayComponentConfig {
|
|
displayType: "text" | "heading" | "divider" | "badge" | "alert" | "stat" | "spacer" | "progress";
|
|
text?: {
|
|
content: string;
|
|
size?: "xs" | "sm" | "md" | "lg" | "xl";
|
|
weight?: "normal" | "medium" | "semibold" | "bold";
|
|
align?: "left" | "center" | "right";
|
|
};
|
|
heading?: {
|
|
content: string;
|
|
level: 1 | 2 | 3 | 4 | 5 | 6;
|
|
};
|
|
badge?: {
|
|
text: string;
|
|
variant?: "default" | "secondary" | "destructive" | "outline";
|
|
};
|
|
alert?: {
|
|
title?: string;
|
|
message: string;
|
|
variant?: "default" | "destructive";
|
|
};
|
|
stat?: {
|
|
value: string;
|
|
label: string;
|
|
change?: string;
|
|
changeType?: "increase" | "decrease" | "neutral";
|
|
};
|
|
progress?: {
|
|
value: number;
|
|
max?: number;
|
|
label?: string;
|
|
};
|
|
spacer?: {
|
|
height: number;
|
|
};
|
|
dataBinding?: {
|
|
tableName?: string;
|
|
columnName?: string;
|
|
expression?: string;
|
|
};
|
|
}
|
|
|
|
export interface SearchComponentConfig {
|
|
targetDataView: string;
|
|
mode: "simple" | "advanced" | "combined";
|
|
fields?: {
|
|
columnName: string;
|
|
label: string;
|
|
searchType: "text" | "exact" | "range" | "select" | "date_range";
|
|
options?: { value: string; label: string }[];
|
|
}[];
|
|
quickFilters?: {
|
|
label: string;
|
|
filter: { column: string; operator: string; value: any };
|
|
}[];
|
|
}
|
|
|
|
export interface ModalComponentConfig {
|
|
trigger: "button" | "row_click" | "row_double_click" | "action";
|
|
triggerLabel?: string;
|
|
content: {
|
|
type: "form" | "screen" | "custom";
|
|
formConfig?: {
|
|
tableName: string;
|
|
mode: "create" | "edit" | "view";
|
|
columns: string[];
|
|
layout?: "single" | "two_column";
|
|
};
|
|
screenId?: number;
|
|
};
|
|
size?: "sm" | "md" | "lg" | "xl" | "full";
|
|
onClose?: ActionStep[];
|
|
}
|
|
|
|
export interface Layer {
|
|
id: number;
|
|
name: string;
|
|
visible: boolean;
|
|
order: number;
|
|
}
|
|
|
|
export interface ReactiveBinding {
|
|
id?: number;
|
|
sourceComponentId: string;
|
|
sourceEvent: "change" | "select" | "click" | "load";
|
|
sourceField?: string;
|
|
targetComponentId: string;
|
|
targetAction: "filter" | "setValue" | "show" | "hide" | "enable" | "disable" | "refresh";
|
|
targetField?: string;
|
|
transformConfig?: any;
|
|
conditionConfig?: any;
|
|
priority?: number;
|
|
}
|
|
|
|
// ============================================
|
|
// API 함수들
|
|
// ============================================
|
|
|
|
/**
|
|
* GET /api/meta/field-config/:tableName/:columnName
|
|
* - 컬럼의 webType 기반 Field 설정 자동 생성
|
|
*/
|
|
export async function getFieldConfig(
|
|
tableName: string,
|
|
columnName: string
|
|
): Promise<ApiResponse<FieldConfig>> {
|
|
try {
|
|
const response = await apiClient.get(`/meta/field-config/${tableName}/${columnName}`);
|
|
return response.data;
|
|
} catch (error: any) {
|
|
return {
|
|
success: false,
|
|
message: error.response?.data?.message || error.message || "Field 설정을 가져올 수 없습니다",
|
|
errorCode: error.response?.data?.errorCode,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/meta/table-columns/:tableName
|
|
* - 테이블 컬럼 목록 + webType + FK 관계 자동 감지
|
|
*/
|
|
export async function getTableColumns(tableName: string): Promise<ApiResponse<TableColumn[]>> {
|
|
try {
|
|
const response = await apiClient.get(`/meta/table-columns/${tableName}`);
|
|
return response.data;
|
|
} catch (error: any) {
|
|
return {
|
|
success: false,
|
|
message: error.response?.data?.message || error.message || "테이블 컬럼 목록을 가져올 수 없습니다",
|
|
errorCode: error.response?.data?.errorCode,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/meta/layout/save
|
|
* - 메타 컴포넌트 레이아웃 저장 (version "3.0", screen_layouts_v3 테이블)
|
|
*/
|
|
export async function saveLayout(layoutData: LayoutData): Promise<ApiResponse<void>> {
|
|
try {
|
|
const response = await apiClient.post("/meta/layout/save", layoutData);
|
|
return response.data;
|
|
} catch (error: any) {
|
|
return {
|
|
success: false,
|
|
message: error.response?.data?.message || error.message || "레이아웃 저장에 실패했습니다",
|
|
errorCode: error.response?.data?.errorCode,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/meta/layout/:screenId
|
|
* - 메타 컴포넌트 레이아웃 로드 (company_code 필터링 자동)
|
|
*/
|
|
export async function getLayout(screenId: number, layerId: number = 1): Promise<ApiResponse<LayoutData>> {
|
|
try {
|
|
const response = await apiClient.get(`/meta/layout/${screenId}`, {
|
|
params: { layerId },
|
|
});
|
|
return response.data;
|
|
} catch (error: any) {
|
|
return {
|
|
success: false,
|
|
message: error.response?.data?.message || error.message || "레이아웃을 가져올 수 없습니다",
|
|
errorCode: error.response?.data?.errorCode,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/meta/bindings/save
|
|
* - Reactive Bindings 저장
|
|
*/
|
|
export async function saveBindings(
|
|
screenId: number,
|
|
layoutV3Id: number | null,
|
|
bindings: ReactiveBinding[]
|
|
): Promise<ApiResponse<void>> {
|
|
try {
|
|
const response = await apiClient.post("/meta/bindings/save", {
|
|
screenId,
|
|
layoutV3Id,
|
|
bindings,
|
|
});
|
|
return response.data;
|
|
} catch (error: any) {
|
|
return {
|
|
success: false,
|
|
message: error.response?.data?.message || error.message || "바인딩 저장에 실패했습니다",
|
|
errorCode: error.response?.data?.errorCode,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/meta/bindings/:screenId
|
|
* - Reactive Bindings 조회 (priority ASC 정렬)
|
|
*/
|
|
export async function getBindings(
|
|
screenId: number,
|
|
layoutV3Id: number | null = null
|
|
): Promise<ApiResponse<ReactiveBinding[]>> {
|
|
try {
|
|
const response = await apiClient.get(`/meta/bindings/${screenId}`, {
|
|
params: { layoutV3Id },
|
|
});
|
|
return response.data;
|
|
} catch (error: any) {
|
|
return {
|
|
success: false,
|
|
message: error.response?.data?.message || error.message || "바인딩을 가져올 수 없습니다",
|
|
errorCode: error.response?.data?.errorCode,
|
|
};
|
|
}
|
|
}
|