Files
vexplor_dev/backend-node/src/services/commonCodeService.ts
2025-12-22 13:45:08 +09:00

1064 lines
32 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;
company_code: 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;
company_code: string;
menu_objid?: number | null; // 메뉴 기반 코드 관리용
// 계층 구조 지원
parent_code_value?: string | null; // 부모 코드 값
depth?: number; // 계층 깊이 (1: 최상위)
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;
// 계층 구조 지원
parentCodeValue?: string;
depth?: number;
}
export class CommonCodeService {
/**
* 카테고리 목록 조회
*/
async getCategories(params: GetCategoriesParams, userCompanyCode?: string, menuObjid?: number) {
try {
const { search, isActive, page = 1, size = 20 } = params;
const whereConditions: string[] = [];
const values: any[] = [];
let paramIndex = 1;
// 메뉴별 필터링 (형제 메뉴 포함)
if (menuObjid) {
const { getSiblingMenuObjids } = await import('./menuService');
const siblingMenuObjids = await getSiblingMenuObjids(menuObjid);
whereConditions.push(`menu_objid = ANY($${paramIndex})`);
values.push(siblingMenuObjids);
paramIndex++;
logger.info(`메뉴별 코드 카테고리 필터링: ${menuObjid}, 형제 메뉴: ${siblingMenuObjids.join(', ')}`);
}
// 회사별 필터링 (최고 관리자가 아닌 경우)
// company_code = '*'인 공통 데이터도 함께 조회
if (userCompanyCode && userCompanyCode !== "*") {
whereConditions.push(`(company_code = $${paramIndex} OR company_code = '*')`);
values.push(userCompanyCode);
paramIndex++;
logger.info(`회사별 코드 카테고리 필터링: ${userCompanyCode} (공통 데이터 포함)`);
} else if (userCompanyCode === "*") {
// 최고 관리자는 모든 데이터 조회 가능
logger.info(`최고 관리자: 모든 코드 카테고리 조회`);
}
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;
// code_category 테이블에서만 조회 (comm_code 제거)
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(
`카테고리 조회 완료: code_category ${categories.length}개, 전체: ${total}개 (회사: ${userCompanyCode || "전체"})`
);
return {
data: categories,
total,
};
} catch (error) {
logger.error("카테고리 조회 중 오류:", error);
throw error;
}
}
/**
* 특정 카테고리 조회 (코드로)
*/
async getCategoryByCode(categoryCode: string): Promise<CodeCategory | null> {
try {
const category = await queryOne<CodeCategory>(
`SELECT * FROM code_category WHERE category_code = $1`,
[categoryCode]
);
return category || null;
} catch (error) {
logger.error(`카테고리 조회 실패 (${categoryCode}):`, error);
return null;
}
}
/**
* 카테고리별 코드 목록 조회
*/
async getCodes(
categoryCode: string,
params: GetCodesParams,
userCompanyCode?: string,
menuObjid?: number
) {
try {
const { search, isActive, page = 1, size = 20 } = params;
logger.info(`🔍 [getCodes] 코드 조회 시작:`, {
categoryCode,
menuObjid,
hasMenuObjid: !!menuObjid,
userCompanyCode,
search,
isActive,
page,
size,
});
const whereConditions: string[] = ["code_category = $1"];
const values: any[] = [categoryCode];
let paramIndex = 2;
// 메뉴별 필터링 (형제 메뉴 포함)
if (menuObjid) {
const { getSiblingMenuObjids } = await import('./menuService');
const siblingMenuObjids = await getSiblingMenuObjids(menuObjid);
whereConditions.push(`menu_objid = ANY($${paramIndex})`);
values.push(siblingMenuObjids);
paramIndex++;
logger.info(`📋 [getCodes] 메뉴별 코드 필터링:`, {
menuObjid,
siblingMenuObjids,
siblingCount: siblingMenuObjids.length,
});
} else {
logger.warn(`⚠️ [getCodes] menuObjid 없음 - 전역 코드 조회`);
}
// 회사별 필터링 (최고 관리자가 아닌 경우)
// company_code = '*'인 공통 데이터도 함께 조회
if (userCompanyCode && userCompanyCode !== "*") {
whereConditions.push(`(company_code = $${paramIndex} OR company_code = '*')`);
values.push(userCompanyCode);
paramIndex++;
logger.info(`회사별 코드 필터링: ${userCompanyCode} (공통 데이터 포함)`);
} else if (userCompanyCode === "*") {
logger.info(`최고 관리자: 모든 코드 조회`);
}
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;
logger.info(`📝 [getCodes] 실행할 쿼리:`, {
whereClause,
values,
whereConditions,
paramIndex,
});
// code_info 테이블에서만 코드 조회 (comm_code fallback 제거)
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}개 (회사: ${userCompanyCode || "전체"}, menuObjid: ${menuObjid || "없음"})`
);
return { data: codes, total };
} catch (error) {
logger.error(`코드 조회 중 오류 (${categoryCode}):`, error);
throw error;
}
}
/**
* 카테고리 생성
*/
async createCategory(
data: CreateCategoryData,
createdBy: string,
companyCode: string,
menuObjid: number
) {
try {
const category = await queryOne<CodeCategory>(
`INSERT INTO code_category
(category_code, category_name, category_name_eng, description, sort_order,
is_active, menu_objid, company_code, created_by, updated_by, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, 'Y', $6, $7, $8, $9, NOW(), NOW())
RETURNING *`,
[
data.categoryCode,
data.categoryName,
data.categoryNameEng || null,
data.description || null,
data.sortOrder || 0,
menuObjid,
companyCode,
createdBy,
createdBy,
]
);
logger.info(
`카테고리 생성 완료: ${data.categoryCode} (메뉴: ${menuObjid}, 회사: ${companyCode})`
);
return category;
} catch (error) {
logger.error("카테고리 생성 중 오류:", error);
throw error;
}
}
/**
* 카테고리 수정
*/
async updateCategory(
categoryCode: string,
data: Partial<CreateCategoryData>,
updatedBy: string,
companyCode?: string
) {
try {
// 디버깅: 받은 데이터 로그
logger.info(`카테고리 수정 데이터:`, { categoryCode, data, companyCode });
// 동적 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);
}
// WHERE 절 구성
let whereClause = `WHERE category_code = $${paramIndex}`;
values.push(categoryCode);
// 회사 필터링 (최고 관리자가 아닌 경우)
if (companyCode && companyCode !== "*") {
paramIndex++;
whereClause += ` AND company_code = $${paramIndex}`;
values.push(companyCode);
}
const category = await queryOne<CodeCategory>(
`UPDATE code_category
SET ${updateFields.join(", ")}
${whereClause}
RETURNING *`,
values
);
logger.info(
`카테고리 수정 완료: ${categoryCode} (회사: ${companyCode || "전체"})`
);
return category;
} catch (error) {
logger.error(`카테고리 수정 중 오류 (${categoryCode}):`, error);
throw error;
}
}
/**
* 카테고리 삭제
*/
async deleteCategory(categoryCode: string, companyCode?: string) {
try {
let sql = `DELETE FROM code_category WHERE category_code = $1`;
const values: any[] = [categoryCode];
// 회사 필터링 (최고 관리자가 아닌 경우)
if (companyCode && companyCode !== "*") {
sql += ` AND company_code = $2`;
values.push(companyCode);
}
await query(sql, values);
logger.info(
`카테고리 삭제 완료: ${categoryCode} (회사: ${companyCode || "전체"})`
);
} catch (error) {
logger.error(`카테고리 삭제 중 오류 (${categoryCode}):`, error);
throw error;
}
}
/**
* 코드 생성
*/
async createCode(
categoryCode: string,
data: CreateCodeData,
createdBy: string,
companyCode: string,
menuObjid: number
) {
try {
// 계층 구조 깊이 계산
let depth = data.depth || 1;
if (data.parentCodeValue && !data.depth) {
// 부모 코드의 depth를 조회하여 +1
const parentCode = await queryOne<{ depth: number }>(
`SELECT depth FROM code_info WHERE code_category = $1 AND code_value = $2`,
[categoryCode, data.parentCodeValue]
);
depth = (parentCode?.depth || 0) + 1;
}
const code = await queryOne<CodeInfo>(
`INSERT INTO code_info
(code_category, code_value, code_name, code_name_eng, description, sort_order,
is_active, menu_objid, company_code, parent_code_value, depth, created_by, updated_by, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, $6, 'Y', $7, $8, $9, $10, $11, $12, NOW(), NOW())
RETURNING *`,
[
categoryCode,
data.codeValue,
data.codeName,
data.codeNameEng || null,
data.description || null,
data.sortOrder || 0,
menuObjid,
companyCode,
data.parentCodeValue || null,
depth,
createdBy,
createdBy,
]
);
logger.info(
`코드 생성 완료: ${categoryCode}.${data.codeValue} (메뉴: ${menuObjid}, 회사: ${companyCode}, 부모: ${data.parentCodeValue || '없음'}, depth: ${depth})`
);
return code;
} catch (error) {
logger.error(
`코드 생성 중 오류 (${categoryCode}.${data.codeValue}):`,
error
);
throw error;
}
}
/**
* 코드 수정
*/
async updateCode(
categoryCode: string,
codeValue: string,
data: Partial<CreateCodeData>,
updatedBy: string,
companyCode?: string
) {
try {
// 디버깅: 받은 데이터 로그
logger.info(`코드 수정 데이터:`, {
categoryCode,
codeValue,
data,
companyCode,
});
// 동적 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);
}
// 계층 구조 필드
if (data.parentCodeValue !== undefined) {
updateFields.push(`parent_code_value = $${paramIndex++}`);
values.push(data.parentCodeValue || null);
// depth 자동 계산
if (data.parentCodeValue) {
const parentCode = await queryOne<{ depth: number }>(
`SELECT depth FROM code_info WHERE code_category = $1 AND code_value = $2`,
[categoryCode, data.parentCodeValue]
);
const newDepth = (parentCode?.depth || 0) + 1;
updateFields.push(`depth = $${paramIndex++}`);
values.push(newDepth);
} else {
updateFields.push(`depth = $${paramIndex++}`);
values.push(1); // 부모 없으면 최상위
}
}
// WHERE 절 구성
let whereClause = `WHERE code_category = $${paramIndex++} AND code_value = $${paramIndex}`;
values.push(categoryCode, codeValue);
// 회사 필터링 (최고 관리자가 아닌 경우)
if (companyCode && companyCode !== "*") {
paramIndex++;
whereClause += ` AND company_code = $${paramIndex}`;
values.push(companyCode);
}
const code = await queryOne<CodeInfo>(
`UPDATE code_info
SET ${updateFields.join(", ")}
${whereClause}
RETURNING *`,
values
);
logger.info(
`코드 수정 완료: ${categoryCode}.${codeValue} (회사: ${companyCode || "전체"})`
);
return code;
} catch (error) {
logger.error(`코드 수정 중 오류 (${categoryCode}.${codeValue}):`, error);
throw error;
}
}
/**
* 코드 삭제
*/
async deleteCode(
categoryCode: string,
codeValue: string,
companyCode?: string
) {
try {
let sql = `DELETE FROM code_info WHERE code_category = $1 AND code_value = $2`;
const values: any[] = [categoryCode, codeValue];
// 회사 필터링 (최고 관리자가 아닌 경우)
if (companyCode && companyCode !== "*") {
sql += ` AND company_code = $3`;
values.push(companyCode);
}
await query(sql, values);
logger.info(
`코드 삭제 완료: ${categoryCode}.${codeValue} (회사: ${companyCode || "전체"})`
);
} catch (error) {
logger.error(`코드 삭제 중 오류 (${categoryCode}.${codeValue}):`, error);
throw error;
}
}
/**
* 카테고리별 옵션 조회 (화면관리용)
*/
async getCodeOptions(categoryCode: string, userCompanyCode?: string) {
try {
let sql = `SELECT code_value, code_name, code_name_eng, sort_order
FROM code_info
WHERE code_category = $1 AND is_active = 'Y'`;
const values: any[] = [categoryCode];
// 회사별 필터링 (최고 관리자가 아닌 경우)
if (userCompanyCode && userCompanyCode !== "*") {
sql += ` AND company_code = $2`;
values.push(userCompanyCode);
logger.info(`회사별 코드 옵션 필터링: ${userCompanyCode}`);
} else if (userCompanyCode === "*") {
logger.info(`최고 관리자: 모든 코드 옵션 조회`);
}
sql += ` ORDER BY sort_order ASC, code_value ASC`;
const codes = await query<{
code_value: string;
code_name: string;
code_name_eng: string | null;
sort_order: number;
}>(sql, values);
const options = codes.map((code) => ({
value: code.code_value,
label: code.code_name,
labelEng: code.code_name_eng,
}));
logger.info(
`코드 옵션 조회 완료: ${categoryCode} - ${options.length}개 (회사: ${userCompanyCode || "전체"})`
);
return options;
} catch (error) {
logger.error(`코드 옵션 조회 중 오류 (${categoryCode}):`, error);
throw error;
}
}
/**
* 계층 구조로 코드 조회 (트리 형태)
*/
async getCodesHierarchy(
categoryCode: string,
userCompanyCode?: string
): Promise<any[]> {
try {
let sql = `
SELECT code_value, code_name, code_name_eng, description, sort_order,
is_active, parent_code_value, depth
FROM code_info
WHERE code_category = $1 AND is_active = 'Y'
`;
const values: any[] = [categoryCode];
// 회사별 필터링
if (userCompanyCode && userCompanyCode !== "*") {
sql += ` AND company_code = $2`;
values.push(userCompanyCode);
}
sql += ` ORDER BY depth ASC, sort_order ASC, code_value ASC`;
const codes = await query<{
code_value: string;
code_name: string;
code_name_eng: string | null;
description: string | null;
sort_order: number;
is_active: string;
parent_code_value: string | null;
depth: number;
}>(sql, values);
// 트리 구조로 변환
const codeMap = new Map<string, any>();
const roots: any[] = [];
// 모든 코드를 맵에 저장
for (const code of codes) {
codeMap.set(code.code_value, {
value: code.code_value,
label: code.code_name,
labelEng: code.code_name_eng,
description: code.description,
depth: code.depth || 1,
parentValue: code.parent_code_value,
children: [],
});
}
// 부모-자식 관계 구성
for (const code of codes) {
const node = codeMap.get(code.code_value);
if (code.parent_code_value && codeMap.has(code.parent_code_value)) {
const parent = codeMap.get(code.parent_code_value);
parent.children.push(node);
} else {
roots.push(node);
}
}
logger.info(
`계층 코드 조회 완료: ${categoryCode} - ${roots.length}개 루트, 전체 ${codes.length}`
);
return roots;
} catch (error) {
logger.error(`계층 코드 조회 중 오류 (${categoryCode}):`, error);
throw error;
}
}
/**
* 특정 부모 코드의 자식 코드 목록 조회 (연쇄 선택용)
*/
async getChildCodes(
categoryCode: string,
parentCodeValue: string | null,
userCompanyCode?: string
): Promise<Array<{ value: string; label: string; hasChildren: boolean }>> {
try {
let sql = `
SELECT
c.code_value,
c.code_name,
c.code_name_eng,
EXISTS(SELECT 1 FROM code_info c2 WHERE c2.parent_code_value = c.code_value AND c2.code_category = c.code_category) as has_children
FROM code_info c
WHERE c.code_category = $1
AND c.is_active = 'Y'
`;
const values: any[] = [categoryCode];
let paramIndex = 2;
// 부모 코드 필터
if (parentCodeValue) {
sql += ` AND c.parent_code_value = $${paramIndex++}`;
values.push(parentCodeValue);
} else {
sql += ` AND (c.parent_code_value IS NULL OR c.depth = 1)`;
}
// 회사별 필터링
if (userCompanyCode && userCompanyCode !== "*") {
sql += ` AND c.company_code = $${paramIndex++}`;
values.push(userCompanyCode);
}
sql += ` ORDER BY c.sort_order ASC, c.code_value ASC`;
const codes = await query<{
code_value: string;
code_name: string;
code_name_eng: string | null;
has_children: boolean;
}>(sql, values);
const result = codes.map((code) => ({
value: code.code_value,
label: code.code_name,
hasChildren: code.has_children,
}));
logger.info(
`자식 코드 조회 완료: ${categoryCode} (부모: ${parentCodeValue || '최상위'}) - ${result.length}`
);
return result;
} catch (error) {
logger.error(`자식 코드 조회 중 오류 (${categoryCode}, ${parentCodeValue}):`, error);
throw error;
}
}
/**
* 테이블.컬럼 기반 카테고리 옵션 조회 (레거시 카테고리 호환용)
*
* 마이그레이션 후 category 타입이 code로 변환되었을 때
* 기존 `tableName_columnName` 형식의 codeGroup으로 조회
*
* @param tableName 테이블명
* @param columnName 컬럼명
* @param userCompanyCode 회사 코드
*/
async getCategoryOptionsAsCode(tableName: string, columnName: string, userCompanyCode?: string) {
try {
// 카테고리 코드 그룹명 생성: TABLENAME_COLUMNNAME
const categoryCode = `${tableName.toUpperCase()}_${columnName.toUpperCase()}`;
logger.info(`카테고리 → 코드 호환 조회: ${tableName}.${columnName}${categoryCode}`);
return await this.getCodeOptions(categoryCode, userCompanyCode);
} catch (error) {
logger.error(`카테고리 호환 조회 중 오류 (${tableName}.${columnName}):`, 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,
userCompanyCode?: 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 (userCompanyCode && userCompanyCode !== "*") {
sql += ` AND company_code = $${paramIndex++}`;
values.push(userCompanyCode);
}
// 수정 시 자기 자신 제외
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: "카테고리 영문명",
};
logger.info(
`카테고리 중복 검사: ${field}=${value}, 회사=${userCompanyCode}, 중복=${isDuplicate}`
);
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,
userCompanyCode?: 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 (userCompanyCode && userCompanyCode !== "*") {
sql += ` AND company_code = $${paramIndex++}`;
values.push(userCompanyCode);
}
// 수정 시 자기 자신 제외
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: "코드 영문명",
};
logger.info(
`코드 중복 검사: ${categoryCode}.${field}=${value}, 회사=${userCompanyCode}, 중복=${isDuplicate}`
);
return {
isDuplicate,
message: isDuplicate
? `이미 사용 중인 ${fieldNames[field]}입니다.`
: `사용 가능한 ${fieldNames[field]}입니다.`,
};
} catch (error) {
logger.error(
`코드 중복 검사 중 오류 (${categoryCode}, ${field}: ${value}):`,
error
);
throw error;
}
}
}