@Controller
@RequiredArgsConstructor
@RequestMapping("chatting")
public class ChattingController {
private final ChattingService service;
// /chatting
/** 채팅페이지 전환
* @return
*/
@GetMapping("")
public String chattingPage(
@SessionAttribute("loginMember") Member loginMember,
Model model) {
List<ChattingRoom> roomList
= service.selectRoomList(loginMember.getMemberNo());
model.addAttribute("roomList", roomList);
return "chatting/chatting";
}
/** 채팅 상대 검색
* @param query : 상태 닉네임 또는 이메일
* @param loginMember : 로그인한 회원정보
* @return 검색 결과
*/
@GetMapping("selectTarget")
@ResponseBody
public List<Member> selectTarget(
@RequestParam("query") String query,
@SessionAttribute("loginMember") Member loginMember
){
return service.selectTarget(query, loginMember.getMemberNo());
}
/** 채팅방입장(처음 채팅이면 채팅방생성(INSERT))
* @param targetNo
* @param loginMember
* @return 두 회원이 포함된 채팅방 번호
*/
@ResponseBody
@PostMapping("enter")
public int chattingEnter(
@RequestBody int targetNo,
@SessionAttribute("loginMember") Member loginMember) {
int chattingNo
= service.chattingEnter(targetNo, loginMember.getMemberNo());
return chattingNo;
}
/**
* 로그인한 회원이 참여한 채팅방 목록 조회
* @param loginMember
* @return
*/
@GetMapping("roomList")
@ResponseBody
public List<ChattingRoom> selectRoomList(
@SessionAttribute("loginMember") Member loginMember){
return service.selectRoomList(loginMember.getMemberNo());
}
/** // 특정 채팅방의 메시지 모두 조회하기
* @param chattingNo
* @param loginMember
* @return
*/
@GetMapping("selectMessage")
@ResponseBody
public List<Message> selectMessage(
@RequestParam("chattingNo") int chattingNo,
@SessionAttribute("loginMember") Member loginMember) {
return service.selectMessage(chattingNo, loginMember.getMemberNo());
}
/** 채팅 읽음 표시
* @param chattingNo
* @param loginMember
* @return
*/
@PutMapping("updateReadFlag")
@ResponseBody
public int putMethodName(
@RequestBody int chattingNo,
@SessionAttribute("loginMember") Member loginMember) {
return service.updateReadFlag(chattingNo, loginMember.getMemberNo());
}
@Component // 빈 등록
@Slf4j
public class ChattingWebsocketHandler extends TextWebSocketHandler{
@Autowired
private ChattingService service;
// WebSocketSession :
// - HTTP Session 객체를 가로챈 값을 가지고 있는 객체
// - 클라이언트 - 서버 전이중 통신담당 (중요한 객체!)
// synchronizedSet : 동기화된 Set(충돌 방지, 속도 조금 느림)
private Set<WebSocketSession> sessions
= Collections.synchronizedSet(new HashSet<>());
// 클라이언트 연결이 완료된 후 수행
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 연결된 클라이언트의 session을 Map에 저장
// -> 연결된 클라이언트를 목록화
sessions.add(session);
}
// 클라이언트와의 연결이 종료되었을때
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
sessions.remove(session); // 목록에서 제거
}
// 클라이언트로부터
// SockJS.send() 구문을 이용해 텍스트 메시지가 전달된 경우
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// : message : {"targetNo":"2","messageContent":"ㅁㅁㅁ","chattingRoomNo":"1"}
log.debug("message : {}", message.getPayload());
// ObjectMapper : JSON <-> DTO 변환하는 객체(Jackson 라이브러리 제공)
ObjectMapper objectMapper = new ObjectMapper();
// 전달받은 JSON 메시지를
// Message 클래스 형태로 변환해서 값을 읽어와
// Message 객체에 대입
Message msg
= objectMapper.readValue(message.getPayload(), Message.class);
// 채팅을 보낸 회원의 회원번호 얻어오기
// -> 로그인한 회원번호(session)
// -> WebSocketSession에 담겨있음
HttpSession currentSession
= (HttpSession) session.getAttributes().get("session");
// 채팅 보낸 회원
Member sendMember =
((Member)currentSession.getAttribute("loginMember") );
int senderNo = sendMember.getMemberNo(); // 보낸 회원번호
msg.setSenderNo(senderNo); // Message 객체에 세팅
// -------------- DB INSERT ---------------
// 1) ChattingService 의존성 주입받기(필드)
// 2) INSERT 서비스 호출
// (msg 값 : chattingRoomNo, messageContent, senderNo, targetNo)
int result = service.insertMessage(msg);
if(result == 0) return;
// 채팅이 보내진 시간을 msg에 기록
SimpleDateFormat sdf
= new SimpleDateFormat("yyyy.MM.dd hh:mm");
msg.setSendTime(sdf.format(new Date()));
// -------------- DB INSERT ---------------
// 연결된 모든 클라이언트를 순차접근
for(WebSocketSession wss : sessions) {
// 채팅방에 입장한 사람들(보낸사람, 받는사람) 에게만
// 메시지(msg) 전달
HttpSession clientSession
= (HttpSession)wss.getAttributes().get("session");
// 웹소켓 접속 회원목록에서 꺼낸 회원번호
int clientNo
=((Member)clientSession.getAttribute("loginMember"))
.getMemberNo();
// 메시지를 보낸사람/받는사람 찾기
if(msg.getTargetNo() == clientNo
|| msg.getSenderNo() == clientNo) {
// msg 객체를 JSON으로 변환
TextMessage textMessage
= new TextMessage(objectMapper.writeValueAsString(msg));
wss.sendMessage(textMessage);
}
}
}
}
/*
WebSocketHandler 인터페이스 :
웹소켓을 위한 메소드를 지원하는 인터페이스
-> WebSocketHandler 인터페이스를 상속받은 클래스를 이용해
웹소켓 기능을 구현
WebSocketHandler 주요 메소드
void handlerMessage(WebSocketSession session, WebSocketMessage message)
- 클라이언트로부터 메세지가 도착하면 실행
void afterConnectionEstablished(WebSocketSession session)
- 클라이언트와 연결이 완료되고, 통신할 준비가 되면 실행
void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus)
- 클라이언트와 연결이 종료되면 실행
void handleTransportError(WebSocketSession session, Throwable exception)
- 메세지 전송중 에러가 발생하면 실행
----------------------------------------------------------------------------
TextWebSocketHandler :
WebSocketHandler 인터페이스를 상속받아 구현한
텍스트 메세지 전용 웹소켓 핸들러 클래스
handlerTextMessage(WebSocketSession session, TextMessage message)
- 클라이언트로부터 텍스트 메세지를 받았을때 실행
*/
@Component // 빈 등록
public class SessionHandShakeInterceptor
implements HandshakeInterceptor{
// 핸들러 동작 전 가로채기
@Override
public boolean beforeHandshake(
ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
// ServerHttpRequest : HttpServletRequest의 부모 인터페이스
// ServerHttpResponse : HttpServletResponse의 부모 인터페이스
// attributes : 해당 맵에 세팅된 속성(데이터)은
// 다음에 동작할 Handler 객체에게 전달됨
// (HandshackeInterceptor -> Handler 데이터 전달하는 역할)
// ServletServerHttpRequest 상속관계가 맞을경우
if(request instanceof ServletServerHttpRequest) {
// 다운캐스팅
ServletServerHttpRequest servletRequest
= (ServletServerHttpRequest)request;
// HTTP Session 얻어오기
HttpSession session
= servletRequest.getServletRequest().getSession();
// HTTP session을 가로채서 핸들러에 전달
attributes.put("session", session);
}
return true; // 가로챈 데이터를 핸들러로 전달할지 결정
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
}
<select id="selectTarget" resultType="Member">
SELECT
MEMBER_NO, MEMBER_EMAIL, MEMBER_NICKNAME, PROFILE_IMG
FROM
"MEMBER"
WHERE
(MEMBER_EMAIL LIKE '%' || #{query} || '%'
OR
MEMBER_NICKNAME LIKE '%' || #{query} || '%')
AND MEMBER_DEL_FL = 'N'
AND MEMBER_NO != #{memberNo}
</select>
<!-- 두 회원이 참여한 채팅방이 존재하는지 확인 -->
<select id="checkChattingRoom" resultType="_int">
SELECT NVL(SUM(CHATTING_ROOM_NO),0) CHATTING_NO
FROM CHATTING_ROOM
WHERE (OPEN_MEMBER = #{memberNo} AND PARTICIPANT = #{targetNo})
OR (OPEN_MEMBER = #{targetNo} AND PARTICIPANT = #{memberNo})
</select>
<!-- 채팅방 테이블 삽입 -->
<insert id="createChattingRoom"
parameterType="map"
useGeneratedKeys="true">
<selectKey order="BEFORE" resultType="_int"
keyProperty="chattingNo">
SELECT SEQ_ROOM_NO.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO "CHATTING_ROOM"
VALUES(
#{chattingNo},
DEFAULT,
#{memberNo},
#{targetNo}
)
</insert>
<!-- 채팅방 목록 조회 -->
<select id="selectRoomList" resultType="ChattingRoom">
SELECT CHATTING_ROOM_NO
,(SELECT MESSAGE_CONTENT FROM (
SELECT * FROM MESSAGE M2
WHERE M2.CHATTING_ROOM_NO = R.CHATTING_ROOM_NO
ORDER BY MESSAGE_NO DESC)
WHERE ROWNUM = 1) LAST_MESSAGE
,TO_CHAR(NVL((SELECT MAX(SEND_TIME) SEND_TIME
FROM MESSAGE M
WHERE R.CHATTING_ROOM_NO = M.CHATTING_ROOM_NO), CREATE_DATE),
'YYYY.MM.DD') SEND_TIME
,NVL2((SELECT OPEN_MEMBER FROM CHATTING_ROOM R2
WHERE R2.CHATTING_ROOM_NO = R.CHATTING_ROOM_NO
AND R2.OPEN_MEMBER = #{memberNo}),
R.PARTICIPANT,
R.OPEN_MEMBER
) TARGET_NO
,NVL2((SELECT OPEN_MEMBER FROM CHATTING_ROOM R2
WHERE R2.CHATTING_ROOM_NO = R.CHATTING_ROOM_NO
AND R2.OPEN_MEMBER = #{memberNo}),
(SELECT MEMBER_NICKNAME FROM MEMBER WHERE MEMBER_NO = R.PARTICIPANT),
(SELECT MEMBER_NICKNAME FROM MEMBER WHERE MEMBER_NO = R.OPEN_MEMBER)
) TARGET_NICKNAME
,NVL2((SELECT OPEN_MEMBER FROM CHATTING_ROOM R2
WHERE R2.CHATTING_ROOM_NO = R.CHATTING_ROOM_NO
AND R2.OPEN_MEMBER = #{memberNo}),
(SELECT PROFILE_IMG FROM MEMBER WHERE MEMBER_NO = R.PARTICIPANT),
(SELECT PROFILE_IMG FROM MEMBER WHERE MEMBER_NO = R.OPEN_MEMBER)
) TARGET_PROFILE
,(SELECT COUNT(*) FROM MESSAGE M WHERE M.CHATTING_ROOM_NO = R.CHATTING_ROOM_NO AND READ_FL = 'N' AND SENDER_NO != #{memberNo}) NOT_READ_COUNT
,(SELECT MAX(MESSAGE_NO) SEND_TIME FROM MESSAGE M WHERE R.CHATTING_ROOM_NO = M.CHATTING_ROOM_NO) MAX_MESSAGE_NO
FROM CHATTING_ROOM R
WHERE OPEN_MEMBER = #{memberNo}
OR PARTICIPANT = #{memberNo}
ORDER BY MAX_MESSAGE_NO DESC NULLS LAST
</select>
<!-- 특정 채팅방 메시지 조회 -->
<select id="selectMessage" resultType="Message">
SELECT MESSAGE_NO, MESSAGE_CONTENT, READ_FL, SENDER_NO, CHATTING_ROOM_NO,
TO_CHAR(SEND_TIME, 'YYYY.MM.DD HH24:MI') SEND_TIME
FROM MESSAGE
WHERE CHATTING_ROOM_NO = #{chattingNo}
ORDER BY MESSAGE_NO
</select>
<!-- 특정 채팅방의 글 중 내가 보내지 않은 글을 읽음 처리 -->
<!-- 채팅 메세지 중 내가 보내지 않은 글을 읽음으로 표시 -->
<update id="updateReadFlag">
UPDATE "MESSAGE" SET
READ_FL = 'Y'
WHERE CHATTING_ROOM_NO = #{chattingNo}
AND SENDER_NO != #{memberNo}
</update>
<!-- 메시지 삽입 -->
<insert id="insertMessage">
INSERT INTO "MESSAGE"
VALUES(
SEQ_MESSAGE_NO.NEXTVAL,
#{messageContent},
DEFAULT,
DEFAULT,
#{senderNo},
#{chattingRoomNo}
)
</insert>