Carpool(React Native & Express & apollo federation & Graphql & Mariadb, Mongodb) - 16. payment-service

#1 payment-service

payment-service는 예약 혹은 예약 취소 시 유저의 요금 처리를 담당하는 서비스입니다. 따라서 ride-service의 예약, 예약 취소 메서드에서 주로 실행되는 서비스라고 할 수 있죠. payment-service 코드를 작성하기 이전에 데이터베이스 테이블, api를 살펴보도록 하겠습니다.

  • 데이터베이스 테이블

  • api

1) api 명세서

2) Billing

카풀 예약 시 사용될 Mutation입니다.

3) CancelBilling

카풀 예약 취소 시 사용될 Mutation입니다.

4) ReBilling

재결제 시 사용될 Mutation입니다.

5) GetPayments

유저가 지불한 요금 내역을 볼 수 있는 Query입니다.

6) GetPayment

유저가 지불한 요금 데이터를 볼 수 있는 Query입니다.

테이블과 api를 살펴보았으니 payment-service를 이용하여 예약과 예약 취소에 관한 시나리오를 만들어 보겠습니다.

  • 예약
    1) 예약하기 버튼을 누르면 결제 페이지로 이동합니다.

2) 결제 방식, 비용, 결제명 등의 데이터를 payment-service로 전송합니다.

3) 결제 데이터를 생성하고, ride-service와 통신을 위해 결제 완료 메시지를 발행합니다. * 실제 계좌와 연동된다면 계좌에서 돈이 출금됩니다.

4) ride-service에서는 메시지를 구독하고 있다가 메시지가 들어오면 reserve subscription을 실행합니다.

5) reserve에서는 예약을 위해 카풀 데이터를 수정합니다.

6) 중간에 에러가 나는 케이스마다 롤백을 위한 메시지를 발행합니다.

7) 에러 발생 시 payment-service에서는 결제 데이터를 CANCEL 상태로 되돌립니다. * 실제 계좌와 연동된다면 계좌에서 돈이 재입금됩니다.

  • 예약 취소

1) 예약취소 버튼을 누르면 payment_id 데이터를 payment-service로 전송합니다.

2) 결제 데이터를 CANCEL 상태로 되돌리고, ride-service와 통신을 위해 결제 취소 메시지를 발행합니다. * 실제 계좌와 연동된다면 계좌에서 돈이 재입금됩니다.

3) ride-service에서는 메시지를 구독하고 있다가 메시지가 들어오면 cancel subscription을 실행합니다.

4) cancel에서는 취소를 위해 카풀 데이터를 수정합니다.

5) 중간에 에러가 나는 케이스마다 롤백을 위한 메시지를 발행합니다.

6) 에러 발생 시 payment-service에서는 결제 데이터를 SUCCESS 상태로 되돌립니다. * 실제 계좌와 연동된다면 계좌에서 돈이 재출금됩니다.

#2 payment-service 생성

예약과 예약 취소에 관한 api 순서대로 코드를 작성해보도록 하겠습니다.

1) payment-service 생성 및 결제 api 구현

mkdir payment-service
cd payment-service
mkdir src
touch src/index.js
npm init -y

프로젝트를 생성했으니 필요한 라이브러리들을 설치하도록 하겠습니다. payment-service는 mariaDB를 이용할 것이기 때문에 mongoose가 아닌 mysql2 라이브러리를 설치합니다.

npm install graphql-constraint-directive apollo-server-core apollo-server-express @apollo/subgraph [email protected] @graphql-tools/merge cors express mongoose nodemon mysql2 graphql-redis-subscriptions ioredis jsonwebtoken kafka-node tsc-watch winston winston-daily-rotate-file
npm install -D @types/express typescript @types/ioredis

라이브러리를 설치했으니 graphql 코드를 작성하도록 하겠습니다.

  • ./src/typeDefs/enum.ts
import { gql } from 'apollo-server-express';

const typeDefs = gql`
    enum PaymentStatus {
        SUCCESS,
        CANCEL
    }
`;

export default typeDefs;
  • ./src/typeDefs/type.ts
import { gql } from 'apollo-server-express';

const typeDefs = gql`
    type Payment {
        payment_id: String!
        payment_type: String!
        cost: Int!
        payment_name: String!
        user_nickname: String!
        user_id: String!
        ride_info_id: String!
        status: String!
    }
`;

export default typeDefs;
  • ./src/typeDefs/input.ts
import { gql } from 'apollo-server-express';

const typeDefs = gql`
    input BillingInput {
        payment_type: String!
        cost: Int!
        payment_name: String!
        user_nickname: String!
        ride_info_id: String!
    }

    input CancelBillingInput {
        ride_info_id: String!
        cost: Int!
        payment_id: String!
    }
`;

export default typeDefs;
  • ./src/typeDefs/query.ts
import { gql } from 'apollo-server-express';

const typeDefs = gql`
    type Query {
        getPayments: [Payment]
        getPayment(payment_id: String!): Payment!
    }
`;

export default typeDefs;
  • ./src/typeDefs/mutation.ts
const { gql } = require('apollo-server-express');
import { gql } from 'apollo-server-express';

const typeDefs = gql`
    type Mutation {
        billing(input: BillingInput!): Int!
        cancelBilling(input: CancelBillingInput!): Int!
        reBilling(payment_id: String!): Int!
    }
`;

export default typeDefs;
  • ./src/typeDefs/index.ts
import queries from './query';
import mutations from './mutation';
import types from './type';
import inputs from './input';
import enums from './enum';
import { mergeTypeDefs } from '@graphql-tools/merge';

const typeDefs = mergeTypeDefs([
    queries,
    mutations,
    types,
    inputs,
    enums
]);

export { typeDefs };

이어서 필요한 유틸들을 작성해보도록 하겠습니다.

  • ./src/utils/jwt.utils.ts
import jwt from 'jsonwebtoken';
import { SECRET_KEY } from '../config/env.variable';

class JwtUtils {
    constructor() {}

    public async generator(user_id: string): Promise<any> {
        return jwt.sign({
            exp: Math.floor(Date.now() / 1000) + (60 *60),
            data: user_id
        }, SECRET_KEY as string);
    }

    public async verify(token: string): Promise<any> {
        return jwt.verify(token.split('Bearer ')[1],
                          SECRET_KEY as string);
    }
}

export { JwtUtils };
  • ./src/utils/maria.connection.ts
import mysql from 'mysql2';

import {
    MARIA_HOST,
    MARIA_USER,
    MARIA_PORT,
    MARIA_PASSWORD,
    MARIA_DATABASE
} from '../config/env.variable';

const pool = mysql.createPool({
    host: MARIA_HOST as string,
    port: MARIA_PORT as number,
    user: MARIA_USER as string,
    password: MARIA_PASSWORD as string,
    database: MARIA_DATABASE as string
});

