1064 lines
33 KiB
TypeScript
1064 lines
33 KiB
TypeScript
/**
|
|
* 채번 규칙 관리 서비스
|
|
*/
|
|
|
|
import { getPool } from "../database/db";
|
|
import { logger } from "../utils/logger";
|
|
import { getMenuAndChildObjids } from "./menuService";
|
|
|
|
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;
|
|
menuObjid?: number;
|
|
scopeType?: string;
|
|
createdAt?: string;
|
|
updatedAt?: string;
|
|
createdBy?: string;
|
|
}
|
|
|
|
class NumberingRuleService {
|
|
/**
|
|
* 규칙 목록 조회 (전체)
|
|
*/
|
|
async getRuleList(companyCode: string): Promise<NumberingRuleConfig[]> {
|
|
try {
|
|
logger.info("채번 규칙 목록 조회 시작", { companyCode });
|
|
|
|
const pool = getPool();
|
|
|
|
// 멀티테넌시: 최고 관리자만 company_code="*" 데이터를 볼 수 있음
|
|
let query: string;
|
|
let params: any[];
|
|
|
|
if (companyCode === "*") {
|
|
// 최고 관리자: 모든 회사 데이터 조회 가능
|
|
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
|
|
ORDER BY created_at DESC
|
|
`;
|
|
params = [];
|
|
logger.info("최고 관리자 전체 채번 규칙 조회");
|
|
} else {
|
|
// 일반 회사: 자신의 회사 데이터만 조회 (company_code="*" 제외)
|
|
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
|
|
ORDER BY created_at DESC
|
|
`;
|
|
params = [companyCode];
|
|
logger.info("회사별 채번 규칙 조회", { companyCode });
|
|
}
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
// 각 규칙의 파트 정보 조회
|
|
for (const rule of result.rows) {
|
|
let partsQuery: string;
|
|
let partsParams: any[];
|
|
|
|
if (companyCode === "*") {
|
|
// 최고 관리자: 모든 파트 조회
|
|
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
|
|
ORDER BY part_order
|
|
`;
|
|
partsParams = [rule.ruleId];
|
|
} else {
|
|
// 일반 회사: 자신의 파트만 조회
|
|
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
|
|
ORDER BY part_order
|
|
`;
|
|
partsParams = [rule.ruleId, companyCode];
|
|
}
|
|
|
|
const partsResult = await pool.query(partsQuery, partsParams);
|
|
rule.parts = partsResult.rows;
|
|
}
|
|
|
|
logger.info(`채번 규칙 목록 조회 완료: ${result.rows.length}개`, {
|
|
companyCode,
|
|
});
|
|
return result.rows;
|
|
} catch (error: any) {
|
|
logger.error(`채번 규칙 목록 조회 중 에러: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 현재 메뉴에서 사용 가능한 규칙 목록 조회 (메뉴 스코프)
|
|
*
|
|
* 메뉴 스코프 규칙:
|
|
* - menuObjid가 제공되면 형제 메뉴의 채번 규칙 포함
|
|
* - 우선순위: menu (형제 메뉴) > table > global
|
|
*/
|
|
async getAvailableRulesForMenu(
|
|
companyCode: string,
|
|
menuObjid?: number
|
|
): Promise<NumberingRuleConfig[]> {
|
|
let menuAndChildObjids: number[] = []; // catch 블록에서 접근 가능하도록 함수 최상단에 선언
|
|
|
|
try {
|
|
logger.info("메뉴별 사용 가능한 채번 규칙 조회 시작 (메뉴 스코프)", {
|
|
companyCode,
|
|
menuObjid,
|
|
});
|
|
|
|
const pool = getPool();
|
|
|
|
// 1. 선택한 메뉴와 하위 메뉴 OBJID 조회 (형제 메뉴 제외)
|
|
if (menuObjid) {
|
|
menuAndChildObjids = await getMenuAndChildObjids(menuObjid);
|
|
logger.info("선택한 메뉴 및 하위 메뉴 OBJID 목록", { menuObjid, menuAndChildObjids });
|
|
}
|
|
|
|
// menuObjid가 없으면 global 규칙만 반환
|
|
if (!menuObjid || menuAndChildObjids.length === 0) {
|
|
let query: string;
|
|
let params: any[];
|
|
|
|
if (companyCode === "*") {
|
|
// 최고 관리자: 모든 global 규칙 조회
|
|
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 scope_type = 'global'
|
|
ORDER BY created_at DESC
|
|
`;
|
|
params = [];
|
|
} else {
|
|
// 일반 회사: 자신의 global 규칙만 조회 (company_code="*" 제외)
|
|
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 AND scope_type = 'global'
|
|
ORDER BY created_at DESC
|
|
`;
|
|
params = [companyCode];
|
|
}
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
// 파트 정보 추가
|
|
for (const rule of result.rows) {
|
|
let partsQuery: string;
|
|
let partsParams: any[];
|
|
|
|
if (companyCode === "*") {
|
|
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
|
|
ORDER BY part_order
|
|
`;
|
|
partsParams = [rule.ruleId];
|
|
} else {
|
|
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
|
|
ORDER BY part_order
|
|
`;
|
|
partsParams = [rule.ruleId, companyCode];
|
|
}
|
|
|
|
const partsResult = await pool.query(partsQuery, partsParams);
|
|
rule.parts = partsResult.rows;
|
|
}
|
|
|
|
return result.rows;
|
|
}
|
|
|
|
// 2. 메뉴 스코프: 형제 메뉴의 채번 규칙 조회
|
|
// 우선순위: menu (형제 메뉴) > table > global
|
|
let query: string;
|
|
let params: any[];
|
|
|
|
if (companyCode === "*") {
|
|
// 최고 관리자: 모든 규칙 조회 (선택한 메뉴 + 하위 메뉴)
|
|
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
|
|
scope_type = 'global'
|
|
OR (scope_type = 'menu' AND menu_objid = ANY($1))
|
|
OR (scope_type = 'table' AND menu_objid = ANY($1))
|
|
ORDER BY
|
|
CASE
|
|
WHEN scope_type = 'menu' OR (scope_type = 'table' AND menu_objid = ANY($1)) THEN 1
|
|
WHEN scope_type = 'table' THEN 2
|
|
WHEN scope_type = 'global' THEN 3
|
|
END,
|
|
created_at DESC
|
|
`;
|
|
params = [menuAndChildObjids];
|
|
logger.info("최고 관리자: 메뉴 및 하위 메뉴 기반 채번 규칙 조회", { menuAndChildObjids });
|
|
} else {
|
|
// 일반 회사: 자신의 규칙만 조회 (선택한 메뉴 + 하위 메뉴)
|
|
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
|
|
AND (
|
|
scope_type = 'global'
|
|
OR (scope_type = 'menu' AND menu_objid = ANY($2))
|
|
OR (scope_type = 'table' AND menu_objid = ANY($2))
|
|
)
|
|
ORDER BY
|
|
CASE
|
|
WHEN scope_type = 'menu' OR (scope_type = 'table' AND menu_objid = ANY($2)) THEN 1
|
|
WHEN scope_type = 'table' THEN 2
|
|
WHEN scope_type = 'global' THEN 3
|
|
END,
|
|
created_at DESC
|
|
`;
|
|
params = [companyCode, menuAndChildObjids];
|
|
logger.info("회사별: 메뉴 및 하위 메뉴 기반 채번 규칙 조회", { companyCode, menuAndChildObjids });
|
|
}
|
|
|
|
logger.info("🔍 채번 규칙 쿼리 실행", {
|
|
queryPreview: query.substring(0, 200),
|
|
paramsTypes: params.map(p => typeof p),
|
|
paramsValues: params,
|
|
});
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
logger.debug("채번 규칙 쿼리 성공", { ruleCount: result.rows.length });
|
|
|
|
// 파트 정보 추가
|
|
for (const rule of result.rows) {
|
|
try {
|
|
let partsQuery: string;
|
|
let partsParams: any[];
|
|
|
|
if (companyCode === "*") {
|
|
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
|
|
ORDER BY part_order
|
|
`;
|
|
partsParams = [rule.ruleId];
|
|
} else {
|
|
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
|
|
ORDER BY part_order
|
|
`;
|
|
partsParams = [rule.ruleId, companyCode];
|
|
}
|
|
|
|
const partsResult = await pool.query(partsQuery, partsParams);
|
|
rule.parts = partsResult.rows;
|
|
|
|
logger.info("✅ 규칙 파트 조회 성공", {
|
|
ruleId: rule.ruleId,
|
|
ruleName: rule.ruleName,
|
|
partsCount: partsResult.rows.length,
|
|
});
|
|
} catch (partError: any) {
|
|
logger.error("❌ 규칙 파트 조회 실패", {
|
|
ruleId: rule.ruleId,
|
|
ruleName: rule.ruleName,
|
|
error: partError.message,
|
|
errorCode: partError.code,
|
|
errorStack: partError.stack,
|
|
});
|
|
throw partError;
|
|
}
|
|
}
|
|
|
|
logger.info("메뉴별 사용 가능한 채번 규칙 조회 완료", {
|
|
companyCode,
|
|
menuObjid,
|
|
menuAndChildCount: menuAndChildObjids.length,
|
|
count: result.rowCount,
|
|
});
|
|
|
|
return result.rows;
|
|
} catch (error: any) {
|
|
logger.error("메뉴별 채번 규칙 조회 실패", {
|
|
error: error.message,
|
|
errorCode: error.code,
|
|
errorStack: error.stack,
|
|
companyCode,
|
|
menuObjid,
|
|
menuAndChildObjids: menuAndChildObjids || [],
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 화면용 채번 규칙 조회 (테이블 기반 필터링 - 간소화)
|
|
* @param companyCode 회사 코드
|
|
* @param tableName 화면의 테이블명
|
|
* @returns 해당 테이블의 채번 규칙 목록
|
|
*/
|
|
async getAvailableRulesForScreen(
|
|
companyCode: string,
|
|
tableName: string
|
|
): Promise<NumberingRuleConfig[]> {
|
|
try {
|
|
logger.info("화면용 채번 규칙 조회", {
|
|
companyCode,
|
|
tableName,
|
|
});
|
|
|
|
const pool = getPool();
|
|
|
|
// 멀티테넌시: 최고 관리자 vs 일반 회사
|
|
let query: string;
|
|
let params: any[];
|
|
|
|
if (companyCode === "*") {
|
|
// 최고 관리자: 모든 회사의 규칙 조회 가능 (최고 관리자 전용 규칙 제외)
|
|
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 != '*'
|
|
AND table_name = $1
|
|
ORDER BY created_at DESC
|
|
`;
|
|
params = [tableName];
|
|
logger.info("최고 관리자: 일반 회사 채번 규칙 조회");
|
|
} else {
|
|
// 일반 회사: 자신의 규칙만 조회
|
|
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
|
|
AND table_name = $2
|
|
ORDER BY created_at DESC
|
|
`;
|
|
params = [companyCode, tableName];
|
|
}
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
// 각 규칙의 파트 정보 로드
|
|
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
|
|
ORDER BY part_order
|
|
`;
|
|
|
|
const partsResult = await pool.query(partsQuery, [
|
|
rule.ruleId,
|
|
companyCode === "*" ? rule.companyCode : companyCode,
|
|
]);
|
|
|
|
rule.parts = partsResult.rows;
|
|
}
|
|
|
|
logger.info(`화면용 채번 규칙 조회 완료: ${result.rows.length}개`, {
|
|
companyCode,
|
|
tableName,
|
|
});
|
|
|
|
return result.rows;
|
|
} catch (error: any) {
|
|
logger.error("화면용 채번 규칙 조회 실패", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 특정 규칙 조회
|
|
*/
|
|
async getRuleById(
|
|
ruleId: string,
|
|
companyCode: string
|
|
): Promise<NumberingRuleConfig | null> {
|
|
const pool = getPool();
|
|
|
|
// 멀티테넌시: 최고 관리자만 company_code="*" 데이터를 볼 수 있음
|
|
let query: string;
|
|
let params: any[];
|
|
|
|
if (companyCode === "*") {
|
|
// 최고 관리자: 모든 규칙 조회 가능
|
|
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 rule_id = $1
|
|
`;
|
|
params = [ruleId];
|
|
} else {
|
|
// 일반 회사: 자신의 규칙만 조회
|
|
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 rule_id = $1 AND company_code = $2
|
|
`;
|
|
params = [ruleId, companyCode];
|
|
}
|
|
|
|
const result = await pool.query(query, params);
|
|
if (result.rowCount === 0) return null;
|
|
|
|
const rule = result.rows[0];
|
|
|
|
// 파트 정보 조회
|
|
let partsQuery: string;
|
|
let partsParams: any[];
|
|
|
|
if (companyCode === "*") {
|
|
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
|
|
ORDER BY part_order
|
|
`;
|
|
partsParams = [ruleId];
|
|
} else {
|
|
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
|
|
ORDER BY part_order
|
|
`;
|
|
partsParams = [ruleId, companyCode];
|
|
}
|
|
|
|
const partsResult = await pool.query(partsQuery, partsParams);
|
|
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,
|
|
menu_objid, scope_type, created_by
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
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",
|
|
menu_objid AS "menuObjid",
|
|
scope_type AS "scopeType",
|
|
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,
|
|
config.menuObjid || null,
|
|
config.scopeType || "global",
|
|
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),
|
|
menu_objid = COALESCE($7, menu_objid),
|
|
scope_type = COALESCE($8, scope_type),
|
|
updated_at = NOW()
|
|
WHERE rule_id = $9 AND company_code = $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",
|
|
menu_objid AS "menuObjid",
|
|
scope_type AS "scopeType",
|
|
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,
|
|
updates.menuObjid,
|
|
updates.scopeType,
|
|
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 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 || 3;
|
|
return String(rule.currentSequence || 1).padStart(length, "0");
|
|
}
|
|
|
|
case "number": {
|
|
// 숫자 (고정 자릿수)
|
|
const length = autoConfig.numberLength || 3;
|
|
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();
|
|
|
|
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 "sequence": {
|
|
// 순번 (자동 증가 숫자)
|
|
const length = autoConfig.sequenceLength || 3;
|
|
return String(rule.currentSequence || 1).padStart(length, "0");
|
|
}
|
|
|
|
case "number": {
|
|
// 숫자 (고정 자릿수)
|
|
const length = autoConfig.numberLength || 3;
|
|
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 allocatedCode = 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, allocatedCode, companyCode });
|
|
return allocatedCode;
|
|
} catch (error: any) {
|
|
await client.query("ROLLBACK");
|
|
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}`;
|
|
}
|
|
}
|
|
|
|
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();
|