포트원 결제 프로세스 구현 - Nest.js

JBoB·2023년 2월 14일
0
post-custom-banner

🐧포트원 연동 결제 프로세스 구현

어제는 결제 연동 프로세스 구현에 대해 배웠다면 오늘은

포트원 연동 결제 프로세스를 배웠다.

결제 API도 마찬가지로 KEY를 연동시켜서 API 를 구현해줘야 하는데 대표적인 사이트로는 포트원이 있다.

자기가 구현하고 싶은 결제대행사 선택후

왼쪽 결제연동을 들어가 식별코드 키를 받아주면 된다.

자세한 것은 콘솔 가이드를 보면서 참고해보자.

🐤포트원 결제 프로세스 흐름

  1. 브라우저에서 결제하기 버튼을 클릭하면 REST API 로 프론트엔드가 결제를 요청
  2. 포트원은 PG사에 결제 요청
  3. PG사는 카드사에 결제 요청
  4. 결제가 다 되면, 포트원이 결제건에 대한 ID값을 보내준다. ⇒ imp_uid
  5. 프론트 엔드는 받은 imp_uidfmf 백엔드에 건네준다.
  6. 백엔드는 DB에 결제정보와 함께 imp_uid를 저장.

솔루션사랑 백엔드에 서버연결API를 하나 만들어줘야 한다. 웹훅 연동하기

웹훅 : 특정 이벤트가 발생하였을 댸 서비스나 응용프로그램으로 알림을 보내는 기능

imp_uid

: 포트원을 통해 받는 고유키로,

uid키를 통해 해당 결제에 관련된 모든 정보들을 조회해 볼 수 있으며, 결제 취소도 가능하다.

이와 같은 구현을 할 경우 복잡한 개발 과정을 거치지 않고 포트원에 있는 결제 과정을 사용해

보다 편리한 개발 구현을 만들 수 있다.

🐤결제 API 작성 시작!!!

🐙 1. 프론트엔드 구현

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>결제하기</title>
// 포트원을 사요하기 위해 해당 라이브러리를 페이지에 추가해야하한다. 
    <script
      type="text/javascript"
      src="https://code.jquery.com/jquery-1.12.4.min.js"
    ></script>
    <script
      type="text/javascript"                // 최신버전 사용 권장
      src="https://cdn.iamport.kr/js/iamport.{SDK-최신버전}-1.2.0.js"
    ></script>
    <script
      type="text/javascript"
      src="https://unpkg.com/axios/dist/axios.min.js"
    ></script>
    <script>
      function mypayment() {
        const myAmount = Number(document.getElementById("amount").value);

        const IMP = window.IMP; // 생략 가능
        IMP.init("나만의 식별키"); // Example: imp00000000 
        IMP.request_pay(
          {
            // param
            pg: "kakaopay", <<== 포트원 등록한 결제 대행사 
            pay_method: "card",
            name: "마우스",
            amount: myAmount,
            buyer_email: "gildong@gmail.com",
            buyer_name: "홍길동",
            buyer_tel: "010-4242-4242",
            buyer_addr: "서울특별시 강남구 신사동",
            buyer_postcode: "01181",
            m_redirect_url: "", // 모바일 결제후 리다이렉트될 주소!!
          },
          async (rsp) => {
            // callback
            if (rsp.success) {
              // 결제 성공시
              console.log(rsp);

              const data = await axios.post(
                "http://localhost:3000/graphql",
                {   // graphql createpoint 부분 
                  query: `
                      mutation{
                        createPointTransaction(impUid:"${rsp.imp_uid}",amount:${rsp.paid_amount}){
                            id                   // 템플릿 사용으로 안에 변수 값 넣어주기 
                            impUid
                            amount
                            status
    
                            }
                        }
                    `,
                },
                {
                  headers: {
									// graphql 에서 로그인 하고 받은 인가 토큰
                    authorization:
                      "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI4NzhhZWE3NS1kMmIyLTRjZjEtYWYwZi1hYjc5Zjg5NThmNDUiLCJpYXQiOjE2NzYzNzQ2ODksImV4cCI6MTY3NjM3ODI4OX0.yQwfzaJmWvna5Ew05viIwo0_YPwTFr3ZOBLCGSEuTrE",
                  },
                }
              );

              console.log(data);
            } else {
              // 결제 실패시
            }
          }
        );
      }
    </script>
  </head>
  <body>
    결제할 금액: <input type="text" id="amount" />
    <button onclick="mypayment()">결제하기</button>
  </body>
</html>
  • IMP.init("{가맹점 식별코드}") : 해당 가맹점 식별코드를 등록해 주는 것
  • IMP.request_pay : 포트원이 연결해주는 PG사의 결제페이지를 사용할 수 있게 된다.
  • pg : 어떤 PG사를 이용할 건지 선택 가능 ⇒ 저는 카카오 선택했으니 카카오 적어준거 처럼?
  • pay_method : 어떤 결제 방식을 사용할 건지 선택 가능
  • rsp(respose) 안에는 결제 정보들이 들어있습니다.

작성하다보니 양식들이 포트원 콘솔가이드에 자세하게 나와있어서 따른 기존 docs보다 많이 편리했다.