const maria = pool.promise();

export { maria };

maria 데이터베이스 연동을 위한 커넥션입니다.

  • ./src/utils/payment.id.generator.ts
const idGenerator = () => {
    const prefix = "BI-"

    return prefix + 
           Math.random().toString(36).substring(2, 11) + 
           "-" + 
           Math.random().toString(36).substring(2, 16);
};

export { idGenerator };
  • ./src/utils/redis.client.ts
import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';
import {
    REDIS_PORT,
    REDIS_URI 
} from '../config/env.variable';

const redis = new Redis(
    REDIS_PORT as number,
    REDIS_URI as string
);

const pubsub = new RedisPubSub({
    publisher: redis,
    subscriber: redis
});

export { pubsub };
  • ./src/utils/kafka.client.ts
import kafka from 'kafka-node';
import { KAFKA_HOST } from '../config/env.variable';

const client = new kafka.KafkaClient({ kafkaHost: KAFKA_HOST as string });

export { client };

카프카 메시지 큐를 사용하기 위한 카프카 클라이언트입니다.

그리고 카프카를 사용하기 위한 카프카 모듈을 작성해보도록 하겠습니다.

메시지를 발행하기 위한 producer 모듈입니다.

  • ./src/kafka/producer.ts
import { client } from '../utils/kafka.client';
import kafka, { Producer } from 'kafka-node';

const producer: Producer = new kafka.Producer(client);

export { producer };

발행받은 메시지를 구독하기 위한 consumer 모듈입니다.

  • ./src/kafka/consumer.ts
import { client } from '../utils/kafka.client';
import kafka, { Consumer, OffsetFetchRequest } from 'kafka-node';

const topics: Array<OffsetFetchRequest> = [
    {
        topic: 'RESERVE_ERROR',
        partition: 0
    },
    {
        topic: 'CANCEL_ERROR',
        partition: 0
    }
];
const option: object = {};

const consumer: Consumer = new kafka.Consumer(client,
                                              topics,
                                              option);

export { consumer };

토픽에 관해 어떤 서비스를 호출할지를 다루는 kafka 모듈입니다.

  • ./src/kafka/kafka.service.ts
import { consumer } from './consumer';
import { PaymentService } from '../service/payment.service';

const kafkaService = async () => {
    const service = new PaymentService();

    consumer.on('message', async function(message: any) {
        if(message.topic === "RESERVE_ERROR") {
            return service.reserve(message.value);
        } else if(message.topic === "CANCEL_ERROR") {
            return service.cancel(message.value);
        }
    });

    consumer.on('error', error => {
        console.log(error);
    });
};

export { kafkaService };

유틸, 카프카 모듈을 작성했으니 인터페이스와 dto를 작성하여 서비스에서 레포지토리로 데이터를 넘기기 위한 데이터 타입을 정의하겠습니다.

  • ./src/interface/payment.interface.ts
interface IPayment {
    payment_id: string,
    payment_type: string,
    cost: number,
    payment_name: string,
    user_nickname: string,
    user_id: string,
    ride_info_id: string,
    status: string,
};

export { IPayment };
  • ./src/dtos/billing.dto.ts
interface BillingDto {
    payment_type: string,
    cost: number,
    payment_name: string,
    user_nickname: string,
    ride_info_id: string
};

export { BillingDto };
  • ./src/dtos/cancel.billing.dto.ts
interface CancelBillingDto {
    ride_info_id: string,
    cost: number,
    payment_id: string,
    user_id: string
};

export { CancelBillingDto };
  • ./src/dtos/cancel.error.dto.ts
interface CancelErrorDto {
    payment_id: string
};

export { CancelErrorDto };
  • ./src/dtos/cancel.reserve.dto.ts
interface CancelReserveDto {
    cost: number,
    payment_id: string,
    ride_info_id: string,
    user_id: string
};

export { CancelReserveDto };
  • ./src/dtos/get.payment.dto.ts
interface GetPaymentDto {
    payment_id: string
};

export { GetPaymentDto };
  • ./src/dtos/re.billing.dto.ts
interface ReBillingDto {
    payment_id: string
};

export { ReBillingDto };
  • ./src/dtos/reserve.dto.ts
interface ReserveDto {
    cost: number,
    payment_id: string,
    ride_info_id: string,
    user_id: string
};

export { ReserveDto };
  • ./src/dtos/reserve.error.dto.ts
interface ReserveErrorDto {
    payment_id: string
};

export { ReserveErrorDto };
  • ./src/dtos/response.dto.ts
interface ResponseDto {
    code: number,
    message: string,
    payload: any
};

export { ResponseDto };

인터페이스와 dto를 작성했으니 service, repository, resolver를 작성하도록 하겠습니다.

  • ./src/respository/payment.repository.ts
import { RowDataPacket } from 'mysql2';
import { PoolConnection } from 'mysql2/promise';
import { CANCELED_BILLING, DATABASE_ERROR, FAILED_RE_BILLING, FAILED_ROLL_BACK_CANCEL, FOUND_PAYMENT, NOT_CANCELED_BILLING, NOT_FOUND_PAYMNET, NOT_SAVED_PAYMENT, RE_BILLING, ROLL_BACK_CANCEL, SAVED_PAYMENT } from '../constants/result.code';
import { ResponseDto } from '../dtos/response.dto';
import { maria } from '../utils/maria.connection';

class PaymentRepository {
    constructor() {}

    public async saveBilling(params: Array<any>): Promise<ResponseDto> {
        const connection: PoolConnection = await maria.getConnection();
    
        try {
            const sql: string = "INSERT INTO " +
                                "payments(payment_id, payment_type, cost, payment_name, user_nickname, user_id, ride_info_id, status) " +
                                "VALUES(?, ?, ?, ?, ?, ?, ?, ?)";
            
            await connection.beginTransaction();

            const [results, rows]: any = await connection.query(
                sql,
                params
            );

            await connection.commit();

            if(results.affectedRows as number < 1) {
                return {
                    code: NOT_SAVED_PAYMENT,
                    message: "결제에 실패했습니다!",
                    payload: params[0]
                };
            }

            return {
                code: SAVED_PAYMENT,
                message: "결제 성공!",
                payload: null
            };
        } catch(err) {
            await connection.rollback();

            connection.release();
            
            return {
                code: DATABASE_ERROR,
                message: err,
                payload: null
            };
        } finally {
            connection.release();
        }
    }

