Carpool(React Native & Express & apollo federation & Mariadb, Mongodb) - 9. ride-service(1)

yellow_note·2021년 12월 30일
0

#1 ride-service

ride service를 작성하기전에 카풀을 진행하기 위한 스토리를 잠깐 살펴 보도록 하겠습니다.

  • 네비게이션 탭을 카풀 탭으로 변경하도록 하겠습니다.

1) 드라이버는 하단의 카풀 탭에 들어갑니다.

2) 카풀 등록 시 주의 사항을 확인한 후 카풀 등록 스크린으로 들어갑니다. (카풀은 운전자 포함 최소 3명부터 운행이 가능하며, 목적지까지 5km당 승객에게 3000원의 요금을 부과합니다.)

3) 드라이버는 다음의 항목을 작성하여 카풀을 등록합니다. (차량 정보, 출발 위치, 목적지, 출발 시간)

4) 홈 화면엔 하나의 카드로, 맵 화면엔 마커로 카풀 정보가 표시됩니다.

5) 승객은 카풀 정보를 확인한 후 자신과 목적지가 맞는 카풀에 예약하기를 누릅니다.

6) 시작 시간까지 탑승객들을 기다린 후 카풀 운행 버튼을 클릭합니다.

7) 목적지까지 안전하게 운행한 후 드라이버는 카풀 종료 버튼을 클릭합니다.

8) 승객은 선택적으로 리뷰를 작성할 수 있습니다.

그러면 이 스토리를 바탕으로 한 api흐름도 및 데이터베이스 컬렉션들을 살펴 보도록 하겠습니다.

#2 데이터베이스 컬렉션, api 흐름도

  • 데이터베이스 컬렉션

1) info collection

2) car collection

위와 같이 컬렉션을 작성하였습니다.

  • api 내역

1) 카풀 등록

2) 카풀 예약

3) 카풀 시작

4) 카풀 도착

5) 카풀 정보 불러오기

6) 현 위치 수정

7) 현 위치 보기

8) 카풀 내역 불러오기

대략적으로 구상이 완성된 것 같습니다. 이를 바탕으로 ride service를 작성하도록 하겠습니다.

#3 ride service 생성

다음의 명령어로 ride-service를 생성하고 라이브러리를 설치하도록 하겠습니다.

mkdir ride-service
cd ride-service
mkdir src && touch src/index.js
npm init -y
npm install @apollo/subgraph @graphql-tools/merge apollo-server-core apollo-server-express cors dotenv express graphql@15.8.0 graphql-constraint-directive jsonwebtoken 
mongoose nodemon uuid tsc-watch winston winston-daily-rotate-file
npm install -D @types/express typescript

우선 mongoose를 이용하여 ride-service에 필요한 interface, model을 작성하도록 하겠습니다.

  • ./src/interface/car.interface.ts
interface ICar {
    car_name: string,
    car_number: string,
    car_size: string
}

export { ICar }; 
  • ./src/interface/location.interface.ts
interface ILocation {
    description: string,
    latitude: string,
    longitude: string
};

export { ILocation }; 
  • ./src/interface/info.interface.ts
import { ILocation } from './location.interface';
import { ICar } from './car.interface';

interface IInfo {
    ride_info_id: string,
    rider_id: string,
    passengers: [string],
    start_time: string,
    start_location: ILocation,
    dest_location: ILocation,
    current_location: ILocation,
    status: string,
    cost: number,
    car: ICar
};

export { IInfo };
  • ./src/model/car.ts
import mongoose, { Schema } from 'mongoose';
import { ICar } from '../interface/car.interface';

const Car: Schema = new mongoose.Schema<ICar>({
    car_name: {
        type: String,
        required: true
    },
    car_number: {
        type: String,
        required: true
    },
    car_size: {
        type: String,
        required: true
    }
});

export { Car };
  • ./src/model/location.ts
import mongoose, { Schema } from 'mongoose';
import { ILocation } from '../interface/location.interface';

const Location: Schema = new mongoose.Schema<ILocation>({
    description: {
        type: String,
        required: true
    },
    latitude: {
        type: String,
        required: true
    },
    longitude: {
        type: String,
        required: true
    }
});

export { Location };};
  • ./src/model/info.ts
