화면관리 테이블 타입관리 연계

This commit is contained in:
kjs
2025-09-01 14:00:31 +09:00
parent 42dbfd98f8
commit ca56cff114
9 changed files with 1330 additions and 1434 deletions

View File

@@ -16,6 +16,13 @@ import {
} from "../types/screen";
import { generateId } from "../utils/generateId";
// 백엔드에서 사용할 테이블 정보 타입
interface TableInfo {
tableName: string;
tableLabel: string;
columns: ColumnInfo[];
}
export class ScreenManagementService {
// ========================================
// 화면 정의 관리
@@ -83,6 +90,21 @@ export class ScreenManagementService {
};
}
/**
* 화면 목록 조회 (간단 버전)
*/
async getScreens(companyCode: string): Promise<ScreenDefinition[]> {
const whereClause =
companyCode === "*" ? {} : { company_code: companyCode };
const screens = await prisma.screen_definitions.findMany({
where: whereClause,
orderBy: { created_date: "desc" },
});
return screens.map((screen) => this.mapToScreenDefinition(screen));
}
/**
* 화면 정의 조회
*/
@@ -102,56 +124,225 @@ export class ScreenManagementService {
updateData: UpdateScreenRequest,
userCompanyCode: string
): Promise<ScreenDefinition> {
// 권한 검증
const screen = await prisma.screen_definitions.findUnique({
// 권한 확인
const existingScreen = await prisma.screen_definitions.findUnique({
where: { screen_id: screenId },
});
if (!screen) {
if (!existingScreen) {
throw new Error("화면을 찾을 수 없습니다.");
}
if (userCompanyCode !== "*" && userCompanyCode !== screen.company_code) {
throw new Error("해당 화면을 수정할 권한이 없습니다.");
if (
userCompanyCode !== "*" &&
existingScreen.company_code !== userCompanyCode
) {
throw new Error("이 화면을 수정할 권한이 없습니다.");
}
const updatedScreen = await prisma.screen_definitions.update({
const screen = await prisma.screen_definitions.update({
where: { screen_id: screenId },
data: {
screen_name: updateData.screenName,
description: updateData.description,
is_active: updateData.isActive,
is_active: updateData.isActive ? "Y" : "N",
updated_by: updateData.updatedBy,
updated_date: new Date(),
},
});
return this.mapToScreenDefinition(updatedScreen);
return this.mapToScreenDefinition(screen);
}
/**
* 화면 정의 삭제
*/
async deleteScreen(screenId: number, userCompanyCode: string): Promise<void> {
// 권한 검증
const screen = await prisma.screen_definitions.findUnique({
// 권한 확인
const existingScreen = await prisma.screen_definitions.findUnique({
where: { screen_id: screenId },
});
if (!screen) {
if (!existingScreen) {
throw new Error("화면을 찾을 수 없습니다.");
}
if (userCompanyCode !== "*" && userCompanyCode !== screen.company_code) {
throw new Error("해당 화면을 삭제할 권한이 없습니다.");
if (
userCompanyCode !== "*" &&
existingScreen.company_code !== userCompanyCode
) {
throw new Error("이 화면을 삭제할 권한이 없습니다.");
}
// CASCADE로 인해 관련 레이아웃과 위젯도 자동 삭제됨
await prisma.screen_definitions.delete({
where: { screen_id: screenId },
});
}
// ========================================
// 테이블 관리
// ========================================
/**
* 테이블 목록 조회
*/
async getTables(companyCode: string): Promise<TableInfo[]> {
try {
// PostgreSQL에서 사용 가능한 테이블 목록 조회
const tables = await prisma.$queryRaw<Array<{ table_name: string }>>`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
ORDER BY table_name
`;
// 각 테이블의 컬럼 정보도 함께 조회
const tableInfos: TableInfo[] = [];
for (const table of tables) {
const columns = await this.getTableColumns(
table.table_name,
companyCode
);
if (columns.length > 0) {
tableInfos.push({
tableName: table.table_name,
tableLabel: this.getTableLabel(table.table_name),
columns: columns,
});
}
}
return tableInfos;
} catch (error) {
console.error("테이블 목록 조회 실패:", error);
throw new Error("테이블 목록을 조회할 수 없습니다.");
}
}
/**
* 테이블 컬럼 정보 조회
*/
async getTableColumns(
tableName: string,
companyCode: string
): Promise<ColumnInfo[]> {
try {
// 테이블 컬럼 정보 조회
const columns = await prisma.$queryRaw<
Array<{
column_name: string;
data_type: string;
is_nullable: string;
column_default: string | null;
character_maximum_length: number | null;
numeric_precision: number | null;
numeric_scale: number | null;
}>
>`
SELECT
column_name,
data_type,
is_nullable,
column_default,
character_maximum_length,
numeric_precision,
numeric_scale
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = ${tableName}
ORDER BY ordinal_position
`;
// column_labels 테이블에서 웹타입 정보 조회 (있는 경우)
const webTypeInfo = await prisma.column_labels.findMany({
where: { table_name: tableName },
select: {
column_name: true,
web_type: true,
column_label: true,
detail_settings: true,
},
});
// 컬럼 정보 매핑
return columns.map((column) => {
const webTypeData = webTypeInfo.find(
(wt) => wt.column_name === column.column_name
);
return {
tableName: tableName,
columnName: column.column_name,
columnLabel:
webTypeData?.column_label ||
this.getColumnLabel(column.column_name),
dataType: column.data_type,
webType:
(webTypeData?.web_type as WebType) ||
this.inferWebType(column.data_type),
isNullable: column.is_nullable,
columnDefault: column.column_default || undefined,
characterMaximumLength: column.character_maximum_length || undefined,
numericPrecision: column.numeric_precision || undefined,
numericScale: column.numeric_scale || undefined,
detailSettings: webTypeData?.detail_settings || undefined,
};
});
} catch (error) {
console.error("테이블 컬럼 조회 실패:", error);
throw new Error("테이블 컬럼 정보를 조회할 수 없습니다.");
}
}
/**
* 테이블 라벨 생성
*/
private getTableLabel(tableName: string): string {
// snake_case를 읽기 쉬운 형태로 변환
return tableName
.replace(/_/g, " ")
.replace(/\b\w/g, (l) => l.toUpperCase())
.replace(/\s+/g, " ")
.trim();
}
/**
* 컬럼 라벨 생성
*/
private getColumnLabel(columnName: string): string {
// snake_case를 읽기 쉬운 형태로 변환
return columnName
.replace(/_/g, " ")
.replace(/\b\w/g, (l) => l.toUpperCase())
.replace(/\s+/g, " ")
.trim();
}
/**
* 데이터 타입으로부터 웹타입 추론
*/
private inferWebType(dataType: string): WebType {
const lowerType = dataType.toLowerCase();
if (lowerType.includes("char") || lowerType.includes("text")) {
return "text";
} else if (
lowerType.includes("int") ||
lowerType.includes("numeric") ||
lowerType.includes("decimal")
) {
return "number";
} else if (lowerType.includes("date") || lowerType.includes("time")) {
return "date";
} else if (lowerType.includes("bool")) {
return "checkbox";
} else {
return "text";
}
}
// ========================================
// 레이아웃 관리
// ========================================
@@ -161,109 +352,107 @@ export class ScreenManagementService {
*/
async saveLayout(
screenId: number,
layoutData: SaveLayoutRequest
layoutData: LayoutData,
companyCode: string
): Promise<void> {
// 화면 존재 확인
const screen = await prisma.screen_definitions.findUnique({
// 권한 확인
const existingScreen = await prisma.screen_definitions.findUnique({
where: { screen_id: screenId },
});
if (!screen) {
if (!existingScreen) {
throw new Error("화면을 찾을 수 없습니다.");
}
if (companyCode !== "*" && existingScreen.company_code !== companyCode) {
throw new Error("이 화면의 레이아웃을 저장할 권한이 없습니다.");
}
// 기존 레이아웃 삭제
await prisma.screen_layouts.deleteMany({
where: { screen_id: screenId },
});
// 새 레이아웃 저장
const layoutPromises = layoutData.components.map((component) =>
prisma.screen_layouts.create({
for (const component of layoutData.components) {
const { id, ...componentData } = component;
// Prisma JSON 필드에 맞는 타입으로 변환
const properties: any = {
...componentData,
position: {
x: component.position.x,
y: component.position.y,
},
size: {
width: component.size.width,
height: component.size.height,
},
};
await prisma.screen_layouts.create({
data: {
screen_id: screenId,
component_type: component.type,
component_id: component.id,
parent_id: component.parentId,
parent_id: component.parentId || null,
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,
properties: properties,
},
})
);
await Promise.all(layoutPromises);
});
}
}
/**
* 레이아웃 조회
*/
async getLayout(screenId: number): Promise<LayoutData | null> {
async getLayout(
screenId: number,
companyCode: string
): Promise<LayoutData | null> {
// 권한 확인
const existingScreen = await prisma.screen_definitions.findUnique({
where: { screen_id: screenId },
});
if (!existingScreen) {
return null;
}
if (companyCode !== "*" && existingScreen.company_code !== companyCode) {
throw new Error("이 화면의 레이아웃을 조회할 권한이 없습니다.");
}
const layouts = await prisma.screen_layouts.findMany({
where: { screen_id: screenId },
orderBy: { display_order: "asc" },
});
if (layouts.length === 0) {
return null;
return {
components: [],
gridSettings: { columns: 12, gap: 16, padding: 16 },
};
}
const components: ComponentData[] = layouts.map((layout) => {
const baseComponent = {
const properties = layout.properties as any;
return {
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,
parentId: layout.parent_id,
...properties,
};
// 컴포넌트 타입별 추가 속성 처리
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,
},
gridSettings: { columns: 12, gap: 16, padding: 16 },
};
}
@@ -616,3 +805,6 @@ export class ScreenManagementService {
};
}
}
// 서비스 인스턴스 export
export const screenManagementService = new ScreenManagementService();