DAY25

yejichoi·2022년 12월 4일
0
post-thumbnail

Algorithm Study

Backend Class

결제 프로세스

  1. 구매자가 구입할 옷에 대한 정보와 금액을 판매자에게 전달
  2. 판매자는 전달받은 금액을 PG사에게 결제해줄 것을 요청
  3. PG사는 요청받은 정보를 은행사에게 다시 결제 요청
  4. 은행사는 요청받은 금액을 구매자의 계좌에서 출금 후 PG사로 전달
  5. PG사는 판매자에게 금액을 전달 (일정량의 수수료를 제외)
  6. 판매자는 금액 확인 후, 구매자에게 옷을 배송

    💡 PG
    : P
    ayment Gateway
    구매자와 판매자 사이에서의 이뤄지는 결제를 안전하게 할 수 있도록 대행해주는 역할
    대표적인 PG사로는 KG 이니시스, NHN, KCP, LGU+
    모바일 환경으로는 KG 모빌리언스, 다날, 카카오Pay

✅ PG사에 따라서 사용하는 모듈 다름 => 사용하고있던 PG사를 다른 PG사로 옮기게 된다면 결제 연동 시스템을 다시 구축 해야함=>결제 외부 API를 사용하면 간단하게 결제 시스템을 구현 가능 i.e) iamport, 부트페이

결제솔루션( 아임포트, 부트페이 ) 이해

