7 Commits

Author SHA1 Message Date
curry772 2929fc753f BIZ_ERROR_DESC_VALUE 추가 2026-06-18 08:52:02 +09:00
curry772 52b968e8f8 STDMESSAGE_ERROR_CODE 프러퍼티 그룹 설정 변경 시 리로드 기능 수정 2026-06-17 14:15:13 +09:00
curry772 27e9cd7d67 거래별 로그(sift logger)에 http client 로거도 추가 2026-06-17 14:06:16 +09:00
curry772 658f2a0485 fix: SiftingAppender 파일 락 및 on/off 제어 개선
- PatternLayoutEncoder를 buildAppender() 내부에서 appender별 독립 생성 (공유 인스턴스
제거)
   - SiftingAppender timeout 5분으로 단축 (기본 30분)
   - siftAppenderMap으로 appender 직접 관리, closeSiftAppender()로 즉시 파일 핸들 해제
   - siftEnabled 플래그 추가 및 txIdFilter에서 체크 (on/off 제어)
   - doConfigure() 기동 시 DB log.level 값으로 siftEnabled 초기화
   - FileLoggerPropertyChangeListener에서 SIFT log.level 변경 시
setSiftEnabled() 연동
2026-06-05 14:03:57 +09:00
curry772 98cd6198b2 Logger 로그레벨 로그 추가 2026-06-04 11:06:04 +09:00
curry772 8f7513865e MaskingUtils elink-online-common -> elink-online-core로 이동 2026-05-20 17:14:24 +09:00
curry772 f612ca1c88 거래파일로그(SIFT_LOGGER) 출력기능 개발
- UUID별로 거래파일로그 생성
2026-05-19 11:19:13 +09:00
5 changed files with 1186 additions and 57 deletions
@@ -25,6 +25,11 @@ public class FileLoggerPropertyChangeListener implements PropertyChangeListener
}
Logger.setLoggerLevel(propGroupVO.getName());
}
if ( LogKeys.LOGGER_SIFT_PREFIX.equals(propGroupVO.getName()) ) {
String logLevel = prptManager.getProperty(LogKeys.LOGGER_SIFT_PREFIX, LogKeys.LOG_LEVEL);
Logger.setSiftEnabled( !"off".equalsIgnoreCase(logLevel) );
}
}
@Override
@@ -5,6 +5,7 @@ import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.StrSubstitutor;
@@ -29,6 +30,7 @@ import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy;
import ch.qos.logback.core.sift.AppenderFactory;
import ch.qos.logback.core.util.Duration;
import ch.qos.logback.core.util.FileSize;
import co.elastic.logging.logback.EcsEncoder;
@@ -56,6 +58,9 @@ public class Logger implements LogKeys {
public static String LOGGER_CLIENT;
public static String LOGGER_SIFT;
private static final ConcurrentHashMap<String, RollingFileAppender<ILoggingEvent>> siftAppenderMap = new ConcurrentHashMap<>();
private static volatile boolean siftEnabled = true;
org.slf4j.Logger loggerImpl;
// ch.qos.logback.classic.Logger loggerImpl;
@@ -796,17 +801,8 @@ public class Logger implements LogKeys {
// GW SIFT Logger
PatternLayoutEncoder sple = new PatternLayoutEncoder();
sple.setContext(lc);
String appenderLayout = pmanager.getProperty(APPENDER_SIFT, PATTERN_LAYOUT);
if (StringUtils.isBlank(appenderLayout))
sple.setPattern(defaultLayout);
else
sple.setPattern(appenderLayout);
String encoding = pmanager.getProperty(APPENDER_SIFT, ENCODING);
if (StringUtils.isNotBlank(encoding))
sple.setCharset(Charset.forName(encoding));
sple.start();
final String siftLayout = StringUtils.defaultIfBlank(pmanager.getProperty(APPENDER_SIFT, PATTERN_LAYOUT), defaultLayout);
final String siftEncoding = pmanager.getProperty(APPENDER_SIFT, ENCODING);
MDCBasedDiscriminator discriminator = new MDCBasedDiscriminator();
discriminator.setKey(DISCRIMINATOR);
@@ -817,6 +813,7 @@ public class Logger implements LogKeys {
siftAppender.setContext(lc);
siftAppender.setName(APPENDER_SIFT);
siftAppender.setDiscriminator(discriminator);
siftAppender.setTimeout(new Duration(60000*5)); // 1분으로 단축 (기본 30분 → 5분)
siftAppender.setAppenderFactory(new AppenderFactory<ILoggingEvent>() {
@Override
public FileAppender<ILoggingEvent> buildAppender(Context context, String discriminatingValue) {
@@ -842,6 +839,13 @@ public class Logger implements LogKeys {
maxBackupIndex = 60;
}
PatternLayoutEncoder sple = new PatternLayoutEncoder();
sple.setContext(context);
sple.setPattern(siftLayout);
if (StringUtils.isNotBlank(siftEncoding))
sple.setCharset(Charset.forName(siftEncoding));
sple.start();
RollingFileAppender<ILoggingEvent> rollingFileAppender = new RollingFileAppender<>();
rollingFileAppender.setContext(context);
rollingFileAppender.setName("FILE-" + discriminatingValue);
@@ -865,6 +869,7 @@ public class Logger implements LogKeys {
rollingFileAppender.setRollingPolicy(rollingPolicy);
rollingFileAppender.start();
siftAppenderMap.put(discriminatingValue, rollingFileAppender);
return rollingFileAppender;
}
});
@@ -873,6 +878,7 @@ public class Logger implements LogKeys {
String logLevel = pmanager.getProperty(LOGGER_SIFT_PREFIX, LOG_LEVEL);
String aditivity = pmanager.getProperty(LOGGER_SIFT_PREFIX, ADITIVITY);
siftEnabled = !"off".equalsIgnoreCase(logLevel);
ch.qos.logback.classic.Logger siftLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(LOGGER_SIFT);
siftLogger.setLevel(Level.toLevel(logLevel));
@@ -908,6 +914,44 @@ public class Logger implements LogKeys {
}
}
// * 거래 파일 로그 추가 코드
// MDC txId가 있을 때만 SiftingAppender로 기록하는 필터
// (txId 없는 일반 로그가 default.log로 쌓이는 것 방지)
ch.qos.logback.core.filter.Filter<ILoggingEvent> txIdFilter = new ch.qos.logback.core.filter.AbstractMatcherFilter<ILoggingEvent>() {
@Override
public ch.qos.logback.core.spi.FilterReply decide(ILoggingEvent event) {
if (!siftEnabled) {
return ch.qos.logback.core.spi.FilterReply.DENY;
}
String txId = event.getMDCPropertyMap().get(DISCRIMINATOR);
if (org.apache.commons.lang3.StringUtils.isNotBlank(txId) && !"default".equals(txId)) {
return ch.qos.logback.core.spi.FilterReply.ACCEPT;
}
return ch.qos.logback.core.spi.FilterReply.DENY;
}
};
txIdFilter.start();
siftAppender.addFilter(txIdFilter);
// SIFT appender를 추가할 대상 로거 목록
// → ESBFW, ADAPTER, DEFAULT 등 거래 처리에 관여하는 로거 추가
String[] siftTargetLoggers = { LOGGER_ESBFW, LOGGER_ADAPTER, LOGGER_DEFAULT, "org.apache.hc.client5.http"
// 필요 시 LOGGER_TRAN, LOGGER_NET 등 추가
};
for (String targetLoggerName : siftTargetLoggers) {
ch.qos.logback.classic.Logger targetLogger = (ch.qos.logback.classic.Logger) LoggerFactory
.getLogger(targetLoggerName);
if (targetLogger == null)
continue;
// siftAppender 추가 (기존 appender는 그대로 유지 - additive 건드리지 않음)
targetLogger.addAppender(siftAppender);
System.out.println("@ add SiftAppender to " + targetLogger.getName() + " - " + siftAppender.getName());
}
} catch(Exception e) {
System.err.println("@@ Logger Configure Error.===================");
e.printStackTrace();
@@ -939,7 +983,7 @@ public class Logger implements LogKeys {
}
asisLevel = logger.getLevel();
if ( asisLevel.toInt() != toLevel.toInt() ) {
if ( toLevel == null || asisLevel.toInt() != toLevel.toInt() ) {
logger.setLevel( toLevel );
System.out.println("@@ Logger setLoggerLevel | " + loggerName
+ " AS-IS [" + asisLevel.toString() + "] TO [" + toLevel.toString() + "]");
@@ -995,6 +1039,7 @@ public class Logger implements LogKeys {
System.err.println("@@ addFileAppender | " + loggerName + " | " + e.getMessage() );
logger = null;
}
System.out.println("@ add Logger to "+logger.getName()+" - "+logger.getLevel());
return logger;
}
@@ -1111,6 +1156,23 @@ public class Logger implements LogKeys {
// public boolean isFatal() {
// return this.loggerImpl.getLevel().toInt()<=Level.ERROR_INT; // 5000
// }
public static void closeSiftAppender(String discriminatingValue) {
if (StringUtils.isBlank(discriminatingValue) || "default".equals(discriminatingValue)) return;
RollingFileAppender<ILoggingEvent> appender = siftAppenderMap.remove(discriminatingValue);
if (appender != null && appender.isStarted()) {
appender.stop();
}
}
public static void setSiftEnabled(boolean enabled) {
siftEnabled = enabled;
System.out.println("@ SiftAppender " + (enabled ? "enabled" : "disabled"));
}
public static boolean isSiftEnabled() {
return siftEnabled;
}
public static void stop() {
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
lc.stop();
@@ -0,0 +1,440 @@
package com.eactive.eai.common.util;
import java.util.Arrays;
/**
* 보안아키텍처 마스킹 정책 구현 유틸리티
*
* <pre>
* [고유식별정보]
* 주민등록번호 123456-1234567 → 123456-1******
* 운전면허번호 제주-22-300001-50 → 제주-22-3*****-**
* 여권번호 M12345678 → M1234****
* 외국인등록번호 123456-5234567 → 123456-5******
*
* [금융식별정보]
* 계좌번호 99-912345-1 → 99-9*-*****1
* 카드번호 4518-4212-3456-1234 → 4518-42**-****-1234
* 유효기간 12/25 → **&#47;**
* CVV 123 → ***
*
* [인증정보]
* 비밀번호 password → ********
*
* [신상정보]
* 전화번호 064-1234-5678 → 064-12**-56**
* 이메일 aajjbacc@shinhan.com → **jjba**@shinhan.com
* 한글 성명 홍길동 → 홍*동
* 영문 성명 GILDONG HONG → ******* HONG
* 지번주소 제주특별자치도 제주시 연동 123번지, 101동 202호 → 제주특별자치도 제주시 연동 ***번지, **** ****
* 도로명주소 제주특별자치도 제주시 은남2길 5, 101동 202호 → 제주특별자치도 제주시 은남**길 ***, **** ****
*
* [온라인 정보]
* IPv4 100.200.123.45 → 100.200.***.45
* IPv6 21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A → 21DA:00D3:0000:2F3B:02AA:00FF:FE28:****
* </pre>
*/
public final class MaskingUtils {
private static final char MASK = '*';
private MaskingUtils() {}
// =========================================================================
// 고유식별정보
// =========================================================================
/**
* 주민등록번호 마스킹: 뒷 6자리 마스킹
* <pre>123456-1234567 → 123456-1******</pre>
*/
public static String maskResidentNumber(String value) {
if (value == null || value.isEmpty()) return value;
int idx = value.indexOf('-');
if (idx < 0) {
return maskTailChars(value, 6);
}
// 하이픈 + 성별 1자리까지 노출, 이후 6자리 마스킹
String front = value.substring(0, Math.min(idx + 2, value.length()));
return front + stars(6);
}
/**
* 운전면허번호 마스킹: 뒤에서부터 숫자 7자리 마스킹
* <pre>제주-22-300001-50 → 제주-22-3*****-**</pre>
*/
public static String maskDriverLicense(String value) {
if (value == null || value.isEmpty()) return value;
char[] chars = value.toCharArray();
int digitCount = 0;
for (int i = chars.length - 1; i >= 0 && digitCount < 7; i--) {
if (Character.isDigit(chars[i])) {
chars[i] = MASK;
digitCount++;
}
}
return new String(chars);
}
/**
* 여권번호 마스킹: 뒤에서부터 4자리 마스킹
* <pre>M12345678 → M1234****</pre>
*/
public static String maskPassport(String value) {
if (value == null || value.isEmpty()) return value;
return maskTailChars(value, 4);
}
/**
* 외국인등록번호 마스킹: 뒷 6자리 마스킹 (주민등록번호와 동일)
* <pre>123456-5234567 → 123456-5******</pre>
*/
public static String maskForeignerNumber(String value) {
return maskResidentNumber(value);
}
// =========================================================================
// 금융식별정보
// =========================================================================
/**
* 계좌번호 마스킹: 첫 3자리, 마지막 1자리만 노출
* <pre>99-912345-1 → 99-9*-*****1</pre>
*/
public static String maskAccountNumber(String value) {
if (value == null || value.isEmpty()) return value;
String digitsOnly = value.replaceAll("[^0-9]", "");
int totalDigits = digitsOnly.length();
if (totalDigits <= 4) return value;
char[] chars = value.toCharArray();
int digitIdx = 0;
for (int i = 0; i < chars.length; i++) {
if (Character.isDigit(chars[i])) {
digitIdx++;
// 4번째 자리부터 마지막 자리 직전까지 마스킹
if (digitIdx > 3 && digitIdx < totalDigits) {
chars[i] = MASK;
}
}
}
return new String(chars);
}
/**
* 카드번호 마스킹: PCI-DSS 기준 7~12번째 숫자 자리 마스킹
* <pre>4518-4212-3456-1234 → 4518-42**-****-1234</pre>
*/
public static String maskCardNumber(String value) {
if (value == null || value.isEmpty()) return value;
char[] chars = value.toCharArray();
int digitIdx = 0;
for (int i = 0; i < chars.length; i++) {
if (Character.isDigit(chars[i])) {
digitIdx++;
if (digitIdx >= 7 && digitIdx <= 12) {
chars[i] = MASK;
}
}
}
return new String(chars);
}
/**
* 카드 유효기간 마스킹: 월/년 모두 마스킹
* <pre>12/25 → **&#47;**</pre>
*/
public static String maskCardExpiry(String value) {
if (value == null || value.isEmpty()) return value;
return value.replaceAll("[0-9]", String.valueOf(MASK));
}
/**
* CVV 마스킹: 3자리 전체 마스킹
* <pre>123 → ***</pre>
*/
public static String maskCvv(String value) {
if (value == null || value.isEmpty()) return value;
return stars(value.length());
}
// =========================================================================
// 인증정보
// =========================================================================
/**
* 비밀번호 마스킹: 전체 자리 마스킹
* <pre>password1234 → ************</pre>
*/
public static String maskPassword(String value) {
if (value == null || value.isEmpty()) return value;
return stars(value.length());
}
// =========================================================================
// 신상정보
// =========================================================================
/**
* 전화번호/FAX 마스킹: 가운데 뒷 2자리, 뒷자리 뒷 2자리 마스킹
* 하이픈 없는 형식은 자릿수·접두사 기반으로 세그먼트를 분리한 뒤 마스킹하고 하이픈 없이 반환
* <pre>
* 064-1234-5678 → 064-12**-56**
* 06412345678 → 0641234**56**
* 01012345678 → 01012**56**
* </pre>
*/
public static String maskPhoneNumber(String value) {
if (value == null || value.isEmpty()) return value;
boolean hasHyphen = value.contains("-");
String normalized = hasHyphen ? value : normalizePhoneNumber(value);
String[] parts = normalized.split("-");
if (parts.length == 3) {
String m0 = parts[0];
String m1 = maskSegmentTail(parts[1], 2);
String m2 = maskSegmentTail(parts[2], 2);
return hasHyphen ? m0 + "-" + m1 + "-" + m2 : m0 + m1 + m2;
}
return value;
}
/**
* 한국 전화번호(숫자만) → XXX-XXXX-XXXX 형식으로 정규화
* <pre>
* 02 + 7자리 → 02-XXX-XXXX
* 02 + 8자리 → 02-XXXX-XXXX
* 0XX + 7자리 → 0XX-XXX-XXXX
* 0XX + 8자리 → 0XX-XXXX-XXXX
* </pre>
*/
private static String normalizePhoneNumber(String digits) {
int len = digits.length();
if (digits.startsWith("02")) {
if (len == 9) return digits.substring(0, 2) + "-" + digits.substring(2, 5) + "-" + digits.substring(5);
if (len == 10) return digits.substring(0, 2) + "-" + digits.substring(2, 6) + "-" + digits.substring(6);
} else if (digits.startsWith("0")) {
if (len == 10) return digits.substring(0, 3) + "-" + digits.substring(3, 6) + "-" + digits.substring(6);
if (len == 11) return digits.substring(0, 3) + "-" + digits.substring(3, 7) + "-" + digits.substring(7);
}
return digits;
}
/**
* 휴대폰 번호 마스킹: 가운데 뒷 2자리, 뒷자리 뒷 2자리 마스킹
* <pre>010-1234-5678 → 010-12**-56**</pre>
*/
public static String maskMobileNumber(String value) {
return maskPhoneNumber(value);
}
/**
* 이메일 마스킹: 앞 2자리와 @ 앞 2자리 마스킹
* <pre>aajjbacc@shinhan.com → **jjba**@shinhan.com</pre>
*/
public static String maskEmail(String value) {
if (value == null || value.isEmpty() || !value.contains("@")) return value;
int atIdx = value.indexOf('@');
String local = value.substring(0, atIdx);
String domain = value.substring(atIdx);
if (local.length() <= 4) {
return stars(local.length()) + domain;
}
return stars(2) + local.substring(2, local.length() - 2) + stars(2) + domain;
}
/**
* 한글/한자 성명 마스킹
* <pre>
* 2자: 뒤 1자리 마스킹 홍동 → 홍*
* 3자: 가운데 1자리 마스킹 홍길동 → 홍*동
* 4자 이상: 앞뒤 1자 제외한 가운데 전체 마스킹 홍길순동 → 홍**동
* </pre>
*/
public static String maskKoreanName(String value) {
if (value == null || value.isEmpty()) return value;
int len = value.length();
if (len == 1) return value;
if (len == 2) {
return String.valueOf(value.charAt(0)) + MASK;
}
if (len == 3) {
return String.valueOf(value.charAt(0)) + MASK + value.charAt(2);
}
// 4자 이상: 첫 자 + 가운데 전체 마스킹 + 마지막 자
return String.valueOf(value.charAt(0)) + stars(len - 2) + value.charAt(len - 1);
}
/**
* 영문 성명 마스킹: 성(last name)만 노출, 이름(first name)은 마스킹
* <pre>GILDONG HONG → ******* HONG</pre>
* (마지막 공백 구분 토큰이 성(last name)으로 간주)
*/
public static String maskEnglishName(String value) {
if (value == null || value.isEmpty()) return value;
String[] parts = value.trim().split("\\s+");
if (parts.length == 1) return value;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < parts.length - 1; i++) {
if (i > 0) sb.append(' ');
sb.append(stars(parts[i].length()));
}
sb.append(' ').append(parts[parts.length - 1]);
return sb.toString();
}
/**
* 지번주소 마스킹: 번지 이하 주소 마스킹 (String.length() 보존)
* <pre>
* 제주특별자치도 제주시 연동 123번지, 101동 202호 → 제주특별자치도 제주시 연동 ***번지, **** ****
* </pre>
*/
public static String maskJibunAddress(String value) {
if (value == null || value.isEmpty()) return value;
// 번지 앞 숫자 자릿수 보존 마스킹
String result = maskDigitsBefore(value, "번지");
// 쉼표 이후 상세주소 마스킹 (공백 유지, 자릿수 보존)
int commaIdx = result.indexOf(',');
if (commaIdx >= 0) {
String after = result.substring(commaIdx + 1);
result = result.substring(0, commaIdx + 1) + after.replaceAll("[^\\s]", String.valueOf(MASK));
}
return result;
}
/**
* 도로명주소 마스킹: 도로명 번호 및 번지 이하 마스킹 (String.length() 보존)
* <pre>
* 제주특별자치도 제주시 은남2길 5, 101동 202호 → 제주특별자치도 제주시 은남*길 *, **** ****
* </pre>
*/
public static String maskRoadAddress(String value) {
if (value == null || value.isEmpty()) return value;
// 도로명 번호 자릿수 보존 마스킹 (긴 suffix 먼저 처리)
String result = maskDigitsBefore(value, "대로");
result = maskDigitsBefore(result, "번길");
result = maskDigitsBefore(result, "");
result = maskDigitsBefore(result, "");
// 도로명 뒤 집 번호 자릿수 보존 마스킹
result = maskDigitsAfter(result, "대로");
result = maskDigitsAfter(result, "번길");
result = maskDigitsAfter(result, "");
result = maskDigitsAfter(result, "");
// 쉼표 이후 상세주소 마스킹 (공백 유지, 자릿수 보존)
int commaIdx = result.indexOf(',');
if (commaIdx >= 0) {
String after = result.substring(commaIdx + 1);
result = result.substring(0, commaIdx + 1) + after.replaceAll("[^\\s]", String.valueOf(MASK));
}
return result;
}
// =========================================================================
// 온라인 정보
// =========================================================================
/**
* IPv4 마스킹: 3번째 옥텟(17~24 비트) 마스킹
* <pre>100.200.123.45 → 100.200.***.45</pre>
*/
public static String maskIpv4(String value) {
if (value == null || value.isEmpty()) return value;
String[] parts = value.split("\\.", -1);
if (parts.length != 4) return value;
return parts[0] + "." + parts[1] + "." + stars(3) + "." + parts[3];
}
/**
* IPv6 마스킹: 마지막 그룹(113~128 비트) 마스킹
* <pre>21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A → 21DA:00D3:0000:2F3B:02AA:00FF:FE28:****</pre>
*/
public static String maskIpv6(String value) {
if (value == null || value.isEmpty()) return value;
int lastColon = value.lastIndexOf(':');
if (lastColon < 0) return value;
return value.substring(0, lastColon + 1) + stars(4);
}
/**
* IP 주소 마스킹: IPv4/IPv6 자동 판별, 쉼표 구분 다중 IP 지원
* <pre>
* 100.200.123.45 → 100.200.***.45
* 21DA:...FE28:9C5A → 21DA:...FE28:****
* 192.168.1.1,10.0.0.1 → 192.168.***.1,10.0.***.1
* </pre>
*/
public static String maskIpAddress(String value) {
if (value == null || value.isEmpty()) return value;
String[] ips = value.split(",");
StringBuilder sb = new StringBuilder();
for (int i = 0; i < ips.length; i++) {
if (i > 0) sb.append(',');
String ip = ips[i].trim();
if (ip.contains(":")) {
sb.append(maskIpv6(ip));
} else {
sb.append(maskIpv4(ip));
}
}
return sb.toString();
}
// =========================================================================
// private helpers
// =========================================================================
private static String stars(int count) {
if (count <= 0) return "";
char[] arr = new char[count];
Arrays.fill(arr, MASK);
return new String(arr);
}
private static String maskTailChars(String value, int count) {
if (value.length() <= count) return stars(value.length());
return value.substring(0, value.length() - count) + stars(count);
}
private static String maskSegmentTail(String segment, int count) {
if (segment.length() <= count) return stars(segment.length());
return segment.substring(0, segment.length() - count) + stars(count);
}
/** keyword 바로 앞에 연속된 숫자를 동일 자릿수의 '*'로 마스킹 */
private static String maskDigitsBefore(String value, String keyword) {
StringBuilder sb = new StringBuilder(value);
int from = 0;
while (true) {
int kIdx = sb.indexOf(keyword, from);
if (kIdx < 0) break;
int end = kIdx;
int start = end;
while (start > 0 && Character.isDigit(sb.charAt(start - 1))) {
start--;
}
for (int i = start; i < end; i++) {
sb.setCharAt(i, MASK);
}
from = kIdx + keyword.length();
}
return sb.toString();
}
/** keyword 바로 뒤 공백 이후의 연속된 숫자를 동일 자릿수의 '*'로 마스킹 */
private static String maskDigitsAfter(String value, String keyword) {
StringBuilder sb = new StringBuilder(value);
int from = 0;
while (true) {
int kIdx = sb.indexOf(keyword, from);
if (kIdx < 0) break;
int pos = kIdx + keyword.length();
while (pos < sb.length() && sb.charAt(pos) == ' ') pos++;
int start = pos;
while (pos < sb.length() && Character.isDigit(sb.charAt(pos))) pos++;
for (int i = start; i < pos; i++) {
sb.setCharAt(i, MASK);
}
from = kIdx + keyword.length();
}
return sb.toString();
}
}
@@ -10,12 +10,16 @@ public class STDMessageErrorKeys {
private static final String STDMESSAGE_ERROR_CODE ="STDMESSAGE_ERROR_CODE";
private static final String SUCCESS_CODE ="SUCCESS_CODE";
private static final String SUCCESS_MSG ="SUCCESS_MSG";
private static final String SUCCESS_DESC ="SUCCESS_DESC";
private static final String SYSTEM_ERROR_CODE ="SYSTEM_ERROR_CODE";
private static final String SYSTEM_ERROR_MSG ="SYSTEM_ERROR_MSG";
private static final String SYSTEM_ERROR_DESC ="SYSTEM_ERROR_DESC";
private static final String TIMEOUT_ERROR_CODE ="TIMEOUT_ERROR_CODE";
private static final String TIMEOUT_ERROR_MSG ="TIMEOUT_ERROR_MSG";
private static final String TIMEOUT_ERROR_DESC ="TIMEOUT_ERROR_DESC";
private static final String BIZ_ERROR_CODE ="BIZ_ERROR_CODE";
private static final String BIZ_ERROR_MSG ="BIZ_ERROR_MSG";
private static final String BIZ_ERROR_DESC ="BIZ_ERROR_DESC";
private static final String DUP_LOGIN_ERROR_CODE ="DUP_LOGIN_ERROR_CODE";
private static final String DUP_LOGIN_ERROR_MSG ="DUP_LOGIN_ERROR_MSG";
private static final String NOT_LOGIN_ERROR_CODE ="NOT_LOGIN_ERROR_CODE";
@@ -28,44 +32,72 @@ public class STDMessageErrorKeys {
private static final String UUID_MISMATCH_ERROR_MSG ="UUID_MISMATCH_ERROR_MSG";
private static final String VALID_VALUE_ERROR_CODE ="VALID_VALUE_ERROR_CODE";
private static final String VALID_VALUE_ERROR_MSG ="VALID_VALUE_ERROR_MSG";
private static final String VALID_VALUE_ERROR_DESC ="VALID_VALUE_ERROR_DESC";
private static final String RATELIMIT_ERROR_CODE ="RATELIMIT_ERROR_CODE";
private static final String RATELIMIT_ERROR_MSG ="RATELIMIT_ERROR_MSG";
private static final String RATELIMIT_ERROR_DESC ="RATELIMIT_ERROR_DESC";
private static final String RESTRICT_ERROR_CODE ="RESTRICT_ERROR_CODE";
private static final String RESTRICT_ERROR_MSG ="RESTRICT_ERROR_MSG";
private static final String RESTRICT_ERROR_DESC ="RESTRICT_ERROR_DESC";
//사이트별 에러코드
public static String SUCCESS_CODE_VALUE ;//성공코드
public static String SUCCESS_MSG_VALUE ;//성공메시지
public static String SYSTEM_ERROR_CODE_VALUE ;//시스템 에러코드
public static String SYSTEM_ERROR_MSG_VALUE ;//시스템 에러메시지
public static String BIZ_ERROR_CODE_VALUE ;//업무 에러코드
public static String BIZ_ERROR_MSG_VALUE ;//업무 에러메시지
public static String TIMEOUT_ERROR_CODE_VALUE ;//타임아웃 에러코드
public static String TIMEOUT_ERROR_MSG_VALUE ;//타임아웃 에러메시지
public static String DUP_LOGIN_ERROR_CODE_VALUE ;//중복로그인 에러코드
public static String DUP_LOGIN_ERROR_MSG_VALUE ;//중복로그인 에러메시지
public static String NOT_LOGIN_ERROR_CODE_VALUE ;//미로그인 에러코드
public static String NOT_LOGIN_ERROR_MSG_VALUE ;//미로그인 에러메시지
public static String APPROVE_ERROR_CODE_VALUE ;//책임자승인 에러코드
public static String APPROVE_ERROR_MSG_VALUE ;//책임자승인 에러메시지
public static String APPROVE_NOT_FOUND_ERROR_CODE_VALUE ;//책임자 승인자가 존재하지 않는 에러코드
public static String APPROVE_NOT_FOUND_ERROR_MSG_VALUE ;//책임자 승인자가 존재하지 않는 에러메시지
public static String UUID_MISMATCH_ERROR_CODE_VALUE ;//UUID가 같지 않은 에러코드
public static String UUID_MISMATCH_ERROR_MSG_VALUE ;//UUID가 같지 않은 에러메시지
public static String VALID_VALUE_ERROR_CODE_VALUE ;//유효값 에러코드
public static String VALID_VALUE_ERROR_MSG_VALUE ;//유효값 에러메시지
public static String RATELIMIT_ERROR_CODE_VALUE ;//유량제어 에러코드
public static String RATELIMIT_ERROR_MSG_VALUE ;//유량제어 에러메시지
public static volatile String SUCCESS_CODE_VALUE ;//성공코드
public static volatile String SUCCESS_MSG_VALUE ;//성공메시지
public static volatile String SUCCESS_DESC_VALUE ;//성공메시지
public static volatile String SYSTEM_ERROR_CODE_VALUE ;//시스템 에러코드
public static volatile String SYSTEM_ERROR_MSG_VALUE ;//시스템 에러메시지
public static volatile String SYSTEM_ERROR_DESC_VALUE ;//시스템 에러메시지
public static volatile String BIZ_ERROR_CODE_VALUE ;//업무 에러코드
public static volatile String BIZ_ERROR_MSG_VALUE ;//업무 에러메시지
public static volatile String BIZ_ERROR_DESC_VALUE ;//업무 에러메시지
public static volatile String TIMEOUT_ERROR_CODE_VALUE ;//타임아웃 에러코드
public static volatile String TIMEOUT_ERROR_MSG_VALUE ;//타임아웃 에러메시지
public static volatile String TIMEOUT_ERROR_DESC_VALUE ;//타임아웃 에러메시지
public static volatile String DUP_LOGIN_ERROR_CODE_VALUE ;//중복로그인 에러코드
public static volatile String DUP_LOGIN_ERROR_MSG_VALUE ;//중복로그인 에러메시지
public static volatile String NOT_LOGIN_ERROR_CODE_VALUE ;//미로그인 에러코드
public static volatile String NOT_LOGIN_ERROR_MSG_VALUE ;//미로그인 에러메시지
public static volatile String APPROVE_ERROR_CODE_VALUE ;//책임자승인 에러코드
public static volatile String APPROVE_ERROR_MSG_VALUE ;//책임자승인 에러메시지
public static volatile String APPROVE_NOT_FOUND_ERROR_CODE_VALUE ;//책임자 승인자가 존재하지 않는 에러코드
public static volatile String APPROVE_NOT_FOUND_ERROR_MSG_VALUE ;//책임자 승인자가 존재하지 않는 에러메시지
public static volatile String UUID_MISMATCH_ERROR_CODE_VALUE ;//UUID가 같지 않은 에러코드
public static volatile String UUID_MISMATCH_ERROR_MSG_VALUE ;//UUID가 같지 않은 에러메시지
public static volatile String VALID_VALUE_ERROR_CODE_VALUE ;//유효값 에러코드
public static volatile String VALID_VALUE_ERROR_MSG_VALUE ;//유효값 에러메시지
public static volatile String VALID_VALUE_ERROR_DESC_VALUE ;//유효값 에러메시지
public static volatile String RATELIMIT_ERROR_CODE_VALUE ;//유량제어 에러코드
public static volatile String RATELIMIT_ERROR_MSG_VALUE ;//유량제어 에러메시지
public static volatile String RATELIMIT_ERROR_DESC_VALUE ;//유량제어 에러메시지
public static volatile String RESTRICT_ERROR_CODE_VALUE ;//거래제한 에러코드
public static volatile String RESTRICT_ERROR_MSG_VALUE ;//거래제한 에러메시지
public static volatile String RESTRICT_ERROR_DESC_VALUE ;//거래제한 에러메시지
//기존 오류코드는 900400 과 900401만 사용했음
static {
PropManager manager = PropManager.getInstance();
loadValues(manager);
// STDMESSAGE_ERROR_CODE 그룹 리로드 시 자동 갱신
manager.addPropertyChangeListener(evt -> {
if (STDMESSAGE_ERROR_CODE.equals(evt.getPropertyName())) {
loadValues(PropManager.getInstance());
}
});
}
private static synchronized void loadValues(PropManager manager) {
SUCCESS_CODE_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,SUCCESS_CODE,"900000");
SUCCESS_MSG_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,SUCCESS_MSG,"정상 처리되었습니다.");
SUCCESS_DESC_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,SUCCESS_DESC,SUCCESS_MSG_VALUE);
SYSTEM_ERROR_CODE_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,SYSTEM_ERROR_CODE,"900400");
SYSTEM_ERROR_MSG_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,SYSTEM_ERROR_MSG,"시스템오류가 발생하였습니다.");
SYSTEM_ERROR_DESC_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,SYSTEM_ERROR_DESC,SYSTEM_ERROR_MSG_VALUE);
TIMEOUT_ERROR_CODE_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,TIMEOUT_ERROR_CODE,"900401");
TIMEOUT_ERROR_MSG_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,TIMEOUT_ERROR_MSG,"타임아웃오류가 발생하였습니다.");
TIMEOUT_ERROR_DESC_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,TIMEOUT_ERROR_DESC,TIMEOUT_ERROR_MSG_VALUE);
BIZ_ERROR_CODE_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,BIZ_ERROR_CODE,"900402");
BIZ_ERROR_MSG_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,BIZ_ERROR_MSG,"업무오류가 발생하였습니다.");
BIZ_ERROR_MSG_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,BIZ_ERROR_MSG,"API오류가 발생하였습니다.");
BIZ_ERROR_DESC_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,BIZ_ERROR_DESC,BIZ_ERROR_MSG_VALUE);
DUP_LOGIN_ERROR_CODE_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,DUP_LOGIN_ERROR_CODE,"900403");
DUP_LOGIN_ERROR_MSG_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,DUP_LOGIN_ERROR_MSG,"중복로그인오류가 발생하였습니다.");
NOT_LOGIN_ERROR_CODE_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,NOT_LOGIN_ERROR_CODE,"900404");
@@ -78,8 +110,17 @@ public class STDMessageErrorKeys {
UUID_MISMATCH_ERROR_MSG_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,UUID_MISMATCH_ERROR_MSG,"MCI 세션ID가 같지않은 오류가 발생하였습니다.");
VALID_VALUE_ERROR_CODE_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,VALID_VALUE_ERROR_CODE,"900408");
VALID_VALUE_ERROR_MSG_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,VALID_VALUE_ERROR_MSG,"유효값 오류가 발생하였습니다.");
VALID_VALUE_ERROR_DESC_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,VALID_VALUE_ERROR_DESC,VALID_VALUE_ERROR_MSG_VALUE);
RATELIMIT_ERROR_CODE_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,RATELIMIT_ERROR_CODE,"900409");
RATELIMIT_ERROR_MSG_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,RATELIMIT_ERROR_MSG,"RateLimiter Overflow");
RATELIMIT_ERROR_DESC_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,RATELIMIT_ERROR_DESC,RATELIMIT_ERROR_MSG_VALUE);
RESTRICT_ERROR_CODE_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,RESTRICT_ERROR_CODE,"900410");
RESTRICT_ERROR_MSG_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,RESTRICT_ERROR_MSG,"거래제한");
RESTRICT_ERROR_DESC_VALUE = manager.getProperty(STDMESSAGE_ERROR_CODE,RESTRICT_ERROR_DESC,RESTRICT_ERROR_MSG_VALUE);
}
public static void reload() {
loadValues(PropManager.getInstance());
}
private STDMessageErrorKeys() {
@@ -0,0 +1,581 @@
package com.eactive.eai.common.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
@TestMethodOrder(MethodOrderer.DisplayName.class)
class MaskingUtilsTest {
// =========================================================================
// 1. 고유식별정보
// =========================================================================
// --- 1-1. 주민등록번호 ---
@Test
@DisplayName("1-1-1. 주민등록번호 — 하이픈 포함 정상 형식")
void testResidentNumber_withHyphen() {
String input = "123456-1234567";
String result = MaskingUtils.maskResidentNumber(input);
assertEquals("123456-1******", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("1-1-2. 주민등록번호 — 하이픈 없는 형식 (뒤 6자리 마스킹)")
void testResidentNumber_withoutHyphen() {
String input = "0701011234567";
String result = MaskingUtils.maskResidentNumber(input);
assertEquals("0701011******", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("1-1-3. 주민등록번호 — null/빈 문자열 방어")
void testResidentNumber_nullAndEmpty() {
assertNull(MaskingUtils.maskResidentNumber(null));
assertEquals("", MaskingUtils.maskResidentNumber(""));
}
// --- 1-2. 운전면허번호 ---
@Test
@DisplayName("1-2-1. 운전면허번호 — 숫자 7자리 역방향 마스킹")
void testDriverLicense_normal() {
String input = "제주-22-300001-50";
String result = MaskingUtils.maskDriverLicense(input);
assertEquals("제주-22-3*****-**", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("1-2-2. 운전면허번호 — null/빈 문자열 방어")
void testDriverLicense_nullAndEmpty() {
assertNull(MaskingUtils.maskDriverLicense(null));
assertEquals("", MaskingUtils.maskDriverLicense(""));
}
// --- 1-3. 여권번호 ---
@Test
@DisplayName("1-3-1. 여권번호 — 구여권 형식")
void testPassport_old() {
String input = "M12345678";
String result = MaskingUtils.maskPassport(input);
assertEquals("M1234****", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("1-3-2. 여권번호 — 신여권 형식 (영문+숫자 혼합)")
void testPassport_new() {
String input = "M123A5678";
String result = MaskingUtils.maskPassport(input);
assertEquals("M123A****", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("1-3-3. 여권번호 — null/빈 문자열 방어")
void testPassport_nullAndEmpty() {
assertNull(MaskingUtils.maskPassport(null));
assertEquals("", MaskingUtils.maskPassport(""));
}
// --- 1-4. 외국인등록번호 ---
@Test
@DisplayName("1-4-1. 외국인등록번호 — 주민등록번호와 동일 방식")
void testForeignerNumber_normal() {
String input = "123456-5234567";
String result = MaskingUtils.maskForeignerNumber(input);
assertEquals("123456-5******", result);
assertEquals(input.length(), result.length());
}
// =========================================================================
// 2. 금융식별정보
// =========================================================================
// --- 2-1. 계좌번호 ---
@Test
@DisplayName("2-1-1. 계좌번호 — 명세 예시 형식 (XX-XX-XXXXXX)")
void testAccountNumber_specExample() {
String input = "99-94-123451";
String result = MaskingUtils.maskAccountNumber(input);
assertEquals("99-9*-*****1", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("2-1-2. 계좌번호 — 일반 형식 (XX-XXXXXX-X): 하이픈 원위치 유지")
void testAccountNumber_commonFormat() {
String input = "99-912345-1";
String result = MaskingUtils.maskAccountNumber(input);
assertEquals("99-9*****-1", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("2-1-3. 계좌번호 — 하이픈 없는 형식")
void testAccountNumber_noHyphen() {
String input = "123456789";
String result = MaskingUtils.maskAccountNumber(input);
assertEquals("123*****9", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("2-1-4. 계좌번호 — 4자리 이하는 마스킹 안 함")
void testAccountNumber_tooShort() {
assertEquals("1234", MaskingUtils.maskAccountNumber("1234"));
}
@Test
@DisplayName("2-1-5. 계좌번호 — null/빈 문자열 방어")
void testAccountNumber_nullAndEmpty() {
assertNull(MaskingUtils.maskAccountNumber(null));
assertEquals("", MaskingUtils.maskAccountNumber(""));
}
// --- 2-2. 카드번호 ---
@Test
@DisplayName("2-2-1. 카드번호 — PCI-DSS 7~12번째 자리 마스킹")
void testCardNumber_normal() {
String input = "4518-4212-3456-1234";
String result = MaskingUtils.maskCardNumber(input);
assertEquals("4518-42**-****-1234", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("2-2-2. 카드번호 — 하이픈 없는 16자리 형식")
void testCardNumber_noHyphen() {
String input = "4518421234561234";
String result = MaskingUtils.maskCardNumber(input);
assertEquals("451842******1234", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("2-2-3. 카드번호 — null/빈 문자열 방어")
void testCardNumber_nullAndEmpty() {
assertNull(MaskingUtils.maskCardNumber(null));
assertEquals("", MaskingUtils.maskCardNumber(""));
}
// --- 2-3. 카드 유효기간 ---
@Test
@DisplayName("2-3-1. 카드 유효기간 — 월/년 전체 마스킹")
void testCardExpiry_normal() {
String input = "12/25";
String result = MaskingUtils.maskCardExpiry(input);
assertEquals("**/**", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("2-3-2. 카드 유효기간 — 구분자 없는 형식")
void testCardExpiry_noDelimiter() {
String input = "1225";
String result = MaskingUtils.maskCardExpiry(input);
assertEquals("****", result);
assertEquals(input.length(), result.length());
}
// --- 2-4. CVV ---
@Test
@DisplayName("2-4-1. CVV — 전체 마스킹")
void testCvv_normal() {
String input = "123";
String result = MaskingUtils.maskCvv(input);
assertEquals("***", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("2-4-2. CVV — null/빈 문자열 방어")
void testCvv_nullAndEmpty() {
assertNull(MaskingUtils.maskCvv(null));
assertEquals("", MaskingUtils.maskCvv(""));
}
// =========================================================================
// 3. 인증정보
// =========================================================================
// --- 3-1. 비밀번호 ---
@Test
@DisplayName("3-1-1. 비밀번호 — 전체 자리 마스킹")
void testPassword_normal() {
String input = "password";
String result = MaskingUtils.maskPassword(input);
assertEquals("********", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("3-1-2. 비밀번호 — 숫자/특수문자 혼합")
void testPassword_mixed() {
String input = "Pass1234!@";
String result = MaskingUtils.maskPassword(input);
assertEquals("**********", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("3-1-3. 비밀번호 — null/빈 문자열 방어")
void testPassword_nullAndEmpty() {
assertNull(MaskingUtils.maskPassword(null));
assertEquals("", MaskingUtils.maskPassword(""));
}
// =========================================================================
// 4. 신상정보
// =========================================================================
// --- 4-1. 전화번호/휴대폰 ---
@Test
@DisplayName("4-1-1. 전화번호 — 가운데·뒷자리 각 2자리 마스킹 (지역)")
void testPhoneNumber_local() {
String input = "064-1234-5678";
String result = MaskingUtils.maskPhoneNumber(input);
assertEquals("064-12**-56**", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("4-1-2. 전화번호 — 서울 02 형식")
void testPhoneNumber_seoul() {
String input = "02-1234-5678";
String result = MaskingUtils.maskPhoneNumber(input);
assertEquals("02-12**-56**", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("4-1-3. 전화번호 — 010 형식 (하이픈 있음)")
void testMobileNumber_normal() {
String input = "010-1234-5678";
String result = MaskingUtils.maskMobileNumber(input);
assertEquals("010-12**-56**", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("4-1-4. 전화번호 — 하이픈 없는 11자리 휴대폰")
void testPhoneNumber_noHyphen_mobile() {
String input = "01012345678";
String result = MaskingUtils.maskPhoneNumber(input);
assertEquals("01012**56**", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("4-1-5. 전화번호 — 하이픈 없는 10자리 서울(02)")
void testPhoneNumber_noHyphen_seoul10() {
String input = "0212345678";
String result = MaskingUtils.maskPhoneNumber(input);
assertEquals("0212**56**", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("4-1-6. 전화번호 — 하이픈 없는 9자리 서울(02)")
void testPhoneNumber_noHyphen_seoul9() {
String input = "021234567";
String result = MaskingUtils.maskPhoneNumber(input);
assertEquals("021**45**", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("4-1-7. 전화번호 — 하이픈 없는 10자리 지역(0XX-XXX-XXXX)")
void testPhoneNumber_noHyphen_local10() {
String input = "0641234567";
String result = MaskingUtils.maskPhoneNumber(input);
assertEquals("0641**45**", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("4-1-8. 전화번호 — 하이픈 없는 11자리 지역(0XX-XXXX-XXXX)")
void testPhoneNumber_noHyphen_local11() {
String input = "06412345678";
String result = MaskingUtils.maskPhoneNumber(input);
assertEquals("06412**56**", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("4-1-9. 전화번호 — 인식 불가 형식은 원본 반환")
void testPhoneNumber_invalidFormat() {
assertEquals("0641234", MaskingUtils.maskPhoneNumber("0641234"));
}
@Test
@DisplayName("4-1-10. 전화번호 — null/빈 문자열 방어")
void testPhoneNumber_nullAndEmpty() {
assertNull(MaskingUtils.maskPhoneNumber(null));
assertEquals("", MaskingUtils.maskPhoneNumber(""));
}
// --- 4-2. 이메일 ---
@Test
@DisplayName("4-2-1. 이메일 — 앞 2자리·@ 앞 2자리 마스킹")
void testEmail_normal() {
String input = "aajjbacc@shinhan.com";
String result = MaskingUtils.maskEmail(input);
assertEquals("**jjba**@shinhan.com", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("4-2-2. 이메일 — 로컬 파트 4자 이하는 전체 마스킹")
void testEmail_shortLocal() {
String input = "abc@test.com";
String result = MaskingUtils.maskEmail(input);
assertEquals("***@test.com", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("4-2-3. 이메일 — @ 없는 경우 원본 반환")
void testEmail_noAtSign() {
assertEquals("notanemail", MaskingUtils.maskEmail("notanemail"));
}
@Test
@DisplayName("4-2-4. 이메일 — null/빈 문자열 방어")
void testEmail_nullAndEmpty() {
assertNull(MaskingUtils.maskEmail(null));
assertEquals("", MaskingUtils.maskEmail(""));
}
// --- 4-3. 한글 성명 ---
@Test
@DisplayName("4-3-1. 한글 성명 — 2자: 뒤 1자리 마스킹")
void testKoreanName_2chars() {
String input = "홍동";
String result = MaskingUtils.maskKoreanName(input);
assertEquals("홍*", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("4-3-2. 한글 성명 — 3자: 가운데 1자리 마스킹")
void testKoreanName_3chars() {
String input = "홍길동";
String result = MaskingUtils.maskKoreanName(input);
assertEquals("홍*동", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("4-3-3. 한글 성명 — 4자: 앞뒤 1자 제외 가운데 전체 마스킹")
void testKoreanName_4chars() {
String input = "홍길순동";
String result = MaskingUtils.maskKoreanName(input);
assertEquals("홍**동", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("4-3-4. 한글 성명 — 한자 2자 성명")
void testKoreanName_hanja() {
String input = "田中";
String result = MaskingUtils.maskKoreanName(input);
assertEquals("田*", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("4-3-5. 한글 성명 — 1자는 마스킹 없이 반환")
void testKoreanName_1char() {
String input = "";
String result = MaskingUtils.maskKoreanName(input);
assertEquals("", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("4-3-6. 한글 성명 — null/빈 문자열 방어")
void testKoreanName_nullAndEmpty() {
assertNull(MaskingUtils.maskKoreanName(null));
assertEquals("", MaskingUtils.maskKoreanName(""));
}
// --- 4-4. 영문 성명 ---
@Test
@DisplayName("4-4-1. 영문 성명 — 이름(first name) 마스킹·성(last name) 노출")
void testEnglishName_normal() {
String input = "GILDONG HONG";
String result = MaskingUtils.maskEnglishName(input);
assertEquals("******* HONG", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("4-4-2. 영문 성명 — 이름이 2개인 경우")
void testEnglishName_multipleFirstNames() {
String input = "MARY GRACE HONG";
String result = MaskingUtils.maskEnglishName(input);
assertEquals("**** ***** HONG", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("4-4-3. 영문 성명 — 단일 토큰은 마스킹 없이 반환")
void testEnglishName_singleToken() {
assertEquals("HONG", MaskingUtils.maskEnglishName("HONG"));
}
@Test
@DisplayName("4-4-4. 영문 성명 — null/빈 문자열 방어")
void testEnglishName_nullAndEmpty() {
assertNull(MaskingUtils.maskEnglishName(null));
assertEquals("", MaskingUtils.maskEnglishName(""));
}
// --- 4-5. 지번주소 ---
@Test
@DisplayName("4-5-1. 지번주소 — 번지 마스킹 및 상세주소 전체 마스킹")
void testJibunAddress_normal() {
String input = "제주특별자치도 제주시 연동 123번지, 101동 202호";
String result = MaskingUtils.maskJibunAddress(input);
assertEquals("제주특별자치도 제주시 연동 ***번지, **** ****", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("4-5-2. 지번주소 — 상세주소(쉼표 이후) 없는 경우")
void testJibunAddress_noDetail() {
String input = "제주특별자치도 제주시 연동 123번지";
String result = MaskingUtils.maskJibunAddress(input);
assertEquals("제주특별자치도 제주시 연동 ***번지", result);
assertEquals(input.length(), result.length());
}
// --- 4-6. 도로명주소 ---
@Test
@DisplayName("4-6-1. 도로명주소 — 도로명 번호 및 번지·상세주소 마스킹 (자릿수 보존)")
void testRoadAddress_normal() {
String input = "제주특별자치도 제주시 은남2길 5, 101동 202호";
String result = MaskingUtils.maskRoadAddress(input);
assertEquals("제주특별자치도 제주시 은남*길 *, **** ****", result);
assertEquals(input.length(), result.length());
}
@Test
@DisplayName("4-6-2. 도로명주소 — 상세주소(쉼표 이후) 없는 경우")
void testRoadAddress_noDetail() {
String input = "제주특별자치도 제주시 은남2길 5";
String result = MaskingUtils.maskRoadAddress(input);
assertEquals("제주특별자치도 제주시 은남*길 *", result);
assertEquals(input.length(), result.length());
}
// =========================================================================
// 5. 온라인 정보 (IP는 고정 길이 마스킹 String.length() 보존 단언 없음)
// =========================================================================
// --- 5-1. IPv4 ---
@Test
@DisplayName("5-1-1. IPv4 — 3번째 옥텟 고정 마스킹 (***)")
void testIpv4_normal() {
assertEquals("100.200.***.45", MaskingUtils.maskIpv4("100.200.123.45"));
}
@Test
@DisplayName("5-1-2. IPv4 — 3rd octet 2자리: *** 고정으로 길이 증가 (의도된 동작)")
void testIpv4_twoDigitOctet() {
String input = "10.0.10.5";
String result = MaskingUtils.maskIpv4(input);
assertEquals("10.0.***.5", result);
// 2자리(10) 3자리(***): length 9 10
assertEquals(input.length() + 1, result.length());
}
@Test
@DisplayName("5-1-3. IPv4 — 3rd octet 1자리: *** 고정으로 길이 증가 (의도된 동작)")
void testIpv4_oneDigitOctet() {
String input = "10.0.1.5";
String result = MaskingUtils.maskIpv4(input);
assertEquals("10.0.***.5", result);
// 1자리(1) 3자리(***): length 8 10
assertEquals(input.length() + 2, result.length());
}
@Test
@DisplayName("5-1-4. IPv4 — 형식이 아닌 경우 원본 반환")
void testIpv4_invalidFormat() {
assertEquals("100.200.123", MaskingUtils.maskIpv4("100.200.123"));
}
@Test
@DisplayName("5-1-5. IPv4 — null/빈 문자열 방어")
void testIpv4_nullAndEmpty() {
assertNull(MaskingUtils.maskIpv4(null));
assertEquals("", MaskingUtils.maskIpv4(""));
}
// --- 5-2. IPv6 ---
@Test
@DisplayName("5-2-1. IPv6 — 마지막 그룹(113~128 비트) 고정 마스킹 (****)")
void testIpv6_normal() {
assertEquals(
"21DA:00D3:0000:2F3B:02AA:00FF:FE28:****",
MaskingUtils.maskIpv6("21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A")
);
}
@Test
@DisplayName("5-2-2. IPv6 — null/빈 문자열 방어")
void testIpv6_nullAndEmpty() {
assertNull(MaskingUtils.maskIpv6(null));
assertEquals("", MaskingUtils.maskIpv6(""));
}
// --- 5-3. IP 자동 판별 ---
@Test
@DisplayName("5-3-1. IP 자동 판별 — IPv4")
void testIpAddress_detectIpv4() {
assertEquals("192.168.***.1", MaskingUtils.maskIpAddress("192.168.100.1"));
}
@Test
@DisplayName("5-3-2. IP 자동 판별 — IPv6")
void testIpAddress_detectIpv6() {
assertEquals(
"21DA:00D3:0000:2F3B:02AA:00FF:FE28:****",
MaskingUtils.maskIpAddress("21DA:00D3:0000:2F3B:02AA:00FF:FE28:9C5A")
);
}
@Test
@DisplayName("5-3-3. IP 자동 판별 — 쉼표 구분 다중 IPv4 (3자리/1자리 혼합)")
void testIpAddress_multipleIpv4() {
// 192.168.100.1: 3rd octet 3자리 길이 유지
// 10.0.0.2: 3rd octet 1자리(0) *** 고정으로 길이 증가
assertEquals("192.168.***.1,10.0.***.2", MaskingUtils.maskIpAddress("192.168.100.1,10.0.0.2"));
}
}