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

425 lines
12 KiB
TypeScript

import { PrismaClient } from "@prisma/client";
import prisma from "../config/database";
import {
CreateLayoutRequest,
UpdateLayoutRequest,
LayoutStandard,
LayoutType,
LayoutCategory,
} from "../types/layout";
// JSON 데이터를 안전하게 파싱하는 헬퍼 함수
function safeJSONParse(data: any): any {
if (data === null || data === undefined) {
return null;
}
// 이미 객체인 경우 그대로 반환
if (typeof data === "object") {
return data;
}
// 문자열인 경우 파싱 시도
if (typeof data === "string") {
try {
return JSON.parse(data);
} catch (error) {
console.error("JSON 파싱 오류:", error, "Data:", data);
return null;
}
}
return data;
}
// JSON 데이터를 안전하게 문자열화하는 헬퍼 함수
function safeJSONStringify(data: any): string | null {
if (data === null || data === undefined) {
return null;
}
// 이미 문자열인 경우 그대로 반환
if (typeof data === "string") {
return data;
}
// 객체인 경우 문자열로 변환
try {
return JSON.stringify(data);
} catch (error) {
console.error("JSON 문자열화 오류:", error, "Data:", data);
return null;
}
}
export class LayoutService {
/**
* 레이아웃 목록 조회
*/
async getLayouts(params: {
page?: number;
size?: number;
category?: string;
layoutType?: string;
searchTerm?: string;
companyCode: string;
includePublic?: boolean;
}): Promise<{ data: LayoutStandard[]; total: number }> {
const {
page = 1,
size = 20,
category,
layoutType,
searchTerm,
companyCode,
includePublic = true,
} = params;
const skip = (page - 1) * size;
// 검색 조건 구성
const where: any = {
is_active: "Y",
OR: [
{ company_code: companyCode },
...(includePublic ? [{ is_public: "Y" }] : []),
],
};
if (category) {
where.category = category;
}
if (layoutType) {
where.layout_type = layoutType;
}
if (searchTerm) {
where.OR = [
...where.OR,
{ layout_name: { contains: searchTerm, mode: "insensitive" } },
{ layout_name_eng: { contains: searchTerm, mode: "insensitive" } },
{ description: { contains: searchTerm, mode: "insensitive" } },
];
}
const [data, total] = await Promise.all([
prisma.layout_standards.findMany({
where,
skip,
take: size,
orderBy: [{ sort_order: "asc" }, { created_date: "desc" }],
}),
prisma.layout_standards.count({ where }),
]);
return {
data: data.map(
(layout) =>
({
layoutCode: layout.layout_code,
layoutName: layout.layout_name,
layoutNameEng: layout.layout_name_eng,
description: layout.description,
layoutType: layout.layout_type as LayoutType,
category: layout.category as LayoutCategory,
iconName: layout.icon_name,
defaultSize: safeJSONParse(layout.default_size),
layoutConfig: safeJSONParse(layout.layout_config),
zonesConfig: safeJSONParse(layout.zones_config),
previewImage: layout.preview_image,
sortOrder: layout.sort_order,
isActive: layout.is_active,
isPublic: layout.is_public,
companyCode: layout.company_code,
createdDate: layout.created_date,
createdBy: layout.created_by,
updatedDate: layout.updated_date,
updatedBy: layout.updated_by,
}) as LayoutStandard
),
total,
};
}
/**
* 레이아웃 상세 조회
*/
async getLayoutById(
layoutCode: string,
companyCode: string
): Promise<LayoutStandard | null> {
const layout = await prisma.layout_standards.findFirst({
where: {
layout_code: layoutCode,
is_active: "Y",
OR: [{ company_code: companyCode }, { is_public: "Y" }],
},
});
if (!layout) return null;
return {
layoutCode: layout.layout_code,
layoutName: layout.layout_name,
layoutNameEng: layout.layout_name_eng,
description: layout.description,
layoutType: layout.layout_type as LayoutType,
category: layout.category as LayoutCategory,
iconName: layout.icon_name,
defaultSize: safeJSONParse(layout.default_size),
layoutConfig: safeJSONParse(layout.layout_config),
zonesConfig: safeJSONParse(layout.zones_config),
previewImage: layout.preview_image,
sortOrder: layout.sort_order,
isActive: layout.is_active,
isPublic: layout.is_public,
companyCode: layout.company_code,
createdDate: layout.created_date,
createdBy: layout.created_by,
updatedDate: layout.updated_date,
updatedBy: layout.updated_by,
} as LayoutStandard;
}
/**
* 레이아웃 생성
*/
async createLayout(
request: CreateLayoutRequest,
companyCode: string,
userId: string
): Promise<LayoutStandard> {
// 레이아웃 코드 생성 (자동)
const layoutCode = await this.generateLayoutCode(
request.layoutType,
companyCode
);
const layout = await prisma.layout_standards.create({
data: {
layout_code: layoutCode,
layout_name: request.layoutName,
layout_name_eng: request.layoutNameEng,
description: request.description,
layout_type: request.layoutType,
category: request.category,
icon_name: request.iconName,
default_size: safeJSONStringify(request.defaultSize) as any,
layout_config: safeJSONStringify(request.layoutConfig) as any,
zones_config: safeJSONStringify(request.zonesConfig) as any,
is_public: request.isPublic ? "Y" : "N",
company_code: companyCode,
created_by: userId,
updated_by: userId,
},
});
return this.mapToLayoutStandard(layout);
}
/**
* 레이아웃 수정
*/
async updateLayout(
request: UpdateLayoutRequest,
companyCode: string,
userId: string
): Promise<LayoutStandard | null> {
// 수정 권한 확인
const existing = await prisma.layout_standards.findFirst({
where: {
layout_code: request.layoutCode,
company_code: companyCode,
is_active: "Y",
},
});
if (!existing) {
throw new Error("레이아웃을 찾을 수 없거나 수정 권한이 없습니다.");
}
const updateData: any = {
updated_by: userId,
updated_date: new Date(),
};
// 수정할 필드만 업데이트
if (request.layoutName !== undefined)
updateData.layout_name = request.layoutName;
if (request.layoutNameEng !== undefined)
updateData.layout_name_eng = request.layoutNameEng;
if (request.description !== undefined)
updateData.description = request.description;
if (request.layoutType !== undefined)
updateData.layout_type = request.layoutType;
if (request.category !== undefined) updateData.category = request.category;
if (request.iconName !== undefined) updateData.icon_name = request.iconName;
if (request.defaultSize !== undefined)
updateData.default_size = safeJSONStringify(request.defaultSize) as any;
if (request.layoutConfig !== undefined)
updateData.layout_config = safeJSONStringify(request.layoutConfig) as any;
if (request.zonesConfig !== undefined)
updateData.zones_config = safeJSONStringify(request.zonesConfig) as any;
if (request.isPublic !== undefined)
updateData.is_public = request.isPublic ? "Y" : "N";
const updated = await prisma.layout_standards.update({
where: { layout_code: request.layoutCode },
data: updateData,
});
return this.mapToLayoutStandard(updated);
}
/**
* 레이아웃 삭제 (소프트 삭제)
*/
async deleteLayout(
layoutCode: string,
companyCode: string,
userId: string
): Promise<boolean> {
const existing = await prisma.layout_standards.findFirst({
where: {
layout_code: layoutCode,
company_code: companyCode,
is_active: "Y",
},
});
if (!existing) {
throw new Error("레이아웃을 찾을 수 없거나 삭제 권한이 없습니다.");
}
await prisma.layout_standards.update({
where: { layout_code: layoutCode },
data: {
is_active: "N",
updated_by: userId,
updated_date: new Date(),
},
});
return true;
}
/**
* 레이아웃 복제
*/
async duplicateLayout(
layoutCode: string,
newName: string,
companyCode: string,
userId: string
): Promise<LayoutStandard> {
const original = await this.getLayoutById(layoutCode, companyCode);
if (!original) {
throw new Error("복제할 레이아웃을 찾을 수 없습니다.");
}
const duplicateRequest: CreateLayoutRequest = {
layoutName: newName,
layoutNameEng: original.layoutNameEng
? `${original.layoutNameEng} Copy`
: undefined,
description: original.description,
layoutType: original.layoutType,
category: original.category,
iconName: original.iconName,
defaultSize: original.defaultSize,
layoutConfig: original.layoutConfig,
zonesConfig: original.zonesConfig,
isPublic: false, // 복제본은 비공개로 시작
};
return this.createLayout(duplicateRequest, companyCode, userId);
}
/**
* 카테고리별 레이아웃 개수 조회
*/
async getLayoutCountsByCategory(
companyCode: string
): Promise<Record<string, number>> {
const counts = await prisma.layout_standards.groupBy({
by: ["category"],
_count: {
layout_code: true,
},
where: {
is_active: "Y",
OR: [{ company_code: companyCode }, { is_public: "Y" }],
},
});
return counts.reduce(
(acc: Record<string, number>, item: any) => {
acc[item.category] = item._count.layout_code;
return acc;
},
{} as Record<string, number>
);
}
/**
* 레이아웃 코드 자동 생성
*/
private async generateLayoutCode(
layoutType: string,
companyCode: string
): Promise<string> {
const prefix = `${layoutType.toUpperCase()}_${companyCode}`;
const existingCodes = await prisma.layout_standards.findMany({
where: {
layout_code: {
startsWith: prefix,
},
},
select: {
layout_code: true,
},
});
const maxNumber = existingCodes.reduce((max: number, item: any) => {
const match = item.layout_code.match(/_(\d+)$/);
if (match) {
const number = parseInt(match[1], 10);
return Math.max(max, number);
}
return max;
}, 0);
return `${prefix}_${String(maxNumber + 1).padStart(3, "0")}`;
}
/**
* 데이터베이스 모델을 LayoutStandard 타입으로 변환
*/
private mapToLayoutStandard(layout: any): LayoutStandard {
return {
layoutCode: layout.layout_code,
layoutName: layout.layout_name,
layoutNameEng: layout.layout_name_eng,
description: layout.description,
layoutType: layout.layout_type,
category: layout.category,
iconName: layout.icon_name,
defaultSize: layout.default_size,
layoutConfig: layout.layout_config,
zonesConfig: layout.zones_config,
previewImage: layout.preview_image,
sortOrder: layout.sort_order,
isActive: layout.is_active,
isPublic: layout.is_public,
companyCode: layout.company_code,
createdDate: layout.created_date,
createdBy: layout.created_by,
updatedDate: layout.updated_date,
updatedBy: layout.updated_by,
};
}
}
export const layoutService = new LayoutService();