개발관리>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:
hjjeong
2026-05-12 18:51:19 +09:00
parent d74263eaed
commit 9ff61cf2f9
4 changed files with 66 additions and 15 deletions
+7
View File
@@ -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",
+1
View File
@@ -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);
}
}
@@ -0,0 +1,37 @@
-- wace 운영판 매퍼에서 사용하는 시퀀스 5종을 RPS DB 에 생성.
-- 운영DB last_value 보다 충분히 큰 값으로 setval — 향후 운영 데이터 sync 시 PK 충돌 방지.
--
-- 운영DB current last_value (2026-05-12 기준):
-- seq_bom_qty 179,258 → RPS 200,000
-- seq_as_no 109 → RPS 1,000
-- seq_comm_code 1,839 → RPS 10,000
-- seq_eo_no 62 → RPS 1,000
-- seq_ecr_no 33 → RPS 이미 존재 (보존)
--
-- 매퍼 사용처:
-- seq_bom_qty — partMng.relatePartInfo (BOM_PART_QTY.SEQ)
-- seq_as_no — 영업관리 (AS 번호 채번)
-- seq_comm_code — comm_code 신규 등록
-- seq_ecr_no — 설계변경 ECR 번호
-- seq_eo_no — wace 일부 매퍼 (현재 partMng deploy 는 EO_NO 직접 SUBSTR 채번)
BEGIN;
CREATE SEQUENCE IF NOT EXISTS seq_bom_qty AS bigint MINVALUE 1 NO CYCLE;
CREATE SEQUENCE IF NOT EXISTS seq_as_no AS bigint MINVALUE 1 NO CYCLE;
CREATE SEQUENCE IF NOT EXISTS seq_comm_code AS bigint MINVALUE 1 NO CYCLE;
CREATE SEQUENCE IF NOT EXISTS seq_eo_no AS bigint MINVALUE 1 NO CYCLE;
SELECT setval('seq_bom_qty', 200000, true);
SELECT setval('seq_as_no', 1000, true);
SELECT setval('seq_comm_code', 10000, true);
SELECT setval('seq_eo_no', 1000, true);
COMMIT;
-- 검증
SELECT sequence_name, last_value
FROM information_schema.sequences s
JOIN pg_sequences ps ON ps.sequencename = s.sequence_name
WHERE sequence_name IN ('seq_bom_qty','seq_as_no','seq_comm_code','seq_ecr_no','seq_eo_no')
ORDER BY 1;