강의 : 메타코딩
1.환경설정
2)[MongoDB] 몽고디비 GUI 개발/관리도구 Studio 3T 설치 (Robo 3T)
2.몽고DB 장점
3.비동기 Netty 서버
4.@Tailable
Chat.java
import java.time.LocalDateTime; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; import lombok.Data; //STS 툴에 lombok 설정하는 법(인터넷) @Data @Document(collection="chat") public class Chat { @Id private String id; private String msg; private String sender;//보내는 사람 private String receiver;//받는 사람 private LocalDateTime createdAt; }
ChatRepository.java
package com.cos.chatapp; import org.springframework.data.mongodb.repository.Query; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.data.mongodb.repository.Tailable; import reactor.core.publisher.Flux; public interface ChatRepository extends ReactiveMongoRepository{ @Tailable //커서를 안닫고 계속 유지한다. @Query("{sender:?0,receiver:?1}") Flux mFindBySender(String sender, String receiver);//Flux(흐름) response를 유지하면서 데이터를 계속 흘러보내기 }
몽고DB
db.chat.save({ sender:'ssar', receiver:'cos', msg:'안녕' }); db.chat.save({ sender:'ssar2', receiver:'cos2', msg:'안녕' }); db.chat.find().pretty(); db.chat.find({sender:'ssar',receiver:'cos'});
5. SSE프로토콜과 TextEventStream
//@Tailable 사용하기 위해서 Collection 사이즈를 높여 줘야 한다.
//db.createCollection("컬렉션이름",{capped: true, size: 16384, max:100})
//최대 크기가 16384바이트고, 최대 100도큐먼트까지 저장되는 컬렉션
db.runCommand({convertToCapped:'chat', size:8192});
ChatController.java
package com.cos.chatapp; import java.time.LocalDateTime; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import lombok.RequiredArgsConstructor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; @RequiredArgsConstructor @RestController//데이터 리턴 서버 public class ChatController { private final ChatRepository chatRepository; //Flux 지속적으로 데이터를 계속 흘러보내기 //SSE프로토콜과 TextEventStream 데이터가 생길때마다 지속적으로 보냄 @GetMapping(value="/sender/{sender}/receiver/{receiver}", produces=MediaType.TEXT_EVENT_STREAM_VALUE) public Flux getMsg(@PathVariable String sender, @PathVariable String receiver) { return chatRepository.mFindBySender(sender, receiver) .subscribeOn(Schedulers.boundedElastic()); } //Mono 한번만 실행 @PostMapping("/chat") public Mono setMsg(@RequestBody Chat chat){ chat.setCreatedAt(LocalDateTime.now()); return chatRepository.save(chat); } }
postman 으로 테스트
6. 채팅화면 디자인하기
7. 채팅화면 자바스크립트 이벤트 적용
chat.js
function getMsg(msg) { let today = new Date(); let year = today.getFullYear(); // 년도 let month = (("00" + (today.getMonth() + 1)).slice(-2)); // 월 let date = (("00" + (today.getDate() + 1)).slice(-2)); // 날짜 let day = today.getDay(); // 요일 let hours = (("00" + today.getHours()).slice(-2)); // 시 let minutes = (("00" + today.getMinutes()).slice(-2)); // 분 let seconds = (("00" + today.getSeconds()).slice(-2)); // 초 let milliseconds = today.getMilliseconds(); // 밀리초 console.log(year + '-' + month + '-' + date); console.log(hours + ':' + minutes + ':' + seconds + ':' + milliseconds); let dayDiffer = getDateDiff("2022-08-15", year + '-' + month + '-' + date); if (dayDiffer === 0) { dayDiffer = "오늘"; } else { dayDiffer = dayDiffer + "일전"; } //엔터키를 br 태그로 전환 msg = msg.replace(/(?:\r\n|\r|\n)/g, ' '); return `
${msg}
${hours}:${minutes} | ${dayDiffer}`; } const getDateDiff = (d1, d2) => { const date1 = new Date(d1); const date2 = new Date(d2); const diffDate = date1.getTime() - date2.getTime(); return Math.abs(diffDate / (1000 * 60 * 60 * 24)); // 밀리세컨 * 초 * 분 * 시 = 일 } document.querySelector("#chat-outgoing-button").addEventListener("click", () => { //alert("클릭"); let chatBox = document.querySelector("#chat-box"); let chatOutgoinBox = document.createElement("div"); chatOutgoinBox.className = "outgoing_msg"; chatOutgoinBox.innerHTML = getMsg(document.querySelector("#chat-outgoing-msg").value); chatBox.append(chatOutgoinBox); document.querySelector("#chat-outgoing-msg").value = ""; }); document.querySelector("#chat-outgoing-msg").addEventListener("keyup", (e) => { console.log(e.target.value); if (e.keyCode === 13) { console.log("enter key"); } });
8. EventSource 객체 사용하기
ChatController
크로스 도메인 허용 @CrossOrigin
import java.time.LocalDateTime; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import lombok.RequiredArgsConstructor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; @RequiredArgsConstructor @RestController // 데이터 리턴 서버 public class ChatController { private final ChatRepository chatRepository; // Flux 지속적으로 데이터를 계속 흘러보내기 // SSE프로토콜과 TextEventStream 데이터가 생길때마다 지속적으로 보냄 @CrossOrigin @GetMapping(value = "/sender/{sender}/receiver/{receiver}", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<Chat> getMsg(@PathVariable String sender, @PathVariable String receiver) { return chatRepository.mFindBySender(sender, receiver) .subscribeOn(Schedulers.boundedElastic()); } // Mono 한번만 실행 @CrossOrigin @PostMapping("/chat") public Mono<Chat> setMsg(@RequestBody Chat chat) { chat.setCreatedAt(LocalDateTime.now()); return chatRepository.save(chat); } }
chat.js
const eventSource = new EventSource("http://localhost:8080/sender/ssar/receiver/cos"); eventSource.onmessage = (event) => { console.log(1, event); const data = JSON.parse(event.data); console.log(2, data); }
9. 기존 채팅 내역 출력하기
chat.js
const eventSource = new EventSource("http://localhost:8080/sender/ssar/receiver/cos"); eventSource.onmessage = (event) => { // console.log(1, event); const data = JSON.parse(event.data); // console.log(2, data); //서버에서 데이터를 가져온 후 데이터 파싱 함수호출 initMessage(data.msg, data); } function getSendMsgBox(msg, data) { let today = new Date(); let year = today.getFullYear(); // 년도 let month = (("00" + (today.getMonth() + 1)).slice(-2)); // 월 let date = (("00" + (today.getDate() + 1)).slice(-2)); // 날짜 let day = today.getDay(); // 요일 let hours = (("00" + today.getHours()).slice(-2)); // 시 let minutes = (("00" + today.getMinutes()).slice(-2)); // 분 let seconds = (("00" + today.getSeconds()).slice(-2)); // 초 let milliseconds = today.getMilliseconds(); // 밀리초 // console.log(year + '-' + month + '-' + date); // console.log(hours + ':' + minutes + ':' + seconds + ':' + milliseconds); //console.log(data); let todayDate = year + '-' + month + '-' + date; //오늘 날짜 if (data != undefined) { //데이터를 서버에서 가져올때 console.log(data.createdAt); if (data.createdAt != undefined && data.createdAt != null && data.createdAt != "") { let dbDate = new Date(data.createdAt); year = dbDate.getFullYear(); // 년도 month = (("00" + (dbDate.getMonth() + 1)).slice(-2)); // 월 date = (("00" + (dbDate.getDate() + 1)).slice(-2)); // 날짜 minutes = (("00" + dbDate.getMinutes()).slice(-2)); // 분 seconds = (("00" + dbDate.getSeconds()).slice(-2)); // 초 } } else { //엔터키를 br 태그로 전환 msg = msg.replace(/(?:\r\n|\r|\n)/g, '<br />'); } let dayDiffer = getDateDiff(todayDate, year + '-' + month + '-' + date); if (dayDiffer === 0) dayDiffer = "오늘"; else dayDiffer = dayDiffer + "일전"; return ` <div class="sent_msg"> <p>${msg}</p> <span class="time_date"> ${hours}:${minutes} | ${dayDiffer}</span> </div> `; } const getDateDiff = (d1, d2) => { const date1 = new Date(d1); const date2 = new Date(d2); const diffDate = date1.getTime() - date2.getTime(); return Math.abs(diffDate / (1000 * 60 * 60 * 24)); // 밀리세컨 * 초 * 분 * 시 = 일 } function initMessage(historyMsg, data) { let chatBox = document.querySelector("#chat-box"); let chatOutgoinBox = document.createElement("div"); chatOutgoinBox.className = "outgoing_msg"; chatOutgoinBox.innerHTML = getSendMsgBox(historyMsg, data); chatBox.append(chatOutgoinBox); } function addMessage() { let chatBox = document.querySelector("#chat-box"); let chatOutgoinBox = document.createElement("div"); let msgInput = document.querySelector("#chat-outgoing-msg"); chatOutgoinBox.className = "outgoing_msg"; chatOutgoinBox.innerHTML = getSendMsgBox(msgInput.value); chatBox.append(chatOutgoinBox); msgInput.value = ""; } document.querySelector("#chat-outgoing-button").addEventListener("click", () => { //alert("클릭"); addMessage(); }); document.querySelector("#chat-outgoing-msg").addEventListener("keyup", (e) => { // console.log(e.target.value); if (e.keyCode === 13) { // console.log("enter key"); } });
10. 채팅 메시지 전송 및 저장
chat.js
async function addMessage() { let chatBox = document.querySelector("#chat-box"); let chatOutgoinBox = document.createElement("div"); let msgInput = document.querySelector("#chat-outgoing-msg"); //엔터시 <br/> 변경 msg = msgInput.value.replace(/(?:\r\n|\r|\n)/g, '<br />'); let chat = { sender: "ssar", receiver: "cos", msg: msg } await fetch("http://localhost:8080/chat", { method: "POST", headers: { "Content-type": "application/json; charset=utf-8" }, body: JSON.stringify(chat) }) .then((res) => res.json()) .then(data => { console.log("data ", data); }) .catch(error => { console.log("에러 :", error); }) // chatOutgoinBox.className = "outgoing_msg"; // chatOutgoinBox.innerHTML = getSendMsgBox(msgInput.value); // chatBox.append(chatOutgoinBox); msgInput.value = ""; }
11. HTTP, SSE, WebSocket 비교
12. 채팅 유튜브 강의 완료
Chat.java
package com.cos.chatapp; import java.time.LocalDateTime; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; import lombok.Data; //STS 툴에 lombok 설정하는 법(인터넷) @Data @Document(collection="chat") public class Chat { @Id private String id; private String msg; private String sender;//보내는 사람 private String receiver;//받는 사람 private Integer roomNum;//방 번호 private LocalDateTime createdAt; }
ChatRepository.java
package com.cos.chatapp; import org.springframework.data.mongodb.repository.Query; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.data.mongodb.repository.Tailable; import reactor.core.publisher.Flux; public interface ChatRepository extends ReactiveMongoRepository<Chat, String>{ @Tailable //커서를 안닫고 계속 유지한다. @Query("{sender:?0,receiver:?1}") Flux<Chat> mFindBySender(String sender, String receiver);//Flux(흐름) response를 유지하면서 데이터를 계속 흘러보내기 @Tailable @Query("{roomNum:?0}") Flux<Chat> mFindByRoomNum(Integer roomNum); }
ChatController.java
package com.cos.chatapp; import java.time.LocalDateTime; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import lombok.RequiredArgsConstructor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; @RequiredArgsConstructor @RestController // 데이터 리턴 서버 public class ChatController { private final ChatRepository chatRepository; //귓속말시 사용 // Flux 지속적으로 데이터를 계속 흘러보내기 // SSE프로토콜과 TextEventStream 데이터가 생길때마다 지속적으로 보냄 @CrossOrigin @GetMapping(value = "/sender/{sender}/receiver/{receiver}", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<Chat> getMsg(@PathVariable String sender, @PathVariable String receiver) { return chatRepository.mFindBySender(sender, receiver) .subscribeOn(Schedulers.boundedElastic()); } @CrossOrigin @GetMapping(value = "/chat/roomNum/{roomNum}", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<Chat> findByRoomNum(@PathVariable Integer roomNum) { return chatRepository.mFindByRoomNum(roomNum) .subscribeOn(Schedulers.boundedElastic()); } // Mono 한번만 실행 @CrossOrigin @PostMapping("/chat") public Mono<Chat> setMsg(@RequestBody Chat chat) { chat.setCreatedAt(LocalDateTime.now()); return chatRepository.save(chat); } }
chat.html
<!DOCTYPE html> <html> <head> <title>What's App</title> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css"> <link rel="stylesheet" type="text/css" href="./css/style.css"> </head> <body> <div class="container-fluid"> <div class="row"> <div class="col-sm-12"> <div id="user_chat_data" class="user_chat_data"> <div class="profile_name"> <h1 id="title"></h1> <img src="./img/profile.png" class="mr-3 rounded-circle"> <span class="username">Sankar Mahadevan</span> </div> <div class="container-fluid chat_section" id="chat-box"> </div> <div class="type_msg"> <div class="input_msg_write"> <!-- <input id="chat-outgoing-msg" type="text" class="write_msg" placeholder="Type a message" /> --> <textarea id="chat-outgoing-msg" type="text" class="write_msg" placeholder="메시지를 입력해 주세요." ></textarea> <button id="chat-outgoing-button" class="msg_send_btn" type="button"><i class="fa fa-paper-plane" aria-hidden="true"></i></button> </div> </div> </div> </div> </div> </div> </div> </div> <script src="js/chat.js"></script> <!-- jQuery first, then Popper.js, then Bootstrap JS --> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script> </body> </html>
chat.js
//로그인 시스템 대신 임시 방편 let username = prompt("아이디를 입력하세요."); let roomNum = prompt("채팅방 번호를 입력하세요."); //SSE 연결하기 //const eventSource = new EventSource("http://localhost:8080/sender/ssar/receiver/cos"); const eventSource = new EventSource(`http://localhost:8080/chat/roomNum/${roomNum}`); eventSource.onmessage = (event) => { // console.log(1, event); const data = JSON.parse(event.data); document.querySelector("#title").innerHTML = roomNum + "번 방"; document.querySelector(".username").innerText = username; if (data.sender === username) { //로그인한 유저가 보낸 메시지 //파란박스 (오른쪽) initMyMessage(data); } else { //회색밗(왼쪽) initYourMessage(data); } //서버에서 데이터를 가져온 후 데이터 파싱 함수호출 // initMyMessage(data); } //파란박스 만들기 function getSendMsgBox(data) { //날짜 가공 let mDate = manufactureDate(data); return ` <div class="sent_msg"> <p>${data.msg}</p> <span class="time_date"> ${mDate.day} ${mDate.hours}:${mDate.minutes} | ${mDate.dayDiffer} / ${data.sender} </span> </div> `; } //회색박스 만들기 function getReceiveMsgBox(data) { //날짜 가공 let mDate = manufactureDate(data); return ` <div class="received_withd_msg"> <p>${data.msg}</p> <span class="time_date"> ${mDate.day} ${mDate.hours}:${mDate.minutes} | ${mDate.dayDiffer} / ${data.sender}</span> </div> `; } //최초 초기화뙬 때 1번방 3건이 있으면 3건을 다 가져옴 //addMessage() 함수 호출시 DB에 insert 되고, 그 데이터가 자동으로 흘러들어감(SSE) //파란박스 초기화 하기 function initMyMessage(data) { let chatBox = document.querySelector("#chat-box"); let sendBox = document.createElement("div"); sendBox.className = "outgoing_msg"; sendBox.innerHTML = getSendMsgBox(data); chatBox.append(sendBox); document.documentElement.scrollTop = document.body.scrollHeight; } //회색박스 초기화 하기 function initYourMessage(data) { let chatBox = document.querySelector("#chat-box"); let receivedBox = document.createElement("div"); receivedBox.className = "received_msg"; receivedBox.innerHTML = getReceiveMsgBox(data); chatBox.append(receivedBox); document.documentElement.scrollTop = document.body.scrollHeight; } //DB 에 등록시 자동으로 Flux 처리되어 채팅 목록으로 표시됨 async function addMessage() { let msgInput = document.querySelector("#chat-outgoing-msg"); //엔터시 <br/> 변경 msg = msgInput.value.replace(/(?:\r\n|\r|\n)/g, '<br />'); let chat = { sender: username, roomNum: roomNum, msg: msg } await fetch("http://localhost:8080/chat", { method: "POST", headers: { "Content-type": "application/json; charset=utf-8" }, body: JSON.stringify(chat) }) .then((res) => res.json()) .then(data => { //console.log("data ", data); }) .catch(error => { console.log("에러 :", error); }) // chatOutgoinBox.className = "outgoing_msg"; // chatOutgoinBox.innerHTML = getSendMsgBox(msgInput.value); // chatBox.append(chatOutgoinBox); msgInput.value = ""; } //날짜 가공 함수 function manufactureDate(data) { let today = new Date(); let year = today.getFullYear(); // 년도 let month = (("00" + (today.getMonth() + 1)).slice(-2)); // 월 let date = (("00" + (today.getDate() + 1)).slice(-2)); // 날짜 let day = today.getDay(); // 요일 let hours = (("00" + today.getHours()).slice(-2)); // 시 let minutes = (("00" + today.getMinutes()).slice(-2)); // 분 let seconds = (("00" + today.getSeconds()).slice(-2)); // 초 let milliseconds = today.getMilliseconds(); // 밀리초 let todayDate = year + '-' + month + '-' + date; //오늘 날짜 //console.log("data.createdAt : ", data.createdAt); let dbDate = new Date(data.createdAt); year = dbDate.getFullYear(); // 년도 month = (("00" + (dbDate.getMonth() + 1)).slice(-2)); // 월 date = (("00" + (dbDate.getDate() + 1)).slice(-2)); // 날짜 hours = (("00" + dbDate.getHours()).slice(-2)); // 분 minutes = (("00" + dbDate.getMinutes()).slice(-2)); // 분 seconds = (("00" + dbDate.getSeconds()).slice(-2)); // 초 let dayDiffer = getDateDiff(todayDate, year + '-' + month + '-' + date); if (dayDiffer === 0) dayDiffer = "오늘"; else dayDiffer = dayDiffer + "일전"; const mDate = { "day": year + '-' + month + '-' + date, "hours": hours, "minutes": minutes, "dayDiffer": dayDiffer } return mDate; } const getDateDiff = (d1, d2) => { const date1 = new Date(d1); const date2 = new Date(d2); const diffDate = date1.getTime() - date2.getTime(); return Math.abs(diffDate / (1000 * 60 * 60 * 24)); // 밀리세컨 * 초 * 분 * 시 = 일 } document.querySelector("#chat-outgoing-button").addEventListener("click", () => { //alert("클릭"); addMessage(); }); document.querySelector("#chat-outgoing-msg").addEventListener("keyup", (e) => { // console.log(e.target.value); if (e.keyCode === 13) { // console.log("enter key"); addMessage(); } });
1. Spring R2DBC CRUD 예제Spring R2DBC CRUD 예제
댓글 ( 4)
댓글 남기기