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

458 lines
13 KiB
TypeScript

import { query, queryOne } from "../database/db";
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;
// 동적 WHERE 조건 구성
const whereConditions: string[] = ["is_active = $1"];
const values: any[] = ["Y"];
let paramIndex = 2;
// company_code OR is_public 조건
if (includePublic) {
whereConditions.push(
`(company_code = $${paramIndex} OR is_public = $${paramIndex + 1})`
);
values.push(companyCode, "Y");
paramIndex += 2;
} else {
whereConditions.push(`company_code = $${paramIndex++}`);
values.push(companyCode);
}
if (category) {
whereConditions.push(`category = $${paramIndex++}`);
values.push(category);
}
if (layoutType) {
whereConditions.push(`layout_type = $${paramIndex++}`);
values.push(layoutType);
}
if (searchTerm) {
whereConditions.push(
`(layout_name ILIKE $${paramIndex} OR layout_name_eng ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`
);
values.push(`%${searchTerm}%`);
paramIndex++;
}
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
const [data, countResult] = await Promise.all([
query<any>(
`SELECT * FROM layout_standards
${whereClause}
ORDER BY sort_order ASC, created_date DESC
LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
[...values, size, skip]
),
queryOne<{ count: string }>(
`SELECT COUNT(*) as count FROM layout_standards ${whereClause}`,
values
),
]);
const total = parseInt(countResult?.count || "0");
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 queryOne<any>(
`SELECT * FROM layout_standards
WHERE layout_code = $1 AND is_active = $2
AND (company_code = $3 OR is_public = $4)
LIMIT 1`,
[layoutCode, "Y", companyCode, "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 queryOne<any>(
`INSERT INTO layout_standards
(layout_code, layout_name, layout_name_eng, description, layout_type, category,
icon_name, default_size, layout_config, zones_config, is_public, is_active,
company_code, created_by, updated_by, created_date, updated_date, sort_order)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW(), NOW(), 0)
RETURNING *`,
[
layoutCode,
request.layoutName,
request.layoutNameEng,
request.description,
request.layoutType,
request.category,
request.iconName,
safeJSONStringify(request.defaultSize),
safeJSONStringify(request.layoutConfig),
safeJSONStringify(request.zonesConfig),
request.isPublic ? "Y" : "N",
"Y",
companyCode,
userId,
userId,
]
);
return this.mapToLayoutStandard(layout);
}
/**
* 레이아웃 수정
*/
async updateLayout(
request: UpdateLayoutRequest,
companyCode: string,
userId: string
): Promise<LayoutStandard | null> {
// 수정 권한 확인
const existing = await queryOne<any>(
`SELECT * FROM layout_standards
WHERE layout_code = $1 AND company_code = $2 AND is_active = $3`,
[request.layoutCode, companyCode, "Y"]
);
if (!existing) {
throw new Error("레이아웃을 찾을 수 없거나 수정 권한이 없습니다.");
}
// 동적 UPDATE 쿼리 생성
const updateFields: string[] = ["updated_by = $1", "updated_date = NOW()"];
const values: any[] = [userId];
let paramIndex = 2;
if (request.layoutName !== undefined) {
updateFields.push(`layout_name = $${paramIndex++}`);
values.push(request.layoutName);
}
if (request.layoutNameEng !== undefined) {
updateFields.push(`layout_name_eng = $${paramIndex++}`);
values.push(request.layoutNameEng);
}
if (request.description !== undefined) {
updateFields.push(`description = $${paramIndex++}`);
values.push(request.description);
}
if (request.layoutType !== undefined) {
updateFields.push(`layout_type = $${paramIndex++}`);
values.push(request.layoutType);
}
if (request.category !== undefined) {
updateFields.push(`category = $${paramIndex++}`);
values.push(request.category);
}
if (request.iconName !== undefined) {
updateFields.push(`icon_name = $${paramIndex++}`);
values.push(request.iconName);
}
if (request.defaultSize !== undefined) {
updateFields.push(`default_size = $${paramIndex++}`);
values.push(safeJSONStringify(request.defaultSize));
}
if (request.layoutConfig !== undefined) {
updateFields.push(`layout_config = $${paramIndex++}`);
values.push(safeJSONStringify(request.layoutConfig));
}
if (request.zonesConfig !== undefined) {
updateFields.push(`zones_config = $${paramIndex++}`);
values.push(safeJSONStringify(request.zonesConfig));
}
if (request.isPublic !== undefined) {
updateFields.push(`is_public = $${paramIndex++}`);
values.push(request.isPublic ? "Y" : "N");
}
const updated = await queryOne<any>(
`UPDATE layout_standards
SET ${updateFields.join(", ")}
WHERE layout_code = $${paramIndex}
RETURNING *`,
[...values, request.layoutCode]
);
return this.mapToLayoutStandard(updated);
}
/**
* 레이아웃 삭제 (소프트 삭제)
*/
async deleteLayout(
layoutCode: string,
companyCode: string,
userId: string
): Promise<boolean> {
const existing = await queryOne<any>(
`SELECT * FROM layout_standards
WHERE layout_code = $1 AND company_code = $2 AND is_active = $3`,
[layoutCode, companyCode, "Y"]
);
if (!existing) {
throw new Error("레이아웃을 찾을 수 없거나 삭제 권한이 없습니다.");
}
await query(
`UPDATE layout_standards
SET is_active = $1, updated_by = $2, updated_date = NOW()
WHERE layout_code = $3`,
["N", userId, layoutCode]
);
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 query<{ category: string; count: string }>(
`SELECT category, COUNT(*) as count
FROM layout_standards
WHERE is_active = $1 AND (company_code = $2 OR is_public = $3)
GROUP BY category`,
["Y", companyCode, "Y"]
);
return counts.reduce(
(acc: Record<string, number>, item: any) => {
acc[item.category] = parseInt(item.count);
return acc;
},
{} as Record<string, number>
);
}
/**
* 레이아웃 코드 자동 생성
*/
private async generateLayoutCode(
layoutType: string,
companyCode: string
): Promise<string> {
const prefix = `${layoutType.toUpperCase()}_${companyCode}`;
const existingCodes = await query<{ layout_code: string }>(
`SELECT layout_code FROM layout_standards
WHERE layout_code LIKE $1`,
[`${prefix}%`]
);
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();