674 lines
18 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|