import { PrismaClient } from "@prisma/client"; import prisma from "../config/database"; 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; let whereClause: any = {}; if (search) { whereClause.OR = [ { category_name: { contains: search, mode: "insensitive" } }, { category_code: { contains: search, mode: "insensitive" } }, ]; } if (isActive !== undefined) { whereClause.is_active = isActive ? "Y" : "N"; } const offset = (page - 1) * size; const [categories, total] = await Promise.all([ prisma.code_category.findMany({ where: whereClause, orderBy: [{ sort_order: "asc" }, { category_code: "asc" }], skip: offset, take: size, }), prisma.code_category.count({ where: whereClause }), ]); 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; let whereClause: any = { code_category: categoryCode, }; if (search) { whereClause.OR = [ { code_name: { contains: search, mode: "insensitive" } }, { code_value: { contains: search, mode: "insensitive" } }, ]; } if (isActive !== undefined) { whereClause.is_active = isActive ? "Y" : "N"; } const offset = (page - 1) * size; const [codes, total] = await Promise.all([ prisma.code_info.findMany({ where: whereClause, orderBy: [{ sort_order: "asc" }, { code_value: "asc" }], skip: offset, take: size, }), prisma.code_info.count({ where: whereClause }), ]); 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 prisma.code_category.create({ data: { category_code: data.categoryCode, category_name: data.categoryName, category_name_eng: data.categoryNameEng, description: data.description, sort_order: data.sortOrder || 0, is_active: "Y", created_by: createdBy, updated_by: createdBy, }, }); logger.info(`카테고리 생성 완료: ${data.categoryCode}`); return category; } catch (error) { logger.error("카테고리 생성 중 오류:", error); throw error; } } /** * 카테고리 수정 */ async updateCategory( categoryCode: string, data: Partial, updatedBy: string ) { try { // 디버깅: 받은 데이터 로그 logger.info(`카테고리 수정 데이터:`, { categoryCode, data }); const category = await prisma.code_category.update({ where: { category_code: categoryCode }, data: { category_name: data.categoryName, category_name_eng: data.categoryNameEng, description: data.description, sort_order: data.sortOrder, is_active: typeof data.isActive === "boolean" ? data.isActive ? "Y" : "N" : data.isActive, // boolean이면 "Y"/"N"으로 변환 updated_by: updatedBy, updated_date: new Date(), }, }); logger.info(`카테고리 수정 완료: ${categoryCode}`); return category; } catch (error) { logger.error(`카테고리 수정 중 오류 (${categoryCode}):`, error); throw error; } } /** * 카테고리 삭제 */ async deleteCategory(categoryCode: string) { try { await prisma.code_category.delete({ where: { category_code: categoryCode }, }); logger.info(`카테고리 삭제 완료: ${categoryCode}`); } catch (error) { logger.error(`카테고리 삭제 중 오류 (${categoryCode}):`, error); throw error; } } /** * 코드 생성 */ async createCode( categoryCode: string, data: CreateCodeData, createdBy: string ) { try { const code = await prisma.code_info.create({ data: { code_category: categoryCode, code_value: data.codeValue, code_name: data.codeName, code_name_eng: data.codeNameEng, description: data.description, sort_order: data.sortOrder || 0, is_active: "Y", created_by: createdBy, updated_by: 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, updatedBy: string ) { try { // 디버깅: 받은 데이터 로그 logger.info(`코드 수정 데이터:`, { categoryCode, codeValue, data }); const code = await prisma.code_info.update({ where: { code_category_code_value: { code_category: categoryCode, code_value: codeValue, }, }, data: { code_name: data.codeName, code_name_eng: data.codeNameEng, description: data.description, sort_order: data.sortOrder, is_active: typeof data.isActive === "boolean" ? data.isActive ? "Y" : "N" : data.isActive, // boolean이면 "Y"/"N"으로 변환 updated_by: updatedBy, updated_date: new Date(), }, }); logger.info(`코드 수정 완료: ${categoryCode}.${codeValue}`); return code; } catch (error) { logger.error(`코드 수정 중 오류 (${categoryCode}.${codeValue}):`, error); throw error; } } /** * 코드 삭제 */ async deleteCode(categoryCode: string, codeValue: string) { try { await prisma.code_info.delete({ where: { code_category_code_value: { code_category: categoryCode, code_value: codeValue, }, }, }); logger.info(`코드 삭제 완료: ${categoryCode}.${codeValue}`); } catch (error) { logger.error(`코드 삭제 중 오류 (${categoryCode}.${codeValue}):`, error); throw error; } } /** * 카테고리별 옵션 조회 (화면관리용) */ async getCodeOptions(categoryCode: string) { try { const codes = await prisma.code_info.findMany({ where: { code_category: categoryCode, is_active: "Y", }, select: { code_value: true, code_name: true, code_name_eng: true, sort_order: true, }, orderBy: [{ sort_order: "asc" }, { code_value: "asc" }], }); 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 existingCodes = await prisma.code_info.findMany({ where: { code_category: categoryCode, code_value: { in: codes.map((c) => c.codeValue) }, }, select: { code_value: true }, }); 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}에 순서를 변경할 유효한 코드가 없습니다.` ); } const updatePromises = validCodes.map(({ codeValue, sortOrder }) => prisma.code_info.update({ where: { code_category_code_value: { code_category: categoryCode, code_value: codeValue, }, }, data: { sort_order: sortOrder, updated_by: updatedBy, updated_date: new Date(), }, }) ); await Promise.all(updatePromises); 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; } // 수정 시 자기 자신 제외 if (excludeCategoryCode) { whereCondition.category_code = { ...whereCondition.category_code, not: excludeCategoryCode, }; } const existingCategory = await prisma.code_category.findFirst({ where: whereCondition, select: { category_code: true }, }); 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; } // 수정 시 자기 자신 제외 if (excludeCodeValue) { whereCondition.code_value = { ...whereCondition.code_value, not: excludeCodeValue, }; } const existingCode = await prisma.code_info.findFirst({ where: whereCondition, select: { code_value: true }, }); 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; } } }