개발관리>BOM CSV Import — RFC4180 파서 적용 + 누락 시퀀스 5종 생성
CSV 파싱 함정 정정:
사용자 검증 중 운영판 wace 에서 3건만 파싱되는 CSV 가 RPS 에서 4건으로 파싱되며 4번째
행이 깨진 채 들어가는 문제 발견. 단순 line.split(",") 로는 RFC4180 따옴표 처리 실패.
backend-node:
- csv-parse ^6.2.1 추가
- devBomExcelImportService.parseAndValidate :
text.split(/\r\n|\r|\n/).map(line => line.split(",")) 단순 split →
parseCsvSync(text, { relax_quotes, relax_column_count, skip_empty_lines: false })
· 따옴표 내부 콤마/줄바꿈, "" 이스케이프 모두 안전 처리
· 운영판 사용자가 만든 비정형 quote 도 relax_quotes 로 관대 처리
· 1차 스캔(자품번 수집) 도 동일 allRows 재사용
- getCsvValue 헬퍼는 보존 (csv-parse 후에도 안전 trim/quote-strip 으로 유지)
시퀀스 누락 (별 함정):
저장 시 "relation seq_bom_qty does not exist" 에러 발생. wace 매퍼에서 사용하는
nextval('seq_*') 시퀀스 5종 중 RPS DB 에 seq_ecr_no 만 존재. 나머지 4종 신규 생성.
02_sequences.sql (data-sync 디렉토리에 보존):
- seq_bom_qty 200,000 (운영 179,258 + 여유)
- seq_as_no 1,000 (운영 109 + 여유)
- seq_comm_code 10,000 (운영 1,839 + 여유)
- seq_eo_no 1,000 (운영 62 + 여유)
- seq_ecr_no (RPS 기존 보존, 운영 33)
운영 last_value 보다 충분히 큰 값으로 setval — 향후 운영 데이터 sync 시 PK 충돌 방지.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+7
@@ -16,6 +16,7 @@
|
||||
"cheerio": "^1.2.0",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"csv-parse": "^6.2.1",
|
||||
"docx": "^9.5.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
@@ -5314,6 +5315,12 @@
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/csv-parse": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-6.2.1.tgz",
|
||||
"integrity": "sha512-LRLMV+UCyfMokp8Wb411duBf1gaBKJfOfBWU9eHMJ+b+cJYZsNu3AFmjJf3+yPGd59Exz1TsMjaSFyxnYB9+IQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"cheerio": "^1.2.0",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"csv-parse": "^6.2.1",
|
||||
"docx": "^9.5.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
// ============================================================
|
||||
|
||||
import * as iconv from "iconv-lite";
|
||||
import { parse as parseCsvSync } from "csv-parse/sync";
|
||||
import { PoolClient } from "pg";
|
||||
import { getPool, transaction } from "../database/db";
|
||||
import { logger } from "../utils/logger";
|
||||
@@ -187,26 +188,31 @@ export async function parseAndValidate(buffer: Buffer): Promise<{
|
||||
encoding: string;
|
||||
}> {
|
||||
const { text, encoding } = detectAndDecode(buffer);
|
||||
const lines = text.split(/\r\n|\r|\n/);
|
||||
|
||||
// RFC4180 호환 파서 — 따옴표 내부 콤마/줄바꿈, "" 이스케이프 모두 안전 처리
|
||||
// (이전: line.split(",") 단순 split → 따옴표 안 콤마/멀티라인 셀 깨짐)
|
||||
// relax_quotes / relax_column_count : 운영판 wace 사용자가 만든 CSV 의 비정형 quote 도 관대 처리
|
||||
const allRows: string[][] = parseCsvSync(text, {
|
||||
columns: false,
|
||||
skip_empty_lines: false,
|
||||
relax_quotes: true,
|
||||
relax_column_count: true,
|
||||
trim: false,
|
||||
});
|
||||
|
||||
// 1차 스캔: 모든 자품번 + 수준→품번 매핑 (wace 1:1)
|
||||
const allPartNumbers = new Set<string>();
|
||||
const allRows: string[][] = [];
|
||||
const levelToPartNoMap = new Map<string, string>();
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i] === undefined) continue;
|
||||
const values = lines[i].split(",");
|
||||
allRows.push(values);
|
||||
if (i > 0 && values.length > 1) {
|
||||
let level = (values[0] ?? "").trim();
|
||||
let partNo = (values[1] ?? "").trim();
|
||||
if (level.length > 1 && level.startsWith('"') && level.endsWith('"')) level = level.substring(1, level.length - 1);
|
||||
if (partNo.length > 1 && partNo.startsWith('"') && partNo.endsWith('"')) partNo = partNo.substring(1, partNo.length - 1);
|
||||
if (partNo) {
|
||||
allPartNumbers.add(partNo);
|
||||
if (level) levelToPartNoMap.set(level, partNo);
|
||||
}
|
||||
for (let i = 0; i < allRows.length; i++) {
|
||||
const values = allRows[i];
|
||||
if (!values || values.length < 2) continue;
|
||||
if (i === 0) continue; // 헤더
|
||||
const level = (values[0] ?? "").trim();
|
||||
const partNo = (values[1] ?? "").trim();
|
||||
if (partNo) {
|
||||
allPartNumbers.add(partNo);
|
||||
if (level) levelToPartNoMap.set(level, partNo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user