CREATE TABLE chatRoom (
roomNo BIGINT PRIMARY KEY AUTO_INCREMENT, -- 고유 번호
userId VARCHAR(20) NOT NULL, -- member.id 구매희망자
pno INT NOT NULL, -- product.pno
status VARCHAR(50) DEFAULT 'ON', -- ON(진행), OFF(차단)
UNIQUE (userId, pno) -- userId와 pno를 묶어서 UNIQUE 제약 설정
);
CREATE TABLE chatMessage(
chatNo BIGINT PRIMARY KEY AUTO_INCREMENT, -- 채팅 번호
type VARCHAR(20) NOT NULL, -- 채팅 타입: ENTER, TALK, LEAVE, NOTICE
roomNo INT NOT NULL, -- 채팅방 번호
sender VARCHAR(20) NOT NULL, -- 송신자
message VARCHAR(2000) NOT NULL, -- 채팅 메시지
status VARCHAR(50) DEFAULT 'UNREAD', -- 읽음 여부
time TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- 채팅 발송 시간
);
package com.team45.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
public enum MessageType{
ENTER, TALK, LEAVE, NOTICE
}
private int chatNo;
@NotNull
private MessageType type; // 메시지 타입
@NotNull
private int roomNo; // 방 번호
@NotNull
private String sender; // 채팅을 보낸 사람
@NotNull
private String message; // 메시지
private String status = "UNREAD"; // 읽음 여부
private String time; // 채팅 발송 시간
}
package com.team45.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChatRoom {
private int roomNo;
private String userId;
private int pno;
private String status = "ON";
}
chatRoomGetUnique
: 상품과 구매희망자가 같은 방이 생성되지 않도록 미리 확인한다.package com.team45.mapper;
import com.team45.entity.ChatRoom;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface ChatRoomMapper {
@Select("SELECT * FROM chatRoom")
public List<ChatRoom> chatRoomList();
@Select("SELECT * FROM chatRoom WHERE usedNo=#{usedNo} AND status!='BLOCK'")
public List<ChatRoom> chatRoomProductList(int usedNo);
@Select("SELECT * FROM chatRoom where roomNo=#{roomNo}")
public ChatRoom chatRoomGet(int roomNo);
@Select("SELECT * FROM chatRoom WHERE usedNo=#{usedNo} AND userId=#{userId}")
public ChatRoom chatRoomGetId(int usedNo, String userId);
@Select("SELECT COUNT(*) FROM chatRoom WHERE userId=#{userId} AND usedNo=#{usedNo}")
public int chatRoomGetUnique(String userId, int usedNo);
@Insert("INSERT INTO chatRoom(userId, usedNo) VALUES(#{userId}, #{usedNo})")
public void chatRoomInsert(String userId, int usedNo);
@Update("UPDATE chatRoom SET status='BLOCK' WHERE roomNo=#{roomNo}")
public int chatRoomBlockUpdate(int roomNo);
@Delete("DELETE FROM chatroom WHERE roomNo=#{roomNo}")
public int chatRoomDelete(int roomNo);
}
package com.team45.mapper;
import com.team45.entity.ChatMessage;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface ChatMessageMapper {
@Select("SELECT * FROM chatMessage WHERE roomNo=#{roomNo} AND status!='REMOVE'")
public List<ChatMessage> chatMessageList(int roomNo);
@Select("SELECT COUNT(*) FROM chatMessage WHERE roomNo=#{roomNo} AND status='UNREAD'")
public int chatMessageUnread(int roomNo);
@Select("SELECT * FROM chatMessage ORDER BY chatNo DESC LIMIT 1")
public ChatMessage chatMessageGetLast();
@Insert("INSERT INTO chatMessage(type, roomNo, sender, message) VALUES(#{type}, #{roomNo}, #{sender}, #{message})")
public int chatMessageInsert(ChatMessage chatMessage);
@Update("UPDATE chatMessage SET status='READ' WHERE chatNo=#{chatNo} AND sender!=#{sender}")
public int chatMessageReadUpdate(int chatNo, String sender);
@Update("UPDATE chatMessage SET status='READ' WHERE roomNo=#{roomNo} AND sender!=#{sender}")
public int chatMessageReadUpdates(int roomNo, String sender);
@Update("UPDATE chatMessage SET status='REMOVE' WHERE chatNo=#{chatNo}")
public int chatMessageRemoveUpdate(int chatNo);
@Delete("DELETE FROM chatMessage WHERE chatNo=#{chatNo}")
public int chatMessageDelete(int chatNo);
}
package com.team45.service;
import com.team45.entity.ChatMessage;
import com.team45.mapper.ChatMessageMapper;
import com.team45.mapper.ChatRoomMapper;
import com.team45.entity.ChatRoom;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
public class ChatServiceImpl implements ChatService {
@Autowired
ChatRoomMapper roomMapper;
@Autowired
ChatMessageMapper chatMapper;
@Override
public List<ChatRoom> chatRoomProductList(int usedNo) {
return roomMapper.chatRoomProductList(usedNo);
}
@Override
public ChatRoom chatRoomGetNo(int roomNo) {
return roomMapper.chatRoomGet(roomNo);
}
@Override
public ChatRoom chatRoomInsert(String userId, int usedNo) {
if(roomMapper.chatRoomGetUnique(userId, usedNo)>0){
return null;
}
roomMapper.chatRoomInsert(userId, usedNo);
return roomMapper.chatRoomGetId(usedNo, userId);
}
@Override
public int chatRoomBlockUpdate(int roomNo) {
return roomMapper.chatRoomBlockUpdate(roomNo);
}
@Override
public List<ChatMessage> chatMessageList(int roomNo, String sender) {
chatMapper.chatMessageReadUpdates(roomNo, sender);
return chatMapper.chatMessageList(roomNo);
}
@Override
public ChatMessage chatMessageInsert(ChatMessage chatMessage) {
int roomNo = chatMessage.getRoomNo();
ChatRoom room = roomMapper.chatRoomGet(roomNo);
if(room.getStatus().equals("BLOCK")){
return null; // 차단된 경우에는 메시지 전송하지 않음.
}
chatMapper.chatMessageInsert(chatMessage);
return chatMapper.chatMessageGetLast();
}
@Override
public int chatMessageReadUpdate(int chatNo, String sender) {
return chatMapper.chatMessageReadUpdate(chatNo, sender);
}
@Override
public int chatMessageReadUpdates(int roomNo, String sender) {
return chatMapper.chatMessageReadUpdates(roomNo, sender);
}
@Override
public int chatMessageRemoveUpdate(int chatNo) {
return chatMapper.chatMessageRemoveUpdate(chatNo);
}
}
loadHome
: 아직 상품 페이지가 안 만들어져서 임시로 만든 페이지이다. chat.html
의 경우 admin이 채팅한다고 가정한다.loadHome2
: chat2.html
의 경우 kim이 채팅한다고 가정한다.createRoom
: 채팅방을 만드는 함수이다. 추후 상품 페이지와 연결하여, 상품 페이지의 '채팅하기' 버튼을 누르면 이 함수를 호출하여 채팅창을 띄울 예정이다.getRoom
: 채팅방을 만들고 해당 채팅방에 입장했을 때 호출한다. 과거에 채팅을 한 기록을 불러와 채팅방에 띄워준다.readRoom
: 채팅방에 입장하게 되면 과거 채팅에서 내가 읽지 않았던 부분을 한꺼번에 모두 읽음 처리해준다. 또는 카톡을 읽음 표시하기와 같이 읽지 않았던 메시지를 읽음 처리하기 위한 함수이다.
insertChat
: 데이터베이스에 채팅 내용을 추가한다.
readChat
: 개별 채팅을 읽음 처리해준다.
코드
package com.team45.ctrl;
import com.team45.service.ChatService;
import com.team45.entity.ChatMessage;
import com.team45.entity.ChatRoom;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
@Controller
@Slf4j
@RequestMapping("/chat/")
public class ChatRoomCtrl {
private static final ObjectMapper mapper = new ObjectMapper();
@Autowired
private ChatService service;
@GetMapping("chat")
public String loadHome(Model model, HttpServletRequest request){
int usedNo = Integer.parseInt(request.getParameter("usedNo"));
List<ChatRoom> chatRooms = service.chatRoomProductList(usedNo);
model.addAttribute("chatRooms", chatRooms);
return "/chat/chat";
}
@GetMapping("chat2")
public String loadHome2(Model model, HttpServletRequest request){
int usedNo = Integer.parseInt(request.getParameter("usedNo"));
List<ChatRoom> chatRooms = service.chatRoomProductList(usedNo);
model.addAttribute("chatRooms", chatRooms);
return "/chat/chat2";
}
@PostMapping("createRoom")
@ResponseBody
public ChatRoom createRoom(@RequestParam String userId, @RequestParam int usedNo){
return service.chatRoomInsert(userId, usedNo);
}
@PostMapping("getRoom")
@ResponseBody
public List<ChatMessage> getRoom(HttpServletRequest request){
int roomNo = Integer.parseInt(request.getParameter("roomNo"));
String sender = request.getParameter("userId");
service.chatMessageReadUpdates(roomNo, sender);
return service.chatMessageList(roomNo, sender);
}
@PostMapping("blockRoom")
@ResponseBody
public String blockRoom(HttpServletRequest request){
int roomNo = Integer.parseInt(request.getParameter("roomNo"));
int returnNo = service.chatRoomBlockUpdate(roomNo);
if(returnNo>0){
return "Block Successfully";
}
return "Something went wrong";
}
@PostMapping("readRoom")
@ResponseBody
public String readRoom(HttpServletRequest request){
int roomNo = Integer.parseInt(request.getParameter("roomNo"));
String sender = request.getParameter("userId");
int returnNo = service.chatMessageReadUpdates(roomNo, sender);
if(returnNo>0){
return "Success";
}
return "Something went wrong";
}
@PostMapping("insertChat")
@ResponseBody
public ChatMessage insertChat(@RequestParam String message) throws JsonProcessingException {
ChatMessage chat = mapper.readValue(message, ChatMessage.class);
return service.chatMessageInsert(chat);
}
@PostMapping("readChat")
@ResponseBody
public String readChat(@RequestParam String message, @RequestParam String user) throws JsonProcessingException {
ChatMessage chat = mapper.readValue(message, ChatMessage.class);
System.out.println(chat);
service.chatMessageReadUpdate(chat.getChatNo(), user);
return "readChat Completed";
}
}
sessionList: 현재 채팅방에 접속한 사용자의 세션 정보가 저장된다.
onOpen
: 소켓이 열렸을 때, 즉 채팅방에 접속했을 때 실행된다. javascript 부분에서 joinRoom
하면서 ws = new WebSocket(블라블라)
을 할 때 실행된다. sessionList에 사용자의 session 정보를 저장한다.
onMesage: 소켓이 열리고 메시지를 보낼 때 실행된다. 자바 스크립트 부분에서 ws.send(jsonstr)
할 때 실행된다. 이때 jsonstr부분이 onMessage의 String message가 되고, 채팅을 보낸 사용자의 세션이 Session session이 된다.
message는 텍스트로 온다. 따라서 chat.html의 javascript에서 JSON.stringfiy(chat)
을 사용했다. 이때 chat은 Chat Entity와 같은 구조로 가진 JSON(객체)이다.
let chat = {
"chatNo": chatNo,
"type": type,
"roomNo": room,
"sender": username,
"message": message,
"time": time
};
`mapper.readValue(message, ChatMessage.class)`: String으로 온 message를 편리하게 해석하기 위해 Objectmapper mapper를 사용하여 ChatMessage 형태로 바꿔준다.
session.getRequestParameterMap
: 세션 정보의 쿼리로 보낸 사용자 정의 파라미터를 받기 위한 함수이다. new WebSocket("ws://"+*location*.host+"/socket?roomNo="+roomNo+"&userId="+*username*);
chat.html의 javascript 부분 중 해당 부분을 보면 쿼리 스트링을 써서 roomNo와 userId를 보내고 있다. 그렇게 보낸 데이터가 바로 여기 session.getRequestParametMap
에 한번에 저장된다. 이 함수는 Map<String, List<String>>
으로 반환된다. 즉 쿼리 스트링의 키값인 roomNo가 String에 들어가고, roomNo의 값, 예컨대 3은 List에 들어간다.
onError: 에러가 났을 때. 뜨지 않는게 좋다. 잘 활용하면 할 수 있겠지만…
onClose: 소켓이 닫힐 때 실행되면서, sessionList에서 해당 사용자의 session 정보를 삭제한다.
sendAllSessionToMessage: 모든 세션의 사용자에게 메시지를 보낸다. 공지 아닌 이상에야 쓸일 없다.
sendRoomMessage: 같은 룸에 있는 사람에게 메시지를 보낸다. 즉, 메세지를 보낸 나와 메시지를 받는 상대 모두에게 메시지가 보내진다.
session.getBasicRemote().sendText(msg)
: 해당 세션에 msg를 보낸다. 어차피 msg는 String이여야 하므로 onMessage에서 받았던 String message를 그대로 입력한다.코드
package com.team45.ctrl;
import com.team45.service.ChatService;
import com.team45.entity.ChatMessage;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Component
@ServerEndpoint(value = "/socket")
public class ChatCtrl {
private static final ObjectMapper mapper = new ObjectMapper();
@Autowired
private ChatService service;
private static final List<Session> sessionList = new ArrayList<Session>();
@OnOpen // socket 연결 시
public void onOpen(Session session) {
sessionList.add(session);
}
@OnMessage
public void onMessage (String message, Session session) throws IOException {
// 다른 사람에게 메세지 보내기
Map<String, List<String>> requestParameter = session.getRequestParameterMap();
int roomNo = Integer.parseInt(requestParameter.get("roomNo").get(0));
ChatMessage chat = mapper.readValue(message, ChatMessage.class);
System.out.println(chat);
sendRoomMessage(message, roomNo, chat);
/*ChatMessage chatReturn = service.chatMessageInsert(chat);
if(chatReturn!=null) {
sendRoomMessage(message, roomNo, chatReturn);
} else {
chat.setType(ChatMessage.MessageType.NOTICE);
chat.setSender("admin");
chat.setMessage("대화 상대에게 차단되어 메시지를 보낼 수 없어요.");
sendRoomMessage(message, roomNo, chatReturn);
}*/
}
@OnError
public void onError(Throwable e, Session session) {
System.out.println(e.getMessage() + "by session : " + session.getId());
}
@OnClose
public void onClose(Session session) throws IOException {
sessionList.remove(session);
}
private void sendAllSessionToMessage(String msg){ // 연결된 모든 사용자에게 메세지 전달
try {
for(Session s : ChatCtrl.sessionList){
s.getBasicRemote().sendText(msg);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void sendRoomMessage(String msg, int roomNo, ChatMessage chat){
try {
for(Session s : ChatCtrl.sessionList){
Map<String, List<String>> requetParameter = s.getRequestParameterMap();
int sroomNo = Integer.parseInt(requetParameter.get("roomNo").get(0));
if(sroomNo == roomNo){
s.getBasicRemote().sendText(msg);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
<div class="container">
<div class="row">
<div class="col-6">
<h4>채팅방 목록</h4>
<ul class="list-group" id="roomList" style="margin: 10px 0;" th:each="room : ${chatRooms}">
<li class="list-group-item"><a th:onclick="joinRoom(this.getAttribute('roomno'))" th:roomno="${room.roomNo}" th:text="${room.userId}"></a></li>
</ul>
<div class="input-group mb-3">
<button class="btn btn-outline-secondary" onclick="createRoom()">Create Chat Room</button>
</div>
</div>
<div id="chatRoom" class="col-6" style="visibility: hidden;">
<section>
<div class="container py-5">
<div class="row d-flex justify-content-center">
<div class="col">
<div class="card" style="border-radius: 15px;">
<div class="card-header d-flex justify-content-center align-items-center p-3 bg-info text-white border-bottom-0" style="border-top-left-radius: 15px; border-top-right-radius: 15px;">
<p class="mb-0 fw-bold" id="chatRoomName">Live chat</p>
<input type="hidden" id="chat-roomNo">
</div>
<div class="card-body">
<div id="msgArea" style="height: 500px; overflow-y: auto;">
</div>
<textarea class="form-control" id="chat-input" placeholder="Type Your message and Enter" aria-label="chat input" rows="3"></textarea>
<div class="d-flex justify-content-end mt-2"><button class="btn btn-primary" id="chat-send">Send</button></div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
</div>