import mongoose, { Model, Schema } from 'mongoose';
import { IInfo } from '../interface/info.interface';
import { Car } from './car';
import { Location } from './location';

const infoSchema: Schema = new mongoose.Schema<IInfo>({
    ride_info_id: {
        type: String,
        required: true,
    },
    rider_id: {
        type: String,
        required: true,
    },
    passengers: {
        type: [String],
        required: false
    },
    start_time: {
        type: String,
        required: true,
    },
    start_location: {
        type: Location,
        required: true
    },
    dest_location: {
        type: Location,
        required: true
    },
    current_location: {
        type: Location,
        required: false
    },
    status: {
        type: String,
        required: true,
    },
    cost: {
        type: Number,
        required: false,
    },
    car: {
        type: Car,
        required: true
    }
});

const Info: Model<IInfo> = mongoose.model("Info", infoSchema);

export { Info };

스키마와 인터페이스를 작성했으니 graphql에 필요한 모듈들을 작성해보도록 하겠습니다.

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

const typeDefs = gql`
    enum RideStatus {
        PENDING,
        BEING,
        FINISHED
    }
`;

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

const typeDefs = gql`
    input CarpoolRegisterInput {
        start_time: String!
        start_location: LocationInput!
        dest_location: LocationInput!
        car: CarInput!
    } 

    input ModifyCurrentLocationInput {
        current_location: LocationInput!
        ride_info_id: String!
    }

    input LocationInput {
        description: String!
        latitude: Float!
        longitude: Float!
    }

    input CarInput {
        car_name: String!
        car_number: String!
        car_size: String!
    }
`;

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

const typeDefs = gql`
    type Info {
        ride_info_id: String!
        rider_id: String!
        passengers: [String]
        start_time: String!
        start_location: Location!
        dest_location: Location!
        current_location: Location
        status: String!
        cost: Int!
        car: Car!
    }
    
    type Location {
        description: String!,
        latitude: Float!,
        longitude: Float!
    }

    type Car {
        car_name: String!,
        car_number: String!,
        car_size: String!
    }
`;

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

const typeDefs = gql`
    type Query {
        loadCarpool: [Info]
        loadCarpoolsByDriver(rider_id: String!): [Info] 
        detailCarpool(ride_info_id: String!): Info
        getCarpools: [Info]
    }
`;

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

const typeDefs = gql`
    type Mutation {
        carpoolRegister(input: CarpoolRegisterInput!): Int!
        start(ride_info_id: String!): Int!
        arrival(ride_info_id: String!): Int!
        modifyCurrentLocation(input: ModifyCurrentLocationInput!): Int!
    }
`;

export default typeDefs;

이렇게 query, mutation, subscription, enum, type, input을 위한 코드를 작성했습니다.

  • jwt.utils.js는 기존의 auth-service의 모듈과 동일합니다.
  • ./src/utils/ride.id.generator.ts
