엑셀 업로드 카테고리 타입 자동 감지
This commit is contained in:
parent
c181385f11
commit
d90a403ed9
|
|
@ -1,6 +1,7 @@
|
|||
import { query, queryOne, transaction, getPool } from "../database/db";
|
||||
import { EventTriggerService } from "./eventTriggerService";
|
||||
import { DataflowControlService } from "./dataflowControlService";
|
||||
import tableCategoryValueService from "./tableCategoryValueService";
|
||||
|
||||
export interface FormDataResult {
|
||||
id: number;
|
||||
|
|
@ -427,6 +428,24 @@ export class DynamicFormService {
|
|||
dataToInsert,
|
||||
});
|
||||
|
||||
// 카테고리 타입 컬럼의 라벨 값을 코드 값으로 변환 (엑셀 업로드 등 지원)
|
||||
console.log("🏷️ 카테고리 라벨→코드 변환 시작...");
|
||||
const companyCodeForCategory = company_code || "*";
|
||||
const { convertedData: categoryConvertedData, conversions } =
|
||||
await tableCategoryValueService.convertCategoryLabelsToCodesForData(
|
||||
tableName,
|
||||
companyCodeForCategory,
|
||||
dataToInsert
|
||||
);
|
||||
|
||||
if (conversions.length > 0) {
|
||||
console.log(`🏷️ 카테고리 라벨→코드 변환 완료: ${conversions.length}개`, conversions);
|
||||
// 변환된 데이터로 교체
|
||||
Object.assign(dataToInsert, categoryConvertedData);
|
||||
} else {
|
||||
console.log("🏷️ 카테고리 라벨→코드 변환 없음 (카테고리 컬럼 없거나 이미 코드 값)");
|
||||
}
|
||||
|
||||
// 테이블 컬럼 정보 조회하여 타입 변환 적용
|
||||
console.log("🔍 테이블 컬럼 정보 조회 중...");
|
||||
const columnInfo = await this.getTableColumnInfo(tableName);
|
||||
|
|
|
|||
|
|
@ -1398,6 +1398,220 @@ class TableCategoryValueService {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블의 카테고리 타입 컬럼과 해당 값 매핑 조회 (라벨 → 코드 변환용)
|
||||
*
|
||||
* 엑셀 업로드 등에서 라벨 값을 코드 값으로 변환할 때 사용
|
||||
*
|
||||
* @param tableName - 테이블명
|
||||
* @param companyCode - 회사 코드
|
||||
* @returns { [columnName]: { [label]: code } } 형태의 매핑 객체
|
||||
*/
|
||||
async getCategoryLabelToCodeMapping(
|
||||
tableName: string,
|
||||
companyCode: string
|
||||
): Promise<Record<string, Record<string, string>>> {
|
||||
try {
|
||||
logger.info("카테고리 라벨→코드 매핑 조회", { tableName, companyCode });
|
||||
|
||||
const pool = getPool();
|
||||
|
||||
// 1. 해당 테이블의 카테고리 타입 컬럼 조회
|
||||
const categoryColumnsQuery = `
|
||||
SELECT column_name
|
||||
FROM table_type_columns
|
||||
WHERE table_name = $1
|
||||
AND input_type = 'category'
|
||||
`;
|
||||
const categoryColumnsResult = await pool.query(categoryColumnsQuery, [tableName]);
|
||||
|
||||
if (categoryColumnsResult.rows.length === 0) {
|
||||
logger.info("카테고리 타입 컬럼 없음", { tableName });
|
||||
return {};
|
||||
}
|
||||
|
||||
const categoryColumns = categoryColumnsResult.rows.map(row => row.column_name);
|
||||
logger.info(`카테고리 컬럼 ${categoryColumns.length}개 발견`, { categoryColumns });
|
||||
|
||||
// 2. 각 카테고리 컬럼의 라벨→코드 매핑 조회
|
||||
const result: Record<string, Record<string, string>> = {};
|
||||
|
||||
for (const columnName of categoryColumns) {
|
||||
let query: string;
|
||||
let params: any[];
|
||||
|
||||
if (companyCode === "*") {
|
||||
// 최고 관리자: 모든 카테고리 값 조회
|
||||
query = `
|
||||
SELECT value_code, value_label
|
||||
FROM table_column_category_values
|
||||
WHERE table_name = $1
|
||||
AND column_name = $2
|
||||
AND is_active = true
|
||||
`;
|
||||
params = [tableName, columnName];
|
||||
} else {
|
||||
// 일반 회사: 자신의 카테고리 값 + 공통 카테고리 값 조회
|
||||
query = `
|
||||
SELECT value_code, value_label
|
||||
FROM table_column_category_values
|
||||
WHERE table_name = $1
|
||||
AND column_name = $2
|
||||
AND is_active = true
|
||||
AND (company_code = $3 OR company_code = '*')
|
||||
`;
|
||||
params = [tableName, columnName, companyCode];
|
||||
}
|
||||
|
||||
const valuesResult = await pool.query(query, params);
|
||||
|
||||
// { [label]: code } 형태로 변환
|
||||
const labelToCodeMap: Record<string, string> = {};
|
||||
for (const row of valuesResult.rows) {
|
||||
// 라벨을 소문자로 변환하여 대소문자 구분 없이 매핑
|
||||
labelToCodeMap[row.value_label] = row.value_code;
|
||||
// 소문자 키도 추가 (대소문자 무시 검색용)
|
||||
labelToCodeMap[row.value_label.toLowerCase()] = row.value_code;
|
||||
}
|
||||
|
||||
if (Object.keys(labelToCodeMap).length > 0) {
|
||||
result[columnName] = labelToCodeMap;
|
||||
logger.info(`컬럼 ${columnName}의 라벨→코드 매핑 ${valuesResult.rows.length}개 조회`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`카테고리 라벨→코드 매핑 조회 완료`, {
|
||||
tableName,
|
||||
columnCount: Object.keys(result).length
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
logger.error(`카테고리 라벨→코드 매핑 조회 실패: ${error.message}`, { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터의 카테고리 라벨 값을 코드 값으로 변환
|
||||
*
|
||||
* 엑셀 업로드 등에서 사용자가 입력한 라벨 값을 DB 저장용 코드 값으로 변환
|
||||
*
|
||||
* @param tableName - 테이블명
|
||||
* @param companyCode - 회사 코드
|
||||
* @param data - 변환할 데이터 객체
|
||||
* @returns 라벨이 코드로 변환된 데이터 객체
|
||||
*/
|
||||
async convertCategoryLabelsToCodesForData(
|
||||
tableName: string,
|
||||
companyCode: string,
|
||||
data: Record<string, any>
|
||||
): Promise<{ convertedData: Record<string, any>; conversions: Array<{ column: string; label: string; code: string }> }> {
|
||||
try {
|
||||
// 라벨→코드 매핑 조회
|
||||
const labelToCodeMapping = await this.getCategoryLabelToCodeMapping(tableName, companyCode);
|
||||
|
||||
if (Object.keys(labelToCodeMapping).length === 0) {
|
||||
// 카테고리 컬럼 없음
|
||||
return { convertedData: data, conversions: [] };
|
||||
}
|
||||
|
||||
const convertedData = { ...data };
|
||||
const conversions: Array<{ column: string; label: string; code: string }> = [];
|
||||
|
||||
for (const [columnName, labelCodeMap] of Object.entries(labelToCodeMapping)) {
|
||||
const value = data[columnName];
|
||||
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
const stringValue = String(value).trim();
|
||||
|
||||
// 다중 값 확인 (쉼표로 구분된 경우)
|
||||
if (stringValue.includes(",")) {
|
||||
// 다중 카테고리 값 처리
|
||||
const labels = stringValue.split(",").map(s => s.trim()).filter(s => s !== "");
|
||||
const convertedCodes: string[] = [];
|
||||
let allConverted = true;
|
||||
|
||||
for (const label of labels) {
|
||||
// 정확한 라벨 매칭 시도
|
||||
let matchedCode = labelCodeMap[label];
|
||||
|
||||
// 대소문자 무시 매칭
|
||||
if (!matchedCode) {
|
||||
matchedCode = labelCodeMap[label.toLowerCase()];
|
||||
}
|
||||
|
||||
if (matchedCode) {
|
||||
convertedCodes.push(matchedCode);
|
||||
conversions.push({
|
||||
column: columnName,
|
||||
label: label,
|
||||
code: matchedCode,
|
||||
});
|
||||
logger.info(`카테고리 라벨→코드 변환 (다중): ${columnName} "${label}" → "${matchedCode}"`);
|
||||
} else {
|
||||
// 이미 코드값인지 확인
|
||||
const isAlreadyCode = Object.values(labelCodeMap).includes(label);
|
||||
if (isAlreadyCode) {
|
||||
// 이미 코드값이면 그대로 사용
|
||||
convertedCodes.push(label);
|
||||
} else {
|
||||
// 라벨도 코드도 아니면 원래 값 유지
|
||||
convertedCodes.push(label);
|
||||
allConverted = false;
|
||||
logger.warn(`카테고리 값 매핑 없음 (다중): ${columnName} = "${label}" (라벨도 코드도 아님)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 변환된 코드들을 쉼표로 합쳐서 저장
|
||||
convertedData[columnName] = convertedCodes.join(",");
|
||||
logger.info(`다중 카테고리 변환 완료: ${columnName} "${stringValue}" → "${convertedData[columnName]}"`);
|
||||
} else {
|
||||
// 단일 값 처리
|
||||
// 정확한 라벨 매칭 시도
|
||||
let matchedCode = labelCodeMap[stringValue];
|
||||
|
||||
// 대소문자 무시 매칭
|
||||
if (!matchedCode) {
|
||||
matchedCode = labelCodeMap[stringValue.toLowerCase()];
|
||||
}
|
||||
|
||||
if (matchedCode) {
|
||||
// 라벨 값을 코드 값으로 변환
|
||||
convertedData[columnName] = matchedCode;
|
||||
conversions.push({
|
||||
column: columnName,
|
||||
label: stringValue,
|
||||
code: matchedCode,
|
||||
});
|
||||
logger.info(`카테고리 라벨→코드 변환: ${columnName} "${stringValue}" → "${matchedCode}"`);
|
||||
} else {
|
||||
// 이미 코드값인지 확인 (역방향 확인)
|
||||
const isAlreadyCode = Object.values(labelCodeMap).includes(stringValue);
|
||||
if (!isAlreadyCode) {
|
||||
logger.warn(`카테고리 값 매핑 없음: ${columnName} = "${stringValue}" (라벨도 코드도 아님)`);
|
||||
}
|
||||
// 변환 없이 원래 값 유지
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`카테고리 라벨→코드 변환 완료`, {
|
||||
tableName,
|
||||
conversionCount: conversions.length,
|
||||
conversions,
|
||||
});
|
||||
|
||||
return { convertedData, conversions };
|
||||
} catch (error: any) {
|
||||
logger.error(`카테고리 라벨→코드 변환 실패: ${error.message}`, { error });
|
||||
// 실패 시 원본 데이터 반환
|
||||
return { convertedData: data, conversions: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new TableCategoryValueService();
|
||||
|
|
|
|||
|
|
@ -307,10 +307,23 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
|||
return mappedRow;
|
||||
});
|
||||
|
||||
// 빈 행 필터링: 모든 값이 비어있거나 undefined/null인 행 제외
|
||||
const filteredData = mappedData.filter((row) => {
|
||||
const values = Object.values(row);
|
||||
// 하나라도 유효한 값이 있는지 확인
|
||||
return values.some((value) => {
|
||||
if (value === undefined || value === null) return false;
|
||||
if (typeof value === "string" && value.trim() === "") return false;
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`📊 엑셀 업로드: 전체 ${mappedData.length}행 중 유효한 ${filteredData.length}행`);
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (const row of mappedData) {
|
||||
for (const row of filteredData) {
|
||||
try {
|
||||
if (uploadMode === "insert") {
|
||||
const formData = { screenId: 0, tableName, data: row };
|
||||
|
|
|
|||
Loading…
Reference in New Issue