diff --git a/backend-node/package-lock.json b/backend-node/package-lock.json index 12507fbc..04f4f11c 100644 --- a/backend-node/package-lock.json +++ b/backend-node/package-lock.json @@ -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", diff --git a/backend-node/package.json b/backend-node/package.json index 23360432..527c068f 100644 --- a/backend-node/package.json +++ b/backend-node/package.json @@ -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", diff --git a/backend-node/src/services/devBomExcelImportService.ts b/backend-node/src/services/devBomExcelImportService.ts index b118aced..7500d497 100644 --- a/backend-node/src/services/devBomExcelImportService.ts +++ b/backend-node/src/services/devBomExcelImportService.ts @@ -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(); - const allRows: string[][] = []; const levelToPartNoMap = new Map(); - 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); } } diff --git a/docs/migration/development/data-sync/02_sequences.sql b/docs/migration/development/data-sync/02_sequences.sql new file mode 100644 index 00000000..431d0f8c --- /dev/null +++ b/docs/migration/development/data-sync/02_sequences.sql @@ -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;