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; // 메뉴 기반 코드 관리용 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, 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( `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 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 없음 - 전역 코드 조회`); } // 회사별 필터링 (최고 관리자가 아닌 경우) if (userCompanyCode && userCompanyCode !== "*") { whereConditions.push(`company_code = $${paramIndex}`); 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( `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( `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, 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( `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 { const code = await queryOne( `INSERT INTO code_info (code_category, code_value, code_name, code_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, $6, 'Y', $7, $8, $9, $10, NOW(), NOW()) RETURNING *`, [ categoryCode, data.codeValue, data.codeName, data.codeNameEng || null, data.description || null, data.sortOrder || 0, menuObjid, companyCode, createdBy, createdBy, ] ); logger.info( `코드 생성 완료: ${categoryCode}.${data.codeValue} (메뉴: ${menuObjid}, 회사: ${companyCode})` ); return code; } catch (error) { logger.error( `코드 생성 중 오류 (${categoryCode}.${data.codeValue}):`, error ); throw error; } } /** * 코드 수정 */ async updateCode( categoryCode: string, codeValue: string, data: Partial, 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); } // 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( `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 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; } } }