96637a9cb6
- Integrated client IP address retrieval in the audit logging functionality across multiple controllers, including admin, common code, department, flow, screen, and table management. - Updated the `auditLogService` to include a new method for obtaining the client's IP address, ensuring accurate logging of user actions. - This enhancement improves traceability and accountability by capturing the source of requests, thereby strengthening the overall logging mechanism within the application.
591 lines
17 KiB
TypeScript
591 lines
17 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, prefix: beforeRule?.prefix },
|
|
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(
|
|
"/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;
|