- Implemented a new API endpoint to retrieve numbering rules based on table and column names, enhancing the flexibility of numbering rule management. - Added a new service method to handle the retrieval of numbering columns specific to a company, ensuring proper company code filtering. - Updated the frontend to load and display numbering columns, allowing users to select and manage numbering rules effectively. - Refactored existing logic to improve the handling of numbering rules, including fallback mechanisms for legacy data. These changes enhance the functionality and user experience in managing numbering rules within the application.
615 lines
18 KiB
TypeScript
615 lines
18 KiB
TypeScript
/**
|
|
* 채번 규칙 관리 컨트롤러
|
|
*/
|
|
|
|
import { Router, Response } from "express";
|
|
import {
|
|
authenticateToken,
|
|
AuthenticatedRequest,
|
|
} from "../middleware/authMiddleware";
|
|
import { numberingRuleService } from "../services/numberingRuleService";
|
|
import { logger } from "../utils/logger";
|
|
import { auditLogService, getClientIp } from "../services/auditLogService";
|
|
|
|
const router = Router();
|
|
|
|
// 규칙 목록 조회 (전체)
|
|
router.get(
|
|
"/",
|
|
authenticateToken,
|
|
async (req: AuthenticatedRequest, res: Response) => {
|
|
const companyCode = req.user!.companyCode;
|
|
|
|
try {
|
|
const rules = await numberingRuleService.getRuleList(companyCode);
|
|
return res.json({ success: true, data: rules });
|
|
} catch (error: any) {
|
|
logger.error("규칙 목록 조회 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
// 메뉴별 사용 가능한 규칙 조회
|
|
router.get(
|
|
"/available/:menuObjid?",
|
|
authenticateToken,
|
|
async (req: AuthenticatedRequest, res: Response) => {
|
|
const companyCode = req.user!.companyCode;
|
|
const menuObjid = req.params.menuObjid
|
|
? parseInt(req.params.menuObjid)
|
|
: undefined;
|
|
|
|
logger.info("메뉴별 채번 규칙 조회 요청", { menuObjid, companyCode });
|
|
|
|
try {
|
|
const rules = await numberingRuleService.getAvailableRulesForMenu(
|
|
companyCode,
|
|
menuObjid
|
|
);
|
|
|
|
logger.info("✅ 메뉴별 채번 규칙 조회 성공 (컨트롤러)", {
|
|
companyCode,
|
|
menuObjid,
|
|
rulesCount: rules.length,
|
|
});
|
|
|
|
return res.json({ success: true, data: rules });
|
|
} catch (error: any) {
|
|
logger.error("❌ 메뉴별 사용 가능한 규칙 조회 실패 (컨트롤러)", {
|
|
error: error.message,
|
|
errorCode: error.code,
|
|
errorStack: error.stack,
|
|
companyCode,
|
|
menuObjid,
|
|
});
|
|
return res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
// 화면용 채번 규칙 조회 (테이블 기반 필터링 - 간소화)
|
|
router.get(
|
|
"/available-for-screen",
|
|
authenticateToken,
|
|
async (req: AuthenticatedRequest, res: Response) => {
|
|
const companyCode = req.user!.companyCode;
|
|
const { tableName } = req.query;
|
|
|
|
try {
|
|
// tableName 필수 검증
|
|
if (!tableName || typeof tableName !== "string") {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: "tableName is required",
|
|
});
|
|
}
|
|
|
|
const rules = await numberingRuleService.getAvailableRulesForScreen(
|
|
companyCode,
|
|
tableName
|
|
);
|
|
|
|
logger.info("화면용 채번 규칙 조회 성공", {
|
|
companyCode,
|
|
tableName,
|
|
count: rules.length,
|
|
});
|
|
|
|
return res.json({ success: true, data: rules });
|
|
} catch (error: any) {
|
|
logger.error("화면용 채번 규칙 조회 실패", {
|
|
error: error.message,
|
|
tableName,
|
|
});
|
|
return res.status(500).json({
|
|
success: false,
|
|
error: error.message,
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// 특정 규칙 조회
|
|
router.get(
|
|
"/:ruleId",
|
|
authenticateToken,
|
|
async (req: AuthenticatedRequest, res: Response) => {
|
|
const companyCode = req.user!.companyCode;
|
|
const { ruleId } = req.params;
|
|
|
|
try {
|
|
const rule = await numberingRuleService.getRuleById(ruleId, companyCode);
|
|
if (!rule) {
|
|
return res
|
|
.status(404)
|
|
.json({ success: false, error: "규칙을 찾을 수 없습니다" });
|
|
}
|
|
return res.json({ success: true, data: rule });
|
|
} catch (error: any) {
|
|
logger.error("규칙 조회 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
// 규칙 생성
|
|
router.post(
|
|
"/",
|
|
authenticateToken,
|
|
async (req: AuthenticatedRequest, res: Response) => {
|
|
const companyCode = req.user!.companyCode;
|
|
const userId = req.user!.userId;
|
|
const ruleConfig = req.body;
|
|
|
|
logger.info("🔍 [POST /numbering-rules] 채번 규칙 생성 요청:", {
|
|
companyCode,
|
|
userId,
|
|
ruleId: ruleConfig.ruleId,
|
|
ruleName: ruleConfig.ruleName,
|
|
scopeType: ruleConfig.scopeType,
|
|
menuObjid: ruleConfig.menuObjid,
|
|
tableName: ruleConfig.tableName,
|
|
partsCount: ruleConfig.parts?.length,
|
|
});
|
|
|
|
try {
|
|
if (!ruleConfig.ruleId || !ruleConfig.ruleName) {
|
|
return res
|
|
.status(400)
|
|
.json({ success: false, error: "규칙 ID와 규칙명은 필수입니다" });
|
|
}
|
|
|
|
if (!Array.isArray(ruleConfig.parts) || ruleConfig.parts.length === 0) {
|
|
return res
|
|
.status(400)
|
|
.json({
|
|
success: false,
|
|
error: "최소 1개 이상의 규칙 파트가 필요합니다",
|
|
});
|
|
}
|
|
|
|
// 🆕 scopeType이 'table'인 경우 tableName 필수 체크
|
|
if (ruleConfig.scopeType === "table") {
|
|
if (!ruleConfig.tableName || ruleConfig.tableName.trim() === "") {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: "테이블 범위 규칙은 테이블명(tableName)이 필수입니다",
|
|
});
|
|
}
|
|
}
|
|
|
|
const newRule = await numberingRuleService.createRule(
|
|
ruleConfig,
|
|
companyCode,
|
|
userId
|
|
);
|
|
|
|
logger.info("✅ [POST /numbering-rules] 채번 규칙 생성 성공:", {
|
|
ruleId: newRule.ruleId,
|
|
menuObjid: newRule.menuObjid,
|
|
});
|
|
|
|
auditLogService.log({
|
|
companyCode,
|
|
userId,
|
|
action: "CREATE",
|
|
resourceType: "NUMBERING_RULE",
|
|
resourceId: String(newRule.ruleId),
|
|
resourceName: ruleConfig.ruleName,
|
|
summary: `채번 규칙 "${ruleConfig.ruleName}" 생성`,
|
|
changes: { after: { ruleName: ruleConfig.ruleName, prefix: ruleConfig.prefix } },
|
|
ipAddress: getClientIp(req),
|
|
requestPath: req.originalUrl,
|
|
});
|
|
|
|
return res.status(201).json({ success: true, data: newRule });
|
|
} catch (error: any) {
|
|
if (error.code === "23505") {
|
|
return res
|
|
.status(409)
|
|
.json({ success: false, error: "이미 존재하는 규칙 ID입니다" });
|
|
}
|
|
logger.error("❌ [POST /numbering-rules] 규칙 생성 실패:", {
|
|
error: error.message,
|
|
stack: error.stack,
|
|
code: error.code,
|
|
});
|
|
return res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
// 규칙 수정
|
|
router.put(
|
|
"/:ruleId",
|
|
authenticateToken,
|
|
async (req: AuthenticatedRequest, res: Response) => {
|
|
const companyCode = req.user!.companyCode;
|
|
const { ruleId } = req.params;
|
|
const updates = req.body;
|
|
|
|
logger.info("채번 규칙 수정 요청", { ruleId, companyCode, updates });
|
|
|
|
try {
|
|
const beforeRule = await numberingRuleService.getRuleById(ruleId, companyCode);
|
|
const updatedRule = await numberingRuleService.updateRule(
|
|
ruleId,
|
|
updates,
|
|
companyCode
|
|
);
|
|
logger.info("채번 규칙 수정 성공", { ruleId, companyCode });
|
|
|
|
auditLogService.log({
|
|
companyCode,
|
|
userId: req.user?.userId || "",
|
|
action: "UPDATE",
|
|
resourceType: "NUMBERING_RULE",
|
|
resourceId: ruleId,
|
|
summary: `채번 규칙(ID:${ruleId}) 수정`,
|
|
changes: {
|
|
before: { ruleName: beforeRule?.ruleName, separator: beforeRule?.separator },
|
|
after: updates,
|
|
},
|
|
ipAddress: getClientIp(req),
|
|
requestPath: req.originalUrl,
|
|
});
|
|
|
|
return res.json({ success: true, data: updatedRule });
|
|
} catch (error: any) {
|
|
logger.error("채번 규칙 수정 실패", {
|
|
ruleId,
|
|
companyCode,
|
|
error: error.message,
|
|
stack: error.stack,
|
|
});
|
|
if (error.message.includes("찾을 수 없거나")) {
|
|
return res.status(404).json({ success: false, error: error.message });
|
|
}
|
|
return res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
// 규칙 삭제
|
|
router.delete(
|
|
"/:ruleId",
|
|
authenticateToken,
|
|
async (req: AuthenticatedRequest, res: Response) => {
|
|
const companyCode = req.user!.companyCode;
|
|
const { ruleId } = req.params;
|
|
|
|
try {
|
|
await numberingRuleService.deleteRule(ruleId, companyCode);
|
|
|
|
auditLogService.log({
|
|
companyCode,
|
|
userId: req.user?.userId || "",
|
|
action: "DELETE",
|
|
resourceType: "NUMBERING_RULE",
|
|
resourceId: ruleId,
|
|
summary: `채번 규칙(ID:${ruleId}) 삭제`,
|
|
ipAddress: getClientIp(req),
|
|
requestPath: req.originalUrl,
|
|
});
|
|
|
|
return res.json({ success: true, message: "규칙이 삭제되었습니다" });
|
|
} catch (error: any) {
|
|
if (error.message.includes("찾을 수 없거나")) {
|
|
return res.status(404).json({ success: false, error: error.message });
|
|
}
|
|
logger.error("규칙 삭제 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
// 코드 미리보기 (순번 증가 없음)
|
|
router.post(
|
|
"/:ruleId/preview",
|
|
authenticateToken,
|
|
async (req: AuthenticatedRequest, res: Response) => {
|
|
const companyCode = req.user!.companyCode;
|
|
const { ruleId } = req.params;
|
|
const { formData } = req.body; // 폼 데이터 (카테고리 기반 채번 시 사용)
|
|
|
|
try {
|
|
const previewCode = await numberingRuleService.previewCode(
|
|
ruleId,
|
|
companyCode,
|
|
formData
|
|
);
|
|
return res.json({ success: true, data: { generatedCode: previewCode } });
|
|
} catch (error: any) {
|
|
logger.error("코드 미리보기 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
// 코드 할당 (저장 시점에 실제 순번 증가)
|
|
router.post(
|
|
"/:ruleId/allocate",
|
|
authenticateToken,
|
|
async (req: AuthenticatedRequest, res: Response) => {
|
|
const companyCode = req.user!.companyCode;
|
|
const { ruleId } = req.params;
|
|
const { formData, userInputCode } = req.body; // 폼 데이터 + 사용자가 편집한 코드
|
|
|
|
logger.info("코드 할당 요청", {
|
|
ruleId,
|
|
companyCode,
|
|
hasFormData: !!formData,
|
|
userInputCode,
|
|
});
|
|
|
|
try {
|
|
const allocatedCode = await numberingRuleService.allocateCode(
|
|
ruleId,
|
|
companyCode,
|
|
formData,
|
|
userInputCode
|
|
);
|
|
logger.info("코드 할당 성공", { ruleId, allocatedCode });
|
|
return res.json({
|
|
success: true,
|
|
data: { generatedCode: allocatedCode },
|
|
});
|
|
} catch (error: any) {
|
|
logger.error("코드 할당 실패", {
|
|
ruleId,
|
|
companyCode,
|
|
error: error.message,
|
|
});
|
|
return res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
// 코드 생성 (기존 호환성 유지, deprecated)
|
|
router.post(
|
|
"/:ruleId/generate",
|
|
authenticateToken,
|
|
async (req: AuthenticatedRequest, res: Response) => {
|
|
const companyCode = req.user!.companyCode;
|
|
const { ruleId } = req.params;
|
|
|
|
try {
|
|
const generatedCode = await numberingRuleService.generateCode(
|
|
ruleId,
|
|
companyCode
|
|
);
|
|
return res.json({ success: true, data: { generatedCode } });
|
|
} catch (error: any) {
|
|
logger.error("코드 생성 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
// 시퀀스 초기화
|
|
router.post(
|
|
"/:ruleId/reset",
|
|
authenticateToken,
|
|
async (req: AuthenticatedRequest, res: Response) => {
|
|
const companyCode = req.user!.companyCode;
|
|
const { ruleId } = req.params;
|
|
|
|
try {
|
|
await numberingRuleService.resetSequence(ruleId, companyCode);
|
|
return res.json({ success: true, message: "시퀀스가 초기화되었습니다" });
|
|
} catch (error: any) {
|
|
logger.error("시퀀스 초기화 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
// 테이블+컬럼 기반 채번 규칙 조회 (메인 API)
|
|
router.get(
|
|
"/by-column/:tableName/:columnName",
|
|
authenticateToken,
|
|
async (req: AuthenticatedRequest, res: Response) => {
|
|
const companyCode = req.user!.companyCode;
|
|
const { tableName, columnName } = req.params;
|
|
|
|
try {
|
|
const rule = await numberingRuleService.getNumberingRuleByColumn(
|
|
companyCode,
|
|
tableName,
|
|
columnName
|
|
);
|
|
return res.json({ success: true, data: rule });
|
|
} catch (error: any) {
|
|
logger.error("테이블+컬럼 기반 채번 규칙 조회 실패", {
|
|
error: error.message,
|
|
});
|
|
return res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
// ==================== 테스트 테이블용 API ====================
|
|
|
|
// [테스트] 테스트 테이블에서 채번 규칙 목록 조회
|
|
router.get(
|
|
"/test/list/:menuObjid?",
|
|
authenticateToken,
|
|
async (req: AuthenticatedRequest, res: Response) => {
|
|
const companyCode = req.user!.companyCode;
|
|
const menuObjid = req.params.menuObjid
|
|
? parseInt(req.params.menuObjid)
|
|
: undefined;
|
|
|
|
logger.info("[테스트] 채번 규칙 목록 조회 요청", {
|
|
companyCode,
|
|
menuObjid,
|
|
});
|
|
|
|
try {
|
|
const rules = await numberingRuleService.getRulesFromTest(
|
|
companyCode,
|
|
menuObjid
|
|
);
|
|
logger.info("[테스트] 채번 규칙 목록 조회 성공", {
|
|
companyCode,
|
|
menuObjid,
|
|
count: rules.length,
|
|
});
|
|
return res.json({ success: true, data: rules });
|
|
} catch (error: any) {
|
|
logger.error("[테스트] 채번 규칙 목록 조회 실패", {
|
|
error: error.message,
|
|
});
|
|
return res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
// [테스트] 테이블+컬럼 기반 채번 규칙 조회
|
|
router.get(
|
|
"/test/by-column/:tableName/:columnName",
|
|
authenticateToken,
|
|
async (req: AuthenticatedRequest, res: Response) => {
|
|
const companyCode = req.user!.companyCode;
|
|
const { tableName, columnName } = req.params;
|
|
|
|
try {
|
|
const rule = await numberingRuleService.getNumberingRuleByColumn(
|
|
companyCode,
|
|
tableName,
|
|
columnName
|
|
);
|
|
return res.json({ success: true, data: rule });
|
|
} catch (error: any) {
|
|
logger.error("테이블+컬럼 기반 채번 규칙 조회 실패", {
|
|
error: error.message,
|
|
});
|
|
return res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
// [테스트] 테스트 테이블에 채번 규칙 저장
|
|
// 채번 규칙은 독립적으로 생성 가능 (나중에 테이블 타입 관리에서 컬럼에 연결)
|
|
router.post(
|
|
"/test/save",
|
|
authenticateToken,
|
|
async (req: AuthenticatedRequest, res: Response) => {
|
|
const companyCode = req.user!.companyCode;
|
|
const userId = req.user!.userId;
|
|
const ruleConfig = req.body;
|
|
|
|
logger.info("[테스트] 채번 규칙 저장 요청", {
|
|
ruleId: ruleConfig.ruleId,
|
|
ruleName: ruleConfig.ruleName,
|
|
tableName: ruleConfig.tableName || "(미지정)",
|
|
columnName: ruleConfig.columnName || "(미지정)",
|
|
});
|
|
|
|
try {
|
|
// ruleName만 필수, tableName/columnName은 선택 (나중에 테이블 타입 관리에서 연결)
|
|
if (!ruleConfig.ruleName) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: "ruleName is required",
|
|
});
|
|
}
|
|
|
|
const savedRule = await numberingRuleService.saveRuleToTest(
|
|
ruleConfig,
|
|
companyCode,
|
|
userId
|
|
);
|
|
return res.json({ success: true, data: savedRule });
|
|
} catch (error: any) {
|
|
logger.error("[테스트] 채번 규칙 저장 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
// [테스트] 테스트 테이블에서 채번 규칙 삭제
|
|
router.delete(
|
|
"/test/:ruleId",
|
|
authenticateToken,
|
|
async (req: AuthenticatedRequest, res: Response) => {
|
|
const companyCode = req.user!.companyCode;
|
|
const { ruleId } = req.params;
|
|
|
|
try {
|
|
await numberingRuleService.deleteRuleFromTest(ruleId, companyCode);
|
|
return res.json({
|
|
success: true,
|
|
message: "테스트 채번 규칙이 삭제되었습니다",
|
|
});
|
|
} catch (error: any) {
|
|
logger.error("[테스트] 채번 규칙 삭제 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
// [테스트] 코드 미리보기 (테스트 테이블 사용)
|
|
router.post(
|
|
"/test/:ruleId/preview",
|
|
authenticateToken,
|
|
async (req: AuthenticatedRequest, res: Response) => {
|
|
const companyCode = req.user!.companyCode;
|
|
const { ruleId } = req.params;
|
|
const { formData } = req.body;
|
|
|
|
try {
|
|
const previewCode = await numberingRuleService.previewCode(
|
|
ruleId,
|
|
companyCode,
|
|
formData
|
|
);
|
|
return res.json({ success: true, data: { generatedCode: previewCode } });
|
|
} catch (error: any) {
|
|
logger.error("[테스트] 코드 미리보기 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
// ==================== 회사별 채번규칙 복제 API ====================
|
|
|
|
// 회사별 채번규칙 복제
|
|
router.post(
|
|
"/copy-for-company",
|
|
authenticateToken,
|
|
async (req: AuthenticatedRequest, res: Response) => {
|
|
const userCompanyCode = req.user!.companyCode;
|
|
const { sourceCompanyCode, targetCompanyCode } = req.body;
|
|
|
|
// 최고 관리자만 사용 가능
|
|
if (userCompanyCode !== "*") {
|
|
return res.status(403).json({
|
|
success: false,
|
|
error: "최고 관리자만 사용할 수 있습니다",
|
|
});
|
|
}
|
|
|
|
if (!sourceCompanyCode || !targetCompanyCode) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: "sourceCompanyCode와 targetCompanyCode가 필요합니다",
|
|
});
|
|
}
|
|
|
|
try {
|
|
const result = await numberingRuleService.copyRulesForCompany(
|
|
sourceCompanyCode,
|
|
targetCompanyCode
|
|
);
|
|
return res.json({ success: true, data: result });
|
|
} catch (error: any) {
|
|
logger.error("회사별 채번규칙 복제 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, error: error.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
export default router;
|