- 이름,이메일,계좌번호 등 마스킹정책 적용
- 권한 체크 오 기입 수정 - 다국어 누락 부분 수정
This commit is contained in:
66
src/shared/lib/masking-utils.ts
Normal file
66
src/shared/lib/masking-utils.ts
Normal 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;
|
||||
};
|
||||
8
src/shared/ui/masked-input/index.ts
Normal file
8
src/shared/ui/masked-input/index.ts
Normal 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';
|
||||
54
src/shared/ui/masked-input/masked-account-number-input.tsx
Normal file
54
src/shared/ui/masked-input/masked-account-number-input.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
61
src/shared/ui/masked-input/masked-email-input.tsx
Normal file
61
src/shared/ui/masked-input/masked-email-input.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
66
src/shared/ui/masked-input/masked-name-input.tsx
Normal file
66
src/shared/ui/masked-input/masked-name-input.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
59
src/shared/ui/masked-input/masked-phone-input.tsx
Normal file
59
src/shared/ui/masked-input/masked-phone-input.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user