엑셀 업로드 카테고리 타입 자동 감지
This commit is contained in:
parent
c181385f11
commit
d90a403ed9
|
|
@ -1,6 +1,7 @@
|
||||||
import { query, queryOne, transaction, getPool } from "../database/db";
|
import { query, queryOne, transaction, getPool } from "../database/db";
|
||||||
import { EventTriggerService } from "./eventTriggerService";
|
import { EventTriggerService } from "./eventTriggerService";
|
||||||
import { DataflowControlService } from "./dataflowControlService";
|
import { DataflowControlService } from "./dataflowControlService";
|
||||||
|
import tableCategoryValueService from "./tableCategoryValueService";
|
||||||
|
|
||||||
export interface FormDataResult {
|
export interface FormDataResult {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -427,6 +428,24 @@ export class DynamicFormService {
|
||||||
dataToInsert,
|
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("🔍 테이블 컬럼 정보 조회 중...");
|
console.log("🔍 테이블 컬럼 정보 조회 중...");
|
||||||
const columnInfo = await this.getTableColumnInfo(tableName);
|
const columnInfo = await this.getTableColumnInfo(tableName);
|
||||||
|
|
|
||||||
|
|
@ -1398,6 +1398,220 @@ class TableCategoryValueService {
|
||||||
throw error;
|
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();
|
export default new TableCategoryValueService();
|
||||||
|
|
|
||||||
|
|
@ -307,10 +307,23 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
return mappedRow;
|
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 successCount = 0;
|
||||||
let failCount = 0;
|
let failCount = 0;
|
||||||
|
|
||||||
for (const row of mappedData) {
|
for (const row of filteredData) {
|
||||||
try {
|
try {
|
||||||
if (uploadMode === "insert") {
|
if (uploadMode === "insert") {
|
||||||
const formData = { screenId: 0, tableName, data: row };
|
const formData = { screenId: 0, tableName, data: row };
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue