Skip to content

Commit

Permalink
Merge pull request #119 from sw326/main
Browse files Browse the repository at this point in the history
payment
  • Loading branch information
sw326 authored Sep 19, 2024
2 parents 52e65d4 + aa0b628 commit ad1ee8e
Show file tree
Hide file tree
Showing 9 changed files with 242 additions and 64 deletions.
18 changes: 14 additions & 4 deletions src/api/payment.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import api from './axiosConfig';
import { PaymentRequest, PaymentResponse } from '../types/portone';
import {
CompletePaymentRequest,
CompletePaymentResponse,
PaymentRequest,
PaymentResponse,
} from '../types/portone';

export const getPaymentData = async (
estimateId: number,
commissionId: number,
): Promise<PaymentRequest> => {
const response = await api.get<PaymentRequest>(
`/payments/data/${estimateId}`,
`/payments/data?estimateId=${estimateId}&commissionId=${commissionId}`,
);
return response.data;
};

export const completePayment = async (
impUid: string,
): Promise<PaymentResponse> => {
const response = await api.get<PaymentResponse>(`/payments/${impUid}`);
paymentData: CompletePaymentRequest,
): Promise<CompletePaymentResponse> => {
const response = await api.post<CompletePaymentResponse>(
`/payments/complete/${impUid}`,
paymentData,
);
return response.data;
};

Expand Down
22 changes: 16 additions & 6 deletions src/hooks/usePayment.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import * as api from '../api/payment';
import { PaymentRequest, PaymentResponse } from '../types/portone';
import {
CompletePaymentRequest,
CompletePaymentResponse,
PaymentRequest,
PaymentResponse,
} from '../types/portone';

