|
|
|
|
@@ -47,11 +47,11 @@ class NumberingRuleService {
|
|
|
|
|
logger.info("채번 규칙 목록 조회 시작", { companyCode });
|
|
|
|
|
|
|
|
|
|
const pool = getPool();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 멀티테넌시: 최고 관리자만 company_code="*" 데이터를 볼 수 있음
|
|
|
|
|
let query: string;
|
|
|
|
|
let params: any[];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (companyCode === "*") {
|
|
|
|
|
// 최고 관리자: 모든 회사 데이터 조회 가능
|
|
|
|
|
query = `
|
|
|
|
|
@@ -107,7 +107,7 @@ class NumberingRuleService {
|
|
|
|
|
for (const rule of result.rows) {
|
|
|
|
|
let partsQuery: string;
|
|
|
|
|
let partsParams: any[];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (companyCode === "*") {
|
|
|
|
|
// 최고 관리자: 모든 파트 조회
|
|
|
|
|
partsQuery = `
|
|
|
|
|
@@ -156,7 +156,7 @@ class NumberingRuleService {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 현재 메뉴에서 사용 가능한 규칙 목록 조회 (메뉴 스코프)
|
|
|
|
|
*
|
|
|
|
|
*
|
|
|
|
|
* 메뉴 스코프 규칙:
|
|
|
|
|
* - menuObjid가 제공되면 형제 메뉴의 채번 규칙 포함
|
|
|
|
|
* - 우선순위: menu (형제 메뉴) > table > global
|
|
|
|
|
@@ -166,7 +166,7 @@ class NumberingRuleService {
|
|
|
|
|
menuObjid?: number
|
|
|
|
|
): Promise<NumberingRuleConfig[]> {
|
|
|
|
|
let menuAndChildObjids: number[] = []; // catch 블록에서 접근 가능하도록 함수 최상단에 선언
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
logger.info("메뉴별 사용 가능한 채번 규칙 조회 시작 (메뉴 스코프)", {
|
|
|
|
|
companyCode,
|
|
|
|
|
@@ -178,14 +178,17 @@ class NumberingRuleService {
|
|
|
|
|
// 1. 선택한 메뉴와 하위 메뉴 OBJID 조회 (형제 메뉴 제외)
|
|
|
|
|
if (menuObjid) {
|
|
|
|
|
menuAndChildObjids = await getMenuAndChildObjids(menuObjid);
|
|
|
|
|
logger.info("선택한 메뉴 및 하위 메뉴 OBJID 목록", { menuObjid, menuAndChildObjids });
|
|
|
|
|
logger.info("선택한 메뉴 및 하위 메뉴 OBJID 목록", {
|
|
|
|
|
menuObjid,
|
|
|
|
|
menuAndChildObjids,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// menuObjid가 없으면 global 규칙만 반환
|
|
|
|
|
if (!menuObjid || menuAndChildObjids.length === 0) {
|
|
|
|
|
let query: string;
|
|
|
|
|
let params: any[];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (companyCode === "*") {
|
|
|
|
|
// 최고 관리자: 모든 global 규칙 조회
|
|
|
|
|
query = `
|
|
|
|
|
@@ -239,7 +242,7 @@ class NumberingRuleService {
|
|
|
|
|
for (const rule of result.rows) {
|
|
|
|
|
let partsQuery: string;
|
|
|
|
|
let partsParams: any[];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (companyCode === "*") {
|
|
|
|
|
partsQuery = `
|
|
|
|
|
SELECT
|
|
|
|
|
@@ -281,7 +284,7 @@ class NumberingRuleService {
|
|
|
|
|
// 우선순위: menu (형제 메뉴) > table > global
|
|
|
|
|
let query: string;
|
|
|
|
|
let params: any[];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (companyCode === "*") {
|
|
|
|
|
// 최고 관리자: 모든 규칙 조회
|
|
|
|
|
query = `
|
|
|
|
|
@@ -333,7 +336,7 @@ class NumberingRuleService {
|
|
|
|
|
|
|
|
|
|
logger.info("🔍 채번 규칙 쿼리 실행", {
|
|
|
|
|
queryPreview: query.substring(0, 200),
|
|
|
|
|
paramsTypes: params.map(p => typeof p),
|
|
|
|
|
paramsTypes: params.map((p) => typeof p),
|
|
|
|
|
paramsValues: params,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@@ -346,7 +349,7 @@ class NumberingRuleService {
|
|
|
|
|
try {
|
|
|
|
|
let partsQuery: string;
|
|
|
|
|
let partsParams: any[];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (companyCode === "*") {
|
|
|
|
|
partsQuery = `
|
|
|
|
|
SELECT
|
|
|
|
|
@@ -379,7 +382,7 @@ class NumberingRuleService {
|
|
|
|
|
|
|
|
|
|
const partsResult = await pool.query(partsQuery, partsParams);
|
|
|
|
|
rule.parts = partsResult.rows;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.info("✅ 규칙 파트 조회 성공", {
|
|
|
|
|
ruleId: rule.ruleId,
|
|
|
|
|
ruleName: rule.ruleName,
|
|
|
|
|
@@ -537,11 +540,11 @@ class NumberingRuleService {
|
|
|
|
|
companyCode: string
|
|
|
|
|
): Promise<NumberingRuleConfig | null> {
|
|
|
|
|
const pool = getPool();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 멀티테넌시: 최고 관리자만 company_code="*" 데이터를 볼 수 있음
|
|
|
|
|
let query: string;
|
|
|
|
|
let params: any[];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (companyCode === "*") {
|
|
|
|
|
// 최고 관리자: 모든 규칙 조회 가능
|
|
|
|
|
query = `
|
|
|
|
|
@@ -598,7 +601,7 @@ class NumberingRuleService {
|
|
|
|
|
// 파트 정보 조회
|
|
|
|
|
let partsQuery: string;
|
|
|
|
|
let partsParams: any[];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (companyCode === "*") {
|
|
|
|
|
partsQuery = `
|
|
|
|
|
SELECT
|
|
|
|
|
@@ -836,12 +839,12 @@ class NumberingRuleService {
|
|
|
|
|
return { ...ruleResult.rows[0], parts };
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
await client.query("ROLLBACK");
|
|
|
|
|
logger.error("채번 규칙 수정 실패", {
|
|
|
|
|
logger.error("채번 규칙 수정 실패", {
|
|
|
|
|
ruleId,
|
|
|
|
|
companyCode,
|
|
|
|
|
error: error.message,
|
|
|
|
|
stack: error.stack,
|
|
|
|
|
updates
|
|
|
|
|
updates,
|
|
|
|
|
});
|
|
|
|
|
throw error;
|
|
|
|
|
} finally {
|
|
|
|
|
@@ -875,7 +878,7 @@ class NumberingRuleService {
|
|
|
|
|
* @param formData 폼 데이터 (카테고리 기반 채번 시 사용)
|
|
|
|
|
*/
|
|
|
|
|
async previewCode(
|
|
|
|
|
ruleId: string,
|
|
|
|
|
ruleId: string,
|
|
|
|
|
companyCode: string,
|
|
|
|
|
formData?: Record<string, any>
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
@@ -911,21 +914,26 @@ class NumberingRuleService {
|
|
|
|
|
case "date": {
|
|
|
|
|
// 날짜 (다양한 날짜 형식)
|
|
|
|
|
const dateFormat = autoConfig.dateFormat || "YYYYMMDD";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼 기준 생성인 경우 폼 데이터에서 날짜 추출
|
|
|
|
|
if (autoConfig.useColumnValue && autoConfig.sourceColumnName && formData) {
|
|
|
|
|
if (
|
|
|
|
|
autoConfig.useColumnValue &&
|
|
|
|
|
autoConfig.sourceColumnName &&
|
|
|
|
|
formData
|
|
|
|
|
) {
|
|
|
|
|
const columnValue = formData[autoConfig.sourceColumnName];
|
|
|
|
|
if (columnValue) {
|
|
|
|
|
const dateValue = columnValue instanceof Date
|
|
|
|
|
? columnValue
|
|
|
|
|
: new Date(columnValue);
|
|
|
|
|
|
|
|
|
|
const dateValue =
|
|
|
|
|
columnValue instanceof Date
|
|
|
|
|
? columnValue
|
|
|
|
|
: new Date(columnValue);
|
|
|
|
|
|
|
|
|
|
if (!isNaN(dateValue.getTime())) {
|
|
|
|
|
return this.formatDate(dateValue, dateFormat);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return this.formatDate(new Date(), dateFormat);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -938,63 +946,68 @@ class NumberingRuleService {
|
|
|
|
|
// 카테고리 기반 코드 생성
|
|
|
|
|
const categoryKey = autoConfig.categoryKey; // 예: "item_info.material"
|
|
|
|
|
const categoryMappings = autoConfig.categoryMappings || [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!categoryKey || !formData) {
|
|
|
|
|
logger.warn("카테고리 키 또는 폼 데이터 없음", { categoryKey, hasFormData: !!formData });
|
|
|
|
|
logger.warn("카테고리 키 또는 폼 데이터 없음", {
|
|
|
|
|
categoryKey,
|
|
|
|
|
hasFormData: !!formData,
|
|
|
|
|
});
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// categoryKey에서 컬럼명 추출 (예: "item_info.material" -> "material")
|
|
|
|
|
const columnName = categoryKey.includes(".")
|
|
|
|
|
? categoryKey.split(".")[1]
|
|
|
|
|
const columnName = categoryKey.includes(".")
|
|
|
|
|
? categoryKey.split(".")[1]
|
|
|
|
|
: categoryKey;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 폼 데이터에서 해당 컬럼의 값 가져오기
|
|
|
|
|
const selectedValue = formData[columnName];
|
|
|
|
|
|
|
|
|
|
logger.info("카테고리 파트 처리", {
|
|
|
|
|
categoryKey,
|
|
|
|
|
columnName,
|
|
|
|
|
|
|
|
|
|
logger.info("카테고리 파트 처리", {
|
|
|
|
|
categoryKey,
|
|
|
|
|
columnName,
|
|
|
|
|
selectedValue,
|
|
|
|
|
formDataKeys: Object.keys(formData),
|
|
|
|
|
mappingsCount: categoryMappings.length
|
|
|
|
|
mappingsCount: categoryMappings.length,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!selectedValue) {
|
|
|
|
|
logger.warn("카테고리 값이 선택되지 않음", { columnName, formDataKeys: Object.keys(formData) });
|
|
|
|
|
logger.warn("카테고리 값이 선택되지 않음", {
|
|
|
|
|
columnName,
|
|
|
|
|
formDataKeys: Object.keys(formData),
|
|
|
|
|
});
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 카테고리 매핑에서 해당 값에 대한 형식 찾기
|
|
|
|
|
// selectedValue는 valueCode일 수 있음 (V2Select에서 valueCode를 value로 사용)
|
|
|
|
|
const selectedValueStr = String(selectedValue);
|
|
|
|
|
const mapping = categoryMappings.find(
|
|
|
|
|
(m: any) => {
|
|
|
|
|
// ID로 매칭
|
|
|
|
|
if (m.categoryValueId?.toString() === selectedValueStr) return true;
|
|
|
|
|
// 라벨로 매칭
|
|
|
|
|
if (m.categoryValueLabel === selectedValueStr) return true;
|
|
|
|
|
// valueCode로 매칭 (라벨과 동일할 수 있음)
|
|
|
|
|
if (m.categoryValueLabel === selectedValueStr) return true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const mapping = categoryMappings.find((m: any) => {
|
|
|
|
|
// ID로 매칭
|
|
|
|
|
if (m.categoryValueId?.toString() === selectedValueStr)
|
|
|
|
|
return true;
|
|
|
|
|
// 라벨로 매칭
|
|
|
|
|
if (m.categoryValueLabel === selectedValueStr) return true;
|
|
|
|
|
// valueCode로 매칭 (라벨과 동일할 수 있음)
|
|
|
|
|
if (m.categoryValueLabel === selectedValueStr) return true;
|
|
|
|
|
return false;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (mapping) {
|
|
|
|
|
logger.info("카테고리 매핑 적용", {
|
|
|
|
|
selectedValue,
|
|
|
|
|
logger.info("카테고리 매핑 적용", {
|
|
|
|
|
selectedValue,
|
|
|
|
|
format: mapping.format,
|
|
|
|
|
categoryValueLabel: mapping.categoryValueLabel
|
|
|
|
|
categoryValueLabel: mapping.categoryValueLabel,
|
|
|
|
|
});
|
|
|
|
|
return mapping.format || "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.warn("카테고리 매핑을 찾을 수 없음", {
|
|
|
|
|
selectedValue,
|
|
|
|
|
availableMappings: categoryMappings.map((m: any) => ({
|
|
|
|
|
id: m.categoryValueId,
|
|
|
|
|
label: m.categoryValueLabel
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
logger.warn("카테고리 매핑을 찾을 수 없음", {
|
|
|
|
|
selectedValue,
|
|
|
|
|
availableMappings: categoryMappings.map((m: any) => ({
|
|
|
|
|
id: m.categoryValueId,
|
|
|
|
|
label: m.categoryValueLabel,
|
|
|
|
|
})),
|
|
|
|
|
});
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
@@ -1006,7 +1019,12 @@ class NumberingRuleService {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const previewCode = parts.join(rule.separator || "");
|
|
|
|
|
logger.info("코드 미리보기 생성", { ruleId, previewCode, companyCode, hasFormData: !!formData });
|
|
|
|
|
logger.info("코드 미리보기 생성", {
|
|
|
|
|
ruleId,
|
|
|
|
|
previewCode,
|
|
|
|
|
companyCode,
|
|
|
|
|
hasFormData: !!formData,
|
|
|
|
|
});
|
|
|
|
|
return previewCode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1018,8 +1036,8 @@ class NumberingRuleService {
|
|
|
|
|
* @param userInputCode 사용자가 편집한 최종 코드 (수동 입력 부분 추출용)
|
|
|
|
|
*/
|
|
|
|
|
async allocateCode(
|
|
|
|
|
ruleId: string,
|
|
|
|
|
companyCode: string,
|
|
|
|
|
ruleId: string,
|
|
|
|
|
companyCode: string,
|
|
|
|
|
formData?: Record<string, any>,
|
|
|
|
|
userInputCode?: string
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
@@ -1033,9 +1051,11 @@ class NumberingRuleService {
|
|
|
|
|
if (!rule) throw new Error("규칙을 찾을 수 없습니다");
|
|
|
|
|
|
|
|
|
|
// 수동 입력 파트가 있고, 사용자가 입력한 코드가 있으면 수동 입력 부분 추출
|
|
|
|
|
const manualParts = rule.parts.filter((p: any) => p.generationMethod === "manual");
|
|
|
|
|
const manualParts = rule.parts.filter(
|
|
|
|
|
(p: any) => p.generationMethod === "manual"
|
|
|
|
|
);
|
|
|
|
|
let extractedManualValues: string[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (manualParts.length > 0 && userInputCode) {
|
|
|
|
|
// 프리뷰 코드를 생성해서 ____ 위치 파악
|
|
|
|
|
// 🔧 category 파트도 처리하여 올바른 템플릿 생성
|
|
|
|
|
@@ -1059,39 +1079,38 @@ class NumberingRuleService {
|
|
|
|
|
// 카테고리 파트: formData에서 실제 값을 가져와서 매핑된 형식 사용
|
|
|
|
|
const categoryKey = autoConfig.categoryKey;
|
|
|
|
|
const categoryMappings = autoConfig.categoryMappings || [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!categoryKey || !formData) {
|
|
|
|
|
return "CATEGORY"; // 폴백
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const columnName = categoryKey.includes(".")
|
|
|
|
|
? categoryKey.split(".")[1]
|
|
|
|
|
|
|
|
|
|
const columnName = categoryKey.includes(".")
|
|
|
|
|
? categoryKey.split(".")[1]
|
|
|
|
|
: categoryKey;
|
|
|
|
|
const selectedValue = formData[columnName];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!selectedValue) {
|
|
|
|
|
return "CATEGORY"; // 폴백
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const selectedValueStr = String(selectedValue);
|
|
|
|
|
const mapping = categoryMappings.find(
|
|
|
|
|
(m: any) => {
|
|
|
|
|
if (m.categoryValueId?.toString() === selectedValueStr) return true;
|
|
|
|
|
if (m.categoryValueLabel === selectedValueStr) return true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const mapping = categoryMappings.find((m: any) => {
|
|
|
|
|
if (m.categoryValueId?.toString() === selectedValueStr)
|
|
|
|
|
return true;
|
|
|
|
|
if (m.categoryValueLabel === selectedValueStr) return true;
|
|
|
|
|
return false;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return mapping?.format || "CATEGORY";
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const separator = rule.separator || "";
|
|
|
|
|
const previewTemplate = previewParts.join(separator);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 사용자 입력 코드에서 수동 입력 부분 추출
|
|
|
|
|
// 예: 템플릿 "R-____-XXX", 사용자입력 "R-MYVALUE-012" → "MYVALUE" 추출
|
|
|
|
|
const templateParts = previewTemplate.split("____");
|
|
|
|
|
@@ -1100,19 +1119,23 @@ class NumberingRuleService {
|
|
|
|
|
for (let i = 0; i < templateParts.length - 1; i++) {
|
|
|
|
|
const prefix = templateParts[i];
|
|
|
|
|
const suffix = templateParts[i + 1];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// prefix 이후 부분 추출
|
|
|
|
|
if (prefix && remainingCode.startsWith(prefix)) {
|
|
|
|
|
remainingCode = remainingCode.slice(prefix.length);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// suffix 이전까지가 수동 입력 값
|
|
|
|
|
if (suffix) {
|
|
|
|
|
// suffix에서 순번(XXX)이나 날짜 부분을 제외한 실제 구분자 찾기
|
|
|
|
|
const suffixStart = suffix.replace(/X+|DATEPART/g, "");
|
|
|
|
|
const manualEndIndex = suffixStart ? remainingCode.indexOf(suffixStart) : remainingCode.length;
|
|
|
|
|
const manualEndIndex = suffixStart
|
|
|
|
|
? remainingCode.indexOf(suffixStart)
|
|
|
|
|
: remainingCode.length;
|
|
|
|
|
if (manualEndIndex > 0) {
|
|
|
|
|
extractedManualValues.push(remainingCode.slice(0, manualEndIndex));
|
|
|
|
|
extractedManualValues.push(
|
|
|
|
|
remainingCode.slice(0, manualEndIndex)
|
|
|
|
|
);
|
|
|
|
|
remainingCode = remainingCode.slice(manualEndIndex);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
@@ -1120,8 +1143,10 @@ class NumberingRuleService {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info(`수동 입력 값 추출: userInputCode=${userInputCode}, previewTemplate=${previewTemplate}, extractedManualValues=${JSON.stringify(extractedManualValues)}`);
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
`수동 입력 값 추출: userInputCode=${userInputCode}, previewTemplate=${previewTemplate}, extractedManualValues=${JSON.stringify(extractedManualValues)}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let manualPartIndex = 0;
|
|
|
|
|
@@ -1130,7 +1155,10 @@ class NumberingRuleService {
|
|
|
|
|
.map((part: any) => {
|
|
|
|
|
if (part.generationMethod === "manual") {
|
|
|
|
|
// 추출된 수동 입력 값 사용, 없으면 기본값 사용
|
|
|
|
|
const manualValue = extractedManualValues[manualPartIndex] || part.manualConfig?.value || "";
|
|
|
|
|
const manualValue =
|
|
|
|
|
extractedManualValues[manualPartIndex] ||
|
|
|
|
|
part.manualConfig?.value ||
|
|
|
|
|
"";
|
|
|
|
|
manualPartIndex++;
|
|
|
|
|
return manualValue;
|
|
|
|
|
}
|
|
|
|
|
@@ -1155,16 +1183,21 @@ class NumberingRuleService {
|
|
|
|
|
case "date": {
|
|
|
|
|
// 날짜 (다양한 날짜 형식)
|
|
|
|
|
const dateFormat = autoConfig.dateFormat || "YYYYMMDD";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼 기준 생성인 경우 폼 데이터에서 날짜 추출
|
|
|
|
|
if (autoConfig.useColumnValue && autoConfig.sourceColumnName && formData) {
|
|
|
|
|
if (
|
|
|
|
|
autoConfig.useColumnValue &&
|
|
|
|
|
autoConfig.sourceColumnName &&
|
|
|
|
|
formData
|
|
|
|
|
) {
|
|
|
|
|
const columnValue = formData[autoConfig.sourceColumnName];
|
|
|
|
|
if (columnValue) {
|
|
|
|
|
// 날짜 문자열 또는 Date 객체를 Date로 변환
|
|
|
|
|
const dateValue = columnValue instanceof Date
|
|
|
|
|
? columnValue
|
|
|
|
|
: new Date(columnValue);
|
|
|
|
|
|
|
|
|
|
const dateValue =
|
|
|
|
|
columnValue instanceof Date
|
|
|
|
|
? columnValue
|
|
|
|
|
: new Date(columnValue);
|
|
|
|
|
|
|
|
|
|
if (!isNaN(dateValue.getTime())) {
|
|
|
|
|
logger.info("컬럼 기준 날짜 생성", {
|
|
|
|
|
sourceColumn: autoConfig.sourceColumnName,
|
|
|
|
|
@@ -1185,7 +1218,7 @@ class NumberingRuleService {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 기본: 현재 날짜 사용
|
|
|
|
|
return this.formatDate(new Date(), dateFormat);
|
|
|
|
|
}
|
|
|
|
|
@@ -1199,60 +1232,65 @@ class NumberingRuleService {
|
|
|
|
|
// 카테고리 기반 코드 생성 (allocateCode용)
|
|
|
|
|
const categoryKey = autoConfig.categoryKey; // 예: "item_info.material"
|
|
|
|
|
const categoryMappings = autoConfig.categoryMappings || [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!categoryKey || !formData) {
|
|
|
|
|
logger.warn("allocateCode: 카테고리 키 또는 폼 데이터 없음", { categoryKey, hasFormData: !!formData });
|
|
|
|
|
logger.warn("allocateCode: 카테고리 키 또는 폼 데이터 없음", {
|
|
|
|
|
categoryKey,
|
|
|
|
|
hasFormData: !!formData,
|
|
|
|
|
});
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// categoryKey에서 컬럼명 추출 (예: "item_info.material" -> "material")
|
|
|
|
|
const columnName = categoryKey.includes(".")
|
|
|
|
|
? categoryKey.split(".")[1]
|
|
|
|
|
const columnName = categoryKey.includes(".")
|
|
|
|
|
? categoryKey.split(".")[1]
|
|
|
|
|
: categoryKey;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 폼 데이터에서 해당 컬럼의 값 가져오기
|
|
|
|
|
const selectedValue = formData[columnName];
|
|
|
|
|
|
|
|
|
|
logger.info("allocateCode: 카테고리 파트 처리", {
|
|
|
|
|
categoryKey,
|
|
|
|
|
columnName,
|
|
|
|
|
|
|
|
|
|
logger.info("allocateCode: 카테고리 파트 처리", {
|
|
|
|
|
categoryKey,
|
|
|
|
|
columnName,
|
|
|
|
|
selectedValue,
|
|
|
|
|
formDataKeys: Object.keys(formData),
|
|
|
|
|
mappingsCount: categoryMappings.length
|
|
|
|
|
mappingsCount: categoryMappings.length,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!selectedValue) {
|
|
|
|
|
logger.warn("allocateCode: 카테고리 값이 선택되지 않음", { columnName, formDataKeys: Object.keys(formData) });
|
|
|
|
|
logger.warn("allocateCode: 카테고리 값이 선택되지 않음", {
|
|
|
|
|
columnName,
|
|
|
|
|
formDataKeys: Object.keys(formData),
|
|
|
|
|
});
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 카테고리 매핑에서 해당 값에 대한 형식 찾기
|
|
|
|
|
const selectedValueStr = String(selectedValue);
|
|
|
|
|
const mapping = categoryMappings.find(
|
|
|
|
|
(m: any) => {
|
|
|
|
|
// ID로 매칭
|
|
|
|
|
if (m.categoryValueId?.toString() === selectedValueStr) return true;
|
|
|
|
|
// 라벨로 매칭
|
|
|
|
|
if (m.categoryValueLabel === selectedValueStr) return true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const mapping = categoryMappings.find((m: any) => {
|
|
|
|
|
// ID로 매칭
|
|
|
|
|
if (m.categoryValueId?.toString() === selectedValueStr)
|
|
|
|
|
return true;
|
|
|
|
|
// 라벨로 매칭
|
|
|
|
|
if (m.categoryValueLabel === selectedValueStr) return true;
|
|
|
|
|
return false;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (mapping) {
|
|
|
|
|
logger.info("allocateCode: 카테고리 매핑 적용", {
|
|
|
|
|
selectedValue,
|
|
|
|
|
logger.info("allocateCode: 카테고리 매핑 적용", {
|
|
|
|
|
selectedValue,
|
|
|
|
|
format: mapping.format,
|
|
|
|
|
categoryValueLabel: mapping.categoryValueLabel
|
|
|
|
|
categoryValueLabel: mapping.categoryValueLabel,
|
|
|
|
|
});
|
|
|
|
|
return mapping.format || "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.warn("allocateCode: 카테고리 매핑을 찾을 수 없음", {
|
|
|
|
|
selectedValue,
|
|
|
|
|
availableMappings: categoryMappings.map((m: any) => ({
|
|
|
|
|
id: m.categoryValueId,
|
|
|
|
|
label: m.categoryValueLabel
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
logger.warn("allocateCode: 카테고리 매핑을 찾을 수 없음", {
|
|
|
|
|
selectedValue,
|
|
|
|
|
availableMappings: categoryMappings.map((m: any) => ({
|
|
|
|
|
id: m.categoryValueId,
|
|
|
|
|
label: m.categoryValueLabel,
|
|
|
|
|
})),
|
|
|
|
|
});
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
@@ -1344,14 +1382,17 @@ class NumberingRuleService {
|
|
|
|
|
menuObjid?: number
|
|
|
|
|
): Promise<NumberingRuleConfig[]> {
|
|
|
|
|
try {
|
|
|
|
|
logger.info("[테스트] 채번 규칙 목록 조회 시작", { companyCode, menuObjid });
|
|
|
|
|
logger.info("[테스트] 채번 규칙 목록 조회 시작", {
|
|
|
|
|
companyCode,
|
|
|
|
|
menuObjid,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const pool = getPool();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 멀티테넌시: 최고 관리자 vs 일반 회사
|
|
|
|
|
let query: string;
|
|
|
|
|
let params: any[];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (companyCode === "*") {
|
|
|
|
|
// 최고 관리자: 모든 규칙 조회
|
|
|
|
|
query = `
|
|
|
|
|
@@ -1508,7 +1549,10 @@ class NumberingRuleService {
|
|
|
|
|
WHERE rule_id = $1 AND company_code = $2
|
|
|
|
|
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("테이블+컬럼 기반 채번 규칙 조회 성공 (테스트)", {
|
|
|
|
|
@@ -1556,7 +1600,10 @@ class NumberingRuleService {
|
|
|
|
|
SELECT rule_id FROM numbering_rules
|
|
|
|
|
WHERE rule_id = $1 AND company_code = $2
|
|
|
|
|
`;
|
|
|
|
|
const existingResult = await client.query(existingQuery, [config.ruleId, companyCode]);
|
|
|
|
|
const existingResult = await client.query(existingQuery, [
|
|
|
|
|
config.ruleId,
|
|
|
|
|
companyCode,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
if (existingResult.rows.length > 0) {
|
|
|
|
|
// 업데이트
|
|
|
|
|
@@ -1671,7 +1718,10 @@ class NumberingRuleService {
|
|
|
|
|
try {
|
|
|
|
|
await client.query("BEGIN");
|
|
|
|
|
|
|
|
|
|
logger.info("테스트 테이블에서 채번 규칙 삭제 시작", { ruleId, companyCode });
|
|
|
|
|
logger.info("테스트 테이블에서 채번 규칙 삭제 시작", {
|
|
|
|
|
ruleId,
|
|
|
|
|
companyCode,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 파트 먼저 삭제
|
|
|
|
|
await client.query(
|
|
|
|
|
@@ -1779,7 +1829,10 @@ class NumberingRuleService {
|
|
|
|
|
WHERE rule_id = $1 AND company_code = $2
|
|
|
|
|
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("카테고리 조건 매칭 채번 규칙 찾음", {
|
|
|
|
|
@@ -1814,7 +1867,11 @@ class NumberingRuleService {
|
|
|
|
|
AND r.category_value_id IS NULL
|
|
|
|
|
LIMIT 1
|
|
|
|
|
`;
|
|
|
|
|
const defaultResult = await pool.query(defaultQuery, [companyCode, tableName, columnName]);
|
|
|
|
|
const defaultResult = await pool.query(defaultQuery, [
|
|
|
|
|
companyCode,
|
|
|
|
|
tableName,
|
|
|
|
|
columnName,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
if (defaultResult.rows.length > 0) {
|
|
|
|
|
const rule = defaultResult.rows[0];
|
|
|
|
|
@@ -1831,7 +1888,10 @@ class NumberingRuleService {
|
|
|
|
|
WHERE rule_id = $1 AND company_code = $2
|
|
|
|
|
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("기본 채번 규칙 찾음 (카테고리 조건 없음)", {
|
|
|
|
|
@@ -1891,8 +1951,12 @@ class NumberingRuleService {
|
|
|
|
|
AND r.column_name = $3
|
|
|
|
|
ORDER BY r.category_value_id NULLS FIRST, r.created_at
|
|
|
|
|
`;
|
|
|
|
|
const result = await pool.query(query, [companyCode, tableName, columnName]);
|
|
|
|
|
|
|
|
|
|
const result = await pool.query(query, [
|
|
|
|
|
companyCode,
|
|
|
|
|
tableName,
|
|
|
|
|
columnName,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 각 규칙의 파트 정보 조회
|
|
|
|
|
for (const rule of result.rows) {
|
|
|
|
|
const partsQuery = `
|
|
|
|
|
@@ -1907,7 +1971,10 @@ class NumberingRuleService {
|
|
|
|
|
WHERE rule_id = $1 AND company_code = $2
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1928,11 +1995,21 @@ class NumberingRuleService {
|
|
|
|
|
async copyRulesForCompany(
|
|
|
|
|
sourceCompanyCode: string,
|
|
|
|
|
targetCompanyCode: string
|
|
|
|
|
): Promise<{ copiedCount: number; skippedCount: number; details: string[]; ruleIdMap: Record<string, string> }> {
|
|
|
|
|
): Promise<{
|
|
|
|
|
copiedCount: number;
|
|
|
|
|
skippedCount: number;
|
|
|
|
|
details: string[];
|
|
|
|
|
ruleIdMap: Record<string, string>;
|
|
|
|
|
}> {
|
|
|
|
|
const pool = getPool();
|
|
|
|
|
const client = await pool.connect();
|
|
|
|
|
|
|
|
|
|
const result = { copiedCount: 0, skippedCount: 0, details: [] as string[], ruleIdMap: {} as Record<string, string> };
|
|
|
|
|
|
|
|
|
|
const result = {
|
|
|
|
|
copiedCount: 0,
|
|
|
|
|
skippedCount: 0,
|
|
|
|
|
details: [] as string[],
|
|
|
|
|
ruleIdMap: {} as Record<string, string>,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await client.query("BEGIN");
|
|
|
|
|
@@ -1950,9 +2027,9 @@ class NumberingRuleService {
|
|
|
|
|
[targetCompanyCode]
|
|
|
|
|
);
|
|
|
|
|
if (deleteResult.rowCount && deleteResult.rowCount > 0) {
|
|
|
|
|
logger.info("기존 채번규칙 삭제", {
|
|
|
|
|
targetCompanyCode,
|
|
|
|
|
deletedCount: deleteResult.rowCount
|
|
|
|
|
logger.info("기존 채번규칙 삭제", {
|
|
|
|
|
targetCompanyCode,
|
|
|
|
|
deletedCount: deleteResult.rowCount,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1962,9 +2039,9 @@ class NumberingRuleService {
|
|
|
|
|
[sourceCompanyCode]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
logger.info("원본 채번규칙 조회", {
|
|
|
|
|
sourceCompanyCode,
|
|
|
|
|
count: sourceRulesResult.rowCount
|
|
|
|
|
logger.info("원본 채번규칙 조회", {
|
|
|
|
|
sourceCompanyCode,
|
|
|
|
|
count: sourceRulesResult.rowCount,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 2. 각 채번규칙 복제
|
|
|
|
|
@@ -2038,18 +2115,18 @@ class NumberingRuleService {
|
|
|
|
|
result.ruleIdMap[rule.rule_id] = newRuleId;
|
|
|
|
|
result.copiedCount++;
|
|
|
|
|
result.details.push(`복제 완료: ${rule.rule_name}`);
|
|
|
|
|
logger.info("채번규칙 복제 완료", {
|
|
|
|
|
ruleName: rule.rule_name,
|
|
|
|
|
logger.info("채번규칙 복제 완료", {
|
|
|
|
|
ruleName: rule.rule_name,
|
|
|
|
|
oldRuleId: rule.rule_id,
|
|
|
|
|
newRuleId
|
|
|
|
|
newRuleId,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. 화면 레이아웃의 numberingRuleId 참조 업데이트
|
|
|
|
|
if (Object.keys(result.ruleIdMap).length > 0) {
|
|
|
|
|
logger.info("화면 레이아웃 numberingRuleId 참조 업데이트 시작", {
|
|
|
|
|
logger.info("화면 레이아웃 numberingRuleId 참조 업데이트 시작", {
|
|
|
|
|
targetCompanyCode,
|
|
|
|
|
mappingCount: Object.keys(result.ruleIdMap).length
|
|
|
|
|
mappingCount: Object.keys(result.ruleIdMap).length,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 대상 회사의 모든 화면 레이아웃 조회
|
|
|
|
|
@@ -2069,9 +2146,13 @@ class NumberingRuleService {
|
|
|
|
|
let updated = false;
|
|
|
|
|
|
|
|
|
|
// 각 매핑에 대해 치환
|
|
|
|
|
for (const [oldRuleId, newRuleId] of Object.entries(result.ruleIdMap)) {
|
|
|
|
|
for (const [oldRuleId, newRuleId] of Object.entries(
|
|
|
|
|
result.ruleIdMap
|
|
|
|
|
)) {
|
|
|
|
|
if (propsStr.includes(`"${oldRuleId}"`)) {
|
|
|
|
|
propsStr = propsStr.split(`"${oldRuleId}"`).join(`"${newRuleId}"`);
|
|
|
|
|
propsStr = propsStr
|
|
|
|
|
.split(`"${oldRuleId}"`)
|
|
|
|
|
.join(`"${newRuleId}"`);
|
|
|
|
|
updated = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -2085,27 +2166,33 @@ class NumberingRuleService {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info("화면 레이아웃 numberingRuleId 참조 업데이트 완료", {
|
|
|
|
|
logger.info("화면 레이아웃 numberingRuleId 참조 업데이트 완료", {
|
|
|
|
|
targetCompanyCode,
|
|
|
|
|
updatedLayouts
|
|
|
|
|
updatedLayouts,
|
|
|
|
|
});
|
|
|
|
|
result.details.push(`화면 레이아웃 ${updatedLayouts}개의 채번규칙 참조 업데이트`);
|
|
|
|
|
result.details.push(
|
|
|
|
|
`화면 레이아웃 ${updatedLayouts}개의 채번규칙 참조 업데이트`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await client.query("COMMIT");
|
|
|
|
|
|
|
|
|
|
logger.info("회사별 채번규칙 복제 완료", {
|
|
|
|
|
sourceCompanyCode,
|
|
|
|
|
targetCompanyCode,
|
|
|
|
|
|
|
|
|
|
logger.info("회사별 채번규칙 복제 완료", {
|
|
|
|
|
sourceCompanyCode,
|
|
|
|
|
targetCompanyCode,
|
|
|
|
|
copiedCount: result.copiedCount,
|
|
|
|
|
skippedCount: result.skippedCount,
|
|
|
|
|
ruleIdMapCount: Object.keys(result.ruleIdMap).length
|
|
|
|
|
ruleIdMapCount: Object.keys(result.ruleIdMap).length,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
await client.query("ROLLBACK");
|
|
|
|
|
logger.error("회사별 채번규칙 복제 실패", { error, sourceCompanyCode, targetCompanyCode });
|
|
|
|
|
logger.error("회사별 채번규칙 복제 실패", {
|
|
|
|
|
error,
|
|
|
|
|
sourceCompanyCode,
|
|
|
|
|
targetCompanyCode,
|
|
|
|
|
});
|
|
|
|
|
throw error;
|
|
|
|
|
} finally {
|
|
|
|
|
client.release();
|
|
|
|
|
|