ERP-node/backend-node/src/services/screenManagementService.ts

619 lines
17 KiB
TypeScript
Raw Normal View History

2025-09-01 11:48:12 +09:00
import prisma from "../config/database";
import {
ScreenDefinition,
CreateScreenRequest,
UpdateScreenRequest,
LayoutData,
SaveLayoutRequest,
ScreenTemplate,
MenuAssignmentRequest,
PaginatedResponse,
ComponentData,
ColumnInfo,
ColumnWebTypeSetting,
WebType,
WidgetData,
} from "../types/screen";
import { generateId } from "../utils/generateId";
export class ScreenManagementService {
// ========================================
// 화면 정의 관리
// ========================================
/**
*
*/
async createScreen(
screenData: CreateScreenRequest,
userCompanyCode: string
): Promise<ScreenDefinition> {
// 화면 코드 중복 확인
const existingScreen = await prisma.screen_definitions.findUnique({
where: { screen_code: screenData.screenCode },
});
if (existingScreen) {
throw new Error("이미 존재하는 화면 코드입니다.");
}
const screen = await prisma.screen_definitions.create({
data: {
screen_name: screenData.screenName,
screen_code: screenData.screenCode,
table_name: screenData.tableName,
company_code: screenData.companyCode,
description: screenData.description,
created_by: screenData.createdBy,
},
});
return this.mapToScreenDefinition(screen);
}
/**
* ( )
*/
async getScreensByCompany(
companyCode: string,
page: number = 1,
size: number = 20
): Promise<PaginatedResponse<ScreenDefinition>> {
const whereClause =
companyCode === "*" ? {} : { company_code: companyCode };
const [screens, total] = await Promise.all([
prisma.screen_definitions.findMany({
where: whereClause,
skip: (page - 1) * size,
take: size,
orderBy: { created_date: "desc" },
}),
prisma.screen_definitions.count({ where: whereClause }),
]);
return {
data: screens.map((screen) => this.mapToScreenDefinition(screen)),
pagination: {
page,
size,
total,
totalPages: Math.ceil(total / size),
},
};
}
/**
*
*/
async getScreenById(screenId: number): Promise<ScreenDefinition | null> {
const screen = await prisma.screen_definitions.findUnique({
where: { screen_id: screenId },
});
return screen ? this.mapToScreenDefinition(screen) : null;
}
/**
*
*/
async updateScreen(
screenId: number,
updateData: UpdateScreenRequest,
userCompanyCode: string
): Promise<ScreenDefinition> {
// 권한 검증
const screen = await prisma.screen_definitions.findUnique({
where: { screen_id: screenId },
});
if (!screen) {
throw new Error("화면을 찾을 수 없습니다.");
}
if (userCompanyCode !== "*" && userCompanyCode !== screen.company_code) {
throw new Error("해당 화면을 수정할 권한이 없습니다.");
}
const updatedScreen = await prisma.screen_definitions.update({
where: { screen_id: screenId },
data: {
screen_name: updateData.screenName,
description: updateData.description,
is_active: updateData.isActive,
updated_by: updateData.updatedBy,
updated_date: new Date(),
},
});
return this.mapToScreenDefinition(updatedScreen);
}
/**
*
*/
async deleteScreen(screenId: number, userCompanyCode: string): Promise<void> {
// 권한 검증
const screen = await prisma.screen_definitions.findUnique({
where: { screen_id: screenId },
});
if (!screen) {
throw new Error("화면을 찾을 수 없습니다.");
}
if (userCompanyCode !== "*" && userCompanyCode !== screen.company_code) {
throw new Error("해당 화면을 삭제할 권한이 없습니다.");
}
// CASCADE로 인해 관련 레이아웃과 위젯도 자동 삭제됨
await prisma.screen_definitions.delete({
where: { screen_id: screenId },
});
}
// ========================================
// 레이아웃 관리
// ========================================
/**
*
*/
async saveLayout(
screenId: number,
layoutData: SaveLayoutRequest
): Promise<void> {
// 화면 존재 확인
const screen = await prisma.screen_definitions.findUnique({
where: { screen_id: screenId },
});
if (!screen) {
throw new Error("화면을 찾을 수 없습니다.");
}
// 기존 레이아웃 삭제
await prisma.screen_layouts.deleteMany({
where: { screen_id: screenId },
});
// 새 레이아웃 저장
const layoutPromises = layoutData.components.map((component) =>
prisma.screen_layouts.create({
data: {
screen_id: screenId,
component_type: component.type,
component_id: component.id,
parent_id: component.parentId,
position_x: component.position.x,
position_y: component.position.y,
width: component.size.width,
height: component.size.height,
properties: component.properties,
display_order: component.displayOrder || 0,
},
})
);
await Promise.all(layoutPromises);
}
/**
*
*/
async getLayout(screenId: number): Promise<LayoutData | null> {
const layouts = await prisma.screen_layouts.findMany({
where: { screen_id: screenId },
orderBy: { display_order: "asc" },
});
if (layouts.length === 0) {
return null;
}
const components: ComponentData[] = layouts.map((layout) => {
const baseComponent = {
id: layout.component_id,
type: layout.component_type as any,
position: { x: layout.position_x, y: layout.position_y },
size: { width: layout.width, height: layout.height },
properties: layout.properties as Record<string, any>,
displayOrder: layout.display_order,
};
// 컴포넌트 타입별 추가 속성 처리
switch (layout.component_type) {
case "group":
return {
...baseComponent,
type: "group",
title: (layout.properties as any)?.title,
backgroundColor: (layout.properties as any)?.backgroundColor,
border: (layout.properties as any)?.border,
borderRadius: (layout.properties as any)?.borderRadius,
shadow: (layout.properties as any)?.shadow,
padding: (layout.properties as any)?.padding,
margin: (layout.properties as any)?.margin,
collapsible: (layout.properties as any)?.collapsible,
collapsed: (layout.properties as any)?.collapsed,
children: (layout.properties as any)?.children || [],
};
case "widget":
return {
...baseComponent,
type: "widget",
tableName: (layout.properties as any)?.tableName,
columnName: (layout.properties as any)?.columnName,
widgetType: (layout.properties as any)?.widgetType,
label: (layout.properties as any)?.label,
placeholder: (layout.properties as any)?.placeholder,
required: (layout.properties as any)?.required,
readonly: (layout.properties as any)?.readonly,
validationRules: (layout.properties as any)?.validationRules,
displayProperties: (layout.properties as any)?.displayProperties,
};
default:
return baseComponent;
}
});
return {
components,
gridSettings: {
columns: 12,
gap: 16,
padding: 16,
},
};
}
// ========================================
// 템플릿 관리
// ========================================
/**
* 릿 ()
*/
async getTemplatesByCompany(
companyCode: string,
type?: string,
isPublic?: boolean
): Promise<ScreenTemplate[]> {
const whereClause: any = {};
if (companyCode !== "*") {
whereClause.company_code = companyCode;
}
if (type) {
whereClause.template_type = type;
}
if (isPublic !== undefined) {
whereClause.is_public = isPublic;
}
const templates = await prisma.screen_templates.findMany({
where: whereClause,
orderBy: { created_date: "desc" },
});
return templates.map(this.mapToScreenTemplate);
}
/**
* 릿
*/
async createTemplate(
templateData: Partial<ScreenTemplate>
): Promise<ScreenTemplate> {
const template = await prisma.screen_templates.create({
data: {
template_name: templateData.templateName!,
template_type: templateData.templateType!,
company_code: templateData.companyCode!,
description: templateData.description,
layout_data: templateData.layoutData
? JSON.parse(JSON.stringify(templateData.layoutData))
: null,
is_public: templateData.isPublic || false,
created_by: templateData.createdBy,
},
});
return this.mapToScreenTemplate(template);
}
// ========================================
// 메뉴 할당 관리
// ========================================
/**
* -
*/
async assignScreenToMenu(
screenId: number,
assignmentData: MenuAssignmentRequest
): Promise<void> {
// 중복 할당 방지
const existingAssignment = await prisma.screen_menu_assignments.findFirst({
where: {
screen_id: screenId,
menu_objid: assignmentData.menuObjid,
company_code: assignmentData.companyCode,
},
});
if (existingAssignment) {
throw new Error("이미 할당된 화면입니다.");
}
await prisma.screen_menu_assignments.create({
data: {
screen_id: screenId,
menu_objid: assignmentData.menuObjid,
company_code: assignmentData.companyCode,
display_order: assignmentData.displayOrder || 0,
created_by: assignmentData.createdBy,
},
});
}
/**
*
*/
async getScreensByMenu(
menuObjid: number,
companyCode: string
): Promise<ScreenDefinition[]> {
const assignments = await prisma.screen_menu_assignments.findMany({
where: {
menu_objid: menuObjid,
company_code: companyCode,
is_active: "Y",
},
include: {
screen: true,
},
orderBy: { display_order: "asc" },
});
return assignments.map((assignment) =>
this.mapToScreenDefinition(assignment.screen)
);
}
// ========================================
// 테이블 타입 연계
// ========================================
/**
* ( )
*/
async getColumnInfo(tableName: string): Promise<ColumnInfo[]> {
const columns = await prisma.$queryRaw`
SELECT
c.column_name,
COALESCE(cl.column_label, c.column_name) as column_label,
c.data_type,
COALESCE(cl.web_type, 'text') as web_type,
c.is_nullable,
c.column_default,
c.character_maximum_length,
c.numeric_precision,
c.numeric_scale,
cl.detail_settings,
cl.code_category,
cl.reference_table,
cl.reference_column,
cl.is_visible,
cl.display_order,
cl.description
FROM information_schema.columns c
LEFT JOIN column_labels cl ON c.table_name = cl.table_name
AND c.column_name = cl.column_name
WHERE c.table_name = ${tableName}
ORDER BY COALESCE(cl.display_order, c.ordinal_position)
`;
return columns as ColumnInfo[];
}
/**
*
*/
async setColumnWebType(
tableName: string,
columnName: string,
webType: WebType,
additionalSettings?: Partial<ColumnWebTypeSetting>
): Promise<void> {
await prisma.column_labels.upsert({
where: {
table_name_column_name: {
table_name: tableName,
column_name: columnName,
},
},
update: {
web_type: webType,
column_label: additionalSettings?.columnLabel,
detail_settings: additionalSettings?.detailSettings
? JSON.stringify(additionalSettings.detailSettings)
: null,
code_category: additionalSettings?.codeCategory,
reference_table: additionalSettings?.referenceTable,
reference_column: additionalSettings?.referenceColumn,
is_visible: additionalSettings?.isVisible ?? true,
display_order: additionalSettings?.displayOrder ?? 0,
description: additionalSettings?.description,
updated_date: new Date(),
},
create: {
table_name: tableName,
column_name: columnName,
column_label: additionalSettings?.columnLabel,
web_type: webType,
detail_settings: additionalSettings?.detailSettings
? JSON.stringify(additionalSettings.detailSettings)
: null,
code_category: additionalSettings?.codeCategory,
reference_table: additionalSettings?.referenceTable,
reference_column: additionalSettings?.referenceColumn,
is_visible: additionalSettings?.isVisible ?? true,
display_order: additionalSettings?.displayOrder ?? 0,
description: additionalSettings?.description,
created_date: new Date(),
},
});
}
/**
*
*/
generateWidgetFromColumn(column: ColumnInfo): WidgetData {
const baseWidget = {
id: generateId(),
tableName: column.tableName,
columnName: column.columnName,
type: column.webType || "text",
label: column.columnLabel || column.columnName,
required: column.isNullable === "N",
readonly: false,
};
// detail_settings JSON 파싱
const detailSettings = column.detailSettings
? JSON.parse(column.detailSettings)
: {};
switch (column.webType) {
case "text":
return {
...baseWidget,
maxLength: detailSettings.maxLength || column.characterMaximumLength,
placeholder: `Enter ${column.columnLabel || column.columnName}`,
pattern: detailSettings.pattern,
};
case "number":
return {
...baseWidget,
min: detailSettings.min,
max:
detailSettings.max ||
(column.numericPrecision
? Math.pow(10, column.numericPrecision) - 1
: undefined),
step:
detailSettings.step ||
(column.numericScale && column.numericScale > 0
? Math.pow(10, -column.numericScale)
: 1),
};
case "date":
return {
...baseWidget,
format: detailSettings.format || "YYYY-MM-DD",
minDate: detailSettings.minDate,
maxDate: detailSettings.maxDate,
};
case "code":
return {
...baseWidget,
codeCategory: column.codeCategory,
multiple: detailSettings.multiple || false,
searchable: detailSettings.searchable || false,
};
case "entity":
return {
...baseWidget,
referenceTable: column.referenceTable,
referenceColumn: column.referenceColumn,
searchable: detailSettings.searchable || true,
multiple: detailSettings.multiple || false,
};
case "textarea":
return {
...baseWidget,
rows: detailSettings.rows || 3,
maxLength: detailSettings.maxLength || column.characterMaximumLength,
};
case "select":
return {
...baseWidget,
options: detailSettings.options || [],
multiple: detailSettings.multiple || false,
searchable: detailSettings.searchable || false,
};
case "checkbox":
return {
...baseWidget,
defaultChecked: detailSettings.defaultChecked || false,
label: detailSettings.label || column.columnLabel,
};
case "radio":
return {
...baseWidget,
options: detailSettings.options || [],
inline: detailSettings.inline || false,
};
case "file":
return {
...baseWidget,
accept: detailSettings.accept || "*/*",
maxSize: detailSettings.maxSize || 10485760, // 10MB
multiple: detailSettings.multiple || false,
};
default:
return {
...baseWidget,
type: "text",
};
}
}
// ========================================
// 유틸리티 메서드
// ========================================
private mapToScreenDefinition(data: any): ScreenDefinition {
return {
screenId: data.screen_id,
screenName: data.screen_name,
screenCode: data.screen_code,
tableName: data.table_name,
companyCode: data.company_code,
description: data.description,
isActive: data.is_active,
createdDate: data.created_date,
createdBy: data.created_by,
updatedDate: data.updated_date,
updatedBy: data.updated_by,
};
}
private mapToScreenTemplate(data: any): ScreenTemplate {
return {
templateId: data.template_id,
templateName: data.template_name,
templateType: data.template_type,
companyCode: data.company_code,
description: data.description,
layoutData: data.layout_data,
isPublic: data.is_public,
createdBy: data.created_by,
createdDate: data.created_date,
};
}
}