425 lines
12 KiB
TypeScript
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();
|