채번 자동생성기능
This commit is contained in:
@@ -26,6 +26,8 @@ interface NumberingRuleConfig {
|
||||
tableName?: string;
|
||||
columnName?: string;
|
||||
companyCode?: string;
|
||||
menuObjid?: number;
|
||||
scopeType?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
createdBy?: string;
|
||||
@@ -33,7 +35,7 @@ interface NumberingRuleConfig {
|
||||
|
||||
class NumberingRuleService {
|
||||
/**
|
||||
* 규칙 목록 조회
|
||||
* 규칙 목록 조회 (전체)
|
||||
*/
|
||||
async getRuleList(companyCode: string): Promise<NumberingRuleConfig[]> {
|
||||
try {
|
||||
@@ -78,11 +80,16 @@ class NumberingRuleService {
|
||||
ORDER BY part_order
|
||||
`;
|
||||
|
||||
const partsResult = await pool.query(partsQuery, [rule.ruleId, companyCode]);
|
||||
const partsResult = await pool.query(partsQuery, [
|
||||
rule.ruleId,
|
||||
companyCode,
|
||||
]);
|
||||
rule.parts = partsResult.rows;
|
||||
}
|
||||
|
||||
logger.info(`채번 규칙 목록 조회 완료: ${result.rows.length}개`, { companyCode });
|
||||
logger.info(`채번 규칙 목록 조회 완료: ${result.rows.length}개`, {
|
||||
companyCode,
|
||||
});
|
||||
return result.rows;
|
||||
} catch (error: any) {
|
||||
logger.error(`채번 규칙 목록 조회 중 에러: ${error.message}`);
|
||||
@@ -90,10 +97,170 @@ class NumberingRuleService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 메뉴에서 사용 가능한 규칙 목록 조회
|
||||
*/
|
||||
async getAvailableRulesForMenu(
|
||||
companyCode: string,
|
||||
menuObjid?: number
|
||||
): Promise<NumberingRuleConfig[]> {
|
||||
try {
|
||||
logger.info("메뉴별 사용 가능한 채번 규칙 조회 시작", {
|
||||
companyCode,
|
||||
menuObjid,
|
||||
});
|
||||
|
||||
const pool = getPool();
|
||||
|
||||
// menuObjid가 없으면 global 규칙만 반환
|
||||
if (!menuObjid) {
|
||||
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",
|
||||
menu_objid AS "menuObjid",
|
||||
scope_type AS "scopeType",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt",
|
||||
created_by AS "createdBy"
|
||||
FROM numbering_rules
|
||||
WHERE (company_code = $1 OR company_code = '*')
|
||||
AND scope_type = 'global'
|
||||
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;
|
||||
}
|
||||
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
// 현재 메뉴의 상위 계층 조회 (2레벨 메뉴 찾기)
|
||||
const menuHierarchyQuery = `
|
||||
WITH RECURSIVE menu_path AS (
|
||||
SELECT objid, objid_parent, menu_level
|
||||
FROM menu_info
|
||||
WHERE objid = $1
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT mi.objid, mi.objid_parent, mi.menu_level
|
||||
FROM menu_info mi
|
||||
INNER JOIN menu_path mp ON mi.objid = mp.objid_parent
|
||||
)
|
||||
SELECT objid, menu_level
|
||||
FROM menu_path
|
||||
WHERE menu_level = 2
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
const hierarchyResult = await pool.query(menuHierarchyQuery, [menuObjid]);
|
||||
const level2MenuObjid =
|
||||
hierarchyResult.rowCount > 0 ? hierarchyResult.rows[0].objid : null;
|
||||
|
||||
// 사용 가능한 규칙 조회
|
||||
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",
|
||||
menu_objid AS "menuObjid",
|
||||
scope_type AS "scopeType",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt",
|
||||
created_by AS "createdBy"
|
||||
FROM numbering_rules
|
||||
WHERE (company_code = $1 OR company_code = '*')
|
||||
AND (
|
||||
scope_type = 'global'
|
||||
OR (scope_type = 'menu' AND menu_objid = $2)
|
||||
)
|
||||
ORDER BY scope_type DESC, created_at DESC
|
||||
`;
|
||||
|
||||
const result = await pool.query(query, [companyCode, level2MenuObjid]);
|
||||
|
||||
// 파트 정보 추가
|
||||
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("메뉴별 사용 가능한 채번 규칙 조회 완료", {
|
||||
companyCode,
|
||||
menuObjid,
|
||||
level2MenuObjid,
|
||||
count: result.rowCount,
|
||||
});
|
||||
|
||||
return result.rows;
|
||||
} catch (error: any) {
|
||||
logger.error("메뉴별 채번 규칙 조회 실패", {
|
||||
error: error.message,
|
||||
companyCode,
|
||||
menuObjid,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 규칙 조회
|
||||
*/
|
||||
async getRuleById(ruleId: string, companyCode: string): Promise<NumberingRuleConfig | null> {
|
||||
async getRuleById(
|
||||
ruleId: string,
|
||||
companyCode: string
|
||||
): Promise<NumberingRuleConfig | null> {
|
||||
const pool = getPool();
|
||||
const query = `
|
||||
SELECT
|
||||
@@ -106,7 +273,7 @@ class NumberingRuleService {
|
||||
table_name AS "tableName",
|
||||
column_name AS "columnName",
|
||||
company_code AS "companyCode",
|
||||
menu_id AS "menuId",
|
||||
menu_objid AS "menuObjid",
|
||||
scope_type AS "scopeType",
|
||||
created_at AS "createdAt",
|
||||
updated_at AS "updatedAt",
|
||||
@@ -223,7 +390,10 @@ class NumberingRuleService {
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
logger.info("채번 규칙 생성 완료", { ruleId: config.ruleId, companyCode });
|
||||
logger.info("채번 규칙 생성 완료", {
|
||||
ruleId: config.ruleId,
|
||||
companyCode,
|
||||
});
|
||||
return { ...ruleResult.rows[0], parts };
|
||||
} catch (error: any) {
|
||||
await client.query("ROLLBACK");
|
||||
@@ -364,9 +534,63 @@ class NumberingRuleService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드 생성
|
||||
* 코드 미리보기 (순번 증가 없음)
|
||||
*/
|
||||
async generateCode(ruleId: string, companyCode: string): Promise<string> {
|
||||
async previewCode(ruleId: string, companyCode: string): Promise<string> {
|
||||
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 "sequence": {
|
||||
// 순번 (현재 순번으로 미리보기, 증가 안 함)
|
||||
const length = autoConfig.sequenceLength || 4;
|
||||
return String(rule.currentSequence || 1).padStart(length, "0");
|
||||
}
|
||||
|
||||
case "number": {
|
||||
// 숫자 (고정 자릿수)
|
||||
const length = autoConfig.numberLength || 4;
|
||||
const value = autoConfig.numberValue || 1;
|
||||
return String(value).padStart(length, "0");
|
||||
}
|
||||
|
||||
case "date": {
|
||||
// 날짜 (다양한 날짜 형식)
|
||||
return this.formatDate(
|
||||
new Date(),
|
||||
autoConfig.dateFormat || "YYYYMMDD"
|
||||
);
|
||||
}
|
||||
|
||||
case "text": {
|
||||
// 텍스트 (고정 문자열)
|
||||
return autoConfig.textValue || "TEXT";
|
||||
}
|
||||
|
||||
default:
|
||||
logger.warn("알 수 없는 파트 타입", { partType: part.partType });
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
const previewCode = parts.join(rule.separator || "");
|
||||
logger.info("코드 미리보기 생성", { ruleId, previewCode, companyCode });
|
||||
return previewCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 코드 할당 (저장 시점에 실제 순번 증가)
|
||||
*/
|
||||
async allocateCode(ruleId: string, companyCode: string): Promise<string> {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
@@ -386,37 +610,44 @@ class NumberingRuleService {
|
||||
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 "number": {
|
||||
// 숫자 (고정 자릿수)
|
||||
const length = autoConfig.numberLength || 4;
|
||||
const value = autoConfig.numberValue || 1;
|
||||
return String(value).padStart(length, "0");
|
||||
}
|
||||
|
||||
case "month":
|
||||
return String(new Date().getMonth() + 1).padStart(2, "0");
|
||||
case "date": {
|
||||
// 날짜 (다양한 날짜 형식)
|
||||
return this.formatDate(
|
||||
new Date(),
|
||||
autoConfig.dateFormat || "YYYYMMDD"
|
||||
);
|
||||
}
|
||||
|
||||
case "custom":
|
||||
return autoConfig.value || "CUSTOM";
|
||||
case "text": {
|
||||
// 텍스트 (고정 문자열)
|
||||
return autoConfig.textValue || "TEXT";
|
||||
}
|
||||
|
||||
default:
|
||||
logger.warn("알 수 없는 파트 타입", { partType: part.partType });
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
const generatedCode = parts.join(rule.separator || "");
|
||||
const allocatedCode = parts.join(rule.separator || "");
|
||||
|
||||
const hasSequence = rule.parts.some((p: any) => p.partType === "sequence");
|
||||
// 순번이 있는 경우에만 증가
|
||||
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",
|
||||
@@ -425,30 +656,52 @@ class NumberingRuleService {
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
logger.info("코드 생성 완료", { ruleId, generatedCode });
|
||||
return generatedCode;
|
||||
logger.info("코드 할당 완료", { ruleId, allocatedCode, companyCode });
|
||||
return allocatedCode;
|
||||
} catch (error: any) {
|
||||
await client.query("ROLLBACK");
|
||||
logger.error("코드 생성 실패", { error: error.message });
|
||||
logger.error("코드 할당 실패", {
|
||||
ruleId,
|
||||
companyCode,
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 기존 generateCode는 allocateCode를 사용하세요
|
||||
*/
|
||||
async generateCode(ruleId: string, companyCode: string): Promise<string> {
|
||||
logger.warn(
|
||||
"generateCode는 deprecated 되었습니다. previewCode 또는 allocateCode를 사용하세요"
|
||||
);
|
||||
return this.allocateCode(ruleId, companyCode);
|
||||
}
|
||||
|
||||
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}`;
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user