J2ong 님의 블로그

포트원 ((구)아이포트) 정기 전자결제 연동 가이드 (Test) 본문

카테고리 없음

포트원 ((구)아이포트) 정기 전자결제 연동 가이드 (Test)

J2ong 2025. 5. 18. 19:52

안녕하세요!

이번 글에서는 포트원 ((구)아이포트) 정기  전자 결제 연동 가이드(Test)에 대해 설명하겠습니다.

저는 KG이니시스 통합인증 + 정기결제 (비인증 결제) 두개의 결제 모듈로 진행하였습니다.


📌 두개의 모듈을 선택한 이유

KG이니시스는 하나의 정기결제 모듈로 본인인증과 수기결제를 함께 제공하지만 다음과 같은 이유로 두 모듈을 별도로 사용하였습니다.

  • 본인인증 모듈의 단점
    • 인증서 기반 본인확인 필요
    • 본인인증 UI 커스터마이징 불가
    • → 사용자 경험 저하
  • 선택한 방식
    • 통합인증 모듈을 이용한 간편 본인인증 처리
    • 수기/정기 결제 모듈(비인증 결제)을 통한 카드정보 직접 입력 방식

⚠️ 참고: 비인증 결제는 대행사 심사 기준이 더 까다롭습니다.

 

📌 포트원 전자 결제의 장점

  • 다양한 PG사 통합 관리
  • 간편한 스크립트 연동
  • 직관적인 관리자 콘솔
  • Webhook 기능 통한 안정적인 결제 검증

 

 


 

📌 포트원 전자 결제 연동 방법

1. 포트원 연동 가이드 확인

 

결제 연동 준비하기

포트원을 이용한 연동 개발이 처음이시라면 아래 안내 사항에 따라 진행하세요.

developers.portone.io

 

2. 포트원 관리자 콘솔에서 회원가입 및 로그인

 

포트원 관리자콘솔

단 하나의 솔루션, 결제, 그 이상의 경험

admin.portone.io

 

3. 결제연동 > 연동정보에서 테스트 채널 추가

  • 테스트 모드 선택 (자동 결제정정 처리됨)
  • KG이니시스 통합인증 + 수기결제 채널 모두 등록

 

4. API Key 확인

5. 웹훅 엔드포인트 등록

  • Webhook은 공개 도메인만 호출 가능 (localhost 사용 불가)
  • 하지만 ngrok 이라는 서비스를 통해 localhost를 외부망에서 접근 가능한 도메인으로 포워딩 하면 callback URL로 설정할 수 있습니다.
  • 저는 서버를 연동해 놓았기 때문에 ngrok 서비스를 이용하지 않고 실제 저의 서버 엔드포인트를 등록했습니다.
  • localhost로 테스트를 진행하실 분들은 밑에 가이드를 연동해 놓을테니 보시고 진행하시면 되겠습니다.

웹훅 연동하기

 

웹훅 연동하기

포트원 웹훅을 사용하여 포트원 서버에 저장된 결제 정보를 고객사 서버에 동기화하고 네트워크 불안정성을 보완하는 방법을 설명합니다.

developers.portone.io

 

1. 프론트 엔드

아임포트 스크립트 추가
<script src="https://cdn.iamport.kr/v1/iamport.js"></script>

 

본인 인증 함수
IMP.init('고객사 식별코드');
IMP.certification(
    {
        channelKey: '통합인증 채널키',
        merchant_uid: `merchant_${crypto.randomUUID()}`, // 주문 번호
    },
    function (rsp) {
        if (rsp.success) {
            // 본인인증 성공
            // ex) 빌링키 발급 요청
        } else {
            // 실패
        }
    },
);

 

2. 백엔드 (Node.js)

아임포트 토큰 발급 함수
let accessToken = null;

async function getAccessToken() {
    try {
        const response = await axios.post("https://api.iamport.kr/users/getToken", {
            imp_key: IAMPORT_API_KEY, // REST API Key
            imp_secret: IAMPORT_API_SECRET // REST API Secret
        });
        return response.data.response.access_token;
    } catch (error) {
        console.error('아임포트 토큰 발급 오류:', error.response?.data || error.message);
        throw error;
    }
}

 

