결제 과정은 프론트에서 결제사에 금액과 정보 등을 가지고 요청을 보내고, 식별가능한 유니크 id를 받은 다음, 해당 id를 백에 전달한다. id를 받은 백은 해당 id를 결제사에 보내 유효한 결제 id인지 확인한 후, 해당 결제 정보를 받아와서 db에 저장한다.
백은 어떤 경우든 프론트에서 완벽한 정보만 올 것이라고 생각해서는 안됨. 결제 id는 1000원으로 결제 해놓고 100000 만원이라고 서버에 보낼 가능성이 존재하기 때문에 안전하게 결제사에서 정보를 받아와야함.
결제 내역 history를 제어하기 위함. 특정 기간의 결제 내역을 불러오거나, 특정 결제 대상 품목 필터링 등을 위해서 서버에 저장한다. 결제 내역을 서버에 저장하는게 추후 get할 때 결제사 개입 없어서 더 편함. 데이터 유지가능.
import postPaymentHistory from '@/api/post/postPaymentHistory';
import { useLoginStore } from '@/store';
import { useState } from 'react';
import styles from '../styles/UserId.PaymentSelect.module.scss';
const PaymentSelect = ({
email,
username,
id,
toggleIsOpenPaymentSelect,
}: {
email: string;
username: string;
id: number;
toggleIsOpenPaymentSelect: () => void;
}) => {
const [selectedAmount, setSelectedAmount] = useState<null | number>(null);
const [customAmount, setCustomAmount] = useState<undefined | number>();
const handleSelectChange = (amount: number | null) => {
setSelectedAmount(amount);
setCustomAmount(0);
};
const handleCustomInputChange = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const inputValue = Number(event.target.value);
if (isNaN(inputValue)) {
alert('숫자를 입력해주세요.');
setCustomAmount(undefined);
return;
} else {
setSelectedAmount(null);
setCustomAmount(inputValue);
}
};
return (
<div className={styles.container}>
<h2>후원하기</h2>
<label>
<input
type="radio"
name="paymentAmount"
value="10000"
checked={selectedAmount === 10000}
onChange={() => handleSelectChange(10000)}
/>
10000원
</label>
<label>
<input
type="radio"
name="paymentAmount"
value="5000"
checked={selectedAmount === 5000}
onChange={() => handleSelectChange(5000)}
/>
5000원
</label>
<label>
<input
type="radio"
name="paymentAmount"
value="3000"
checked={selectedAmount === 3000}
onChange={() => handleSelectChange(3000)}
/>
3000원
</label>
<label>
<input
type="radio"
name="paymentAmount"
value="1000"
checked={selectedAmount === 1000}
onChange={() => handleSelectChange(1000)}
/>
1000원
</label>
<label>
<input
type="radio"
name="paymentAmount"
value="custom"
checked={selectedAmount === null && customAmount === undefined}
onChange={() => handleSelectChange(null)}
/>
직접입력
</label>
<input
type="text"
placeholder="원하는 금액 입력"
value={customAmount}
onChange={handleCustomInputChange}
disabled={selectedAmount !== null}
/>
<RequestPay
payAmount={
selectedAmount !== null && selectedAmount !== 0
? selectedAmount
: Number(customAmount)
}
email={email}
username={username}
id={id}
toggleIsOpenPaymentSelect={toggleIsOpenPaymentSelect}
/>
<button className={styles.gray} onClick={toggleIsOpenPaymentSelect}>
후원 취소
</button>
</div>
);
};
export default PaymentSelect;
interface Response {
apply_num?: number;
bank_name?: string;
buyer_addr?: string;
buyer_email?: string;
buyer_name?: string;
buyer_postcode?: string;
buyer_tel?: string;
card_name?: string;
card_quota?: number;
custom_data?: string;
imp_uid?: string;
merchant_uid?: string;
name?: string;
paid_amount?: number;
paid_at?: string;
pay_method?: string;
pg_provider?: string;
pg_tid?: string;
receipt_url?: string;
status?: string;
success: boolean;
}
interface RequestPayParams {
pg: string;
pay_method: string;
merchant_uid: string;
name: string;
amount: number;
buyer_email: string;
buyer_name: string;
buyer_tel: string;
buyer_addr: string;
buyer_postcode: string;
}
const RequestPay = ({
email,
username,
payAmount,
id,
toggleIsOpenPaymentSelect,
}: {
email: string;
username: string;
id: number;
payAmount: number;
toggleIsOpenPaymentSelect: () => void;
}) => {
const code = import.meta.env.VITE_IAMPORT_CODE;
const { loginUsername, loginEmail } = useLoginStore();
if (!window.IMP || !code) return;
const { IMP } = window;
IMP.init(code);
const today = new Date();
const hours = today.getHours(); // 시
const minutes = today.getMinutes(); // 분
const seconds = today.getSeconds(); // 초
const milliseconds = today.getMilliseconds();
const makeMerchantUid = hours + minutes + seconds + milliseconds;
const requestPay = () => {
(
IMP.request_pay as (
params: RequestPayParams,
callback: (rsp: Response) => void,
) => void
)(
{
pg: `kakaopay.${import.meta.env.VITE_IAMPORT_KAKAOPAY_ID!}`,
pay_method: 'card',
merchant_uid: `IMP_${makeMerchantUid}`,
name: `${loginUsername}의 ${username} 후원`,
amount: payAmount,
buyer_email: loginEmail!,
buyer_name: loginUsername!,
buyer_tel: '010-4242-4242',
buyer_addr: '서울특별시 강남구 신사동',
buyer_postcode: '01181',
},
(rsp: Response) => {
if (rsp.success) {
postPaymentHistory({
sellerName: username,
sellerEmail: email,
impUid: rsp.imp_uid,
sellerId: id,
});
} else {
console.log('결제실패', rsp);
toggleIsOpenPaymentSelect();
}
},
);
};
return (
<button className={styles.green} onClick={requestPay}>
후원하기
</button>
);
};
프론트에서는 결제 내역의 유니크값을 가지고 백 서버에 delete 요청을 보냄. 백은 유니크값을 가지고 결제내역 데이터를 찾음. 유니크값 결제 id를 결제사에 보내 결제 취소요청을 보내고, 취소된 정보를 받아서 결제내역 데이터를 변경함.
결제 관련 데이터는 중요함. 환불 일시, 환불 금액, 환불 카드사 등 추후 문제가 될 수 있는 부분을 가지고 있어야함
전의 모든 trigger는 테스트 버전이다. 사업자등록 필요한데, 법적인 문제가 걸려있어 발들이기 어려움. 유저에게 노출가능한 값은 무엇이 있는가. 특정 정보만으로 결제사로 들어가 접근할 수 없도록 보안이 필요함. 프론트에서 해줄 수 있는 보안 강화는 무엇이 있을까. 수수료는 어디서 얼마나 빠져나가야하는가.