SQL

ChatRoom.sql

  • 한 상품에 대해 구매 희망자마다 채팅방이 생성되어야 한다. 따라서 상품과 userId가 같은 채팅방이 생성되지 않도록 UNIQUE 제약을 설정했다.
    • chatroom1[kim, book1], chatroom2[lee, book1], chatroom3[kim, pencil] → 가능
    • chatroom4[kim, book1], chatroom5[lee, book1] → 불가능
  • status는 당근과 같이 채팅방 차단 기능을 위한 것으로 선택사항이다.
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 제약 설정
);

ChatMessage.sql

  • 메시지를 보낸 사람을 구분해, 내가 보낸 메시지는 왼쪽에, 남이 보낸 메시지는 오른쪽에 띄울 수 있으므로, sender가 필요하다.
  • status의 경우 카톡처럼 읽음 여부를 표현하기 위한 것으로 선택사항이다.
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    -- 채팅 발송 시간
);

Entity

ChatMessage.java

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;                // 채팅 발송 시간
}

ChatRoom.java

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";
}

Mapper

ChatRoomMapper

  • 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);
}

ChatMessageMapper

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);
}

Service

ChatService

  • 굳이 ChatRoom과 ChatMessage를 따로 할 필요 없을 것 같아서 한 번에 작성했다.
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);
    }
}

Controller

ChatRoomCtrl

  • loadHome: 아직 상품 페이지가 안 만들어져서 임시로 만든 페이지이다. chat.html의 경우 admin이 채팅한다고 가정한다.
  • loadHome2: chat2.html의 경우 kim이 채팅한다고 가정한다.
  • createRoom: 채팅방을 만드는 함수이다. 추후 상품 페이지와 연결하여, 상품 페이지의 '채팅하기' 버튼을 누르면 이 함수를 호출하여 채팅창을 띄울 예정이다.
  • getRoom: 채팅방을 만들고 해당 채팅방에 입장했을 때 호출한다. 과거에 채팅을 한 기록을 불러와 채팅방에 띄워준다.
  • blockRoom: 아직 실험 기능이다. 채팅방을 차단하는 함수이다.
💡 아래 함수들은 ChatRoom보다는 ChatMessage와 관련있는 기능이지만 그냥 한번에 묶어서 처리했다.
  • 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";
        }
    }

ChatCtrl

  • 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();
            }
    
        }
    }

HTML

채팅 예시 화면

<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>

Javascript

profile
상민

0개의 댓글