- 이름,이메일,계좌번호 등 마스킹정책 적용

- 권한 체크 오 기입 수정
- 다국어 누락 부분 수정
This commit is contained in:
HyeonJongKim
2025-11-17 19:14:56 +09:00
parent fd5333e4a2
commit 4e1baffb13
13 changed files with 400 additions and 67 deletions

View File

@@ -0,0 +1,66 @@
/**
* 마스킹 유틸리티 함수
* 개인정보 보호를 위한 데이터 마스킹 처리
*/
/**
* 이름 마스킹
* - 1글자: 그대로 표시
* - 2글자: 첫 글자 + * (홍*, j*)
* - 3글자: 첫 글자 + * + 마지막 글자 (홍*동, j*n)
* - 4글자 이상: 첫 글자 + *** + 마지막 글자 (선**녀, j***n)
*/
export const getMaskedName = (name?: string): string => {
if (!name) return '';
const length = name.length;
if (length <= 1) return name;
if (length === 2) return name[0] + '*';
if (length === 3) return name[0] + '*' + name[2];
const firstChar = name[0];
const lastChar = name[length - 1];
const maskedLength = length - 2;
const masked = '*'.repeat(maskedLength);
return firstChar + masked + lastChar;
};
/**
* 전화번호 마스킹
* - 마지막 4자리 마스킹
* - 예: 0101234****
*/
export const getMaskedPhoneNumber = (phone?: string): string => {
if (!phone) return '';
if (phone.length <= 7) return phone;
const visiblePart = phone.slice(0, -4);
return visiblePart + '****';
};
/**
* 이메일 마스킹
* - 앞 2자리만 표시, @ 이전 부분 마스킹
* - 예: te**@nicepay.co.kr
*/
export const getMaskedEmail = (email?: string): string => {
if (!email) return '';
const atIndex = email.indexOf('@');
if (atIndex === -1 || atIndex <= 2) return email;
const visiblePart = email.slice(0, 2);
const domainPart = email.slice(atIndex);
const maskedLength = atIndex - 2;
const masked = '*'.repeat(maskedLength);
return visiblePart + masked + domainPart;
};
/**
* 계좌번호 마스킹
* - 마지막 5자리만 표시
* - 예: *********74018
*/
export const getMaskedAccountNumber = (accountNo?: string): string => {
if (!accountNo) return '';
if (accountNo.length <= 5) return accountNo;
const visiblePart = accountNo.slice(-5);
const maskedLength = accountNo.length - 5;
const masked = '*'.repeat(maskedLength);
return masked + visiblePart;
};

View File

@@ -0,0 +1,8 @@
export { MaskedNameInput } from './masked-name-input';
export { MaskedPhoneInput } from './masked-phone-input';
export { MaskedEmailInput } from './masked-email-input';
export { MaskedAccountNumberInput } from './masked-account-number-input';
export type { MaskedNameInputProps } from './masked-name-input';
export type { MaskedPhoneInputProps } from './masked-phone-input';
export type { MaskedEmailInputProps } from './masked-email-input';
export type { MaskedAccountNumberInputProps } from './masked-account-number-input';

View File

@@ -0,0 +1,54 @@
import { ChangeEvent } from 'react';
import { getMaskedAccountNumber } from '@/shared/lib/masking-utils';
export interface MaskedAccountNumberInputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
className?: string;
maxLength?: number;
}
export const MaskedAccountNumberInput = ({
value,
onChange,
placeholder = '*********74018',
onFocus,
className = '',
maxLength = 14
}: MaskedAccountNumberInputProps) => {
return (
<input
type="tel"
className={className}
value={getMaskedAccountNumber(value)}
placeholder={placeholder}
onKeyDown={(e) => {
// 백스페이스 처리
if (e.key === 'Backspace') {
e.preventDefault();
onChange(value.slice(0, -1));
}
// 숫자 입력 처리
else if (/^[0-9]$/.test(e.key)) {
e.preventDefault();
if (value.length < maxLength) {
onChange(value + e.key);
}
}
}}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
// Android 등에서 한글 키보드로 입력 시 대비
const input = e.target.value;
const digitsOnly = input.replace(/\*/g, '').replace(/[^0-9]/g, '');
if (digitsOnly.length <= maxLength) {
onChange(digitsOnly);
}
}}
inputMode="numeric"
pattern="[0-9]*"
onFocus={onFocus}
/>
);
};

View File

