diff --git a/src/entities/additional-service/model/types.ts b/src/entities/additional-service/model/types.ts index 29015ac..f4f26aa 100644 --- a/src/entities/additional-service/model/types.ts +++ b/src/entities/additional-service/model/types.ts @@ -137,6 +137,7 @@ export interface KeyInPaymentListItem { export interface KeyInPaymentListProps { additionalServiceCategory: AdditionalServiceCategory; listItems: Record>; + mid?: string; } export interface KeyInPaymentFilterProps extends FilterProps { diff --git a/src/entities/additional-service/ui/key-in-payment/key-in-payment-list.tsx b/src/entities/additional-service/ui/key-in-payment/key-in-payment-list.tsx index 6e62608..976f8d4 100644 --- a/src/entities/additional-service/ui/key-in-payment/key-in-payment-list.tsx +++ b/src/entities/additional-service/ui/key-in-payment/key-in-payment-list.tsx @@ -5,7 +5,8 @@ import { ListDateGroup } from '../list-date-group'; export const KeyInPaymentList = ({ additionalServiceCategory, - listItems + listItems, + mid }: KeyInPaymentListProps) => { const { navigate } = useNavigate(); @@ -25,7 +26,9 @@ export const KeyInPaymentList = ({ }; const onClickToNavigate = () => { - navigate(PATHS.additionalService.keyInPayment.request) + navigate(PATHS.additionalService.keyInPayment.request, { + state: { mid } + }); }; return ( diff --git a/src/entities/additional-service/ui/link-payment/bottom-sheet/extended-period-bottom-sheet.tsx b/src/entities/additional-service/ui/link-payment/bottom-sheet/extended-period-bottom-sheet.tsx new file mode 100644 index 0000000..6cdc1e0 --- /dev/null +++ b/src/entities/additional-service/ui/link-payment/bottom-sheet/extended-period-bottom-sheet.tsx @@ -0,0 +1,71 @@ +import { motion } from 'framer-motion'; +import { IMAGE_ROOT } from '@/shared/constants/common'; +import { BottomSheetMotionDuration, BottomSheetMotionVaiants } from "@/entities/common/model/constant"; + + +export interface ExtendedPeriodBottomSheetProps { + bottomSheetOn: boolean, + setBottomSheetOn: (bottomSheetOn: boolean) => void; + extendPeriod: () => void; +}; + +export const ExtendedPeriodBottomSheet = ({ + bottomSheetOn, + setBottomSheetOn, + extendPeriod +}: ExtendedPeriodBottomSheetProps) => { + const onClickToClose = () => { + setBottomSheetOn(false); + }; + + const onClickToResendSms = () => { + if(extendPeriod) { + extendPeriod(); + } + onClickToClose(); + }; + + return ( + <> + { (bottomSheetOn) && +
+ } + +
+
+

기간 연장

+ +
+
+
+
+

유효기간 연장을 하시겠습니끼?

+

개별 상태를 확인해주세요.

+
+
+
+ +
+
+ + ); +} \ No newline at end of file diff --git a/src/entities/additional-service/ui/link-payment/bottom-sheet/link-break-bottom-sheet.tsx b/src/entities/additional-service/ui/link-payment/bottom-sheet/link-break-bottom-sheet.tsx new file mode 100644 index 0000000..b3b45a8 --- /dev/null +++ b/src/entities/additional-service/ui/link-payment/bottom-sheet/link-break-bottom-sheet.tsx @@ -0,0 +1,71 @@ +import { motion } from 'framer-motion'; +import { IMAGE_ROOT } from '@/shared/constants/common'; +import { BottomSheetMotionDuration, BottomSheetMotionVaiants } from "@/entities/common/model/constant"; + + +export interface LinkBreakBottomSheetProps { + bottomSheetOn: boolean, + setBottomSheetOn: (bottomSheetOn: boolean) => void; + linkBreak: () => void; +}; + +export const LinkBreakBottomSheet = ({ + bottomSheetOn, + setBottomSheetOn, + linkBreak +}: LinkBreakBottomSheetProps) => { + const onClickToClose = () => { + setBottomSheetOn(false); + }; + + const onClickToLinkBreak = () => { + if(linkBreak) { + linkBreak(); + } + onClickToClose(); + }; + + return ( + <> + { (bottomSheetOn) && +
+ } + +
+
+

링크중단

+ +
+
+
+
+

링크중단 요청 시

+

결제불가 상태가 되며 원복 불가합니다.

+
+
+
+ +
+
+ + ); +} \ No newline at end of file diff --git a/src/entities/additional-service/ui/link-payment/link-payment-history-wrap.tsx b/src/entities/additional-service/ui/link-payment/link-payment-history-wrap.tsx index 465e8bb..bc27f42 100644 --- a/src/entities/additional-service/ui/link-payment/link-payment-history-wrap.tsx +++ b/src/entities/additional-service/ui/link-payment/link-payment-history-wrap.tsx @@ -54,7 +54,7 @@ export const LinkPaymentHistoryWrap = () => { mid: mid, searchCl: searchType === LinkPaymentSearchType.ALL ? '' : searchType, searchValue: searchKeyword, - paymentMethod: 'st', // 추후 변경 필요 빼야함 + paymentMethod: '', fromDate: startDate, toDate: endDate, paymentStatus: transactionStatus === LinkPaymentTransactionStatus.ALL ? '' : transactionStatus, @@ -91,7 +91,7 @@ export const LinkPaymentHistoryWrap = () => { mid: mid, searchCl: (searchType === LinkPaymentSearchType.ALL)? '': searchType, searchValue: searchKeyword, - paymentMethod: 'st', // 추후 변경 필요 빼야함 + paymentMethod: '', fromDate: startDate, toDate: endDate, paymentStatus: (transactionStatus === LinkPaymentTransactionStatus.ALL)? '': transactionStatus, diff --git a/src/pages/additional-service/additional-service-pages.tsx b/src/pages/additional-service/additional-service-pages.tsx index c01c060..9af3e23 100644 --- a/src/pages/additional-service/additional-service-pages.tsx +++ b/src/pages/additional-service/additional-service-pages.tsx @@ -38,6 +38,7 @@ import { KeyInPaymentRequestSuccessPage } from './key-in-payment/request-success import { AccountHolderSearchRequestPage } from './account-holder-search/request-page'; import { AccountHolderSearchDetailPage } from './account-holder-search/detail-page'; import { AccountHolderAuthDetailPage } from './account-holder-auth/detail-page'; +import { LinkPaymentSeparateApprovalPage } from './link-payment/separate-approval/link-payment-separate-approval-page'; export const AdditionalServicePages = () => { return ( @@ -73,6 +74,7 @@ export const AdditionalServicePages = () => { } /> } /> } /> + } /> } /> diff --git a/src/pages/additional-service/ars/list-page.tsx b/src/pages/additional-service/ars/list-page.tsx index 7feda20..554d366 100644 --- a/src/pages/additional-service/ars/list-page.tsx +++ b/src/pages/additional-service/ars/list-page.tsx @@ -105,7 +105,9 @@ export const ArsListPage = () => { }; const onClickToNavigate = () => { - navigate(PATHS.additionalService.ars.request); + navigate(PATHS.additionalService.ars.request, { + state: { mid } + }); }; const onClickToDownloadExcel = () => { diff --git a/src/pages/additional-service/ars/request-page.tsx b/src/pages/additional-service/ars/request-page.tsx index ee56639..1bfb31f 100644 --- a/src/pages/additional-service/ars/request-page.tsx +++ b/src/pages/additional-service/ars/request-page.tsx @@ -1,23 +1,27 @@ import { ChangeEvent, useState } from 'react'; import { PATHS } from '@/shared/constants/paths'; +import { useLocation } from 'react-router'; import { useNavigate } from '@/shared/lib/hooks/use-navigate'; import { IMAGE_ROOT } from '@/shared/constants/common'; import { useExtensionArsApplyMutation } from '@/entities/additional-service/api/ars/use-extension-ars-apply-mutation'; import { HeaderType } from '@/entities/common/model/types'; -import { - useSetHeaderTitle, - useSetHeaderType, - useSetFooterMode, +import { + useSetHeaderTitle, + useSetHeaderType, + useSetFooterMode, useSetOnBack } from '@/widgets/sub-layout/use-sub-layout'; import { ArsPaymentMethod, ExtensionArsApplyParams } from '@/entities/additional-service/model/ars/types'; export const ArsRequestPage = () => { const { navigate } = useNavigate(); + const location = useLocation(); + + const { mid: receivedMid } = location.state || {}; const { mutateAsync: arsApply } = useExtensionArsApplyMutation(); - const [mid, setMid] = useState(''); + const [mid, setMid] = useState(receivedMid || ''); const [moid, setMoid] = useState(''); const [goodsName, setGoodsName] = useState(''); const [amount, setAmount] = useState(0); @@ -52,30 +56,47 @@ export const ArsRequestPage = () => { }).catch(() => { }).finally(() => { - + }); }; const onClickToRequest = () => { callArsApply(); - }; + }; + + const isValidPhoneNumber = (phone: string) => { + // 한국 휴대폰 번호: 010, 011, 016, 017, 018, 019로 시작, 10-11자리 + const phoneRegex = /^01[0|1|6|7|8|9][0-9]{7,8}$/; + return phoneRegex.test(phone); + }; + + const isFormValid = () => { + return ( + mid.trim() !== '' && + moid.trim() !== '' && + goodsName.trim() !== '' && + amount > 0 && + buyerName.trim() !== '' && + isValidPhoneNumber(phoneNumber) + ); + }; const getArsPaymentMethodBtns = () => { let rs = []; rs.push( -
- - + +
); return rs; @@ -91,10 +112,9 @@ export const ArsRequestPage = () => {
가맹점 *
-
@@ -102,10 +122,10 @@ export const ArsRequestPage = () => {
주문번호 *
- ) => setMoid(e.target.value) } + ) => setMoid(e.target.value)} />
@@ -113,80 +133,95 @@ export const ArsRequestPage = () => {
상품명 *
- ) => setGoodsName(e.target.value) } + ) => setGoodsName(e.target.value)} />
-
-
금액 *
-
- ) => setAmount(parseInt(e.target.value)) } - /> -
+
+
금액 *
+
+ ) => { + const onlyNumbers = e.target.value.replace(/[^0-9]/g, ''); // 숫자만 남김 + setAmount(onlyNumbers === '' ? 0 : parseInt(onlyNumbers)); + }} + inputMode="numeric" // 모바일 키보드 숫자 전용 + pattern="[0-9]*" // 브라우저 기본 숫자만 유효하도록 + />
+
-
-
할부기간 *
-
- -
+
+
할부기간 *
+
+
+
-
-
구매자명 *
-
- ) => setBuyerName(e.target.value) } - /> -
+
+
구매자명 *
+
+ ) => setBuyerName(e.target.value)} + />
+
-
-
휴대폰 번호 *
-
- ) => setPhoneNumber(e.target.value) } - /> -
+
+
휴대폰 번호 *
+
+ ) => { + const onlyNumbers = e.target.value.replace(/[^0-9]/g, ''); + setPhoneNumber(onlyNumbers); + }} + className={phoneNumber && !isValidPhoneNumber(phoneNumber) ? 'error' : ''} + inputMode="numeric" + pattern="[0-9]*" + maxLength={11} + />
+
-
-
이메일
-
- ) => setEamil(e.target.value) } - /> -
+
+
이메일
+
+ ) => setEamil(e.target.value)} + />
+
-
-
결제 방식 *
-
- { getArsPaymentMethodBtns() } -
+
+
결제 방식 *
+
+ {getArsPaymentMethodBtns()}
+
-
diff --git a/src/pages/additional-service/key-in-payment/key-in-payment-page.tsx b/src/pages/additional-service/key-in-payment/key-in-payment-page.tsx index 88030b7..765a8c4 100644 --- a/src/pages/additional-service/key-in-payment/key-in-payment-page.tsx +++ b/src/pages/additional-service/key-in-payment/key-in-payment-page.tsx @@ -206,6 +206,7 @@ export const KeyInPaymentPage = () => {
diff --git a/src/pages/additional-service/key-in-payment/requeset-page.tsx b/src/pages/additional-service/key-in-payment/requeset-page.tsx index c905d25..99444e5 100644 --- a/src/pages/additional-service/key-in-payment/requeset-page.tsx +++ b/src/pages/additional-service/key-in-payment/requeset-page.tsx @@ -1,5 +1,6 @@ -import { useState } from 'react'; +import { ChangeEvent, useState } from 'react'; import { PATHS } from '@/shared/constants/paths'; +import { useLocation } from 'react-router'; import { useNavigate } from '@/shared/lib/hooks/use-navigate'; import { IMAGE_ROOT } from '@/shared/constants/common'; import { HeaderType } from '@/entities/common/model/types'; @@ -10,10 +11,29 @@ import { useSetFooterMode, useSetOnBack } from '@/widgets/sub-layout/use-sub-layout'; -import { number } from 'framer-motion'; +import { overlay } from 'overlay-kit'; +import { Dialog } from '@/shared/ui/dialogs/dialog'; export const KeyInPaymentRequestPage = () => { const { navigate } = useNavigate(); + const location = useLocation(); + + const { mid: receivedMid } = location.state || {}; + + const [mid, setMid] = useState(receivedMid || ''); + const [goodsName, setGoodsName] = useState(''); + const [amount, setAmount] = useState(0); + const [buyerName, setBuyerName] = useState(''); + const [email, setEmail] = useState(''); + const [phoneNumber, setPhoneNumber] = useState(''); + const [cardNo1, setCardNo1] = useState(''); + const [cardNo2, setCardNo2] = useState(''); + const [cardNo3, setCardNo3] = useState(''); + const [cardNo4, setCardNo4] = useState(''); + const [cardExpirationMonth, setCardExpirationMonth] = useState(''); + const [cardExpirationYear, setCardExpirationYear] = useState(''); + const [instmntMonth, setInstmntMonth] = useState('00'); + const [moid, setMoid] = useState(''); const { mutateAsync: keyInApply } = useExtensionKeyinApplyMutation(); @@ -25,31 +45,99 @@ export const KeyInPaymentRequestPage = () => { }); const callKeyInPaymentRequest = () => { + const cardNo = `${cardNo1}${cardNo2}${cardNo3}${cardNo4}`; + const cardExpirationDate = `${cardExpirationMonth}${cardExpirationYear}`; + let keyInApplyParams = { - mid: 'string', - goodsName: 'string', - amount: 0, - buyerName: 'string', - email: 'string', - phoneNumber: 'string', - cardNo: 'string', - cardExpirationDate: 'string', - instmntMonth: 'string', - moid: 'SMS', + mid: mid, + goodsName: goodsName, + amount: amount, + buyerName: buyerName, + email: email, + phoneNumber: phoneNumber, + cardNo: cardNo, + cardExpirationDate: cardExpirationDate, + instmntMonth: instmntMonth, + moid: moid, }; + keyInApply(keyInApplyParams).then((rs) => { - navigate(PATHS.additionalService.keyInPayment.requestSuccess); - console.log(rs) - }).catch(() => { - - }).finally(() => { - + console.log('결제 성공:', rs); + showSuccessDialog(); + }).catch((error) => { + console.error('결제 실패:', error); + showErrorDialog(error?.message || '결제에 실패했습니다'); }); - }; + const showSuccessDialog = () => { + overlay.open(({ isOpen, close, unmount }) => { + return ( + { + close(); + navigate(PATHS.additionalService.keyInPayment.list); + }} + message="결제가 성공적으로 처리되었습니다" + buttonLabel={['확인']} + /> + ); + }); + }; + const showErrorDialog = (errorMessage: string) => { + overlay.open(({ isOpen, close, unmount }) => { + return ( + + ); + }); + }; + const isValidPhoneNumber = (phone: string) => { + const phoneRegex = /^01[0|1|6|7|8|9][0-9]{7,8}$/; + return phoneRegex.test(phone); + }; + + const isValidEmail = (email: string) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + const isValidCardNumber = () => { + return cardNo1.length === 4 && cardNo2.length === 4 && + cardNo3.length === 4 && cardNo4.length === 4; + }; + + const isValidCardExpiration = () => { + if (cardExpirationMonth.length !== 2 || cardExpirationYear.length !== 2) { + return false; + } + const month = parseInt(cardExpirationMonth); + return month >= 1 && month <= 12; + }; + + const isFormValid = () => { + return ( + mid.trim() !== '' && + goodsName.trim() !== '' && + amount > 0 && + buyerName.trim() !== '' && + isValidEmail(email) && + isValidPhoneNumber(phoneNumber) && + isValidCardNumber() && + isValidCardExpiration() + ); + }; const onClickToRequest = () => { callKeyInPaymentRequest(); @@ -65,11 +153,11 @@ export const KeyInPaymentRequestPage = () => {
가맹점 *
- +
@@ -78,7 +166,8 @@ export const KeyInPaymentRequestPage = () => {
) => setGoodsName(e.target.value)} />
@@ -88,7 +177,13 @@ export const KeyInPaymentRequestPage = () => {
) => { + const onlyNumbers = e.target.value.replace(/[^0-9]/g, ''); // 숫자만 남김 + setAmount(onlyNumbers === '' ? 0 : parseInt(onlyNumbers)); + }} + inputMode="numeric" // 모바일 키보드 숫자 전용 + pattern="[0-9]*" // 브라우저 기본 숫자만 유효하도록 />
@@ -98,7 +193,8 @@ export const KeyInPaymentRequestPage = () => {
) => setBuyerName(e.target.value)} />
@@ -108,7 +204,10 @@ export const KeyInPaymentRequestPage = () => {
) => setEmail(e.target.value)} + className={email && !isValidEmail(email) ? 'error' : ''} />
@@ -118,8 +217,16 @@ export const KeyInPaymentRequestPage = () => {
) => { + const onlyNumbers = e.target.value.replace(/[^0-9]/g, ''); + setPhoneNumber(onlyNumbers); + }} + className={phoneNumber && !isValidPhoneNumber(phoneNumber) ? 'error' : ''} + inputMode="numeric" + pattern="[0-9]*" + maxLength={11} />
@@ -129,36 +236,90 @@ export const KeyInPaymentRequestPage = () => {
) => { + const onlyNumbers = e.target.value.replace(/[^0-9]/g, ''); + if (onlyNumbers.length <= 4) setCardNo1(onlyNumbers); + }} + inputMode="numeric" + pattern="[0-9]*" + maxLength={4} + placeholder="1234" />
) => { + const onlyNumbers = e.target.value.replace(/[^0-9]/g, ''); + if (onlyNumbers.length <= 4) setCardNo2(onlyNumbers); + }} + inputMode="numeric" + pattern="[0-9]*" + maxLength={4} + placeholder="5678" />
) => { + const onlyNumbers = e.target.value.replace(/[^0-9]/g, ''); + if (onlyNumbers.length <= 4) setCardNo3(onlyNumbers); + }} + inputMode="numeric" + pattern="[0-9]*" + maxLength={4} + placeholder="9012" />
) => { + const onlyNumbers = e.target.value.replace(/[^0-9]/g, ''); + if (onlyNumbers.length <= 4) setCardNo4(onlyNumbers); + }} + inputMode="numeric" + pattern="[0-9]*" + maxLength={4} + placeholder="3456" />
유효기간(월/년)*
-
+
) => { + const onlyNumbers = e.target.value.replace(/[^0-9]/g, ''); + if (onlyNumbers.length <= 2) setCardExpirationMonth(onlyNumbers); + }} + inputMode="numeric" + pattern="[0-9]*" + maxLength={2} + placeholder='MM' + style={{flex: 1}} + /> + / + ) => { + const onlyNumbers = e.target.value.replace(/[^0-9]/g, ''); + if (onlyNumbers.length <= 2) setCardExpirationYear(onlyNumbers); + }} + inputMode="numeric" + pattern="[0-9]*" + maxLength={2} + placeholder='YY' + style={{flex: 1}} />
@@ -167,9 +328,26 @@ export const KeyInPaymentRequestPage = () => {
할부 기간*
@@ -179,7 +357,8 @@ export const KeyInPaymentRequestPage = () => {
) => setMoid(e.target.value)} />
@@ -188,7 +367,8 @@ export const KeyInPaymentRequestPage = () => {
diff --git a/src/pages/additional-service/link-payment/apply/link-payment-apply-confirm-page.tsx b/src/pages/additional-service/link-payment/apply/link-payment-apply-confirm-page.tsx index 39b8be7..1c7a3eb 100644 --- a/src/pages/additional-service/link-payment/apply/link-payment-apply-confirm-page.tsx +++ b/src/pages/additional-service/link-payment/apply/link-payment-apply-confirm-page.tsx @@ -95,7 +95,6 @@ export const LinkPaymentApplyConfirmPage = () => { onClick={() => onClickToConfirm()} >결제 신청 -
); diff --git a/src/pages/additional-service/link-payment/apply/link-payment-apply-page.tsx b/src/pages/additional-service/link-payment/apply/link-payment-apply-page.tsx index 6b612b4..921deba 100644 --- a/src/pages/additional-service/link-payment/apply/link-payment-apply-page.tsx +++ b/src/pages/additional-service/link-payment/apply/link-payment-apply-page.tsx @@ -111,6 +111,7 @@ export const LinkPaymentApplyPage = () => { + ); }; \ No newline at end of file diff --git a/src/pages/additional-service/link-payment/apply/link-payment-apply-success-page.tsx b/src/pages/additional-service/link-payment/apply/link-payment-apply-success-page.tsx index 356143a..2d784d3 100644 --- a/src/pages/additional-service/link-payment/apply/link-payment-apply-success-page.tsx +++ b/src/pages/additional-service/link-payment/apply/link-payment-apply-success-page.tsx @@ -11,7 +11,7 @@ export const LinkPaymentApplySuccessPage = () => { useSetFooterMode(false); const onClickToHome = () => { - navigate(PATHS.home); + navigate(PATHS.additionalService.linkPayment.base); }; return ( diff --git a/src/pages/additional-service/link-payment/link-payment-detail-page.tsx b/src/pages/additional-service/link-payment/link-payment-detail-page.tsx index 1fa389e..404ceb2 100644 --- a/src/pages/additional-service/link-payment/link-payment-detail-page.tsx +++ b/src/pages/additional-service/link-payment/link-payment-detail-page.tsx @@ -76,7 +76,7 @@ export const LinkPaymentDetailPage = () => { setShowPayment(!showPayment); }; - const onClickToCancel = () => { + const onClickToResend = () => { let msg = '재발송 하시겠습니까?'; overlay.open(({ @@ -96,6 +96,12 @@ export const LinkPaymentDetailPage = () => { ); }); }; + + const onClickToSeparateApproval = () => { + navigate(PATHS.additionalService.linkPayment.separateApproval, { + state: { mid, tid } + }); + }; useEffect(() => { callDetail(); }, []); @@ -122,12 +128,18 @@ export const LinkPaymentDetailPage = () => { detailInfo={detailInfo} > - -
- +
+ +
+ {/*
+ +
*/}
diff --git a/src/pages/additional-service/link-payment/separate-approval/link-payment-separate-approval-fail.tsx b/src/pages/additional-service/link-payment/separate-approval/link-payment-separate-approval-fail.tsx new file mode 100644 index 0000000..edd3d5f --- /dev/null +++ b/src/pages/additional-service/link-payment/separate-approval/link-payment-separate-approval-fail.tsx @@ -0,0 +1,69 @@ +import { motion } from 'framer-motion'; +import { useNavigate } from '@/shared/lib/hooks/use-navigate'; +import { PATHS } from "@/shared/constants/paths"; +import { + FilterMotionDuration, + FilterMotionStyle, + FilterMotionVariants +} from '@/entities/common/model/constant'; + +export interface LinkPaymentApplyFailPageProps { + pageOn: boolean; + setPageOn: (pageOn: boolean) => void; + errorMessage?: string; +} + +export const LinkPaymentApplyFailPage = ({ + pageOn, + setPageOn, + errorMessage +}: LinkPaymentApplyFailPageProps) => { + const { navigate } = useNavigate(); + + const onClickToClose = () => { + setPageOn(false); + navigate(PATHS.additionalService.linkPayment.shippingHistory); + }; + + return ( + <> + +
+
+
+ +

+ 링크결제_분리승인 +
+ 처리에 실패했습니다 + +

+
+

+ 결과 : + {errorMessage || '다시 시도해 주세요'} +

+
+
+
+ +
+
+
+
+ + ); +}; diff --git a/src/pages/additional-service/link-payment/separate-approval/link-payment-separate-approval-page.tsx b/src/pages/additional-service/link-payment/separate-approval/link-payment-separate-approval-page.tsx new file mode 100644 index 0000000..41b8556 --- /dev/null +++ b/src/pages/additional-service/link-payment/separate-approval/link-payment-separate-approval-page.tsx @@ -0,0 +1,297 @@ +import { useEffect, useState } from 'react'; +import { PATHS } from '@/shared/constants/paths'; +import { useNavigate } from '@/shared/lib/hooks/use-navigate'; +import { HeaderType } from '@/entities/common/model/types'; +import { + useSetOnBack, + useSetHeaderTitle, + useSetHeaderType, + useSetFooterMode +} from '@/widgets/sub-layout/use-sub-layout'; +import { ExtendedPeriodBottomSheet } from '@/entities/additional-service/ui/link-payment/bottom-sheet/extended-period-bottom-sheet'; +import { LinkBreakBottomSheet } from '@/entities/additional-service/ui/link-payment/bottom-sheet/link-break-bottom-sheet'; +import { LinkPaymentApplySuccessPage } from './link-payment-separate-approval-success-page'; +import { LinkPaymentApplyFailPage } from './link-payment-separate-approval-fail'; + +interface SeparateApprovalItem { + tid: string; + merchantId: string; + amount: number; + status: string; + validityPeriod: string; + approvalCount: number; +} + +export const LinkPaymentSeparateApprovalPage = () => { + const { navigate } = useNavigate(); + + const [selectedItems, setSelectedItems] = useState([]); + const [extendedPeriodBottomSheetOn, setExtendedPeriodBottomSheetOn] = useState(false); + const [linkBreakBottomSheetOn, setLinkBreakBottomSheetOn] = useState(false); + const [successPageOn, setSuccessPageOn] = useState(false); + const [failPageOn, setFailPageOn] = useState(false); + const [resultMessage, setResultMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + + const [mainItem] = useState({ + tid: 'NLPDAR2025101315330', + merchantId: '100,000', + amount: 100000, + status: '활성화', + validityPeriod: '2025/10/13', + approvalCount: 0 + }); + + const [additionalItems] = useState([ + { + tid: '', + merchantId: '(NNN,NNN)', + amount: 0, + status: '(결제상태)', + validityPeriod: 'YYYY/MM/DD', + approvalCount: 0 + } + ]); + + useSetHeaderTitle('분리승인 상세'); + useSetHeaderType(HeaderType.RightClose); + useSetOnBack(() => { + navigate(PATHS.additionalService.linkPayment.shippingHistory); + }); + useSetFooterMode(false); + + const handleCheckboxChange = (tid: string) => { + setSelectedItems(prev => { + if (prev.includes(tid)) { + return prev.filter(id => id !== tid); + } else { + return [...prev, tid]; + } + }); + }; + + const handleMainCheckboxChange = () => { + const mainTid = mainItem.tid; + setSelectedItems(prev => { + if (prev.includes(mainTid)) { + return prev.filter(id => id !== mainTid); + } else { + return [...prev, mainTid]; + } + }); + }; + + const handleAdditionalCheckboxChange = (index: number) => { + const itemTid = `additional-${index}`; + handleCheckboxChange(itemTid); + }; + + const onClickToValidityPeriod = () => { + // 기간연장 바텀시트 열기 + setExtendedPeriodBottomSheetOn(true); + }; + + const onClickToSendLink = () => { + // 링크중단 바텀시트 열기 + setLinkBreakBottomSheetOn(true); + }; + + const handleExtendPeriod = () => { + // 바텀시트 닫기 + setExtendedPeriodBottomSheetOn(false); + + // 기간연장 API 호출 로직 + console.log('기간연장 실행', selectedItems); + // TODO: 실제 API 호출 + const apiCallSuccess = true; // 임시로 성공으로 설정 + + if (apiCallSuccess) { + setResultMessage('기간이 성공적으로 연장되었습니다'); + setSuccessPageOn(true); + } else { + setErrorMessage('기간 연장에 실패했습니다'); + setFailPageOn(true); + } + }; + + const handleLinkBreak = () => { + // 바텀시트 닫기 + setLinkBreakBottomSheetOn(false); + + // 링크중단 API 호출 로직 + console.log('링크중단 실행', selectedItems); + // TODO: 실제 API 호출 + const apiCallSuccess = true; // 임시로 성공으로 설정 + + if (apiCallSuccess) { + setResultMessage('링크가 성공적으로 중단되었습니다'); + setSuccessPageOn(true); + } else { + setErrorMessage('링크 중단에 실패했습니다'); + setFailPageOn(true); + } + }; + + useEffect(() => { + // API 호출하여 데이터 로드 + // const { mid, tid } = location.state || {}; + }, []); + + return ( + <> +
+
+
+
+
+

※ 연장 기간: 최대 7일, 총 3번 연장 가능

+

※ 링크 중단: 유효기간 전, 결제를 마감하는 기능, 링크중단 시 원복 불가

+
+ +
+ {/* Main Item */} +
+
+ + [MAIN] + {mainItem.tid} +
+
+
    +
  • + • 거래금액: + {mainItem.merchantId} +
  • +
  • + • 결제상태: + {mainItem.status} +
  • +
  • + • 유효기간: + {mainItem.validityPeriod} +
  • +
  • + • 연장횟수: + {mainItem.approvalCount} +
  • +
+
+
+
+ + +
+
+
+ + {/* Additional Items */} + {additionalItems.map((item, index) => ( +
+ handleAdditionalCheckboxChange(index)} + className="card-checkbox" + /> +
+ (유형) + 주문번호 +
+
+
    +
  • + • 거래금액: + {item.merchantId} +
  • +
  • + • 결제상태: + {item.status} +
  • +
  • + • 유효기간: + {item.validityPeriod} +
  • +
  • + • 연장횟수: + (N) +
  • +
+
+
+
+ + +
+
+
+ ))} +
+
+ +
+ + +
+
+
+
+ + + + + + + + + + ); +}; \ No newline at end of file diff --git a/src/pages/additional-service/link-payment/separate-approval/link-payment-separate-approval-success-page.tsx b/src/pages/additional-service/link-payment/separate-approval/link-payment-separate-approval-success-page.tsx new file mode 100644 index 0000000..4af1e32 --- /dev/null +++ b/src/pages/additional-service/link-payment/separate-approval/link-payment-separate-approval-success-page.tsx @@ -0,0 +1,66 @@ +import { motion } from 'framer-motion'; +import { useNavigate } from '@/shared/lib/hooks/use-navigate'; +import { PATHS } from "@/shared/constants/paths"; +import { + FilterMotionDuration, + FilterMotionStyle, + FilterMotionVariants +} from '@/entities/common/model/constant'; + +export interface LinkPaymentApplySuccessPageProps { + pageOn: boolean; + setPageOn: (pageOn: boolean) => void; + resultMessage?: string; +} + +export const LinkPaymentApplySuccessPage = ({ + pageOn, + setPageOn, + resultMessage +}: LinkPaymentApplySuccessPageProps) => { + const { navigate } = useNavigate(); + + const onClickToClose = () => { + setPageOn(false); + navigate(PATHS.additionalService.linkPayment.shippingHistory); + }; + + return ( + <> + +
+
+
+ +

+ 링크결제_분리승인 +

+
+

+ 결과 : + {resultMessage || '성공적으로 처리되었습니다'} +

+
+
+
+ +
+
+
+
+ + ); +}; \ No newline at end of file diff --git a/src/shared/constants/paths.ts b/src/shared/constants/paths.ts index 4537206..c32386f 100644 --- a/src/shared/constants/paths.ts +++ b/src/shared/constants/paths.ts @@ -227,6 +227,10 @@ export const PATHS: RouteNamesType = { pendingDetail: generatePath( `${ROUTE_NAMES.additionalService.base}${ROUTE_NAMES.additionalService.linkPayment.base}`, ROUTE_NAMES.additionalService.linkPayment.pendingDetail, + ), + separateApproval: generatePath( + `${ROUTE_NAMES.additionalService.base}${ROUTE_NAMES.additionalService.linkPayment.base}`, + ROUTE_NAMES.additionalService.linkPayment.separateApproval ) }, alimtalk: { diff --git a/src/shared/constants/route-names.ts b/src/shared/constants/route-names.ts index d9964a1..a3bc439 100644 --- a/src/shared/constants/route-names.ts +++ b/src/shared/constants/route-names.ts @@ -101,7 +101,8 @@ export const ROUTE_NAMES = { requestConfirm: 'request-confirm', confirmSuccess: 'confirm-success', detail: 'detail', - pendingDetail: 'pending-detail' + pendingDetail: 'pending-detail', + separateApproval: 'separate-approval', }, alimtalk: { base: '/alimtalk/*', diff --git a/src/shared/ui/assets/css/style.css b/src/shared/ui/assets/css/style.css index 1ee5e7b..3c7be67 100644 --- a/src/shared/ui/assets/css/style.css +++ b/src/shared/ui/assets/css/style.css @@ -1261,7 +1261,7 @@ input[type="radio"] { border-radius: 50%; background: var(--color-CCCCCC); cursor: pointer; - transition: all 0.2s ease; + transition: all 0.4s ease; } .banner-dot.active { @@ -5948,3 +5948,223 @@ ul.txn-amount-detail li span:last-child { box-sizing: border-box; } +/* 분할승인 상세 */ +.separate-approval-main { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100%; + height: 100%; +} + +.separate-approval-main .tab-content { + height: 100%; +} + +.separate-approval-main .tab-pane.sub.active { + height: 100%; + display: flex; + flex-direction: column; +} + +.separate-approval-section { + padding: 0; + flex: 1; + overflow-y: auto; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; +} + +.approval-cards-wrapper { + padding-bottom: 20px; +} + +.separate-approval-main .apply-row.two-button { + flex-shrink: 0; + position: sticky; + bottom: 0; + background: white; + z-index: 10; +} + +/* 분할승인 안내 박스 */ +.approval-notice-box { + background: var(--color-white); + padding: 8px; + margin-bottom: 16px; + border-radius: 12px; + flex-shrink: 0; +} + +.approval-notice-box p { + font-size: var(--fs-15); + color: var(--color-666666); + line-height: 1.5; + margin: 0; +} + +.approval-notice-box p + p { + margin-top: 4px; +} + +/* 분할승인 카드 */ +.approval-card { + position: relative; + background: var(--color-white); + border: 2px solid var(--color-d6d6d6); + border-radius: 16px; + padding: 16px; + margin-bottom: 16px; + transition: all 0.4s ease; +} + +.approval-card.selected { + border-color: var(--color-3E6AFC); + background: var(--color-F4F8FF); +} + +/* 분할승인 체크박스 */ +.approval-card .card-checkbox { + display: block !important; + position: absolute; + top: 16px; + left: 16px; + width: 24px; + height: 24px; + margin: 0; + padding: 0; + appearance: none; + border: 2px solid var(--color-d6d6d6); + border-radius: 5px; + background-color: var(--color-white); + cursor: pointer; + outline: none; + box-sizing: border-box; + z-index: 1; +} + +.approval-card .card-checkbox:focus, +.approval-card .card-checkbox:active { + outline: none !important; + box-shadow: none !important; + border-color: var(--color-d6d6d6); +} + +.approval-card .card-checkbox:checked { + background-color: var(--color-3E6AFC); + border-color: var(--color-3E6AFC); + outline: none; +} + +.approval-card .card-checkbox:checked::after { + content: ''; + position: absolute; + left: 5px; + top: 2px; + width: 6px; + height: 11px; + border: solid white; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +/* 분할승인 카드 헤더 */ +.approval-card .card-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; + padding-bottom: 12px; + padding-left: 36px; + border-bottom: 1px solid var(--color-E5E5E5); +} + +.approval-card .card-tag { + font-size: var(--fs-14); + font-weight: var(--fw-700); + padding: 2px 8px; + border-radius: 4px; + color: var(--color-666666); + background: var(--color-F3F3F3); + transition: all 0.4s ease; +} + +.approval-card .card-tag.main-tag { + color: var(--color-3E6AFC); + background: var(--color-E9F1FB); +} + +.approval-card.selected .card-tag { + color: var(--color-3E6AFC); + background: var(--color-E9F1FB); +} + +.approval-card .card-tid { + font-size: var(--fs-16); + color: var(--color-2D3436); + font-weight: var(--fw-500); +} + +/* 분할승인 카드 바디 */ +.approval-card .card-body { + margin-bottom: 16px; +} + +.approval-card .info-list { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 8px; +} + +.approval-card .info-list li { + display: flex; + align-items: center; + font-size: var(--fs-14); + color: var(--color-2D3436); +} + +.approval-card .info-list .label { + min-width: 85px; + color: var(--color-666666); + font-weight: var(--fw-400); +} + +.approval-card .info-list .value { + color: var(--color-2D3436); + font-weight: var(--fw-500); +} + +/* 분할승인 카드 푸터 */ +.approval-card .card-footer { + padding-top: 12px; + border-top: 1px solid var(--color-E5E5E5); +} + +.approval-card .period-selector { + display: flex; + align-items: center; + gap: 12px; +} + +.approval-card .period-selector label { + font-size: var(--fs-14); + color: var(--color-666666); + font-weight: var(--fw-500); + min-width: 60px; +} + +.approval-card .period-selector select { + flex: 1; + height: 36px; + font-size: var(--fs-14); + padding: 6px 30px 6px 12px; + border: 1px solid var(--color-d6d6d6); + border-radius: 4px; + background-color: var(--color-white); + transition: all 0.2s ease; +} \ No newline at end of file