diff --git a/src/assets/close.svg b/src/assets/close.svg new file mode 100644 index 000000000..bf30c21f1 --- /dev/null +++ b/src/assets/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/card/components/CardPayments/CardPayments.tsx b/src/card/components/CardPayments/CardPayments.tsx new file mode 100644 index 000000000..968519dbd --- /dev/null +++ b/src/card/components/CardPayments/CardPayments.tsx @@ -0,0 +1,54 @@ +import { + CardAddForm, + CardCompleteForm, + CardListForm, + CardPageIndex, + CardPaymentForm, + CardProvider, + CardState, + PaymentForm, +} from '@/card'; +import { AppDisplay, Funnel, OverlayProvider } from '@/shared'; + +type CardPaymentsProps = PaymentForm & { + cardStorageKey: string; + initialOwnerCards: CardState[]; + onClose?: () => void; +}; + +export const CardPayments = ({ + cardStorageKey, + orderId, + totalAmount, + initialOwnerCards, + onPaymentComplete, + onPaymentCancel, + onClose, +}: CardPaymentsProps) => ( + + + + + + + + + + + + + + + + + + + + +); diff --git a/src/card/components/CardPayments/hook/useLoadNearPayments.tsx b/src/card/components/CardPayments/hook/useLoadNearPayments.tsx new file mode 100644 index 000000000..e5f788832 --- /dev/null +++ b/src/card/components/CardPayments/hook/useLoadNearPayments.tsx @@ -0,0 +1,79 @@ +import { CardPayments, isValidateCardState, PaymentForm } from '@/card'; +import { useModal } from '@/shared'; + +const ERROR_CODES = { + INVALID_CLIENT_ID: 'INVALID_CLIENT_ID', + INVALID_ORDER_ID: 'INVALID_ORDER_ID', + INVALID_AMOUNT: 'INVALID_AMOUNT', + INVALID_OWNER_CARDS: 'INVALID_OWNER_CARDS', + INVALID_PAYMENT_COMPLETE_CALLBACK: 'INVALID_PAYMENT_COMPLETE_CALLBACK', + INVALID_PAYMENT_CANCEL_CALLBACK: 'INVALID_PAYMENT_CANCEL_CALLBACK', + PAYMENT_PROCESS_ERROR: 'PAYMENT_PROCESS_ERROR', +} as const; + +const ERROR_MESSAGES = { + [ERROR_CODES.INVALID_CLIENT_ID]: '유효하지 않은 CLIENT ID입니다.', + [ERROR_CODES.INVALID_ORDER_ID]: '유효하지 않은 주문 번호입니다.', + [ERROR_CODES.INVALID_AMOUNT]: '유효하지 않은 금액입니다.', + [ERROR_CODES.INVALID_OWNER_CARDS]: '유효하지 않은 카드 정보입니다.', + [ERROR_CODES.INVALID_PAYMENT_COMPLETE_CALLBACK]: '결제 완료 콜백 함수가 유효하지 않습니다.', + [ERROR_CODES.INVALID_PAYMENT_CANCEL_CALLBACK]: '결제 취소 콜백 함수가 유효하지 않습니다.', + [ERROR_CODES.PAYMENT_PROCESS_ERROR]: '결제 프로세스 중 오류가 발생했습니다.', +}; + +const createError = (code: keyof typeof ERROR_CODES, error?: unknown) => ({ + code, + message: ERROR_MESSAGES[code], + error, +}); + +type UseLoadNearPaymentsProps = { + clientId: string; +}; + +export const useLoadNearPayments = ({ clientId }: UseLoadNearPaymentsProps) => { + const showModal = useModal(); + const cardStorageKey = `near-payments-${clientId}`; + const ownerCards = JSON.parse(localStorage.getItem(cardStorageKey) ?? '[]'); + + return ({ orderId, totalAmount, onPaymentComplete, onPaymentCancel }: PaymentForm) => { + if (!clientId || clientId.trim() === '') { + throw createError(ERROR_CODES.INVALID_ORDER_ID); + } + + if (!orderId || orderId.trim() === '') { + throw createError(ERROR_CODES.INVALID_ORDER_ID); + } + + if (Number.isNaN(totalAmount) || totalAmount <= 0) { + throw createError(ERROR_CODES.INVALID_AMOUNT); + } + + if (ownerCards.length > 0 && !ownerCards.every((card: any) => isValidateCardState(card))) { + throw createError(ERROR_CODES.INVALID_OWNER_CARDS); + } + + if (typeof onPaymentComplete !== 'function') { + throw createError(ERROR_CODES.INVALID_PAYMENT_COMPLETE_CALLBACK); + } + + if (typeof onPaymentCancel !== 'function') { + throw createError(ERROR_CODES.INVALID_PAYMENT_CANCEL_CALLBACK); + } + + try { + return showModal( + , + ); + } catch (error) { + throw createError(ERROR_CODES.PAYMENT_PROCESS_ERROR, error); + } + }; +}; diff --git a/src/card/components/CardPayments/index.ts b/src/card/components/CardPayments/index.ts new file mode 100644 index 000000000..e4213392f --- /dev/null +++ b/src/card/components/CardPayments/index.ts @@ -0,0 +1,2 @@ +export * from './CardPayments'; +export * from './hook/useLoadNearPayments'; diff --git a/src/card/hooks/loadNearPayments.tsx b/src/card/hooks/loadNearPayments.tsx new file mode 100644 index 000000000..e5f788832 --- /dev/null +++ b/src/card/hooks/loadNearPayments.tsx @@ -0,0 +1,79 @@ +import { CardPayments, isValidateCardState, PaymentForm } from '@/card'; +import { useModal } from '@/shared'; + +const ERROR_CODES = { + INVALID_CLIENT_ID: 'INVALID_CLIENT_ID', + INVALID_ORDER_ID: 'INVALID_ORDER_ID', + INVALID_AMOUNT: 'INVALID_AMOUNT', + INVALID_OWNER_CARDS: 'INVALID_OWNER_CARDS', + INVALID_PAYMENT_COMPLETE_CALLBACK: 'INVALID_PAYMENT_COMPLETE_CALLBACK', + INVALID_PAYMENT_CANCEL_CALLBACK: 'INVALID_PAYMENT_CANCEL_CALLBACK', + PAYMENT_PROCESS_ERROR: 'PAYMENT_PROCESS_ERROR', +} as const; + +const ERROR_MESSAGES = { + [ERROR_CODES.INVALID_CLIENT_ID]: '유효하지 않은 CLIENT ID입니다.', + [ERROR_CODES.INVALID_ORDER_ID]: '유효하지 않은 주문 번호입니다.', + [ERROR_CODES.INVALID_AMOUNT]: '유효하지 않은 금액입니다.', + [ERROR_CODES.INVALID_OWNER_CARDS]: '유효하지 않은 카드 정보입니다.', + [ERROR_CODES.INVALID_PAYMENT_COMPLETE_CALLBACK]: '결제 완료 콜백 함수가 유효하지 않습니다.', + [ERROR_CODES.INVALID_PAYMENT_CANCEL_CALLBACK]: '결제 취소 콜백 함수가 유효하지 않습니다.', + [ERROR_CODES.PAYMENT_PROCESS_ERROR]: '결제 프로세스 중 오류가 발생했습니다.', +}; + +const createError = (code: keyof typeof ERROR_CODES, error?: unknown) => ({ + code, + message: ERROR_MESSAGES[code], + error, +}); + +type UseLoadNearPaymentsProps = { + clientId: string; +}; + +export const useLoadNearPayments = ({ clientId }: UseLoadNearPaymentsProps) => { + const showModal = useModal(); + const cardStorageKey = `near-payments-${clientId}`; + const ownerCards = JSON.parse(localStorage.getItem(cardStorageKey) ?? '[]'); + + return ({ orderId, totalAmount, onPaymentComplete, onPaymentCancel }: PaymentForm) => { + if (!clientId || clientId.trim() === '') { + throw createError(ERROR_CODES.INVALID_ORDER_ID); + } + + if (!orderId || orderId.trim() === '') { + throw createError(ERROR_CODES.INVALID_ORDER_ID); + } + + if (Number.isNaN(totalAmount) || totalAmount <= 0) { + throw createError(ERROR_CODES.INVALID_AMOUNT); + } + + if (ownerCards.length > 0 && !ownerCards.every((card: any) => isValidateCardState(card))) { + throw createError(ERROR_CODES.INVALID_OWNER_CARDS); + } + + if (typeof onPaymentComplete !== 'function') { + throw createError(ERROR_CODES.INVALID_PAYMENT_COMPLETE_CALLBACK); + } + + if (typeof onPaymentCancel !== 'function') { + throw createError(ERROR_CODES.INVALID_PAYMENT_CANCEL_CALLBACK); + } + + try { + return showModal( + , + ); + } catch (error) { + throw createError(ERROR_CODES.PAYMENT_PROCESS_ERROR, error); + } + }; +}; diff --git a/src/card/types/PaymentForm.type.ts b/src/card/types/PaymentForm.type.ts new file mode 100644 index 000000000..f097f0e9d --- /dev/null +++ b/src/card/types/PaymentForm.type.ts @@ -0,0 +1,8 @@ +import { PaymentResult } from './PaymentResult.type'; + +export type PaymentForm = { + orderId: string; + totalAmount: number; + onPaymentCancel: (paymentResult: Pick) => void; + onPaymentComplete: (paymentResult: PaymentResult) => void; +}; diff --git a/src/card/types/PaymentResult.type.ts b/src/card/types/PaymentResult.type.ts new file mode 100644 index 000000000..caf5d45be --- /dev/null +++ b/src/card/types/PaymentResult.type.ts @@ -0,0 +1,10 @@ +export type PaymentResult = { + success: boolean; + message: string; + orderId: string; + totalAmount: number; + cardNumber: string; + paymentTimestamp: number; +}; + +export type PaymentCancel = Pick;