비밀번호 변경 기능 개선 및 검증 강화

- 비밀번호 변경 페이지에 확인 Dialog 추가 (로그인/거래취소)
- 비밀번호 에러 코드별 상세 메시지 처리
  * INVALID_PASSWORD, UPDATE_PASSWORD_FAIL, PREVIOUS_PASSWORD
  * MERCHANT_INFO_MATCH_PASSWORD, PASSWORD_LENGHT
  * DISALLOWED_CHARACTERS_INCLUDED, DISALLOWED_WHITE_SPACE
  * NOT_ENOUGH_COMPLEXITY, REPEATED_CHARACTER_SEQUENCE
  * COMMON_PASSWORD_DETECTED
- 비밀번호 입력 검증 로직 통합 (validatePassword)
- 이메일/전화번호 마스킹 기능 추가
- 사용자 추가 페이지 에러 처리 개선
- 다국어 메시지 추가 (한국어/영어)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jay Sheen
2025-11-17 20:50:53 +09:00
parent 522ccf7464
commit 2d1dd6f9e7
10 changed files with 748 additions and 128 deletions

View File

@@ -1,3 +1,4 @@
export * from './error';
export * from './toast';
export * from './web-view-bridge';
export * from './web-view-bridge';
export * from './masking';

41
src/shared/lib/masking.ts Normal file
View File

@@ -0,0 +1,41 @@
/**
* 전화번호 마스킹 함수
* @param phoneNumber - 마스킹할 전화번호 (예: "01012345678")
* @returns 마스킹된 전화번호 (예: "0101234****")
*/
export const maskPhoneNumber = (phoneNumber: string): string => {
const cleaned = phoneNumber.replace(/[^0-9]/g, '');
if (cleaned.length === 11 && cleaned.startsWith('010')) {
const prefix = cleaned.substring(0, 7);
return `${prefix}****`;
}
return phoneNumber;
};
/**
* 이메일 마스킹 함수
* @param email - 마스킹할 이메일 (예: "hanbyeol@gmail.com")
* @returns 마스킹된 이메일 (예: "ha******@gmail.com")
*/
export const maskEmail = (email: string): string => {
const atIndex = email.indexOf('@');
if (atIndex === -1) {
return email;
}
const localPart = email.substring(0, atIndex);
const domainPart = email.substring(atIndex);
if (localPart.length <= 2) {
return email;
}
const visiblePart = localPart.substring(0, 2);
const maskedCount = localPart.length - 2;
const maskedPart = '*'.repeat(maskedCount);
return `${visiblePart}${maskedPart}${domainPart}`;
};

View File

