diff --git a/backend-node/prisma/schema.prisma b/backend-node/prisma/schema.prisma index a80080bf..34099936 100644 --- a/backend-node/prisma/schema.prisma +++ b/backend-node/prisma/schema.prisma @@ -5145,3 +5145,50 @@ model screen_menu_assignments { @@unique([screen_id, menu_objid, company_code]) @@index([company_code]) } + +// ===================================================== +// 공통코드 관리 시스템 모델 +// ===================================================== + +/// 공통코드 카테고리 테이블 +model code_category { + category_code String @id @db.VarChar(50) + category_name String @db.VarChar(100) + category_name_eng String? @db.VarChar(100) + description String? @db.Text + sort_order Int @default(0) + is_active String @default("Y") @db.Char(1) + created_date DateTime? @default(now()) @db.Timestamp(6) + created_by String? @db.VarChar(50) + updated_date DateTime? @default(now()) @db.Timestamp(6) + updated_by String? @db.VarChar(50) + + // 관계 - 코드 상세 정보 + codes code_info[] + + @@index([is_active]) + @@index([sort_order]) +} + +/// 공통코드 상세 정보 테이블 +model code_info { + code_category String @db.VarChar(50) + code_value String @db.VarChar(50) + code_name String @db.VarChar(100) + code_name_eng String? @db.VarChar(100) + description String? @db.Text + sort_order Int @default(0) + is_active String @default("Y") @db.Char(1) + created_date DateTime? @default(now()) @db.Timestamp(6) + created_by String? @db.VarChar(50) + updated_date DateTime? @default(now()) @db.Timestamp(6) + updated_by String? @db.VarChar(50) + + // 관계 - 코드 카테고리 + category code_category @relation(fields: [code_category], references: [category_code], onDelete: Cascade, onUpdate: Cascade) + + @@id([code_category, code_value]) + @@index([code_category]) + @@index([is_active]) + @@index([code_category, sort_order]) +} diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index df6cf3bd..3b281d19 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -14,6 +14,7 @@ import adminRoutes from "./routes/adminRoutes"; import multilangRoutes from "./routes/multilangRoutes"; import tableManagementRoutes from "./routes/tableManagementRoutes"; import screenManagementRoutes from "./routes/screenManagementRoutes"; +import commonCodeRoutes from "./routes/commonCodeRoutes"; // import userRoutes from './routes/userRoutes'; // import menuRoutes from './routes/menuRoutes'; @@ -65,6 +66,7 @@ app.use("/api/admin", adminRoutes); app.use("/api/multilang", multilangRoutes); app.use("/api/table-management", tableManagementRoutes); app.use("/api/screen-management", screenManagementRoutes); +app.use("/api/common-codes", commonCodeRoutes); // app.use('/api/users', userRoutes); // app.use('/api/menus', menuRoutes); diff --git a/backend-node/src/controllers/commonCodeController.ts b/backend-node/src/controllers/commonCodeController.ts new file mode 100644 index 00000000..cf9637d3 --- /dev/null +++ b/backend-node/src/controllers/commonCodeController.ts @@ -0,0 +1,504 @@ +import { Request, Response } from "express"; +import { + CommonCodeService, + CreateCategoryData, + CreateCodeData, +} from "../services/commonCodeService"; +import { AuthenticatedRequest } from "../types/auth"; +import { logger } from "../utils/logger"; + +export class CommonCodeController { + private commonCodeService: CommonCodeService; + + constructor() { + this.commonCodeService = new CommonCodeService(); + } + + /** + * 카테고리 목록 조회 + * GET /api/common-codes/categories + */ + async getCategories(req: AuthenticatedRequest, res: Response) { + try { + const { search, isActive, page = "1", size = "20" } = req.query; + + const categories = await this.commonCodeService.getCategories({ + search: search as string, + isActive: + isActive === "true" ? true : isActive === "false" ? false : undefined, + page: parseInt(page as string), + size: parseInt(size as string), + }); + + return res.json({ + success: true, + data: categories.data, + total: categories.total, + message: "카테고리 목록 조회 성공", + }); + } catch (error) { + logger.error("카테고리 목록 조회 실패:", error); + return res.status(500).json({ + success: false, + message: "카테고리 목록 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "Unknown error", + }); + } + } + + /** + * 카테고리별 코드 목록 조회 + * GET /api/common-codes/categories/:categoryCode/codes + */ + async getCodes(req: AuthenticatedRequest, res: Response) { + try { + const { categoryCode } = req.params; + const { search, isActive, page, size } = req.query; + + const result = await this.commonCodeService.getCodes(categoryCode, { + search: search as string, + isActive: + isActive === "true" ? true : isActive === "false" ? false : undefined, + page: page ? parseInt(page as string) : undefined, + size: size ? parseInt(size as string) : undefined, + }); + + return res.json({ + success: true, + data: result.data, + total: result.total, + message: `코드 목록 조회 성공 (${categoryCode})`, + }); + } catch (error) { + logger.error(`코드 목록 조회 실패 (${req.params.categoryCode}):`, error); + return res.status(500).json({ + success: false, + message: "코드 목록 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "Unknown error", + }); + } + } + + /** + * 카테고리 생성 + * POST /api/common-codes/categories + */ + async createCategory(req: AuthenticatedRequest, res: Response) { + try { + const categoryData: CreateCategoryData = req.body; + const userId = req.user?.userId || "SYSTEM"; // 인증 미들웨어에서 설정된 사용자 ID + + // 입력값 검증 + if (!categoryData.categoryCode || !categoryData.categoryName) { + return res.status(400).json({ + success: false, + message: "카테고리 코드와 이름은 필수입니다.", + }); + } + + const category = await this.commonCodeService.createCategory( + categoryData, + userId + ); + + return res.status(201).json({ + success: true, + data: category, + message: "카테고리 생성 성공", + }); + } catch (error) { + logger.error("카테고리 생성 실패:", error); + + // Prisma 에러 처리 + if ( + error instanceof Error && + error.message.includes("Unique constraint") + ) { + return res.status(409).json({ + success: false, + message: "이미 존재하는 카테고리 코드입니다.", + }); + } + + return res.status(500).json({ + success: false, + message: "카테고리 생성 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "Unknown error", + }); + } + } + + /** + * 카테고리 수정 + * PUT /api/common-codes/categories/:categoryCode + */ + async updateCategory(req: AuthenticatedRequest, res: Response) { + try { + const { categoryCode } = req.params; + const categoryData: Partial = req.body; + const userId = req.user?.userId || "SYSTEM"; + + const category = await this.commonCodeService.updateCategory( + categoryCode, + categoryData, + userId + ); + + return res.json({ + success: true, + data: category, + message: "카테고리 수정 성공", + }); + } catch (error) { + logger.error(`카테고리 수정 실패 (${req.params.categoryCode}):`, error); + + if ( + error instanceof Error && + error.message.includes("Record to update not found") + ) { + return res.status(404).json({ + success: false, + message: "존재하지 않는 카테고리입니다.", + }); + } + + return res.status(500).json({ + success: false, + message: "카테고리 수정 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "Unknown error", + }); + } + } + + /** + * 카테고리 삭제 + * DELETE /api/common-codes/categories/:categoryCode + */ + async deleteCategory(req: AuthenticatedRequest, res: Response) { + try { + const { categoryCode } = req.params; + + await this.commonCodeService.deleteCategory(categoryCode); + + return res.json({ + success: true, + message: "카테고리 삭제 성공", + }); + } catch (error) { + logger.error(`카테고리 삭제 실패 (${req.params.categoryCode}):`, error); + + if ( + error instanceof Error && + error.message.includes("Record to delete does not exist") + ) { + return res.status(404).json({ + success: false, + message: "존재하지 않는 카테고리입니다.", + }); + } + + return res.status(500).json({ + success: false, + message: "카테고리 삭제 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "Unknown error", + }); + } + } + + /** + * 코드 생성 + * POST /api/common-codes/categories/:categoryCode/codes + */ + async createCode(req: AuthenticatedRequest, res: Response) { + try { + const { categoryCode } = req.params; + const codeData: CreateCodeData = req.body; + const userId = req.user?.userId || "SYSTEM"; + + // 입력값 검증 + if (!codeData.codeValue || !codeData.codeName) { + return res.status(400).json({ + success: false, + message: "코드값과 코드명은 필수입니다.", + }); + } + + const code = await this.commonCodeService.createCode( + categoryCode, + codeData, + userId + ); + + return res.status(201).json({ + success: true, + data: code, + message: "코드 생성 성공", + }); + } catch (error) { + logger.error(`코드 생성 실패 (${req.params.categoryCode}):`, error); + + if ( + error instanceof Error && + error.message.includes("Unique constraint") + ) { + return res.status(409).json({ + success: false, + message: "이미 존재하는 코드값입니다.", + }); + } + + return res.status(500).json({ + success: false, + message: "코드 생성 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "Unknown error", + }); + } + } + + /** + * 코드 수정 + * PUT /api/common-codes/categories/:categoryCode/codes/:codeValue + */ + async updateCode(req: AuthenticatedRequest, res: Response) { + try { + const { categoryCode, codeValue } = req.params; + const codeData: Partial = req.body; + const userId = req.user?.userId || "SYSTEM"; + + const code = await this.commonCodeService.updateCode( + categoryCode, + codeValue, + codeData, + userId + ); + + return res.json({ + success: true, + data: code, + message: "코드 수정 성공", + }); + } catch (error) { + logger.error( + `코드 수정 실패 (${req.params.categoryCode}.${req.params.codeValue}):`, + error + ); + + if ( + error instanceof Error && + error.message.includes("Record to update not found") + ) { + return res.status(404).json({ + success: false, + message: "존재하지 않는 코드입니다.", + }); + } + + return res.status(500).json({ + success: false, + message: "코드 수정 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "Unknown error", + }); + } + } + + /** + * 코드 삭제 + * DELETE /api/common-codes/categories/:categoryCode/codes/:codeValue + */ + async deleteCode(req: AuthenticatedRequest, res: Response) { + try { + const { categoryCode, codeValue } = req.params; + + await this.commonCodeService.deleteCode(categoryCode, codeValue); + + return res.json({ + success: true, + message: "코드 삭제 성공", + }); + } catch (error) { + logger.error( + `코드 삭제 실패 (${req.params.categoryCode}.${req.params.codeValue}):`, + error + ); + + if ( + error instanceof Error && + error.message.includes("Record to delete does not exist") + ) { + return res.status(404).json({ + success: false, + message: "존재하지 않는 코드입니다.", + }); + } + + return res.status(500).json({ + success: false, + message: "코드 삭제 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "Unknown error", + }); + } + } + + /** + * 카테고리별 옵션 조회 (화면관리용) + * GET /api/common-codes/categories/:categoryCode/options + */ + async getCodeOptions(req: AuthenticatedRequest, res: Response) { + try { + const { categoryCode } = req.params; + + const options = await this.commonCodeService.getCodeOptions(categoryCode); + + return res.json({ + success: true, + data: options, + message: `코드 옵션 조회 성공 (${categoryCode})`, + }); + } catch (error) { + logger.error(`코드 옵션 조회 실패 (${req.params.categoryCode}):`, error); + return res.status(500).json({ + success: false, + message: "코드 옵션 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "Unknown error", + }); + } + } + + /** + * 코드 순서 변경 + * PUT /api/common-codes/categories/:categoryCode/codes/reorder + */ + async reorderCodes(req: AuthenticatedRequest, res: Response) { + try { + const { categoryCode } = req.params; + const { codes } = req.body as { + codes: Array<{ codeValue: string; sortOrder: number }>; + }; + const userId = req.user?.userId || "SYSTEM"; + + if (!codes || !Array.isArray(codes)) { + return res.status(400).json({ + success: false, + message: "코드 순서 정보가 올바르지 않습니다.", + }); + } + + await this.commonCodeService.reorderCodes(categoryCode, codes, userId); + + return res.json({ + success: true, + message: "코드 순서 변경 성공", + }); + } catch (error) { + logger.error(`코드 순서 변경 실패 (${req.params.categoryCode}):`, error); + return res.status(500).json({ + success: false, + message: "코드 순서 변경 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "Unknown error", + }); + } + } + + /** + * 카테고리 중복 검사 + * GET /api/common-codes/categories/check-duplicate?field=categoryCode&value=USER_STATUS&excludeCode=OLD_CODE + */ + async checkCategoryDuplicate(req: AuthenticatedRequest, res: Response) { + try { + const { field, value, excludeCode } = req.query; + + // 입력값 검증 + if (!field || !value) { + return res.status(400).json({ + success: false, + message: "field와 value 파라미터가 필요합니다.", + }); + } + + const validFields = ["categoryCode", "categoryName", "categoryNameEng"]; + if (!validFields.includes(field as string)) { + return res.status(400).json({ + success: false, + message: + "field는 categoryCode, categoryName, categoryNameEng 중 하나여야 합니다.", + }); + } + + const result = await this.commonCodeService.checkCategoryDuplicate( + field as "categoryCode" | "categoryName" | "categoryNameEng", + value as string, + excludeCode as string + ); + + return res.json({ + success: true, + data: { + ...result, + field, + value, + }, + message: "카테고리 중복 검사 완료", + }); + } catch (error) { + logger.error("카테고리 중복 검사 실패:", error); + return res.status(500).json({ + success: false, + message: "카테고리 중복 검사 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "Unknown error", + }); + } + } + + /** + * 코드 중복 검사 + * GET /api/common-codes/categories/:categoryCode/codes/check-duplicate?field=codeValue&value=ACTIVE&excludeCode=OLD_CODE + */ + async checkCodeDuplicate(req: AuthenticatedRequest, res: Response) { + try { + const { categoryCode } = req.params; + const { field, value, excludeCode } = req.query; + + // 입력값 검증 + if (!field || !value) { + return res.status(400).json({ + success: false, + message: "field와 value 파라미터가 필요합니다.", + }); + } + + const validFields = ["codeValue", "codeName", "codeNameEng"]; + if (!validFields.includes(field as string)) { + return res.status(400).json({ + success: false, + message: + "field는 codeValue, codeName, codeNameEng 중 하나여야 합니다.", + }); + } + + const result = await this.commonCodeService.checkCodeDuplicate( + categoryCode, + field as "codeValue" | "codeName" | "codeNameEng", + value as string, + excludeCode as string + ); + + return res.json({ + success: true, + data: { + ...result, + categoryCode, + field, + value, + }, + message: "코드 중복 검사 완료", + }); + } catch (error) { + logger.error(`코드 중복 검사 실패 (${req.params.categoryCode}):`, error); + return res.status(500).json({ + success: false, + message: "코드 중복 검사 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "Unknown error", + }); + } + } +} diff --git a/backend-node/src/routes/commonCodeRoutes.ts b/backend-node/src/routes/commonCodeRoutes.ts new file mode 100644 index 00000000..6772a6e9 --- /dev/null +++ b/backend-node/src/routes/commonCodeRoutes.ts @@ -0,0 +1,61 @@ +import { Router } from "express"; +import { CommonCodeController } from "../controllers/commonCodeController"; +import { authenticateToken } from "../middleware/authMiddleware"; + +const router = Router(); +const commonCodeController = new CommonCodeController(); + +// 모든 공통코드 API는 인증이 필요 +router.use(authenticateToken); + +// 카테고리 관련 라우트 +router.get("/categories", (req, res) => + commonCodeController.getCategories(req, res) +); + +// 카테고리 중복 검사 (구체적인 경로를 먼저 배치) +router.get("/categories/check-duplicate", (req, res) => + commonCodeController.checkCategoryDuplicate(req, res) +); + +router.post("/categories", (req, res) => + commonCodeController.createCategory(req, res) +); +router.put("/categories/:categoryCode", (req, res) => + commonCodeController.updateCategory(req, res) +); +router.delete("/categories/:categoryCode", (req, res) => + commonCodeController.deleteCategory(req, res) +); + +// 코드 관련 라우트 +router.get("/categories/:categoryCode/codes", (req, res) => + commonCodeController.getCodes(req, res) +); +router.post("/categories/:categoryCode/codes", (req, res) => + commonCodeController.createCode(req, res) +); + +// 코드 중복 검사 (구체적인 경로를 먼저 배치) +router.get("/categories/:categoryCode/codes/check-duplicate", (req, res) => + commonCodeController.checkCodeDuplicate(req, res) +); + +// 코드 순서 변경 (구체적인 경로를 먼저 배치) +router.put("/categories/:categoryCode/codes/reorder", (req, res) => + commonCodeController.reorderCodes(req, res) +); + +router.put("/categories/:categoryCode/codes/:codeValue", (req, res) => + commonCodeController.updateCode(req, res) +); +router.delete("/categories/:categoryCode/codes/:codeValue", (req, res) => + commonCodeController.deleteCode(req, res) +); + +// 화면관리용 옵션 조회 +router.get("/categories/:categoryCode/options", (req, res) => + commonCodeController.getCodeOptions(req, res) +); + +export default router; diff --git a/backend-node/src/services/commonCodeService.ts b/backend-node/src/services/commonCodeService.ts new file mode 100644 index 00000000..fca352ff --- /dev/null +++ b/backend-node/src/services/commonCodeService.ts @@ -0,0 +1,565 @@ +import { PrismaClient } from "@prisma/client"; +import { logger } from "../utils/logger"; + +const prisma = new PrismaClient(); + +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; + } + } +} diff --git a/backend-node/src/types/commonCode.ts b/backend-node/src/types/commonCode.ts new file mode 100644 index 00000000..d68022e5 --- /dev/null +++ b/backend-node/src/types/commonCode.ts @@ -0,0 +1,93 @@ +// 공통코드 관련 타입 정의 + +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 CreateCategoryRequest { + categoryCode: string; + categoryName: string; + categoryNameEng?: string; + description?: string; + sortOrder?: number; +} + +export interface UpdateCategoryRequest { + categoryName?: string; + categoryNameEng?: string; + description?: string; + sortOrder?: number; + isActive?: boolean; +} + +export interface CreateCodeRequest { + codeValue: string; + codeName: string; + codeNameEng?: string; + description?: string; + sortOrder?: number; +} + +export interface UpdateCodeRequest { + codeName?: string; + codeNameEng?: string; + description?: string; + sortOrder?: number; + isActive?: boolean; +} + +export interface CodeOption { + value: string; + label: string; + labelEng?: string | null; +} + +export interface ReorderCodesRequest { + codes: Array<{ + codeValue: string; + sortOrder: number; + }>; +} + +export interface GetCategoriesQuery { + search?: string; + isActive?: string; + page?: string; + size?: string; +} + +export interface GetCodesQuery { + search?: string; + isActive?: string; +} + +export interface ApiResponse { + success: boolean; + data?: T; + message: string; + error?: string; + total?: number; +} diff --git a/docs/공통코드_관리_시스템_설계.md b/docs/공통코드_관리_시스템_설계.md new file mode 100644 index 00000000..87f69f7d --- /dev/null +++ b/docs/공통코드_관리_시스템_설계.md @@ -0,0 +1,1057 @@ +# 공통코드 관리 시스템 설계 문서 + +## 📋 목차 + +1. [시스템 개요](#🎯-시스템-개요) +2. [아키텍처 구조](#🏗️-아키텍처-구조) +3. [핵심 기능](#🚀-핵심-기능) +4. [데이터베이스 설계](#🗄️-데이터베이스-설계) +5. [화면 구성 요소](#🎨-화면-구성-요소) +6. [API 설계](#🌐-api-설계) +7. [프론트엔드 구현](#🎭-프론트엔드-구현) +8. [백엔드 구현](#⚙️-백엔드-구현) +9. [사용 시나리오](#🎬-사용-시나리오) +10. [개발 계획](#📅-개발-계획-및-진행상황) + +## 🎯 시스템 개요 + +### 공통코드 관리 시스템이란? + +공통코드 관리 시스템은 **시스템에서 사용하는 공통적인 코드값들을 중앙에서 관리하는 기능**입니다. 드롭다운, 선택박스 등에서 반복적으로 사용되는 코드-값 쌍을 체계적으로 관리하여 데이터 일관성을 보장하고 개발 효율성을 높입니다. + +### 주요 특징 + +- **중앙 집중 관리**: 모든 공통코드를 한 곳에서 통합 관리 +- **카테고리 기반 분류**: 코드를 카테고리별로 체계적 분류 +- **다국어 지원**: 한국어/영어 코드명 지원 +- **화면관리 시스템 연계**: 웹 타입 'code'와 완벽 연동 +- **실시간 반영**: 코드 변경사항 즉시 시스템 전체 반영 +- **관리자 전용**: 관리자 메뉴에서만 접근 가능 + +### 🎯 **필수 요구사항** + +- ✅ **관리자 메뉴 접근**: 관리자 메뉴에서만 접근 가능 +- ✅ **코드 카테고리 관리**: 카테고리 생성/수정/삭제 +- ✅ **코드 상세 관리**: 코드값과 코드명 매핑 관리 +- ✅ **정렬 순서 관리**: 코드 표시 순서 조정 +- ✅ **활성/비활성 관리**: 코드 사용 여부 제어 +- ✅ **검색 및 필터링**: 대량 코드 효율적 관리 +- ✅ **화면관리 연계**: column_labels.code_category와 연동 + +## 🏗️ 아키텍처 구조 + +### 전체 구조도 + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Frontend │ │ Backend │ │ Database │ +│ │ │ │ │ │ +│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ +│ │ CodeCategory│ │ │ │ CommonCode │ │ │ │ code_ │ │ +│ │ Management │ │ │ │ Controller │ │ │ │ category │ │ +│ │ (React) │ │ │ │ │ │ │ │ Table │ │ +│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │ +│ │ │ │ │ │ +│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ +│ │ CodeDetail │ │ │ │ CommonCode │ │ │ │ code_info │ │ +│ │ Management │ │ │ │ Service │ │ │ │ Table │ │ +│ │ (React) │ │ │ │ │ │ │ │ │ │ +│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +### 화면관리 시스템 연계 + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 화면관리 시스템 │ │ 공통코드 관리 │ │ 실제 화면 │ +│ │ │ │ │ │ +│ column_labels │───▶│ code_category │───▶│ Select Widget │ +│ web_type='code' │ │ code_info │ │