ERP-node/backend-node/src/services/masterDetailExcelService.ts

887 lines
29 KiB
TypeScript

/**
* 마스터-디테일 엑셀 처리 서비스
*
* 분할 패널 화면의 마스터-디테일 구조를 자동 감지하고
* 엑셀 다운로드/업로드 시 JOIN 및 그룹화 처리를 수행합니다.
*/
import { query, queryOne, transaction, getPool } from "../database/db";
import { logger } from "../utils/logger";
// ================================
// 인터페이스 정의
// ================================
/**
* 마스터-디테일 관계 정보
*/
export interface MasterDetailRelation {
masterTable: string;
detailTable: string;
masterKeyColumn: string; // 마스터 테이블의 키 컬럼 (예: order_no)
detailFkColumn: string; // 디테일 테이블의 FK 컬럼 (예: order_no)
masterColumns: ColumnInfo[];
detailColumns: ColumnInfo[];
}
/**
* 컬럼 정보
*/
export interface ColumnInfo {
name: string;
label: string;
inputType: string;
isFromMaster: boolean;
}
/**
* 분할 패널 설정
*/
export interface SplitPanelConfig {
leftPanel: {
tableName: string;
columns: Array<{ name: string; label: string; width?: number }>;
};
rightPanel: {
tableName: string;
columns: Array<{ name: string; label: string; width?: number }>;
relation?: {
type: string;
foreignKey?: string;
leftColumn?: string;
// 복합키 지원 (새로운 방식)
keys?: Array<{
leftColumn: string;
rightColumn: string;
}>;
};
};
}
/**
* 엑셀 다운로드 결과
*/
export interface ExcelDownloadData {
headers: string[]; // 컬럼 라벨들
columns: string[]; // 컬럼명들
data: Record<string, any>[];
masterColumns: string[]; // 마스터 컬럼 목록
detailColumns: string[]; // 디테일 컬럼 목록
joinKey: string; // 조인 키
}
/**
* 엑셀 업로드 결과
*/
export interface ExcelUploadResult {
success: boolean;
masterInserted: number;
masterUpdated: number;
detailInserted: number;
detailDeleted: number;
errors: string[];
}
// ================================
// 서비스 클래스
// ================================
class MasterDetailExcelService {
/**
* 화면 ID로 분할 패널 설정 조회
*/
async getSplitPanelConfig(screenId: number): Promise<SplitPanelConfig | null> {
try {
logger.info(`분할 패널 설정 조회: screenId=${screenId}`);
// screen_layouts에서 split-panel-layout 컴포넌트 찾기
const result = await queryOne<any>(
`SELECT properties->>'componentConfig' as config
FROM screen_layouts
WHERE screen_id = $1
AND component_type = 'component'
AND properties->>'componentType' = 'split-panel-layout'
LIMIT 1`,
[screenId]
);
if (!result || !result.config) {
logger.info(`분할 패널 없음: screenId=${screenId}`);
return null;
}
const config = typeof result.config === "string"
? JSON.parse(result.config)
: result.config;
logger.info(`분할 패널 설정 발견:`, {
leftTable: config.leftPanel?.tableName,
rightTable: config.rightPanel?.tableName,
relation: config.rightPanel?.relation,
});
return {
leftPanel: config.leftPanel,
rightPanel: config.rightPanel,
};
} catch (error: any) {
logger.error(`분할 패널 설정 조회 실패: ${error.message}`);
return null;
}
}
/**
* column_labels에서 Entity 관계 정보 조회
* 디테일 테이블에서 마스터 테이블을 참조하는 컬럼 찾기
*/
async getEntityRelation(
detailTable: string,
masterTable: string
): Promise<{ detailFkColumn: string; masterKeyColumn: string } | null> {
try {
logger.info(`Entity 관계 조회: ${detailTable} -> ${masterTable}`);
const result = await queryOne<any>(
`SELECT column_name, reference_column
FROM column_labels
WHERE table_name = $1
AND input_type = 'entity'
AND reference_table = $2
LIMIT 1`,
[detailTable, masterTable]
);
if (!result) {
logger.warn(`Entity 관계 없음: ${detailTable} -> ${masterTable}`);
return null;
}
logger.info(`Entity 관계 발견: ${detailTable}.${result.column_name} -> ${masterTable}.${result.reference_column}`);
return {
detailFkColumn: result.column_name,
masterKeyColumn: result.reference_column,
};
} catch (error: any) {
logger.error(`Entity 관계 조회 실패: ${error.message}`);
return null;
}
}
/**
* 테이블의 컬럼 라벨 정보 조회
*/
async getColumnLabels(tableName: string): Promise<Map<string, string>> {
try {
const result = await query<any>(
`SELECT column_name, column_label
FROM column_labels
WHERE table_name = $1`,
[tableName]
);
const labelMap = new Map<string, string>();
for (const row of result) {
labelMap.set(row.column_name, row.column_label || row.column_name);
}
return labelMap;
} catch (error: any) {
logger.error(`컬럼 라벨 조회 실패: ${error.message}`);
return new Map();
}
}
/**
* 마스터-디테일 관계 정보 조합
*/
async getMasterDetailRelation(
screenId: number
): Promise<MasterDetailRelation | null> {
try {
// 1. 분할 패널 설정 조회
const splitPanel = await this.getSplitPanelConfig(screenId);
if (!splitPanel) {
return null;
}
const masterTable = splitPanel.leftPanel.tableName;
const detailTable = splitPanel.rightPanel.tableName;
if (!masterTable || !detailTable) {
logger.warn("마스터 또는 디테일 테이블명 없음");
return null;
}
// 2. 분할 패널의 relation 정보가 있으면 우선 사용
// 🔥 keys 배열을 우선 사용 (새로운 복합키 지원 방식)
let masterKeyColumn: string | undefined;
let detailFkColumn: string | undefined;
const relationKeys = splitPanel.rightPanel.relation?.keys;
if (relationKeys && relationKeys.length > 0) {
// keys 배열에서 첫 번째 키 사용
masterKeyColumn = relationKeys[0].leftColumn;
detailFkColumn = relationKeys[0].rightColumn;
logger.info(`keys 배열에서 관계 정보 사용: ${masterKeyColumn} -> ${detailFkColumn}`);
} else {
// 하위 호환성: 기존 leftColumn/foreignKey 사용
masterKeyColumn = splitPanel.rightPanel.relation?.leftColumn;
detailFkColumn = splitPanel.rightPanel.relation?.foreignKey;
}
// 3. relation 정보가 없으면 column_labels에서 Entity 관계 조회
if (!masterKeyColumn || !detailFkColumn) {
const entityRelation = await this.getEntityRelation(detailTable, masterTable);
if (entityRelation) {
masterKeyColumn = entityRelation.masterKeyColumn;
detailFkColumn = entityRelation.detailFkColumn;
}
}
if (!masterKeyColumn || !detailFkColumn) {
logger.warn("조인 키 정보를 찾을 수 없음");
return null;
}
// 4. 컬럼 라벨 정보 조회
const masterLabels = await this.getColumnLabels(masterTable);
const detailLabels = await this.getColumnLabels(detailTable);
// 5. 마스터 컬럼 정보 구성
const masterColumns: ColumnInfo[] = splitPanel.leftPanel.columns.map(col => ({
name: col.name,
label: masterLabels.get(col.name) || col.label || col.name,
inputType: "text",
isFromMaster: true,
}));
// 6. 디테일 컬럼 정보 구성 (FK 컬럼 제외)
const detailColumns: ColumnInfo[] = splitPanel.rightPanel.columns
.filter(col => col.name !== detailFkColumn) // FK 컬럼 제외
.map(col => ({
name: col.name,
label: detailLabels.get(col.name) || col.label || col.name,
inputType: "text",
isFromMaster: false,
}));
logger.info(`마스터-디테일 관계 구성 완료:`, {
masterTable,
detailTable,
masterKeyColumn,
detailFkColumn,
masterColumnCount: masterColumns.length,
detailColumnCount: detailColumns.length,
});
return {
masterTable,
detailTable,
masterKeyColumn,
detailFkColumn,
masterColumns,
detailColumns,
};
} catch (error: any) {
logger.error(`마스터-디테일 관계 조회 실패: ${error.message}`);
return null;
}
}
/**
* 마스터-디테일 JOIN 데이터 조회 (엑셀 다운로드용)
*/
async getJoinedData(
relation: MasterDetailRelation,
companyCode: string,
filters?: Record<string, any>
): Promise<ExcelDownloadData> {
try {
const { masterTable, detailTable, masterKeyColumn, detailFkColumn, masterColumns, detailColumns } = relation;
// 조인 컬럼과 일반 컬럼 분리
// 조인 컬럼 형식: "테이블명.컬럼명" (예: customer_mng.customer_name)
const entityJoins: Array<{
refTable: string;
refColumn: string;
sourceColumn: string;
alias: string;
displayColumn: string;
}> = [];
// SELECT 절 구성
const selectParts: string[] = [];
let aliasIndex = 0;
// 마스터 컬럼 처리
for (const col of masterColumns) {
if (col.name.includes(".")) {
// 조인 컬럼: 테이블명.컬럼명
const [refTable, displayColumn] = col.name.split(".");
const alias = `ej${aliasIndex++}`;
// column_labels에서 FK 컬럼 찾기
const fkColumn = await this.findForeignKeyColumn(masterTable, refTable);
if (fkColumn) {
entityJoins.push({
refTable,
refColumn: fkColumn.referenceColumn,
sourceColumn: fkColumn.sourceColumn,
alias,
displayColumn,
});
selectParts.push(`${alias}."${displayColumn}" AS "${col.name}"`);
} else {
// FK를 못 찾으면 NULL로 처리
selectParts.push(`NULL AS "${col.name}"`);
}
} else {
// 일반 컬럼
selectParts.push(`m."${col.name}"`);
}
}
// 디테일 컬럼 처리
for (const col of detailColumns) {
if (col.name.includes(".")) {
// 조인 컬럼: 테이블명.컬럼명
const [refTable, displayColumn] = col.name.split(".");
const alias = `ej${aliasIndex++}`;
// column_labels에서 FK 컬럼 찾기
const fkColumn = await this.findForeignKeyColumn(detailTable, refTable);
if (fkColumn) {
entityJoins.push({
refTable,
refColumn: fkColumn.referenceColumn,
sourceColumn: fkColumn.sourceColumn,
alias,
displayColumn,
});
selectParts.push(`${alias}."${displayColumn}" AS "${col.name}"`);
} else {
selectParts.push(`NULL AS "${col.name}"`);
}
} else {
// 일반 컬럼
selectParts.push(`d."${col.name}"`);
}
}
const selectClause = selectParts.join(", ");
// 엔티티 조인 절 구성
const entityJoinClauses = entityJoins.map(ej =>
`LEFT JOIN "${ej.refTable}" ${ej.alias} ON m."${ej.sourceColumn}" = ${ej.alias}."${ej.refColumn}"`
).join("\n ");
// WHERE 절 구성
const whereConditions: string[] = [];
const params: any[] = [];
let paramIndex = 1;
// 회사 코드 필터 (최고 관리자 제외)
if (companyCode && companyCode !== "*") {
whereConditions.push(`m.company_code = $${paramIndex}`);
params.push(companyCode);
paramIndex++;
}
// 추가 필터 적용
if (filters) {
for (const [key, value] of Object.entries(filters)) {
if (value !== undefined && value !== null && value !== "") {
// 조인 컬럼인지 확인
if (key.includes(".")) continue;
// 마스터 테이블 컬럼인지 확인
const isMasterCol = masterColumns.some(c => c.name === key);
const tableAlias = isMasterCol ? "m" : "d";
whereConditions.push(`${tableAlias}."${key}" = $${paramIndex}`);
params.push(value);
paramIndex++;
}
}
}
const whereClause = whereConditions.length > 0
? `WHERE ${whereConditions.join(" AND ")}`
: "";
// JOIN 쿼리 실행
const sql = `
SELECT ${selectClause}
FROM "${masterTable}" m
LEFT JOIN "${detailTable}" d
ON m."${masterKeyColumn}" = d."${detailFkColumn}"
AND m.company_code = d.company_code
${entityJoinClauses}
${whereClause}
ORDER BY m."${masterKeyColumn}", d.id
`;
logger.info(`마스터-디테일 JOIN 쿼리:`, { sql, params });
const data = await query<any>(sql, params);
// 헤더 및 컬럼 정보 구성
const headers = [...masterColumns.map(c => c.label), ...detailColumns.map(c => c.label)];
const columns = [...masterColumns.map(c => c.name), ...detailColumns.map(c => c.name)];
logger.info(`마스터-디테일 데이터 조회 완료: ${data.length}`);
return {
headers,
columns,
data,
masterColumns: masterColumns.map(c => c.name),
detailColumns: detailColumns.map(c => c.name),
joinKey: masterKeyColumn,
};
} catch (error: any) {
logger.error(`마스터-디테일 데이터 조회 실패: ${error.message}`);
throw error;
}
}
/**
* 특정 테이블에서 참조 테이블로의 FK 컬럼 찾기
*/
private async findForeignKeyColumn(
sourceTable: string,
referenceTable: string
): Promise<{ sourceColumn: string; referenceColumn: string } | null> {
try {
const result = await query<{ column_name: string; reference_column: string }>(
`SELECT column_name, reference_column
FROM column_labels
WHERE table_name = $1
AND reference_table = $2
AND input_type = 'entity'
LIMIT 1`,
[sourceTable, referenceTable]
);
if (result.length > 0) {
return {
sourceColumn: result[0].column_name,
referenceColumn: result[0].reference_column,
};
}
return null;
} catch (error) {
logger.error(`FK 컬럼 조회 실패: ${sourceTable} -> ${referenceTable}`, error);
return null;
}
}
/**
* 마스터-디테일 데이터 업로드 (엑셀 업로드용)
*
* 처리 로직:
* 1. 엑셀 데이터를 마스터 키로 그룹화
* 2. 각 그룹의 첫 번째 행에서 마스터 데이터 추출 → UPSERT
* 3. 해당 마스터 키의 기존 디테일 삭제
* 4. 새 디테일 데이터 INSERT
*/
async uploadJoinedData(
relation: MasterDetailRelation,
data: Record<string, any>[],
companyCode: string,
userId?: string
): Promise<ExcelUploadResult> {
const result: ExcelUploadResult = {
success: false,
masterInserted: 0,
masterUpdated: 0,
detailInserted: 0,
detailDeleted: 0,
errors: [],
};
const pool = getPool();
const client = await pool.connect();
try {
await client.query("BEGIN");
const { masterTable, detailTable, masterKeyColumn, detailFkColumn, masterColumns, detailColumns } = relation;
// 1. 데이터를 마스터 키로 그룹화
const groupedData = new Map<string, Record<string, any>[]>();
for (const row of data) {
const masterKey = row[masterKeyColumn];
if (!masterKey) {
result.errors.push(`마스터 키(${masterKeyColumn}) 값이 없는 행이 있습니다.`);
continue;
}
if (!groupedData.has(masterKey)) {
groupedData.set(masterKey, []);
}
groupedData.get(masterKey)!.push(row);
}
logger.info(`데이터 그룹화 완료: ${groupedData.size}개 마스터 그룹`);
// 2. 각 그룹 처리
for (const [masterKey, rows] of groupedData.entries()) {
try {
// 2a. 마스터 데이터 추출 (첫 번째 행에서)
const masterData: Record<string, any> = {};
for (const col of masterColumns) {
if (rows[0][col.name] !== undefined) {
masterData[col.name] = rows[0][col.name];
}
}
// 회사 코드, 작성자 추가
masterData.company_code = companyCode;
if (userId) {
masterData.writer = userId;
}
// 2b. 마스터 UPSERT
const existingMaster = await client.query(
`SELECT id FROM "${masterTable}" WHERE "${masterKeyColumn}" = $1 AND company_code = $2`,
[masterKey, companyCode]
);
if (existingMaster.rows.length > 0) {
// UPDATE
const updateCols = Object.keys(masterData)
.filter(k => k !== masterKeyColumn && k !== "id")
.map((k, i) => `"${k}" = $${i + 1}`);
const updateValues = Object.keys(masterData)
.filter(k => k !== masterKeyColumn && k !== "id")
.map(k => masterData[k]);
if (updateCols.length > 0) {
await client.query(
`UPDATE "${masterTable}"
SET ${updateCols.join(", ")}, updated_date = NOW()
WHERE "${masterKeyColumn}" = $${updateValues.length + 1} AND company_code = $${updateValues.length + 2}`,
[...updateValues, masterKey, companyCode]
);
}
result.masterUpdated++;
} else {
// INSERT
const insertCols = Object.keys(masterData);
const insertPlaceholders = insertCols.map((_, i) => `$${i + 1}`);
const insertValues = insertCols.map(k => masterData[k]);
await client.query(
`INSERT INTO "${masterTable}" (${insertCols.map(c => `"${c}"`).join(", ")}, created_date)
VALUES (${insertPlaceholders.join(", ")}, NOW())`,
insertValues
);
result.masterInserted++;
}
// 2c. 기존 디테일 삭제
const deleteResult = await client.query(
`DELETE FROM "${detailTable}" WHERE "${detailFkColumn}" = $1 AND company_code = $2`,
[masterKey, companyCode]
);
result.detailDeleted += deleteResult.rowCount || 0;
// 2d. 새 디테일 INSERT
for (const row of rows) {
const detailData: Record<string, any> = {};
// FK 컬럼 추가
detailData[detailFkColumn] = masterKey;
detailData.company_code = companyCode;
if (userId) {
detailData.writer = userId;
}
// 디테일 컬럼 데이터 추출
for (const col of detailColumns) {
if (row[col.name] !== undefined) {
detailData[col.name] = row[col.name];
}
}
const insertCols = Object.keys(detailData);
const insertPlaceholders = insertCols.map((_, i) => `$${i + 1}`);
const insertValues = insertCols.map(k => detailData[k]);
await client.query(
`INSERT INTO "${detailTable}" (${insertCols.map(c => `"${c}"`).join(", ")}, created_date)
VALUES (${insertPlaceholders.join(", ")}, NOW())`,
insertValues
);
result.detailInserted++;
}
} catch (error: any) {
result.errors.push(`마스터 키 ${masterKey} 처리 실패: ${error.message}`);
logger.error(`마스터 키 ${masterKey} 처리 실패:`, error);
}
}
await client.query("COMMIT");
result.success = result.errors.length === 0 || result.masterInserted + result.masterUpdated > 0;
logger.info(`마스터-디테일 업로드 완료:`, {
masterInserted: result.masterInserted,
masterUpdated: result.masterUpdated,
detailInserted: result.detailInserted,
detailDeleted: result.detailDeleted,
errors: result.errors.length,
});
} catch (error: any) {
await client.query("ROLLBACK");
result.errors.push(`트랜잭션 실패: ${error.message}`);
logger.error(`마스터-디테일 업로드 트랜잭션 실패:`, error);
} finally {
client.release();
}
return result;
}
/**
* 마스터-디테일 간단 모드 업로드
*
* 마스터 정보는 UI에서 선택하고, 엑셀은 디테일 데이터만 포함
* 채번 규칙을 통해 마스터 키 자동 생성
*
* @param screenId 화면 ID
* @param detailData 디테일 데이터 배열
* @param masterFieldValues UI에서 선택한 마스터 필드 값
* @param numberingRuleId 채번 규칙 ID (optional)
* @param companyCode 회사 코드
* @param userId 사용자 ID
* @param afterUploadFlowId 업로드 후 실행할 노드 플로우 ID (optional, 하위 호환성)
* @param afterUploadFlows 업로드 후 실행할 노드 플로우 배열 (optional)
*/
async uploadSimple(
screenId: number,
detailData: Record<string, any>[],
masterFieldValues: Record<string, any>,
numberingRuleId: string | undefined,
companyCode: string,
userId: string,
afterUploadFlowId?: string,
afterUploadFlows?: Array<{ flowId: string; order: number }>
): Promise<{
success: boolean;
masterInserted: number;
detailInserted: number;
generatedKey: string;
errors: string[];
controlResult?: any;
}> {
const result: {
success: boolean;
masterInserted: number;
detailInserted: number;
generatedKey: string;
errors: string[];
controlResult?: any;
} = {
success: false,
masterInserted: 0,
detailInserted: 0,
generatedKey: "",
errors: [] as string[],
};
const pool = getPool();
const client = await pool.connect();
try {
await client.query("BEGIN");
// 1. 마스터-디테일 관계 정보 조회
const relation = await this.getMasterDetailRelation(screenId);
if (!relation) {
throw new Error("마스터-디테일 관계 정보를 찾을 수 없습니다.");
}
const { masterTable, detailTable, masterKeyColumn, detailFkColumn } = relation;
// 2. 채번 처리
let generatedKey: string;
if (numberingRuleId) {
// 채번 규칙으로 키 생성
generatedKey = await this.generateNumberWithRule(client, numberingRuleId, companyCode);
} else {
// 채번 규칙 없으면 마스터 필드에서 키 값 사용
generatedKey = masterFieldValues[masterKeyColumn];
if (!generatedKey) {
throw new Error(`마스터 키(${masterKeyColumn}) 값이 필요합니다.`);
}
}
result.generatedKey = generatedKey;
logger.info(`채번 결과: ${generatedKey}`);
// 3. 마스터 레코드 생성
const masterData: Record<string, any> = {
...masterFieldValues,
[masterKeyColumn]: generatedKey,
company_code: companyCode,
writer: userId,
};
// 마스터 컬럼명 목록 구성
const masterCols = Object.keys(masterData).filter(k => masterData[k] !== undefined);
const masterPlaceholders = masterCols.map((_, i) => `$${i + 1}`);
const masterValues = masterCols.map(k => masterData[k]);
await client.query(
`INSERT INTO "${masterTable}" (${masterCols.map(c => `"${c}"`).join(", ")}, created_date)
VALUES (${masterPlaceholders.join(", ")}, NOW())`,
masterValues
);
result.masterInserted = 1;
logger.info(`마스터 레코드 생성: ${masterTable}, key=${generatedKey}`);
// 4. 디테일 레코드들 생성
for (const row of detailData) {
try {
const detailRowData: Record<string, any> = {
...row,
[detailFkColumn]: generatedKey,
company_code: companyCode,
writer: userId,
};
// 빈 값 필터링 및 id 제외
const detailCols = Object.keys(detailRowData).filter(k =>
k !== "id" &&
detailRowData[k] !== undefined &&
detailRowData[k] !== null &&
detailRowData[k] !== ""
);
const detailPlaceholders = detailCols.map((_, i) => `$${i + 1}`);
const detailValues = detailCols.map(k => detailRowData[k]);
await client.query(
`INSERT INTO "${detailTable}" (${detailCols.map(c => `"${c}"`).join(", ")}, created_date)
VALUES (${detailPlaceholders.join(", ")}, NOW())`,
detailValues
);
result.detailInserted++;
} catch (error: any) {
result.errors.push(`디테일 행 처리 실패: ${error.message}`);
logger.error(`디테일 행 처리 실패:`, error);
}
}
await client.query("COMMIT");
result.success = result.errors.length === 0 || result.detailInserted > 0;
logger.info(`마스터-디테일 간단 모드 업로드 완료:`, {
masterInserted: result.masterInserted,
detailInserted: result.detailInserted,
generatedKey: result.generatedKey,
errors: result.errors.length,
});
// 업로드 후 제어 실행 (단일 또는 다중)
const flowsToExecute = afterUploadFlows && afterUploadFlows.length > 0
? afterUploadFlows // 다중 제어
: afterUploadFlowId
? [{ flowId: afterUploadFlowId, order: 1 }] // 단일 (하위 호환성)
: [];
if (flowsToExecute.length > 0 && result.success) {
try {
const { NodeFlowExecutionService } = await import("./nodeFlowExecutionService");
// 마스터 데이터를 제어에 전달
const masterData = {
...masterFieldValues,
[relation!.masterKeyColumn]: result.generatedKey,
company_code: companyCode,
};
const controlResults: any[] = [];
// 순서대로 제어 실행
for (const flow of flowsToExecute.sort((a, b) => a.order - b.order)) {
logger.info(`업로드 후 제어 실행: flowId=${flow.flowId}, order=${flow.order}`);
const controlResult = await NodeFlowExecutionService.executeFlow(
parseInt(flow.flowId),
{
sourceData: [masterData],
dataSourceType: "formData",
buttonId: "excel-upload-button",
screenId: screenId,
userId: userId,
companyCode: companyCode,
formData: masterData,
}
);
controlResults.push({
flowId: flow.flowId,
order: flow.order,
success: controlResult.success,
message: controlResult.message,
executedNodes: controlResult.nodes?.length || 0,
});
}
result.controlResult = {
success: controlResults.every(r => r.success),
executedFlows: controlResults.length,
results: controlResults,
};
logger.info(`업로드 후 제어 실행 완료: ${controlResults.length}개 실행`, result.controlResult);
} catch (controlError: any) {
logger.error(`업로드 후 제어 실행 실패:`, controlError);
result.controlResult = {
success: false,
message: `제어 실행 실패: ${controlError.message}`,
};
}
}
} catch (error: any) {
await client.query("ROLLBACK");
result.errors.push(`트랜잭션 실패: ${error.message}`);
logger.error(`마스터-디테일 간단 모드 업로드 실패:`, error);
} finally {
client.release();
}
return result;
}
/**
* 채번 규칙으로 번호 생성 (기존 numberingRuleService 사용)
*/
private async generateNumberWithRule(
client: any,
ruleId: string,
companyCode: string
): Promise<string> {
try {
// 기존 numberingRuleService를 사용하여 코드 할당
const { numberingRuleService } = await import("./numberingRuleService");
const generatedCode = await numberingRuleService.allocateCode(ruleId, companyCode);
logger.info(`채번 생성 (numberingRuleService): rule=${ruleId}, result=${generatedCode}`);
return generatedCode;
} catch (error: any) {
logger.error(`채번 생성 실패: rule=${ruleId}, error=${error.message}`);
throw error;
}
}
}
export const masterDetailExcelService = new MasterDetailExcelService();