내일이 최종발표일이라 오늘이 제일 바빴다. 깃헙의 이슈 및 그에 따른 커밋 수를 보면... 음... 이슈를 만들때에 몇 가지를 묶어 올리거나 생성한걸 고려하면, 하나하나 땄으면 양이 더 많았을 것이다.......🤣 작업을 다 마친 후에 폭풍커밋했는데, 물론 중간중간 진행상황은 공유해가면서 했다! 메세지는 나 혼자서 하니까 conflict 날 일이 없어서 한번에 푸시했다.
어제에 이어 오늘은 채팅방과 채팅메세지에 대한 API를 설계했다. API뿐만 아니라 여기에서 쓰인 각종 메소드들도 직접 만들었는데, 아무래도 글이 너무 길어질듯 하니 일부만 소개하도록 하겠다.
router.get('/list', async (req, res) => {
const { user_id } = req.query;
let chatroomdata = await chatrooms.getChatList(user_id);
if (chatroomdata.length === 0) {
res.send('false');
return;
}
let rooms = chatroomdata.map(p => JSON.parse(p.connection_ids));
const conns = [];
rooms = rooms.map(p => p.filter(it => conns.push(it)));
const frienddata = await connections.getChatroomDatas(user_id, rooms);
chatroomdata = chatroomdata.map(p => [
p.id,
p.connection_ids,
p.user_ids,
p.last_msg_time,
p.last_msg_text,
]);
res.send({ chatroom: chatroomdata, data: frienddata });
});
우선 유저가 속한 모든 채팅방을 찾는다.
이때, 만약 속한 채팅방이 하나도 없으면 바로 false를 내주고 리턴시켰다.
이후엔 찾은 채팅방을 기준으로, 이번에는 하나의 대화라도 있었는지를 확인한다. 채팅방 개설만 되고 메세지는 나누지 않은 경우엔 굳이 대화목록에 가져와줄 필요가 없으니 제외시킨다. 그리고, 해당 채팅방에 있는 상대의 정보들을 가져와서 함께 반환해주도록 하였다.
// connection.controller.js
Controller.getChatroomDatas = async (user_id, rooms) => {
const result = await db.sequelize.query(
`SELECT ca.user_id, ca.nickname, ca.intro, ca.image FROM cards AS ca JOIN connections as conn ON ca.user_id = conn.user_id_2 WHERE conn.user_id_1 = ${user_id} and conn.id in (${rooms})`,
);
return result;
};
유저 A와 유저 B가 같은 채팅방에 속한 상황에서, 둘 중 누가 채팅리스트를 요청하더라도 채팅방 정보는 똑같이, 친구(상대방) 정보는 다르게 가져와야 한다는 점이 다소 까다로웠다 🥲 물론 Data redundancy를 무시하고 짠다면야 쉽지만... cards - connections - chatrooms 라는 3가지 DB에서 동시에 정보를 빼내서 채팅방 하나하나에 대한 정보를 구성하다보니 JOIN 만으론 쉽지 않았다.
// chatroom.service.js
router.get('/enter', async (req, res) => {
const { id_1, id_2 } = req.query;
let chatroom;
const idintlist = [id_1, id_2].map(i => parseInt(i, 10));
function sortId(id1, id2) {
return id1 - id2;
}
idintlist.sort(sortId);
const users = JSON.stringify(idintlist);
chatroom = await chatrooms.findChatRoom(users);
// 채팅방이 이미 있다면
if (chatroom != null) {
res.send(chatroom.id.toString());
}
// 채팅방이 아직 없다면
else {
const connectionlist = await connections.findAllConnectionsIds(users);
const conns = JSON.stringify(connectionlist);
chatroom = await chatrooms.makeChatRoom(conns, users);
res.send(chatroom.id.toString());
}
});
채팅목록을 거치지않고 상대방의 프로필에서 DM버튼을 눌러서 곧장 들어갈 때를 생각해서 만든 api이다. 서로 명함을 교환하고 최초에 대화할 때 1번만 채팅방을 생성(makeChatRoom)해 배정하고, 이후에는 이전에 존재하는 채팅방을 똑같이 내준다. 이렇게 해서 같은 채팅방에 대한 최근 대화, 읽지 않은 메세지 등의 채팅방 정보와 그에 속한 메세지 내역 등을 관리할 수 있도록 해주었다. 또한, 소켓에 연결할 때도 소켓 연결과 동시에 해당 방으로 입장하도록 만들어서 둘만의 Direct Message도 실시간으로 문제없이 가능하도록 하였다.
// chatroom.controller.js
Controller.findChatRoom = async user_ids => {
const room = await Chatroom.findOne({
where: { user_ids },
});
return room ? room.dataValues : null;
};
Controller.makeChatRoom = async (connection_ids, user_ids) => {
const room = await Chatroom.create({ connection_ids, user_ids });
const final_room = room.dataValues;
return final_room;
};
활용한 메소드는 위와 같이 구성했다.
// chatroom.service.js
router.get('/message', async (req, res) => {
const { room_id } = req.query;
const messagelist = await chatmessages.getMessagelist(room_id);
if (messagelist) {
res.send(messagelist);
} else {
res.send(null);
}
});
말 그대로, 특정 채팅방에 대한 대화목록을 불러와주도록 하였다.
사용자가 메세지를 보냈을 때, 어떤 작업을 처리해줄 것인지도 물론 만들었다.
//chat.service.js
client.on('message', async data => {
// message를 DB에 등록해준다.
await chatmessages.postMessage(
data.chatroomID,
data.senderID,
data.messagetext,
);
// 채팅방 정보를 업데이트 해준다
chatmessages.updateLastMsg(data.chatroomID, data.messagetext);
// controller에서 상대방의 connection을 찾는다 (상대방-나)
const connids = await connections.findConnectionId(
data.senderID,
data.receiverID,
);
// 상대방-나 connection에 기록되는 읽지 않은 메세지 갯수를 늘려줌
connections.upreadCnt(connids[0]);
// 나-상대방 connection에 기록되는 읽지 않운 메세지 갯수를 초기화
connections.resetreadCnt(connids[1]);
// 같은 채팅방의 상대방에게 emit
client.to(data.chatroomID).emit('message', data);
});
우선 메세지가 도착하면 DB에 등록하고, 채팅방 정보를 업데이트 해준다.
이후엔 상대방의 not read cnt는 1 증가시켜 주고, 내 cnt는 0으로 초기화한다.
// chatmessage.controller.js
Controller.postMessage = async (chatroom_id, sender, messagetext) => {
const result = await Chatmessage.create({
chatroom_id,
sender,
messagetext,
});
return result;
};
Controller.getMessagelist = async chatroom_id => {
const messages = await Chatmessage.findAll({ where: { chatroom_id } });
return messages;
};
Controller.updateLastMsg = async (chatroom_id, messagetext) => {
const result = await db.sequelize.query(
`UPDATE chatrooms SET last_msg_time = CURRENT_TIMESTAMP, last_msg_text = '${messagetext}' WHERE id = ${chatroom_id};`,
);
return result;
};
글을 쓰면서 생각해보니까, 메세지를 보낼 때 나의 read cnt를 0으로 리셋하는 것은 다소 허술하다. 채팅방에 동시에 접속해 있는 상황에서 실시간으로 '읽씹'을 하는 경우를 고려하지 못한 것 같다. 개선해야지... 내가 소켓에 접속해있는 상황에서 상대방이 메세지를 보내서 내 대화창이 재렌더링 되는 경우에 처리할 수 있는 루트를 따로 설정해줘야겠다.
그리고 이 작업한 모든걸 플러터에 연결해주었다.
하나하나 연결하고나서, 에뮬레이터 2개를 띄워놓고 서로 메세지를 보내봤다.
잘 보내진다....! 내일 시연은 문제 없겠다. 이전 대화목록도 잘 불러와지고, 아직 채팅리스트를 실시간 갱신해주진 못하는거 빼고는 상당히 괜찮은거 같다.
원래는 Sequelize에서 어떻게든 열심히 JSON_CONTAINS 등을 쓰면서 용을 썼는데, 그냥 SQL 쿼리 쓰는게 가능하다는걸 뒤늦게 발견해서 웬만한건 SQL 쿼리로 해결했다만 그래도 아까워서 하나 남겨본다. Node.js에서 JSON_CONTAINS 쓰는 법...
Controller.findAllFriendsID = async user_id => {
const friends = await Connection.findAll({
where: Sequelize.where(
Sequelize.fn(
'JSON_CONTAINS',
Sequelize.col('user_ids'),
Sequelize.literal(JSON.stringify(user_id)),
Sequelize.literal(JSON.stringify('$')),
),
1,
),
});
const ids = friends.map(item => item.dataValues.user_id_2);
return ids;
};