미래학자
우버 클론 코딩 (nomad coders) #15 본문
#1.81 Creating a ChatRoom
승객이 Ride를 요청하고, 운전자가 Ride를 수락하면 채팅이 가능하게 해야 한다. 여기서 내가 잘못 생각한게 있었다. 채팅방이 생성되면 거기서 여러 승객이 합석했을 때 다 같이 채팅하는 줄 알았는데, 여기서는 운전자와 Ride를 요청한 승객이 1:1 대화만 한다. 그리고 차에 타면 채팅방은 사라지는 것 같다.
니콜라스도 처음에 여럿이 채팅하는 걸로 entity구조를 짠거 같은데 보니까, 이번에 조금 수정해서 1:1 대화 형태로 만들었다. 그렇기 떄문에 아래처럼 entity를 수정해줘야 한다.
src/entities/Chat.ts 기존에 있던
participants
를passenger
와driver
로 구분했고, 별도의 각 id 필드passengerId
,driverId
를 생성했다.import { BaseEntity, Column, CreateDateColumn, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm' import Message from './Message'; import User from './User'; @Entity() class Chat extends BaseEntity { @PrimaryGeneratedColumn() id: number; @OneToMany(type => Message, message => message.chat) messages: Message[] @ManyToOne(type => User, user => user.chatsAsPassenger) passenger: User; @Column({nullable: true}) passengerId: number; @ManyToOne(type => User, user => user.chatsAsDriver) driver: User; @Column({nullable: true}) driverId: number; @CreateDateColumn() createAt: string; @UpdateDateColumn() updateAt: string; } export default Chat;
src/entities/User.ts
chat
필드를chatsAsPassenger
와chatsAsDriver
로 구분했다.... @Column({ type: "text"}) profilePhoto: string; @OneToMany(type => Chat, chat => chat.passenger) chatsAsPassenger: Chat[]; @OneToMany(type => Chat, chat => chat.driver) chatsAsDriver: Chat[]; @OneToMany(type => Message, message => message.user) messages: Message[]; ...
변경된 필드에 따라 graphql 타입도 변경하자.
src/api/Chat/shared/Chat.graphql
type Chat { id: Int! messages: [Message]! passenger: User! passengerId: Int! driver: User! driverId: Int! createAt: String! updateAt: String }
src/api/User/shared/User.graphql
chat
필드가chatsAsPassenger
,chatsAsDriver
로 나뉘었다.... fullName: String chatsAsPassenger: [Chat] chatsAsDriver: [Chat] messages: [Message] ...
여기서 궁금한게 왜 Chat의 리스트형태를 갖는지다. 이렇게 하려면 동시에 여러 요청을 수락해야 하는건데,, 동시에 여러 차를 탈것도 아니고,,
src/api/Ride/UpdateRideStatus/UpdateRideStatus.resolvers.ts 에서 채팅을 생성시키자.
import { UpdateRideStatusMutationArgs, UpdateRideStatusResponse } from "src/types/graph"; import { Resolvers } from "src/types/resolvers"; import Chat from "../../../entities/Chat"; import Ride from "../../../entities/Ride"; ... if(args.status === "ACCEPTED") { ride = await Ride.findOne( { id: args.rideId, status: "REQUESTING" }, { relations: ["passenger"]} ); if(ride) { ride.driver = user; user.isTaken = true; user.save(); await Chat.create({ driver: user, passenger: ride.passenger }).save(); } ...
운전자가 승인을 하면 채팅방이 생성되는데 이때, 운전자가
driver
로,ride.passenger
가chat.passenger
가 되어야 하기 때문에 relations 옵션으로passenger
정보를ride
에 포함시키게 했다.
#1.82 GetChat Resolver
GetChat Query를 작성하자. 그냥 하던데로 작성하면 된다.
src/api/Chat/GetChat.grpahql
type GetChatResponse { ok: Boolean! error: String chat: Chat } type Query { GetChat(chatId: Int!): GetChatResponse! }
src/api/Chat/GetChat.resolvers.ts
import { GetChatQueryArgs, GetChatResponse } from "src/types/graph"; import { Resolvers } from "src/types/resolvers"; import Chat from "../../../entities/Chat"; import User from "../../../entities/User"; import privateResolver from "../../../utils/privateResolver"; const resolvers: Resolvers = { Query : { GetChat: privateResolver(async( _, args: GetChatQueryArgs, { req } ) :Promise<GetChatResponse> => { const user: User = req.user; try { const chat = await Chat.findOne({ id: args.chatId }); if(chat) { if(chat.driverId === user.id || chat.passengerId === user.id) { return { ok: true, error: null, chat } } else { return { ok: false, error: "Not Authorized", chat: null } } } else { return { ok: false, error: "Chat Not found", chat: null } } } catch (error) { return { ok: false, error: error.message, chat: null } } }) } } export default resolvers;
위에 했던 것처럼 chatId로 chat 객체를 가져오고, 운전자 또는 승객일 때만 정상적으로 리턴해준다.
#1.83 BugFixing
니콜라스가 진행하면서 빠뜨린게 있어서 기능 수정을 했다. Ride가 생성되면 운전자와 승객이 1:1로 채팅을 할 수 있도록 채팅 객체를 생성해야 한다. 그래서 Ride 와 Chat가 1:1 관계를 가지도록 수정해야 한다.
(내가 위에서 의구심을 제기한.. driver chat과 passenger chat..은 ..?)
타입을 위처럼 조금 수정하자.
src/api/Chat/shared/Chat.graphql
ride
와rideId
를 추가하자.... driverId: Int! ride: Ride! rideId: Int createAt: String! ...
src/api/Ride/shared/Ride.graphql
chat
와chatId
를 추가하자... passengerId: Int chat: Chat chatId: Int distance: String! ...
src/entities/Chat.ts
ride
와rideId
를 추가하자.import { BaseEntity, Column, CreateDateColumn, Entity, ManyToOne, OneToMany, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm' import Message from './Message'; import Ride from './Ride'; import User from './User'; ... @Column({nullable: true}) driverId: number; @OneToOne(type => Ride, ride => ride.chat) ride: Ride; @Column({nullable: true}) rideId: number; @CreateDateColumn() createAt: string; @UpdateDateColumn() updateAt: string; } export default Chat;
src/entities/Ride.ts
chat
와chatId
를 추가하자.import { rideStatus } from 'src/types/types'; import { BaseEntity, Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm' import Chat from './Chat'; import User from './User'; ... @Column({nullable: true}) driverId: number; @OneToOne(type => Chat, chat => chat.ride) @JoinColumn() chat: Chat; @Column({nullable: true}) chatId: number; @CreateDateColumn() createAt: string; @UpdateDateColumn() updateAt: string; } export default Ride;
src/api/Ride/RequestRide/RequestRide.resolvers.ts 아래 두 줄을 조금 수정하자.
... if(!user.isRiding && !user.isDriving) { ... error: "You can't request two rides or request a ride with driving", ...
src/api/Ride/UpdateRideStatus/UpdateRideStatus.resolvers.ts 운전자가 Ride 승인하면 chat를 생성해서 ride 에 넣어줘야 한다.
... const chat = await Chat.create({ driver: user, passenger: ride.passenger }).save(); ride.chat = chat; ride.save(); } ...
#1.84 Testing GetChat Resolver
채팅 객체가 잘 생성되는지 테스트를 해보자.
src/api/Chat/GetChat/GetChat.resolvers.ts chat 데이터를 가져올 떄 관계된 데이터를 가져오게 변경하자.
... const chat = await Chat.findOne( { id: args.chatId }, { relations: ["messages", "passenger", "driver"]} ); ...
테스트에 앞서 데이터를 조금 수정해야 한다. pgAdmin 4 프로그램으로 승객 유저의 isRiding
필드를 false로 두고, 모든 Ride 레코드를 지우자.
다시 승객은 RequestRide 를 요청하고, 운전자는 UpdateRideStatus로 ACCEPTED로 만들자. 그러면 Chat가 생성되었을 것이다.
query {
GetRide(rideId: 10) { #rideId는 얻은 값으로 넣자.
ok
error
ride {
pickUpLat
pickUpLng
status
chatId
}
}
}
chatId를 얻었다면,, 아래를 요청하면 데이터가 잘 나올 것이다.
query {
GetChat(chatId: 1) {
ok
error
chat {
messages {
text
}
passenger {
fullName
}
driver {
fullName
}
}
}
}
테스트를 했다면 아래처럼 다시 바꿔주자. chat에는 messages만 필요하기 때문이다.
src/api/Chat/GetChat/GetChat.resolvers.ts chat 데이터를 가져올 떄 관계된 데이터를 가져오게 변경하자.
... const chat = await Chat.findOne( { id: args.chatId }, { relations: ["messages"]} ); ...
#1.85 SendChatMessage Resolver
채팅방은 만들었고 이제 채팅 메시지를 만들어서 채팅을 해야 한다. 위에서 했던 내용과 크게 다르지 않다.
src/api/Chat/SendChatMessage/SendChatMessage.graphql
type SendChatMessageResponse { ok: Boolean! error: String message: Message } type Mutation { SendChatMessage(chatId: Int!, text: String!): SendChatMessageResponse! }
src/api/Chat/SendChatMessage/SendChatMessage.resolvers.ts
import { SendChatMessageMutationArgs, SendChatMessageResponse } from "src/types/graph"; import { Resolvers } from "src/types/resolvers"; import Chat from "../../../entities/Chat"; import Message from "../../../entities/Message"; import User from "../../../entities/User"; import privateResolver from "../../../utils/privateResolver"; const resolvers: Resolvers = { Mutation : { SendChatMessage: privateResolver(async ( _, args: SendChatMessageMutationArgs, { req } ) : Promise<SendChatMessageResponse> => { const user: User = req.user; try { const chat = await Chat.findOne({ id: args.chatId }); if (chat) { if(chat.driverId === user.id || chat.passengerId === user.id) { const message = await Message.create({ text: args.text, user, chat }).save(); return { ok: true, error: null, message }; } else { return { ok: false, error: "Not Authorized", message: null }; } } else { return { ok: false, error: "Chat not found", message: null }; } } catch (error) { return { ok: false, error: error.message, message: null }; } }) } }; export default resolvers;
여기까지 작성했다면 테스트도 바로 해보자. 먼저 텍스트를 보내고, GetChat 할때도 메시지가 잘 가져와야 된다.
mutation {
SendChatMessage(chatId: 1, text: "i love it") {
ok
error
message {
text
}
}
}
query {
GetChat(chatId: 1) {
ok
error
chat {
messages {
text
}
}
}
}
#1.86 MessageSubscription
메시지를 보내면 실시간으로 구독하여 메시지를 확인할 수 있게 하자. 메시지 type에 chatId 필드를 추가하자.
src/api/Chat/shared/Message.graphql
type Message { id: Int! text: String! chat: Chat! chatId: Int user: User! createAt: String! updateAt: String }
src/entities/Message.ts
... @ManyToOne(type => Chat, chat => chat.messages) chat: Chat; @Column({nullable: true}) chatId: number; @ManyToOne(type => User, user => user.messages) user: User; ...
src/api/Chat/MessageSubscription/MessageSubscription.graphql
type Subscription { MessageSubscription: Message }
src/api/Chat/MessageSubscription/MessageSubscription.resolvers.ts
import { withFilter } from "graphql-yoga"; import Chat from "../../../entities/Chat"; import User from "../../../entities/User"; const resolvers = { Subscription: { MessageSubscription: { subscribe: withFilter( (_, __, { pubSub }) => pubSub.asyncIterator("newChatMessage"), async (payload, _, context ) => { const user: User = context.currentUser; const { MessageSubscription: { chatId } } = payload; try { const chat = await Chat.findOne({id: chatId}); if(chat) { return chat.driverId === user.id || chat.passengerId === user.id; } else { return false; } } catch (error) { return false; } } ) } } } export default resolvers;
아래 처럼 구독한 후 메시지를 보내게 되면 실시간으로 메시지를 확인할 수 있다.
subscription {
MessageSubscription {
user {
fullName
}
text
createAt
}
}
#1.87 Backend Conclusions
typescript, graphql-yoga, postgresql, typedorm을 써서 백엔드 부분을 완성했다. 큰 서비스지만, 코드를 깔끔하고 간결하게 작성하게 된 것 같다.
'nomad corders' 카테고리의 다른 글
우버 클론 코딩 (nomad coders) #17 (0) | 2019.05.20 |
---|---|
우버 클론 코딩 (nomad coders) #16 (0) | 2019.05.09 |
우버 클론 코딩 (nomad coders) #14 (0) | 2019.05.02 |
우버 클론 코딩 (nomad coders) #13 (0) | 2019.05.01 |
우버 클론 코딩 (nomad coders) #12 (0) | 2019.04.29 |