728x90
이전 시간에 채팅 구현하기 앞서 어떻게 세팅해야할지 어떤 식으로 돌아가는지 콘솔을 통해 알아보았다.
채팅을 구현하기 전 필요한 내용을 모두 세팅을 하고 진행하는데 보기가 좋을 것 같아서
이번시간은 DB및 화면에 대해서 먼저 구현해보자
🔮 구현 화면 미리보기
체팅 기능을 실행하기 위한 DB 테이블을 만들어준다.
CREATE TABLE "CHATTING_ROOM" (
"CHATTING_NO" NUMBER NOT NULL,
"CH_CREATE_DATE" DATE DEFAULT SYSDATE NOT NULL,
"OPEN_MEMBER" NUMBER NOT NULL,
"PARTICIPANT" NUMBER NOT NULL
);
COMMENT ON COLUMN "CHATTING_ROOM"."CHATTING_NO" IS '채팅방 번호';
COMMENT ON COLUMN "CHATTING_ROOM"."CH_CREATE_DATE" IS '채팅방 생성일';
COMMENT ON COLUMN "CHATTING_ROOM"."OPEN_MEMBER" IS '개설자 회원 번호';
COMMENT ON COLUMN "CHATTING_ROOM"."PARTICIPANT" IS '참여자 회원 번호';
ALTER TABLE "CHATTING_ROOM" ADD CONSTRAINT "PK_CHATTING_ROOM" PRIMARY KEY (
"CHATTING_NO"
);
ALTER TABLE "CHATTING_ROOM"
ADD CONSTRAINT "FK_OPEN_MEMBER"
FOREIGN KEY ("OPEN_MEMBER") REFERENCES "MEMBER";
ALTER TABLE "CHATTING_ROOM"
ADD CONSTRAINT "FK_PARTICIPANT"
FOREIGN KEY ("PARTICIPANT") REFERENCES "MEMBER";
DROP TABLE "MESSAGE";
CREATE TABLE "MESSAGE" (
"MESSAGE_NO" NUMBER NOT NULL,
"MESSAGE_CONTENT" VARCHAR2(4000) NOT NULL,
"READ_FL" CHAR DEFAULT 'N' NOT NULL,
"SEND_TIME" DATE DEFAULT SYSDATE NOT NULL,
"SENDER_NO" NUMBER NOT NULL,
"CHATTING_NO" NUMBER NOT NULL
);
COMMENT ON COLUMN "MESSAGE"."MESSAGE_NO" IS '메세지 번호';
COMMENT ON COLUMN "MESSAGE"."MESSAGE_CONTENT" IS '메세지 내용';
COMMENT ON COLUMN "MESSAGE"."READ_FL" IS '읽음 여부';
COMMENT ON COLUMN "MESSAGE"."SEND_TIME" IS '메세지 보낸 시간';
COMMENT ON COLUMN "MESSAGE"."SENDER_NO" IS '보낸 회원의 번호';
COMMENT ON COLUMN "MESSAGE"."CHATTING_NO" IS '채팅방 번호';
ALTER TABLE "MESSAGE" ADD CONSTRAINT "PK_MESSAGE" PRIMARY KEY (
"MESSAGE_NO"
);
ALTER TABLE "MESSAGE"
ADD CONSTRAINT "FK_CHATTING_NO"
FOREIGN KEY ("CHATTING_NO") REFERENCES "CHATTING_ROOM";
ALTER TABLE "MESSAGE"
ADD CONSTRAINT "FK_SENDER_NO"
FOREIGN KEY ("SENDER_NO") REFERENCES "MEMBER";
-- 시퀀스 생성
CREATE SEQUENCE SEQ_ROOM_NO NOCACHE;
CREATE SEQUENCE SEQ_MESSAGE_NO NOCACHE;
📚 Spring
1. mapper 생성
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0/
/EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="chattingMapper">
</mapper>
2. mybatis-config 별칭생성(필요한부분만 발췌)
<typeAlias type="edu.kh.project.chatting.model.dto.ChattingRoom" alias="ChattingRoom"/>
<mapper resource="/mappers/chatting-mapper.xml" />
3. 필요한 controller / dao / dto / servcie 클래스 생성
📗 Message
package edu.kh.project.chatting.model.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class Message {
private int messageNo;
private String messageContent;
private String readFlag;
private int senderNo;
private int targetNo;
private int chattingNo;
private String sendTime;
}
📗 ChattingRoom
package edu.kh.project.chatting.model.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class ChattingRoom {
private int chattingNo;
private String lastMessage;
private String sendTime;
private int targetNo;
private String targetNickName;
private String targetProfile;
private int notReadCount;
}
4. servlet-context.xml 에 설정
<!-- 웹소켓 처리 클래스를 bean으로 등록 -->
<beans:bean id="chatHandler" class="edu.kh.project.chatting.model.websocket.ChattingWebsocketHandler"/>
<!-- 어떤 주소로 웹소켓 요청이 오면 세션을 가로챌지 지정 -->
<websocket:handlers>
<!-- websocket 매핑 주소 -->
<websocket:mapping handler="chatHandler" path="/chattingSock"/>
<!-- 요청 클라이언트의 세션을 가로채서 WebSocketSession에 넣어주는 역할 -->
<websocket:handshake-interceptors>
<!-- interceptor : http통신에서 request, response를 가로채는 역할
handshake-interceptors :Httpsession에 있는 값을 가로채서 WebSocketSession 넣어주는 역할 -->
<beans:bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
</websocket:handshake-interceptors>
<websocket:sockjs/>
</websocket:handlers>
5. TextWebSocketHandler 작성
public class ChattingWebsocketHandler extends TextWebSocketHandler{
private Logger logger = LoggerFactory.getLogger(ChattingWebsocketHandler.class);
@Autowired
private ChattingService service;
// WebSocketSession : 클라이언트 - 서버간 전이중통신을 담당하는 객체 (JDBC Connection과 유사)
// 클라이언트의 최초 웹소켓 요청 시 생성
private Set<WebSocketSession> sessions = Collections.synchronizedSet(new HashSet<WebSocketSession>());
// synchronizedSet : 동기화된 Set 반환(HashSet은 기본적으로 비동기)
// -> 멀티스레드 환경에서 하나의 컬렉션에 여러 스레드가 접근하여 의도치 않은 문제가 발생되지 않게 하기 위해
// 동기화를 진행하여 스레드가 여러 순서대로 한 컬렉션에 순서대로 접근할 수 있게 변경.
// afterConnectionEstablished - 클라이언트와 연결이 완료되고, 통신할 준비가 되면 실행.
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 연결 요청이 접수되면 해당 클라이언트와 통신을 담당하는 WebSocketSession 객체가 전달되어져 옴.
// 이를 필드에 선언해준sessions에 저장
sessions.add(session);
//logger.info("{}연결됨", session.getId());
// System.out.println(session.getId() + "연결됨");
}
//handlerTextMessage - 클라이언트로부터 텍스트 메세지를 받았을때 실행
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 전달받은 내용은 JSON 형태의 String
logger.info("전달받은 내용 : " + message.getPayload());
// Jackson에서 제공하는 객체
// JSON String -> VO Object
ObjectMapper objectMapper = new ObjectMapper();
Message msg = objectMapper.readValue( message.getPayload(), Message.class);
// Message 객체 확인
System.out.println(msg);
// DB 삽입 서비스 호출
int result = service.insertMessage(msg);
if(result > 0 ) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd hh:mm");
msg.setSendTime(sdf.format(new Date()) );
// 전역변수로 선언된 sessions에는 접속중인 모든 회원의 세션 정보가 담겨 있음
for(WebSocketSession s : sessions) {
// WebSocketSession은 HttpSession의 속성을 가로채서 똑같이 가지고 있기 때문에
// 회원 정보를 나타내는 loginMember도 가지고 있음.
// 로그인된 회원 정보 중 회원 번호 얻어오기
int loginMemberNo = ((Member)s.getAttributes().get("loginMember")).getMemberNo();
logger.debug("loginMemberNo : " + loginMemberNo);
// 로그인 상태인 회원 중 targetNo가 일티하는 회원에게 메세지 전달
if(loginMemberNo == msg.getTargetNo() || loginMemberNo == msg.getSenderNo()) {
s.sendMessage(new TextMessage(new Gson().toJson(msg)));
}
}
}
}
// afterConnectionClosed - 클라이언트와 연결이 종료되면 실행된다.
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
sessions.remove(session);
//logger.info("{}연결끊김",session.getId());
}
}
💻 화면 구현
📚 VS Code
📕 header.jsp (필요한 부분말 발췌)
<%-- 로그인 했을때 채팅 보여짐 --%>
<c:if test="${!empty loginMember}" >
<li><a href = "/chatting">채팅</a></li>
</c:if>
📕 chatting.jsp (웹소켓 구현을 귀한 라이브러리 추가 x 파일)
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang=ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>채팅방</title>
<link rel="stylesheet" href="/resources/css/main-style.css">
<link rel="stylesheet" href="/resources/css/board/boardDetail-style.css">
<link rel="stylesheet" href="/resources/css/chatting/chatting-style.css">
<script src="https://kit.fontawesome.com/a2e8ca0ae3.js" crossorigin="anonymous"></script>
</head>
<body>
<main>
<jsp:include page="../common/header.jsp"></jsp:include>
<button id="addTarget">추가</button>
<div id="addTargetPopupLayer" class="popup-layer-close">
<span id="closeBtn">×</span>
<div class="target-input-area">
<input type="search" id="targetInput"
placeholder="닉네임 또는 이메일을 입력하세요" autocomplete="off">
</div>
<ul id="resultArea">
<%-- <li class="result-row" data-id="1">
<img class="result-row-img" src="/resources/images/user.png">
<span> <mark>유저</mark>일</span>
</li>
<li class="result-row" data-id="2">
<img class="result-row-img" src="/resources/images/user.png">
<span><mark>유저</mark>이</span>
</li>
<li class="result-row">일치하는 회원이 없습니다</li> --%>
</ul>
</div>
<div class="chatting-area">
<ul class="chatting-list">
<c:forEach var="room" items="${roomList}">
<li class="chatting-item" chat-no="${room.chattingNo}" target-no="${room.targetNo}">
<div class="item-header">
<c:if test="${not empty room.targetProfile}">
<img class="list-profile" src="${room.targetProfile}">
</c:if>
<c:if test="${empty room.targetProfile}">
<img class="list-profile" src="/resources/images/user.png">
</c:if>
</div>
<div class="item-body">
<p>
<span class="target-name">${room.targetNickName}</span>
<span class="recent-send-time">${room.sendTime}</span>
</p>
<div>
<p class="recent-message">${room.lastMessage}</p>
<c:if test="${room.notReadCount > 0}">
<p class="not-read-count">${room.notReadCount}</p>
</c:if>
</div>
</div>
</li>
</c:forEach>
</ul>
<div class="chatting-content">
<ul class="display-chatting">
<%-- <li class="my-chat">
<span class="chatDate">14:01</span>
<p class="chat">가나다라마바사</p>
</li>
<li class="target-chat">
<img src="/resources/images/user.png">
<div>
<b>이번유저</b> <br>
<p class="chat">
안녕하세요?? 반갑습니다.<br>
ㅎㅎㅎㅎㅎ
</p>
<span class="chatDate">14:05</span>
</div>
</li> --%>
</ul>
<div class="input-area">
<textarea id="inputChatting" rows="3"></textarea>
<button id="send">보내기</button>
</div>
</div>
</div>
</main>
<jsp:include page="../common/footer.jsp"></jsp:include>
</body>
</html>
📕 chatting.jsp (웹소켓 구현을 귀한 라이브러리 추가 o)
<!-------------- sockjs를 이용한 WebSocket 구현을 위해 라이브러리 추가 -------------------->
<!-- https://github.com/sockjs/sockjs-client -->
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script>
// 로그인한 회원 번호
const loginMemberNo = "${loginMember.memberNo}";
</script>
<script src="/resources/js/chatting//chatting.js"></script>
📕 chatting-style.css
/* 채팅방 생성 버튼 */
#openChatRoom {
width: 120px;
}
/* 채팅방 생성 모달*/
#chat-modal {
justify-content: center;
align-items: center;
}
.modal-body {
text-align: center;
background-color: white;
padding: 50px;
border-radius: 20px;
}
#chatRoomTitle {
width: 250px;
}
/* 추가 버튼 */
#addTarget{
margin: 30px 0 0 70px;
width: 100px;
height: 30px;
background-color: #455BA7;
border: none;
border-radius: 10px;
font-weight: bold;
color: white;
cursor: pointer;
}
/* 팝업레이어 */
#addTargetPopupLayer{
position: fixed;
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 1200;
background-color: #f5f5f5;
border: 5px inset #455ba8;
width: 450px;
height: 600px;
}
.popup-layer-close{ display: none; }
#closeBtn{
position: absolute;
top: -15px;
right: -30px;
font-size: 30px;
cursor: pointer;
}
.target-input-area{
width: 100%;
height: 40px;
border-bottom: 2px solid black;
}
#targetInput{
width: 100%;
height: 100%;
outline: none;
border: none;
padding: 3px 10px;
font-size: 20px;
}
#resultArea{list-style: none;}
.result-row{
width: 100%;
height: 50px;
padding: 5px;
cursor: pointer;
display: flex;
align-items: center;
}
.result-row:hover{ background-color: #dadada;}
.result-row > *{
margin-right: 10px;
user-select: none;
}
.result-row-img{ width: 40px;}
/* 채팅창 영역 */
.chatting-area {
margin: auto;
height: 650px;
width: 1000px;
margin-top: 20px;
margin-bottom: 50px;
display: flex;;
}
/* 채팅 목록 */
.chatting-list{
width: 30%;
border : 1px solid black;
list-style: none;
overflow: auto;
}
.chatting-item{
height: 12%;
display: flex;
padding: 5px 0;
border-bottom: 1px solid black;
cursor: pointer;
}
.chatting-item * {
pointer-events: none;
}
.chatting-item > div{ height: 100%; text-align: center;}
.item-header{ width : 25%; }
.item-body{
width : 75%;
padding : 2px 0;
}
.item-body> p{
display: flex;
justify-content: space-between;
}
.chatting-item.select{
background-color: #ddd;
}
.list-profile{
max-width: 65px;
max-height: 65px;
}
.target-name{
font-size: 1.2em;
font-weight: bold;
}
.recent-send-time{
margin-right: 5px;
}
.item-body > div{
display: flex;
justify-content: space-between;
}
.recent-message{
width: 180px;
/* 여러 줄 말줄임 처리 */
white-space: normal;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
/* 줄바꿈 처리 */
word-break: break-all;
}
.not-read-count{
width: 25px;
height: 25px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
background-color: red;
color: white;
font-size: 12px;
margin: 10px 10px 0 0;
}
/* 채팅 내용 */
.chatting-content{
width: 70%;
}
.display-chatting {
width: 100%;
height: 570px;
border: 1px solid black;
overflow: auto;
list-style: none;
padding: 10px 10px;
}
.display-chatting > li{
margin: 10px 0;
}
.target-chat{
display: flex;
align-items: flex-start;
}
.target-chat > img{
width: 50px;
margin-right: 5px;
}
.chat {
display: inline-block;
border-radius: 5px;
padding: 5px;
background-color: #eee;
text-align: left;
max-width: 500px;
word-break: break-all;
white-space: pre-wrap;
}
.input-area {
height: 80px;
width: 100%;
display: flex;
}
#inputChatting {
padding: 3px;
font-size: 1.3em;
width: 80%;
resize: none;
}
#send {
width: 20%;
}
.my-chat {
text-align: right;
}
.my-chat>p {
background-color: yellow;
}
.chatDate {
font-size: 9px;
}
#exit-area {
text-align: right;
margin-bottom: 10px;
}
.exit {
text-align: center;
}
.exit>p {
background-color: rgba(0, 0, 0, 0.3);
}
.chat-exit {
width: 100%;
text-align: center;
background-color: black;
color: white;
}
728x90
'ON > spring' 카테고리의 다른 글
[Spring Boot] 설치하기 (0) | 2023.10.04 |
---|---|
[ Spring ] 채팅 구현하기 - 실전편 ③ (0) | 2023.09.04 |
[ Spring ] 채팅 구현하기 - 이론편 ① (0) | 2023.09.04 |
[ Spring ] SpringAOP 사용하기 ⑳ (0) | 2023.09.02 |
[ Spring ] SpringAOP 이론 ⑱ (0) | 2023.09.01 |