From ba1a37d823e6ea5b862b62e3f2c7da1632c65457 Mon Sep 17 00:00:00 2001 From: hjjeong Date: Mon, 27 Apr 2026 18:19:46 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B5=AC=EB=A7=A4=EB=A6=AC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=97=91=EC=85=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20-=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=BB=AC=EB=9F=BC=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=95=20=EB=B0=8F=20=EC=88=AB=EC=9E=90=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - USE_YN(사용여부) 정적 코드('사용'/'미사용') 검증 추가 (mutator가 잘못된 값을 기본 '사용'으로 변환해 묻혔던 케이스 차단) - 소재단가/발주수량/가공단가 숫자 형식 검증 추가 (천단위 콤마 허용) - fn_reverseSelectValue 강건화: 코드 컬럼 화이트리스트 기반으로 list 비어 있어도 isCodeColumn=true 처리, trim 비교 적용 - 코드 컬럼은 editable 체크 없이 무조건 검증 (가공업체 누락 방지) - 알람을 html 모드로 변경: 행번호 정렬, 줄바꿈, 좌측정렬·모노스페이스 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../view/salesMng/purchaseListFormPopUp.jsp | 92 ++++++++++++++----- 1 file changed, 68 insertions(+), 24 deletions(-) diff --git a/WebContent/WEB-INF/view/salesMng/purchaseListFormPopUp.jsp b/WebContent/WEB-INF/view/salesMng/purchaseListFormPopUp.jsp index ff709ef..bbcd5bf 100644 --- a/WebContent/WEB-INF/view/salesMng/purchaseListFormPopUp.jsp +++ b/WebContent/WEB-INF/view/salesMng/purchaseListFormPopUp.jsp @@ -1431,21 +1431,26 @@ function fn_buildHeaderFieldMap() { } // Select2 코드 컬럼: 엑셀의 코드명(text) → 코드값(id) 역매핑 -// Select2 코드 컬럼: 엑셀의 코드명(text) → 코드값(id) 역매핑 -// 반환: { value: 변환값(매칭 시 id, 미매칭/비코드 시 원본), isCodeColumn: 코드 컬럼 여부, matched: 기준정보 매칭 여부 } -// isCodeColumn=true && matched=false 이면 기준정보에 없는 값 (호출부에서 invalid 처리) +// Select2/정적 코드 컬럼: 엑셀의 코드명(text) → 코드값(id) 역매핑 +// 반환: { value, isCodeColumn, matched } +// isCodeColumn=true && matched=false 이면 기준정보에 없는 값 (호출부에서 invalid 처리) +// 화이트리스트에 있는 필드는 list가 비어 있어도 isCodeColumn=true로 간주(검증 누락 방지) function fn_reverseSelectValue(field, value) { if(value === null || value === undefined || value === '') { return { value: value, isCodeColumn: false, matched: true }; } var list = null; - if(field === 'VENDOR_PM' || field === 'PROCESSING_VENDOR') list = processingVendorList; - else if(field === 'CURRENCY') list = currencyList; - if(!list || list.length === 0) { + var isCode = false; + if(field === 'VENDOR_PM' || field === 'PROCESSING_VENDOR') { list = processingVendorList; isCode = true; } + else if(field === 'CURRENCY') { list = currencyList; isCode = true; } + else if(field === 'USE_YN') { list = [{id:'사용', text:'사용'}, {id:'미사용', text:'미사용'}]; isCode = true; } + if(!isCode) { return { value: value, isCodeColumn: false, matched: true }; } + if(!list) list = []; + var v = String(value).trim(); for(var i = 0; i < list.length; i++) { - if(list[i].text === value || String(list[i].id) === String(value)) { + if(String(list[i].text).trim() === v || String(list[i].id).trim() === v) { return { value: list[i].id, isCodeColumn: true, matched: true }; } } @@ -1457,9 +1462,16 @@ function fn_getFieldLabel(field) { if(field === 'VENDOR_PM') return '공급업체'; if(field === 'PROCESSING_VENDOR') return '가공업체'; if(field === 'CURRENCY') return '환종'; + if(field === 'USE_YN') return '사용여부'; + if(field === 'UNIT_PRICE') return '소재단가'; + if(field === 'PO_QTY') return '발주수량'; + if(field === 'PROCESSING_UNIT_PRICE') return '가공단가'; return field; } +// 숫자만 허용 컬럼 (엑셀 업로드 시 형식 검증). 천단위 콤마는 허용. +var NUMERIC_FIELDS = { UNIT_PRICE: true, PO_QTY: true, PROCESSING_UNIT_PRICE: true }; + // 저장 버튼 표시/숨김 (엑셀 업로드에서 기준정보 미매칭 발생 시 저장 차단) function fn_toggleSaveBtn(show) { var $btn = $('#btnSave'); @@ -1468,6 +1480,12 @@ function fn_toggleSaveBtn(show) { else $btn.hide(); } +// SweetAlert html 모드에서 사용자 입력값이 HTML로 해석되지 않도록 이스케이프 +function fn_escapeHtml(s) { + if(s === null || s === undefined) return ''; + return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); +} + // 엑셀 업로드: 파싱 → 역매핑 → OBJID 무결성 검증 → PART_NO 우선 매칭(중복 시 OBJID) → 그리드 주입 function fn_excelUpload(file) { if(!file) return; @@ -1507,9 +1525,9 @@ function fn_excelUpload(file) { if(oid) existingObjidSet[oid] = true; } - // 1) 헤더 → field 변환 + Select2 역매핑 + 기준정보 미매칭 수집 (편집 가능 컬럼만) + // 1) 헤더 → field 변환 + Select2 역매핑 + 기준정보 미매칭/숫자 형식 오류 수집 (편집 가능 컬럼만) var converted = []; - var invalidSelects = []; + var invalidCells = []; // { row, field, label, value, reason } for(var i = 0; i < rows.length; i++) { var src = rows[i]; var out = {}; @@ -1517,14 +1535,34 @@ function fn_excelUpload(file) { if(!src.hasOwnProperty(header)) continue; var field = headerFieldMap[header]; if(!field) continue; - var r = fn_reverseSelectValue(field, src[header]); + var rawVal = src[header]; + // 숫자 컬럼: 빈 값 통과, 변환 실패 시 invalid + if(NUMERIC_FIELDS[field] && editableFields[field]) { + if(rawVal !== null && rawVal !== undefined && String(rawVal).trim() !== '') { + var numVal = Number(String(rawVal).replace(/,/g, '')); + if(isNaN(numVal)) { + invalidCells.push({ + row: i + 2, field: field, label: fn_getFieldLabel(field), + value: rawVal, reason: '숫자만 입력 가능합니다' + }); + out[field] = rawVal; + } else { + out[field] = numVal; + } + } else { + out[field] = rawVal; + } + continue; + } + // 코드 컬럼: 역매핑 + 기준정보 매칭 검증 + // (editable 체크 없이 검증 — 코드 컬럼은 모두 사용자 입력 대상이므로 + // editableFields 판정 누락 시에도 안전하게 잡기 위함) + var r = fn_reverseSelectValue(field, rawVal); out[field] = r.value; - if(r.isCodeColumn && !r.matched && editableFields[field]) { - invalidSelects.push({ - row: i + 2, - field: field, - label: fn_getFieldLabel(field), - value: src[header] + if(r.isCodeColumn && !r.matched) { + invalidCells.push({ + row: i + 2, field: field, label: fn_getFieldLabel(field), + value: rawVal, reason: '기준정보에 없습니다' }); } } @@ -1629,17 +1667,23 @@ function fn_excelUpload(file) { return; } - // 5) 기준정보 미매칭 분기: 데이터는 그리드에 반영하되 저장 버튼 숨김 - if(invalidSelects.length > 0) { + // 5) 잘못된 셀(기준정보 미매칭/숫자 형식 오류) 분기: 데이터는 그리드에 반영하되 저장 버튼 숨김 + if(invalidCells.length > 0) { _tabulGrid.replaceData(merged).then(function() { fn_toggleSaveBtn(false); - var msg = '기준정보에 없는 값이 포함되어 저장이 차단됩니다.\n정상 엑셀로 다시 업로드하세요.\n\n'; - for(var i = 0; i < Math.min(invalidSelects.length, 10); i++) { - var it = invalidSelects[i]; - msg += ' - ' + it.row + '행: ' + it.label + " '" + it.value + "' 기준정보에 없습니다\n"; + invalidCells.sort(function(a, b) { return (a.row || 0) - (b.row || 0); }); + var lines = ''; + for(var i = 0; i < Math.min(invalidCells.length, 20); i++) { + var it = invalidCells[i]; + lines += ' - ' + it.row + '행: ' + fn_escapeHtml(it.label) + + " '" + fn_escapeHtml(it.value) + "' " + + fn_escapeHtml(it.reason) + '
'; } - if(invalidSelects.length > 10) msg += ' ... (총 ' + invalidSelects.length + '건)'; - Swal.fire('오류', msg, 'error'); + if(invalidCells.length > 20) lines += ' ... (총 ' + invalidCells.length + '건)
'; + var html = '잘못된 값이 포함되어 저장이 차단됩니다.
' + + '정상 엑셀로 다시 업로드하세요.

' + + '
' + lines + '
'; + Swal.fire({ title: '오류', html: html, icon: 'error', width: 700 }); }); return; }