채번 컴포넌트 생성
This commit is contained in:
@@ -64,6 +64,7 @@ import flowExternalDbConnectionRoutes from "./routes/flowExternalDbConnectionRou
|
||||
import workHistoryRoutes from "./routes/workHistoryRoutes"; // 작업 이력 관리
|
||||
import tableHistoryRoutes from "./routes/tableHistoryRoutes"; // 테이블 변경 이력 조회
|
||||
import roleRoutes from "./routes/roleRoutes"; // 권한 그룹 관리
|
||||
import numberingRuleController from "./controllers/numberingRuleController"; // 채번 규칙 관리
|
||||
import { BatchSchedulerService } from "./services/batchSchedulerService";
|
||||
// import collectionRoutes from "./routes/collectionRoutes"; // 임시 주석
|
||||
// import batchRoutes from "./routes/batchRoutes"; // 임시 주석
|
||||
@@ -222,6 +223,7 @@ app.use("/api/flow", flowRoutes); // 플로우 관리 (마지막에 등록하여
|
||||
app.use("/api/work-history", workHistoryRoutes); // 작업 이력 관리
|
||||
app.use("/api/table-history", tableHistoryRoutes); // 테이블 변경 이력 조회
|
||||
app.use("/api/roles", roleRoutes); // 권한 그룹 관리
|
||||
app.use("/api/numbering-rules", numberingRuleController); // 채번 규칙 관리
|
||||
// app.use("/api/collections", collectionRoutes); // 임시 주석
|
||||
// app.use("/api/batch", batchRoutes); // 임시 주석
|
||||
// app.use('/api/users', userRoutes);
|
||||
|
||||
131
backend-node/src/controllers/numberingRuleController.ts
Normal file
131
backend-node/src/controllers/numberingRuleController.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 채번 규칙 관리 컨트롤러
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from "express";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
import { numberingRuleService } from "../services/numberingRuleService";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// 규칙 목록 조회
|
||||
router.get("/", authenticateToken, async (req: Request, 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("/:ruleId", authenticateToken, async (req: Request, 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: Request, res: Response) => {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const ruleConfig = req.body;
|
||||
|
||||
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개 이상의 규칙 파트가 필요합니다" });
|
||||
}
|
||||
|
||||
const newRule = await numberingRuleService.createRule(ruleConfig, companyCode, userId);
|
||||
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("규칙 생성 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 규칙 수정
|
||||
router.put("/:ruleId", authenticateToken, async (req: Request, res: Response) => {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { ruleId } = req.params;
|
||||
const updates = req.body;
|
||||
|
||||
try {
|
||||
const updatedRule = await numberingRuleService.updateRule(ruleId, updates, companyCode);
|
||||
return res.json({ success: true, data: updatedRule });
|
||||
} 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.delete("/:ruleId", authenticateToken, async (req: Request, res: Response) => {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { ruleId } = req.params;
|
||||
|
||||
try {
|
||||
await numberingRuleService.deleteRule(ruleId, companyCode);
|
||||
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/generate", authenticateToken, async (req: Request, 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: { code: 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: Request, 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 });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
450
backend-node/src/services/numberingRuleService.ts
Normal file
450
backend-node/src/services/numberingRuleService.ts
Normal file
@@ -0,0 +1,450 @@
|
||||
/**
|
||||
* 채번 규칙 관리 서비스
|
||||
*/
|
||||
|
||||
import { getPool } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
interface NumberingRulePart {
|
||||
id?: number;
|
||||
order: number;
|
||||
partType: string;
|
||||
generationMethod: string;
|
||||
autoConfig?: any;
|
||||
manualConfig?: any;
|
||||
generatedValue?: string;
|
||||
}
|
||||
|
||||
interface NumberingRuleConfig {
|
||||
ruleId: string;
|
||||
ruleName: string;
|
||||
description?: string;
|
||||
parts: NumberingRulePart[];
|
||||
separator?: string;
|
||||
resetPeriod?: string;
|
||||
currentSequence?: number;
|
||||
tableName?: string;
|
||||
columnName?: string;
|
||||
companyCode?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
createdBy?: string;
|
||||
}
|
||||
|
||||
class NumberingRuleService {
|
||||
/**
|
||||
* 규칙 목록 조회
|
||||
*/
|
||||
async getRuleList(companyCode: string): Promise<NumberingRuleConfig[]> {
|
||||
try {
|
||||
logger.info("채번 규칙 목록 조회 시작", { companyCode });
|
||||
|
||||
const pool = getPool();
|
||||
const query = `
|
||||
SELECT
|
||||
rule_id AS "ruleId",
|
||||
rule_name AS "ruleName",
|
||||
description,
|
||||
separator,
|
||||
reset_period AS "resetPeriod",
|
||||
current_sequence AS "currentSequence",
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
company_code AS "companyCode",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt",
|
||||
created_by AS "createdBy"
|
||||
FROM numbering_rules
|
||||
WHERE company_code = $1 OR company_code = '*'
|
||||
ORDER BY created_at DESC
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [companyCode]);
|
||||
|
||||
// 각 규칙의 파트 정보 조회
|
||||
for (const rule of result.rows) {
|
||||
const partsQuery = `
|
||||
SELECT
|
||||
id,
|
||||
part_order AS "order",
|
||||
part_type AS "partType",
|
||||
generation_method AS "generationMethod",
|
||||
auto_config AS "autoConfig",
|
||||
manual_config AS "manualConfig"
|
||||
FROM numbering_rule_parts
|
||||
WHERE rule_id = $1 AND (company_code = $2 OR company_code = '*')
|
||||
ORDER BY part_order
|
||||
`;
|
||||
|
||||
const partsResult = await pool.query(partsQuery, [rule.ruleId, companyCode]);
|
||||
rule.parts = partsResult.rows;
|
||||
}
|
||||
|
||||
logger.info(`채번 규칙 목록 조회 완료: ${result.rows.length}개`, { companyCode });
|
||||
return result.rows;
|
||||
} catch (error: any) {
|
||||
logger.error(`채번 규칙 목록 조회 중 에러: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 규칙 조회
|
||||
*/
|
||||
async getRuleById(ruleId: string, companyCode: string): Promise<NumberingRuleConfig | null> {
|
||||
const pool = getPool();
|
||||
const query = `
|
||||
SELECT
|
||||
rule_id AS "ruleId",
|
||||
rule_name AS "ruleName",
|
||||
description,
|
||||
separator,
|
||||
reset_period AS "resetPeriod",
|
||||
current_sequence AS "currentSequence",
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
company_code AS "companyCode",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt",
|
||||
created_by AS "createdBy"
|
||||
FROM numbering_rules
|
||||
WHERE rule_id = $1 AND (company_code = $2 OR company_code = '*')
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [ruleId, companyCode]);
|
||||
if (result.rowCount === 0) return null;
|
||||
|
||||
const rule = result.rows[0];
|
||||
|
||||
const partsQuery = `
|
||||
SELECT
|
||||
id,
|
||||
part_order AS "order",
|
||||
part_type AS "partType",
|
||||
generation_method AS "generationMethod",
|
||||
auto_config AS "autoConfig",
|
||||
manual_config AS "manualConfig"
|
||||
FROM numbering_rule_parts
|
||||
WHERE rule_id = $1 AND (company_code = $2 OR company_code = '*')
|
||||
ORDER BY part_order
|
||||
`;
|
||||
|
||||
const partsResult = await pool.query(partsQuery, [ruleId, companyCode]);
|
||||
rule.parts = partsResult.rows;
|
||||
|
||||
return rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* 규칙 생성
|
||||
*/
|
||||
async createRule(
|
||||
config: NumberingRuleConfig,
|
||||
companyCode: string,
|
||||
userId: string
|
||||
): Promise<NumberingRuleConfig> {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 마스터 삽입
|
||||
const insertRuleQuery = `
|
||||
INSERT INTO numbering_rules (
|
||||
rule_id, rule_name, description, separator, reset_period,
|
||||
current_sequence, table_name, column_name, company_code, created_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING
|
||||
rule_id AS "ruleId",
|
||||
rule_name AS "ruleName",
|
||||
description,
|
||||
separator,
|
||||
reset_period AS "resetPeriod",
|
||||
current_sequence AS "currentSequence",
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
company_code AS "companyCode",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt",
|
||||
created_by AS "createdBy"
|
||||
`;
|
||||
|
||||
const ruleResult = await client.query(insertRuleQuery, [
|
||||
config.ruleId,
|
||||
config.ruleName,
|
||||
config.description || null,
|
||||
config.separator || "-",
|
||||
config.resetPeriod || "none",
|
||||
config.currentSequence || 1,
|
||||
config.tableName || null,
|
||||
config.columnName || null,
|
||||
companyCode,
|
||||
userId,
|
||||
]);
|
||||
|
||||
// 파트 삽입
|
||||
const parts: NumberingRulePart[] = [];
|
||||
for (const part of config.parts) {
|
||||
const insertPartQuery = `
|
||||
INSERT INTO numbering_rule_parts (
|
||||
rule_id, part_order, part_type, generation_method,
|
||||
auto_config, manual_config, company_code
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING
|
||||
id,
|
||||
part_order AS "order",
|
||||
part_type AS "partType",
|
||||
generation_method AS "generationMethod",
|
||||
auto_config AS "autoConfig",
|
||||
manual_config AS "manualConfig"
|
||||
`;
|
||||
|
||||
const partResult = await client.query(insertPartQuery, [
|
||||
config.ruleId,
|
||||
part.order,
|
||||
part.partType,
|
||||
part.generationMethod,
|
||||
JSON.stringify(part.autoConfig || {}),
|
||||
JSON.stringify(part.manualConfig || {}),
|
||||
companyCode,
|
||||
]);
|
||||
|
||||
parts.push(partResult.rows[0]);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
logger.info("채번 규칙 생성 완료", { ruleId: config.ruleId, companyCode });
|
||||
return { ...ruleResult.rows[0], parts };
|
||||
} catch (error: any) {
|
||||
await client.query("ROLLBACK");
|
||||
logger.error("채번 규칙 생성 실패", { error: error.message });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 규칙 수정
|
||||
*/
|
||||
async updateRule(
|
||||
ruleId: string,
|
||||
updates: Partial<NumberingRuleConfig>,
|
||||
companyCode: string
|
||||
): Promise<NumberingRuleConfig> {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
const updateRuleQuery = `
|
||||
UPDATE numbering_rules
|
||||
SET
|
||||
rule_name = COALESCE($1, rule_name),
|
||||
description = COALESCE($2, description),
|
||||
separator = COALESCE($3, separator),
|
||||
reset_period = COALESCE($4, reset_period),
|
||||
table_name = COALESCE($5, table_name),
|
||||
column_name = COALESCE($6, column_name),
|
||||
updated_at = NOW()
|
||||
WHERE rule_id = $7 AND company_code = $8
|
||||
RETURNING
|
||||
rule_id AS "ruleId",
|
||||
rule_name AS "ruleName",
|
||||
description,
|
||||
separator,
|
||||
reset_period AS "resetPeriod",
|
||||
current_sequence AS "currentSequence",
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
company_code AS "companyCode",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt",
|
||||
created_by AS "createdBy"
|
||||
`;
|
||||
|
||||
const ruleResult = await client.query(updateRuleQuery, [
|
||||
updates.ruleName,
|
||||
updates.description,
|
||||
updates.separator,
|
||||
updates.resetPeriod,
|
||||
updates.tableName,
|
||||
updates.columnName,
|
||||
ruleId,
|
||||
companyCode,
|
||||
]);
|
||||
|
||||
if (ruleResult.rowCount === 0) {
|
||||
throw new Error("규칙을 찾을 수 없거나 권한이 없습니다");
|
||||
}
|
||||
|
||||
// 파트 업데이트
|
||||
let parts: NumberingRulePart[] = [];
|
||||
if (updates.parts) {
|
||||
await client.query(
|
||||
"DELETE FROM numbering_rule_parts WHERE rule_id = $1 AND company_code = $2",
|
||||
[ruleId, companyCode]
|
||||
);
|
||||
|
||||
for (const part of updates.parts) {
|
||||
const insertPartQuery = `
|
||||
INSERT INTO numbering_rule_parts (
|
||||
rule_id, part_order, part_type, generation_method,
|
||||
auto_config, manual_config, company_code
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING
|
||||
id,
|
||||
part_order AS "order",
|
||||
part_type AS "partType",
|
||||
generation_method AS "generationMethod",
|
||||
auto_config AS "autoConfig",
|
||||
manual_config AS "manualConfig"
|
||||
`;
|
||||
|
||||
const partResult = await client.query(insertPartQuery, [
|
||||
ruleId,
|
||||
part.order,
|
||||
part.partType,
|
||||
part.generationMethod,
|
||||
JSON.stringify(part.autoConfig || {}),
|
||||
JSON.stringify(part.manualConfig || {}),
|
||||
companyCode,
|
||||
]);
|
||||
|
||||
parts.push(partResult.rows[0]);
|
||||
}
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
logger.info("채번 규칙 수정 완료", { ruleId, companyCode });
|
||||
return { ...ruleResult.rows[0], parts };
|
||||
} catch (error: any) {
|
||||
await client.query("ROLLBACK");
|
||||
logger.error("채번 규칙 수정 실패", { error: error.message });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 규칙 삭제
|
||||
*/
|
||||
async deleteRule(ruleId: string, companyCode: string): Promise<void> {
|
||||
const pool = getPool();
|
||||
const query = `
|
||||
DELETE FROM numbering_rules
|
||||
WHERE rule_id = $1 AND company_code = $2
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [ruleId, companyCode]);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
throw new Error("규칙을 찾을 수 없거나 권한이 없습니다");
|
||||
}
|
||||
|
||||
logger.info("채번 규칙 삭제 완료", { ruleId, companyCode });
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드 생성
|
||||
*/
|
||||
async generateCode(ruleId: string, companyCode: string): Promise<string> {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
const rule = await this.getRuleById(ruleId, companyCode);
|
||||
if (!rule) throw new Error("규칙을 찾을 수 없습니다");
|
||||
|
||||
const parts = rule.parts
|
||||
.sort((a: any, b: any) => a.order - b.order)
|
||||
.map((part: any) => {
|
||||
if (part.generationMethod === "manual") {
|
||||
return part.manualConfig?.value || "";
|
||||
}
|
||||
|
||||
const autoConfig = part.autoConfig || {};
|
||||
|
||||
switch (part.partType) {
|
||||
case "prefix":
|
||||
return autoConfig.prefix || "PREFIX";
|
||||
|
||||
case "sequence": {
|
||||
const length = autoConfig.sequenceLength || 4;
|
||||
return String(rule.currentSequence || 1).padStart(length, "0");
|
||||
}
|
||||
|
||||
case "date":
|
||||
return this.formatDate(new Date(), autoConfig.dateFormat || "YYYYMMDD");
|
||||
|
||||
case "year": {
|
||||
const format = autoConfig.dateFormat || "YYYY";
|
||||
const year = new Date().getFullYear();
|
||||
return format === "YY" ? String(year).slice(-2) : String(year);
|
||||
}
|
||||
|
||||
case "month":
|
||||
return String(new Date().getMonth() + 1).padStart(2, "0");
|
||||
|
||||
case "custom":
|
||||
return autoConfig.value || "CUSTOM";
|
||||
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
const generatedCode = parts.join(rule.separator || "");
|
||||
|
||||
const hasSequence = rule.parts.some((p: any) => p.partType === "sequence");
|
||||
if (hasSequence) {
|
||||
await client.query(
|
||||
"UPDATE numbering_rules SET current_sequence = current_sequence + 1 WHERE rule_id = $1 AND company_code = $2",
|
||||
[ruleId, companyCode]
|
||||
);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
logger.info("코드 생성 완료", { ruleId, generatedCode });
|
||||
return generatedCode;
|
||||
} catch (error: any) {
|
||||
await client.query("ROLLBACK");
|
||||
logger.error("코드 생성 실패", { error: error.message });
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
private formatDate(date: Date, format: string): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
|
||||
switch (format) {
|
||||
case "YYYY": return String(year);
|
||||
case "YY": return String(year).slice(-2);
|
||||
case "YYYYMM": return `${year}${month}`;
|
||||
case "YYMM": return `${String(year).slice(-2)}${month}`;
|
||||
case "YYYYMMDD": return `${year}${month}${day}`;
|
||||
case "YYMMDD": return `${String(year).slice(-2)}${month}${day}`;
|
||||
default: return `${year}${month}${day}`;
|
||||
}
|
||||
}
|
||||
|
||||
async resetSequence(ruleId: string, companyCode: string): Promise<void> {
|
||||
const pool = getPool();
|
||||
await pool.query(
|
||||
"UPDATE numbering_rules SET current_sequence = 1, updated_at = NOW() WHERE rule_id = $1 AND company_code = $2",
|
||||
[ruleId, companyCode]
|
||||
);
|
||||
logger.info("시퀀스 초기화 완료", { ruleId, companyCode });
|
||||
}
|
||||
}
|
||||
|
||||
export const numberingRuleService = new NumberingRuleService();
|
||||
Reference in New Issue
Block a user