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

674 lines
18 KiB
TypeScript

import { query, queryOne, transaction } from "../database/db";
import { logger } from "../utils/logger";
export interface CodeCategory {
category_code: string;
category_name: string;
category_name_eng?: string | null;
description?: string | null;
sort_order: number;
is_active: string;
created_date?: Date | null;
created_by?: string | null;
updated_date?: Date | null;
updated_by?: string | null;
}
export interface CodeInfo {
code_category: string;
code_value: string;
code_name: string;
code_name_eng?: string | null;
description?: string | null;
sort_order: number;
is_active: string;
created_date?: Date | null;
created_by?: string | null;
updated_date?: Date | null;
updated_by?: string | null;
}
export interface GetCategoriesParams {
search?: string;
isActive?: boolean;
page?: number;
size?: number;
}
export interface GetCodesParams {
search?: string;
isActive?: boolean;
page?: number;
size?: number;
}
export interface CreateCategoryData {
categoryCode: string;
categoryName: string;
categoryNameEng?: string;
description?: string;
sortOrder?: number;
isActive?: string;
}
export interface CreateCodeData {
codeValue: string;
codeName: string;
codeNameEng?: string;
description?: string;
sortOrder?: number;
isActive?: string;
}
export class CommonCodeService {
/**
* 카테고리 목록 조회
*/
async getCategories(params: GetCategoriesParams) {
try {
const { search, isActive, page = 1, size = 20 } = params;
const whereConditions: string[] = [];
const values: any[] = [];
let paramIndex = 1;
if (search) {
whereConditions.push(
`(category_name ILIKE $${paramIndex} OR category_code ILIKE $${paramIndex})`
);
values.push(`%${search}%`);
paramIndex++;
}
if (isActive !== undefined) {
whereConditions.push(`is_active = $${paramIndex++}`);
values.push(isActive ? "Y" : "N");
}
const whereClause =
whereConditions.length > 0
? `WHERE ${whereConditions.join(" AND ")}`
: "";
const offset = (page - 1) * size;
// 카테고리 조회
const categories = await query<CodeCategory>(
`SELECT * FROM code_category
${whereClause}
ORDER BY sort_order ASC, category_code ASC
LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
[...values, size, offset]
);
// 전체 개수 조회
const countResult = await queryOne<{ count: string }>(
`SELECT COUNT(*) as count FROM code_category ${whereClause}`,
values
);
const total = parseInt(countResult?.count || "0");
logger.info(
`카테고리 조회 완료: ${categories.length}개, 전체: ${total}`
);
return {
data: categories,
total,
};
} catch (error) {
logger.error("카테고리 조회 중 오류:", error);
throw error;
}
}
/**
* 카테고리별 코드 목록 조회
*/
async getCodes(categoryCode: string, params: GetCodesParams) {
try {
const { search, isActive, page = 1, size = 20 } = params;
const whereConditions: string[] = ["code_category = $1"];
const values: any[] = [categoryCode];
let paramIndex = 2;
if (search) {
whereConditions.push(
`(code_name ILIKE $${paramIndex} OR code_value ILIKE $${paramIndex})`
);
values.push(`%${search}%`);
paramIndex++;
}
if (isActive !== undefined) {
whereConditions.push(`is_active = $${paramIndex++}`);
values.push(isActive ? "Y" : "N");
}
const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
const offset = (page - 1) * size;
// 코드 조회
const codes = await query<CodeInfo>(
`SELECT * FROM code_info
${whereClause}
ORDER BY sort_order ASC, code_value ASC
LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
[...values, size, offset]
);
// 전체 개수 조회
const countResult = await queryOne<{ count: string }>(
`SELECT COUNT(*) as count FROM code_info ${whereClause}`,
values
);
const total = parseInt(countResult?.count || "0");
logger.info(
`코드 조회 완료: ${categoryCode} - ${codes.length}개, 전체: ${total}`
);
return { data: codes, total };
} catch (error) {
logger.error(`코드 조회 중 오류 (${categoryCode}):`, error);
throw error;
}
}
/**
* 카테고리 생성
*/
async createCategory(data: CreateCategoryData, createdBy: string) {
try {
const category = await queryOne<CodeCategory>(
`INSERT INTO code_category
(category_code, category_name, category_name_eng, description, sort_order,
is_active, created_by, updated_by, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, 'Y', $6, $7, NOW(), NOW())
RETURNING *`,
[
data.categoryCode,
data.categoryName,
data.categoryNameEng || null,
data.description || null,
data.sortOrder || 0,
createdBy,
createdBy,
]
);
logger.info(`카테고리 생성 완료: ${data.categoryCode}`);
return category;
} catch (error) {
logger.error("카테고리 생성 중 오류:", error);
throw error;
}
}
/**
* 카테고리 수정
*/
async updateCategory(
categoryCode: string,
data: Partial<CreateCategoryData>,
updatedBy: string
) {
try {
// 디버깅: 받은 데이터 로그
logger.info(`카테고리 수정 데이터:`, { categoryCode, data });
// 동적 UPDATE 쿼리 생성
const updateFields: string[] = [
"updated_by = $1",
"updated_date = NOW()",
];
const values: any[] = [updatedBy];
let paramIndex = 2;
if (data.categoryName !== undefined) {
updateFields.push(`category_name = $${paramIndex++}`);
values.push(data.categoryName);
}
if (data.categoryNameEng !== undefined) {
updateFields.push(`category_name_eng = $${paramIndex++}`);
values.push(data.categoryNameEng);
}
if (data.description !== undefined) {
updateFields.push(`description = $${paramIndex++}`);
values.push(data.description);
}
if (data.sortOrder !== undefined) {
updateFields.push(`sort_order = $${paramIndex++}`);
values.push(data.sortOrder);
}
if (data.isActive !== undefined) {
const activeValue =
typeof data.isActive === "boolean"
? data.isActive
? "Y"
: "N"
: data.isActive;
updateFields.push(`is_active = $${paramIndex++}`);
values.push(activeValue);
}
const category = await queryOne<CodeCategory>(
`UPDATE code_category
SET ${updateFields.join(", ")}
WHERE category_code = $${paramIndex}
RETURNING *`,
[...values, categoryCode]
);
logger.info(`카테고리 수정 완료: ${categoryCode}`);
return category;
} catch (error) {
logger.error(`카테고리 수정 중 오류 (${categoryCode}):`, error);
throw error;
}
}
/**
* 카테고리 삭제
*/
async deleteCategory(categoryCode: string) {
try {
await query(`DELETE FROM code_category WHERE category_code = $1`, [
categoryCode,
]);
logger.info(`카테고리 삭제 완료: ${categoryCode}`);
} catch (error) {
logger.error(`카테고리 삭제 중 오류 (${categoryCode}):`, error);
throw error;
}
}
/**
* 코드 생성
*/
async createCode(
categoryCode: string,
data: CreateCodeData,
createdBy: string
) {
try {
const code = await queryOne<CodeInfo>(
`INSERT INTO code_info
(code_category, code_value, code_name, code_name_eng, description, sort_order,
is_active, created_by, updated_by, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, $6, 'Y', $7, $8, NOW(), NOW())
RETURNING *`,
[
categoryCode,
data.codeValue,
data.codeName,
data.codeNameEng || null,
data.description || null,
data.sortOrder || 0,
createdBy,
createdBy,
]
);
logger.info(`코드 생성 완료: ${categoryCode}.${data.codeValue}`);
return code;
} catch (error) {
logger.error(
`코드 생성 중 오류 (${categoryCode}.${data.codeValue}):`,
error
);
throw error;
}
}
/**
* 코드 수정
*/
async updateCode(
categoryCode: string,
codeValue: string,
data: Partial<CreateCodeData>,
updatedBy: string
) {
try {
// 디버깅: 받은 데이터 로그
logger.info(`코드 수정 데이터:`, { categoryCode, codeValue, data });
// 동적 UPDATE 쿼리 생성
const updateFields: string[] = [
"updated_by = $1",
"updated_date = NOW()",
];
const values: any[] = [updatedBy];
let paramIndex = 2;
if (data.codeName !== undefined) {
updateFields.push(`code_name = $${paramIndex++}`);
values.push(data.codeName);
}
if (data.codeNameEng !== undefined) {
updateFields.push(`code_name_eng = $${paramIndex++}`);
values.push(data.codeNameEng);
}
if (data.description !== undefined) {
updateFields.push(`description = $${paramIndex++}`);
values.push(data.description);
}
if (data.sortOrder !== undefined) {
updateFields.push(`sort_order = $${paramIndex++}`);
values.push(data.sortOrder);
}
if (data.isActive !== undefined) {
const activeValue =
typeof data.isActive === "boolean"
? data.isActive
? "Y"
: "N"
: data.isActive;
updateFields.push(`is_active = $${paramIndex++}`);
values.push(activeValue);
}
const code = await queryOne<CodeInfo>(
`UPDATE code_info
SET ${updateFields.join(", ")}
WHERE code_category = $${paramIndex++} AND code_value = $${paramIndex}
RETURNING *`,
[...values, categoryCode, codeValue]
);
logger.info(`코드 수정 완료: ${categoryCode}.${codeValue}`);
return code;
} catch (error) {
logger.error(`코드 수정 중 오류 (${categoryCode}.${codeValue}):`, error);
throw error;
}
}
/**
* 코드 삭제
*/
async deleteCode(categoryCode: string, codeValue: string) {
try {
await query(
`DELETE FROM code_info WHERE code_category = $1 AND code_value = $2`,
[categoryCode, codeValue]
);
logger.info(`코드 삭제 완료: ${categoryCode}.${codeValue}`);
} catch (error) {
logger.error(`코드 삭제 중 오류 (${categoryCode}.${codeValue}):`, error);
throw error;
}
}
/**
* 카테고리별 옵션 조회 (화면관리용)
*/
async getCodeOptions(categoryCode: string) {
try {
const codes = await query<{
code_value: string;
code_name: string;
code_name_eng: string | null;
sort_order: number;
}>(
`SELECT code_value, code_name, code_name_eng, sort_order
FROM code_info
WHERE code_category = $1 AND is_active = 'Y'
ORDER BY sort_order ASC, code_value ASC`,
[categoryCode]
);
const options = codes.map((code) => ({
value: code.code_value,
label: code.code_name,
labelEng: code.code_name_eng,
}));
logger.info(`코드 옵션 조회 완료: ${categoryCode} - ${options.length}`);
return options;
} catch (error) {
logger.error(`코드 옵션 조회 중 오류 (${categoryCode}):`, error);
throw error;
}
}
/**
* 코드 순서 변경
*/
async reorderCodes(
categoryCode: string,
codes: Array<{ codeValue: string; sortOrder: number }>,
updatedBy: string
) {
try {
// 먼저 존재하는 코드들을 확인
const codeValues = codes.map((c) => c.codeValue);
const placeholders = codeValues.map((_, i) => `$${i + 2}`).join(", ");
const existingCodes = await query<{ code_value: string }>(
`SELECT code_value FROM code_info
WHERE code_category = $1 AND code_value IN (${placeholders})`,
[categoryCode, ...codeValues]
);
const existingCodeValues = existingCodes.map((c) => c.code_value);
const validCodes = codes.filter((c) =>
existingCodeValues.includes(c.codeValue)
);
if (validCodes.length === 0) {
throw new Error(
`카테고리 ${categoryCode}에 순서를 변경할 유효한 코드가 없습니다.`
);
}
// 트랜잭션으로 업데이트
await transaction(async (client) => {
for (const { codeValue, sortOrder } of validCodes) {
await client.query(
`UPDATE code_info
SET sort_order = $1, updated_by = $2, updated_date = NOW()
WHERE code_category = $3 AND code_value = $4`,
[sortOrder, updatedBy, categoryCode, codeValue]
);
}
});
const skippedCodes = codes.filter(
(c) => !existingCodeValues.includes(c.codeValue)
);
if (skippedCodes.length > 0) {
logger.warn(
`코드 순서 변경 시 존재하지 않는 코드들을 건너뜀: ${skippedCodes.map((c) => c.codeValue).join(", ")}`
);
}
logger.info(
`코드 순서 변경 완료: ${categoryCode} - ${validCodes.length}개 (전체 ${codes.length}개 중)`
);
} catch (error) {
logger.error(`코드 순서 변경 중 오류 (${categoryCode}):`, error);
throw error;
}
}
/**
* 카테고리 중복 검사
*/
async checkCategoryDuplicate(
field: "categoryCode" | "categoryName" | "categoryNameEng",
value: string,
excludeCategoryCode?: string
): Promise<{ isDuplicate: boolean; message: string }> {
try {
if (!value || !value.trim()) {
return {
isDuplicate: false,
message: "값을 입력해주세요.",
};
}
const trimmedValue = value.trim();
let whereCondition: any = {};
// 필드별 검색 조건 설정
switch (field) {
case "categoryCode":
whereCondition.category_code = trimmedValue;
break;
case "categoryName":
whereCondition.category_name = trimmedValue;
break;
case "categoryNameEng":
whereCondition.category_name_eng = trimmedValue;
break;
}
// SQL 쿼리 생성
let sql = "";
const values: any[] = [];
let paramIndex = 1;
switch (field) {
case "categoryCode":
sql = `SELECT category_code FROM code_category WHERE category_code = $${paramIndex++}`;
values.push(trimmedValue);
break;
case "categoryName":
sql = `SELECT category_code FROM code_category WHERE category_name = $${paramIndex++}`;
values.push(trimmedValue);
break;
case "categoryNameEng":
sql = `SELECT category_code FROM code_category WHERE category_name_eng = $${paramIndex++}`;
values.push(trimmedValue);
break;
}
// 수정 시 자기 자신 제외
if (excludeCategoryCode) {
sql += ` AND category_code != $${paramIndex++}`;
values.push(excludeCategoryCode);
}
sql += ` LIMIT 1`;
const existingCategory = await queryOne<{ category_code: string }>(
sql,
values
);
const isDuplicate = !!existingCategory;
const fieldNames = {
categoryCode: "카테고리 코드",
categoryName: "카테고리명",
categoryNameEng: "카테고리 영문명",
};
return {
isDuplicate,
message: isDuplicate
? `이미 사용 중인 ${fieldNames[field]}입니다.`
: `사용 가능한 ${fieldNames[field]}입니다.`,
};
} catch (error) {
logger.error(`카테고리 중복 검사 중 오류 (${field}: ${value}):`, error);
throw error;
}
}
/**
* 코드 중복 검사
*/
async checkCodeDuplicate(
categoryCode: string,
field: "codeValue" | "codeName" | "codeNameEng",
value: string,
excludeCodeValue?: string
): Promise<{ isDuplicate: boolean; message: string }> {
try {
if (!value || !value.trim()) {
return {
isDuplicate: false,
message: "값을 입력해주세요.",
};
}
const trimmedValue = value.trim();
let whereCondition: any = {
code_category: categoryCode,
};
// 필드별 검색 조건 설정
switch (field) {
case "codeValue":
whereCondition.code_value = trimmedValue;
break;
case "codeName":
whereCondition.code_name = trimmedValue;
break;
case "codeNameEng":
whereCondition.code_name_eng = trimmedValue;
break;
}
// SQL 쿼리 생성
let sql =
"SELECT code_value FROM code_info WHERE code_category = $1 AND ";
const values: any[] = [categoryCode];
let paramIndex = 2;
switch (field) {
case "codeValue":
sql += `code_value = $${paramIndex++}`;
values.push(trimmedValue);
break;
case "codeName":
sql += `code_name = $${paramIndex++}`;
values.push(trimmedValue);
break;
case "codeNameEng":
sql += `code_name_eng = $${paramIndex++}`;
values.push(trimmedValue);
break;
}
// 수정 시 자기 자신 제외
if (excludeCodeValue) {
sql += ` AND code_value != $${paramIndex++}`;
values.push(excludeCodeValue);
}
sql += ` LIMIT 1`;
const existingCode = await queryOne<{ code_value: string }>(sql, values);
const isDuplicate = !!existingCode;
const fieldNames = {
codeValue: "코드값",
codeName: "코드명",
codeNameEng: "코드 영문명",
};
return {
isDuplicate,
message: isDuplicate
? `이미 사용 중인 ${fieldNames[field]}입니다.`
: `사용 가능한 ${fieldNames[field]}입니다.`,
};
} catch (error) {
logger.error(
`코드 중복 검사 중 오류 (${categoryCode}, ${field}: ${value}):`,
error
);
throw error;
}
}
}