458 lines
13 KiB
TypeScript
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();
|