비밀번호 변경 기능 개선 및 검증 강화
- 비밀번호 변경 페이지에 확인 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:
@@ -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
41
src/shared/lib/masking.ts
Normal 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}`;
|
||||
};
|
||||
259
src/shared/lib/password-validation.ts
Normal file
259
src/shared/lib/password-validation.ts
Normal 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: ''
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user