레이아웃 추가기능
This commit is contained in:
425
backend-node/src/services/layoutService.ts
Normal file
425
backend-node/src/services/layoutService.ts
Normal file
@@ -0,0 +1,425 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import {
|
||||
CreateLayoutRequest,
|
||||
UpdateLayoutRequest,
|
||||
LayoutStandard,
|
||||
LayoutType,
|
||||
LayoutCategory,
|
||||
} from "../types/layout";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 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();
|
||||
Reference in New Issue
Block a user