@@ -0,0 +1,259 @@
/**
* 비밀번호 검증 함수
*/
// 허용되는 문자 목록
const ALLOWED_CHARS = new Set([
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F',
'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '!', '@',
'#', '$', '%', '^', '&', '*', '(', ')', '-', '_', '+', '=', '[', ']', '{', '}',
'|', '\\', ':', ';', '"', '\'', '<', '>', ',', '.', '?', '/'
]);
// 키보드 배열 순서 (연속 3자리 검증용)
const KEYBOARD_SEQUENCES = [
// 일반 키
'1234567890-=',
'qwertyuiop[]\\',
'asdfghjkl;\'',
'zxcvbnm,./',
// 대문자 (shift + 문자)
'QWERTYUIOP',
'ASDFGHJKL',
'ZXCVBNM',
// shift 누른 특수문자
'!@#$%^&*()_+',
'{}|',
':"',
'<>?'
];
// Shift 키 매핑 (shift 누른 문자 -> 원래 문자)
const SHIFT_MAP: { [key: string]: string } = {
// 숫자열
'!': '1', '@': '2', '#': '3', '$': '4', '%': '5',
'^': '6', '&': '7', '*': '8', '(': '9', ')': '0',
'_': '-', '+': '=',
// 2번째 줄
'{': '[', '}': ']', '|': '\\',
// 3번째 줄
':': ';', '"': '\'',
// 4번째 줄
'<': ',', '>': '.', '?': '/'
};
// 문자를 shift 미적용 버전으로 변환
const unshiftChar = (char: string): string => {
return SHIFT_MAP[char] || char;
};
export interface PasswordValidationResult {
isValid: boolean;
errorMessage: string;
}
/**
* 비밀번호 검증 함수
* @param password - 검증할 비밀번호
* @param t - 번역 함수
* @returns 검증 결과 객체
*/
export const validatePassword = (
password: string,
t: (key: string) => string
): PasswordValidationResult => {
// 1. 공백 검증
if (!password || !password.trim() || /\s/.test(password)) {
return {
isValid: false,
errorMessage: t('account.passwordNoSpaceAllowed')
};
}
// 2. 길이 검증 (8-20자)
if (password.length < 8 || password.length > 20) {
return {
isValid: false,
errorMessage: t('account.passwordLengthRequirement')
};
}
// 3. 허용된 문자만 사용했는지 검증
for (const char of password) {
if (!ALLOWED_CHARS.has(char)) {
return {
isValid: false,
errorMessage: t('account.passwordInvalidCharacter')
};
}
}
// 4. 문자 조합 검증: 영문+숫자, 영문+특수문자, 숫자+특수문자 중 2가지 이상
const hasLetter = /[a-zA-Z]/.test(password);
const hasNumber = /\d/.test(password);
const hasSpecial = /[!@#$%^&*()\-_+=[\]{}|\\:;"'<>,.?/]/.test(password);
const combinationCount = [hasLetter, hasNumber, hasSpecial].filter(Boolean).length;
if (combinationCount < 2) {
return {
isValid: false,
errorMessage: t('account.passwordCombinationRequirement')
};
}
// 5. 동일 문자 3번 연속 금지
for (let i = 0; i < password.length - 2; i++) {
if (password[i] === password[i + 1] && password[i] === password[i + 2]) {
return {
isValid: false,
errorMessage: t('account.passwordNoRepeatingCharacters')
};
}
}
// 5-1. 연속된 알파벳/숫자 3자리 금지 (abc, 123, xyz, 987 등, 대소문자/shift 문자 포함)
for (let i = 0; i < password.length - 2; i++) {
const char1 = password[i];
const char2 = password[i + 1];
const char3 = password[i + 2];
if (!char1 || !char2 || !char3) continue;
// 1) 원본 문자 검사
const code1 = char1.charCodeAt(0);
const code2 = char2.charCodeAt(0);
const code3 = char3.charCodeAt(0);
// 연속 증가하는 패턴 (abc, 123)
if (code2 === code1 + 1 && code3 === code2 + 1) {
return {
isValid: false,
errorMessage: t('account.passwordNoConsecutiveSequence')
};
}
// 연속 감소하는 패턴 (cba, 321)
if (code2 === code1 - 1 && code3 === code2 - 1) {
return {
isValid: false,
errorMessage: t('account.passwordNoConsecutiveSequence')
};
}
// 2) 대소문자 구분 없이 검사 (예: dFG -> dfg)
const lower1 = char1.toLowerCase();
const lower2 = char2.toLowerCase();
const lower3 = char3.toLowerCase();
const lowerCode1 = lower1.charCodeAt(0);
const lowerCode2 = lower2.charCodeAt(0);
const lowerCode3 = lower3.charCodeAt(0);
// 소문자 버전에서 연속 증가하는 패턴
if (lowerCode2 === lowerCode1 + 1 && lowerCode3 === lowerCode2 + 1) {
return {
isValid: false,
errorMessage: t('account.passwordNoConsecutiveSequence')
};
}
// 소문자 버전에서 연속 감소하는 패턴
if (lowerCode2 === lowerCode1 - 1 && lowerCode3 === lowerCode2 - 1) {
return {
isValid: false,
errorMessage: t('account.passwordNoConsecutiveSequence')
};
}
// 3) shift 미적용 버전 검사 (예: !2# -> 123)
const unshifted1 = unshiftChar(char1);
const unshifted2 = unshiftChar(char2);
const unshifted3 = unshiftChar(char3);
const unshiftedCode1 = unshifted1.charCodeAt(0);
const unshiftedCode2 = unshifted2.charCodeAt(0);
const unshiftedCode3 = unshifted3.charCodeAt(0);
// shift 미적용 버전에서 연속 증가하는 패턴
if (unshiftedCode2 === unshiftedCode1 + 1 && unshiftedCode3 === unshiftedCode2 + 1) {
return {
isValid: false,
errorMessage: t('account.passwordNoConsecutiveSequence')
};
}
// shift 미적용 버전에서 연속 감소하는 패턴
if (unshiftedCode2 === unshiftedCode1 - 1 && unshiftedCode3 === unshiftedCode2 - 1) {
return {
isValid: false,
errorMessage: t('account.passwordNoConsecutiveSequence')
};
}
// 4) shift 미적용 + 소문자 버전 검사 (예: !@C -> 123 -> abc 형태도 체크)
const unshiftedLower1 = unshiftChar(lower1);
const unshiftedLower2 = unshiftChar(lower2);
const unshiftedLower3 = unshiftChar(lower3);
const unshiftedLowerCode1 = unshiftedLower1.charCodeAt(0);
const unshiftedLowerCode2 = unshiftedLower2.charCodeAt(0);
const unshiftedLowerCode3 = unshiftedLower3.charCodeAt(0);
// shift 미적용 + 소문자에서 연속 증가하는 패턴
if (unshiftedLowerCode2 === unshiftedLowerCode1 + 1 && unshiftedLowerCode3 === unshiftedLowerCode2 + 1) {
return {
isValid: false,
errorMessage: t('account.passwordNoConsecutiveSequence')
};
}
// shift 미적용 + 소문자에서 연속 감소하는 패턴
if (unshiftedLowerCode2 === unshiftedLowerCode1 - 1 && unshiftedLowerCode3 === unshiftedLowerCode2 - 1) {
return {
isValid: false,
errorMessage: t('account.passwordNoConsecutiveSequence')
};
}
}
// 6. 키보드 순서 3자리 연속 금지 (대소문자 구분 없이 검사, shift 문자 포함)
for (let i = 0; i < password.length - 2; i++) {
const threeChars = password.substring(i, i + 3);
// 원본 문자 (소문자)
const threeCharsLower = threeChars.toLowerCase();
const threeCharsReversedLower = threeChars.split('').reverse().join('').toLowerCase();
// shift 미적용 버전 (소문자)
const threeCharsUnshifted = threeChars.split('').map(unshiftChar).join('').toLowerCase();
const threeCharsUnshiftedReversed = threeChars.split('').reverse().map(unshiftChar).join('').toLowerCase();
for (const sequence of KEYBOARD_SEQUENCES) {
const sequenceLower = sequence.toLowerCase();
// 원본 문자 검사
if (sequenceLower.includes(threeCharsLower) || sequenceLower.includes(threeCharsReversedLower)) {
return {
isValid: false,
errorMessage: t('account.passwordNoKeyboardSequence')
};
}
// shift 미적용 버전 검사 (예: !@# -> 123)
if (sequenceLower.includes(threeCharsUnshifted) || sequenceLower.includes(threeCharsUnshiftedReversed)) {
return {
isValid: false,
errorMessage: t('account.passwordNoKeyboardSequence')
};
}
}
}
return {
isValid: true,
errorMessage: ''
};
};