    public async cancelBilling(params: Array<any>): Promise<ResponseDto> {
        const connection: PoolConnection = await maria.getConnection();

        try {
            const sql = "UPDATE payments " +
                        "SET status=? " +
                        "WHERE payment_id=?";

            await connection.beginTransaction();

            const [results, rows]: any = await connection.query(
                sql,
                params
            );

            await connection.commit();

            if(results.affectedRows < 1) {
                return {
                    code: NOT_CANCELED_BILLING,
                    message: "결제 취소 실패!",
                    payload: null
                };
            }

            return {
                code: CANCELED_BILLING,
                message: "결제 취소!",
                payload: params[1]
            };
        } catch(error) {
            await connection.rollback();

            connection.release();
            
            return {
                code: DATABASE_ERROR,
                message: error,
                payload: null
            };
        } finally {
            connection.release();
        }
    }

    public async reBilling(params: Array<any>): Promise<ResponseDto> {
        const connection: PoolConnection = await maria.getConnection();

        try {
            const sql = "UPDATE payments " +
                        "SET status=? " +
                        "WHERE payment_id=?";

            await connection.beginTransaction();

            const [results]: any = await connection.query(
                sql,
                params
            );

            await connection.commit();

            const dto: any = await this.getPayment(params[1]);

            if(results.affectedRows < 1) {
                return {
                    code: FAILED_RE_BILLING,
                    message: "재결제 실패!",
                    payload: null
                };
            }

            return {
                code: RE_BILLING,
                message: "재결제 성공!",
                payload: dto.payload
            };
        } catch(error) {
            await connection.rollback();

            connection.release();
            
            return {
                code: DATABASE_ERROR,
                message: error,
                payload: null
            };
        } finally {
            connection.release();
        }
    }

    public async getPayment(params: Array<any>): Promise<ResponseDto> {
        const connection: PoolConnection = await maria.getConnection();
    
        try {
            const sql: string = "SELECT * " +
                                "FROM payments " +
                                "WHERE payment_id=?";
            
            await connection.beginTransaction();

            const [rows, fields]: any = await connection.query(
                sql,
                params
            );

            await connection.commit();

            if(!(rows[0] as RowDataPacket)) {
                return {
                    code: NOT_FOUND_PAYMNET,
                    message: "결제 데이터가 없습니다!",
                    payload: null
                };
            }

            return {
                code: FOUND_PAYMENT,
                message: "결제 데이터를 찾았습니다!",
                payload: rows[0]
            };
        } catch(error) {
            await connection.rollback();

            connection.release();
            
            return {
                code: DATABASE_ERROR,
                message: error,
                payload: null
            };
        } finally {
            connection.release();
        }
    }

    public async getPayments(params: Array<any>): Promise<ResponseDto> {
        const connection: PoolConnection = await maria.getConnection();
    
        try {
            const sql: string = "SELECT * " +
                                "FROM payments " +
                                "WHERE user_id=?";
            
            await connection.beginTransaction();

            const [rows, fields]: any = await connection.query(
                sql,
                params
            );

            await connection.commit();

            if(!(rows as RowDataPacket)) {
                return {
                    code: NOT_FOUND_PAYMNET,
                    message: "결제 데이터가 없습니다!",
                    payload: null
                };
            }

            console.log(rows);

            return {
                code: FOUND_PAYMENT,
                message: "결제 데이터를 찾았습니다!",
                payload: rows
            };
        } catch(error) {
            await connection.rollback();

            connection.release();
            
            return {
                code: DATABASE_ERROR,
                message: error,
                payload: null
            };
        } finally {
            connection.release();
        }
    }

    public async reserveError(params: Array<any>): Promise<ResponseDto> {
        const connection: PoolConnection = await maria.getConnection();
    
        try {
            const sql = "UPDATE payments " +
                        "SET status=? " +
                        "WHERE payment_id=?";

            await connection.beginTransaction();

            const [results, rows]: any = await connection.query(
                sql,
                params
            );

            await connection.commit();

            if(results.affectedRows < 1) {
                return {
                    code: FAILED_RE_BILLING,
                    message: "카풀 예약 롤백 실패!",
                    payload: null
                };
            }

            return {
                code: RE_BILLING,
                message: "카풀 예약 롤백 성공!",
                payload: rows[0]
            };
        } catch(error) {
            await connection.rollback();

            connection.release();

            return {
                code: DATABASE_ERROR,
                message: error,
                payload: null
            };
        } finally {
            connection.release();
        }
    }

    public async cancelError(params: Array<any>): Promise<ResponseDto> {
        const connection: PoolConnection = await maria.getConnection();
    
        try {
            const sql = "UPDATE payments " +
                        "SET status=? " +
                        "WHERE payment_id=?";

            await connection.beginTransaction();

            const [results, rows]: any = await connection.query(
                sql,
                params
            );

            await connection.commit();

            if(results.affectedRows < 1) {
                return {
                    code: FAILED_ROLL_BACK_CANCEL,
                    message: "카풀 취소 롤백 실패!",
                    payload: null
                };
            }

            return {
                code: ROLL_BACK_CANCEL,
                message: "카풀 취소 롤백 성공!",
                payload: rows[0]
            };
        } catch(error) {
            await connection.rollback();

            connection.release();

            return {
                code: DATABASE_ERROR,
                message: error,
                payload: null
            };
        } finally {
            connection.release();
        }
    }
}

export { PaymentRepository };
  • ./src/service/payment.service.ts
import { ApolloError } from "apollo-server-core";
import { JwtUtils } from '../utils/jwt.utils';
import { PaymentRepository } from '../repository/payment.repository';
import { CANCELED_BILLING, DATABASE_ERROR, EXCEPTION_ERROR, FAILED_RE_BILLING, FAILED_ROLL_BACK_CANCEL, FAILED_ROLL_BACK_RESERVE, FAILURE, FOUND_PAYMENT, NOT_CANCELED_BILLING, NOT_FOUND_PAYMNET, NOT_SAVED_PAYMENT, RE_BILLING, ROLL_BACK_CANCEL, ROLL_BACK_RESERVE, SAVED_PAYMENT, UN_AUTHENTICATION } from "../constants/result.code";
import { logger } from "../middlewares/logging";
import { idGenerator } from "../utils/payment.id.generator";
import { CANCELED, COMPLETE } from "../constants/payment.status";
import { BillingDto } from "../dtos/billing.dto";
import { ResponseDto } from "../dtos/response.dto";
import { producer } from "../kafka/producer";
import { CancelBillingDto } from "../dtos/cancel.billing.dto";
import { ReBillingDto } from "../dtos/re.billing.dto";
import { IPayment } from "../interface/payment.interface";
import { GetPaymentDto } from "../dtos/get.payment.dto";
import { ReserveErrorDto } from "../dtos/reserve.error.dto";
import { ReserveDto } from "../dtos/reserve.dto";
import { CancelErrorDto } from "../dtos/cancel.error.dto";

class PaymentService {
    private jwtUtils: JwtUtils;
    private repository: PaymentRepository;

