This commit is contained in:
focp212@naver.com
2025-10-29 14:36:31 +09:00
17 changed files with 462 additions and 334 deletions

View File

@@ -84,6 +84,17 @@ export interface ExtensionAccountHolderSearchRequestParams extends ExtensionRequ
export interface ExtensionAccountHolderSearchRequestResponse { export interface ExtensionAccountHolderSearchRequestResponse {
status: boolean; status: boolean;
data?: string;
error?: {
root?: string;
errKey?: string;
code?: string;
message?: string;
timestamp?: string;
details?: {
[key: string]: string;
};
};
} }
export interface ExtensionAccountHolderSearchDownloadExcelParams extends ExtensionRequestParams { // Request export interface ExtensionAccountHolderSearchDownloadExcelParams extends ExtensionRequestParams { // Request

View File

@@ -32,6 +32,17 @@ export interface ExtensionFundAccountTransferRegistParams {
export interface ExtensionFundAccountTransferRegistResponse { export interface ExtensionFundAccountTransferRegistResponse {
status: boolean; status: boolean;
data?: string;
error?: {
root?: string;
errKey?: string;
code?: string;
message?: string;
timestamp?: string;
details?: {
[key: string]: string;
};
};
}; };
export interface ExtensionFundAccountTransferRequestParams { export interface ExtensionFundAccountTransferRequestParams {
@@ -39,7 +50,18 @@ export interface ExtensionFundAccountTransferRequestParams {
} }
export interface ExtensionFundAccountTransferRequestResponse { export interface ExtensionFundAccountTransferRequestResponse {
status: boolean status: boolean;
data?: string;
error?: {
root?: string;
errKey?: string;
code?: string;
message?: string;
timestamp?: string;
details?: {
[key: string]: string;
};
};
} }
export enum FundAccountSearchCl { export enum FundAccountSearchCl {

View File

@@ -105,4 +105,30 @@ export interface ExtensionKeyinApplyParams extends ExtensionRequestParams {
export interface ExtensionKeyinApplyResponse { export interface ExtensionKeyinApplyResponse {
status: boolean; status: boolean;
data?: {
tid?: string | null;
approvalNumber?: string | null;
approvalDateTime?: string | null;
resultCode?: string;
resultMessage?: string;
cardName?: string | null;
cardCode?: string | null;
amount?: number | null;
installmentMonth?: string | null;
merchantNumber?: string | null;
acquirerCode?: string | null;
acquirerName?: string | null;
slipNumber?: string | null;
success?: boolean;
}
error?: {
root?: string;
errKey?: string;
code?: string;
resultMessage?: string;
timestamp?: string;
details?: {
path?: string;
};
};
} }

View File

@@ -198,7 +198,19 @@ export interface ExtensionLinkPayRequestParams extends ExtensionRequestParams {
} }
export interface ExtensionLinkPayRequestResponse { export interface ExtensionLinkPayRequestResponse {
status: boolean status: boolean;
error?: {
root?: string;
errKey?: string;
code?: string;
message?: string;
timestamp?: string;
details?: {
validationErrors?: {
[key: string]: string;
}
}
}
} }
export interface LinkPaymentFormData { export interface LinkPaymentFormData {

View File

@@ -8,6 +8,10 @@ export interface ExtensionPayoutRequestParams {
}; };
export interface ExtensionPayoutRequestResponse { export interface ExtensionPayoutRequestResponse {
status: boolean; status: boolean;
error?: {
message?: string
}
}; };
export enum PayoutSearchDateType { export enum PayoutSearchDateType {
REQUEST_DATE = 'REQUEST_DATE', REQUEST_DATE = 'REQUEST_DATE',

View File

@@ -3,6 +3,8 @@ import { useNavigate } from '@/shared/lib/hooks/use-navigate';
import { useSetOnBack } from '@/widgets/sub-layout/use-sub-layout'; import { useSetOnBack } from '@/widgets/sub-layout/use-sub-layout';
import { SingleDatePicker } from '@/shared/ui/filter/single-date-picker'; import { SingleDatePicker } from '@/shared/ui/filter/single-date-picker';
import { LinkPaymentFormData, LinkPaymentSendMethod } from '@/entities/additional-service/model/link-pay/types'; import { LinkPaymentFormData, LinkPaymentSendMethod } from '@/entities/additional-service/model/link-pay/types';
import { NumericFormat } from 'react-number-format';
import { ChangeEvent } from 'react';
interface LinkPaymentStep1Props { interface LinkPaymentStep1Props {
formData: LinkPaymentFormData; formData: LinkPaymentFormData;
@@ -25,9 +27,7 @@ export const LinkPaymentStep1 = ({ formData, setFormData }: LinkPaymentStep1Prop
}; };
const handleAmountChange = (value: string) => { const handleAmountChange = (value: string) => {
// 숫자만 추출
const onlyNumbers = value.replace(/[^0-9]/g, ''); const onlyNumbers = value.replace(/[^0-9]/g, '');
// 빈 문자열이면 0, 아니면 숫자로 변환 (앞의 0 제거됨)
const numericValue = onlyNumbers === '' ? 0 : parseInt(onlyNumbers, 10); const numericValue = onlyNumbers === '' ? 0 : parseInt(onlyNumbers, 10);
setFormData({ ...formData, amount: numericValue }); setFormData({ ...formData, amount: numericValue });
}; };
@@ -93,13 +93,15 @@ export const LinkPaymentStep1 = ({ formData, setFormData }: LinkPaymentStep1Prop
<div className="issue-row gap-10"> <div className="issue-row gap-10">
<div className="issue-label"></div> <div className="issue-label"></div>
<div className="issue-field"> <div className="issue-field">
<input <NumericFormat
type="text" value={formData.amount}
placeholder="0" allowNegative={false}
value={formData.amount === 0 ? '' : formData.amount} displayType="input"
onChange={(e) => handleAmountChange(e.target.value)} thousandSeparator={true}
inputMode="numeric" onValueChange={(values) => {
pattern="[0-9]*" const { floatValue } = values;
setFormData({...formData, amount: floatValue ?? 0});
}}
/> />
</div> </div>
</div> </div>

View File

@@ -1,7 +1,8 @@
import {ProcessStep} from "@/entities/transaction/model/types"; import { ProcessStep } from "@/entities/transaction/model/types";
import {useSetOnBack} from "@/widgets/sub-layout/use-sub-layout"; import { useSetOnBack } from "@/widgets/sub-layout/use-sub-layout";
import { IdentityType, Language } from '@/entities/additional-service/model/types' import { IdentityType, Language } from '@/entities/additional-service/model/types'
import { LinkContentType, LinkPaymentFormData } from "@/entities/additional-service/model/link-pay/types"; import { LinkContentType, LinkPaymentFormData } from "@/entities/additional-service/model/link-pay/types";
import { PatternFormat } from 'react-number-format';
export interface LinkPaymentStep2Props { export interface LinkPaymentStep2Props {
setProcessStep: ((processStep: ProcessStep) => void); setProcessStep: ((processStep: ProcessStep) => void);
@@ -18,6 +19,7 @@ export const LinkPaymentStep2 = ({
setProcessStep(ProcessStep.One); setProcessStep(ProcessStep.One);
}); });
const handleInputChange = (field: string, value: string) => { const handleInputChange = (field: string, value: string) => {
setFormData({ ...formData, [field]: value }); setFormData({ ...formData, [field]: value });
}; };
@@ -53,34 +55,39 @@ export const LinkPaymentStep2 = ({
</div> </div>
</div> </div>
<div className="issue-row gap-10"> {formData.sendMethod === 'EMAIL' &&
<div className="issue-label wid-105"> </div> <div className="issue-row gap-10">
<div className="issue-field"> <div className="issue-label wid-105"> </div>
<input <div className="issue-field">
type="text" <input
placeholder="test@nicepay.co.kr" type="text"
value={formData.email} placeholder="test@nicepay.co.kr"
onChange={(e) => handleInputChange('email', e.target.value)} value={formData.email}
/> onChange={(e) => handleInputChange('email', e.target.value)}
/>
</div>
</div> </div>
</div> }
<div className="issue-row gap-10"> {(formData.sendMethod === 'SMS' || formData.sendMethod === 'KAKAO') &&
<div className="issue-label wid-105"><br/> </div> <div className="issue-row gap-10">
<div className="issue-field"> <div className="issue-label wid-105"><br /> </div>
<input <div className="issue-field">
type="text" <input
placeholder="01012345678" type="text"
value={formData.phoneNumber} placeholder="01012345678"
onChange={(e) => handleInputChange('phoneNumber', e.target.value)} value={formData.phoneNumber}
/> onChange={(e) => handleInputChange('phoneNumber', e.target.value)}
/>
</div>
</div> </div>
</div> }
<div className="issue-row gap-10 beetween"> <div className="issue-row gap-10 beetween">
<div className="issue-label wid-105"> </div> <div className="issue-label wid-105"> </div>
<label className="settings-switch"> <label className="settings-switch">
<input type="checkbox" checked={formData.isIdentity} onChange={(e) => handleIdentityToggle(e.target.checked)}/> <input type="checkbox" checked={formData.isIdentity} onChange={(e) => handleIdentityToggle(e.target.checked)} />
<span className="slider"></span> <span className="slider"></span>
</label> </label>
</div> </div>
@@ -89,14 +96,16 @@ export const LinkPaymentStep2 = ({
<div className="issue-field"> <div className="issue-field">
<div className="chip-row"> <div className="chip-row">
<span <span
className={`keyword-tag flex-1 ${formData.identityType === IdentityType.INDIVIDUAL ? 'active' : ''}`} className={`keyword-tag flex-1 ${formData.identityType === IdentityType.INDIVIDUAL ? 'active' : ''} ${!formData.isIdentity ? 'disabled' : ''}`}
onClick={() => handleIdendityTypeChange(IdentityType.INDIVIDUAL)} onClick={() => formData.isIdentity && handleIdendityTypeChange(IdentityType.INDIVIDUAL)}
style={!formData.isIdentity ? { opacity: 0.5, cursor: 'not-allowed' } : {}}
> >
</span> </span>
<span <span
className={`keyword-tag flex-1 ${formData.identityType === IdentityType.CORPORATE ? 'active' : ''}`} className={`keyword-tag flex-1 ${formData.identityType === IdentityType.CORPORATE ? 'active' : ''} ${!formData.isIdentity ? 'disabled' : ''}`}
onClick={() => handleIdendityTypeChange(IdentityType.CORPORATE)} onClick={() => formData.isIdentity && handleIdendityTypeChange(IdentityType.CORPORATE)}
style={!formData.isIdentity ? { opacity: 0.5, cursor: 'not-allowed' } : {}}
> >
</span> </span>
@@ -107,12 +116,31 @@ export const LinkPaymentStep2 = ({
<div className="issue-row gap-10"> <div className="issue-row gap-10">
<div className="issue-label wid-105"></div> <div className="issue-label wid-105"></div>
<div className='issue-field'> <div className='issue-field'>
<input {formData.identityType === IdentityType.CORPORATE ? (
type="number" <PatternFormat
placeholder="생년월일 6자리" format="##########"
value={formData.identityValue} placeholder="사업자번호 10자리"
onChange={(e) => handleInputChange('identityValue', e.target.value)} value={formData.identityValue}
/> valueIsNumericString
onValueChange={(values) => {
const { value } = values;
handleInputChange('identityValue', value);
}}
disabled={!formData.isIdentity}
/>
) : (
<PatternFormat
format="######"
placeholder="생년월일 6자리"
value={formData.identityValue}
valueIsNumericString
onValueChange={(values) => {
const { value } = values;
handleInputChange('identityValue', value);
}}
disabled={!formData.isIdentity}
/>
)}
</div> </div>
</div> </div>

View File

@@ -11,6 +11,8 @@ import {
import { useExtensionAccountHolderSearchRequestMutation } from '@/entities/additional-service/api/account-holder-search/use-extension-account-holder-search-reqeust-mutation'; import { useExtensionAccountHolderSearchRequestMutation } from '@/entities/additional-service/api/account-holder-search/use-extension-account-holder-search-reqeust-mutation';
import { ExtensionAccountHolderSearchRequestParams } from '@/entities/additional-service/model/account-holder-search/types'; import { ExtensionAccountHolderSearchRequestParams } from '@/entities/additional-service/model/account-holder-search/types';
import { useStore } from '@/shared/model/store'; import { useStore } from '@/shared/model/store';
import { NumericFormat } from 'react-number-format';
import { snackBar } from '@/shared/lib';
export const AccountHolderSearchRequestPage = () => { export const AccountHolderSearchRequestPage = () => {
const { navigate } = useNavigate(); const { navigate } = useNavigate();
@@ -48,11 +50,21 @@ export const AccountHolderSearchRequestPage = () => {
accountHolderSearchRequest(reuqestParams) accountHolderSearchRequest(reuqestParams)
.then((response) => { .then((response) => {
console.log("계좌성명 조회 조회 신청 성공 응답: ", response.status) if (response.status) {
navigate(PATHS.additionalService.accountHolderSearch.list); console.log("계좌성명 조회 조회 신청 성공 응답: ", response.status)
snackBar("계좌성명 조회 신청을 성공하였습니다.")
navigate(PATHS.additionalService.accountHolderSearch.list);
} else {
const errorMessage = response.error?.message || '계좌성명 조회 신청이 실패하였습니다.';
snackBar(`[실패] ${errorMessage}`);
}
}) })
.catch((error) => { .catch((error) => {
console.error("계좌성명 조회 조회 신청 실패: ", error) console.error("계좌성명 조회 조회 신청 실패: ", error)
const errorMessage = error?.response?.data?.error?.message ||
error?.message ||
'계좌성명 조회 신청 중 오류가 발생했습니다.';
snackBar(`[실패] ${errorMessage}`);
}) })
}; };
@@ -109,12 +121,19 @@ export const AccountHolderSearchRequestPage = () => {
<div className="billing-row"> <div className="billing-row">
<div className="billing-label"></div> <div className="billing-label"></div>
<div className="billing-field"> <div className="billing-field">
<input <NumericFormat
type="number"
placeholder=''
value={formData.accountNo} value={formData.accountNo}
onChange={(e) => handleInputChange('accountNo', e.target.value)} valueIsNumericString
></input> allowNegative={false}
decimalScale={0}
isAllowed={(values) => {
const { value } = values;
return !value || value.length <= 14;
}}
onValueChange={(values) => {
setFormData({ ...formData, accountNo: values.value });
}}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -15,6 +15,7 @@ import { ArsPaymentMethod, ExtensionArsApplyParams, ExtensionArsApplyResponse }
import { ArsRequestSuccessPage } from './request-success-page'; import { ArsRequestSuccessPage } from './request-success-page';
import { useStore } from '@/shared/model/store'; import { useStore } from '@/shared/model/store';
import { snackBar } from '@/shared/lib'; import { snackBar } from '@/shared/lib';
import { NumericFormat, PatternFormat } from 'react-number-format';
export const ArsRequestPage = () => { export const ArsRequestPage = () => {
const { navigate } = useNavigate(); const { navigate } = useNavigate();
@@ -58,17 +59,17 @@ export const ArsRequestPage = () => {
arsPaymentMethod: arsPaymentMethod, arsPaymentMethod: arsPaymentMethod,
}; };
arsApply(arsApplyParams) arsApply(arsApplyParams)
.then((rs: ExtensionArsApplyResponse) => { .then((rs: ExtensionArsApplyResponse) => {
if (rs.status === true) { if (rs.status === true) {
setSuccessPageOn(true); setSuccessPageOn(true);
} else { } else {
const errorMessage = rs.error?.message || '신청을 실패하였습니다.'; const errorMessage = rs.error?.message || '신청을 실패하였습니다.';
snackBar(`[실패] ${errorMessage}`); snackBar(`[실패] ${errorMessage}`);
} }
}) })
.catch((error) => { .catch((error) => {
snackBar(`[실패] ${error?.response?.data?.message || error?.response?.data?.error?.message}` || '[실패] 신청을 실패하였습니다.') snackBar(`[실패] ${error?.response?.data?.message || error?.response?.data?.error?.message}` || '[실패] 신청을 실패하였습니다.')
}) })
}; };
@@ -82,6 +83,11 @@ export const ArsRequestPage = () => {
return phoneRegex.test(phone); return phoneRegex.test(phone);
}; };
const isValidEmail = (email: string) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const isFormValid = () => { const isFormValid = () => {
return ( return (
mid.trim() !== '' && mid.trim() !== '' &&
@@ -164,16 +170,12 @@ export const ArsRequestPage = () => {
<div className="billing-row"> <div className="billing-row">
<div className="billing-label"> <span>*</span></div> <div className="billing-label"> <span>*</span></div>
<div className="billing-field"> <div className="billing-field">
<input <NumericFormat
type="text"
value={amount} value={amount}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { allowNegative={false}
const onlyNumbers = e.target.value.replace(/[^0-9]/g, ''); // 숫자만 남김 displayType="input"
setAmount(onlyNumbers === '' ? 0 : parseInt(onlyNumbers)); onChange={(e: ChangeEvent<HTMLInputElement>) => setAmount(parseInt(e.target.value))}
}} ></NumericFormat>
inputMode="numeric" // 모바일 키보드 숫자 전용
pattern="[0-9]*" // 브라우저 기본 숫자만 유효하도록
/>
</div> </div>
</div> </div>
@@ -228,6 +230,8 @@ export const ArsRequestPage = () => {
value={email} value={email}
placeholder='test@nicepay.co.kr' placeholder='test@nicepay.co.kr'
onChange={(e: ChangeEvent<HTMLInputElement>) => setEamil(e.target.value)} onChange={(e: ChangeEvent<HTMLInputElement>) => setEamil(e.target.value)}
className={email && !isValidEmail(email) ? 'error' : ''}
/> />
</div> </div>
</div> </div>

View File

@@ -54,8 +54,18 @@ export const FundAccountTransferDetailPage = () => {
seq: seq seq: seq
}; };
extensionFundAccountTransferRequest(params).then((rs: ExtensionFundAccountTransferRequestResponse) => { extensionFundAccountTransferRequest(params).then((rs: ExtensionFundAccountTransferRequestResponse) => {
callDetail(); if (rs.status) {
snackBar("이체요청을 성공하였습니다.") callDetail();
snackBar("이체요청을 성공하였습니다.")
} else {
const errorMessage = rs.error?.message || '이체요청이 실패하였습니다.';
snackBar(`[실패] ${errorMessage}`);
}
}).catch((error) => {
const errorMessage = error?.response?.data?.error?.message ||
error?.message ||
'이체요청 중 오류가 발생했습니다.';
snackBar(`[실패] ${errorMessage}`);
}); });
}; };

View File

@@ -63,8 +63,14 @@ export const FundAccountTransferRequestPage = () => {
snackBar("이체등록을 성공하였습니다.") snackBar("이체등록을 성공하였습니다.")
resetForm(); resetForm();
} else { } else {
snackBar("이체등록이 실패하였습니다.") const errorMessage = rs.error?.message || '이체등록이 실패하였습니다.';
snackBar(`[실패] ${errorMessage}`);
} }
}).catch((error) => {
const errorMessage = error?.response?.data?.error?.message ||
error?.message ||
'이체등록 중 오류가 발생했습니다.';
snackBar(`[실패] ${errorMessage}`);
}); });
}; };
@@ -128,6 +134,10 @@ export const FundAccountTransferRequestPage = () => {
valueIsNumericString valueIsNumericString
allowNegative={false} allowNegative={false}
decimalScale={0} decimalScale={0}
isAllowed={(values) => {
const { value } = values;
return !value || value.length <= 14;
}}
onValueChange={(values) => setAccountNo(values.value)} onValueChange={(values) => setAccountNo(values.value)}
/> />
</div> </div>
@@ -138,7 +148,7 @@ export const FundAccountTransferRequestPage = () => {
<input <input
type="text" type="text"
value={accountName} value={accountName}
onChange={(e: ChangeEvent<HTMLInputElement>) => setAccountName(e.target.value) } onChange={(e: ChangeEvent<HTMLInputElement>) => setAccountName(e.target.value)}
/> />
</div> </div>
</div> </div>
@@ -148,8 +158,12 @@ export const FundAccountTransferRequestPage = () => {
<NumericFormat <NumericFormat
value={amount} value={amount}
allowNegative={false} allowNegative={false}
thousandSeparator={true}
displayType='input' displayType='input'
onChange={(e: ChangeEvent<HTMLInputElement>) => setAmount(parseInt(e.target.value))} onValueChange={(values) => {
const { floatValue } = values;
setAmount(floatValue ?? 0);
}}
></NumericFormat> ></NumericFormat>
</div> </div>
</div> </div>

View File

@@ -15,12 +15,14 @@ import { overlay } from 'overlay-kit';
import { Dialog } from '@/shared/ui/dialogs/dialog'; import { Dialog } from '@/shared/ui/dialogs/dialog';
import { useStore } from '@/shared/model/store'; import { useStore } from '@/shared/model/store';
import { snackBar } from '@/shared/lib'; import { snackBar } from '@/shared/lib';
import { NumericFormat, PatternFormat } from 'react-number-format';
export const KeyInPaymentRequestPage = () => { export const KeyInPaymentRequestPage = () => {
const { navigate } = useNavigate(); const { navigate } = useNavigate();
const location = useLocation(); const location = useLocation();
const userMid = useStore.getState().UserStore.mid; const userMid = useStore.getState().UserStore.mid;
const midOptions = useStore.getState().UserStore.selectOptionsMids
const [mid, setMid] = useState<string>(userMid || ''); const [mid, setMid] = useState<string>(userMid || '');
const [productName, setProductName] = useState<string>(''); const [productName, setProductName] = useState<string>('');
@@ -79,50 +81,24 @@ export const KeyInPaymentRequestPage = () => {
}; };
keyInApply(keyInApplyParams).then((rs) => { keyInApply(keyInApplyParams).then((rs) => {
console.log('결제 응답:', rs); if (rs.data?.success) {
if (rs.status) {
// 성공: 화면 유지 & 입력 내용 초기화 // 성공: 화면 유지 & 입력 내용 초기화
snackBar("KEY-IN 결제 신청을 성공하였습니다.") snackBar("KEY-IN 결제 신청을 성공하였습니다.")
resetForm(); resetForm();
} else { } else {
// 실패: 화면 유지 & 입력 내용 유지 // 실패: 화면 유지 & 입력 내용 유지
showErrorDialog('결제에 실패했습니다. 입력 내용을 확인해주세요.'); const errorMessage = rs.data?.resultMessage || rs.error?.resultMessage || '결제 신청에 실패했습니다.';
// HTML 태그 제거
const cleanMessage = errorMessage.replace(/<br\s*\/?>/gi, ' ').trim();
snackBar(`[실패] ${cleanMessage}`);
} }
}).catch((error) => { }).catch((error) => {
console.error('결제 실패:', error); console.error('결제 실패:', error);
showErrorDialog(error?.message || '결제 요청 중 오류가 발생했습니다'); const errorMessage = error?.response?.data?.error?.resultMessage || error?.message || '결제 요청 중 오류가 발생했습니다';
snackBar(`[실패] ${errorMessage}`);
}); });
}; };
const showSuccessDialog = () => {
overlay.open(({ isOpen, close, unmount }) => {
return (
<Dialog
afterLeave={unmount}
open={isOpen}
onClose={close}
onConfirmClick={close}
message="결제가 성공적으로 처리되었습니다"
buttonLabel={['확인']}
/>
);
});
};
const showErrorDialog = (errorMessage: string) => {
overlay.open(({ isOpen, close, unmount }) => {
return (
<Dialog
afterLeave={unmount}
open={isOpen}
onClose={close}
onConfirmClick={close}
message={errorMessage}
buttonLabel={['확인']}
/>
);
});
};
const isValidPhoneNumber = (phone: string) => { const isValidPhoneNumber = (phone: string) => {
const phoneRegex = /^01[0|1|6|7|8|9][0-9]{7,8}$/; const phoneRegex = /^01[0|1|6|7|8|9][0-9]{7,8}$/;
@@ -139,24 +115,17 @@ export const KeyInPaymentRequestPage = () => {
cardNo3.length === 4 && cardNo4.length === 4; cardNo3.length === 4 && cardNo4.length === 4;
}; };
const isValidCardExpiration = () => {
if (expMon.length !== 2 || expYear.length !== 2) {
return false;
}
const month = parseInt(expMon);
return month >= 1 && month <= 12;
};
const isFormValid = () => { const isFormValid = () => {
return ( return (
mid.trim() !== '' && mid.trim() !== '' &&
productName.trim() !== '' && productName.trim() !== '' &&
amount > 0 && amount > 0 &&
customerName.trim() !== '' && customerName.trim() !== '' &&
expMon.trim() !== '' &&
expYear.trim() !== '' &&
isValidEmail(email) && isValidEmail(email) &&
isValidPhoneNumber(phoneNumber) && isValidPhoneNumber(phoneNumber) &&
isValidCardNumber() && isValidCardNumber()
isValidCardExpiration()
); );
}; };
@@ -174,11 +143,19 @@ export const KeyInPaymentRequestPage = () => {
<div className="billing-row"> <div className="billing-row">
<div className="billing-label"> <span>*</span></div> <div className="billing-label"> <span>*</span></div>
<div className="billing-field"> <div className="billing-field">
<input <select
type="text"
value={mid} value={mid}
readOnly={true} onChange={(e: ChangeEvent<HTMLSelectElement>) => setMid(e.target.value)}
/> >
{
midOptions.map((value) => (
<option
key={value.value}
value={value.value}
>{value.name}</option>
))
}
</select>
</div> </div>
</div> </div>
@@ -196,17 +173,16 @@ export const KeyInPaymentRequestPage = () => {
<div className="billing-row"> <div className="billing-row">
<div className="billing-label"> <span>*</span></div> <div className="billing-label"> <span>*</span></div>
<div className="billing-field"> <div className="billing-field">
<input <NumericFormat
type="text" value={amount}
value={amount === 0 ? '' : amount.toString()} thousandSeparator={true}
placeholder="금액을 입력하세요" allowNegative={false}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { displayType="input"
const onlyNumbers = e.target.value.replace(/[^0-9]/g, ''); // 숫자만 남김 onValueChange={(values) => {
setAmount(onlyNumbers === '' ? 0 : parseInt(onlyNumbers, 10)); const { floatValue} = values;
setAmount( floatValue ?? 0);
}} }}
inputMode="numeric" // 모바일 키보드 숫자 전용 ></NumericFormat>
pattern="[0-9]*" // 브라우저 기본 숫자만 유효하도록
/>
</div> </div>
</div> </div>
@@ -227,7 +203,6 @@ export const KeyInPaymentRequestPage = () => {
<input <input
type="email" type="email"
value={email} value={email}
placeholder='test@nicepay.co.kr'
onChange={(e: ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)} onChange={(e: ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)}
className={email && !isValidEmail(email) ? 'error' : ''} className={email && !isValidEmail(email) ? 'error' : ''}
/> />
@@ -240,7 +215,7 @@ export const KeyInPaymentRequestPage = () => {
<input <input
type="tel" type="tel"
value={phoneNumber} value={phoneNumber}
placeholder='01012345678' placeholder='- 제외하고 입력'
onChange={(e: ChangeEvent<HTMLInputElement>) => { onChange={(e: ChangeEvent<HTMLInputElement>) => {
const onlyNumbers = e.target.value.replace(/[^0-9]/g, ''); const onlyNumbers = e.target.value.replace(/[^0-9]/g, '');
setPhoneNumber(onlyNumbers); setPhoneNumber(onlyNumbers);
@@ -256,92 +231,85 @@ export const KeyInPaymentRequestPage = () => {
<div className="billing-row"> <div className="billing-row">
<div className="billing-label"> <span>*</span></div> <div className="billing-label"> <span>*</span></div>
<div className="billing-field"> <div className="billing-field">
<input <PatternFormat
type="text"
value={cardNo1} value={cardNo1}
onChange={(e: ChangeEvent<HTMLInputElement>) => { allowEmptyFormatting
const onlyNumbers = e.target.value.replace(/[^0-9]/g, ''); valueIsNumericString
if (onlyNumbers.length <= 4) setCardNo1(onlyNumbers); format="####"
}} onChange={(e: ChangeEvent<HTMLInputElement>) => setCardNo1(e.target.value)}
inputMode="numeric" >
pattern="[0-9]*" </PatternFormat>
maxLength={4}
placeholder="1234"
/>
</div> </div>
<div className="billing-field"> <div className="billing-field">
<input <PatternFormat
type="text"
value={cardNo2} value={cardNo2}
onChange={(e: ChangeEvent<HTMLInputElement>) => { allowEmptyFormatting
const onlyNumbers = e.target.value.replace(/[^0-9]/g, ''); valueIsNumericString
if (onlyNumbers.length <= 4) setCardNo2(onlyNumbers); format="####"
}} onChange={(e: ChangeEvent<HTMLInputElement>) => setCardNo2(e.target.value)}
inputMode="numeric" >
pattern="[0-9]*" </PatternFormat>
maxLength={4}
placeholder="5678"
/>
</div> </div>
<div className="billing-field"> <div className="billing-field">
<input <PatternFormat
type="text"
value={cardNo3} value={cardNo3}
onChange={(e: ChangeEvent<HTMLInputElement>) => { allowEmptyFormatting
const onlyNumbers = e.target.value.replace(/[^0-9]/g, ''); valueIsNumericString
if (onlyNumbers.length <= 4) setCardNo3(onlyNumbers); format="####"
}} onChange={(e: ChangeEvent<HTMLInputElement>) => setCardNo3(e.target.value)}
inputMode="numeric" >
pattern="[0-9]*" </PatternFormat>
maxLength={4}
placeholder="9012"
/>
</div> </div>
<div className="billing-field"> <div className="billing-field">
<input <PatternFormat
type="text"
value={cardNo4} value={cardNo4}
onChange={(e: ChangeEvent<HTMLInputElement>) => { allowEmptyFormatting
const onlyNumbers = e.target.value.replace(/[^0-9]/g, ''); valueIsNumericString
if (onlyNumbers.length <= 4) setCardNo4(onlyNumbers); format="####"
}} onChange={(e: ChangeEvent<HTMLInputElement>) => setCardNo4(e.target.value)}
inputMode="numeric" >
pattern="[0-9]*" </PatternFormat>
maxLength={4}
placeholder="3456"
/>
</div> </div>
</div> </div>
<div className="billing-row"> <div className="billing-row">
<div className="billing-label">(/)<span>*</span></div> <div className="billing-label">(/)<span>*</span></div>
<div className="billing-field" style={{ display: 'flex', gap: '8px', alignItems: 'center' }}> <div className="billing-field" style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<input <PatternFormat
type="text"
value={expMon}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
const onlyNumbers = e.target.value.replace(/[^0-9]/g, '');
if (onlyNumbers.length <= 2) setExpMon(onlyNumbers);
}}
inputMode="numeric"
pattern="[0-9]*"
maxLength={2}
placeholder='MM' placeholder='MM'
value={expMon}
valueIsNumericString
format='##'
isAllowed={(values) => {
const { value } = values;
if (!value) return true;
const numValue = parseInt(value);
return numValue >= 1 && numValue <= 12;
}}
onValueChange={(values) => {
const { value } = values;
setExpMon(value);
}}
onBlur={() => {
if (expMon.length === 1 && parseInt(expMon) >= 1 && parseInt(expMon) <= 9) {
setExpMon(expMon.padStart(2, '0'));
}
}}
style={{ flex: 1 }} style={{ flex: 1 }}
inputMode="numeric"
/> />
<span>/</span> <span>/</span>
<input <PatternFormat
type="text"
value={expYear}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
const onlyNumbers = e.target.value.replace(/[^0-9]/g, '');
if (onlyNumbers.length <= 2) setExpYear(onlyNumbers);
}}
inputMode="numeric"
pattern="[0-9]*"
maxLength={2}
placeholder='YY' placeholder='YY'
value={expYear}
valueIsNumericString
format='##'
onValueChange={(values) => {
const { value } = values;
setExpYear(value);
}}
style={{ flex: 1 }} style={{ flex: 1 }}
inputMode="numeric"
/> />
</div> </div>
</div> </div>
@@ -354,8 +322,8 @@ export const KeyInPaymentRequestPage = () => {
value={instmnt} value={instmnt}
onChange={(e: ChangeEvent<HTMLSelectElement>) => setInstmnt(e.target.value)} onChange={(e: ChangeEvent<HTMLSelectElement>) => setInstmnt(e.target.value)}
> >
<option value="00"> ()</option> <option value="00"></option>
{amount >= 50000 && ( {/* {amount >= 50000 && (
<> <>
<option value="02">2개월</option> <option value="02">2개월</option>
<option value="03">3개월</option> <option value="03">3개월</option>
@@ -369,7 +337,7 @@ export const KeyInPaymentRequestPage = () => {
<option value="11">11개월</option> <option value="11">11개월</option>
<option value="12">12개월</option> <option value="12">12개월</option>
</> </>
)} )} */}
</select> </select>
</div> </div>
</div> </div>

View File

@@ -5,7 +5,8 @@ import { useLocation } from 'react-router';
import { IMAGE_ROOT } from "@/shared/constants/common"; import { IMAGE_ROOT } from "@/shared/constants/common";
import { PATHS } from '@/shared/constants/paths'; import { PATHS } from '@/shared/constants/paths';
import { useExtensionLinkPayRequestMutation } from '@/entities/additional-service/api/link-payment/use-extension-link-pay-request-mutation'; import { useExtensionLinkPayRequestMutation } from '@/entities/additional-service/api/link-payment/use-extension-link-pay-request-mutation';
import { ExtensionLinkPayRequestParams, LinkPaymentFormData } from '@/entities/additional-service/model/link-pay/types'; import { ExtensionLinkPayRequestParams, ExtensionLinkPayRequestResponse, LinkPaymentFormData } from '@/entities/additional-service/model/link-pay/types';
import { snackBar } from '@/shared/lib';
export const LinkPaymentApplyConfirmPage = () => { export const LinkPaymentApplyConfirmPage = () => {
const { navigate } = useNavigate(); const { navigate } = useNavigate();
@@ -40,17 +41,30 @@ export const LinkPaymentApplyConfirmPage = () => {
language: formData.language, language: formData.language,
linkContentType: formData.linkContentType linkContentType: formData.linkContentType
}; };
console.log("Link Payment 요청 파라미터: ", requestParams);
linkPayRequest(requestParams) linkPayRequest(requestParams)
.then((response) => { .then((rs: ExtensionLinkPayRequestResponse) => {
console.log("Link Payment 성공 응답: ", response); if (rs.status) {
navigate(PATHS.additionalService.linkPayment.confirmSuccess); navigate(PATHS.additionalService.linkPayment.confirmSuccess);
} else {
// 응답은 성공했지만 status가 false인 경우
const validationErrors = rs.error?.details?.validationErrors;
if (validationErrors) {
// validation 에러 메시지들을 수집
const errorMessages = Object.values(validationErrors).join('\n');
snackBar(`[실패] ${errorMessages}`);
} else {
// 일반 에러 메시지
const errorMessage = rs.error?.message || '요청을 처리할 수 없습니다.';
snackBar(`[실패] ${errorMessage}`);
}
}
}) })
.catch((error) => { .catch((error) => {
console.error("Link Payment 실패: ", error); // 네트워크 에러 등 예외 상황
// 에러 처리 로직 추가 가능 const errorMessage = error?.response?.data?.error?.message ||
error?.message ||
'요청 중 오류가 발생했습니다';
snackBar(`[실패] ${errorMessage}`);
}); });
}; };
@@ -66,18 +80,18 @@ export const LinkPaymentApplyConfirmPage = () => {
<div className="attention-icon" aria-hidden="true"> <div className="attention-icon" aria-hidden="true">
<img src={IMAGE_ROOT + '/ico_alert.svg'} alt="주의" /> <img src={IMAGE_ROOT + '/ico_alert.svg'} alt="주의" />
</div> </div>
<h1 className="preview-title"> <br/> </h1> <h1 className="preview-title"> <br /> </h1>
<div className="preview-result"> <div className="preview-result">
<p className="preview-text"> <p className="preview-text">
TEST , ?<br/> {formData.buyerName} , ?<br />
<br/> <br />
.<br/> .<br />
URL로 .<br/><br/> URL로 .<br /><br />
!$&#123;pay_url&#125;<br/><br/> !$&#123;pay_url&#125;<br /><br />
<b> <b>
상호 : 나이스페이먼츠 <br/> 상호 : 나이스페이먼츠 <br />
: {formData.goodsName}<br/> : {formData.goodsName}<br />
: {formData.amount} : {formData.amount.toLocaleString()}
</b> </b>
</p> </p>
</div> </div>

View File

@@ -1,11 +1,11 @@
import {useState} from 'react'; import { useState } from 'react';
import {LinkPaymentStep1} from '@/entities/additional-service/ui/link-payment/apply/link-payment-step1'; import { LinkPaymentStep1 } from '@/entities/additional-service/ui/link-payment/apply/link-payment-step1';
import {LinkPaymentStep2} from '@/entities/additional-service/ui/link-payment/apply/link-payment-step2'; import { LinkPaymentStep2 } from '@/entities/additional-service/ui/link-payment/apply/link-payment-step2';
import {HeaderType} from '@/entities/common/model/types'; import { HeaderType } from '@/entities/common/model/types';
import {ProcessStep} from '@/entities/transaction/model/types'; import { ProcessStep } from '@/entities/transaction/model/types';
import {useSetFooterMode, useSetHeaderTitle, useSetHeaderType} from '@/widgets/sub-layout/use-sub-layout'; import { useSetFooterMode, useSetHeaderTitle, useSetHeaderType } from '@/widgets/sub-layout/use-sub-layout';
import {useNavigate} from '@/shared/lib/hooks/use-navigate'; import { useNavigate } from '@/shared/lib/hooks/use-navigate';
import {PATHS} from "@/shared/constants/paths"; import { PATHS } from "@/shared/constants/paths";
import { IdentityType, Language } from '@/entities/additional-service/model/types'; import { IdentityType, Language } from '@/entities/additional-service/model/types';
import { LinkContentType, LinkPaymentFormData, LinkPaymentSendMethod } from '@/entities/additional-service/model/link-pay/types'; import { LinkContentType, LinkPaymentFormData, LinkPaymentSendMethod } from '@/entities/additional-service/model/link-pay/types';
import { useStore } from '@/shared/model/store'; import { useStore } from '@/shared/model/store';
@@ -14,25 +14,25 @@ import moment from 'moment';
export const LinkPaymentApplyPage = () => { export const LinkPaymentApplyPage = () => {
const { navigate } = useNavigate(); const { navigate } = useNavigate();
const userMid = useStore.getState().UserStore.mid; const userMid = useStore.getState().UserStore.mid;
const [processStep, setProcessStep] = useState<ProcessStep>(ProcessStep.One); const [processStep, setProcessStep] = useState<ProcessStep>(ProcessStep.One);
const [formData, setFormData] = useState<LinkPaymentFormData>({ const [formData, setFormData] = useState<LinkPaymentFormData>({
mid: userMid, mid: userMid,
sendMethod: LinkPaymentSendMethod.SMS, sendMethod: LinkPaymentSendMethod.SMS,
goodsName: '', goodsName: '',
amount: 0, amount: 0,
moid: '', moid: '',
paymentLimitDate: moment().format('YYYY.MM.DD'), paymentLimitDate: moment().format('YYYY.MM.DD'),
buyerName: '', buyerName: '',
email: '', email: '',
phoneNumber: '', phoneNumber: '',
isIdentity: false, isIdentity: false,
identityType: IdentityType.INDIVIDUAL, identityType: IdentityType.INDIVIDUAL,
identityValue: '', identityValue: '',
language: Language.KR, language: Language.KR,
linkContentType: LinkContentType.BASIC linkContentType: LinkContentType.BASIC
}); });
useSetHeaderTitle('링크결제 신청'); useSetHeaderTitle('링크결제 신청');
useSetHeaderType(HeaderType.LeftArrow); useSetHeaderType(HeaderType.LeftArrow);
@@ -64,15 +64,11 @@ export const LinkPaymentApplyPage = () => {
// Step2 필수 필드 검증 // Step2 필수 필드 검증
const isStep2Valid = () => { const isStep2Valid = () => {
const basicFieldsValid = (
formData.buyerName.trim() !== '' &&
formData.email.trim() !== '' &&
isValidEmail(formData.email) &&
formData.phoneNumber.trim() !== '' &&
isValidPhoneNumber(formData.phoneNumber)
);
// isIdentity가 true면 identityValue도 필수 const basicFieldsValid = formData.sendMethod === 'EMAIL'
? formData.email.trim() !== '' && isValidEmail(formData.email)
: formData.buyerName.trim() !== '' && formData.phoneNumber.trim() !== '' && isValidPhoneNumber(formData.phoneNumber);
if (formData.isIdentity) { if (formData.isIdentity) {
return basicFieldsValid && formData.identityValue.trim() !== ''; return basicFieldsValid && formData.identityValue.trim() !== '';
} }
@@ -85,14 +81,14 @@ export const LinkPaymentApplyPage = () => {
}; };
const onClickToChangeTab = () => { const onClickToChangeTab = () => {
if(processStep === ProcessStep.One) { if (processStep === ProcessStep.One) {
setProcessStep(ProcessStep.Two); setProcessStep(ProcessStep.Two);
} }
else if(processStep === ProcessStep.Two) { else if (processStep === ProcessStep.Two) {
navigate(PATHS.additionalService.linkPayment.requestConfirm, { navigate(PATHS.additionalService.linkPayment.requestConfirm, {
state: { formData } state: { formData }
}); });
} }
}; };
return ( return (
@@ -103,58 +99,58 @@ export const LinkPaymentApplyPage = () => {
<div className="option-list"> <div className="option-list">
<div className="issue-progress"> <div className="issue-progress">
<div className="bar"> <div className="bar">
{(processStep === ProcessStep.One) && {(processStep === ProcessStep.One) &&
<div <div
className="fill" className="fill"
style={{ width: '50%' }} style={{ width: '50%' }}
></div> ></div>
} }
{(processStep === ProcessStep.Two) && {(processStep === ProcessStep.Two) &&
<div <div
className="fill" className="fill"
style={{ width: '100%' }} style={{ width: '100%' }}
></div> ></div>
} }
</div> </div>
</div> </div>
{(processStep === ProcessStep.One) &&
<LinkPaymentStep1
formData={formData}
setFormData={setFormData}
></LinkPaymentStep1>
}
{ (processStep === ProcessStep.Two) &&
<LinkPaymentStep2
setProcessStep={ setProcessStep }
formData={formData}
setFormData={setFormData}
></LinkPaymentStep2>
}
</div>
{(processStep === ProcessStep.One) && {(processStep === ProcessStep.One) &&
<div className="apply-row"> <LinkPaymentStep1
<button formData={formData}
className="btn-50 btn-blue flex-1" setFormData={setFormData}
onClick={() => onClickToChangeTab()} ></LinkPaymentStep1>
disabled={!isStep1Valid()}
></button>
</div>
} }
{(processStep === ProcessStep.Two) && {(processStep === ProcessStep.Two) &&
<div className="apply-row two-button"> <LinkPaymentStep2
<button setProcessStep={setProcessStep}
className="btn-50 btn-darkgray flex-1" formData={formData}
onClick={() => onClickToBack()} setFormData={setFormData}
></button> ></LinkPaymentStep2>
<button
className="btn-50 btn-blue flex-3"
onClick={() => onClickToChangeTab()}
disabled={!isStep2Valid()}
> </button>
</div>
} }
</div>
{(processStep === ProcessStep.One) &&
<div className="apply-row">
<button
className="btn-50 btn-blue flex-1"
onClick={() => onClickToChangeTab()}
disabled={!isStep1Valid()}
></button>
</div>
}
{(processStep === ProcessStep.Two) &&
<div className="apply-row two-button">
<button
className="btn-50 btn-darkgray flex-1"
onClick={() => onClickToBack()}
></button>
<button
className="btn-50 btn-blue flex-3"
onClick={() => onClickToChangeTab()}
disabled={!isStep2Valid()}
> </button>
</div>
}
</div> </div>
</div> </div>
</main> </main>

View File

@@ -26,7 +26,7 @@ export const LinkPaymentWaitDetailPage = () => {
const [titleInfo, setTitleInfo] = useState<TitleInfo>(); const [titleInfo, setTitleInfo] = useState<TitleInfo>();
const [paymentInfo, setPaymentInfo] = useState<PaymentInfo>(); const [paymentInfo, setPaymentInfo] = useState<PaymentInfo>();
useSetHeaderTitle('링크결제 상세_발송대기'); useSetHeaderTitle('링크결제 상세 발송대기');
useSetHeaderType(HeaderType.RightClose); useSetHeaderType(HeaderType.RightClose);
useSetOnBack(() => { useSetOnBack(() => {
navigate(PATHS.additionalService.linkPayment.pendingSend); navigate(PATHS.additionalService.linkPayment.pendingSend);
@@ -56,23 +56,14 @@ export const LinkPaymentWaitDetailPage = () => {
} }
linkPayWaitDelete(deleteParam) linkPayWaitDelete(deleteParam)
.then((rs) => { .then((rs) => {
onClickToNavigate(PATHS.additionalService.linkPayment.pendingSend) callDetail();
snackBar("삭제를 성공하였습니다.") snackBar("삭제를 성공하였습니다.")
}) })
.catch((error) => { .catch((error) => {
console.error("Resend 실패: ", error);
snackBar(`[실패] ${error?.response?.data?.message}`) snackBar(`[실패] ${error?.response?.data?.message}`)
}); });
} }
const onClickToNavigate = (path: string) => {
let timeout = setTimeout(() => {
clearTimeout(timeout);
navigate(PATHS.additionalService.linkPayment.pendingSend, {
});
}, 10)
};
const onClickToCancel = () => { const onClickToCancel = () => {
let msg = '삭제 하시겠습니까?'; let msg = '삭제 하시겠습니까?';

View File

@@ -50,8 +50,8 @@ export const PayoutListPage = () => {
const [fromDate, setFromDate] = useState<string>(moment().format('YYYYMMDD')); const [fromDate, setFromDate] = useState<string>(moment().format('YYYYMMDD'));
const [toDate, setToDate] = useState<string>(moment().format('YYYYMMDD')); const [toDate, setToDate] = useState<string>(moment().format('YYYYMMDD'));
const [status, setStatus] = useState<PayoutDisbursementStatus>(PayoutDisbursementStatus.ALL); const [status, setStatus] = useState<PayoutDisbursementStatus>(PayoutDisbursementStatus.ALL);
const [minAmount, setMinAmount] = useState<number | undefined>(0); const [minAmount, setMinAmount] = useState<number | undefined>();
const [maxAmount, setMaxAmount] = useState<number | undefined>(50000000); const [maxAmount, setMaxAmount] = useState<number | undefined>();
const [emailBottomSheetOn, setEmailBottomSheetOn] = useState<boolean>(false); const [emailBottomSheetOn, setEmailBottomSheetOn] = useState<boolean>(false);
const { mutateAsync: extensionPayoutList } = useExtensionPayoutListMutation(); const { mutateAsync: extensionPayoutList } = useExtensionPayoutListMutation();

View File

@@ -44,13 +44,17 @@ export const PayoutRequestPage = () => {
settlementDate: settlementDate, settlementDate: settlementDate,
}; };
extensionPayoutRequest(params) extensionPayoutRequest(params)
.then((rs) => { .then((rs) => {
snackBar("신청을 성공하였습니다.") if (rs.status) {
}) snackBar("신청을 성공하였습니다.")
.catch((error) => { } else {
snackBar(`[실패] ${error?.response?.data?.message} `|| '[실패] 신청을 실패하였습니다.') snackBar(`[실패] ${rs.error?.message}`)
}) }
; })
.catch((error) => {
snackBar(`[실패] ${error?.response?.data?.message} ` || '[실패] 신청을 실패하였습니다.')
})
;
}; };
const isFormValid = () => { const isFormValid = () => {
@@ -90,9 +94,13 @@ export const PayoutRequestPage = () => {
<NumericFormat <NumericFormat
value={disbursementAmount} value={disbursementAmount}
allowNegative={false} allowNegative={false}
thousandSeparator={true}
displayType="input" displayType="input"
onChange={(e: ChangeEvent<HTMLInputElement>) => setDisbursementAmount(parseInt(e.target.value))} onValueChange={(values) => {
></NumericFormat> const { floatValue } = values;
setDisbursementAmount(floatValue ?? 0);
}}
/>
</div> </div>
</div> </div>
<div className="billing-row"> <div className="billing-row">
@@ -102,8 +110,7 @@ export const PayoutRequestPage = () => {
<input <input
type="text" type="text"
placeholder="날짜 선택" placeholder="날짜 선택"
value={settlementDate ? moment(settlementDate).format('YYYY.MM.DD') : '' } value={settlementDate ? moment(settlementDate).format('YYYY.MM.DD') : ''}
readOnly={true}
/> />
<button <button
className="date-btn" className="date-btn"