2024. 10. 10. 19:14ㆍSpring
내가 맡은 역할 중 실시간 1:1 채팅 기능이 있다.
이미 STOMP 프로토콜을 활용한 채팅 구현을 해본 경험이 있기 때문에,
처음엔 별로 대수롭지 않게 생각했었다.
하지만 그건 나의 엄청난 오만이었다.
실시간으로 채팅을 하는 시스템은 비슷하지만 저번에 했던 채팅은 단순한 단체 채팅방 이고,
이번 프로젝트에 필요한 기능은 관리자와 고객들의 1:n 관계 채팅 시스템이다.
channel 생성 부터 구독 하는 것까지 엄청 골머리를 썩혔지만 결국 해결해냈다.
그럼 내가 이 난관을 어떻게 극복했는지 한 번 적어보겠다.
유저 채팅 채널 생성
<script>
const chat = document.getElementById('chatIcon')
const questions = document.querySelectorAll('.question')
const userid = '${login.userid}'
questions.forEach(e => e.onclick = clickHandler)
function clickHandler(event) {
const arr = Array.from(questions)
const idx = arr.indexOf(event.currentTarget)
const answer = document.querySelectorAll('.answer')
answer[idx].classList.toggle('hidden')
}
chat.onclick = async function() {
const url = '${cpath}/create/' + userid
const result = await fetch(url).then(resp=>resp.text())
}
</script>
일단 관리자가 아닌 일반 사용자의 입장에서 채팅 icon을 클릭하면
해당 유저의 고유키인 "userid"를 이용해 독단적인 채널을 생성하게 했다.
"사용자 마다 각각 다르게 개별적으로 채널을 어떻게 생성해야 하지?" 라는 고민을
했었는데, "그럼 각각 마다 다르게 가지고 있는게 뭐가 있을까" 라고 생각을 하던 중
그럼 고유키인 "userid"를 사용하자 ! 라는 결론이 나오게 되었다.
유저 채팅 기능
// onclick
exit.onclick = exitChat
send.onclick = sendMsg
inputMsg.onkeyup = function(e) {
if(e.key == 'Enter') sendMsg()
if(e.key == 'Escape') e.target.value = ''
}
stomp.connect({}, onConnect)
function onConnect() {
stomp.subscribe('/sendTo/' + userid)
stomp.send('/app/enter/' + userid, {}, JSON.stringify({
writer: username
}))
onInput()
}
async function onInput() {
const ad = '${cpath}/getRoom/' + userid
const adResult = await fetch(ad).then(resp=>resp.json())
const arr = Array.from(adResult)
console.log(arr)
chat_idx = arr[0].idx
console.log(chat_idx)
const url = '${cpath}/getMsg/' + chat_idx
const result = await fetch(url).then(resp=>resp.json())
console.log(result)
const arr2 = Array.from(result)
if(arr2.length != 0) {
arr2.forEach(e => {
let messageDirection = 'message-left'; // 기본적으로 왼쪽 정렬
if(e.writer != 'admin') { // 현재 사용자가 보낸 메시지인 경우 오른쪽 정렬
messageDirection = 'message-right';
}
let str = ''
str += '<div class="' + messageDirection + '">'
str += '<div>' + e.nickname + '</div>'
str += '<div class="content">' + e.content + '</div>'
str += '</div>'
chatArea.innerHTML += str
})
}
chatArea.scrollTop = chatArea.scrollHeight;
stomp.subscribe('/sendTo/' + userid, onReceive)
stomp.send('app/sendTo/admin/' + userid, {}, JSON.stringify({
writer: username
}))
}
async function sendMsg() {
const text = document.querySelector('input[name="msg"]').value
if(text == '') {
return
}
document.querySelector('input[name="msg"]').value = ''
console.log(chat_idx)
const url = '${cpath}/sendMsg'
const opt = {
method: 'POST',
body: JSON.stringify({
chat_idx: chat_idx,
writer: userid,
content: text
}),
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
}
const result = await fetch(url,opt).then(resp=>resp.text())
stomp.send('/app/sendTo/' + userid, {}, JSON.stringify({
content: text,
writer: username
}))
}
async function exitChat() {
const url = '${cpath}/exitChat';
try {
await fetch(url).then(resp => resp.text());
stomp.send('/app/disconnect', {}, JSON.stringify({
writer: username,
}));
stomp.disconnect(function() {
console.log('Disconnected');
});
window.location.href = '${cpath}/inquiry/list';
} catch (error) {
console.error('에러:', error);
}
}
function onReceive(chat) {
const chatContent = JSON.parse(chat.body)
const text = chatContent.content
const from = chatContent.writer
let messageDirection = 'message-left'; // 기본적으로 왼쪽 정렬
if (from === username) { // 현재 사용자가 보낸 메시지인 경우 오른쪽 정렬
messageDirection = 'message-right';
}
let str = ''
str += '<div class="' + messageDirection + '">'
str += '<div>' + from + '</div>'
str += '<div class="content">' + text + '</div>'
str += '</div>'
chatArea.innerHTML += str
chatArea.scrollTop = chatArea.scrollHeight;
}
</script>
사용자는 자신이 userid를 이용하여 개설한 채팅방에 message input을 해주어야 하기 때문에
adResult를 배열로 만들어 그 배열의 0번째에 해당하는 채널의 메세지를 볼 수 있게 해주었다.
이 작업도 처음엔 어떻게 같은 채널의 메세지를 보게 할 지 잘 몰랐었는데, 팀원들과 협업하여
다행히 잘 해결되었다.
관리자 관점 채팅
stomp.connect({}, onConnect)
function onConnect() {
stomp.subscribe('/sendTo/admin', onReceive)
stomp.send('/app/enter', {}, JSON.stringify({
writer: username
}))
}
send.onclick = sendMsg
close.onclick = function() {
chatFrame.style.bottom = "-600px";
setTimeout(() => {
chatFrame.classList.add('hidden');
chatFrame.classList.remove('visible')
chatOpenButton.classList.remove('hidden');
}, 500);
}
chatOpenButton.onclick = function() {
chatFrame.style.bottom = "-600px"
chatFrame.classList.remove('hidden')
chatFrame.classList.add('visible')
setTimeout(() => {
chatFrame.style.bottom = "20px"
}, 10);
chatOpenButton.classList.add('hidden');
}
inputMsg.onkeyup = function(e) {
if(e.key == 'Enter') sendMsg()
if(e.key == 'Escape') e.target.value = ''
}
chatList()
async function chatList() {
const url = '${cpath}/chatList'
const result = await fetch(url).then(resp=>resp.json())
const arr = Array.from(result)
console.log(arr)
arr.forEach(e=> {
let str = ''
str += '<div class="chatRoom">' + e + '</div>'
listFrame.innerHTML += str
})
const chat = document.querySelectorAll('.chatRoom')
chat.forEach(e => e.addEventListener('click', enterChat))
}
async function enterChat(event) {
userid = event.target.textContent
const ad = '${cpath}/getRoom/' + userid
const adResult = await fetch(ad).then(resp=>resp.json())
const arr = Array.from(adResult)
chat_idx = arr[0].idx
console.log(chat_idx)
const url = '${cpath}/getMsg/' + chat_idx
const result = await fetch(url).then(resp=>resp.json())
console.log(result)
const arr2 = Array.from(result)
console.log(arr2)
if(arr2.length != 0) {
arr2.forEach(e => {
let messageDirection = 'message-left'; // 기본적으로 왼쪽 정렬
if(e.writer == 'admin') { // 현재 사용자가 보낸 메시지인 경우 오른쪽 정렬
messageDirection = 'message-right';
}
let str = ''
str += '<div class="' + messageDirection + '">'
str += '<div>' + e.nickname + '</div>'
str += '<div class="content">' + e.content + '</div>'
str += '</div>'
chatArea.innerHTML += str
})
}
chatArea.scrollTop = chatArea.scrollHeight;
chatFrame.classList.remove('hidden')
chatFrame.classList.add('visible')
stomp.subscribe('/sendTo/' + userid, onReceive)
}
async function sendMsg() {
const text = document.querySelector('input[name="msg"]').value
if(text == '') {
return
}
document.querySelector('input[name="msg"]').value = ''
const url = '${cpath}/sendMsg'
const opt = {
method: 'POST',
body: JSON.stringify({
chat_idx: chat_idx,
writer: '${login.userid}',
content: text
}),
headers: {
'Content-Type' : 'application/json; charset=utf-8'
}
}
const result = await fetch(url,opt).then(resp=>resp.text())
stomp.send('/app/sendTo/' + userid, {}, JSON.stringify({
content: text,
writer: username
}))
}
function onReceive(chat) {
const chatContent = JSON.parse(chat.body)
const text = chatContent.content
const from = chatContent.writer
let messageDirection = 'message-left';
if(from == username) {
messageDirection = 'message-right';
}
let str = ''
str += '<div class="' + messageDirection + '">'
str += '<div>' + from + '</div>'
str += '<div class="content">' + text + '</div>'
str += '</div>'
chatArea.innerHTML += str
chatArea.scrollTop = chatArea.scrollHeight;
}
</script>
먼저, 관리자는 chatList로 채팅이 활성화 되어 있는 전체 리스트를 볼 수 있다.
그리고 그 방을 클릭하면 해당 방으로 입장하게 된다.
당연히 각 사용자마다 채널이 다르기 때문에 각각 채널을 구독시키기 위해서 userid를 let으로
선언해주었고, 각 방을 클릭 시 해당 고객과의 1:1 채팅방에 들어갈 수 있게 했다.
구독할 채널을 추출하는 방식은 전 방식과 동일하게 배열에서 추출하였다.
지금 이렇게 돌아보면 정말 별거 아닌거 처럼 보일진 모르겠지만, 내 딴에는
정말 힘들었던 부분이었다.
막혀서 진도는 안나가는데 마감 기한은 점점 다가오고... 하지만 방법은 도저히 모르겠고...
그래도 팀원들에게 도움을 청해서 같이 의견을 주고받으니까 훨씬 수월하게 해결했던 것 같다.
아직 두번째 팀 프로젝트 이지만 이 경험이 나중에 내 개발자 인생에 있어서 정말 큰 도움이 될 것 같다.
'Spring' 카테고리의 다른 글
[Spring] ViewResolver (0) | 2024.10.11 |
---|---|
[Spring] 인증 메일 보내기 (1) | 2024.10.11 |
[Spring] WebSocket - 실시간 채팅 (0) | 2024.10.10 |
[Spring] WebSocket을 활용한 메모장 만들기 (2) | 2024.10.10 |
[Spring] HashMap json mapping (2) | 2024.10.10 |