    constructor() {
        this.jwtUtils = new JwtUtils();
        this.repository = new PaymentRepository();
    }

    public async billing(
        args: any,
        context: any
    ): Promise<number> {
        try {
            const user_id: string = (await this.jwtUtils.verify(context.token)).data;
            const dto: BillingDto = args.input;

            if(!user_id) {
                logger.error("billing: 다시 로그인해주세요!");

                throw new ApolloError(
                    "다시 로그인해주세요!",
                    "UN_AUTHENTICATION", {
                        'code_number': UN_AUTHENTICATION
                    }
                );
            }

            const PARAMS = [idGenerator(),
                            dto.payment_type,
                            dto.cost,
                            dto.payment_name,
                            dto.user_nickname,
                            user_id,
                            dto.ride_info_id,
                            COMPLETE];

            const response: ResponseDto = await this.repository.saveBilling(PARAMS);

            if(response.code === DATABASE_ERROR) {
                logger.error("billing: " + response.message);

                throw new ApolloError(
                    response.message,
                    response.code.toString(), {
                        'code_number': response.code
                    }
                );
            }
            
            if(response.code === NOT_SAVED_PAYMENT) {
                logger.error("billing: " + response.message);

                throw new ApolloError(
                    response.message,
                    response.code.toString(), {
                        'code_number': response.code
                    }
                );
            }

            if(response.code === SAVED_PAYMENT) {
                const reserveInput: ReserveDto = {
                    cost: dto.cost,
                    ride_info_id: dto.ride_info_id,
                    user_id: user_id
                };
                
                producer.send([
                    { 
                        topic: "RESERVE",
                        messages: JSON.stringify(reserveInput),
                        partition: 0
                    }
                ], function(error, result) {
                    if(error) {
                        logger.error("billing: " + error);
                    }
                });

                return response.code;
            }
        
            logger.error("billing: 서버 에러!");

            return FAILURE;
        } catch(error) {
            logger.error("billing: " + error);

            throw new ApolloError(
                error,
                "EXCEPTION_ERROR", {
                    'code_number': EXCEPTION_ERROR
                }
            );
        }
    }

    public async cancelBilling(
        args: any,
        context: any
    ): Promise<number> {
        try {
            const user_id: string = (await this.jwtUtils.verify(context.token)).data;
            const dto: CancelBillingDto = args.input;

            if(!user_id) {
                logger.error("cancelBilling: 다시 로그인해주세요!");

                throw new ApolloError(
                    "다시 로그인해주세요!",
                    "UN_AUTHENTICATION", {
                        'code_number': UN_AUTHENTICATION
                    }
                );
            }

            const PARAMS = [CANCELED,
                            dto.payment_id];
            const response: ResponseDto = await this.repository.cancelBilling(PARAMS);

            if(response.code === DATABASE_ERROR) {
                logger.error("cancelBilling: " + response.message);

                throw new ApolloError(
                    response.message,
                    response.code.toString(), {
                        'code_number': response.code
                    }
                );
            }

            if(response.code === NOT_CANCELED_BILLING) {
                logger.error("cancelBilling: " + response.message);

                throw new ApolloError(
                    response.message,
                    response.code.toString(), {
                        'code_number': response.code
                    }
                );
            }

            if(response.code === CANCELED_BILLING) {
                const cancelInput: CancelBillingDto = {
                    cost: dto.cost,
                    payment_id: response.payload,
                    ride_info_id: dto.ride_info_id,
                    user_id: user_id
                };

                producer.send([
                    { 
                        topic: "CANCEL",
                        messages: JSON.stringify(cancelInput)
                    }
                ], function(error, result) {
                    if(error) {
                        logger.error("cancelBilling: " + error);
                    }
                });

                return response.code;
            }
            
            logger.error("cancelBilling: 서버 에러!");

            return FAILURE;
        } catch(error) {
            logger.error("cancelBilling: " + error);

            throw new ApolloError(
                error,
                "EXCEPTION_ERROR", {
                    'code_number': EXCEPTION_ERROR
                }
            );
        }
    }

    public async reBilling(
        args: any,
        context: any
    ): Promise<number> {
        try {
            const user_id: string = (await this.jwtUtils.verify(context.token)).data;

            if(!user_id) {
                logger.error("reBilling: 다시 로그인해주세요!");

                throw new ApolloError(
                    "다시 로그인해주세요!",
                    "UN_AUTHENTICATION", {
                        'code_number': UN_AUTHENTICATION
                    }
                );
            }

            const dto: ReBillingDto = args;
            const PARAMS: Array<any> = [COMPLETE,
                                        dto.payment_id];
            const response: ResponseDto = await this.repository.reBilling(PARAMS);

            if(response.code === DATABASE_ERROR) {
                logger.error("reBilling: " + response.message);

                throw new ApolloError(
                    response.message,
                    response.code.toString(), {
                        'code_number': response.code
                    }
                );
            }

            if(response.code === FAILED_RE_BILLING) {
                logger.error("reBilling: " + response.message);

                throw new ApolloError(
                    response.message,
                    response.code.toString(), {
                        'code_number': response.code
                    }
                );
            }

            if(response.code === RE_BILLING) {
                const reserveInput: ReserveDto = {
                    cost: response.payload.cost,
                    ride_info_id: response.payload.ride_info_id,
                    user_id: user_id
                };

                producer.send([
                    { 
                        topic: "RESERVE",
                        messages: JSON.stringify(reserveInput)
                    }
                ], function(error, result) {
                    if(error) {
                        logger.error("reBilling: " + error);
                    }
                });

                logger.info("reBilling: " + response.message);

                return response.code;
            }

            logger.error("reBilling: 서버 에러!");

            return FAILURE;
        } catch(error) {
            logger.error("reBilling: " + error);

            throw new ApolloError(
                error,
                "EXCEPTION_ERROR", {
                    'code_number': EXCEPTION_ERROR
                }
            );
        }
    }

    public async getPayments(context: any): Promise<Array<IPayment> | number> {
        try {
            const user_id: string = (await this.jwtUtils.verify(context.token)).data;

            if(!user_id) {
                logger.error("getPayments: 다시 로그인해주세요!");

                throw new ApolloError(
                    "다시 로그인해주세요!",
                    "UN_AUTHENTICATION", {
                        'code_number': UN_AUTHENTICATION
                    }
                );
            }

            const PARAMS: Array<any> = [user_id];
            const response: ResponseDto = await this.repository.getPayments(PARAMS);

            if(response.code === DATABASE_ERROR) {
                logger.error("getPayments: " + response.message);

                throw new ApolloError(
                    response.message,
                    response.code.toString(), {
                        'code_number': response.code
                    }
                );
            }

            if(response.code === NOT_FOUND_PAYMNET) {
                logger.error("getPayments: " + response.message);

                throw new ApolloError(
                    response.message,
                    response.code.toString(), {
                        'code_number': response.code
                    }
                );
            }

            if(response.code === FOUND_PAYMENT) {
                logger.info("getPayments: " + response.message);

                return response.payload;
            }

            logger.error("getPayments: 서버 에러!");

            return FAILURE;
        } catch(error) {
            logger.error("getPayments: " + error);

            throw new ApolloError(
                error,
                "EXCEPTION_ERROR", {
                    'code_number': EXCEPTION_ERROR
                }
            );
        }
    }

