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

436 lines
12 KiB
TypeScript

import { query, queryOne, transaction } from "../database/db";
export interface ComponentStandardData {
component_code: string;
component_name: string;
component_name_eng?: string;
description?: string;
category: string;
icon_name?: string;
default_size?: any;
component_config: any;
preview_image?: string;
sort_order?: number;
is_active?: string;
is_public?: string;
company_code: string;
created_by?: string;
updated_by?: string;
}
export interface ComponentQueryParams {
category?: string;
active?: string;
is_public?: string;
company_code?: string;
search?: string;
sort?: string;
order?: "asc" | "desc";
limit?: number;
offset?: number;
}
class ComponentStandardService {
/**
* 컴포넌트 목록 조회
*/
async getComponents(params: ComponentQueryParams = {}) {
const {
category,
active = "Y",
is_public,
company_code,
search,
sort = "sort_order",
order = "asc",
limit,
offset = 0,
} = params;
const whereConditions: string[] = [];
const values: any[] = [];
let paramIndex = 1;
// 활성화 상태 필터
if (active) {
whereConditions.push(`is_active = $${paramIndex++}`);
values.push(active);
}
// 카테고리 필터
if (category && category !== "all") {
whereConditions.push(`category = $${paramIndex++}`);
values.push(category);
}
// 공개 여부 필터
if (is_public) {
whereConditions.push(`is_public = $${paramIndex++}`);
values.push(is_public);
}
// 회사별 필터 (공개 컴포넌트 + 해당 회사 컴포넌트)
if (company_code) {
whereConditions.push(
`(is_public = 'Y' OR company_code = $${paramIndex++})`
);
values.push(company_code);
}
// 검색 조건
if (search) {
whereConditions.push(
`(component_name ILIKE $${paramIndex} OR component_name_eng ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`
);
values.push(`%${search}%`);
paramIndex++;
}
const whereClause =
whereConditions.length > 0
? `WHERE ${whereConditions.join(" AND ")}`
: "";
// 정렬 컬럼 검증 (SQL 인젝션 방지)
const validSortColumns = [
"sort_order",
"component_name",
"category",
"created_date",
"updated_date",
];
const sortColumn = validSortColumns.includes(sort) ? sort : "sort_order";
const sortOrder = order === "desc" ? "DESC" : "ASC";
// 컴포넌트 조회
const components = await query<any>(
`SELECT * FROM component_standards
${whereClause}
ORDER BY ${sortColumn} ${sortOrder}
${limit ? `LIMIT $${paramIndex++}` : ""}
${limit ? `OFFSET $${paramIndex++}` : ""}`,
limit ? [...values, limit, offset] : values
);
// 전체 개수 조회
const countResult = await queryOne<{ count: string }>(
`SELECT COUNT(*) as count FROM component_standards ${whereClause}`,
values
);
const total = parseInt(countResult?.count || "0");
return {
components,
total,
limit,
offset,
};
}
/**
* 컴포넌트 상세 조회
*/
async getComponent(component_code: string) {
const component = await queryOne<any>(
`SELECT * FROM component_standards WHERE component_code = $1`,
[component_code]
);
if (!component) {
throw new Error(`컴포넌트를 찾을 수 없습니다: ${component_code}`);
}
return component;
}
/**
* 컴포넌트 생성
*/
async createComponent(data: ComponentStandardData) {
// 중복 코드 확인
const existing = await queryOne<any>(
`SELECT * FROM component_standards WHERE component_code = $1`,
[data.component_code]
);
if (existing) {
throw new Error(
`이미 존재하는 컴포넌트 코드입니다: ${data.component_code}`
);
}
// 'active' 필드를 'is_active'로 변환
const createData = { ...data };
if ("active" in createData) {
createData.is_active = (createData as any).active;
delete (createData as any).active;
}
const component = await queryOne<any>(
`INSERT INTO component_standards
(component_code, component_name, component_name_eng, description, category,
icon_name, default_size, component_config, preview_image, sort_order,
is_active, is_public, company_code, created_by, updated_by, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW(), NOW())
RETURNING *`,
[
createData.component_code,
createData.component_name,
createData.component_name_eng || null,
createData.description || null,
createData.category,
createData.icon_name || null,
createData.default_size || null,
createData.component_config,
createData.preview_image || null,
createData.sort_order || 0,
createData.is_active || "Y",
createData.is_public || "N",
createData.company_code,
createData.created_by || null,
createData.updated_by || null,
]
);
return component;
}
/**
* 컴포넌트 수정
*/
async updateComponent(
component_code: string,
data: Partial<ComponentStandardData>
) {
const existing = await this.getComponent(component_code);
// 'active' 필드를 'is_active'로 변환
const updateData = { ...data };
if ("active" in updateData) {
updateData.is_active = (updateData as any).active;
delete (updateData as any).active;
}
// 동적 UPDATE 쿼리 생성
const updateFields: string[] = ["updated_date = NOW()"];
const values: any[] = [];
let paramIndex = 1;
const fieldMapping: { [key: string]: string } = {
component_name: "component_name",
component_name_eng: "component_name_eng",
description: "description",
category: "category",
icon_name: "icon_name",
default_size: "default_size",
component_config: "component_config",
preview_image: "preview_image",
sort_order: "sort_order",
is_active: "is_active",
is_public: "is_public",
company_code: "company_code",
updated_by: "updated_by",
};
for (const [key, dbField] of Object.entries(fieldMapping)) {
if (key in updateData) {
updateFields.push(`${dbField} = $${paramIndex++}`);
values.push((updateData as any)[key]);
}
}
const component = await queryOne<any>(
`UPDATE component_standards
SET ${updateFields.join(", ")}
WHERE component_code = $${paramIndex}
RETURNING *`,
[...values, component_code]
);
return component;
}
/**
* 컴포넌트 삭제
*/
async deleteComponent(component_code: string) {
const existing = await this.getComponent(component_code);
await query(`DELETE FROM component_standards WHERE component_code = $1`, [
component_code,
]);
return { message: `컴포넌트가 삭제되었습니다: ${component_code}` };
}
/**
* 컴포넌트 정렬 순서 업데이트
*/
async updateSortOrder(
updates: Array<{ component_code: string; sort_order: number }>
) {
await transaction(async (client) => {
for (const { component_code, sort_order } of updates) {
await client.query(
`UPDATE component_standards
SET sort_order = $1, updated_date = NOW()
WHERE component_code = $2`,
[sort_order, component_code]
);
}
});
return { message: "정렬 순서가 업데이트되었습니다." };
}
/**
* 컴포넌트 복제
*/
async duplicateComponent(
source_code: string,
new_code: string,
new_name: string
) {
const source = await this.getComponent(source_code);
// 새 코드 중복 확인
const existing = await queryOne<any>(
`SELECT * FROM component_standards WHERE component_code = $1`,
[new_code]
);
if (existing) {
throw new Error(`이미 존재하는 컴포넌트 코드입니다: ${new_code}`);
}
const component = await queryOne<any>(
`INSERT INTO component_standards
(component_code, component_name, component_name_eng, description, category,
icon_name, default_size, component_config, preview_image, sort_order,
is_active, is_public, company_code, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, NOW(), NOW())
RETURNING *`,
[
new_code,
new_name,
source?.component_name_eng,
source?.description,
source?.category,
source?.icon_name,
source?.default_size,
source?.component_config,
source?.preview_image,
source?.sort_order,
source?.is_active,
source?.is_public,
source?.company_code || "DEFAULT",
]
);
return component;
}
/**
* 카테고리 목록 조회
*/
async getCategories(company_code?: string) {
const whereConditions: string[] = ["is_active = 'Y'"];
const values: any[] = [];
if (company_code) {
whereConditions.push(`(is_public = 'Y' OR company_code = $1)`);
values.push(company_code);
}
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
const categories = await query<{ category: string }>(
`SELECT DISTINCT category FROM component_standards ${whereClause} ORDER BY category`,
values
);
return categories
.map((item) => item.category)
.filter((category) => category !== null);
}
/**
* 컴포넌트 통계
*/
async getStatistics(company_code?: string) {
const whereConditions: string[] = ["is_active = 'Y'"];
const values: any[] = [];
if (company_code) {
whereConditions.push(`(is_public = 'Y' OR company_code = $1)`);
values.push(company_code);
}
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
// 전체 개수
const totalResult = await queryOne<{ count: string }>(
`SELECT COUNT(*) as count FROM component_standards ${whereClause}`,
values
);
const total = parseInt(totalResult?.count || "0");
// 카테고리별 집계
const byCategory = await query<{ category: string; count: string }>(
`SELECT category, COUNT(*) as count
FROM component_standards
${whereClause}
GROUP BY category`,
values
);
// 상태별 집계
const byStatus = await query<{ is_active: string; count: string }>(
`SELECT is_active, COUNT(*) as count
FROM component_standards
GROUP BY is_active`
);
return {
total,
byCategory: byCategory.map((item) => ({
category: item.category,
count: parseInt(item.count),
})),
byStatus: byStatus.map((item) => ({
status: item.is_active,
count: parseInt(item.count),
})),
};
}
/**
* 컴포넌트 코드 중복 체크
*/
async checkDuplicate(
component_code: string,
company_code?: string
): Promise<boolean> {
const whereConditions: string[] = ["component_code = $1"];
const values: any[] = [component_code];
// 회사 코드가 있고 "*"가 아닌 경우에만 조건 추가
if (company_code && company_code !== "*") {
whereConditions.push("company_code = $2");
values.push(company_code);
}
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
const existingComponent = await queryOne<any>(
`SELECT * FROM component_standards ${whereClause} LIMIT 1`,
values
);
return !!existingComponent;
}
}
export default new ComponentStandardService();