From c60893d81c0e9f2be6a0970c09a70a5b183942e9 Mon Sep 17 00:00:00 2001 From: z3rotig4r Date: Thu, 30 Jul 2026 08:27:45 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=EC=82=AC=EC=97=85=EC=9E=90=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=EB=B2=88=ED=98=B8=20=EA=B2=80=EC=A6=9D=20=EA=B0=80?= =?UTF-8?q?=EC=A4=91=EC=B9=98=EB=A5=BC=20=EC=95=9E=209=EC=9E=90=EB=A6=AC?= =?UTF-8?q?=EC=97=90=EB=A7=8C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EgovCrnCheckValidation의 가중치 배열이 10개({1,3,7,1,3,7,1,3,5,1})여서 루프가 검증 대상인 10번째 체크디지트까지 가중합에 포함시켰다. 국세청 사업자등록번호 알고리즘은 앞 9자리에만 가중치를 적용하므로 배열을 9개로 줄인다. 수정 전에는 실제 유효 번호가 거부됐다(124-81-00998, 220-81-62517, 101-81-16293 모두 false). 무작위 10,000건을 국세청 규격과 대조하면 수정 전 8,218건(82.2%) 일치 -> 수정 후 10,000건(100%) 일치. --- .../rte/ptl/reactive/validation/EgovCrnCheckValidation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovCrnCheckValidation.java b/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovCrnCheckValidation.java index 90cdfd62..18f6773a 100755 --- a/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovCrnCheckValidation.java +++ b/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovCrnCheckValidation.java @@ -54,7 +54,7 @@ public boolean isValid(String value, ConstraintValidatorContext context) { } int sum = 0; - int[] weightArray = {1, 3, 7, 1, 3, 7, 1, 3, 5, 1}; + int[] weightArray = {1, 3, 7, 1, 3, 7, 1, 3, 5}; for (int i = 0; i < weightArray.length; i++) { sum += weightArray[i] * Integer.parseInt(mValue.substring(i, i + 1)); } From 6883ada438ba3ae27af731a043f209f22f3ecd7f Mon Sep 17 00:00:00 2001 From: z3rotig4r Date: Thu, 30 Jul 2026 08:27:45 +0900 Subject: [PATCH 2/3] =?UTF-8?q?test:=20reactive=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=EA=B8=B0=208=EC=A2=85=20=EC=8B=9C=EB=A7=A8=ED=8B=B1=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 ReactiveValidatorsNullSafeTest는 null 가드만 검증해 유효/무효 값 판정에 대한 회귀 그물망이 없었다. 사업자등록번호·법인등록번호·주민등록번호·휴대전화·일반전화·영문·한글· 비밀번호 검증기의 판정을 실제 값으로 검증한다. 사업자등록번호는 체크디지트 수정에 대한 회귀 테스트를 포함한다. 주민등록번호는 실제 개인 식별번호를 쓰지 않고, 존재할 수 없는 날짜에 체크섬만 규격에 맞춘 합성값을 사용한다. --- .../ReactiveValidatorsSemanticTest.java | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 Presentation/org.egovframe.rte.ptl.reactive/src/test/java/org/egovframe/rte/ptl/reactive/validation/ReactiveValidatorsSemanticTest.java diff --git a/Presentation/org.egovframe.rte.ptl.reactive/src/test/java/org/egovframe/rte/ptl/reactive/validation/ReactiveValidatorsSemanticTest.java b/Presentation/org.egovframe.rte.ptl.reactive/src/test/java/org/egovframe/rte/ptl/reactive/validation/ReactiveValidatorsSemanticTest.java new file mode 100644 index 00000000..aaf9275e --- /dev/null +++ b/Presentation/org.egovframe.rte.ptl.reactive/src/test/java/org/egovframe/rte/ptl/reactive/validation/ReactiveValidatorsSemanticTest.java @@ -0,0 +1,118 @@ +package org.egovframe.rte.ptl.reactive.validation; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ReactiveValidatorsSemanticTest { + + @Test + void crnCheck_validatesBusinessRegistrationNumberChecksum() { + EgovCrnCheckValidation validator = new EgovCrnCheckValidation(); + + // 가중치는 앞 9자리에만 적용된다(국세청 규격). + assertTrue(validator.isValid("124-81-00998", null), "사업자등록번호는 유효한 체크디지트를 true로 판정해야 한다"); + assertTrue(validator.isValid("220-81-62517", null), "사업자등록번호는 유효한 체크디지트를 true로 판정해야 한다"); + assertTrue(validator.isValid("101-81-16293", null), "사업자등록번호는 유효한 체크디지트를 true로 판정해야 한다"); + assertTrue(validator.isValid("1248100998", null), "사업자등록번호는 하이픈 없는 유효 번호를 true로 판정해야 한다"); + + assertFalse(validator.isValid("124-81-00990", null), "사업자등록번호는 체크디지트 오류를 false로 판정해야 한다"); + assertFalse(validator.isValid("220-81-62510", null), "사업자등록번호는 체크디지트 오류를 false로 판정해야 한다"); + assertFalse(validator.isValid("124-81-0099", null), "사업자등록번호는 9자리 입력을 false로 판정해야 한다"); + assertFalse(validator.isValid("124-81-009989", null), "사업자등록번호는 11자리 입력을 false로 판정해야 한다"); + assertFalse(validator.isValid("12481abcde", null), "사업자등록번호는 숫자가 아닌 입력을 false로 판정해야 한다"); + assertFalse(validator.isValid("", null), "사업자등록번호는 빈 문자열을 false로 판정해야 한다"); + } + + @Test + void cnCheck_validatesCorporateRegistrationNumberChecksum() { + EgovCnCheckValidation validator = new EgovCnCheckValidation(); + + assertTrue(validator.isValid("110111-0000002", null), "법인등록번호는 유효한 체크디지트를 true로 판정해야 한다"); + assertTrue(validator.isValid("134211-0123458", null), "법인등록번호는 유효한 체크디지트를 true로 판정해야 한다"); + + assertFalse(validator.isValid("110111-0000000", null), "법인등록번호는 체크디지트 오류를 false로 판정해야 한다"); + assertFalse(validator.isValid("134211-0123450", null), "법인등록번호는 체크디지트 오류를 false로 판정해야 한다"); + assertFalse(validator.isValid("110111-000000", null), "법인등록번호는 12자리 입력을 false로 판정해야 한다"); + } + + @Test + void rrnCheck_validatesResidentRegistrationNumberChecksum() { + EgovRrnCheckValidation validator = new EgovRrnCheckValidation(); + + // 아래 값은 실제 개인 식별번호가 아니라 존재할 수 없는 날짜로 만든 체크섬 규격 합성값이다. + assertTrue(validator.isValid("991332-1123459", null), "주민등록번호는 합성 유효 체크섬 값을 true로 판정해야 한다"); + assertTrue(validator.isValid("001332-2123452", null), "주민등록번호는 합성 유효 체크섬 값을 true로 판정해야 한다"); + + assertFalse(validator.isValid("991332-1123450", null), "주민등록번호는 체크디지트 오류를 false로 판정해야 한다"); + assertFalse(validator.isValid("991332-5123459", null), "주민등록번호는 성별코드 패턴 위반을 false로 판정해야 한다"); + assertFalse(validator.isValid("991332-112345", null), "주민등록번호는 12자리 입력을 false로 판정해야 한다"); + } + + @Test + void mobilePhoneCheck_validatesMobilePhonePattern() { + EgovMobilePhoneCheckValidation validator = new EgovMobilePhoneCheckValidation(); + + // 검증기가 하이픈을 먼저 제거하므로 하이픈 위치가 아닌 국번·자리수만 판정된다. + assertTrue(validator.isValid("010-1234-5678", null), "휴대전화번호는 010 국번의 유효 번호를 true로 판정해야 한다"); + assertTrue(validator.isValid("011-123-4567", null), "휴대전화번호는 011 국번의 유효 번호를 true로 판정해야 한다"); + assertTrue(validator.isValid("016-1234-5678", null), "휴대전화번호는 016 국번의 유효 번호를 true로 판정해야 한다"); + + assertFalse(validator.isValid("012-1234-5678", null), "휴대전화번호는 국번 규칙 위반을 false로 판정해야 한다"); + assertFalse(validator.isValid("02-1234-5678", null), "휴대전화번호는 일반전화 형식을 false로 판정해야 한다"); + assertFalse(validator.isValid("010-1234-56789", null), "휴대전화번호는 12자리 입력을 false로 판정해야 한다"); + assertFalse(validator.isValid("010-123-456", null), "휴대전화번호는 9자리 입력을 false로 판정해야 한다"); + } + + @Test + void phoneCheck_validatesPhonePattern() { + EgovPhoneCheckValidation validator = new EgovPhoneCheckValidation(); + + assertTrue(validator.isValid("02-1234-5678", null), "일반전화번호는 02 지역번호의 유효 번호를 true로 판정해야 한다"); + assertTrue(validator.isValid("031-123-4567", null), "일반전화번호는 031 지역번호의 유효 번호를 true로 판정해야 한다"); + + assertFalse(validator.isValid("1234", null), "일반전화번호는 너무 짧은 입력을 false로 판정해야 한다"); + assertFalse(validator.isValid("02-1234-56789012", null), "일반전화번호는 너무 긴 입력을 false로 판정해야 한다"); + } + + @Test + void englishCheck_validatesEnglishOnlyPattern() { + EgovEnglishCheckValidation validator = new EgovEnglishCheckValidation(); + + assertTrue(validator.isValid("abc", null), "영문 검증기는 소문자 영문만 있는 입력을 true로 판정해야 한다"); + assertTrue(validator.isValid("ABCdef", null), "영문 검증기는 대소문자 영문 입력을 true로 판정해야 한다"); + // 빈 문자열 허용 계약은 유지하며, 필수값 여부는 @EgovNullCheck가 담당한다. + assertTrue(validator.isValid("", null), "영문 검증기는 빈 문자열을 true로 판정해야 한다"); + + assertFalse(validator.isValid("한글", null), "영문 검증기는 한글 입력을 false로 판정해야 한다"); + assertFalse(validator.isValid("abc123", null), "영문 검증기는 숫자가 포함된 입력을 false로 판정해야 한다"); + assertFalse(validator.isValid("abc def", null), "영문 검증기는 공백이 포함된 입력을 false로 판정해야 한다"); + } + + @Test + void koreanCheck_validatesKoreanOnlyPattern() { + EgovKoreanCheckValidation validator = new EgovKoreanCheckValidation(); + + assertTrue(validator.isValid("한글", null), "한글 검증기는 완성형 한글 입력을 true로 판정해야 한다"); + assertTrue(validator.isValid("ㄱㄴㄷ", null), "한글 검증기는 한글 자음 입력을 true로 판정해야 한다"); + + assertFalse(validator.isValid("abc", null), "한글 검증기는 영문 입력을 false로 판정해야 한다"); + assertFalse(validator.isValid("한글abc", null), "한글 검증기는 한글과 영문이 섞인 입력을 false로 판정해야 한다"); + assertFalse(validator.isValid("한 글", null), "한글 검증기는 공백이 포함된 입력을 false로 판정해야 한다"); + } + + @Test + void pwdCheck_validatesPasswordRules() { + EgovPwdCheckValidation validator = new EgovPwdCheckValidation(); + + assertTrue(validator.isValid("Egov!2xk", null), "비밀번호는 길이와 문자 조합 규칙을 만족하면 true로 판정해야 한다"); + assertTrue(validator.isValid("Pa$5wOrd9", null), "비밀번호는 길이와 문자 조합 규칙을 만족하면 true로 판정해야 한다"); + + assertFalse(validator.isValid("Egov!2x", null), "비밀번호는 8자 미만 입력을 false로 판정해야 한다"); + assertFalse(validator.isValid("Egovxxkq", null), "비밀번호는 숫자와 특수문자가 없으면 false로 판정해야 한다"); + assertFalse(validator.isValid("Egov!2aaa", null), "비밀번호는 동일문자 3회 반복을 false로 판정해야 한다"); + assertFalse(validator.isValid("Egov!2abc", null), "비밀번호는 오름차순 연속 3자를 false로 판정해야 한다"); + assertFalse(validator.isValid("Egov!2123", null), "비밀번호는 연속 숫자 3자를 false로 판정해야 한다"); + } +} From 21b59e82ebe82b8637e83d50010be48140665efe Mon Sep 17 00:00:00 2001 From: z3rotig4r Date: Thu, 30 Jul 2026 08:51:02 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=EC=82=AC=EC=97=85=EC=9E=90=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=EB=B2=88=ED=98=B8=20=EA=B2=80=EC=A6=9D=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EA=B0=9C=ED=96=89=EC=9D=B4=20=EB=B6=99=EC=9D=80=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=EC=9D=98=20=EC=98=88=EC=99=B8=20=EC=A0=84?= =?UTF-8?q?=ED=8C=8C=20=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 패턴 검사에 matcher.find()를 사용하면 정규식의 $가 입력 끝뿐 아니라 마지막 개행 앞에서도 매치되므로 "1248100998\n" 같은 값이 10자리 검사를 통과한다. 이후 Integer.parseInt(mValue.substring(9))가 "8\n"을 파싱해 NumberFormatException을 던지고, ConstraintValidator에서 예외가 나가면 false 판정이 아닌 ValidationException으로 전파되어 400 대신 500 응답이 된다. matcher.matches()로 바꿔 입력 전체가 패턴과 일치할 때만 통과시킨다. 정상 입력 판정은 바뀌지 않는다(무작위 10,000건 국세청 규격 대조 100% 일치 유지). 개행·CRLF 입력이 예외 없이 false가 되는 단언을 테스트에 추가하고, 주민등록번호 합성 픽스처가 존재할 수 없는 날짜인 이유 (이 검증기는 패턴과 체크섬만 검증하고 날짜 유효성은 검증하지 않는다)를 주석과 단언 메시지에 명시했다. --- .../ptl/reactive/validation/EgovCrnCheckValidation.java | 2 +- .../validation/ReactiveValidatorsSemanticTest.java | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovCrnCheckValidation.java b/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovCrnCheckValidation.java index 18f6773a..f4e87227 100755 --- a/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovCrnCheckValidation.java +++ b/Presentation/org.egovframe.rte.ptl.reactive/src/main/java/org/egovframe/rte/ptl/reactive/validation/EgovCrnCheckValidation.java @@ -48,7 +48,7 @@ public boolean isValid(String value, ConstraintValidatorContext context) { } String mValue = value.replaceAll("-", ""); Matcher matcher = CRN_PATTERN.matcher(mValue); - boolean check = matcher.find(); + boolean check = matcher.matches(); if (!check) { return false; } diff --git a/Presentation/org.egovframe.rte.ptl.reactive/src/test/java/org/egovframe/rte/ptl/reactive/validation/ReactiveValidatorsSemanticTest.java b/Presentation/org.egovframe.rte.ptl.reactive/src/test/java/org/egovframe/rte/ptl/reactive/validation/ReactiveValidatorsSemanticTest.java index aaf9275e..b35aed88 100644 --- a/Presentation/org.egovframe.rte.ptl.reactive/src/test/java/org/egovframe/rte/ptl/reactive/validation/ReactiveValidatorsSemanticTest.java +++ b/Presentation/org.egovframe.rte.ptl.reactive/src/test/java/org/egovframe/rte/ptl/reactive/validation/ReactiveValidatorsSemanticTest.java @@ -23,6 +23,8 @@ void crnCheck_validatesBusinessRegistrationNumberChecksum() { assertFalse(validator.isValid("124-81-009989", null), "사업자등록번호는 11자리 입력을 false로 판정해야 한다"); assertFalse(validator.isValid("12481abcde", null), "사업자등록번호는 숫자가 아닌 입력을 false로 판정해야 한다"); assertFalse(validator.isValid("", null), "사업자등록번호는 빈 문자열을 false로 판정해야 한다"); + assertFalse(validator.isValid("1248100998\n", null), "사업자등록번호는 뒤에 개행이 붙은 입력을 예외 없이 false로 판정해야 한다"); + assertFalse(validator.isValid("1248100998\r\n", null), "사업자등록번호는 뒤에 CRLF가 붙은 입력을 예외 없이 false로 판정해야 한다"); } @Test @@ -42,8 +44,9 @@ void rrnCheck_validatesResidentRegistrationNumberChecksum() { EgovRrnCheckValidation validator = new EgovRrnCheckValidation(); // 아래 값은 실제 개인 식별번호가 아니라 존재할 수 없는 날짜로 만든 체크섬 규격 합성값이다. - assertTrue(validator.isValid("991332-1123459", null), "주민등록번호는 합성 유효 체크섬 값을 true로 판정해야 한다"); - assertTrue(validator.isValid("001332-2123452", null), "주민등록번호는 합성 유효 체크섬 값을 true로 판정해야 한다"); + // 이 검증기는 패턴과 체크섬만 검증하며 날짜 유효성은 검증하지 않는다. + assertTrue(validator.isValid("991332-1123459", null), "주민등록번호 검증기는 날짜를 검증하지 않고 체크섬만 검증하므로 존재할 수 없는 날짜라도 true로 판정한다"); + assertTrue(validator.isValid("001332-2123452", null), "주민등록번호 검증기는 날짜를 검증하지 않고 체크섬만 검증하므로 존재할 수 없는 날짜라도 true로 판정한다"); assertFalse(validator.isValid("991332-1123450", null), "주민등록번호는 체크디지트 오류를 false로 판정해야 한다"); assertFalse(validator.isValid("991332-5123459", null), "주민등록번호는 성별코드 패턴 위반을 false로 판정해야 한다");