const idGenerator = () => {
    const prefix = "IC-"

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

export { idGenerator };

ride_info_id 생성기입니다.

다음의 라이브러리를 설치하여 redis client를 작성하도록 하겠습니다. 이 redis client를 이용하여 subscription server에 통신을 하고 실시간으로 데이터를 받아오는 작업을 진행합니다.

npm install graphql-redis-subscriptions ioredis @types/ioredis
  • ./src/utils/redis.client.js
import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';

const {
    REDIS_PORT,
    REDIS_URI 
} = process.env;

const redis = new Redis(
    parseInt(REDIS_PORT),
    REDIS_URI
);

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

export { pubsub };

카풀의 상태를 나타내는 상수 모듈입니다.

  • ./src/constants/ride.status.ts
const PENDING = "PENDING";
const BEING = "BEING";
const FINISHED = "FINISHED";

export {
    PENDING,
    BEING,
    FINISHED
};

최종 코드를 나타내는 상수 모듈입니다.

  • ./src/constants/result.code.ts
// ride-service status code
// 200 ~ 249 positive
// 250 ~ 299 negative
// 900 ~ common

const FOUND_CARPOOL: number = 200;
const REGISTERED_CARPOOL: number = 201;
const REGISTERED_PASSENGER: number = 202;
const CANCELED_CARPOOL: number = 203;
const UPDATED_STATUS: number = 204;
const UPDATED_LOCATION: number = 205;
const RESERVED_CARPOOL: number = 206;

const NOT_FOUND_CARPOOL: number = 250;
const UN_REGISTERED_CARPOOL: number = 251;
const UN_REGISTERED_PASSENGER: number = 252;
const NOT_CANCELED_CARPOOL: number = 253;
const NOT_UPDATED_STATUS: number = 254;
const NOT_UPDATED_LOCATION: number = 255;
const NOT_RESERVED_CARPOOL: number = 256;
const LESS_MIN_PASSENGERS: number = 257;

const SUCCESS: number = 900;
const FAILURE: number = 901;
const DATABASE_ERROR: number = 902;
const EXCEPTION_ERROR: number = 903;
const UN_AUTHENTICATION: number = 904;

export {
    FOUND_CARPOOL,
    REGISTERED_CARPOOL,
    REGISTERED_PASSENGER,
    CANCELED_CARPOOL,
    UPDATED_STATUS,
    UPDATED_LOCATION,
    RESERVED_CARPOOL,
    NOT_FOUND_CARPOOL,
    UN_REGISTERED_CARPOOL,
    UN_REGISTERED_PASSENGER,
    NOT_CANCELED_CARPOOL,
    NOT_UPDATED_STATUS,
    NOT_UPDATED_LOCATION,
    NOT_RESERVED_CARPOOL,
    LESS_MIN_PASSENGERS,
    SUCCESS,
    FAILURE,
    DATABASE_ERROR,
    EXCEPTION_ERROR,
    UN_AUTHENTICATION
};

그러면 이를 이용할 resolver, service, repository 클래스를 작성하도록 하겠습니다.

  • ./src/repository/ride.repository.ts
import { CANCELED_CARPOOL, DATABASE_ERROR, FOUND_CARPOOL, NOT_CANCELED_CARPOOL, NOT_FOUND_CARPOOL, NOT_UPDATED_LOCATION, NOT_UPDATED_STATUS, REGISTERED_CARPOOL, REGISTERED_PASSENGER, UN_REGISTERED_CARPOOL, UN_REGISTERED_PASSENGER, UPDATED_LOCATION, UPDATED_STATUS } from "../constants/result.code";
import { RegisterDto } from "../dtos/register.dto";
import { ResponseDto } from "../dtos/response.dto";
import { IInfo } from "../interface/info.interface";
import { ILocation } from "../interface/location.interface";
import { Info } from "../model/info";

class RideRepository {
    constructor() {}

    public async findInfoByRideInfoId(ride_info_id: string): Promise<ResponseDto> {
        try {
            const dto: IInfo = await Info.findOne({ ride_info_id });

            if(!dto) {
                return {
                    code: NOT_FOUND_CARPOOL,
                    message: "카풀 데이터가 없습니다!",
                    payload: null
                };
            }

            return {
                code: FOUND_CARPOOL,
                message: "카풀 데이터를 찾았습니다.",
                payload: dto
            };
        } catch(error) {
            return {
                code: DATABASE_ERROR,
                message: error,
                payload: null
            };
        }
    }

    public async findInfoByRiderId(rider_id: string): Promise<ResponseDto> {
        try {
            const dto: IInfo = await Info.findOne({ rider_id });

            if(!dto) {
                return {
                    code: NOT_FOUND_CARPOOL,
                    message: "카풀 데이터가 없습니다!",
                    payload: null
                };
            }

            return {
                code: FOUND_CARPOOL,
                message: "카풀 데이터를 찾았습니다.",
                payload: dto
            };
        } catch(error) {
            return {
                code: DATABASE_ERROR,
                message: error,
                payload: null
            };
        }
    }

    public async findInfosByUserId(user_id: string): Promise<ResponseDto> {
        try {
            const dto: Array<IInfo> = await Info.find({ 
                $or: [
                    { rider_id: user_id },
                    { passengers: user_id }
                ]
            });

            if(!dto) {
                return {
                    code: NOT_FOUND_CARPOOL,
                    message: "카풀 데이터가 없습니다!",
                    payload: null
                };
            }

            return {
                code: FOUND_CARPOOL,
                message: "카풀 데이터를 찾았습니다.",
                payload: dto
            };
        } catch(error) {
            return {
                code: DATABASE_ERROR,
                message: error,
                payload: null
            };
        }
    }

    public async findInfosByRiderId(rider_id: string): Promise<ResponseDto> {
        try {
            const dto: Array<IInfo> = await Info.find({ rider_id });

            if(!dto) {
                return {
                    code: NOT_FOUND_CARPOOL,
                    message: "카풀 데이터가 없습니다!",
                    payload: null
                };
            }

            return {
                code: FOUND_CARPOOL,
                message: "카풀 데이터를 찾았습니다.",
                payload: dto
            };
        } catch(error) {
            return {
                code: DATABASE_ERROR,
                message: error,
                payload: null
            };
        }
    }

    public async findInfosByStatus(status: string): Promise<ResponseDto> {
        try {
            const dto: Array<IInfo> = await Info.find({ status });

            if(!dto) {
                return {
                    code: NOT_FOUND_CARPOOL,
                    message: "카풀 데이터가 없습니다!",
                    payload: null
                };
            }

            return {
                code: FOUND_CARPOOL,
                message: "카풀 데이터를 찾았습니다.",
                payload: dto
            };
        } catch(error) {
            return {
                code: DATABASE_ERROR,
                message: error,
                payload: null
            };
        }
    }

    public async saveInfo(dto: RegisterDto): Promise<ResponseDto> {
        try {
            const entity: any = new Info(dto);
            const result: IInfo = await entity.save();

            if(!result) {
                return {
                    code: UN_REGISTERED_CARPOOL,
                    message: "카풀 등록 실패!",
                    payload: null
                };
            }

            return {
                code: REGISTERED_CARPOOL,
                message: "카풀 등록 성공!",
                payload: result
            };
        } catch(error) {
            return {
                code: DATABASE_ERROR,
                message: error,
                payload: null
            };
        }
    }

    public async updatePassenger(
        ride_info_id: string, 
        user_id: string,
        total_cost: number, 
        add_cost: number
    ): Promise<ResponseDto> {
        try {
            const dto: IInfo = await Info.findOneAndUpdate(
                { ride_info_id }, 
                { 
                    $push: { passengers: user_id },
                    $set: { cost : (total_cost + add_cost) }
                },
                { new: true }
            );

            if(!dto) {
                return {
                    code: UN_REGISTERED_PASSENGER,
                    message: "승객 등록 실패!",
                    payload: null
                };
            }

            return { 
                code: REGISTERED_PASSENGER,
                message: "승객 등록 성공!",
                payload: dto
            };
        } catch(error) {
            return {
                code: DATABASE_ERROR,
                message: error,
                payload: null
            };
        }
    }

    public async cancelCarpool(
        ride_info_id: string,
        user_id: string,
        total_cost: number,
        minus_cost: number
    ): Promise<ResponseDto> {
        try {
            const dto: IInfo = await Info.findOneAndUpdate(
                { ride_info_id }, 
                { 
                    $pull: { passengers: user_id },
                    $set: { cost : (total_cost - minus_cost) }
                },
                { new: true }
            );

            if(!dto) {
                return {
                    code: NOT_CANCELED_CARPOOL,
                    message: "카풀 취소 실패!",
                    payload: null
                };
            }

            return {
                code: CANCELED_CARPOOL,
                message: "카풀 취소 성공!",
                payload: dto
            }
        } catch(error) {
            return {
                code: DATABASE_ERROR,
                message: error,
                payload: null
            };
        }
    }

    public async updateStatus(
        ride_info_id: string,
        status: string
    ): Promise<ResponseDto> {
        try {
            const dto: IInfo = await Info.findOneAndUpdate(
                { ride_info_id }, 
                { $set: { status }},
                { new: true }
            );

            if(!dto) {
                return {
                    code: UPDATED_STATUS,
                    message: "카풀 상태 업데이트 실패!",
                    payload: null
                };
            }

            return {
                code: NOT_UPDATED_STATUS,
                message: "카풀 상태 업데이트 성공!",
                payload: dto
            };
        } catch(error) {
            return {
                code: DATABASE_ERROR,
                message: error,
                payload: null
            };
        }
    }

    public async updateLocation(
        current_location: ILocation,
        ride_info_id: string
    ): Promise<any> {
        try {
            const dto: IInfo = await Info.findOneAndUpdate(
                { ride_info_id }, 
                { $set: { current_location }},
                { new: true }
            );

            if(!dto) {
                return {
                    code: NOT_UPDATED_LOCATION,
                    message: "현재 위치 불러오기 실패!",
                    payload: null
                };
            }

            return {
                code: UPDATED_LOCATION,
                message: "현재 위치 불러오기 성공!",
                payload: dto.current_location
            };
        } catch(error) {
            return {
                code: DATABASE_ERROR,
                message: error,
                payload: null
            };
        }
    }
}

export { RideRepository };
  • ./src/service/ride.service.ts
import { ApolloError } from "apollo-server-core";
import { producer } from '../kafka/producer';
import { pubsub } from '../utils/redis.client';
import { IInfo } from "../interface/info.interface";
import { JwtUtils } from "../utils/jwt.utils";
import { logger } from "../middlewares/logging";
import { RideRepository } from "../repository/ride.repository";
import { BEING, FINISHED, PENDING } from "../constants/ride.status";
import { idGenerator } from "../utils/ride.id.generator";
import { ILocation } from "../interface/location.interface";
import { ICar } from "../interface/car.interface";
import { ResponseDto } from "../dtos/response.dto";
import { CANCELED_CARPOOL, DATABASE_ERROR, EXCEPTION_ERROR, FAILURE, LESS_MIN_PASSENGERS, NOT_CANCELED_CARPOOL, NOT_FOUND_CARPOOL, NOT_RESERVED_CARPOOL, NOT_UPDATED_LOCATION, NOT_UPDATED_STATUS, REGISTERED_PASSENGER, UN_AUTHENTICATION, UN_REGISTERED_CARPOOL, UN_REGISTERED_PASSENGER, UPDATED_LOCATION, UPDATED_STATUS } from "../constants/result.code";
import { RegisterDto } from "../dtos/register.dto";
import { ReserveDto } from "../dtos/reserve.dto";
import { CancelDto } from "../dtos/cancel.dto";
import { ModifyCurrentLocationDto } from "../dtos/modify.location.dto";

class RideService {
    private jwtUtils: JwtUtils;
    private repository: RideRepository;

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

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

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

                throw new ApolloError(
                    "다시 로그인해주세요!",
                    "UN_AUTHENTICATION", {
                        'code_number': UN_AUTHENTICATION
                    }
                );
            }
            
            const result: ResponseDto = await this.repository.findInfoByRideInfoId(args.ride_info_id);

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

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

            if(result.code === NOT_FOUND_CARPOOL) {
                logger.error("detailCarpool: " + result.message);

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

            logger.info("detailCarpool: " + result.message);

            return result.payload;
        } catch(err) {
            logger.error("detailCarpool: " + err);

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

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

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

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

            const result: ResponseDto = await this.repository.findInfosByUserId(user_id);

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

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

            if(result.code === NOT_FOUND_CARPOOL) {
                logger.error("loadCarpool: " + result.message);

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

            logger.info("loadCarpool: 카풀 데이터를 찾았습니다!");

            return result.payload;
        } catch(err) {
            logger.error("loadCarpool: " + err);

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

    public async loadCarpoolsByDriver(
        args: any,
        context: any
    ): Promise<Array<IInfo>> {
        try {
            const user_id: string = (await this.jwtUtils.verify(context.token)).data;
            
            if(!user_id) {
                logger.error("loadCarpoolsByDriver: 다시 로그인해주세요!");

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

            const result: ResponseDto = await this.repository.findInfosByRiderId(args.rider_id);
            
            if(result.code === DATABASE_ERROR) {
                logger.error("loadCarpoolsByDriver: " + result.message);

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

            if(result.code === NOT_FOUND_CARPOOL) {
                logger.error("loadCarpoolsByDriver: " + result.message);

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

            logger.info("loadCarpoolsByDriver: 카풀 데이터를 찾았습니다!");

            return result.payload;
        } catch(err) {
            logger.error("loadCarpoolsByDriver: " + err);

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

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

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

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

            const result: ResponseDto = await this.repository.findInfosByStatus(PENDING);

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

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

            if(result.code === NOT_FOUND_CARPOOL) {
                logger.error("getCarpools: " + result.message);

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

            logger.info("getCarpools: 카풀 데이터를 찾았습니다!");

            return result.payload;
        } catch(err) {
            logger.error("getCarpools: " + err);

            throw new ApolloError(
                err, 
                "EXCEPTION_ERROR", {
                    'code_number': EXCEPTION_ERROR as number
                }
            );
        }
    }

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

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

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

            const dto: RegisterDto = args.input;
            const entity: IInfo = {
                ride_info_id: idGenerator(),
                rider_id: user_id,
                passengers: [],
                start_time: dto.start_time,
                start_location: dto.start_location,
                dest_location: dto.dest_location,
                current_location: {
                    description: null,
                    latitude: null,
                    longitude: null
                },
                status: PENDING,
                cost: 0,
                car: dto.car
            };
            const result: ResponseDto = await this.repository.saveInfo(entity);

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

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

            if(result.code === UN_REGISTERED_CARPOOL) {
                logger.error("carpoolRegister: " + result.message);

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

            logger.info("carpoolRegister: " + result.message);

            return result.code;
        } catch(err) {
            logger.error("carpoolRegister: " + err);

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

    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.findInfoByRiderId(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
                }
            );
        }
    }

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

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

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

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

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

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

            if(response.code === NOT_FOUND_CARPOOL) {
                logger.error("start" +  response.message);

                throw new ApolloError(
                    response.message,
                    response.code.toString(), {
                        'code_number': response.code
                    }
                );
            }
            
            if(response.payload.passengers.length < 2) {
                logger.error("start: 승객이 2명 미만입니다! 카풀이 취소됩니다!");

                throw new ApolloError(
                    "승객이 2명 미만입니다! 카풀이 취소됩니다!",
                    "LESS_MIN_PASSENGERS", {
                        'code_number': LESS_MIN_PASSENGERS
                    }
                );
            }

            const updatedResult: ResponseDto = await this.repository.updateStatus(
                args.ride_info_id, 
                BEING,
            );

            if(updatedResult.code === DATABASE_ERROR) {
                logger.error("start :" + updatedResult.message);

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

            if(updatedResult.code === NOT_UPDATED_STATUS) {
                logger.error("start: " + updatedResult.message);

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

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

                if(dto.code === DATABASE_ERROR) {
                    logger.error("start :" + dto.message);

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

                if(dto.code === NOT_FOUND_CARPOOL) {
                    logger.error("start :" + dto.message);

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

                logger.info("start: 카풀 데이터 업데이트!");

                pubsub.publish("UPDATE_CARPOOLS", {
                    updateCarpools: dto.payload
                });

                return updatedResult.code;
            }

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

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

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

    public async arrival(
        args: any,
        context: any
    ) {
        try {
            const user_id: string = (await this.jwtUtils.verify(context.token)).data;

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

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

            const response: ResponseDto = await this.repository.updateStatus(
                args.ride_info_id,
                FINISHED
            );

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

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

            if(response.code === NOT_UPDATED_STATUS) {
                logger.error("arrival: " + response.message);

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

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

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

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

                if(dto.code === NOT_FOUND_CARPOOL) {
                    logger.error("arrival: " + response.message);

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

                logger.info("arrival: 카풀 데이터 업데이트!");

                pubsub.publish("UPDATE_CARPOOLS", {
                    updateCarpools: dto
                });

                return response.code;
            }

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

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

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

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

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

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

            const dto: ModifyCurrentLocationDto = args.input;
            const response: ResponseDto = await this.repository.updateLocation(
                dto.current_location,
                dto.ride_info_id
            );
            
            if(response.code === DATABASE_ERROR) {
                logger.error("modifyCurrentLocation: " + response.message);

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

            if(response.code === NOT_UPDATED_LOCATION) {
                logger.error("modifyCurrentLocation: " + response.message);

                throw new ApolloError(
                    response.message,
                    response.code.toString(), {
                        'code_number': response.code
                    }
                );
            }
            
            if(response.code === UPDATED_LOCATION) {
                logger.info("modifyCurrentLocation: 위치 업데이트!");

                pubsub.publish("VIEW_CURRENT_LOCATION", {
                    viewCurrentLocation: response.payload.current_location
                });
            }
           
            return FAILURE;
        } catch(err) {
            logger.error("modifyCurrentLocation: " + err);

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

export { RideService };

메서드들을 살펴 보겠습니다.

1) detailCarpool
한 개의 카풀을 보기 위한 api입니다. ride_info_id를 argument에 담아 이를 바탕으로 entity를 fetch합니다.

2) loadCarpool
카풀 내역 페이지에서 사용될 api입니다. 마이페이지에서 카풀을 이용하거나 운전한 유저와 관련된 카풀 내역들을 fetch합니다.

3) loadCarpoolsByDriver
운전자의 카풀 데이터입니다. 운전자의 상세페이지를 클릭하게 되면 운전자가 카풀을 한 내역들을 볼 수 있게끔 데이터를 fetch합니다.

4) getCarpools
전체 카풀 데이터를 불러올 api입니다. 홈, 맵 화면에서 카풀 데이터를 띄우기 위해 데이터들을 fetch합니다.

5) carpoolRegsiter
운전자 대상으로 카풀 등록을 위한 api입니다. dto에 input을 담아 entity에 값들을 저장 후 mutation을 합니다.

6) reserve
카풀 예약 api입니다. 승객들은 원하는 카풀 데이터를 보고, 예약 버튼을 누른 후 금액을 지불하여 예약을 진행합니다. 현재는 임시로 cost에 바로 데이터를 수정하고 있지만 payment-service 추후에 작성하여 이 부분의 코드를 변경하도록 하겠습니다.

7) cancel
카풀 예약 취소 api입니다. 승객이 카풀을 취소 버튼을 누르면 지불한 금액만큼 다시 환불받고, passengers에서 해당 유저 아이디를 없앱니다.

8) start
운행 출발 api입니다. 우선 ride_info_id를 이용하여 카풀 데이터를 찾습니다. 그리고 이 데이터의 예약 탑승자 숫자를 확인한 후 2명 아래라면 카풀을 취소시킵니다. 2명 이상일 경우 예정대로 status값을 BEING으로 수정합니다.

9) arrival
운행 도착 api입니다. 운행 종료 후 운전자는 운행 종료 버튼을 클릭하여 해당 api를 실행합니다.

10) modifyCurrentLocation
주행중인 차량의 위치를 업데이트해주는 api입니다. 이 api를 통해 현 위치를 업데이트시켜주고 then메서드를 이용하여 앞서 작성한 redis client를 호출하여 메시지를 발행하도록 하였습니다. 즉, react native에서는 useEffect를 이용하여 지속적으로 운전자의 차량이 움직일 때마다 변화하는 위치를 mutation으로 현 위치를 변경해주고 탑승객은 subscription으로 데이터를 실시간으로 받아오도록 합니다.

  • ./src/resolvers/resolvers.js
import { IInfo } from '../interface/info.interface';
import { RideService } from '../service/ride.service';

const service = new RideService();

const resolvers = {
    Query: {
        loadCarpool: async (
            _,
            args,
            context,
            info
        ): Promise<Array<IInfo>> => {
            return await service.loadCarpool(context);
        },
        loadCarpoolsByDriver: async (
            _,
            args,
            context,
            info
        ): Promise<Array<IInfo>> => {
            return await service.loadCarpoolsByDriver(args, context);
        },
        detailCarpool: async (
            _,
            args,
            context,
            info
        ): Promise<IInfo> => {
            return await service.detailCarpool(args, context);
        },
        getCarpools: async (
            _,
            args,
            context,
            info
        ): Promise<Array<IInfo>> => {
            return await service.getCarpools(context);
        }
    },
    Mutation: {
        carpoolRegister: async (
            _,
            args,
            context,
            info
        ): Promise<number> => {
            return await service.carpoolRegister(args, context);
        },
        start: async (
            _,
            args,
            context,
            info 
        ): Promise<number> => {
            return await service.start(args, context);
        },
        arrival: async (
            _,
            args,
            context,
            info
        ): Promise<number> => {
            return await service.arrival(args, context);
        },
        modifyCurrentLocation: async (
            _,
            args,
            context,
            info
        ): Promise<number> => {
            return await service.modifyCurrentLocation(args, context);
        }
    },
};

export { resolvers };

query, mutation api를 작성하고, service 모듈을 작성하기 전에 util 디렉토리에 subscription 메시지 발행을 위한 redis client를 작성하도록 하겠습니다, constants를 작성하도록 하겠습니다.

여기까지 ride-service가 완성되었고 다음 포스트에서는 subscription 서버를 생성하여 실시간으로 데이터를 받아오도록 하겠습니다.

0개의 댓글