    public async getPayment(
        args: any,
        context: any
    ): Promise<IPayment | number> {
        try {
            const user_id: string = (await this.jwtUtils.verify(context.token)).data;

            if(!user_id) {
                logger.error("getPayment: 다시 로그인해주세요!");

                throw new ApolloError(
                    "다시 로그인해주세요!",
                    "UN_AUTHENTICATION", {
                        'code_number': UN_AUTHENTICATION
                    }
                );
            }

            const dto: GetPaymentDto = args;
            const PARAMS: Array<any> = [dto.payment_id];
            const response: ResponseDto = await this.repository.getPayment(PARAMS);

            if(response.code === DATABASE_ERROR) {
                logger.error("getPayment: " + response.message);

                throw new ApolloError(
                    response.message,
                    response.code.toString(), {
                        'code_number': response.code
                    }
                );
            }

            if(response.code === NOT_FOUND_PAYMNET) {
                logger.error("getPayment: " + response.message);

                throw new ApolloError(
                    response.message,
                    response.code.toString(), {
                        'code_number': response.code
                    }
                );
            }

            if(response.code === FOUND_PAYMENT) {
                logger.info("getPayment: " + response.message);

                return response.payload;
            }

            logger.error("getPayment: 서버 에러!");

            return FAILURE;
        } catch(error) {
            logger.error("getPayment: " + error);

            throw new ApolloError(
                error,
                "EXCEPTION_ERROR", {
                    'code_number': EXCEPTION_ERROR
                }
            );
        }
    } 

    public async reserveError(payload: string): Promise<number> {
        try {
            const dto: ReserveErrorDto = JSON.parse(payload);
            const PARAMS: Array<any> = [CANCELED, 
                            dto.payment_id];
            const response: ResponseDto = await this.repository.reserveError(PARAMS);

            if(response.code === DATABASE_ERROR) {
                logger.error("reserveError: " + response.message);

                throw new ApolloError(
                    response.message,
                    response.code.toString(), {
                        'code_number': response.code
                    }
                );
            }

            if(response.code === FAILED_ROLL_BACK_RESERVE) {
                logger.error("reserveError: " + response.message);

                throw new ApolloError(
                    response.message,
                    response.code.toString(), {
                        'code_number': response.code
                    }
                );
            }

            if(response.code === ROLL_BACK_RESERVE) {
                logger.info("reserveError: " + response.message);

                return response.code;
            }

            logger.error("reserveError: 서버 에러!");

            return FAILURE;
        } catch(error) {
            logger.error("getPayment: " + error);

            throw new ApolloError(
                error,
                "EXCEPTION_ERROR", {
                    'code_number': EXCEPTION_ERROR
                }
            );
        }
    }

    public async cancelError(payload: string): Promise<number> {
        try {
            const dto: CancelErrorDto = JSON.parse(payload);
            const PARAMS: Array<any> = [COMPLETE,
                                        dto.payment_id];
            const response: ResponseDto = await this.repository.cancelError(PARAMS);

            if(response.code === DATABASE_ERROR) {
                logger.error("cancelError: " + response.message);

                throw new ApolloError(
                    response.message,
                    response.code.toString(), {
                        'code_number': response.code
                    }
                );
            }

            if(response.code === FAILED_ROLL_BACK_CANCEL) {
                logger.error("cancelError: " + response.message);

                throw new ApolloError(
                    response.message,
                    response.code.toString(), {
                        'code_number': response.code
                    }
                );
            }

            if(response.code === ROLL_BACK_CANCEL) {
                logger.info("cancelError: " + response.message);

                return response.code;
            }
        } catch(error) {
            logger.error("getPayment: " + error);

            throw new ApolloError(
                error,
                "EXCEPTION_ERROR", {
                    'code_number': EXCEPTION_ERROR
                }
            );
        }
    }
}

export { PaymentService };

메서드를 설명하도록 하겠습니다.

1) billing

요금을 지불하는 mutation입니다. user_id를 검사하여 인증에 대한 부분을 체크하고, payment 데이터 저장을 위한 파라미터를 생성하여 데이터 베이스에 저장합니다. 그리고 반환받은 여러 에러 케이스들을 서비스 클래스에서 처리하고, 최종적으로 성공 코드로 넘어가게 됩니다. 이 부분에서는 kafka 메시지 큐를 이용하여 ride-service의 카풀 예약 메서드와 통신하게 됩니다. 즉, 성공적으로 결제가 완료되면 카풀 데이터의 passengers 데이터에 결제한 user_id가 추가됩니다.

2) cancelBilling

요금을 취소하는 mutation입니다. 카풀이 시작되기전까지 다시 재결제를 할지도 모르는 경우가 존재하므로 상태값을 CANCELED로 변경시키고 kafka 메시지 큐를 이용하여 카풀 데이터에서 해당 유저의 아이디 값을 빼도록 합니다.

3) reBilling

재결제를 진행하는 mutation입니다. 취소 혹은 네트워크나 서버 장애로 인해 정상적으로 결제가 진행이 안될 경우 해당 메서드를 이용하여 재결제를 진행하고, 카풀 데이터에 유저 아이디 값을 추가하도록 해주는 메서드입니다.

4) getPayments

유저의 아이디 값을 이용하여 유저가 결제한 내역들을 불러오는 query입니다.

5) getPayment

payment_id 값을 이용하여 결제 상세 정보를 볼 수 있게 해주는 query입니다.

6) reserveError

ride-service의 reserve mutation이 오류가 났을 경우 수행되는 메서드입니다. 예약 오류가 생기게 되면 reserve 메서드에서는 이전 상태로 데이터를 돌리는 롤백 로직을 수행하고, payment-service의 reserveError에서는 payment_id에 해당하는 데이터의 상태를 CANCELED로 변경시킵니다.

7) cancelError

ride-service의 cancel mutation이 오류가 발생했을 경우 수행되는 메서드입니다. 예약 취소 오류가 발생하면 cancel 메서드에서는 롤백이 수행되고, payment-service의 cancelError 메서드에서는 마찬가지로 payment_id의 데이터 상태를 COMPLETED로 되돌립니다.

  • ./src/resolvers/resolver.ts