빌링키 발급
try {
    // 1. 아임포트 Access Token 확인
    accessToken = await getAccessToken();

    // 2. PG에서 빌링키 발급 요청 (REST API)
    const billingKeyResponse = await axios.post(`https://api.iamport.kr/subscribe/customers/${customer_uid}`, {
            pg: "inicis", // 연동한 정기결제 PG Provider
            card_number: '숫자만 16자리', // 카드번호
            expiry: 'YYYY-MM' // 유효기간
            birth: 'YYMMDD', // 생년월일
            pwd_2digit: '비밀번호 앞 2자리' // 비밀번호 앞 2자리
        },
        {
            headers: { 
                Authorization: accessToken,
                "Content-Type": "application/json"
            }
        }
    );

    // 3. 빌링키 발급 결과 확인
    if (billingKeyResponse.data.code !== 0) {
        throw new Error(`빌링키 발급 실패: ${billingKeyResponse.data.message}`);
    }

    const scheduleAt = Math.floor(Date.now() / 1000) + 10;

    // 4. 빌링키 발급 후 첫 결제 수행
    const firstPaymentResponse = await axios.post(
        "https://api.iamport.kr/subscribe/payments/schedule", 
        {
            customer_uid: customer_uid,
            schedules: [
                {
                    merchant_uid: merchant_uid,
                    schedule_at: scheduleAt,
                    amount: amount, // 구독권 가격
                    name: '월간 구독권',
                    // buyer_name: '선택사항: 구매자 이름', 
                    // buyer_email: '선택사항: 구매자 이메일', 
                    custom_data: {
                        plan: '월간 구독권',
                        items: [
                            {
                                name: '월간 구독권',
                                price: amount // 구독권 가격
                            }
                        ]
                    }
                }
            ]
        },
        {
            headers: { 
                Authorization: accessToken,
                "Content-Type": "application/json"
            }
        }
    );

    // 5. 첫 결제 결과 확인
    if (firstPaymentResponse.data.code !== 0) {
        // 첫 결제 실패 시 빌링키 삭제
        await axios.delete(
            `https://api.iamport.kr/subscribe/customers/${customer_uid}`, 
            {
                headers: { 
                    Authorization: accessToken,
                    "Content-Type": "application/json"
                }
            }
        );
        throw new Error(`첫 결제 실패: ${firstPaymentResponse.data.message}`);
    }

    const scheduled = firstPaymentResponse.data.response;

    // 성공 응답
    return res.json({
        success: true,
        scheduled_at: scheduled.schedule_at,
        merchant_uid: scheduled.merchant_uid,
        status: scheduled.schedule_status,  // ready, failed, paid 등
        customer_uid: customer_uid,
        imp_uid: scheduled.imp_uid
    });

} catch (error) {
    console.error("빌링키 발급 오류:", error.response?.data || error.message);
    return res.status(500).json({ 
        error: "빌링키 발급 중 오류가 발생했습니다.",
        details: error.response?.data || error.message
    });
}
  • 카드 유효기간은 YYYY-MM 형식이어야 정상 작동 (KG이니시스, 나이스페이먼츠 기준 - 다른 대행사는 잘 모르겠습니다.)
  • 저는 유효기간 형식을 알기 위해 엄청 고생했습니다. (제가 못찾은건지 어떤 형식으로 넘기라는 문서를 발견하지 못해서 YY-DD, DD-YY, YYDD, DDYY 등  온갖 형식으로 넘기다가 YYYY-MM 형식으로 겨우 해결했네요. ㅠㅠ)
  • 스케쥴 방식을 이용할 경우 schedule_at 값은 반드시 현재 시각보다 +5~10초 이상으로 설정. 그러지 않으면 오류 발생

 

웹훅 구독 등록
app.post("/iamport-webhook", async (req, res) => {
    const { imp_uid } = req.body;

    if (!imp_uid) {
        return res.status(400).json({ error: "imp_uid가 없습니다." });
    }

    try {
        accessToken = await getAccessToken();

        // 결제 정보 조회
        const { data } = await axios.get(`https://api.iamport.kr/payments/${imp_uid}`, {
            headers: { Authorization: accessToken }
        });

        const payment = data.response;

        const {
            status,
            amount,
            pg_provider,
            customer_uid,
            merchant_uid,
            custom_data
        } = payment;

        if (status !== "paid" || pg_provider !== "inicis") {
            return res.status(400).json({ error: "유효하지 않은 결제입니다." });
        }

        // 구독 정보 처리
        const now = new Date();
        let expiresAt;
        let licenseKey;
        
        // db를 통한 검증 및 생성, 업데이트 구현 가능

        // 다음 결제 예약 설정
        try {
            const response = await axios.post(
                "https://api.iamport.kr/subscribe/payments/schedule", 
                {
                    customer_uid: customer_uid,
                    schedules: [
                        {
                            merchant_uid: `merchant_${crypto.randomUUID()}`,
                            schedule_at: schedule_at,
                            amount: amount, // 월 구독권의 가격
                            name: '월간 구독권',
                            // buyer_name: '선택사항: 구매자 이름',
                            // buyer_email: '선택사항: 구매자 이메일',
                            custom_data: JSON.stringify({
                                plan: '월간 구독권',
                                items: [
                                    {
                                        name: '월간 구독권',
                                        price: amount, // 구독권 가격
                                    }
                                ]
                            })
                        }
                    ]
                },
                {
                    headers: { 
                        Authorization: accessToken,
                        "Content-Type": "application/json"
                    }
                }
            );

            if (response.data.code !== 0) {
                throw new Error(`예약 실패: ${response.data.message}`);
            }
                
        } catch (scheduleError) {
            // 예약 실패 시 로그만 남기고 전체 트랜잭션은 성공으로 처리
            console.error("다음 결제 예약 실패:", scheduleError.response?.data || scheduleError.message);
        }

        return res.status(200).json({ message: "웹훅 처리 완료" });

    } catch (err) {
        console.error("웹훅 오류:", err.response?.data || err.message);
        return res.status(500).json({ error: "웹훅 처리 중 서버 오류 발생" });
    }
});

 


 

이상으로 포트원 전자결제 연동 가이드를 마칩니다.
실제 서비스 적용을 위해서는 사업자 등록 후 PG 대행사 계약, 그리고 카드사 심사 절차를 완료해야 합니다.
본 가이드가 정기결제 연동을 준비하시는 분들께 실질적인 도움이 되길 바랍니다.