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";
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 14:00:31 +09:00
|
|
|
// 백엔드에서 사용할 테이블 정보 타입
|
|
|
|
|
interface TableInfo {
|
|
|
|
|
tableName: string;
|
|
|
|
|
tableLabel: string;
|
|
|
|
|
columns: ColumnInfo[];
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 11:48:12 +09:00
|
|
|
export class ScreenManagementService {
|
|
|
|
|
// ========================================
|
|
|
|
|
// 화면 정의 관리
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 화면 정의 생성
|
|
|
|
|
*/
|
|
|
|
|
async createScreen(
|
|
|
|
|
screenData: CreateScreenRequest,
|
|
|
|
|
userCompanyCode: string
|
|
|
|
|
): Promise<ScreenDefinition> {
|
2025-09-04 17:01:07 +09:00
|
|
|
console.log(`=== 화면 생성 요청 ===`);
|
|
|
|
|
console.log(`요청 데이터:`, screenData);
|
|
|
|
|
console.log(`사용자 회사 코드:`, userCompanyCode);
|
|
|
|
|
|
2025-09-01 11:48:12 +09:00
|
|
|
// 화면 코드 중복 확인
|
2025-09-08 13:10:09 +09:00
|
|
|
const existingScreen = await prisma.screen_definitions.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
screen_code: screenData.screenCode,
|
|
|
|
|
is_active: { not: "D" }, // 삭제되지 않은 화면만 중복 검사
|
|
|
|
|
},
|
2025-09-01 11:48:12 +09:00
|
|
|
});
|
|
|
|
|
|
2025-09-04 17:01:07 +09:00
|
|
|
console.log(
|
|
|
|
|
`화면 코드 '${screenData.screenCode}' 중복 검사 결과:`,
|
|
|
|
|
existingScreen ? "중복됨" : "사용 가능"
|
|
|
|
|
);
|
|
|
|
|
|
2025-09-01 11:48:12 +09:00
|
|
|
if (existingScreen) {
|
2025-09-04 17:01:07 +09:00
|
|
|
console.log(`기존 화면 정보:`, existingScreen);
|
2025-09-01 11:48:12 +09:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-08 13:10:09 +09:00
|
|
|
* 회사별 화면 목록 조회 (페이징 지원) - 활성 화면만
|
2025-09-01 11:48:12 +09:00
|
|
|
*/
|
|
|
|
|
async getScreensByCompany(
|
|
|
|
|
companyCode: string,
|
|
|
|
|
page: number = 1,
|
|
|
|
|
size: number = 20
|
|
|
|
|
): Promise<PaginatedResponse<ScreenDefinition>> {
|
2025-09-08 13:10:09 +09:00
|
|
|
const whereClause: any = { is_active: { not: "D" } }; // 삭제된 화면 제외
|
|
|
|
|
|
|
|
|
|
if (companyCode !== "*") {
|
|
|
|
|
whereClause.company_code = companyCode;
|
|
|
|
|
}
|
2025-09-01 11:48:12 +09:00
|
|
|
|
|
|
|
|
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 }),
|
|
|
|
|
]);
|
|
|
|
|
|
2025-09-08 14:20:01 +09:00
|
|
|
// 테이블 라벨 정보를 한 번에 조회
|
|
|
|
|
const tableNames = [
|
|
|
|
|
...new Set(screens.map((s) => s.table_name).filter(Boolean)),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
let tableLabelMap = new Map<string, string>();
|
|
|
|
|
|
|
|
|
|
if (tableNames.length > 0) {
|
|
|
|
|
try {
|
|
|
|
|
const tableLabels = await prisma.table_labels.findMany({
|
|
|
|
|
where: { table_name: { in: tableNames } },
|
|
|
|
|
select: { table_name: true, table_label: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 14:00:31 +09:00
|
|
|
/**
|
2025-09-08 13:10:09 +09:00
|
|
|
* 화면 목록 조회 (간단 버전) - 활성 화면만
|
2025-09-01 14:00:31 +09:00
|
|
|
*/
|
|
|
|
|
async getScreens(companyCode: string): Promise<ScreenDefinition[]> {
|
2025-09-08 13:10:09 +09:00
|
|
|
const whereClause: any = { is_active: { not: "D" } }; // 삭제된 화면 제외
|
|
|
|
|
|
|
|
|
|
if (companyCode !== "*") {
|
|
|
|
|
whereClause.company_code = companyCode;
|
|
|
|
|
}
|
2025-09-01 14:00:31 +09:00
|
|
|
|
|
|
|
|
const screens = await prisma.screen_definitions.findMany({
|
|
|
|
|
where: whereClause,
|
|
|
|
|
orderBy: { created_date: "desc" },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return screens.map((screen) => this.mapToScreenDefinition(screen));
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 11:48:12 +09:00
|
|
|
/**
|
2025-09-08 13:10:09 +09:00
|
|
|
* 화면 정의 조회 (활성 화면만)
|
2025-09-01 11:48:12 +09:00
|
|
|
*/
|
|
|
|
|
async getScreenById(screenId: number): Promise<ScreenDefinition | null> {
|
2025-09-08 13:10:09 +09:00
|
|
|
const screen = await prisma.screen_definitions.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
screen_id: screenId,
|
|
|
|
|
is_active: { not: "D" }, // 삭제된 화면 제외
|
|
|
|
|
},
|
2025-09-01 11:48:12 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return screen ? this.mapToScreenDefinition(screen) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 18:42:59 +09:00
|
|
|
/**
|
2025-09-08 13:10:09 +09:00
|
|
|
* 화면 정의 조회 (회사 코드 검증 포함, 활성 화면만)
|
2025-09-01 18:42:59 +09:00
|
|
|
*/
|
|
|
|
|
async getScreen(
|
|
|
|
|
screenId: number,
|
|
|
|
|
companyCode: string
|
|
|
|
|
): Promise<ScreenDefinition | null> {
|
2025-09-08 13:10:09 +09:00
|
|
|
const whereClause: any = {
|
|
|
|
|
screen_id: screenId,
|
|
|
|
|
is_active: { not: "D" }, // 삭제된 화면 제외
|
|
|
|
|
};
|
2025-09-01 18:42:59 +09:00
|
|
|
|
|
|
|
|
// 회사 코드가 '*'가 아닌 경우 회사별 필터링
|
|
|
|
|
if (companyCode !== "*") {
|
|
|
|
|
whereClause.company_code = companyCode;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-08 13:10:09 +09:00
|
|
|
const screen = await prisma.screen_definitions.findFirst({
|
2025-09-01 18:42:59 +09:00
|
|
|
where: whereClause,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return screen ? this.mapToScreenDefinition(screen) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 11:48:12 +09:00
|
|
|
/**
|
|
|
|
|
* 화면 정의 수정
|
|
|
|
|
*/
|
|
|
|
|
async updateScreen(
|
|
|
|
|
screenId: number,
|
|
|
|
|
updateData: UpdateScreenRequest,
|
|
|
|
|
userCompanyCode: string
|
|
|
|
|
): Promise<ScreenDefinition> {
|
2025-09-01 14:00:31 +09:00
|
|
|
// 권한 확인
|
|
|
|
|
const existingScreen = await prisma.screen_definitions.findUnique({
|
2025-09-01 11:48:12 +09:00
|
|
|
where: { screen_id: screenId },
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-01 14:00:31 +09:00
|
|
|
if (!existingScreen) {
|
2025-09-01 11:48:12 +09:00
|
|
|
throw new Error("화면을 찾을 수 없습니다.");
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 14:00:31 +09:00
|
|
|
if (
|
|
|
|
|
userCompanyCode !== "*" &&
|
|
|
|
|
existingScreen.company_code !== userCompanyCode
|
|
|
|
|
) {
|
|
|
|
|
throw new Error("이 화면을 수정할 권한이 없습니다.");
|
2025-09-01 11:48:12 +09:00
|
|
|
}
|
|
|
|
|
|
2025-09-01 14:00:31 +09:00
|
|
|
const screen = await prisma.screen_definitions.update({
|
2025-09-01 11:48:12 +09:00
|
|
|
where: { screen_id: screenId },
|
|
|
|
|
data: {
|
|
|
|
|
screen_name: updateData.screenName,
|
|
|
|
|
description: updateData.description,
|
2025-09-01 14:00:31 +09:00
|
|
|
is_active: updateData.isActive ? "Y" : "N",
|
2025-09-01 11:48:12 +09:00
|
|
|
updated_by: updateData.updatedBy,
|
|
|
|
|
updated_date: new Date(),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-01 14:00:31 +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 targetScreen = await prisma.screen_definitions.findUnique({
|
|
|
|
|
where: { screen_id: screenId },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!targetScreen) {
|
|
|
|
|
throw new Error("화면을 찾을 수 없습니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
userCompanyCode !== "*" &&
|
|
|
|
|
targetScreen.company_code !== "*" &&
|
|
|
|
|
targetScreen.company_code !== userCompanyCode
|
|
|
|
|
) {
|
|
|
|
|
throw new Error("이 화면에 접근할 권한이 없습니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 같은 회사의 모든 활성 화면에서 이 화면을 참조하는지 확인
|
|
|
|
|
const whereClause = {
|
|
|
|
|
is_active: { not: "D" },
|
|
|
|
|
...(userCompanyCode !== "*" && {
|
|
|
|
|
company_code: { in: [userCompanyCode, "*"] },
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const allScreens = await prisma.screen_definitions.findMany({
|
|
|
|
|
where: whereClause,
|
|
|
|
|
include: {
|
|
|
|
|
layouts: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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 테이블에서 버튼 컴포넌트 확인
|
|
|
|
|
const buttonLayouts = screen.layouts.filter(
|
|
|
|
|
(layout) => layout.component_type === "widget"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
for (const layout of buttonLayouts) {
|
|
|
|
|
const properties = layout.properties as any;
|
|
|
|
|
|
|
|
|
|
// 버튼 컴포넌트인지 확인
|
|
|
|
|
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: layout.component_id,
|
|
|
|
|
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: layout.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: layout.component_id,
|
|
|
|
|
componentType: "button",
|
|
|
|
|
referenceType: "url",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 기존 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",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(
|
|
|
|
|
`화면 ${screen.screen_id}의 레이아웃 분석 중 오류:`,
|
|
|
|
|
error
|
|
|
|
|
);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 메뉴 할당 확인
|
2025-09-08 14:20:01 +09:00
|
|
|
// 메뉴에 할당된 화면인지 확인 (임시 주석 처리)
|
|
|
|
|
/*
|
2025-09-08 13:10:09 +09:00
|
|
|
const menuAssignments = await prisma.screen_menu_assignments.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
screen_id: screenId,
|
|
|
|
|
is_active: "Y",
|
|
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
menu_info: true, // 메뉴 정보도 함께 조회
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 메뉴에 할당된 경우 의존성에 추가
|
|
|
|
|
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",
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-09-08 14:20:01 +09:00
|
|
|
*/
|
2025-09-08 13:10:09 +09:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
hasDependencies: dependencies.length > 0,
|
|
|
|
|
dependencies,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 화면 정의 삭제 (휴지통으로 이동 - 소프트 삭제)
|
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> {
|
2025-09-01 14:00:31 +09:00
|
|
|
// 권한 확인
|
|
|
|
|
const existingScreen = await prisma.screen_definitions.findUnique({
|
2025-09-01 11:48:12 +09:00
|
|
|
where: { screen_id: screenId },
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-01 14:00:31 +09:00
|
|
|
if (!existingScreen) {
|
2025-09-01 11:48:12 +09:00
|
|
|
throw new Error("화면을 찾을 수 없습니다.");
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 14:00:31 +09:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 트랜잭션으로 화면 삭제와 메뉴 할당 정리를 함께 처리
|
|
|
|
|
await prisma.$transaction(async (tx) => {
|
|
|
|
|
// 소프트 삭제 (휴지통으로 이동)
|
|
|
|
|
await tx.screen_definitions.update({
|
|
|
|
|
where: { screen_id: screenId },
|
|
|
|
|
data: {
|
|
|
|
|
is_active: "D",
|
|
|
|
|
deleted_date: new Date(),
|
|
|
|
|
deleted_by: deletedBy,
|
|
|
|
|
delete_reason: deleteReason,
|
|
|
|
|
updated_date: new Date(),
|
|
|
|
|
updated_by: deletedBy,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 메뉴 할당도 비활성화
|
|
|
|
|
await tx.screen_menu_assignments.updateMany({
|
|
|
|
|
where: {
|
|
|
|
|
screen_id: screenId,
|
|
|
|
|
is_active: "Y",
|
|
|
|
|
},
|
|
|
|
|
data: {
|
|
|
|
|
is_active: "N",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 화면 복원 (휴지통에서 복원)
|
|
|
|
|
*/
|
|
|
|
|
async restoreScreen(
|
|
|
|
|
screenId: number,
|
|
|
|
|
userCompanyCode: string,
|
|
|
|
|
restoredBy: string
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
// 권한 확인
|
|
|
|
|
const existingScreen = await prisma.screen_definitions.findUnique({
|
|
|
|
|
where: { screen_id: screenId },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!existingScreen) {
|
|
|
|
|
throw new Error("화면을 찾을 수 없습니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
userCompanyCode !== "*" &&
|
|
|
|
|
existingScreen.company_code !== userCompanyCode
|
|
|
|
|
) {
|
|
|
|
|
throw new Error("이 화면을 복원할 권한이 없습니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 삭제된 화면이 아닌 경우
|
|
|
|
|
if (existingScreen.is_active !== "D") {
|
|
|
|
|
throw new Error("삭제된 화면이 아닙니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 화면 코드 중복 확인 (복원 시 같은 코드가 이미 존재하는지)
|
|
|
|
|
const duplicateScreen = await prisma.screen_definitions.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
screen_code: existingScreen.screen_code,
|
|
|
|
|
is_active: { not: "D" },
|
|
|
|
|
screen_id: { not: screenId },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (duplicateScreen) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
"같은 화면 코드를 가진 활성 화면이 이미 존재합니다. 복원하려면 기존 화면의 코드를 변경하거나 삭제해주세요."
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 트랜잭션으로 화면 복원과 메뉴 할당 복원을 함께 처리
|
|
|
|
|
await prisma.$transaction(async (tx) => {
|
|
|
|
|
// 화면 복원
|
|
|
|
|
await tx.screen_definitions.update({
|
|
|
|
|
where: { screen_id: screenId },
|
|
|
|
|
data: {
|
|
|
|
|
is_active: "Y",
|
|
|
|
|
deleted_date: null,
|
|
|
|
|
deleted_by: null,
|
|
|
|
|
delete_reason: null,
|
|
|
|
|
updated_date: new Date(),
|
|
|
|
|
updated_by: restoredBy,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 메뉴 할당도 다시 활성화
|
|
|
|
|
await tx.screen_menu_assignments.updateMany({
|
|
|
|
|
where: {
|
|
|
|
|
screen_id: screenId,
|
|
|
|
|
is_active: "N",
|
|
|
|
|
},
|
|
|
|
|
data: {
|
|
|
|
|
is_active: "Y",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 휴지통 화면들의 메뉴 할당 정리 (관리자용)
|
|
|
|
|
*/
|
|
|
|
|
async cleanupDeletedScreenMenuAssignments(): Promise<{
|
|
|
|
|
updatedCount: number;
|
|
|
|
|
message: string;
|
|
|
|
|
}> {
|
|
|
|
|
const result = await prisma.$executeRaw`
|
|
|
|
|
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'
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
updatedCount: Number(result),
|
|
|
|
|
message: `${result}개의 메뉴 할당이 정리되었습니다.`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 화면 영구 삭제 (휴지통에서 완전 삭제)
|
|
|
|
|
*/
|
|
|
|
|
async permanentDeleteScreen(
|
|
|
|
|
screenId: number,
|
|
|
|
|
userCompanyCode: string
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
// 권한 확인
|
|
|
|
|
const existingScreen = await prisma.screen_definitions.findUnique({
|
|
|
|
|
where: { screen_id: screenId },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!existingScreen) {
|
|
|
|
|
throw new Error("화면을 찾을 수 없습니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
userCompanyCode !== "*" &&
|
|
|
|
|
existingScreen.company_code !== userCompanyCode
|
|
|
|
|
) {
|
|
|
|
|
throw new Error("이 화면을 영구 삭제할 권한이 없습니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 삭제된 화면이 아닌 경우 영구 삭제 불가
|
|
|
|
|
if (existingScreen.is_active !== "D") {
|
|
|
|
|
throw new Error("휴지통에 있는 화면만 영구 삭제할 수 있습니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 물리적 삭제 (CASCADE로 관련 레이아웃과 메뉴 할당도 함께 삭제됨)
|
2025-09-01 11:48:12 +09:00
|
|
|
await prisma.screen_definitions.delete({
|
|
|
|
|
where: { screen_id: screenId },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
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 whereClause: any = { is_active: "D" };
|
|
|
|
|
|
|
|
|
|
if (companyCode !== "*") {
|
|
|
|
|
whereClause.company_code = companyCode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [screens, total] = await Promise.all([
|
|
|
|
|
prisma.screen_definitions.findMany({
|
|
|
|
|
where: whereClause,
|
|
|
|
|
skip: (page - 1) * size,
|
|
|
|
|
take: size,
|
|
|
|
|
orderBy: { deleted_date: "desc" },
|
|
|
|
|
}),
|
|
|
|
|
prisma.screen_definitions.count({ where: whereClause }),
|
|
|
|
|
]);
|
|
|
|
|
|
2025-09-08 14:20:01 +09:00
|
|
|
// 테이블 라벨 정보를 한 번에 조회
|
|
|
|
|
const tableNames = [
|
|
|
|
|
...new Set(screens.map((s) => s.table_name).filter(Boolean)),
|
|
|
|
|
];
|
|
|
|
|
const tableLabels = await prisma.table_labels.findMany({
|
|
|
|
|
where: { table_name: { in: tableNames } },
|
|
|
|
|
select: { table_name: true, table_label: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const tableLabelMap = new Map(
|
|
|
|
|
tableLabels.map((tl) => [tl.table_name, tl.table_label || tl.table_name])
|
|
|
|
|
);
|
|
|
|
|
|
2025-09-08 13:10:09 +09:00
|
|
|
return {
|
|
|
|
|
data: screens.map((screen) => ({
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const screensToDelete = await prisma.screen_definitions.findMany({
|
|
|
|
|
where: whereClause,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let deletedCount = 0;
|
|
|
|
|
let skippedCount = 0;
|
|
|
|
|
const errors: Array<{ screenId: number; error: string }> = [];
|
|
|
|
|
|
|
|
|
|
// 각 화면을 개별적으로 삭제 처리
|
|
|
|
|
for (const screenId of screenIds) {
|
|
|
|
|
try {
|
|
|
|
|
const screenToDelete = screensToDelete.find(
|
|
|
|
|
(s) => s.screen_id === screenId
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!screenToDelete) {
|
|
|
|
|
skippedCount++;
|
|
|
|
|
errors.push({
|
|
|
|
|
screenId,
|
|
|
|
|
error: "화면을 찾을 수 없거나 삭제 권한이 없습니다.",
|
|
|
|
|
});
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 관련 레이아웃 데이터도 함께 삭제
|
|
|
|
|
await prisma.$transaction(async (tx) => {
|
|
|
|
|
// screen_layouts 삭제
|
|
|
|
|
await tx.screen_layouts.deleteMany({
|
|
|
|
|
where: { screen_id: screenId },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// screen_menu_assignments 삭제
|
|
|
|
|
await tx.screen_menu_assignments.deleteMany({
|
|
|
|
|
where: { screen_id: screenId },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// screen_definitions 삭제
|
|
|
|
|
await tx.screen_definitions.delete({
|
|
|
|
|
where: { screen_id: screenId },
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
deletedCount++;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
skippedCount++;
|
|
|
|
|
errors.push({
|
|
|
|
|
screenId,
|
|
|
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
|
|
|
|
});
|
|
|
|
|
console.error(`화면 ${screenId} 영구 삭제 실패:`, error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
deletedCount,
|
|
|
|
|
skippedCount,
|
|
|
|
|
errors,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 14:00:31 +09:00
|
|
|
// ========================================
|
|
|
|
|
// 테이블 관리
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-02 11:16:40 +09:00
|
|
|
* 테이블 목록 조회 (모든 테이블)
|
2025-09-01 14:00:31 +09:00
|
|
|
*/
|
|
|
|
|
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("테이블 목록을 조회할 수 없습니다.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-02 11:16:40 +09:00
|
|
|
/**
|
|
|
|
|
* 특정 테이블 정보 조회 (최적화된 단일 테이블 조회)
|
|
|
|
|
*/
|
|
|
|
|
async getTableInfo(
|
|
|
|
|
tableName: string,
|
|
|
|
|
companyCode: string
|
|
|
|
|
): Promise<TableInfo | null> {
|
|
|
|
|
try {
|
|
|
|
|
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}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
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} 정보를 조회할 수 없습니다.`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 14:00:31 +09:00
|
|
|
/**
|
|
|
|
|
* 테이블 컬럼 정보 조회
|
|
|
|
|
*/
|
|
|
|
|
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 {
|
2025-09-19 18:43:55 +09:00
|
|
|
// 통합 타입 매핑에서 import
|
|
|
|
|
const { DB_TYPE_TO_WEB_TYPE } = require("../types/unified-web-types");
|
|
|
|
|
|
2025-09-01 14:00:31 +09:00
|
|
|
const lowerType = dataType.toLowerCase();
|
|
|
|
|
|
2025-09-19 18:43:55 +09:00
|
|
|
// 정확한 매핑 우선 확인
|
|
|
|
|
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";
|
2025-09-01 14:00:31 +09:00
|
|
|
} else if (
|
2025-09-19 18:43:55 +09:00
|
|
|
lowerType.includes("timestamp") ||
|
|
|
|
|
lowerType.includes("datetime")
|
2025-09-01 14:00:31 +09:00
|
|
|
) {
|
2025-09-19 18:43:55 +09:00
|
|
|
return "datetime";
|
|
|
|
|
} else if (lowerType.includes("date")) {
|
2025-09-01 14:00:31 +09:00
|
|
|
return "date";
|
2025-09-19 18:43:55 +09:00
|
|
|
} else if (lowerType.includes("time")) {
|
|
|
|
|
return "datetime";
|
2025-09-01 14:00:31 +09:00
|
|
|
} else if (lowerType.includes("bool")) {
|
|
|
|
|
return "checkbox";
|
2025-09-19 18:43:55 +09:00
|
|
|
} else if (
|
|
|
|
|
lowerType.includes("char") ||
|
|
|
|
|
lowerType.includes("text") ||
|
|
|
|
|
lowerType.includes("varchar")
|
|
|
|
|
) {
|
|
|
|
|
return lowerType.includes("text") ? "textarea" : "text";
|
2025-09-01 14:00:31 +09:00
|
|
|
}
|
2025-09-19 18:43:55 +09:00
|
|
|
|
|
|
|
|
// 기본값
|
|
|
|
|
return "text";
|
2025-09-01 14:00:31 +09:00
|
|
|
}
|
|
|
|
|
|
2025-09-01 11:48:12 +09:00
|
|
|
// ========================================
|
|
|
|
|
// 레이아웃 관리
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 레이아웃 저장
|
|
|
|
|
*/
|
|
|
|
|
async saveLayout(
|
|
|
|
|
screenId: number,
|
2025-09-01 14:00:31 +09:00
|
|
|
layoutData: LayoutData,
|
|
|
|
|
companyCode: string
|
2025-09-01 11:48:12 +09:00
|
|
|
): Promise<void> {
|
2025-09-02 10:33:41 +09:00
|
|
|
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);
|
2025-09-02 10:33:41 +09:00
|
|
|
|
2025-09-01 14:00:31 +09:00
|
|
|
// 권한 확인
|
|
|
|
|
const existingScreen = await prisma.screen_definitions.findUnique({
|
2025-09-01 11:48:12 +09:00
|
|
|
where: { screen_id: screenId },
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-01 14:00:31 +09:00
|
|
|
if (!existingScreen) {
|
2025-09-01 11:48:12 +09:00
|
|
|
throw new Error("화면을 찾을 수 없습니다.");
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 14:00:31 +09:00
|
|
|
if (companyCode !== "*" && existingScreen.company_code !== companyCode) {
|
|
|
|
|
throw new Error("이 화면의 레이아웃을 저장할 권한이 없습니다.");
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-04 17:01:07 +09:00
|
|
|
// 기존 레이아웃 삭제 (컴포넌트와 메타데이터 모두)
|
2025-09-01 11:48:12 +09:00
|
|
|
await prisma.screen_layouts.deleteMany({
|
|
|
|
|
where: { screen_id: screenId },
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-04 17:01:07 +09:00
|
|
|
// 1. 메타데이터 저장 (격자 설정과 해상도 정보)
|
|
|
|
|
if (layoutData.gridSettings || layoutData.screenResolution) {
|
|
|
|
|
const metadata: any = {
|
|
|
|
|
gridSettings: layoutData.gridSettings,
|
|
|
|
|
screenResolution: layoutData.screenResolution,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await prisma.screen_layouts.create({
|
|
|
|
|
data: {
|
|
|
|
|
screen_id: screenId,
|
|
|
|
|
component_type: "_metadata", // 특별한 타입으로 메타데이터 식별
|
|
|
|
|
component_id: `_metadata_${screenId}`,
|
|
|
|
|
parent_id: null,
|
|
|
|
|
position_x: 0,
|
|
|
|
|
position_y: 0,
|
|
|
|
|
width: 0,
|
|
|
|
|
height: 0,
|
|
|
|
|
properties: metadata,
|
|
|
|
|
display_order: -1, // 메타데이터는 맨 앞에 배치
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log(`메타데이터 저장 완료:`, metadata);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 컴포넌트 저장
|
2025-09-01 14:00:31 +09:00
|
|
|
for (const component of layoutData.components) {
|
|
|
|
|
const { id, ...componentData } = component;
|
|
|
|
|
|
2025-09-02 10:33:41 +09:00
|
|
|
console.log(`저장 중인 컴포넌트:`, {
|
|
|
|
|
id: component.id,
|
|
|
|
|
type: component.type,
|
|
|
|
|
position: component.position,
|
|
|
|
|
size: component.size,
|
|
|
|
|
parentId: component.parentId,
|
|
|
|
|
title: (component as any).title,
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-01 14:00:31 +09:00
|
|
|
// Prisma JSON 필드에 맞는 타입으로 변환
|
|
|
|
|
const properties: any = {
|
|
|
|
|
...componentData,
|
|
|
|
|
position: {
|
|
|
|
|
x: component.position.x,
|
|
|
|
|
y: component.position.y,
|
2025-09-02 10:33:41 +09:00
|
|
|
z: component.position.z || 1, // z 값 포함
|
2025-09-01 14:00:31 +09:00
|
|
|
},
|
|
|
|
|
size: {
|
|
|
|
|
width: component.size.width,
|
|
|
|
|
height: component.size.height,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await prisma.screen_layouts.create({
|
2025-09-01 11:48:12 +09:00
|
|
|
data: {
|
|
|
|
|
screen_id: screenId,
|
|
|
|
|
component_type: component.type,
|
|
|
|
|
component_id: component.id,
|
2025-09-01 14:00:31 +09:00
|
|
|
parent_id: component.parentId || null,
|
2025-09-01 11:48:12 +09:00
|
|
|
position_x: component.position.x,
|
|
|
|
|
position_y: component.position.y,
|
|
|
|
|
width: component.size.width,
|
|
|
|
|
height: component.size.height,
|
2025-09-01 14:00:31 +09:00
|
|
|
properties: properties,
|
2025-09-01 11:48:12 +09:00
|
|
|
},
|
2025-09-01 14:00:31 +09:00
|
|
|
});
|
|
|
|
|
}
|
2025-09-02 10:33:41 +09:00
|
|
|
|
|
|
|
|
console.log(`=== 레이아웃 저장 완료 ===`);
|
2025-09-01 11:48:12 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 레이아웃 조회
|
|
|
|
|
*/
|
2025-09-01 14:00:31 +09:00
|
|
|
async getLayout(
|
|
|
|
|
screenId: number,
|
|
|
|
|
companyCode: string
|
|
|
|
|
): Promise<LayoutData | null> {
|
2025-09-02 10:33:41 +09:00
|
|
|
console.log(`=== 레이아웃 로드 시작 ===`);
|
|
|
|
|
console.log(`화면 ID: ${screenId}`);
|
|
|
|
|
|
2025-09-01 14:00:31 +09:00
|
|
|
// 권한 확인
|
|
|
|
|
const existingScreen = await prisma.screen_definitions.findUnique({
|
|
|
|
|
where: { screen_id: screenId },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!existingScreen) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (companyCode !== "*" && existingScreen.company_code !== companyCode) {
|
|
|
|
|
throw new Error("이 화면의 레이아웃을 조회할 권한이 없습니다.");
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 11:48:12 +09:00
|
|
|
const layouts = await prisma.screen_layouts.findMany({
|
|
|
|
|
where: { screen_id: screenId },
|
|
|
|
|
orderBy: { display_order: "asc" },
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-02 10:33:41 +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) {
|
2025-09-01 14:00:31 +09:00
|
|
|
return {
|
|
|
|
|
components: [],
|
2025-09-04 17:01:07 +09:00
|
|
|
gridSettings,
|
|
|
|
|
screenResolution,
|
2025-09-01 14:00:31 +09:00
|
|
|
};
|
2025-09-01 11:48:12 +09:00
|
|
|
}
|
|
|
|
|
|
2025-09-04 17:01:07 +09:00
|
|
|
const components: ComponentData[] = componentLayouts.map((layout) => {
|
2025-09-01 14:00:31 +09:00
|
|
|
const properties = layout.properties as any;
|
2025-09-02 10:33:41 +09:00
|
|
|
const component = {
|
2025-09-01 11:48:12 +09:00
|
|
|
id: layout.component_id,
|
|
|
|
|
type: layout.component_type as any,
|
2025-09-02 10:33:41 +09:00
|
|
|
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 },
|
2025-09-01 14:00:31 +09:00
|
|
|
parentId: layout.parent_id,
|
|
|
|
|
...properties,
|
2025-09-01 11:48:12 +09:00
|
|
|
};
|
2025-09-02 10:33:41 +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
|
|
|
});
|
|
|
|
|
|
2025-09-02 10:33:41 +09:00
|
|
|
console.log(`=== 레이아웃 로드 완료 ===`);
|
|
|
|
|
console.log(`반환할 컴포넌트 수: ${components.length}`);
|
2025-09-04 17:01:07 +09:00
|
|
|
console.log(`최종 격자 설정:`, gridSettings);
|
|
|
|
|
console.log(`최종 해상도 설정:`, screenResolution);
|
2025-09-02 10:33:41 +09:00
|
|
|
|
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
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
// 템플릿 관리
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 템플릿 목록 조회 (회사별)
|
|
|
|
|
*/
|
|
|
|
|
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)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 18:42:59 +09:00
|
|
|
/**
|
|
|
|
|
* 화면-메뉴 할당 해제
|
|
|
|
|
*/
|
|
|
|
|
async unassignScreenFromMenu(
|
|
|
|
|
screenId: number,
|
|
|
|
|
menuObjid: number,
|
|
|
|
|
companyCode: string
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
await prisma.screen_menu_assignments.deleteMany({
|
|
|
|
|
where: {
|
|
|
|
|
screen_id: screenId,
|
|
|
|
|
menu_objid: menuObjid,
|
|
|
|
|
company_code: companyCode,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 11:48:12 +09:00
|
|
|
// ========================================
|
|
|
|
|
// 테이블 타입 연계
|
|
|
|
|
// ========================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 컬럼 정보 조회 (웹 타입 포함)
|
|
|
|
|
*/
|
|
|
|
|
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,
|
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 = ${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",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
|
// 유틸리티 메서드
|
|
|
|
|
// ========================================
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 화면 코드 자동 생성 (회사코드 + '_' + 순번)
|
|
|
|
|
*/
|
|
|
|
|
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" },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 회사 코드 뒤의 숫자 부분 추출하여 최대값 찾기
|
|
|
|
|
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
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 화면 복사 (화면 정보 + 레이아웃 모두 복사)
|
|
|
|
|
*/
|
|
|
|
|
async copyScreen(
|
|
|
|
|
sourceScreenId: number,
|
|
|
|
|
copyData: CopyScreenRequest
|
|
|
|
|
): Promise<ScreenDefinition> {
|
|
|
|
|
// 트랜잭션으로 처리
|
|
|
|
|
return await prisma.$transaction(async (tx) => {
|
|
|
|
|
// 1. 원본 화면 정보 조회
|
|
|
|
|
const sourceScreen = await tx.screen_definitions.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
screen_id: sourceScreenId,
|
|
|
|
|
company_code: copyData.companyCode,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!sourceScreen) {
|
|
|
|
|
throw new Error("복사할 화면을 찾을 수 없습니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 화면 코드 중복 체크
|
|
|
|
|
const existingScreen = await tx.screen_definitions.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
screen_code: copyData.screenCode,
|
|
|
|
|
company_code: copyData.companyCode,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (existingScreen) {
|
|
|
|
|
throw new Error("이미 존재하는 화면 코드입니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. 새 화면 생성
|
|
|
|
|
const newScreen = await tx.screen_definitions.create({
|
|
|
|
|
data: {
|
|
|
|
|
screen_code: copyData.screenCode,
|
|
|
|
|
screen_name: copyData.screenName,
|
|
|
|
|
description: copyData.description || sourceScreen.description,
|
|
|
|
|
company_code: copyData.companyCode,
|
|
|
|
|
table_name: sourceScreen.table_name,
|
|
|
|
|
is_active: sourceScreen.is_active,
|
|
|
|
|
created_by: copyData.createdBy,
|
|
|
|
|
created_date: new Date(),
|
|
|
|
|
updated_by: copyData.createdBy,
|
|
|
|
|
updated_date: new Date(),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 4. 원본 화면의 레이아웃 정보 조회
|
|
|
|
|
const sourceLayouts = await tx.screen_layouts.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
screen_id: sourceScreenId,
|
|
|
|
|
},
|
|
|
|
|
orderBy: { display_order: "asc" },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 5. 레이아웃이 있다면 복사
|
|
|
|
|
if (sourceLayouts.length > 0) {
|
|
|
|
|
try {
|
|
|
|
|
// ID 매핑 맵 생성
|
|
|
|
|
const idMapping: { [oldId: string]: string } = {};
|
|
|
|
|
|
|
|
|
|
// 새로운 컴포넌트 ID 미리 생성
|
|
|
|
|
sourceLayouts.forEach((layout) => {
|
|
|
|
|
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 tx.screen_layouts.create({
|
|
|
|
|
data: {
|
|
|
|
|
screen_id: newScreen.screen_id,
|
|
|
|
|
component_type: sourceLayout.component_type,
|
|
|
|
|
component_id: newComponentId,
|
|
|
|
|
parent_id: newParentId,
|
|
|
|
|
position_x: sourceLayout.position_x,
|
|
|
|
|
position_y: sourceLayout.position_y,
|
|
|
|
|
width: sourceLayout.width,
|
|
|
|
|
height: sourceLayout.height,
|
|
|
|
|
properties: sourceLayout.properties as any,
|
|
|
|
|
display_order: sourceLayout.display_order,
|
|
|
|
|
created_date: new Date(),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} 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
|
|
|
}
|
2025-09-01 14:00:31 +09:00
|
|
|
|
|
|
|
|
// 서비스 인스턴스 export
|
|
|
|
|
export const screenManagementService = new ScreenManagementService();
|