Phase 2 ScreenManagementService 전환 완료

This commit is contained in:
kjs
2025-09-30 17:19:05 +09:00
parent 4637680de0
commit 1a640850c5
2 changed files with 369 additions and 281 deletions

View File

@@ -127,16 +127,19 @@ export class ScreenManagementService {
const total = parseInt(totalResult[0]?.count || "0", 10);
// 테이블 라벨 정보를 한 번에 조회 (Raw Query)
const tableNames = [
...new Set(screens.map((s: any) => s.table_name).filter(Boolean)),
];
const tableNames = Array.from(
new Set(screens.map((s: any) => s.table_name).filter(Boolean))
);
let tableLabelMap = new Map<string, string>();
if (tableNames.length > 0) {
try {
const placeholders = tableNames.map((_, i) => `$${i + 1}`).join(", ");
const tableLabels = await query<{ table_name: string; table_label: string | null }>(
const tableLabels = await query<{
table_name: string;
table_label: string | null;
}>(
`SELECT table_name, table_label FROM table_labels
WHERE table_name IN (${placeholders})`,
tableNames
@@ -339,7 +342,9 @@ export class ScreenManagementService {
const params: any[] = [];
if (userCompanyCode !== "*") {
whereConditions.push(`sd.company_code IN ($${params.length + 1}, $${params.length + 2})`);
whereConditions.push(
`sd.company_code IN ($${params.length + 1}, $${params.length + 2})`
);
params.push(userCompanyCode, "*");
}
@@ -371,13 +376,9 @@ export class ScreenManagementService {
if (screen.screen_id === screenId) continue; // 자기 자신은 제외
try {
// screen_layouts 테이블에서 버튼 컴포넌트 확인
const buttonLayouts = screen.layouts.filter(
(layout) => layout.component_type === "widget"
);
for (const layout of buttonLayouts) {
const properties = layout.properties as any;
// screen_layouts 테이블에서 버튼 컴포넌트 확인 (위젯 타입만)
if (screen.component_type === "widget") {
const properties = screen.properties as any;
// 버튼 컴포넌트인지 확인
if (properties?.widgetType === "button") {
@@ -393,7 +394,7 @@ export class ScreenManagementService {
screenId: screen.screen_id,
screenName: screen.screen_name,
screenCode: screen.screen_code,
componentId: layout.component_id,
componentId: screen.component_id,
componentType: "button",
referenceType: "popup",
});
@@ -408,7 +409,7 @@ export class ScreenManagementService {
screenId: screen.screen_id,
screenName: screen.screen_name,
screenCode: screen.screen_code,
componentId: layout.component_id,
componentId: screen.component_id,
componentType: "button",
referenceType: "navigate",
});
@@ -423,7 +424,7 @@ export class ScreenManagementService {
screenId: screen.screen_id,
screenName: screen.screen_name,
screenCode: screen.screen_code,
componentId: layout.component_id,
componentId: screen.component_id,
componentType: "button",
referenceType: "url",
});
@@ -431,67 +432,8 @@ export class ScreenManagementService {
}
}
// 기존 layout_metadata도 확인 (하위 호환성)
const layoutMetadata = screen.layout_metadata as any;
if (layoutMetadata?.components) {
const components = layoutMetadata.components;
for (const component of components) {
// 버튼 컴포넌트인지 확인
if (
component.type === "widget" &&
component.widgetType === "button"
) {
const config = component.webTypeConfig;
if (!config) continue;
// popup 액션에서 targetScreenId 확인
if (
config.actionType === "popup" &&
config.targetScreenId === screenId
) {
dependencies.push({
screenId: screen.screen_id,
screenName: screen.screen_name,
screenCode: screen.screen_code,
componentId: component.id,
componentType: "button",
referenceType: "popup",
});
}
// navigate 액션에서 targetScreenId 확인
if (
config.actionType === "navigate" &&
config.targetScreenId === screenId
) {
dependencies.push({
screenId: screen.screen_id,
screenName: screen.screen_name,
screenCode: screen.screen_code,
componentId: component.id,
componentType: "button",
referenceType: "navigate",
});
}
// navigateUrl에서 화면 ID 패턴 확인 (예: /screens/123)
if (
config.navigateUrl &&
config.navigateUrl.includes(`/screens/${screenId}`)
) {
dependencies.push({
screenId: screen.screen_id,
screenName: screen.screen_name,
screenCode: screen.screen_code,
componentId: component.id,
componentType: "button",
referenceType: "url",
});
}
}
}
}
// 기존 layout_metadata도 확인 (하위 호환성) - 현재는 사용하지 않음
// 실제 데이터는 screen_layouts 테이블에서 개별적으로 조회해야 함
} catch (error) {
console.error(
`화면 ${screen.screen_id}의 레이아웃 분석 중 오류:`,
@@ -501,31 +443,35 @@ export class ScreenManagementService {
}
}
// 메뉴 할당 확인
// 메뉴에 할당된 화면인지 확인 (임시 주석 처리)
/*
const menuAssignments = await prisma.screen_menu_assignments.findMany({
where: {
screen_id: screenId,
is_active: "Y",
},
include: {
menu_info: true, // 메뉴 정보도 함께 조회
},
});
// 메뉴 할당 확인 (Raw Query)
try {
const menuAssignments = await query<{
assignment_id: number;
menu_objid: number;
menu_name_kor?: string;
}>(
`SELECT sma.assignment_id, sma.menu_objid, mi.menu_name_kor
FROM screen_menu_assignments sma
LEFT JOIN menu_info mi ON sma.menu_objid = mi.objid
WHERE sma.screen_id = $1 AND sma.is_active = 'Y'`,
[screenId]
);
// 메뉴에 할당된 경우 의존성에 추가
for (const assignment of menuAssignments) {
dependencies.push({
screenId: 0, // 메뉴는 화면이 아니므로 0으로 설정
screenName: assignment.menu_info?.menu_name_kor || "알 수 없는 메뉴",
screenCode: `MENU_${assignment.menu_objid}`,
componentId: `menu_${assignment.assignment_id}`,
componentType: "menu",
referenceType: "menu_assignment",
});
// 메뉴에 할당된 경우 의존성에 추가
for (const assignment of menuAssignments) {
dependencies.push({
screenId: 0, // 메뉴는 화면이 아니므로 0으로 설정
screenName: assignment.menu_name_kor || "알 수 없는 메뉴",
screenCode: `MENU_${assignment.menu_objid}`,
componentId: `menu_${assignment.assignment_id}`,
componentType: "menu",
referenceType: "menu_assignment",
});
}
} catch (error) {
console.error("메뉴 할당 확인 중 오류:", error);
// 메뉴 할당 확인 실패해도 다른 의존성 체크는 계속 진행
}
*/
return {
hasDependencies: dependencies.length > 0,
@@ -544,7 +490,10 @@ export class ScreenManagementService {
force: boolean = false
): Promise<void> {
// 권한 확인 (Raw Query)
const existingResult = await query<{ company_code: string | null; is_active: string }>(
const existingResult = await query<{
company_code: string | null;
is_active: string;
}>(
`SELECT company_code, is_active FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
[screenId]
);
@@ -593,7 +542,14 @@ export class ScreenManagementService {
updated_date = $4,
updated_by = $5
WHERE screen_id = $6`,
[new Date(), deletedBy, deleteReason || null, new Date(), deletedBy, screenId]
[
new Date(),
deletedBy,
deleteReason || null,
new Date(),
deletedBy,
screenId,
]
);
// 메뉴 할당도 비활성화
@@ -607,7 +563,7 @@ export class ScreenManagementService {
}
/**
* 화면 복원 (휴지통에서 복원)
* 화면 복원 (휴지통에서 복원) (✅ Raw Query 전환 완료)
*/
async restoreScreen(
screenId: number,
@@ -615,14 +571,21 @@ export class ScreenManagementService {
restoredBy: string
): Promise<void> {
// 권한 확인
const existingScreen = await prisma.screen_definitions.findUnique({
where: { screen_id: screenId },
});
const screens = await query<{
company_code: string | null;
is_active: string;
screen_code: string;
}>(
`SELECT company_code, is_active, screen_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
[screenId]
);
if (!existingScreen) {
if (screens.length === 0) {
throw new Error("화면을 찾을 수 없습니다.");
}
const existingScreen = screens[0];
if (
userCompanyCode !== "*" &&
existingScreen.company_code !== userCompanyCode
@@ -704,7 +667,10 @@ export class ScreenManagementService {
userCompanyCode: string
): Promise<void> {
// 권한 확인
const screens = await query<{ company_code: string | null; is_active: string }>(
const screens = await query<{
company_code: string | null;
is_active: string;
}>(
`SELECT company_code, is_active FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
[screenId]
);
@@ -729,9 +695,17 @@ export class ScreenManagementService {
// 물리적 삭제 (수동으로 관련 데이터 삭제)
await transaction(async (client) => {
await client.query(`DELETE FROM screen_layouts WHERE screen_id = $1`, [screenId]);
await client.query(`DELETE FROM screen_menu_assignments WHERE screen_id = $1`, [screenId]);
await client.query(`DELETE FROM screen_definitions WHERE screen_id = $1`, [screenId]);
await client.query(`DELETE FROM screen_layouts WHERE screen_id = $1`, [
screenId,
]);
await client.query(
`DELETE FROM screen_menu_assignments WHERE screen_id = $1`,
[screenId]
);
await client.query(
`DELETE FROM screen_definitions WHERE screen_id = $1`,
[screenId]
);
});
}
@@ -779,21 +753,27 @@ export class ScreenManagementService {
const total = parseInt(totalResult[0]?.count || "0", 10);
// 테이블 라벨 정보를 한 번에 조회
const tableNames = [
...new Set(screens.map((s: any) => s.table_name).filter(Boolean)),
];
const tableNames = Array.from(
new Set(screens.map((s: any) => s.table_name).filter(Boolean))
);
let tableLabelMap = new Map<string, string>();
if (tableNames.length > 0) {
const placeholders = tableNames.map((_, i) => `$${i + 1}`).join(", ");
const tableLabels = await query<{ table_name: string; table_label: string | null }>(
const tableLabels = await query<{
table_name: string;
table_label: string | null;
}>(
`SELECT table_name, table_label FROM table_labels WHERE table_name IN (${placeholders})`,
tableNames
);
tableLabelMap = new Map(
tableLabels.map((tl: any) => [tl.table_name, tl.table_label || tl.table_name])
tableLabels.map((tl: any) => [
tl.table_name,
tl.table_label || tl.table_name,
])
);
}
@@ -918,18 +898,19 @@ export class ScreenManagementService {
// ========================================
/**
* 테이블 목록 조회 (모든 테이블)
* 테이블 목록 조회 (모든 테이블) (✅ Raw Query 전환 완료)
*/
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 tables = await query<{ 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[] = [];
@@ -966,13 +947,14 @@ export class ScreenManagementService {
console.log(`=== 단일 테이블 조회 시작: ${tableName} ===`);
// 테이블 존재 여부 확인
const tableExists = await prisma.$queryRaw<Array<{ table_name: string }>>`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
AND table_name = ${tableName}
`;
const tableExists = await query<{ table_name: string }>(
`SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
AND table_name = $1`,
[tableName]
);
if (tableExists.length === 0) {
console.log(`테이블 ${tableName}이 존재하지 않습니다.`);
@@ -1004,7 +986,7 @@ export class ScreenManagementService {
}
/**
* 테이블 컬럼 정보 조회
* 테이블 컬럼 정보 조회 (✅ Raw Query 전환 완료)
*/
async getTableColumns(
tableName: string,
@@ -1012,18 +994,16 @@ export class ScreenManagementService {
): 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
const columns = await query<{
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,
@@ -1031,25 +1011,28 @@ export class ScreenManagementService {
character_maximum_length,
numeric_precision,
numeric_scale
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = ${tableName}
ORDER BY ordinal_position
`;
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = $1
ORDER BY ordinal_position`,
[tableName]
);
// 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,
},
});
const webTypeInfo = await query<{
column_name: string;
web_type: string | null;
column_label: string | null;
detail_settings: any;
}>(
`SELECT column_name, web_type, column_label, detail_settings
FROM column_labels
WHERE table_name = $1`,
[tableName]
);
// 컬럼 정보 매핑
return columns.map((column) => {
return columns.map((column: any) => {
const webTypeData = webTypeInfo.find(
(wt) => wt.column_name === column.column_name
);
@@ -1189,10 +1172,7 @@ export class ScreenManagementService {
}
// 기존 레이아웃 삭제 (컴포넌트와 메타데이터 모두)
await query(
`DELETE FROM screen_layouts WHERE screen_id = $1`,
[screenId]
);
await query(`DELETE FROM screen_layouts WHERE screen_id = $1`, [screenId]);
// 1. 메타데이터 저장 (격자 설정과 해상도 정보)
if (layoutData.gridSettings || layoutData.screenResolution) {
@@ -1260,10 +1240,10 @@ export class ScreenManagementService {
component.type,
component.id,
component.parentId || null,
component.position.x,
component.position.y,
component.size.width,
component.size.height,
Math.round(component.position.x), // 정수로 반올림
Math.round(component.position.y), // 정수로 반올림
Math.round(component.size.width), // 정수로 반올림
Math.round(component.size.height), // 정수로 반올림
JSON.stringify(properties),
]
);
@@ -1414,7 +1394,10 @@ export class ScreenManagementService {
params.push(isPublic);
}
const whereSQL = whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : "";
const whereSQL =
whereConditions.length > 0
? `WHERE ${whereConditions.join(" AND ")}`
: "";
const templates = await query<any>(
`SELECT * FROM screen_templates
@@ -1531,11 +1514,11 @@ export class ScreenManagementService {
// ========================================
/**
* 컬럼 정보 조회 (웹 타입 포함)
* 컬럼 정보 조회 (웹 타입 포함) (✅ Raw Query 전환 완료)
*/
async getColumnInfo(tableName: string): Promise<ColumnInfo[]> {
const columns = await prisma.$queryRaw`
SELECT
const columns = await query<any>(
`SELECT
c.column_name,
COALESCE(cl.column_label, c.column_name) as column_label,
c.data_type,
@@ -1553,18 +1536,19 @@ export class ScreenManagementService {
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)
`;
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 = $1
ORDER BY COALESCE(cl.display_order, c.ordinal_position)`,
[tableName]
);
return columns as ColumnInfo[];
}
/**
* 웹 타입 설정
* 웹 타입 설정 (✅ Raw Query 전환 완료)
*/
async setColumnWebType(
tableName: string,
@@ -1572,44 +1556,45 @@ export class ScreenManagementService {
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
// UPSERT를 INSERT ... ON CONFLICT로 변환
await query(
`INSERT INTO column_labels (
table_name, column_name, column_label, web_type, detail_settings,
code_category, reference_table, reference_column, display_column,
is_visible, display_order, description, created_date, updated_date
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
ON CONFLICT (table_name, column_name)
DO UPDATE SET
web_type = $4,
column_label = $3,
detail_settings = $5,
code_category = $6,
reference_table = $7,
reference_column = $8,
display_column = $9,
is_visible = $10,
display_order = $11,
description = $12,
updated_date = $14`,
[
tableName,
columnName,
additionalSettings?.columnLabel || null,
webType,
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(),
},
});
additionalSettings?.codeCategory || null,
additionalSettings?.referenceTable || null,
additionalSettings?.referenceColumn || null,
(additionalSettings as any)?.displayColumn || null,
additionalSettings?.isVisible ?? true,
additionalSettings?.displayOrder ?? 0,
additionalSettings?.description || null,
new Date(),
new Date(),
]
);
}
/**
@@ -1767,20 +1752,16 @@ export class ScreenManagementService {
}
/**
* 화면 코드 자동 생성 (회사코드 + '_' + 순번)
* 화면 코드 자동 생성 (회사코드 + '_' + 순번) (✅ Raw Query 전환 완료)
*/
async generateScreenCode(companyCode: string): Promise<string> {
// 해당 회사의 기존 화면 코드들 조회
const existingScreens = await prisma.screen_definitions.findMany({
where: {
company_code: companyCode,
screen_code: {
startsWith: companyCode,
},
},
select: { screen_code: true },
orderBy: { screen_code: "desc" },
});
// 해당 회사의 기존 화면 코드들 조회 (Raw Query)
const existingScreens = await query<{ screen_code: string }>(
`SELECT screen_code FROM screen_definitions
WHERE company_code = $1 AND screen_code LIKE $2
ORDER BY screen_code DESC`,
[companyCode, `${companyCode}%`]
);
// 회사 코드 뒤의 숫자 부분 추출하여 최대값 찾기
let maxNumber = 0;
@@ -1902,11 +1883,11 @@ export class ScreenManagementService {
sourceLayout.component_type,
newComponentId,
newParentId,
sourceLayout.position_x,
sourceLayout.position_y,
sourceLayout.width,
sourceLayout.height,
typeof sourceLayout.properties === 'string'
Math.round(sourceLayout.position_x), // 정수로 반올림
Math.round(sourceLayout.position_y), // 정수로 반올림
Math.round(sourceLayout.width), // 정수로 반올림
Math.round(sourceLayout.height), // 정수로 반올림
typeof sourceLayout.properties === "string"
? sourceLayout.properties
: JSON.stringify(sourceLayout.properties),
sourceLayout.display_order,