Direct Messages(1)
🍔 핵심 내용
🥑 DM 기능을 만들어 보자.
일단 nodejs는 실시간 채팅기능에 최적화된 언어는 아니다. 채팅 전용 서비스에 쓰이는 언어가 따로 있지만, 간단히 DM 정도 서비스로는 nodejs로 커버 가능해 보이긴 한다.
큰 개념은, room 모델에 message 모델을 엮는 식으로 하면 된다. (인스타그램에서는 메시지를 보내면 자동으로 방이 생성되는 개념)
먼저 모델을 만들어 보자.
🍔 코드 리뷰
🥑 schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
firstName String
lastName String?
userName String @unique
email String @unique
password String
bio String?
avatar String?
photos Photo[]
likes Like[]
followers User[] @relation("FollowRelation", references: [id])
followings User[] @relation("FollowRelation", references: [id])
comments Comment[]
rooms Room[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
message Message[]
}
model Photo {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id])
userId Int
file String
caption String?
hashtags Hashtag[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
likes Like[]
comments Comment[]
}
model Hashtag {
id Int @id @default(autoincrement())
hashtag String @unique
photos Photo[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Like {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id])
photo Photo @relation(fields: [photoId], references: [id])
userId Int
photoId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, photoId])
}
model Comment {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id])
photo Photo @relation(fields: [photoId], references: [id])
payload String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId Int
photoId Int
}
model Room {
id Int @id @default(autoincrement())
users User[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
message Message[]
}
model Message {
id Int @id @default(autoincrement())
payload String
user User @relation(fields: [userId], references: [id])
userId Int
room Room @relation(fields: [roomId], references: [id])
roomId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Room모델에서 User 배열을 만들면 자동으로 User모델에서는 위와 같이 생성이 된다. 하지만 User <-> Room은 다대다 관계 이기 떄문에 아래와 같이 서로 배열관계가 되어야 한다. 이를 매뉴얼하게 수정해주자.
🍔 핵심 내용
🥑 seeRooms 만들기 (모든 채팅방)
만드는 로직은 앞에 내용들과 비슷하다.
🍔 코드 리뷰
🥑 messages.typeDefs.js
import { gql } from "apollo-server-core";
export default gql`
type Message {
id: Int!
payload: String!
user: User!
room: Room!
createdAt: String!
updatedAt: String!
}
type Room {
id: Int!
user: [User]
messages: [Message]
createdAt: String!
updatedAt: String!
}
`
message와 room의 타입 정의
🥑 seeRooms.typeDefs.js
import { gql } from "apollo-server-core";
export default gql`
type Query {
seeRooms : [Room]
}
`
🥑 seeRooms.resolvers.js
import client from "../../client";
import { protectedResolver } from "../../users/users.utils";
export default {
Query: {
seeRooms: protectedResolver(async (_, __, { loggedInUser }) =>
client.room.findMany({
where: {
users: {
some: loggedInUser.id,
},
},
})
),
},
};
room 모델에서 로그인한 user의 room을 모두 찾는 로직
🍔 핵심 내용
🥑 sendMessage 기능 만들기.
특정 유저에게 메시지를 보낼 때, 채팅 room이 없으면 만들어 주고, 있으면 해당 room에 나와, 특정 유저를 묶어주는 로직을 만들어 보자. (사실 이 파트 뭔가 나중에 에러가 생길듯,,? 일단 해보자)
🍔 코드 리뷰
🥑 sendMessage.typeDefs.js
import { gql } from "apollo-server-core";
export default gql`
type Mutation {
sendMessage(payload: String!, roomId: Int, userId: Int): MutationResponse!
}
`;
roomId와 userId는 필수값으로 하지 않았다. roomId는 처음 대화하는 경우 roomId가 없을 수 있기 때문. (userId는 해야 할 것 같은데,,?)
🥑 sendMessage.resolvers.js
import client from "../../client";
import { protectedResolver } from "../../users/users.utils";
export default {
Mutation: {
sendMessage: protectedResolver(
async (_, { payload, roomId, userId }, { loggedInUser }) => {
let room = null;
if (userId) {
const user = await client.user.findUnique({
where: {
id: userId,
},
select: {
id: true,
},
});
if (!user) {
return {
ok: false,
error: "This user does not exist.",
};
}
room = await client.room.create({
data: {
users: {
connect: [
{
id: userId,
},
{
id: loggedInUser.id,
},
],
},
},
});
} else if (roomId) {
room = await client.room.findUnique({
where: {
id: roomId,
},
select: {
id: true,
},
});
if (!room) {
return {
ok: false,
message: "Room not found",
};
}
}
const Message = client.message.create({
data: {
payload,
room: {
connect: {
id: room.id,
},
},
user: {
connect: {
id: loggedInUser.id,
},
},
},
});
return {
ok: true,
};
}
),
},
};
make a room OR find a room logic. and finally you can make a message and connecting room & loggedInUser.id in the message model
🍔 핵심 내용
🥑 seeRoom 기능 만들기 (단일 채팅방)
단일 채팅방을 만들어보자.
단일 채팅방에서 필요한 값은, 여러 user 값과 message값 그리고 메시지 읽음 확인 정도다. 모델 room의 필드값에는 [user]와 [message]값이 있기 때문에 이를 computed 해주면 된다. (아 이래서 computed fields구나, 뭔가 필드 결과값에 내가 임의로 조정하려고 할 떄)
🍔 코드 리뷰
🥑 seeRoom.typeDefs.js
import { gql } from "apollo-server-core";
export default gql`
type Query {
seeRoom(id: Int!): Room
}
`;
🥑 seeRoom.resolvers.js
import client from "../../client";
import { protectedResolver } from "../../users/users.utils";
export default {
Query: {
seeRoom: protectedResolver(async (_, { id }, { loggedInUser }) =>
client.room.findFirst({
where: {
id,
users: {
some: {
id: loggedInUser.id,
},
},
},
// include:{
// users:true,
// message:true
// }
})
),
},
};
중간 include를 넣어서 uesr 배열값과 message 배열값들을 확인 할 수 있지만, 이렇게 하면 message 폴더 다른 파일에도 복붙해야 한다. (멋있지 않다.) 따라서 아래와 같이 resolver에서 computed해주자.
🥑 messages.resolvers.js
import client from "../client";
export default {
Room: {
users: ({ id }) =>
client.room
.findUnique({
where: {
id,
},
})
.users(),
messages: ({ id }) =>
client.message.findMany({
where: {
roomId: id,
},
}),
},
};
이제, Room의 결과 필드 값 users와 messages값을 확인 할 수 있다. 추가로 메시지 읽음 확인 기능을 다음 내용에 다뤄보자.
🍔 핵심 내용
🥑 읽음 확인 기능을 만들어 보자.
큰 개념을 먼저 설명 하자면, 로그인한 유저가 특정 room에 message값 중에 read가 false로 되어있으면 해당 메시지는 읽지 않음으로 count되고 내가 만약 그것을 확인하게 되면 read가 true로 되는 개념이다. 말로 설명 하자니 더 헷갈리긴 한다. 일단 코드를 보자.
🍔 코드 리뷰
🥑 schema.prisma
model Room {
id Int @id @default(autoincrement())
users User[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
Messages Message[]
}
model Message {
id Int @id @default(autoincrement())
payload String
user User @relation(fields: [userId], references: [id])
userId Int
room Room @relation(fields: [roomId], references: [id])
roomId Int
read Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
message모델에 read를 추가 하였다. (디폴트값 false)
🥑 messages.typeDefs.js
import { gql } from "apollo-server-core";
export default gql`
type Message {
id: Int!
payload: String!
user: User!
room: Room!
read: Boolean!
createdAt: String!
updatedAt: String!
}
type Room {
id: Int!
unreadTotal: Int!
users: [User]
messages: [Message]
createdAt: String!
updatedAt: String!
}
`;
Message 타입에 read를 추가하였고, Room 타입에 unreadTotal(안읽은게 총 몇개 인지 확인하는) 를 추가 하였다. 먼저
computed된 unreadTotal resolver를 확인해보자.
🥑 messages.resolvers.js
import client from "../client";
export default {
Room: {
users: ({ id }) =>
client.room
.findUnique({
where: {
id,
},
})
.users(),
messages: ({ id }) =>
client.message.findMany({
where: {
roomId: id,
},
}),
unreadTotal: ({ id }, _, { loggedInUser }) => {
if (!loggedInUser) {
return 0;
}
return client.message.count({
where: {
read: false,
roomId: id,
user: {
id: {
not: loggedInUser.id,
},
},
},
});
},
},
};
parent id값인 Room id 값을 받고 message 모델값에서 해당 roomId값으로 조회한다. 읽지 않은 수량을 파악해야 하기 때문에, read는 false로 세팅하고, user는 내가 포함되지 않은 것으로 한다. (message의 id값인 본인이면 내가 보낸 메시지이기 때문에 unread가 아니다.)
다음으로는, 내가 읽은 메시지는 read -> true로 바꿔주는 걸 살펴보자.
🥑 readMessage.typeDefs.js
import { gql } from "apollo-server-core";
export default gql`
type Mutation {
readMessage(id: Int!): MutationResponse!
}
`;
특정 값(read값을 true로 업데이트)을 업데이트 해주어야 하기 때문에 고유 값을 넣을 수 있는 id를 인자로 넣는다.
🥑 readMessage.resolvers.js
import client from "../../client";
import { protectedResolver } from "../../users/users.utils";
export default {
Mutation: {
readMessage: protectedResolver(async (_, { id }, { loggedInUser }) => {
const message = await client.message.findFirst({
where: {
id,
userId: {
not: loggedInUser.id,
},
room: {
users: {
some: {
id: loggedInUser.id,
},
},
},
},
select: {
id: true,
},
});
if (!message) {
return {
ok: false,
error: "Message not found.",
};
}
await client.message.update({
where: {
id,
},
data: {
read: true,
},
});
return {
ok: true,
};
}),
},
};
항상 그러했듯이, 해당 값 여부를 먼저 확인 해보자.
존재 여부 확인 로직 (findFirst로 조회)
① messege id 값을 인자로 받고, messege 모델 값에서 해당 id를 찾는다.
② messege의 userId중에 로그인한 유저가 아닌 값을 찾는다. 그 이유는, 로그인한 유저가 unread된 것을 찾는 중이기 때문이다.
③ messege의 room에서 로그인한 유저가 포함되어 있는 room을 찾는다.
(개념적으로 얘기하자면, 채팅방에 내가 포함 되어 있어야 하고, 해당 메시지는 내 것이면 안된다.)
2번이 충족되고 3번이 충족 되는 과정에서 첫번째 발견되는 값만 담는다. (select로)
위 내용이 충족 되면, 인자로 받은 id 값으로 messege 모델 id 값에 read를 true로 변경해준다.