import { IPayment } from '../interface/payment.interface';
import { PaymentService } from '../service/payment.service';
import { pubsub } from '../utils/redis.client';

const service = new PaymentService();

const resolvers = {
    Query: {
        getPayments: async (
            _,
            args,
            context,
            info
        ): Promise<Array<IPayment> | number> => {
            return await service.getPayments(context);
        },
        getPayment: async (
            _,
            args,
            context,
            info
        ): Promise<IPayment | number> => {
            return await service.getPayment(args, context)
        }
    },
    Mutation: {
        billing: async (
            _,
            args,
            context,
            info
        ): Promise<number> => {
            return await service.billing(args, context);
        },
        cancelBilling: async (
            _,
            args,
            context,
            info
        ): Promise<number> => {
            return await service.cancelBilling(args, context);    
        },
        reBilling: async (
            _,
            args,
            context,
            info
        ): Promise<number> => {
            return await service.reBilling(args, context);
        }
    },
    Subscription: {
        reserveError: {
            resolve: async (payload: string): Promise<number> => {
                return await service.reserveError(payload);
            },
            subscribe: () => pubsub.asyncIterator('RESERVE_ERROR')
        },
        cancelError: {
            resolve: async (payload: string): Promise<number> => {
                return await service.cancelError(payload);
            },
            subscribe: () => pubsub.asyncIterator('CANCEL_ERROR')
        }
    }
};

export { resolvers };

이렇게 graphql 통신을 위한 resolver까지 완성하였습니다.

최종적으로 서버 코드를 작성하여 payment-service 서버를 작동시켜보도록 하겠습니다.

  • ./src/index.ts
require('dotenv').config();

const { 
    constraintDirective,
    constraintDirectiveTypeDefs
} = require('graphql-constraint-directive');
const { ApolloServerPluginDrainHttpServer } = require('apollo-server-core');
const { ApolloServer } = require('apollo-server-express');
const { buildSubgraphSchema } = require('@apollo/subgraph');
const { mergeTypeDefs } = require('@graphql-tools/merge');
const cors = require('cors');
const express = require('express');
const http = require('http');

const { PORT } = process.env;

const queries = require('./typeDefs/query');
const mutations = require('./typeDefs/mutation');
const types = require('./typeDefs/type');
const inputs = require('./typeDefs/input');
const enums = require('./typeDefs/enum');

const typeDefs = mergeTypeDefs([
    constraintDirectiveTypeDefs,
    queries,
    mutations,
    types,
    inputs,
    enums
]);

const resolvers = require('./resolvers/resolver');

