From 4e1baffb1377acc93876a53b6858d5686d20904d Mon Sep 17 00:00:00 2001 From: HyeonJongKim Date: Mon, 17 Nov 2025 19:14:56 +0900 Subject: [PATCH] =?UTF-8?q?-=20=EC=9D=B4=EB=A6=84,=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC,=EA=B3=84=EC=A2=8C=EB=B2=88=ED=98=B8=20=EB=93=B1=20?= =?UTF-8?q?=EB=A7=88=EC=8A=A4=ED=82=B9=EC=A0=95=EC=B1=85=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20-=20=EA=B6=8C=ED=95=9C=20=EC=B2=B4=ED=81=AC=20?= =?UTF-8?q?=EC=98=A4=20=EA=B8=B0=EC=9E=85=20=EC=88=98=EC=A0=95=20-=20?= =?UTF-8?q?=EB=8B=A4=EA=B5=AD=EC=96=B4=20=EB=88=84=EB=9D=BD=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...sh-receipt-hand-written-issuance-step1.tsx | 25 ++++--- src/locales/en.json | 18 ++++- src/locales/ko.json | 18 ++++- .../account-holder-search/request-page.tsx | 17 ++--- src/pages/support/qna/register-page.tsx | 22 +++---- src/pages/transaction/billing/charge-page.tsx | 51 +++++++------- .../transaction/cash-receipt/list-page.tsx | 2 +- src/shared/lib/masking-utils.ts | 66 +++++++++++++++++++ src/shared/ui/masked-input/index.ts | 8 +++ .../masked-account-number-input.tsx | 54 +++++++++++++++ .../ui/masked-input/masked-email-input.tsx | 61 +++++++++++++++++ .../ui/masked-input/masked-name-input.tsx | 66 +++++++++++++++++++ .../ui/masked-input/masked-phone-input.tsx | 59 +++++++++++++++++ 13 files changed, 400 insertions(+), 67 deletions(-) create mode 100644 src/shared/lib/masking-utils.ts create mode 100644 src/shared/ui/masked-input/index.ts create mode 100644 src/shared/ui/masked-input/masked-account-number-input.tsx create mode 100644 src/shared/ui/masked-input/masked-email-input.tsx create mode 100644 src/shared/ui/masked-input/masked-name-input.tsx create mode 100644 src/shared/ui/masked-input/masked-phone-input.tsx diff --git a/src/entities/transaction/ui/cash-receipt-hand-written-issuance-step1.tsx b/src/entities/transaction/ui/cash-receipt-hand-written-issuance-step1.tsx index fea081d..558d1a3 100644 --- a/src/entities/transaction/ui/cash-receipt-hand-written-issuance-step1.tsx +++ b/src/entities/transaction/ui/cash-receipt-hand-written-issuance-step1.tsx @@ -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 = ({
{t('transaction.fields.buyer')}
- ) => setBuyerName(e.target.value)} onFocus={handleInputFocus} />
@@ -120,11 +120,10 @@ export const CashReceiptHandWrittenIssuanceStep1 = ({
{t('account.emailAddress')}
- ) => setEmail(e.target.value)} onFocus={handleInputFocus} />
@@ -132,14 +131,12 @@ export const CashReceiptHandWrittenIssuanceStep1 = ({
{t('account.phoneNumber')}
- ) => setPhoneNumber(e.target.value)} onFocus={handleInputFocus} - > + />
diff --git a/src/locales/en.json b/src/locales/en.json index 8bfb730..c5e7a8a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -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", diff --git a/src/locales/ko.json b/src/locales/ko.json index 34cded2..5815dce 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -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": "결제 관리", diff --git a/src/pages/additional-service/account-holder-search/request-page.tsx b/src/pages/additional-service/account-holder-search/request-page.tsx index c72dd72..235f226 100644 --- a/src/pages/additional-service/account-holder-search/request-page.tsx +++ b/src/pages/additional-service/account-holder-search/request-page.tsx @@ -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 = () => {
{t('transaction.fields.accountNo')}
- { - const { value } = values; - return !value || value.length <= 14; - }} - onValueChange={(values) => { - setFormData({ ...formData, accountNo: values.value }); - }} + placeholder='' + onChange={(value) => setFormData({ ...formData, accountNo: value })} + maxLength={14} />
diff --git a/src/pages/support/qna/register-page.tsx b/src/pages/support/qna/register-page.tsx index b1223f2..35439b6 100644 --- a/src/pages/support/qna/register-page.tsx +++ b/src/pages/support/qna/register-page.tsx @@ -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')} {t('support.qna.formLabels.required')}
- ) => setInputValue(e, QnaRegisterPropsName.RequestName)} + placeholder='' + onChange={setRequestName} onFocus={handleInputFocus} />
@@ -203,24 +202,21 @@ export const QnaRegisterPage = () => { {t('support.qna.formLabels.phoneNumber')} {t('support.qna.formLabels.required')}
- ) => setInputValue(e, QnaRegisterPropsName.RequestTel)} + onChange={setRequestTel} onFocus={handleInputFocus} - > + />
{t('support.qna.formLabels.emailAddress')}
- ) => setInputValue(e, QnaRegisterPropsName.RequestEmail)} + onChange={setRequestEmail} onFocus={handleInputFocus} />
diff --git a/src/pages/transaction/billing/charge-page.tsx b/src/pages/transaction/billing/charge-page.tsx index c2f0471..0560ec0 100644 --- a/src/pages/transaction/billing/charge-page.tsx +++ b/src/pages/transaction/billing/charge-page.tsx @@ -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 = () => { + >{t('common.select')} ); rs.push( + >{t('billing.lumpSum')} ); for (let i = 2; i <= 33; i++) { let val = (i < 10) ? '0' + i : '' + i; @@ -130,7 +131,7 @@ export const BillingChargePage = () => { + >{t('billing.months', { count: i })} ); }; return rs; @@ -142,10 +143,10 @@ export const BillingChargePage = () => {
-
결제 정보 입력
+
{t('billing.paymentInfoInput')}
-
빌키 *
+
{t('billing.billKey')} *
{
-
상품명 *
+
{t('transaction.fields.productName')} *
{
-
상품금액 *
+
{t('billing.productAmount')} *
{
-
주문번호 *
+
{t('transaction.fields.orderNumber')} *
{
-
구매자명 *
+
{t('transaction.fields.buyer')} *
- ) => setBuyerName(e.target.value)} + placeholder='' + onChange={setBuyerName} onFocus={handleInputFocus} />
-
결제 요청일자
+
{t('billing.paymentRequestDate')}
@@ -226,7 +227,7 @@ export const BillingChargePage = () => {
-
할부 개월
+
{t('billing.installmentMonth')}
{ + // 백스페이스 처리 + 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) => { + // 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} + /> + ); +}; diff --git a/src/shared/ui/masked-input/masked-email-input.tsx b/src/shared/ui/masked-input/masked-email-input.tsx new file mode 100644 index 0000000..c65ff43 --- /dev/null +++ b/src/shared/ui/masked-input/masked-email-input.tsx @@ -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) => 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 ( + { + // 백스페이스 처리 + 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) => { + // 복사/붙여넣기 등 특수 입력 대비 + 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} + /> + ); +}; diff --git a/src/shared/ui/masked-input/masked-name-input.tsx b/src/shared/ui/masked-input/masked-name-input.tsx new file mode 100644 index 0000000..c0b615e --- /dev/null +++ b/src/shared/ui/masked-input/masked-name-input.tsx @@ -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) => void; + className?: string; +} + +export const MaskedNameInput = ({ + value, + onChange, + placeholder = '홍*동', + onFocus, + className = '' +}: MaskedNameInputProps) => { + const [isComposing, setIsComposing] = useState(false); + + return ( + { + // 스페이스바 입력 차단 + 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) => { + if (isComposing) { + // composition 중에는 스페이스 제거 + const valueWithoutSpace = e.target.value.replace(/\s/g, ''); + onChange(valueWithoutSpace); + } + }} + onFocus={onFocus} + /> + ); +}; diff --git a/src/shared/ui/masked-input/masked-phone-input.tsx b/src/shared/ui/masked-input/masked-phone-input.tsx new file mode 100644 index 0000000..0ab2203 --- /dev/null +++ b/src/shared/ui/masked-input/masked-phone-input.tsx @@ -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) => 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 ( + { + // 백스페이스 처리 + 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) => { + // 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} + /> + ); +};