🐙 2. entity 작성

import { Field, Int, ObjectType, registerEnumType } from '@nestjs/graphql';
import { Product } from 'src/apis/products/entities/product.entity';
import { ProductRoomType } from 'src/apis/productsRoomTypes/entities/productsRoomTypes.entity';
import { User } from 'src/apis/users/entities/user.entity';

import {
  Column,
  Entity,
  JoinColumn,
  ManyToOne,
  OneToOne,
  PrimaryGeneratedColumn,
} from 'typeorm';

// 결제 API 추가
// status 결제완료 취소완료를 해주기 위해 만들어줌
export enum POINT_TRANSACTION_STATUS_ENUM {
  PAYMENT = 'PAYMENT',
  CANCEL = 'CANCEL',
}
// 결제 API 추가
registerEnumType(POINT_TRANSACTION_STATUS_ENUM, {
  name: 'POINT_TRANSACTION_STATUS_ENUM',
});

@Entity()
@ObjectType()
export class Payment {
  @PrimaryGeneratedColumn('uuid')
  @Field(() => String)
  id: string;
// 결제 API 추가
  @Column()
  @Field(() => String)
  impUid: string;

  // @Column()
  // @Field(() => Date)
  // stay: Date;
// 결제 API 추가
  @Column()
  @Field(() => Int)
  amount: number;
// 결제 API 추가
// 결제 타입은 enum 으로 다루어 정해진 문자열만 들어가게 해준다.
  @Column({ type: 'enum', enum: POINT_TRANSACTION_STATUS_ENUM })
  @Field(() => POINT_TRANSACTION_STATUS_ENUM)
  status: POINT_TRANSACTION_STATUS_ENUM;

  // @Column()
  // @Field(() => String)
  // method: string;
// 유저 한명당 여러 번 결제를 할 수 있기에 ManyToOne 연결 
  @ManyToOne(() => User, (users) => users.payment)
  @Field(() => User)
  users: User;

  @ManyToOne(() => Product, (products) => products.payment)
  @Field(() => [Product])
  products: Product[];

  @JoinColumn()
  @OneToOne(() => ProductRoomType)
  @Field(() => [ProductRoomType])
  productsRoomType: ProductRoomType[];
// 결제 API 추가
// 결제가 이루어질때 자동으로 시간 생성 저장 
  @CreateDateColumn()
  @Field(() => Date)
  createdAt: Date;
}

🐙 3. Resolve, Service.ts 작성

// payments.resolver.ts

@Resolver()
export class PaymentsResolver {
  constructor(
    private readonly paymentsService: PaymentsService, //
  ) {}

  @UseGuards(GqlAuthGuard('access')) => 로그인 한 유저만 가능하게 guard 사용 
  @Mutation(() => Payment)
  createPayment(
    @Args('impUid') impUid: string, //
    @Args({ name: 'amount', type: () => Int }) amount: number,
    // 로그인 한 유저의 정보는 Context에 내장되어 있다. 
    @Context() context: IContext,
  ): Promise<Payment> {
    const user = context.req.user;
    return this.paymentsService.create({ impUid, amount, user });
  }
}

// payments.service.ts

@Injectable()
export class PaymentsService {
  constructor(
    @InjectRepository(Payment)
    private readonly paymentsRepository: Repository<Payment>,

    @InjectRepository(User)
    private readonly usersRepository: Repository<User>,
  ) {}

  async create({
    impUid,
    amount,
    user: _user, ==> 2번의 user와 변수가 겹쳐 에러 발생 _user로 변경 
  }: IPaymentsServiceCreate): Promise<Payment> {
    // this.payment.create() // 등록을 위한 빈 객체 만들기
    // this.payment.insert() // 결과는 못 받는 등록 방법
    // this.payment.update() // 결과는 못 받는 수정 방법

    // 1. payment 테이블에 거래기록 1줄 생성
    const payment = this.paymentsRepository.create({
      impUid: impUid,
      amount: amount,
      users: _user,
      status: POINT_TRANSACTION_STATUS_ENUM.PAYMENT, //안전한 코드 만들기의 한 방향
					// 포인트를 충전하는 것 이기에 결제상태를 PAYMENT 를 작성 
    });

    await this.paymentsRepository.save(payment);

    // 2. 유저의 돈 찾아오기
    const user = await this.usersRepository.findOne({
      where: { id: _user.id },
    });
    // 3. 유저의 돈 업데이트

    await this.usersRepository.update(
      { id: _user.id },
      { point: user.point + amount },
    );

    // 4. 최종결과 브라우저에 돌려주기
    return payment;
  }
}

이후 module.ts 와 app.module 추가하여 정상적으로 구현할 수있도록 해준다.

// patments.module.ts

@Module({
  imports: [
    TypeOrmModule.forFeature([
      Payment, //
      User,
    ]),
  ],
  providers: [
    PaymentsResolver, //
    PaymentsService,
  ],
})
export class PaymentsModule {}

// app.module.ts

// 모듈 부분 imports 에 PaymentsModule 넣어주기
profile
간절하고 치열하게 살자
post-custom-banner

0개의 댓글