- 이름,이메일,계좌번호 등 마스킹정책 적용
- 권한 체크 오 기입 수정 - 다국어 누락 부분 수정
This commit is contained in:
@@ -6,6 +6,7 @@ import { CashReceiptPurposeType } from '../model/types';
|
||||
import { PatternFormat } from 'react-number-format';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useKeyboardAware } from '@/shared/lib/hooks/use-keyboard-aware';
|
||||
import { MaskedNameInput, MaskedEmailInput, MaskedPhoneInput } from '@/shared/ui/masked-input';
|
||||
|
||||
export interface CashReceiptHandWrittenIssuanceStep1Props {
|
||||
businessNumber?: string;
|
||||
@@ -95,11 +96,10 @@ export const CashReceiptHandWrittenIssuanceStep1 = ({
|
||||
<div className="issue-row">
|
||||
<div className="issue-label">{t('transaction.fields.buyer')}</div>
|
||||
<div className="issue-field">
|
||||
<input
|
||||
type="text"
|
||||
<MaskedNameInput
|
||||
value={buyerName || ''}
|
||||
onChange={setBuyerName}
|
||||
placeholder={t('transaction.handWrittenIssuance.buyerNamePlaceholder')}
|
||||
value={buyerName}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setBuyerName(e.target.value)}
|
||||
onFocus={handleInputFocus}
|
||||
/>
|
||||
</div>
|
||||
@@ -120,11 +120,10 @@ export const CashReceiptHandWrittenIssuanceStep1 = ({
|
||||
<div className="issue-row">
|
||||
<div className="issue-label">{t('account.emailAddress')}</div>
|
||||
<div className="issue-field">
|
||||
<input
|
||||
type="email"
|
||||
<MaskedEmailInput
|
||||
value={email || ''}
|
||||
onChange={setEmail}
|
||||
placeholder={t('transaction.handWrittenIssuance.emailPlaceholder')}
|
||||
value={email}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)}
|
||||
onFocus={handleInputFocus}
|
||||
/>
|
||||
</div>
|
||||
@@ -132,14 +131,12 @@ export const CashReceiptHandWrittenIssuanceStep1 = ({
|
||||
<div className="issue-row" style={keyboardAwarePadding}>
|
||||
<div className="issue-label">{t('account.phoneNumber')}</div>
|
||||
<div className="issue-field">
|
||||
<PatternFormat
|
||||
<MaskedPhoneInput
|
||||
value={phoneNumber || ''}
|
||||
onChange={setPhoneNumber}
|
||||
placeholder={t('transaction.handWrittenIssuance.phoneNumberPlaceholder')}
|
||||
value={phoneNumber}
|
||||
valueIsNumericString
|
||||
format="###########"
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setPhoneNumber(e.target.value)}
|
||||
onFocus={handleInputFocus}
|
||||
></PatternFormat>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -774,7 +774,23 @@
|
||||
"billing": {
|
||||
"title": "Billing",
|
||||
"detailTitle": "Billing Details",
|
||||
"charge": "Billing Charge"
|
||||
"charge": "Billing Charge",
|
||||
"paymentInfoInput": "Enter Payment Information",
|
||||
"billKey": "Bill Key",
|
||||
"productAmount": "Product Amount",
|
||||
"paymentRequestDate": "Payment Request Date",
|
||||
"selectDate": "Select Date",
|
||||
"installmentMonth": "Installment Months",
|
||||
"lumpSum": "Lump Sum",
|
||||
"months": "{{count}} Months",
|
||||
"chargeRequest": "Request Payment",
|
||||
"billKeyRequired": "Bill key is required.",
|
||||
"productNameRequired": "Product name is required.",
|
||||
"productAmountRequired": "Product amount is required.",
|
||||
"productAmountMinimum": "Product amount must be greater than 0.",
|
||||
"orderNumberRequired": "Order number is required.",
|
||||
"buyerNameRequired": "Buyer name is required.",
|
||||
"chargeSuccess": "Payment request was successful."
|
||||
},
|
||||
"payment": {
|
||||
"title": "Payment Management",
|
||||
|
||||
@@ -774,7 +774,23 @@
|
||||
"billing": {
|
||||
"title": "빌링",
|
||||
"detailTitle": "빌링 상세",
|
||||
"charge": "빌링 청구"
|
||||
"charge": "빌링 청구",
|
||||
"paymentInfoInput": "결제 정보 입력",
|
||||
"billKey": "빌키",
|
||||
"productAmount": "상품금액",
|
||||
"paymentRequestDate": "결제 요청일자",
|
||||
"selectDate": "날짜 선택",
|
||||
"installmentMonth": "할부 개월",
|
||||
"lumpSum": "일시불",
|
||||
"months": "{{count}}개월",
|
||||
"chargeRequest": "결제 신청",
|
||||
"billKeyRequired": "빌키는 필수 입력 항목입니다.",
|
||||
"productNameRequired": "상품명은 필수 입력 항목입니다.",
|
||||
"productAmountRequired": "상품금액은 필수 입력 항목입니다.",
|
||||
"productAmountMinimum": "상품금액은 0보다 커야 합니다.",
|
||||
"orderNumberRequired": "주문번호는 필수 입력 항목입니다.",
|
||||
"buyerNameRequired": "구매자명은 필수 입력 항목입니다.",
|
||||
"chargeSuccess": "결제 신청을 성공하였습니다."
|
||||
},
|
||||
"payment": {
|
||||
"title": "결제 관리",
|
||||
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
import { useExtensionAccountHolderSearchRequestMutation } from '@/entities/additional-service/api/account-holder-search/use-extension-account-holder-search-reqeust-mutation';
|
||||
import { ExtensionAccountHolderSearchRequestParams, ExtensionAccountHolderSearchRequestResponse } from '@/entities/additional-service/model/account-holder-search/types';
|
||||
import { useStore } from '@/shared/model/store';
|
||||
import { NumericFormat } from 'react-number-format';
|
||||
import { snackBar } from '@/shared/lib';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { showAlert } from '@/widgets/show-alert';
|
||||
import { AccountHolderSearchDetail } from '@/entities/additional-service/ui/account-holder-search/detail/account-holder-search-detail';
|
||||
import { MaskedAccountNumberInput } from '@/shared/ui/masked-input';
|
||||
|
||||
export const AccountHolderSearchRequestPage = () => {
|
||||
const { navigate } = useNavigate();
|
||||
@@ -145,18 +145,11 @@ export const AccountHolderSearchRequestPage = () => {
|
||||
<div className="billing-row">
|
||||
<div className="billing-label">{t('transaction.fields.accountNo')}</div>
|
||||
<div className="billing-field">
|
||||
<NumericFormat
|
||||
<MaskedAccountNumberInput
|
||||
value={formData.accountNo}
|
||||
valueIsNumericString
|
||||
allowNegative={false}
|
||||
decimalScale={0}
|
||||
isAllowed={(values) => {
|
||||
const { value } = values;
|
||||
return !value || value.length <= 14;
|
||||
}}
|
||||
onValueChange={(values) => {
|
||||
setFormData({ ...formData, accountNo: values.value });
|
||||
}}
|
||||
placeholder=''
|
||||
onChange={(value) => setFormData({ ...formData, accountNo: value })}
|
||||
maxLength={14}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from '@/widgets/sub-layout/use-sub-layout';
|
||||
import { useStore } from '@/shared/model/store';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PatternFormat } from 'react-number-format';
|
||||
import { overlay } from 'overlay-kit';
|
||||
import { Dialog } from '@/shared/ui/dialogs/dialog';
|
||||
import { QnaSaveParams, QnaSaveResponse } from '@/entities/support/model/types';
|
||||
@@ -19,6 +18,7 @@ import { checkGrant } from '@/shared/lib/check-grant';
|
||||
import { useKeyboardAware } from '@/shared/lib/hooks/use-keyboard-aware';
|
||||
import { snackBar } from '@/shared/lib';
|
||||
import { showAlert } from '@/widgets/show-alert';
|
||||
import { MaskedNameInput, MaskedPhoneInput, MaskedEmailInput } from '@/shared/ui/masked-input';
|
||||
|
||||
export enum QnaRegisterPropsName {
|
||||
Mid = 'Mid',
|
||||
@@ -189,11 +189,10 @@ export const QnaRegisterPage = () => {
|
||||
{t('support.qna.formLabels.requesterName')} <span className="red">{t('support.qna.formLabels.required')}</span>
|
||||
</div>
|
||||
<div className="inq-control">
|
||||
<input
|
||||
type="text"
|
||||
<MaskedNameInput
|
||||
value={requestName}
|
||||
required={true}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setInputValue(e, QnaRegisterPropsName.RequestName)}
|
||||
placeholder=''
|
||||
onChange={setRequestName}
|
||||
onFocus={handleInputFocus}
|
||||
/>
|
||||
</div>
|
||||
@@ -203,24 +202,21 @@ export const QnaRegisterPage = () => {
|
||||
{t('support.qna.formLabels.phoneNumber')} <span className="red">{t('support.qna.formLabels.required')}</span>
|
||||
</div>
|
||||
<div className="inq-control">
|
||||
<PatternFormat
|
||||
<MaskedPhoneInput
|
||||
placeholder={t('support.qna.formLabels.phonePlaceholder')}
|
||||
value={requestTel}
|
||||
valueIsNumericString
|
||||
format="###########"
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setInputValue(e, QnaRegisterPropsName.RequestTel)}
|
||||
onChange={setRequestTel}
|
||||
onFocus={handleInputFocus}
|
||||
></PatternFormat>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="inq-field">
|
||||
<div className="inq-label">{t('support.qna.formLabels.emailAddress')}</div>
|
||||
<div className="inq-control">
|
||||
<input
|
||||
type="email"
|
||||
<MaskedEmailInput
|
||||
placeholder={t('support.qna.formLabels.emailPlaceholder')}
|
||||
value={requestEmail}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setInputValue(e, QnaRegisterPropsName.RequestEmail)}
|
||||
onChange={setRequestEmail}
|
||||
onFocus={handleInputFocus}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { notiBar, snackBar } from '@/shared/lib';
|
||||
import { BillingChargeParams, BillingChargeResponse } from '@/entities/transaction/model/types';
|
||||
import { useKeyboardAware } from '@/shared/lib/hooks/use-keyboard-aware';
|
||||
import { checkGrant } from '@/shared/lib/check-grant';
|
||||
import { MaskedNameInput } from '@/shared/ui/masked-input';
|
||||
|
||||
const menuId = 34;
|
||||
export const BillingChargePage = () => {
|
||||
@@ -56,23 +57,23 @@ export const BillingChargePage = () => {
|
||||
const onClickToBillingCharge = () => {
|
||||
if(checkGrant(menuId, 'W')){
|
||||
if (!billKey) {
|
||||
showAlert('빌키는 필수 입력 항목입니다.');
|
||||
showAlert(t('billing.billKeyRequired'));
|
||||
return;
|
||||
}
|
||||
else if (!productName) {
|
||||
showAlert('상품명은 필수 입력 항목입니다.');
|
||||
showAlert(t('billing.productNameRequired'));
|
||||
}
|
||||
else if (!productAmount) {
|
||||
showAlert('상품금액은 필수 입력 항목입니다.');
|
||||
showAlert(t('billing.productAmountRequired'));
|
||||
}
|
||||
else if (productAmount <= 0) {
|
||||
showAlert('상품금액은 0보다 커야 합니다.');
|
||||
showAlert(t('billing.productAmountMinimum'));
|
||||
}
|
||||
else if (!orderNumber) {
|
||||
showAlert('주문번호는 필수 입력 항목입니다.');
|
||||
showAlert(t('billing.orderNumberRequired'));
|
||||
}
|
||||
else if (!buyerName) {
|
||||
showAlert('구매자명은 필수 입력 항목입니다.');
|
||||
showAlert(t('billing.buyerNameRequired'));
|
||||
}
|
||||
|
||||
let params: BillingChargeParams = {
|
||||
@@ -85,7 +86,7 @@ export const BillingChargePage = () => {
|
||||
installmentMonth: installmentMonth
|
||||
};
|
||||
billingCharge(params).then((rs: BillingChargeResponse) => {
|
||||
snackBar('결제 신청을 성공하였습니다.', function () {
|
||||
snackBar(t('billing.chargeSuccess'), function () {
|
||||
navigate(PATHS.transaction.billing.list);
|
||||
}, 3000);
|
||||
|
||||
@@ -98,8 +99,8 @@ export const BillingChargePage = () => {
|
||||
}
|
||||
else{
|
||||
showAlert(t('common.nopermission'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const onChangeBillKey = (value: string) => {
|
||||
@@ -116,13 +117,13 @@ export const BillingChargePage = () => {
|
||||
<option
|
||||
key={`key-installment`}
|
||||
value=''
|
||||
>선택</option>
|
||||
>{t('common.select')}</option>
|
||||
);
|
||||
rs.push(
|
||||
<option
|
||||
key={`key-installment-0`}
|
||||
value='00'
|
||||
>일시불</option>
|
||||
>{t('billing.lumpSum')}</option>
|
||||
);
|
||||
for (let i = 2; i <= 33; i++) {
|
||||
let val = (i < 10) ? '0' + i : '' + i;
|
||||
@@ -130,7 +131,7 @@ export const BillingChargePage = () => {
|
||||
<option
|
||||
key={`key-installment-${i}`}
|
||||
value={val}
|
||||
>{i}개월</option>
|
||||
>{t('billing.months', { count: i })}</option>
|
||||
);
|
||||
};
|
||||
return rs;
|
||||
@@ -142,10 +143,10 @@ export const BillingChargePage = () => {
|
||||
<div className="tab-content">
|
||||
<div className="tab-pane sub active">
|
||||
<div className="option-list">
|
||||
<div className="billing-title">결제 정보 입력</div>
|
||||
<div className="billing-title">{t('billing.paymentInfoInput')}</div>
|
||||
<div className="billing-form">
|
||||
<div className="billing-row">
|
||||
<div className="billing-label">빌키 <span>*</span></div>
|
||||
<div className="billing-label">{t('billing.billKey')} <span>*</span></div>
|
||||
<div className="billing-field">
|
||||
<input
|
||||
type="text"
|
||||
@@ -156,7 +157,7 @@ export const BillingChargePage = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="billing-row">
|
||||
<div className="billing-label">상품명 <span>*</span></div>
|
||||
<div className="billing-label">{t('transaction.fields.productName')} <span>*</span></div>
|
||||
<div className="billing-field">
|
||||
<input
|
||||
type="text"
|
||||
@@ -167,7 +168,7 @@ export const BillingChargePage = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="billing-row">
|
||||
<div className="billing-label">상품금액 <span>*</span></div>
|
||||
<div className="billing-label">{t('billing.productAmount')} <span>*</span></div>
|
||||
<div className="billing-field">
|
||||
<NumericFormat
|
||||
value={productAmount}
|
||||
@@ -179,7 +180,7 @@ export const BillingChargePage = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="billing-row">
|
||||
<div className="billing-label">주문번호 <span>*</span></div>
|
||||
<div className="billing-label">{t('transaction.fields.orderNumber')} <span>*</span></div>
|
||||
<div className="billing-field">
|
||||
<input
|
||||
type="text"
|
||||
@@ -190,24 +191,24 @@ export const BillingChargePage = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="billing-row">
|
||||
<div className="billing-label">구매자명 <span>*</span></div>
|
||||
<div className="billing-label">{t('transaction.fields.buyer')} <span>*</span></div>
|
||||
<div className="billing-field">
|
||||
<input
|
||||
type="text"
|
||||
<MaskedNameInput
|
||||
value={buyerName}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setBuyerName(e.target.value)}
|
||||
placeholder=''
|
||||
onChange={setBuyerName}
|
||||
onFocus={handleInputFocus}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="billing-row">
|
||||
<div className="billing-label">결제 요청일자</div>
|
||||
<div className="billing-label">{t('billing.paymentRequestDate')}</div>
|
||||
<div className="billing-field">
|
||||
<div className="input-wrapper date wid-100">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="날짜 선택"
|
||||
placeholder={t('billing.selectDate')}
|
||||
value={paymentRequestDate}
|
||||
readOnly={true}
|
||||
/>
|
||||
@@ -226,7 +227,7 @@ export const BillingChargePage = () => {
|
||||
</div>
|
||||
|
||||
<div className="billing-row" style={keyboardAwarePadding}>
|
||||
<div className="billing-label">할부 개월</div>
|
||||
<div className="billing-label">{t('billing.installmentMonth')}</div>
|
||||
<div className="billing-field">
|
||||
<select
|
||||
value={installmentMonth}
|
||||
@@ -242,7 +243,7 @@ export const BillingChargePage = () => {
|
||||
<button
|
||||
className="btn-50 btn-blue flex-1"
|
||||
onClick={() => onClickToBillingCharge()}
|
||||
>결제 신청</button>
|
||||
>{t('billing.chargeRequest')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -247,7 +247,7 @@ export const CashReceiptListPage = () => {
|
||||
setTransactionType(val);
|
||||
};
|
||||
const onClickToNavigate = () => {
|
||||
if(checkGrant(menuId, 'X')){
|
||||
if(checkGrant(menuId, 'W')){
|
||||
navigate(PATHS.transaction.cashReceipt.handWrittenIssuance);
|
||||
}
|
||||
else{
|
||||
|
||||
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