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

1924 lines
56 KiB
TypeScript
Raw Normal View History

// ✅ Prisma → Raw Query 전환 (Phase 2.1)
import { query, transaction } from "../database/db";
2025-09-01 11:48:12 +09:00
import {
ScreenDefinition,
CreateScreenRequest,
UpdateScreenRequest,
LayoutData,
SaveLayoutRequest,
ScreenTemplate,
MenuAssignmentRequest,
PaginatedResponse,
ComponentData,
ColumnInfo,
ColumnWebTypeSetting,
WebType,
WidgetData,
} from "../types/screen";
2025-09-03 18:23:47 +09:00
2025-09-01 11:48:12 +09:00
import { generateId } from "../utils/generateId";
2025-09-03 18:23:47 +09:00
// 화면 복사 요청 인터페이스
interface CopyScreenRequest {
screenName: string;
screenCode: string;
description?: string;
companyCode: string;
createdBy: string;
}
// 백엔드에서 사용할 테이블 정보 타입
interface TableInfo {
tableName: string;
tableLabel: string;
columns: ColumnInfo[];
}
2025-09-01 11:48:12 +09:00
export class ScreenManagementService {
// ========================================
// 화면 정의 관리
// ========================================
/**
* ( Raw Query )
2025-09-01 11:48:12 +09:00
*/
async createScreen(
screenData: CreateScreenRequest,
userCompanyCode: string
): Promise<ScreenDefinition> {
2025-09-04 17:01:07 +09:00
console.log(`=== 화면 생성 요청 ===`);
console.log(`요청 데이터:`, screenData);
console.log(`사용자 회사 코드:`, userCompanyCode);
// 화면 코드 중복 확인 (Raw Query)
const existingResult = await query<{ screen_id: number }>(
`SELECT screen_id FROM screen_definitions
WHERE screen_code = $1 AND is_active != 'D'
LIMIT 1`,
[screenData.screenCode]
);
2025-09-01 11:48:12 +09:00
2025-09-04 17:01:07 +09:00
console.log(
`화면 코드 '${screenData.screenCode}' 중복 검사 결과:`,
existingResult.length > 0 ? "중복됨" : "사용 가능"
2025-09-04 17:01:07 +09:00
);
if (existingResult.length > 0) {
console.log(`기존 화면 정보:`, existingResult[0]);
2025-09-01 11:48:12 +09:00
throw new Error("이미 존재하는 화면 코드입니다.");
}
// 화면 생성 (Raw Query)
const [screen] = await query<any>(
`INSERT INTO screen_definitions (
screen_name, screen_code, table_name, company_code, description, created_by
) VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *`,
[
screenData.screenName,
screenData.screenCode,
screenData.tableName,
screenData.companyCode,
screenData.description || null,
screenData.createdBy,
]
);
2025-09-01 11:48:12 +09:00
return this.mapToScreenDefinition(screen);
}
/**
* ( ) - ( Raw Query )
2025-09-01 11:48:12 +09:00
*/
async getScreensByCompany(
companyCode: string,
page: number = 1,
size: number = 20
): Promise<PaginatedResponse<ScreenDefinition>> {
const offset = (page - 1) * size;
// WHERE 절 동적 생성
const whereConditions: string[] = ["is_active != 'D'"];
const params: any[] = [];
2025-09-08 13:10:09 +09:00
if (companyCode !== "*") {
whereConditions.push(`company_code = $${params.length + 1}`);
params.push(companyCode);
2025-09-08 13:10:09 +09:00
}
2025-09-01 11:48:12 +09:00
const whereSQL = whereConditions.join(" AND ");
// 페이징 쿼리 (Raw Query)
const [screens, totalResult] = await Promise.all([
query<any>(
`SELECT * FROM screen_definitions
WHERE ${whereSQL}
ORDER BY created_date DESC
LIMIT $${params.length + 1} OFFSET $${params.length + 2}`,
[...params, size, offset]
),
query<{ count: string }>(
`SELECT COUNT(*)::text as count FROM screen_definitions
WHERE ${whereSQL}`,
params
),
2025-09-01 11:48:12 +09:00
]);
const total = parseInt(totalResult[0]?.count || "0", 10);
// 테이블 라벨 정보를 한 번에 조회 (Raw Query)
const tableNames = Array.from(
new Set(screens.map((s: any) => s.table_name).filter(Boolean))
);
2025-09-08 14:20:01 +09:00
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;
}>(
`SELECT table_name, table_label FROM table_labels
WHERE table_name IN (${placeholders})`,
tableNames
);
2025-09-08 14:20:01 +09:00
tableLabelMap = new Map(
tableLabels.map((tl) => [
tl.table_name,
tl.table_label || tl.table_name,
])
);
// 테스트: company_mng 라벨 직접 확인
if (tableLabelMap.has("company_mng")) {
console.log(
"✅ company_mng 라벨 찾음:",
tableLabelMap.get("company_mng")
);
} else {
console.log("❌ company_mng 라벨 없음");
}
} catch (error) {
console.error("테이블 라벨 조회 오류:", error);
}
}
2025-09-01 11:48:12 +09:00
return {
2025-09-08 14:20:01 +09:00
data: screens.map((screen) =>
this.mapToScreenDefinition(screen, tableLabelMap)
),
2025-09-01 11:48:12 +09:00
pagination: {
page,
size,
total,
totalPages: Math.ceil(total / size),
},
};
}
/**
* ( ) - ( Raw Query )
*/
async getScreens(companyCode: string): Promise<ScreenDefinition[]> {
// 동적 WHERE 절 생성
const whereConditions: string[] = ["is_active != 'D'"];
const params: any[] = [];
2025-09-08 13:10:09 +09:00
if (companyCode !== "*") {
whereConditions.push(`company_code = $${params.length + 1}`);
params.push(companyCode);
2025-09-08 13:10:09 +09:00
}
const whereSQL = whereConditions.join(" AND ");
const screens = await query<any>(
`SELECT * FROM screen_definitions
WHERE ${whereSQL}
ORDER BY created_date DESC`,
params
);
return screens.map((screen) => this.mapToScreenDefinition(screen));
}
2025-09-01 11:48:12 +09:00
/**
* ( ) ( Raw Query )
2025-09-01 11:48:12 +09:00
*/
async getScreenById(screenId: number): Promise<ScreenDefinition | null> {
const screens = await query<any>(
`SELECT * FROM screen_definitions
WHERE screen_id = $1 AND is_active != 'D'
LIMIT 1`,
[screenId]
);
2025-09-01 11:48:12 +09:00
return screens.length > 0 ? this.mapToScreenDefinition(screens[0]) : null;
2025-09-01 11:48:12 +09:00
}
2025-09-01 18:42:59 +09:00
/**
* ( , ) ( Raw Query )
2025-09-01 18:42:59 +09:00
*/
async getScreen(
screenId: number,
companyCode: string
): Promise<ScreenDefinition | null> {
// 동적 WHERE 절 생성
const whereConditions: string[] = [
"screen_id = $1",
"is_active != 'D'", // 삭제된 화면 제외
];
const params: any[] = [screenId];
2025-09-01 18:42:59 +09:00
// 회사 코드가 '*'가 아닌 경우 회사별 필터링
if (companyCode !== "*") {
whereConditions.push(`company_code = $${params.length + 1}`);
params.push(companyCode);
2025-09-01 18:42:59 +09:00
}
const whereSQL = whereConditions.join(" AND ");
const screens = await query<any>(
`SELECT * FROM screen_definitions
WHERE ${whereSQL}
LIMIT 1`,
params
);
2025-09-01 18:42:59 +09:00
return screens.length > 0 ? this.mapToScreenDefinition(screens[0]) : null;
2025-09-01 18:42:59 +09:00
}
2025-09-01 11:48:12 +09:00
/**
* ( Raw Query )
2025-09-01 11:48:12 +09:00
*/
async updateScreen(
screenId: number,
updateData: UpdateScreenRequest,
userCompanyCode: string
): Promise<ScreenDefinition> {
// 권한 확인 (Raw Query)
const existingResult = await query<{ company_code: string | null }>(
`SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
[screenId]
);
2025-09-01 11:48:12 +09:00
if (existingResult.length === 0) {
2025-09-01 11:48:12 +09:00
throw new Error("화면을 찾을 수 없습니다.");
}
const existingScreen = existingResult[0];
if (
userCompanyCode !== "*" &&
existingScreen.company_code !== userCompanyCode
) {
throw new Error("이 화면을 수정할 권한이 없습니다.");
2025-09-01 11:48:12 +09:00
}
// 화면 업데이트 (Raw Query)
const [screen] = await query<any>(
`UPDATE screen_definitions
SET screen_name = $1,
description = $2,
is_active = $3,
updated_by = $4,
updated_date = $5
WHERE screen_id = $6
RETURNING *`,
[
updateData.screenName,
updateData.description || null,
updateData.isActive ? "Y" : "N",
updateData.updatedBy,
new Date(),
screenId,
]
);
2025-09-01 11:48:12 +09:00
return this.mapToScreenDefinition(screen);
2025-09-01 11:48:12 +09:00
}
/**
2025-09-08 13:10:09 +09:00
* -
*/
async checkScreenDependencies(
screenId: number,
userCompanyCode: string
): Promise<{
hasDependencies: boolean;
dependencies: Array<{
screenId: number;
screenName: string;
screenCode: string;
componentId: string;
componentType: string;
referenceType: string; // 'popup', 'navigate', 'targetScreen' 등
}>;
}> {
// 권한 확인
const targetScreens = await query<{ company_code: string | null }>(
`SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
[screenId]
);
2025-09-08 13:10:09 +09:00
if (targetScreens.length === 0) {
2025-09-08 13:10:09 +09:00
throw new Error("화면을 찾을 수 없습니다.");
}
const targetScreen = targetScreens[0];
2025-09-08 13:10:09 +09:00
if (
userCompanyCode !== "*" &&
targetScreen.company_code !== "*" &&
targetScreen.company_code !== userCompanyCode
) {
throw new Error("이 화면에 접근할 권한이 없습니다.");
}
// 같은 회사의 모든 활성 화면에서 이 화면을 참조하는지 확인
const whereConditions: string[] = ["sd.is_active != 'D'"];
const params: any[] = [];
2025-09-08 13:10:09 +09:00
if (userCompanyCode !== "*") {
whereConditions.push(
`sd.company_code IN ($${params.length + 1}, $${params.length + 2})`
);
params.push(userCompanyCode, "*");
}
const whereSQL = whereConditions.join(" AND ");
// 화면과 레이아웃을 JOIN해서 조회
const allScreens = await query<any>(
`SELECT
sd.screen_id, sd.screen_name, sd.screen_code, sd.company_code,
sl.layout_id, sl.component_id, sl.component_type, sl.properties
FROM screen_definitions sd
LEFT JOIN screen_layouts sl ON sd.screen_id = sl.screen_id
WHERE ${whereSQL}
ORDER BY sd.screen_id, sl.layout_id`,
params
);
2025-09-08 13:10:09 +09:00
const dependencies: Array<{
screenId: number;
screenName: string;
screenCode: string;
componentId: string;
componentType: string;
referenceType: string;
}> = [];
// 각 화면의 레이아웃에서 버튼 컴포넌트들을 검사
for (const screen of allScreens) {
if (screen.screen_id === screenId) continue; // 자기 자신은 제외
try {
// screen_layouts 테이블에서 버튼 컴포넌트 확인 (위젯 타입만)
if (screen.component_type === "widget") {
const properties = screen.properties as any;
2025-09-08 13:10:09 +09:00
// 버튼 컴포넌트인지 확인
if (properties?.widgetType === "button") {
const config = properties.webTypeConfig;
if (!config) continue;
// popup 액션에서 popupScreenId 확인
if (
config.actionType === "popup" &&
config.popupScreenId === screenId
) {
dependencies.push({
screenId: screen.screen_id,
screenName: screen.screen_name,
screenCode: screen.screen_code,
componentId: screen.component_id,
2025-09-08 13:10:09 +09:00
componentType: "button",
referenceType: "popup",
});
}
// navigate 액션에서 navigateScreenId 확인
if (
config.actionType === "navigate" &&
config.navigateScreenId === screenId
) {
dependencies.push({
screenId: screen.screen_id,
screenName: screen.screen_name,
screenCode: screen.screen_code,
componentId: screen.component_id,
2025-09-08 13:10:09 +09:00
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: screen.component_id,
2025-09-08 13:10:09 +09:00
componentType: "button",
referenceType: "url",
});
}
}
}
// 기존 layout_metadata도 확인 (하위 호환성) - 현재는 사용하지 않음
// 실제 데이터는 screen_layouts 테이블에서 개별적으로 조회해야 함
2025-09-08 13:10:09 +09:00
} catch (error) {
console.error(
`화면 ${screen.screen_id}의 레이아웃 분석 중 오류:`,
error
);
continue;
}
}
// 메뉴 할당 확인 (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]
);
2025-09-08 13:10:09 +09:00
// 메뉴에 할당된 경우 의존성에 추가
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);
// 메뉴 할당 확인 실패해도 다른 의존성 체크는 계속 진행
2025-09-08 13:10:09 +09:00
}
return {
hasDependencies: dependencies.length > 0,
dependencies,
};
}
/**
* ( - ) ( Raw Query )
2025-09-01 11:48:12 +09:00
*/
2025-09-08 13:10:09 +09:00
async deleteScreen(
screenId: number,
userCompanyCode: string,
deletedBy: string,
deleteReason?: string,
force: boolean = false
): Promise<void> {
// 권한 확인 (Raw Query)
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]
);
2025-09-01 11:48:12 +09:00
if (existingResult.length === 0) {
2025-09-01 11:48:12 +09:00
throw new Error("화면을 찾을 수 없습니다.");
}
const existingScreen = existingResult[0];
if (
userCompanyCode !== "*" &&
existingScreen.company_code !== userCompanyCode
) {
throw new Error("이 화면을 삭제할 권한이 없습니다.");
2025-09-01 11:48:12 +09:00
}
2025-09-08 13:10:09 +09:00
// 이미 삭제된 화면인지 확인
if (existingScreen.is_active === "D") {
throw new Error("이미 삭제된 화면입니다.");
}
// 강제 삭제가 아닌 경우 의존성 체크
if (!force) {
const dependencyCheck = await this.checkScreenDependencies(
screenId,
userCompanyCode
);
if (dependencyCheck.hasDependencies) {
const error = new Error("다른 화면에서 사용 중인 화면입니다.") as any;
error.code = "SCREEN_HAS_DEPENDENCIES";
error.dependencies = dependencyCheck.dependencies;
throw error;
}
}
// 트랜잭션으로 화면 삭제와 메뉴 할당 정리를 함께 처리 (Raw Query)
await transaction(async (client) => {
2025-09-08 13:10:09 +09:00
// 소프트 삭제 (휴지통으로 이동)
await client.query(
`UPDATE screen_definitions
SET is_active = 'D',
deleted_date = $1,
deleted_by = $2,
delete_reason = $3,
updated_date = $4,
updated_by = $5
WHERE screen_id = $6`,
[
new Date(),
deletedBy,
deleteReason || null,
new Date(),
deletedBy,
screenId,
]
);
2025-09-08 13:10:09 +09:00
// 메뉴 할당도 비활성화
await client.query(
`UPDATE screen_menu_assignments
SET is_active = 'N'
WHERE screen_id = $1 AND is_active = 'Y'`,
[screenId]
);
2025-09-08 13:10:09 +09:00
});
}
/**
* ( ) ( Raw Query )
2025-09-08 13:10:09 +09:00
*/
async restoreScreen(
screenId: number,
userCompanyCode: string,
restoredBy: string
): Promise<void> {
// 권한 확인
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]
);
2025-09-08 13:10:09 +09:00
if (screens.length === 0) {
2025-09-08 13:10:09 +09:00
throw new Error("화면을 찾을 수 없습니다.");
}
const existingScreen = screens[0];
2025-09-08 13:10:09 +09:00
if (
userCompanyCode !== "*" &&
existingScreen.company_code !== userCompanyCode
) {
throw new Error("이 화면을 복원할 권한이 없습니다.");
}
// 삭제된 화면이 아닌 경우
if (existingScreen.is_active !== "D") {
throw new Error("삭제된 화면이 아닙니다.");
}
// 화면 코드 중복 확인 (복원 시 같은 코드가 이미 존재하는지)
const duplicateScreens = await query<{ screen_id: number }>(
`SELECT screen_id FROM screen_definitions
WHERE screen_code = $1 AND is_active != 'D' AND screen_id != $2
LIMIT 1`,
[existingScreen.screen_code, screenId]
);
2025-09-08 13:10:09 +09:00
if (duplicateScreens.length > 0) {
2025-09-08 13:10:09 +09:00
throw new Error(
"같은 화면 코드를 가진 활성 화면이 이미 존재합니다. 복원하려면 기존 화면의 코드를 변경하거나 삭제해주세요."
);
}
// 트랜잭션으로 화면 복원과 메뉴 할당 복원을 함께 처리
await transaction(async (client) => {
2025-09-08 13:10:09 +09:00
// 화면 복원
await client.query(
`UPDATE screen_definitions
SET is_active = 'Y', deleted_date = NULL, deleted_by = NULL,
delete_reason = NULL, updated_date = $1, updated_by = $2
WHERE screen_id = $3`,
[new Date(), restoredBy, screenId]
);
2025-09-08 13:10:09 +09:00
// 메뉴 할당도 다시 활성화
await client.query(
`UPDATE screen_menu_assignments
SET is_active = 'Y'
WHERE screen_id = $1 AND is_active = 'N'`,
[screenId]
);
2025-09-08 13:10:09 +09:00
});
}
/**
* () ( Raw Query )
2025-09-08 13:10:09 +09:00
*/
async cleanupDeletedScreenMenuAssignments(): Promise<{
updatedCount: number;
message: string;
}> {
const result = await query(
`UPDATE screen_menu_assignments
SET is_active = 'N'
WHERE screen_id IN (
SELECT screen_id
FROM screen_definitions
WHERE is_active = 'D'
) AND is_active = 'Y'`,
[]
);
const updatedCount = result.length;
2025-09-08 13:10:09 +09:00
return {
updatedCount,
message: `${updatedCount}개의 메뉴 할당이 정리되었습니다.`,
2025-09-08 13:10:09 +09:00
};
}
/**
* ( ) ( Raw Query )
2025-09-08 13:10:09 +09:00
*/
async permanentDeleteScreen(
screenId: number,
userCompanyCode: string
): Promise<void> {
// 권한 확인
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]
);
2025-09-08 13:10:09 +09:00
if (screens.length === 0) {
2025-09-08 13:10:09 +09:00
throw new Error("화면을 찾을 수 없습니다.");
}
const existingScreen = screens[0];
2025-09-08 13:10:09 +09:00
if (
userCompanyCode !== "*" &&
existingScreen.company_code !== userCompanyCode
) {
throw new Error("이 화면을 영구 삭제할 권한이 없습니다.");
}
// 삭제된 화면이 아닌 경우 영구 삭제 불가
if (existingScreen.is_active !== "D") {
throw new Error("휴지통에 있는 화면만 영구 삭제할 수 있습니다.");
}
// 물리적 삭제 (수동으로 관련 데이터 삭제)
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]
);
2025-09-01 11:48:12 +09:00
});
}
2025-09-08 13:10:09 +09:00
/**
* ( Raw Query )
2025-09-08 13:10:09 +09:00
*/
async getDeletedScreens(
companyCode: string,
page: number = 1,
size: number = 20
): Promise<
PaginatedResponse<
ScreenDefinition & {
deletedDate?: Date;
deletedBy?: string;
deleteReason?: string;
}
>
> {
const offset = (page - 1) * size;
const whereConditions: string[] = ["is_active = 'D'"];
const params: any[] = [];
2025-09-08 13:10:09 +09:00
if (companyCode !== "*") {
whereConditions.push(`company_code = $${params.length + 1}`);
params.push(companyCode);
2025-09-08 13:10:09 +09:00
}
const whereSQL = whereConditions.join(" AND ");
const [screens, totalResult] = await Promise.all([
query<any>(
`SELECT * FROM screen_definitions
WHERE ${whereSQL}
ORDER BY deleted_date DESC NULLS LAST
LIMIT $${params.length + 1} OFFSET $${params.length + 2}`,
[...params, size, offset]
),
query<{ count: string }>(
`SELECT COUNT(*)::text as count FROM screen_definitions WHERE ${whereSQL}`,
params
),
2025-09-08 13:10:09 +09:00
]);
const total = parseInt(totalResult[0]?.count || "0", 10);
2025-09-08 14:20:01 +09:00
// 테이블 라벨 정보를 한 번에 조회
const tableNames = Array.from(
new Set(screens.map((s: any) => s.table_name).filter(Boolean))
);
2025-09-08 14:20:01 +09:00
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;
}>(
`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,
])
);
}
2025-09-08 14:20:01 +09:00
2025-09-08 13:10:09 +09:00
return {
data: screens.map((screen: any) => ({
2025-09-08 14:20:01 +09:00
...this.mapToScreenDefinition(screen, tableLabelMap),
2025-09-08 13:10:09 +09:00
deletedDate: screen.deleted_date || undefined,
deletedBy: screen.deleted_by || undefined,
deleteReason: screen.delete_reason || undefined,
})),
pagination: {
page,
size,
total,
totalPages: Math.ceil(total / size),
},
};
}
/**
*
*/
async bulkPermanentDeleteScreens(
screenIds: number[],
userCompanyCode: string
): Promise<{
deletedCount: number;
skippedCount: number;
errors: Array<{ screenId: number; error: string }>;
}> {
if (screenIds.length === 0) {
throw new Error("삭제할 화면을 선택해주세요.");
}
// 권한 확인 - 해당 회사의 휴지통 화면들만 조회
const whereClause: any = {
screen_id: { in: screenIds },
is_active: "D", // 휴지통에 있는 화면만
};
if (userCompanyCode !== "*") {
whereClause.company_code = userCompanyCode;
}
// WHERE 절 생성
const whereConditions: string[] = ["is_active = 'D'"];
const params: any[] = [];
if (userCompanyCode !== "*") {
whereConditions.push(`company_code = $${params.length + 1}`);
params.push(userCompanyCode);
}
const whereSQL = whereConditions.join(" AND ");
const screensToDelete = await query<{ screen_id: number }>(
`SELECT screen_id FROM screen_definitions WHERE ${whereSQL}`,
params
);
2025-09-08 13:10:09 +09:00
let deletedCount = 0;
let skippedCount = 0;
const errors: Array<{ screenId: number; error: string }> = [];
// 각 화면을 개별적으로 삭제 처리
for (const screenId of screenIds) {
try {
const screenToDelete = screensToDelete.find(
(s: any) => s.screen_id === screenId
2025-09-08 13:10:09 +09:00
);
if (!screenToDelete) {
skippedCount++;
errors.push({
screenId,
error: "화면을 찾을 수 없거나 삭제 권한이 없습니다.",
});
continue;
}
// 관련 레이아웃 데이터도 함께 삭제 (트랜잭션)
await transaction(async (client) => {
2025-09-08 13:10:09 +09:00
// screen_layouts 삭제
await client.query(
`DELETE FROM screen_layouts WHERE screen_id = $1`,
[screenId]
);
2025-09-08 13:10:09 +09:00
// screen_menu_assignments 삭제
await client.query(
`DELETE FROM screen_menu_assignments WHERE screen_id = $1`,
[screenId]
);
2025-09-08 13:10:09 +09:00
// screen_definitions 삭제
await client.query(
`DELETE FROM screen_definitions WHERE screen_id = $1`,
[screenId]
);
2025-09-08 13:10:09 +09:00
});
deletedCount++;
} catch (error) {
skippedCount++;
errors.push({
screenId,
error: error instanceof Error ? error.message : "알 수 없는 오류",
});
console.error(`화면 ${screenId} 영구 삭제 실패:`, error);
}
}
return {
deletedCount,
skippedCount,
errors,
};
}
// ========================================
// 테이블 관리
// ========================================
/**
* ( ) ( Raw Query )
*/
async getTables(companyCode: string): Promise<TableInfo[]> {
try {
// PostgreSQL에서 사용 가능한 테이블 목록 조회
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[] = [];
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("테이블 목록을 조회할 수 없습니다.");
}
}
2025-09-02 11:16:40 +09:00
/**
* ( )
*/
async getTableInfo(
tableName: string,
companyCode: string
): Promise<TableInfo | null> {
try {
console.log(`=== 단일 테이블 조회 시작: ${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]
);
2025-09-02 11:16:40 +09:00
if (tableExists.length === 0) {
console.log(`테이블 ${tableName}이 존재하지 않습니다.`);
return null;
}
// 해당 테이블의 컬럼 정보 조회
const columns = await this.getTableColumns(tableName, companyCode);
if (columns.length === 0) {
console.log(`테이블 ${tableName}의 컬럼 정보가 없습니다.`);
return null;
}
const tableInfo: TableInfo = {
tableName: tableName,
tableLabel: this.getTableLabel(tableName),
columns: columns,
};
console.log(
`단일 테이블 조회 완료: ${tableName}, 컬럼 ${columns.length}`
);
return tableInfo;
} catch (error) {
console.error(`테이블 ${tableName} 조회 실패:`, error);
throw new Error(`테이블 ${tableName} 정보를 조회할 수 없습니다.`);
}
}
/**
* ( Raw Query )
*/
async getTableColumns(
tableName: string,
companyCode: string
): Promise<ColumnInfo[]> {
try {
// 테이블 컬럼 정보 조회
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,
column_default,
character_maximum_length,
numeric_precision,
numeric_scale
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = $1
ORDER BY ordinal_position`,
[tableName]
);
2025-10-14 11:48:04 +09:00
// column_labels 테이블에서 입력타입 정보 조회 (있는 경우)
const webTypeInfo = await query<{
column_name: string;
2025-10-14 11:48:04 +09:00
input_type: string | null;
column_label: string | null;
detail_settings: any;
}>(
2025-10-14 11:48:04 +09:00
`SELECT column_name, input_type, column_label, detail_settings
FROM column_labels
WHERE table_name = $1`,
[tableName]
);
// 컬럼 정보 매핑
return columns.map((column: any) => {
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:
2025-10-14 11:48:04 +09:00
(webTypeData?.input_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 {
// 통합 타입 매핑에서 import
const { DB_TYPE_TO_WEB_TYPE } = require("../types/unified-web-types");
const lowerType = dataType.toLowerCase();
// 정확한 매핑 우선 확인
if (DB_TYPE_TO_WEB_TYPE[lowerType]) {
return DB_TYPE_TO_WEB_TYPE[lowerType];
}
// 부분 문자열 매칭 (더 정교한 규칙)
for (const [dbType, webType] of Object.entries(DB_TYPE_TO_WEB_TYPE)) {
if (
lowerType.includes(dbType.toLowerCase()) ||
dbType.toLowerCase().includes(lowerType)
) {
return webType as WebType;
}
}
// 추가 정밀 매핑
if (lowerType.includes("int") && !lowerType.includes("point")) {
return "number";
} else if (lowerType.includes("numeric") || lowerType.includes("decimal")) {
return "decimal";
} else if (
lowerType.includes("timestamp") ||
lowerType.includes("datetime")
) {
return "datetime";
} else if (lowerType.includes("date")) {
return "date";
} else if (lowerType.includes("time")) {
return "datetime";
} else if (lowerType.includes("bool")) {
return "checkbox";
} else if (
lowerType.includes("char") ||
lowerType.includes("text") ||
lowerType.includes("varchar")
) {
return lowerType.includes("text") ? "textarea" : "text";
}
// 기본값
return "text";
}
2025-09-01 11:48:12 +09:00
// ========================================
// 레이아웃 관리
// ========================================
/**
* ( Raw Query )
2025-09-01 11:48:12 +09:00
*/
async saveLayout(
screenId: number,
layoutData: LayoutData,
companyCode: string
2025-09-01 11:48:12 +09:00
): Promise<void> {
console.log(`=== 레이아웃 저장 시작 ===`);
console.log(`화면 ID: ${screenId}`);
console.log(`컴포넌트 수: ${layoutData.components.length}`);
2025-09-04 17:01:07 +09:00
console.log(`격자 설정:`, layoutData.gridSettings);
console.log(`해상도 설정:`, layoutData.screenResolution);
// 권한 확인
const screens = await query<{ company_code: string | null }>(
`SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
[screenId]
);
2025-09-01 11:48:12 +09:00
if (screens.length === 0) {
2025-09-01 11:48:12 +09:00
throw new Error("화면을 찾을 수 없습니다.");
}
const existingScreen = screens[0];
if (companyCode !== "*" && existingScreen.company_code !== companyCode) {
throw new Error("이 화면의 레이아웃을 저장할 권한이 없습니다.");
}
2025-09-04 17:01:07 +09:00
// 기존 레이아웃 삭제 (컴포넌트와 메타데이터 모두)
await query(`DELETE FROM screen_layouts WHERE screen_id = $1`, [screenId]);
2025-09-01 11:48:12 +09:00
2025-09-04 17:01:07 +09:00
// 1. 메타데이터 저장 (격자 설정과 해상도 정보)
if (layoutData.gridSettings || layoutData.screenResolution) {
const metadata: any = {
gridSettings: layoutData.gridSettings,
screenResolution: layoutData.screenResolution,
};
await query(
`INSERT INTO screen_layouts (
screen_id, component_type, component_id, parent_id,
position_x, position_y, width, height, properties, display_order
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
[
screenId,
"_metadata", // 특별한 타입으로 메타데이터 식별
`_metadata_${screenId}`,
null,
0,
0,
0,
0,
JSON.stringify(metadata),
-1, // 메타데이터는 맨 앞에 배치
]
);
2025-09-04 17:01:07 +09:00
console.log(`메타데이터 저장 완료:`, metadata);
}
// 2. 컴포넌트 저장
for (const component of layoutData.components) {
const { id, ...componentData } = component;
console.log(`저장 중인 컴포넌트:`, {
id: component.id,
type: component.type,
position: component.position,
size: component.size,
parentId: component.parentId,
title: (component as any).title,
});
// JSON 필드에 맞는 타입으로 변환
const properties: any = {
...componentData,
position: {
x: component.position.x,
y: component.position.y,
z: component.position.z || 1, // z 값 포함
},
size: {
width: component.size.width,
height: component.size.height,
},
};
await query(
`INSERT INTO screen_layouts (
screen_id, component_type, component_id, parent_id,
position_x, position_y, width, height, properties
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
[
screenId,
component.type,
component.id,
component.parentId || null,
Math.round(component.position.x), // 정수로 반올림
Math.round(component.position.y), // 정수로 반올림
Math.round(component.size.width), // 정수로 반올림
Math.round(component.size.height), // 정수로 반올림
JSON.stringify(properties),
]
);
}
console.log(`=== 레이아웃 저장 완료 ===`);
2025-09-01 11:48:12 +09:00
}
/**
* ( Raw Query )
2025-09-01 11:48:12 +09:00
*/
async getLayout(
screenId: number,
companyCode: string
): Promise<LayoutData | null> {
console.log(`=== 레이아웃 로드 시작 ===`);
console.log(`화면 ID: ${screenId}`);
// 권한 확인
const screens = await query<{ company_code: string | null }>(
`SELECT company_code FROM screen_definitions WHERE screen_id = $1 LIMIT 1`,
[screenId]
);
if (screens.length === 0) {
return null;
}
const existingScreen = screens[0];
if (companyCode !== "*" && existingScreen.company_code !== companyCode) {
throw new Error("이 화면의 레이아웃을 조회할 권한이 없습니다.");
}
const layouts = await query<any>(
`SELECT * FROM screen_layouts
WHERE screen_id = $1
ORDER BY display_order ASC NULLS LAST, layout_id ASC`,
[screenId]
);
2025-09-01 11:48:12 +09:00
console.log(`DB에서 조회된 레이아웃 수: ${layouts.length}`);
2025-09-04 17:01:07 +09:00
// 메타데이터와 컴포넌트 분리
const metadataLayout = layouts.find(
(layout) => layout.component_type === "_metadata"
);
const componentLayouts = layouts.filter(
(layout) => layout.component_type !== "_metadata"
);
// 기본 메타데이터 설정
let gridSettings = {
columns: 12,
gap: 16,
padding: 16,
snapToGrid: true,
showGrid: true,
};
let screenResolution = null;
// 저장된 메타데이터가 있으면 적용
if (metadataLayout && metadataLayout.properties) {
const metadata = metadataLayout.properties as any;
if (metadata.gridSettings) {
gridSettings = { ...gridSettings, ...metadata.gridSettings };
}
if (metadata.screenResolution) {
screenResolution = metadata.screenResolution;
}
console.log(`메타데이터 로드:`, { gridSettings, screenResolution });
}
if (componentLayouts.length === 0) {
return {
components: [],
2025-09-04 17:01:07 +09:00
gridSettings,
screenResolution,
};
2025-09-01 11:48:12 +09:00
}
2025-09-04 17:01:07 +09:00
const components: ComponentData[] = componentLayouts.map((layout) => {
const properties = layout.properties as any;
const component = {
2025-09-01 11:48:12 +09:00
id: layout.component_id,
type: layout.component_type as any,
position: {
x: layout.position_x,
y: layout.position_y,
z: properties?.position?.z || 1, // z 값 복원
},
2025-09-01 11:48:12 +09:00
size: { width: layout.width, height: layout.height },
parentId: layout.parent_id,
...properties,
2025-09-01 11:48:12 +09:00
};
console.log(`로드된 컴포넌트:`, {
id: component.id,
type: component.type,
position: component.position,
size: component.size,
parentId: component.parentId,
title: (component as any).title,
});
return component;
2025-09-01 11:48:12 +09:00
});
console.log(`=== 레이아웃 로드 완료 ===`);
console.log(`반환할 컴포넌트 수: ${components.length}`);
2025-09-04 17:01:07 +09:00
console.log(`최종 격자 설정:`, gridSettings);
console.log(`최종 해상도 설정:`, screenResolution);
2025-09-01 11:48:12 +09:00
return {
components,
2025-09-04 17:01:07 +09:00
gridSettings,
screenResolution,
2025-09-01 11:48:12 +09:00
};
}
// ========================================
// 템플릿 관리
// ========================================
/**
* 릿 () ( Raw Query )
2025-09-01 11:48:12 +09:00
*/
async getTemplatesByCompany(
companyCode: string,
type?: string,
isPublic?: boolean
): Promise<ScreenTemplate[]> {
const whereConditions: string[] = [];
const params: any[] = [];
2025-09-01 11:48:12 +09:00
if (companyCode !== "*") {
whereConditions.push(`company_code = $${params.length + 1}`);
params.push(companyCode);
2025-09-01 11:48:12 +09:00
}
if (type) {
whereConditions.push(`template_type = $${params.length + 1}`);
params.push(type);
2025-09-01 11:48:12 +09:00
}
if (isPublic !== undefined) {
whereConditions.push(`is_public = $${params.length + 1}`);
params.push(isPublic);
2025-09-01 11:48:12 +09:00
}
const whereSQL =
whereConditions.length > 0
? `WHERE ${whereConditions.join(" AND ")}`
: "";
const templates = await query<any>(
`SELECT * FROM screen_templates
${whereSQL}
ORDER BY created_date DESC`,
params
);
2025-09-01 11:48:12 +09:00
return templates.map(this.mapToScreenTemplate);
}
/**
* 릿 ( Raw Query )
2025-09-01 11:48:12 +09:00
*/
async createTemplate(
templateData: Partial<ScreenTemplate>
): Promise<ScreenTemplate> {
const [template] = await query<any>(
`INSERT INTO screen_templates (
template_name, template_type, company_code, description,
layout_data, is_public, created_by
) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *`,
[
templateData.templateName!,
templateData.templateType!,
templateData.companyCode!,
templateData.description || null,
templateData.layoutData
? JSON.stringify(JSON.parse(JSON.stringify(templateData.layoutData)))
2025-09-01 11:48:12 +09:00
: null,
templateData.isPublic || false,
templateData.createdBy || null,
]
);
2025-09-01 11:48:12 +09:00
return this.mapToScreenTemplate(template);
}
// ========================================
// 메뉴 할당 관리
// ========================================
/**
* - ( Raw Query )
2025-09-01 11:48:12 +09:00
*/
async assignScreenToMenu(
screenId: number,
assignmentData: MenuAssignmentRequest
): Promise<void> {
// 중복 할당 방지
const existing = await query<{ assignment_id: number }>(
`SELECT assignment_id FROM screen_menu_assignments
WHERE screen_id = $1 AND menu_objid = $2 AND company_code = $3
LIMIT 1`,
[screenId, assignmentData.menuObjid, assignmentData.companyCode]
);
2025-09-01 11:48:12 +09:00
if (existing.length > 0) {
2025-09-01 11:48:12 +09:00
throw new Error("이미 할당된 화면입니다.");
}
await query(
`INSERT INTO screen_menu_assignments (
screen_id, menu_objid, company_code, display_order, created_by
) VALUES ($1, $2, $3, $4, $5)`,
[
screenId,
assignmentData.menuObjid,
assignmentData.companyCode,
assignmentData.displayOrder || 0,
assignmentData.createdBy || null,
]
);
2025-09-01 11:48:12 +09:00
}
/**
* ( Raw Query )
2025-09-01 11:48:12 +09:00
*/
async getScreensByMenu(
menuObjid: number,
companyCode: string
): Promise<ScreenDefinition[]> {
const screens = await query<any>(
`SELECT sd.* FROM screen_menu_assignments sma
INNER JOIN screen_definitions sd ON sma.screen_id = sd.screen_id
WHERE sma.menu_objid = $1
AND sma.company_code = $2
AND sma.is_active = 'Y'
ORDER BY sma.display_order ASC`,
[menuObjid, companyCode]
2025-09-01 11:48:12 +09:00
);
return screens.map((screen) => this.mapToScreenDefinition(screen));
2025-09-01 11:48:12 +09:00
}
2025-09-01 18:42:59 +09:00
/**
* - ( Raw Query )
2025-09-01 18:42:59 +09:00
*/
async unassignScreenFromMenu(
screenId: number,
menuObjid: number,
companyCode: string
): Promise<void> {
await query(
`DELETE FROM screen_menu_assignments
WHERE screen_id = $1 AND menu_objid = $2 AND company_code = $3`,
[screenId, menuObjid, companyCode]
);
2025-09-01 18:42:59 +09:00
}
2025-09-01 11:48:12 +09:00
// ========================================
// 테이블 타입 연계
// ========================================
/**
* ( ) ( Raw Query )
2025-09-01 11:48:12 +09:00
*/
async getColumnInfo(tableName: string): Promise<ColumnInfo[]> {
const columns = await query<any>(
`SELECT
2025-09-01 11:48:12 +09:00
c.column_name,
COALESCE(cl.column_label, c.column_name) as column_label,
c.data_type,
2025-10-14 11:48:04 +09:00
COALESCE(cl.input_type, 'text') as web_type,
2025-09-01 11:48:12 +09:00
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,
2025-09-16 15:13:00 +09:00
cl.display_column,
2025-09-01 11:48:12 +09:00
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 = $1
ORDER BY COALESCE(cl.display_order, c.ordinal_position)`,
[tableName]
);
2025-09-01 11:48:12 +09:00
return columns as ColumnInfo[];
}
/**
2025-10-14 11:48:04 +09:00
* ( Raw Query )
2025-09-01 11:48:12 +09:00
*/
async setColumnWebType(
tableName: string,
columnName: string,
webType: WebType,
additionalSettings?: Partial<ColumnWebTypeSetting>
): Promise<void> {
2025-10-14 11:48:04 +09:00
// UPSERT를 INSERT ... ON CONFLICT로 변환 (input_type 사용)
await query(
`INSERT INTO column_labels (
2025-10-14 11:48:04 +09:00
table_name, column_name, column_label, input_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
2025-10-14 11:48:04 +09:00
input_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
2025-09-01 11:48:12 +09:00
? JSON.stringify(additionalSettings.detailSettings)
: null,
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(),
]
);
2025-09-01 11:48:12 +09:00
}
/**
*
*/
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",
};
}
}
// ========================================
// 유틸리티 메서드
// ========================================
2025-09-08 14:20:01 +09:00
private mapToScreenDefinition(
data: any,
tableLabelMap?: Map<string, string>
): ScreenDefinition {
const tableLabel = tableLabelMap?.get(data.table_name) || data.table_name;
2025-09-01 11:48:12 +09:00
return {
screenId: data.screen_id,
screenName: data.screen_name,
screenCode: data.screen_code,
tableName: data.table_name,
2025-09-08 14:20:01 +09:00
tableLabel: tableLabel, // 라벨이 있으면 라벨, 없으면 테이블명
2025-09-01 11:48:12 +09:00
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,
};
}
2025-09-01 17:57:52 +09:00
/**
* ( + '_' + ) ( Raw Query )
2025-09-01 17:57:52 +09:00
*/
async generateScreenCode(companyCode: string): Promise<string> {
// 해당 회사의 기존 화면 코드들 조회 (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}%`]
);
2025-09-01 17:57:52 +09:00
// 회사 코드 뒤의 숫자 부분 추출하여 최대값 찾기
let maxNumber = 0;
const pattern = new RegExp(
`^${companyCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:_)?(\\d+)$`
);
for (const screen of existingScreens) {
const match = screen.screen_code.match(pattern);
if (match) {
const number = parseInt(match[1], 10);
if (number > maxNumber) {
maxNumber = number;
}
}
}
// 다음 순번으로 화면 코드 생성 (3자리 패딩)
const nextNumber = maxNumber + 1;
const paddedNumber = nextNumber.toString().padStart(3, "0");
return `${companyCode}_${paddedNumber}`;
}
2025-09-03 18:23:47 +09:00
/**
* ( + ) ( Raw Query )
2025-09-03 18:23:47 +09:00
*/
async copyScreen(
sourceScreenId: number,
copyData: CopyScreenRequest
): Promise<ScreenDefinition> {
// 트랜잭션으로 처리
return await transaction(async (client) => {
2025-09-03 18:23:47 +09:00
// 1. 원본 화면 정보 조회
const sourceScreens = await client.query<any>(
`SELECT * FROM screen_definitions
WHERE screen_id = $1 AND company_code = $2
LIMIT 1`,
[sourceScreenId, copyData.companyCode]
);
2025-09-03 18:23:47 +09:00
if (sourceScreens.rows.length === 0) {
2025-09-03 18:23:47 +09:00
throw new Error("복사할 화면을 찾을 수 없습니다.");
}
const sourceScreen = sourceScreens.rows[0];
2025-09-03 18:23:47 +09:00
// 2. 화면 코드 중복 체크
const existingScreens = await client.query<any>(
`SELECT screen_id FROM screen_definitions
WHERE screen_code = $1 AND company_code = $2
LIMIT 1`,
[copyData.screenCode, copyData.companyCode]
);
2025-09-03 18:23:47 +09:00
if (existingScreens.rows.length > 0) {
2025-09-03 18:23:47 +09:00
throw new Error("이미 존재하는 화면 코드입니다.");
}
// 3. 새 화면 생성
const newScreenResult = await client.query<any>(
`INSERT INTO screen_definitions (
screen_code, screen_name, description, company_code, table_name,
is_active, created_by, created_date, updated_by, updated_date
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *`,
[
copyData.screenCode,
copyData.screenName,
copyData.description || sourceScreen.description,
copyData.companyCode,
sourceScreen.table_name,
sourceScreen.is_active,
copyData.createdBy,
new Date(),
copyData.createdBy,
new Date(),
]
);
const newScreen = newScreenResult.rows[0];
2025-09-03 18:23:47 +09:00
// 4. 원본 화면의 레이아웃 정보 조회
const sourceLayoutsResult = await client.query<any>(
`SELECT * FROM screen_layouts
WHERE screen_id = $1
ORDER BY display_order ASC NULLS LAST`,
[sourceScreenId]
);
const sourceLayouts = sourceLayoutsResult.rows;
2025-09-03 18:23:47 +09:00
// 5. 레이아웃이 있다면 복사
if (sourceLayouts.length > 0) {
try {
// ID 매핑 맵 생성
const idMapping: { [oldId: string]: string } = {};
// 새로운 컴포넌트 ID 미리 생성
sourceLayouts.forEach((layout: any) => {
2025-09-03 18:23:47 +09:00
idMapping[layout.component_id] = generateId();
});
// 각 레이아웃 컴포넌트 복사
for (const sourceLayout of sourceLayouts) {
const newComponentId = idMapping[sourceLayout.component_id];
const newParentId = sourceLayout.parent_id
? idMapping[sourceLayout.parent_id]
: null;
await client.query(
`INSERT INTO screen_layouts (
screen_id, component_type, component_id, parent_id,
position_x, position_y, width, height, properties,
display_order, created_date
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
[
newScreen.screen_id,
sourceLayout.component_type,
newComponentId,
newParentId,
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,
new Date(),
]
);
2025-09-03 18:23:47 +09:00
}
} catch (error) {
console.error("레이아웃 복사 중 오류:", error);
// 레이아웃 복사 실패해도 화면 생성은 유지
}
}
// 6. 생성된 화면 정보 반환
return {
screenId: newScreen.screen_id,
screenCode: newScreen.screen_code,
screenName: newScreen.screen_name,
description: newScreen.description || "",
companyCode: newScreen.company_code,
tableName: newScreen.table_name,
isActive: newScreen.is_active,
createdBy: newScreen.created_by || undefined,
createdDate: newScreen.created_date,
updatedBy: newScreen.updated_by || undefined,
updatedDate: newScreen.updated_date,
};
});
}
2025-09-01 11:48:12 +09:00
}
// 서비스 인스턴스 export
export const screenManagementService = new ScreenManagementService();