@@ -0,0 +1,61 @@
import { ChangeEvent } from 'react';
import { getMaskedEmail } from '@/shared/lib/masking-utils';
export interface MaskedEmailInputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
className?: string;
isValid?: (value: string) => boolean;
}
export const MaskedEmailInput = ({
value,
onChange,
placeholder = 'te**@nicepay.co.kr',
onFocus,
className = '',
isValid
}: MaskedEmailInputProps) => {
const validationClass = isValid && value ? (isValid(value) ? '' : 'error') : '';
return (
<input
type="text"
className={`${className} ${validationClass}`.trim()}
value={getMaskedEmail(value)}
placeholder={placeholder}
onKeyDown={(e) => {
// 백스페이스 처리
if (e.key === 'Backspace') {
e.preventDefault();
onChange(value.slice(0, -1));
}
// 영문, 숫자, @, ., - 등 이메일에 허용되는 문자만 입력 가능
else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
if (/^[a-zA-Z0-9@._-]$/.test(e.key)) {
e.preventDefault();
onChange(value + e.key);
} else {
e.preventDefault();
}
}
}}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
// 복사/붙여넣기 등 특수 입력 대비
const input = e.target.value;
const filteredInput = input.replace(/[^a-zA-Z0-9@._-]/g, '');
const atIndex = filteredInput.indexOf('@');
if (atIndex !== -1) {
const beforeAt = filteredInput.slice(0, atIndex).replace(/\*/g, '');
const afterAt = filteredInput.slice(atIndex);
onChange(beforeAt + afterAt);
} else {
onChange(filteredInput.replace(/\*/g, ''));
}
}}
onFocus={onFocus}
/>
);
};

View File

@@ -0,0 +1,66 @@
import { ChangeEvent, useState } from 'react';
import { getMaskedName } from '@/shared/lib/masking-utils';
export interface MaskedNameInputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
className?: string;
}
export const MaskedNameInput = ({
value,
onChange,
placeholder = '홍*동',
onFocus,
className = ''
}: MaskedNameInputProps) => {
const [isComposing, setIsComposing] = useState<boolean>(false);
return (
<input
type="text"
className={className}
value={isComposing ? value : getMaskedName(value)}
placeholder={placeholder}
onKeyDown={(e) => {
// 스페이스바 입력 차단
if (e.key === ' ') {
e.preventDefault();
}
// 백스페이스 처리 (composition 중이 아닐 때만)
else if (e.key === 'Backspace' && !isComposing) {
e.preventDefault();
onChange(value.slice(0, -1));
}
// 영문자 입력 처리 (composition 중이 아닐 때만)
else if (!isComposing && e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
if (/^[a-zA-Z]$/.test(e.key)) {
e.preventDefault();
onChange(value + e.key);
}
// 한글이 아닌 다른 문자는 차단
else if (!/^[ㄱ-ㅎㅏ-ㅣ가-힣]$/.test(e.key)) {
e.preventDefault();
}
}
}}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={(e: any) => {
setIsComposing(false);
// 스페이스 제거 후 저장
const valueWithoutSpace = e.target.value.replace(/\s/g, '');
onChange(valueWithoutSpace);
}}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
if (isComposing) {
// composition 중에는 스페이스 제거
const valueWithoutSpace = e.target.value.replace(/\s/g, '');
onChange(valueWithoutSpace);
}
}}
onFocus={onFocus}
/>
);
};

View File

@@ -0,0 +1,59 @@
import { ChangeEvent } from 'react';
import { getMaskedPhoneNumber } from '@/shared/lib/masking-utils';
export interface MaskedPhoneInputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
className?: string;
maxLength?: number;
isValid?: (value: string) => boolean;
}
export const MaskedPhoneInput = ({
value,
onChange,
placeholder = '0101234****',
onFocus,
className = '',
maxLength = 11,
isValid
}: MaskedPhoneInputProps) => {
const validationClass = isValid && value ? (isValid(value) ? '' : 'error') : '';
return (
<input
type="tel"
className={`${className} ${validationClass}`.trim()}
value={getMaskedPhoneNumber(value)}
placeholder={placeholder}
onKeyDown={(e) => {
// 백스페이스 처리
if (e.key === 'Backspace') {
e.preventDefault();
onChange(value.slice(0, -1));
}
// 숫자 입력 처리
else if (/^[0-9]$/.test(e.key)) {
e.preventDefault();
if (value.length < maxLength) {
onChange(value + e.key);
}
}
}}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
// Android 등에서 한글 키보드로 입력 시 대비
const input = e.target.value;
const digitsOnly = input.replace(/\*/g, '').replace(/[^0-9]/g, '');
if (digitsOnly.length <= maxLength) {
onChange(digitsOnly);
}
}}
inputMode="numeric"
pattern="[0-9]*"
maxLength={15}
onFocus={onFocus}
/>
);
};