RestChatController
@GetMapping("")
public ModelAndView chat(ModelAndView mv, HttpSession session, Map<String, Object> paramMap) {
paramMap.put("workNo", (int)session.getAttribute("workNo"));
paramMap.put("workMemberEmail", (String)session.getAttribute("login"));
Map<String, Object> map = chatService.getChatListByWorkNo(paramMap);
// ModelAndView 세팅
mv.setViewName("chat/chat");
mv.addObject("chatRoomList", map.get("chatRoomList"));
mv.addObject("workspaceMemberList", map.get("workspaceMemberList"));
log.debug(TeamColor.CSK + "workspaceMemberList: " + map.get("workspaceMemberList"));
return mv;
}
(채팅이 아닌) 다른 파트를 구현할 때엔 동기와 비동기가 섞여 있기도 했고, 일부분만을 ajax로 처리한 탓에 view는 @Controller에서, 기타 json을 리턴받아야 할 경우에만 @RestController의 메소드를 사용했다. 그러나 채팅을 구현할 땐 (우리 조의 템플릿 특성상) 최초로 채팅 메인 페이지를 띄울 때를 제외하면 view의 이동이 발생하지 않는다.
이에 @RestController에서 DB에서 가져온 데이터와 view 이동을 동시에 처리하기 위해 ModelAndView를 리턴하는 메소드를 작성하였다. ModelAndView를 써보는 것은 처음이었는데, 주요 메소드는 다음과 같다.
메소드 | 역할 |
---|---|
setViewName(String view) | 응답할 view이름 설정 |
addObject(String name, Object value) | view에 전달할 값을 설정 |
addAllObject(Map values) | view에 전달할 값을 Map에 name-value로 저장하여 한번에 설정 |
SELECT
cr.chat_room_no chatRoomNo
, work_member_name chatRoomName
, filename
, file_ext fileExt
, (SELECT
chat_msg
FROM
chat
WHERE
chat_room_no = chatRoomNo
AND
chat_msg_no
IN
(
SELECT
MAX(chat_msg_no)
FROM
chat
GROUP BY
chat_room_no
)
) lastMsg
FROM
chat_room cr
INNER JOIN
chat_member cm
ON
cr.chat_room_no = cm.chat_room_no
INNER JOIN
(SELECT
cr.chat_room_no
, work_member_name
, filename
, file_ext
FROM
chat_member cm
INNER JOIN
v_workspace_member v
ON
cm.chat_member_email = v.work_member_email
INNER JOIN
chat_room cr
ON
cr.chat_room_no = cm.chat_room_no
WHERE
v.work_no = #{workNo}
AND
chat_member_email != #{workMemberEmail}) crn
ON
crn.chat_room_no = cr.chat_room_no
WHERE
cm.chat_member_email = #{workMemberEmail}
AND
cr.work_no = #{workNo};
Controller에서 넘겨받는 데이터는 해당 워크스페이스 번호와 본인의 email이다. 이것을 사용해 다음 데이터를 구해야 한다.
- chatRoomNo: 채팅방 번호
- chatRoomName: 1대1 채팅만을 구현하였으므로 간단히 상대방 이름을 채팅방 이름으로 구해온다.
- filename: 상대방 프로필 이미지의 파일명
- file_ext: 상대방 프로필 이미지의 확장자
- lastMsg: 마지막 메시지
기본적으로 chat_room, chat_member를 조인하여 본인이 속한 채팅방 목록의 chatRoomNo를 구한다. 나는 1:1 채팅만을 구현했으므로, 같은 채팅방의 내가 아닌 사람의 정보를 구하는 쿼리를 INNER JOIN하여 채팅방 이름(상대의 이름), 상대방 프로필 이미지를 구한다.
마지막 메시지는 내가 보낸 것일 수도 있고 상대의 것일 수도 있다. 간단히 스칼라 쿼리를 이용해 해당 채팅방에서 chat_msg_no(PK, auto_increment)가 가장 높은 메시지 내용을 구했다.
괜찮은 쿼리인지는 모르겠다. 1:1 채팅방만을 구현했기 때문에 가능한 SQL문이라는 점을 염두에 두어야 한다.
채팅 사이드바를 살펴보자. CHATS 리스트 아래로는 이미 존재하는 채팅방이, CONTACTS 리스트 아래로는 해당 워크스페이스에 속한 멤버 리스트가 보인다. 나는 1) 채팅방 중복 생성을 막고 싶었고, 2) CONTACTS의 멤버 정보를 클릭하면 기존 채팅방이 리턴되도록 하고 싶었다.
ChatService
@Override
@Transactional
public Map<String, Object> addChatRoom(Map<String, Object> paramMap) {
// 기존 채팅방이 존재하는지 확인
Integer chatRoomNo = chatMemberMapper.selectChatRoomNoByWorkMemberEmail(paramMap);
// 기존 채팅방이 존재하면
if(chatRoomNo != null) {
paramMap.put("chatRoomNo", chatRoomNo);
paramMap.put("insert", "");
return paramMap;
}
// 존재하지 않으면 chatRoom과 chatMember를 INSERT
// 코드는 생략
}
이를 위해 작성한 SQL문은 다음과 같다.
// chatMemberMapper.selectChatRoomNoByWorkMemberEmail(paramMap)
SELECT
cm.chat_room_no chatRoomNo
FROM
chat_member cm
INNER JOIN
chat_member cm2
ON
cm.chat_room_no = cm2.chat_room_no
INNER JOIN
chat_room cr
ON
cm.chat_room_no = cr.chat_room_no
WHERE
cm.chat_member_email = #{login}
AND
cm2.chat_member_email = #{workMemberEmail}
AND
cr.work_no = #{workNo};
chat_member 테이블을 두 번 INNER JOIN하여 login 세션에 담긴 email 값(자기 자신의 email)과 클릭한 멤버의 email로 이루어진 채팅방의 번호를 구했다.
이후 javascript를 사용해 이미 존재하는 채팅방을 CHATS 리스트에서 찾아 클릭하는 코드를 추가했다.
// selector뒤에 빈칸을 넣고 selector를 쓰면, 하위를 찾겠다는 말
// chat-list에서 해당 채팅방을 찾은 다음 클릭 trigger
$('#chat-list [value="' + json.chatRoomNo + '"]').trigger('click');
기존 채팅방 정보가 없어서 INSERT한 경우에도 동일하다. 새로 만든 채팅방을 CHATS 리스트 가장 상단에 append한 후 $('#chat-list li').first().trigger('click');
을 통해 클릭을 발생시킨다.
1:1 채팅만을, 그것도 가장 기본적인 기능 이외의 것은 전혀 구현하지 못했다. 채팅방 수정, 삭제 기능과 멀티채팅을 시도하지 못한 것이 아쉽지만 시간 관계 상 웹 메신저 구현은 이쯤에서 마무리짓도록 한다.