async function startServer(
    typeDefs,
    resolvers
) {
    const app = express();
    const httpServer = http.createServer(app);
    const server = new ApolloServer({
        schema: buildSubgraphSchema([{
            typeDefs: typeDefs,
            resolvers: resolvers
        }]),
        context: ({ req }) => {
            const token = req.headers.token ? req.headers.token : null;
            
            return { token };
        },
        plugins: [
            ApolloServerPluginDrainHttpServer({ httpServer })
        ],
        schemaTransforms: [constraintDirective()]
    });

    app.use(cors());

    await server.start();

    server.applyMiddleware({
        app,
        path: '/payment-service',
        cors: false
    });

    await new Promise(resolve => httpServer.listen({
        port: PORT
    }, resolve));

    console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`);
}

startServer(
    typeDefs,
    resolvers
);

2) ride-service 메시지 구독

  • ride-service
npm install kafka-node

이전에 작성해두었던 mutation의 reserve를 지우고, cancel 메서드를 추가하여 카프카와 연동하도록 하겠습니다.

  • ./src/utils/kafka.client.ts
import kafka from 'kafka-node';
import { KAFKA_HOST } from '../config/env.variable';

const client = new kafka.KafkaClient({ kafkaHost: KAFKA_HOST });

export { client };
  • ./src/kafka/producer.ts
import { client } from '../utils/kafka.client';
import kafka, { Producer } from 'kafka-node';

const producer: Producer = new kafka.Producer(client);

export { producer };
  • ./src/kafka/consumer.ts
import { client } from '../utils/kafka.client';
import kafka, { Consumer } from 'kafka-node';

const topics = [
    {
        topic: 'RESERVE',
        partition: 0
    },
    {
        topic: 'CANCEL',
        partition: 0
    }
];
const options = {};

const consumer: Consumer = new kafka.Consumer(client,
                                              topics,
                                              options);

client.on('error', err => console.log(err));

export { consumer };

ride-service에서는 RESERVE, CANCEL 토픽을 다루므로 이를 구독하기 위한 consumer를 작성하였습니다.

  • ./src/kafka/kafka.service.ts
import { consumer } from './consumer';
import { RideService } from '../service/ride.service';

const kafkaService = async () => {
    const service = new RideService();

    consumer.on('message', async function(message: any) {
        if(message.topic === "RESERVE") {
            return service.reserve(message.value);
        } else if(message.topic === "CANCEL") {
            return service.cancel(message.value);
        }
    });

    consumer.on('error', error => {
        console.log(error);
    });
};

export { kafkaService };
  • ./src/service/ride.service.ts
...

class RideService {
    ...
    public async reserve(payload: string) {
        try {
            const dto: ReserveDto = JSON.parse(payload);

            if(!dto.user_id) {
                producer.send([{
                    topic: "RESERVE_ERROR", 
                    messages: [{
                        key: "reserveError",
                        value: payload
                    }]
                }], error => logger.error("reserve" + error));
                
                logger.error("reserve: 다시 로그인해주세요!");

                throw new ApolloError(
                    "다시 로그인해주세요!",
                    "UN_AUTHENTICATION", {
                        'code_number': UN_AUTHENTICATION
                    }
                );
            }

            const result: ResponseDto = await this.repository.findInfoByRideInfoId(dto.ride_info_id);

            if(result.code === DATABASE_ERROR) {
                logger.error("reserve: " + result.message);

                throw new ApolloError(
                    result.message,
                    result.code.toString(), {
                        'code_number': result.code
                    }
                );
            }

            if(result.code === NOT_FOUND_CARPOOL) {
                producer.send([{
                    topic: "RESERVE_ERROR", 
                    messages: [{
                        key: "reserveError",
                        value: payload
                    }]
                }], error => logger.error("reserve: " + error));
                
                logger.error("reserve: " + result.message);

                throw new ApolloError(
                    result.message, 
                    result.code.toString(), {
                        'code_number': result.code
                    }
                );
            }

            if(result.payload.rider_id === dto.user_id) {
                producer.send([{
                    topic: "RESERVE_ERROR", 
                    messages: [{
                        key: "reserveError",
                        value: payload
                    }]
                }], error => logger.error("reserve: " + error));

                logger.error("reserve: 본인 카풀에 예약할 수 없습니다!");

                throw new ApolloError(
                    "본인 카풀에 예약할 수 없습니다!", 
                    "NOT_RESERVED_CARPOOL", {
                        'code_number': NOT_RESERVED_CARPOOL
                    }
                );
            }

            if(result.payload.passengers.includes(dto.user_id)) {
                producer.send([{
                    topic: "RESERVE_ERROR", 
                    messages: [{
                        key: "reserveError",
                        value: payload
                    }]
                }], error => logger.error("reserve" + error));

                logger.error("reserve: 이미 예약이 되어있습니다!");

                throw new ApolloError(
                    "이미 예약이 되어있습니다!", 
                    "NOT_RESERVED_CARPOOL", {
                        'code_number': NOT_RESERVED_CARPOOL
                    }
                );
            }

            const updatedResult: ResponseDto = await this.repository.updatePassenger(
                dto.ride_info_id, 
                dto.user_id,
                result.payload.cost,
                dto.cost
            );

            if(updatedResult.code === DATABASE_ERROR) {
                producer.send([{
                    topic: "RESERVE_ERROR", 
                    messages: [{
                        key: "reserveError",
                        value: payload
                    }]
                }], error => logger.error("reserve" + error));

                logger.error("reserve: " + updatedResult.message);

                throw new ApolloError(
                    updatedResult.message,
                    updatedResult.code.toString(), {
                        'code_number': updatedResult.code
                    }
                );
            }

            if(updatedResult.code === UN_REGISTERED_PASSENGER) {
                producer.send([{
                    topic: "RESERVE_ERROR", 
                    messages: [{
                        key: "reserveError",
                        value: payload
                    }]
                }], error => logger.error("reserve" + error));

                logger.error("reserve: " + updatedResult.message);

                throw new ApolloError(
                    updatedResult.message, 
                    updatedResult.code.toString(), {
                        'code_number': updatedResult.code
                    }
                );
            }

            if(updatedResult.code === REGISTERED_PASSENGER) {
                const dto: ResponseDto = await this.repository.findInfosByStatus(PENDING);

                if(dto.code === DATABASE_ERROR) {
                    producer.send([{
                        topic: "RESERVE_ERROR", 
                        messages: [{
                            key: "reserveError",
                            value: payload
                        }]
                    }], error => logger.error("reserve" + error));

                    logger.error("reserve: " + dto.message);

                    throw new ApolloError(
                        dto.message,
                        dto.code.toString(), {
                            'code_number': dto.code
                        }
                    );
                }

                if(dto.code === NOT_FOUND_CARPOOL) {
                    producer.send([{
                        topic: "RESERVE_ERROR", 
                        messages: [{
                            key: "reserveError",
                            value: payload
                        }]
                    }], error => logger.error("reserve" + error));

                    logger.error("reserve: " + dto.message);

                    throw new ApolloError(
                        dto.message,
                        dto.code.toString(), {
                            'code_number': dto.code
                        }
                    );
                }

                logger.info("reserve: " + dto.message);

                pubsub.publish("UPDATE_CARPOOLS", {
                    updateCarpools: dto.payload as Array<IInfo>
                });

                return updatedResult.code;
            }

            producer.send([{
                topic: "RESERVE_ERROR", 
                messages: [{
                    key: "reserveError",
                    value: payload
                }]
            }], error => logger.error("reserve" + error));

            logger.error("reserve: 서버 에러");

            return FAILURE;
        } catch(err) {
            producer.send([{
                topic: "RESERVE_ERROR", 
                messages: [{
                    key: "reserveError",
                    value: payload
                }]
            }], error => logger.error("reserve: " + error));

            logger.error("reserve: " + err);

            throw new ApolloError(
                err, 
                "EXCEPTION_ERROR", {
                    'code_number': EXCEPTION_ERROR
                }
            );
        }
    }
    
    public async cancel(payload: string) {
        try {
            const dto: CancelDto = JSON.parse(payload);

            if(!dto.user_id) {
                producer.send([{
                    topic: "CANCEL_ERROR", 
                    messages: [{
                        key: "cancelError",
                        value: payload
                    }]
                }], error => console.log(error));

                logger.error("cancel: 다시 로그인해주세요!");

                throw new ApolloError(
                    "다시 로그인해주세요!",
                    "UN_AUTHENTICATION", {
                        'code_number': UN_AUTHENTICATION
                    }
                );
            }

            const response: ResponseDto = await this.repository.findInfoByRideInfoId(dto.ride_info_id);

            if(response.code === DATABASE_ERROR) {
                producer.send([{
                    topic: "CANCEL_ERROR", 
                    messages: [{
                        key: "cancelError",
                        value: payload
                    }]
                }], error => console.log(error));
                
                logger.error("cancel: " + response.message);

                throw new ApolloError(
                    response.message, 
                    response.code.toString(), {
                        'code_number': response.code
                    }
                );
            }

            if(response.code === NOT_FOUND_CARPOOL) {
                producer.send([{
                    topic: "CANCEL_ERROR", 
                    messages: [{
                        key: "cancelError",
                        value: payload
                    }]
                }], error => console.log(error));
                
                logger.error("cancel: " + response.message);

                throw new ApolloError(
                    response.message, 
                    response.code.toString(), {
                        'code_number': response.code
                    }
                );
            }

            const updatedResult: ResponseDto = await this.repository.cancelCarpool(
                dto.ride_info_id, 
                dto.user_id, 
                response.payload.cost,
                dto.cost
            );

            if(updatedResult.code === DATABASE_ERROR) {
                producer.send([{
                    topic: "CANCEL_ERROR", 
                    messages: [{
                        key: "cancelError",
                        value: payload
                    }]
                }], error => console.log(error));
                
                logger.error("cancel: " + updatedResult.message);

                throw new ApolloError(
                    updatedResult.message, 
                    updatedResult.code.toString(), {
                        'code_number': updatedResult.code
                    }
                );
            }

            if(updatedResult.code === NOT_CANCELED_CARPOOL) {
                producer.send([{
                    topic: "CANCEL_ERROR", 
                    messages: [{
                        key: "cancelError",
                        value: payload
                    }]
                }], error => console.log(error));
                
                logger.error("cancel: " + updatedResult.message);

                throw new ApolloError(
                    updatedResult.message, 
                    updatedResult.code.toString(), {
                        'code_number': updatedResult.code
                    }
                );
            }

            if(updatedResult.code === CANCELED_CARPOOL) {
                const dto: ResponseDto = await this.repository.findInfosByStatus(PENDING);

                if(dto.code === DATABASE_ERROR) {
                    producer.send([{
                        topic: "CANCEL_ERROR", 
                        messages: [{
                            key: "cancelError",
                            value: payload
                        }]
                    }], error => logger.error("cancel: " + error));

                    logger.error("cancel: " + dto.message);

                    throw new ApolloError(
                        dto.message,
                        dto.code.toString(), {
                            'code_number': dto.code
                        }
                    );
                }

                if(dto.code === NOT_FOUND_CARPOOL) {
                    producer.send([{
                        topic: "CANCEL_ERROR", 
                        messages: [{
                            key: "cancelError",
                            value: payload
                        }]
                    }], error => logger.error("cancel: " + error));

                    logger.error("cancel: " + dto.message);

                    throw new ApolloError(
                        dto.message,
                        dto.code.toString(), {
                            'code_number': dto.code
                        }
                    );
                }

                logger.info("cancel: " + dto.message);

                pubsub.publish("UPDATE_CARPOOLS", {
                    updateCarpools: dto.payload as Array<IInfo>
                });

                return updatedResult.code;
            }
            
            producer.send([{
                topic: "CANCEL_ERROR", 
                messages: [{
                    key: "cancelError",
                    value: payload
                }]
            }], error => console.log(error));

            logger.error("cancel: 서버 에러!");

            return FAILURE;
        } catch(err) {
            producer.send([{
                topic: "CANCEL_ERROR", 
                messages: [{
                    key: "cancelError",
                    value: payload
                }]
            }], error => console.log(error));

            logger.error("cancel: " + err);
            
            throw new ApolloError(
                err, 
                "EXCEPTION_ERROR", {
                    'code_number': EXCEPTION_ERROR
                }
            );
        }
    }
    ...
};

export { PaymentService }

reserve, cancel 메서드를 보면 에러가 발생하는 부분마다 ERROR 메시지를 발행하도록 해두었습니다. 이는 payment-service에서 결제를 먼저 진행하고 ride-service에서 예약 혹은 예약 취소를 진행하기 때문에 에러가 발생 시 이에 대한 결제를 롤백하기 위한 트랜잭션입니다.

3) payment-service 롤백 메시지 구독

  • ./src/kafka/consumer.ts
import { client } from '../utils/kafka.client';
import kafka, { Consumer, OffsetFetchRequest } from 'kafka-node';

const topics: Array<OffsetFetchRequest> = [
    {
        topic: 'RESERVE_ERROR',
        partition: 0
    },
    {
        topic: 'CANCEL_ERROR',
        partition: 0
    }
];
const option: object = {};

const consumer: Consumer = new kafka.Consumer(client,
                                              topics,
                                              option);

export { consumer };

메시지 구독을 위한 consumer 모듈입니다. payment-service에서는 RESERVE_ERROR, CANCEL_ERROR 2가지 토픽인 예약 오류, 취소 오류를 다루며 해당 토픽에 메시지가 들어오면 관련 로직을 처리합니다.

  • ./src/kafka/kafka.service.ts
import { consumer } from './consumer';
import { PaymentService } from '../service/payment.service';

const kafkaService = async () => {
    const service = new PaymentService();

    consumer.on('message', async function(message: any) {
        if(message.topic === "RESERVE_ERROR") {
            return service.reserveError(message.value);
        } else if(message.topic === "CANCEL_ERROR") {
            return service.cancelError(message.value);
        }
    });

    consumer.on('error', error => {
        console.log(error);
    });
};

export { kafkaService };

메서드들이 작성되었으니 테스트를 진행해보도록 하겠습니다.

#3 테스트

1) 예약

{
  "_id": {
    "$oid": "61dd04854a3b9e5e990b46fb"
  },
  "ride_info_id": "IC-34zokfbj6-8d35q5ifpnv",
  "rider_id": "cd9f524e-2609-4acc-95f6-abde4df6b479",
  "passengers": [
    "bb1d8ab7-2180-400d-ad7a-d7c88c1d61e9"
  ],
  "start_time": "2022. 1. 11. 오후 7:55:33",
  "start_location": {
    "description": "서울특별시 중구 신당동 청구동 주민센터",
    "latitude": "37.5570753",
    "longitude": "127.0146477"
  },
  "dest_location": {
    "description": "서울특별시 강남구 학동로20길 논현1동 주민센터",
    "latitude": "37.5114869",
    "longitude": "127.0285217"
  },
  "current_location": {
    "latitude": null,
    "longitude": null
  },
  "status": "PENDING",
  "cost": 9000,
  "car": {
    "car_name": "소나타",
    "car_number": "29허1002",
    "car_size": "중형"
  },
  "__v": 0
}

2) 예약 취소

{
  "_id": {
    "$oid": "61dd04854a3b9e5e990b46fb"
  },
  "ride_info_id": "IC-34zokfbj6-8d35q5ifpnv",
  "rider_id": "cd9f524e-2609-4acc-95f6-abde4df6b479",
  "passengers": [],
  "start_time": "2022. 1. 11. 오후 7:55:33",
  "start_location": {
    "description": "서울특별시 중구 신당동 청구동 주민센터",
    "latitude": "37.5570753",
    "longitude": "127.0146477"
  },
  "dest_location": {
    "description": "서울특별시 강남구 학동로20길 논현1동 주민센터",
    "latitude": "37.5114869",
    "longitude": "127.0285217"
  },
  "current_location": {
    "latitude": null,
    "longitude": null
  },
  "status": "PENDING",
  "cost": 6000,
  "car": {
    "car_name": "소나타",
    "car_number": "29허1002",
    "car_size": "중형"
  },
  "__v": 0
}

3) 재결제

{
  "_id": {
    "$oid": "61dd04854a3b9e5e990b46fb"
  },
  "ride_info_id": "IC-34zokfbj6-8d35q5ifpnv",
  "rider_id": "cd9f524e-2609-4acc-95f6-abde4df6b479",
  "passengers": [
    "bb1d8ab7-2180-400d-ad7a-d7c88c1d61e9"
  ],
  "start_time": "2022. 1. 11. 오후 7:55:33",
  "start_location": {
    "description": "서울특별시 중구 신당동 청구동 주민센터",
    "latitude": "37.5570753",
    "longitude": "127.0146477"
  },
  "dest_location": {
    "description": "서울특별시 강남구 학동로20길 논현1동 주민센터",
    "latitude": "37.5114869",
    "longitude": "127.0285217"
  },
  "current_location": {
    "latitude": null,
    "longitude": null
  },
  "status": "PENDING",
  "cost": 9000,
  "car": {
    "car_name": "소나타",
    "car_number": "29허1002",
    "car_size": "중형"
  },
  "__v": 0
}

4) 결제 내역 불러오기

5) 결제 내역 불러오기(상세)

테스트를 진행한 결과 모든 값들이 잘 받아오는 모습을 볼 수 있습니다. 다음 포스트에서는 결제 관련 스크린과 데이터를 받아오는 부분을 작성하도록 하겠습니다.

좋은 웹페이지 즐겨찾기