export const useGetPaymentData = (estimateId: number) => {
export const useGetPaymentData = (estimateId: number, commissionId: number) => {
return useQuery<PaymentRequest, Error>({
queryKey: ['paymentData', estimateId],
queryFn: () => api.getPaymentData(estimateId),
queryKey: ['paymentData', estimateId, commissionId],
queryFn: () => api.getPaymentData(estimateId, commissionId),
});
};

export const useCompletePayment = () => {
return useMutation<PaymentResponse, Error, string>({
mutationFn: (impUid: string) => api.completePayment(impUid),
return useMutation<
CompletePaymentResponse,
Error,
{ impUid: string; paymentData: CompletePaymentRequest }
>({
mutationFn: ({ impUid, paymentData }) =>
api.completePayment(impUid, paymentData),
});
};

Expand Down
4 changes: 3 additions & 1 deletion src/pages/members/EstimateDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ const EstimateDetail: React.FC = () => {
);

const handlePaymentNavigation = () => {
navigate('/payment', { state: { estimateId: data.id } });
navigate('/payment', {
state: { estimateId: data.id, commissionId: data.commissionId },
});
};

const InfoItem: React.FC<{
Expand Down
49 changes: 49 additions & 0 deletions src/pages/members/PaymentCancelPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { XCircle } from 'lucide-react';

const PaymentCancelPage: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const cancelInfo = location.state?.cancelInfo;

return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-100">
<div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full">
<div className="flex items-center justify-center mb-6">
<XCircle className="text-red-500 w-16 h-16" />
</div>
<h1 className="text-3xl font-bold mb-4 text-center text-red-600">
결제 취소
</h1>
<p className="text-gray-600 mb-6 text-center">결제가 취소되었습니다.</p>
{cancelInfo && (
<div className="mb-6 text-sm">
<p>
<strong>취소 금액:</strong>{' '}
{cancelInfo.cancelAmount.toLocaleString()}
</p>
<p>
<strong>주문 번호:</strong> {cancelInfo.merchantUid}
</p>
<p>
<strong>취소 사유:</strong> {cancelInfo.reason}
</p>
<p>
<strong>취소 시간:</strong>{' '}
{new Date(cancelInfo.cancelledAt * 1000).toLocaleString()}
</p>
</div>
)}
<button
onClick={() => navigate('/memberhome')}
className="w-full bg-brand text-white py-3 px-4 rounded-lg hover:bg-brand-dark transition-colors text-lg font-bold"
>
홈으로 돌아가기
</button>
</div>
</div>
);
};

export default PaymentCancelPage;
83 changes: 59 additions & 24 deletions src/pages/members/PaymentPage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useCompletePayment } from '../../hooks/usePayment';
import { useGetPaymentData, useCompletePayment } from '../../hooks/usePayment';
import { adaptEstimateToPayment } from '../../utils/paymentAdapter';
import {
PayMethod,
RequestPayParams,
RequestPayResponse,
CompletePaymentRequest,
} from '../../types/portone';
import LoadingSpinner from '../../utils/LoadingSpinner';
import { showErrorNotification } from '../../utils/errorHandler';
Expand All @@ -18,25 +19,29 @@ import {
CreditCard as SimplePayIcon,
} from 'lucide-react';

// Mockup data for testing without backend
const mockPaymentData = {
amount: 10000,
buyer_name: '홍길동',
buyer_tel: '010-1234-5678',
buyer_email: '[email protected]',
};
type ExtendedPayMethod = PayMethod | 'SIMPLE_PAY';

const PaymentPage: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const { estimateId } = location.state as { estimateId: number };
const { estimateId, commissionId } = location.state as {
estimateId: number;
commissionId: number;
};

const [paymentMethod, setPaymentMethod] = useState<PayMethod>('card');
const [paymentMethod, setPaymentMethod] = useState<ExtendedPayMethod>('card');
const [simplePayMethod, setSimplePayMethod] = useState<string>('');
const [isLoading, setIsLoading] = useState(true);
const [isScriptLoading, setIsScriptLoading] = useState(true);

const {
data: paymentData,
isLoading: isDataLoading,
error,
} = useGetPaymentData(estimateId, commissionId);
const completePaymentMutation = useCompletePayment();

console.log('Payment Data:', paymentData);

useEffect(() => {
const jquery = document.createElement('script');
jquery.src = 'https://code.jquery.com/jquery-1.12.4.min.js';
Expand All @@ -46,10 +51,10 @@ const PaymentPage: React.FC = () => {
document.head.appendChild(iamport);

jquery.onload = () => {
if (iamport.readyState === 'complete') {
setIsLoading(false);
if ((iamport as any).readyState === 'complete') {
setIsScriptLoading(false);
} else {
iamport.onload = () => setIsLoading(false);
iamport.onload = () => setIsScriptLoading(false);
}
};

Expand All @@ -59,7 +64,9 @@ const PaymentPage: React.FC = () => {
};
}, []);

if (isLoading) return <LoadingSpinner />;
if (isScriptLoading || isDataLoading) return <LoadingSpinner />;
if (error) return <div>Error: {error.message}</div>;
if (!paymentData) return <div>결제 정보를 불러올 수 없습니다.</div>;

const onClickPayment = () => {
if (!window.IMP) {
Expand All @@ -70,23 +77,51 @@ const PaymentPage: React.FC = () => {
const { IMP } = window;
IMP.init(import.meta.env.VITE_IMP_KEY);

const finalPayMethod =
paymentMethod === 'SIMPLE_PAY' ? simplePayMethod : paymentMethod;
const data: RequestPayParams = adaptEstimateToPayment(
mockPaymentData,
paymentMethod,
paymentData,
finalPayMethod as PayMethod,
);

IMP.request_pay(data, callback);
console.log(data, callback);
};

const callback = async (response: RequestPayResponse) => {
const { success, error_msg, imp_uid } = response;
const {
success,
error_msg,
imp_uid,
merchant_uid,
pay_method,
buyer_email,
buyer_tel,
} = response;

if (success && imp_uid) {
try {
const result = await completePaymentMutation.mutateAsync(imp_uid);
navigate('/payment-success', {
state: { paymentInfo: result.response },
const paymentData: CompletePaymentRequest = {
imp_uid,
merchant_uid: merchant_uid || '',
pay_method: (pay_method as PayMethod) || 'card',
buyer_email: buyer_email || '',
buyer_tel: buyer_tel || '',
};

const result = await completePaymentMutation.mutateAsync({
impUid: imp_uid,
paymentData,
});

if (result.code === 0) {
// Assuming 0 means success, adjust as needed
navigate('/payment-success', {
state: { paymentInfo: result.response },
});
} else {
showErrorNotification(`결제 완료 처리 실패: ${result.message}`);
}
} catch (error) {
showErrorNotification('결제 완료 처리 중 오류가 발생했습니다.');
}
Expand Down Expand Up @@ -126,7 +161,7 @@ const PaymentPage: React.FC = () => {
value={method.id}
checked={paymentMethod === method.id}
onChange={() => {
setPaymentMethod(method.id as PayMethod);
setPaymentMethod(method.id as ExtendedPayMethod);
if (method.id !== 'SIMPLE_PAY') {
setSimplePayMethod('');
}
Expand Down Expand Up @@ -155,7 +190,7 @@ const PaymentPage: React.FC = () => {
<div className="flex justify-between items-center text-lg">
<span>청소 서비스 금액</span>
<span className="font-bold">
{mockPaymentData.amount.toLocaleString()}
{paymentData.estimate_amount.toLocaleString()}
</span>
</div>
</div>
Expand All @@ -181,7 +216,7 @@ const PaymentPage: React.FC = () => {
}
className="w-full bg-brand text-white py-3 px-4 rounded-lg hover:bg-brand-dark transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-lg font-bold"
>
{mockPaymentData.amount.toLocaleString()}원 결제하기
{paymentData.estimate_amount.toLocaleString()}원 결제하기
</button>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/shared/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import MapTest from '../pages/partners/MapTest';
import PaymentPage from '../pages/members/PaymentPage';
import Redirection from '../pages/members/Redirection';
import PaymentSuccessPage from '../pages/members/PaymentSuccessPage';
import PaymentCancelPage from '../pages/members/PaymentCancelPage';

const ProtectedRoute: React.FC<{ allowedRole: 'member' | 'partner' }> = ({
allowedRole,
Expand Down Expand Up @@ -99,6 +100,7 @@ const Router: React.FC = () => {
<Route path="/estimatedetail" element={<EstimateDetail />} />
<Route path="/payment" element={<PaymentPage />} />
<Route path="/payment-success" element={<PaymentSuccessPage />} />
<Route path="/payment-cancel" element={<PaymentCancelPage />} />
</Route>
</Route>

Expand Down
40 changes: 23 additions & 17 deletions src/types/portone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,42 +8,48 @@ export type PayMethod =
| 'samsungpay'
| 'ssgpay';

export interface EstimateDetail {
id: number;
commissionId: number;
partnerId: number;
price: number;
fixedDate: string;
statement: string;
status: string;
size: number;
desiredDate: string;
significant: string;
commissionStatus: string;
phoneNumber: string;
managerName: string;
companyName: string;
export enum CleanType {
NORMAL = 'NORMAL',
SPECIAL = 'SPECIAL',
}

export interface EstimateAndCommissionResponseDto {
// 서버와 통신하는 인터페이스
export interface PaymentRequest {
estimate_id: number;
commission_id: number;
estimate_amount: number;
member_nick: string;
member_phone_number: string;
member_email: string;
clean_type: CleanType;
}

export interface PaymentRequest {
// 포트원 관련 인터페이스
export interface PortonePaymentRequest {
pg: string;
pay_method: PayMethod;
merchant_uid: string;
name: string;
amount: number;
buyer_name: string;
buyer_tel: string;
buyer_email: string;
}

export interface CompletePaymentRequest {
imp_uid: string;
merchant_uid: string;
pay_method: PayMethod;
buyer_email: string;
buyer_tel: string;
}

export interface CompletePaymentResponse {
code: number;
message: string;
response: any; // 실제 응답 구조에 맞게 타입을 정의해야 합니다.
}

export interface PaymentResponse {
code: number;
message: string;
Expand Down
Loading

0 comments on commit ad1ee8e

Please sign in to comment.