엑셀 업로드,다운로드 기능 개선

This commit is contained in:
kjs
2026-01-09 15:32:02 +09:00
parent ee3a648917
commit aa0698556e
9 changed files with 1619 additions and 387 deletions

View File

@@ -283,10 +283,81 @@ class MasterDetailExcelService {
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 masterSelectCols = masterColumns.map(col => `m."${col.name}"`);
const detailSelectCols = detailColumns.map(col => `d."${col.name}"`);
const selectClause = [...masterSelectCols, ...detailSelectCols].join(", ");
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[] = [];
@@ -304,6 +375,8 @@ class MasterDetailExcelService {
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";
@@ -325,6 +398,7 @@ class MasterDetailExcelService {
LEFT JOIN "${detailTable}" d
ON m."${masterKeyColumn}" = d."${detailFkColumn}"
AND m.company_code = d.company_code
${entityJoinClauses}
${whereClause}
ORDER BY m."${masterKeyColumn}", d.id
`;
@@ -353,6 +427,37 @@ class MasterDetailExcelService {
}
}
/**
* 특정 테이블에서 참조 테이블로의 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;
}
}
/**
* 마스터-디테일 데이터 업로드 (엑셀 업로드용)
*
@@ -521,6 +626,168 @@ class MasterDetailExcelService {
return result;
}
/**
* 마스터-디테일 간단 모드 업로드
*
* 마스터 정보는 UI에서 선택하고, 엑셀은 디테일 데이터만 포함
* 채번 규칙을 통해 마스터 키 자동 생성
*
* @param screenId 화면 ID
* @param detailData 디테일 데이터 배열
* @param masterFieldValues UI에서 선택한 마스터 필드 값
* @param numberingRuleId 채번 규칙 ID (optional)
* @param companyCode 회사 코드
* @param userId 사용자 ID
*/
async uploadSimple(
screenId: number,
detailData: Record<string, any>[],
masterFieldValues: Record<string, any>,
numberingRuleId: string | undefined,
companyCode: string,
userId: string
): Promise<{
success: boolean;
masterInserted: number;
detailInserted: number;
generatedKey: string;
errors: string[];
}> {
const result = {
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,
});
} 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();