아임포트 (I'mport) 는 개발환경과 상관없이

원하는 PG사와의 결제시스템을 연결시켜주는 결제 API 서비스 (결제 솔루션)

imp_uid
: 아임포트를 통해 결제시 받는 고유키로,
uid 키를 통해 해당 결제에 관련된 모든 정보들을 조회해 볼 수 있으며, 결제 취소도 가능

아임포트 결제 프로세스( imp_uid의 이동 흐름 )


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


포인트 충전하기(결제) 구현

//아임포트 Docs 통해서 코드 구조화 
//아임포트 라이브러리와 아임포트를 쓰기 위해 jquery 라이브러리를 추가
<!DOCTYPE html>
<html lang="ko">
  <head>
    <title>결제페이지</title>
    <!-- Axios -->
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <!-- jQuery -->
    <script
      type="text/javascript"
      src="https://code.jquery.com/jquery-1.12.4.min.js"
    ></script>
    <!-- iamport.payment.js -->
    <script
      type="text/javascript"
      src="https://cdn.iamport.kr/js/iamport.payment-1.2.0.js"
    ></script>
    <script>
      function requestPay() {
				const amount = document.getElementById("qqq").value;
           const IMP = window.IMP; // 생략 가능
        IMP.init("가맹점 식별코드"); // Example: imp00000000
        IMP.request_pay(
          {
            // param
            pg: "html5_inicis",
            pay_method: "card",
            name: "노르웨이 회전 의자",
            amount: amount,
            buyer_email: "gildong@gmail.com",
            buyer_name: "홍길동",
            buyer_tel: "010-4242-4242",
            buyer_addr: "서울특별시 강남구 신사동",
            buyer_postcode: "01181",
          },
          function (rsp) {
            // callback
            if (rsp.success) {
              // 결제 성공 시 로직,
              console.log(rsp);
              axios.post(
                "http://localhost:3000/graphql",
                {
                  query: `
                    mutation {
                      createPointTransaction(impUid: "${rsp.imp_uid}", amount: ${rsp.paid_amount}){
                        id
                      }
                    }
                `,
                },
                {
                  headers: {
                    Authorization:
                      "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImNAYy5jb20iLCJzdWIiOiJlNzU1Zjc0MC0zMGNlLTQ4NWMtYjJlYS03M2U0OTA0YmNlZWEiLCJpYXQiOjE2NTQ3NjgxNDIsImV4cCI6MTY1NDc3MTc0Mn0.xAWo-7cEbTTshoOo7gcT5uTbsr8BS-Qh2529hjf-A10",
                  },
                }
              );

              alert("결제에 성공했습니다!!");
            } else {
              // 결제 실패 시 로직,
              alert("결제에 실패했습니다!!");
            }
          }
        );
      }
    </script>
  </head>
  <body>
    결제할금액: <input type="text" id="qqq" />
    <button onclick="requestPay()">결제하기</button>
  </body>
</html>
  • IMP.init("{가맹점 식별코드}") : 가맹점 식별코드(Merchant ID)는 본인의 가맹점 식별코드로 작성
  • IMP.request_pay : 아임포트가 연결해주는 PG사의 결제페이지를 사용할 수 있게됨, 결제가 성공하거나 실패하냐에 따라서 실행되는 로직을 만들어줄 수도 있음
    • pg : 어떤 PG사를 이용할 건지 선택 가능
    • pay_method : 어떤 결제 방식을 사용할 건지 선택 가능
    • merchant_uid : 고유한 주문 ID로 중복 값이 존재하면 오류가 나타남 merchant_uid 를 코드로 작성해주지 않는다면 자동으로 랜덤 생성되기에 오류를 피하기 위해 삭제
      • 특정한 규칙을 가진 주문 ID가 필요하다면 코드로 작성
    • name 또는 amount 에는 주문 이름과 결제 금액, buyer 에는 구매자에 대한 정보를 추가 가능
  • 결제 후 이루어지는 함수들을 추가로 작성해 주었고, rsp(respose) 안에는 결제 정보들이 들어있음

    결제에 실패하셨다면, 결제 금액을 확인 => 100원 미만으로는 결제 불가

// pointTransaction.entity.ts

import { Field, Int, ObjectType, registerEnumType } from '@nestjs/graphql';
import { User } from 'src/apis/users/entities/user.entity';
import {
  Column,
  CreateDateColumn,
  Entity,
  ManyToOne,
  PrimaryGeneratedColumn,
} from 'typeorm';

//타입스크립트의 enum을 사용해서 enum 타입을 정의
//enum type : 열거형
//정해진 특정한 값만 가질 수 있음 => 허용된 값들 중에 하나
export enum POINT_TRANSACTION_STATUS_ENUM {
  PAYMENT = 'PAYMENT',
  CANCEL = 'CANCEL',
}
//graphql에서 쓰기 위해 함수 등록 
registerEnumType(POINT_TRANSACTION_STATUS_ENUM, {
  name: 'POINT_TRANSACTION_STATUS_ENUM',
});

@Entity()
@ObjectType()
export class PointTransaction {
  @PrimaryGeneratedColumn('uuid')
  @Field(() => String)
  id: string;

  @Column()
  @Field(() => String)
  impUid: string;

  @Column()
  @Field(() => Int)
  amount: number;

  @Column({ type: 'enum', enum: POINT_TRANSACTION_STATUS_ENUM })
  @Field(() => POINT_TRANSACTION_STATUS_ENUM)
  status: string;

  
  //유저 한 명 당 여러 번의 결제를 할 수 있기에 ManyToOne으로 연결
  @ManyToOne(() => User)
  @Field(() => User)
  user: User;

  @CreateDateColumn()
  @Field(() => Date)
  createdAt: Date;
  
  // 추가
  @Column({ default: 0 }) //point의 기본값을 0원
  @Field(() => Int)
  point: number;
}
// pointTransaction.resolver.ts

import { UseGuards } from '@nestjs/common';
import { Args, Context, Mutation, Resolver } from '@nestjs/graphql';
import { GqlAuthAccessGuard } from 'src/commons/auth/gql-auth.guard';
import { IContext } from 'src/commons/types/context';
import { PointTransaction } from './entities/pointTransaction.entity';
import { PointTransactionService } from './pointTransaction.service';

@Resolver()
export class PointTransactionResolver {
  constructor(
    private readonly pointTransactionService: PointTransactionService,
  ) {}

  @UseGuards(GqlAuthAccessGuard)
  @Mutation(() => PointTransaction)
  createPointTransaction(
    @Args('impUid') impUid: string, //프론트엔드에서 로그인한 유저의 impUid와 
     //amount 받아옴
    @Args('amount') amount: number,
    @Context() context: IContext,
  ) {
		const user = context.req.user;
    return this.pointTransactionService.create({ impUid, amount, user });
  }
}
// pointTransaction.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../users/entities/user.entity';
import {
 PointTransaction,
 POINT_TRANSACTION_STATUS_ENUM,
} from './entities/pointTransaction.entity';

@Injectable()
export class PointTransactionService {
 constructor(
   @InjectRepository(PointTransaction)
   private readonly pointTransactionRepository: Repository<PointTransaction>,

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

 async create({ impUid, amount, user: _user }) {
   // 1. pointTransaction 테이블에 거래기록 1줄 생성
   const pointTransaction = this.pointTransactionRepository.create({
     impUid: impUid,
     amount: amount,
     user: _user,
     status: POINT_TRANSACTION_STATUS_ENUM.PAYMENT,
     //포인트를 충전하는 것이기에 결제 상태로 지정해서 테이블에 저장
   });
   await this.pointTransactionRepository.save(pointTransaction);

   // 2. 유저의 돈 찾아오기
   const user = await this.userRepository.findOne({ id: _user.id });

   // 3. 유저의 돈 업데이트
   await this.userRepository.update(//.update(조건, 변경할 값)
     { id: _user.id },
     { point: user.point + amount },
   );

   // 4. 최종결과 프론트엔드에 돌려주기
   return pointTransaction;
 }
}

.create( )
: DB로 저장되는 것이 아니라, 해당 데이터를 가진 객체가 만들어짐
.save( )
: save를 통해 만들어진 객체가 DB에 저장
.update( )
: 얼만큼 수정이 이루어졌는가의 대한 결과가 return 되며, DB에 저장
함수에 마우스를 올려놓고 리턴 타입이 promise 가 나오면 db에 접속하기 때문에 await을 사용함


Graphql / rest-api endpoint

axios는 rest API 만들 때 사용하지만 GraphQL도 rest API를 가공해서 만든 것으로 axios 통신이 가능 => 엔드포인트를 하나로 통합하여(/graphql) 사용하는 것 가능

장점
GraphQL은 한 번의 요청으로 여러 개의 API 요청이 가능( rest api underFetching 문제 개선 ),
원하는 정보만을 골라서 받을 수 있는 장점( rest api overFetching 문제 개선 )이 존재
주의할점
GraphQL은 항상 POST 방식
=>어떤 API를 요청하는 것인지 body에 작성해 줘야 하기 때문
여러개의 API를 한 번에 요청하면, 어떤 API 요청이 성공했고 실패했는지 파악하기에 애매하므로 전체에 대한 status는 항상 200으로 응답

HW

0개의 댓글