프로젝트에서 채팅 기능이 필요해서 STOMP 웹소켓으로 구현했다.
웹소켓 공부 내용
STOMP 선택
소스코드, 스크린샷
처음에는 Postman에서 웹소켓 STOMP 테스트를 하려고 했으나 매번 메세지 조작하는게 번거롭기도하고, 서치해보면 STOMP 연결 테스트 사이트도 있긴 있으나 로그인 세션값 세팅 하는게 마찬가지로 번거로웠음.
그래서 따로 테스트 용 프론트 페이지를 만들어서 사용함.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>세션 로그인 테스트 (/auth/signin)</title>
<style>
body { font-family: sans-serif; padding: 2rem; }
label, input { display: block; margin-top: 1rem; }
button { margin-top: 1.5rem; }
#response { margin-top: 2rem; color: green; }
#error { color: red; }
</style>
</head>
<body>
<h2>로그인</h2>
<form id="loginForm">
<label for="email">이메일:</label>
<input type="email" id="email" name="email" value="email@email.com" required />
<label for="password">비밀번호:</label>
<input type="password" id="password" name="password" value="asdf@1234" required />
<button type="submit">로그인</button>
</form>
<div id="response"></div>
<div id="error"></div>
<script>
document.getElementById("loginForm").addEventListener("submit", function (event) {
event.preventDefault();
const email = document.getElementById("email").value;
const password = document.getElementById("password").value;
fetch("http://localhost:8080/auth/signin", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
credentials: "include", // 세션 쿠키 저장
body: JSON.stringify({ email, password }),
})
.then((response) => {
if (response.ok) {
document.getElementById("response").innerText = "✅ 로그인 성공! 이동 중...";
document.getElementById("error").innerText = "";
setTimeout(() => {
window.location.href = "StompTesting.html"; // ✅ 같은 디렉토리 내로 이동
}, 1000); // 1초 후 이동 (선택)
} else {
return response.text().then(text => {
throw new Error(text || "로그인 실패");
});
}
})
.catch((err) => {
document.getElementById("error").innerText = `❌ ${err.message}`;
document.getElementById("response").innerText = "";
});
});
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>파티 채팅방</title>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
<style>
body { font-family: 'Pretendard', 'Noto Sans KR', sans-serif; background: #f5f6fa; margin: 0; }
.container { max-width: 430px; margin: 0 auto; background: #fff; min-height: 100vh; }
.chat-header { background: #fafbfc; padding: 16px; border-bottom: 1px solid #ececec; }
.chat-header-title { font-size: 1.15rem; font-weight: bold; margin-bottom: 4px; }
.chat-header-row { display: flex; align-items: center; font-size: 0.98rem;}
.chat-header-row .fa-user-group { margin-right: 6px;}
.chat-header-row .fa-location-dot, .chat-header-row .fa-calendar-days { margin-right: 6px; color: #4e73fa;}
#messages { border: none; padding: 10px 14px; margin-top: 0; min-height: 300px; max-height: 350px; overflow-y: auto; }
.message-block { margin-bottom: 16px; }
.sender-info {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.sender-info img {
width: 36px; height: 36px; border-radius: 50%; object-fit: cover; margin-right: 8px;
}
.sender-nickname { font-weight: bold; }
.message-content { margin-left: 44px; }
#messageInput { width: 70%; padding: 9px; border-radius: 16px; border: 1px solid #ccc;}
#sendButton { padding: 9px 16px; border: none; background: #4e73fa; color: #fff; border-radius: 18px; cursor: pointer;}
.chat-input-row { display: flex; align-items: center; border-top: 1px solid #eee; background: #fafbfc; padding: 12px 10px; position: fixed; bottom: 0; left: 0; right: 0; max-width: 430px; margin: 0 auto;}
.bottom-bar { display: none; }
</style>
</head>
<body>
<div class="container">
<div class="chat-header">
<div class="chat-header-title" id="chatTitle">채팅방</div>
<div class="chat-header-row" id="chatMeta"></div>
</div>
<div id="messages"></div>
<form class="chat-input-row" autocomplete="off" onsubmit="return false;">
<input type="text" id="messageInput" placeholder="메시지를 입력하세요" autocomplete="off" />
<button id="sendButton">전송</button>
</form>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/js/all.min.js"></script>
<script>
// 1. URL에서 partyId 추출
function getPartyId() {
const params = new URLSearchParams(window.location.search);
return params.get('partyId');
}
const partyId = getPartyId();
if (!partyId) {
alert('올바른 파티 ID가 없습니다.');
window.location.href = "party-list.html";
}
// 유저 정보 (로그인 세션에서 받아오거나, 임시값)
const senderId = 1; // 실제 구현시 로그인 유저ID로 교체
let partyInfo = null;
// 2. 채팅방 정보/메타데이터 불러오기 (파티명 등)
async function fetchPartyInfo() {
// 실제 API 주소 맞게 수정
const res = await fetch("http://localhost:8080/parties/" + partyId, { credentials: 'include' });
if (!res.ok) throw new Error('파티 정보 불러오기 실패');
return res.json();
}
async function renderPartyMeta() {
try {
const data = await fetchPartyInfo();
partyInfo = data.data || data; // {"data": {...}} 또는 {...}
document.getElementById('chatTitle').textContent = partyInfo.title || '채팅방';
document.getElementById('chatMeta').innerHTML =
`<i class="fa-solid fa-location-dot"></i> ${partyInfo.storeName || ''}
<i class="fa-solid fa-calendar-days" style="margin-left:12px"></i> ${(partyInfo.meetingDate || '').replace('T',' ').slice(0,16)}
<i class="fa-solid fa-user-group" style="margin-left:12px"></i> ${partyInfo.nowMembers||''}/${partyInfo.maxMembers||''}`;
} catch (e) {
document.getElementById('chatTitle').textContent = '채팅방';
}
}
renderPartyMeta();
// 3. STOMP 채팅 연결/구독
const socket = new SockJS('http://localhost:8080/ws');
const stompClient = Stomp.over(socket);
let currentPartyId = partyId;
stompClient.connect({}, function (frame) {
const destination = `/sub/parties/${currentPartyId}/chat`;
stompClient.subscribe(destination, function (message) {
const received = JSON.parse(message.body);
displayMessage(received);
});
// 채팅 로그 불러오기
fetch("http://localhost:8080/parties/" + currentPartyId + "/chats", {
method: 'GET',
credentials: 'include'
})
.then(res => res.json())
.then(json => {
const messages = json.data || [];
messages.forEach(displayMessage);
})
.catch(e => {
displaySystemMessage('❌ 채팅 로그 불러오기 실패: ' + e.message);
});
document.getElementById('sendButton').addEventListener('click', function () {
sendChat();
});
document.getElementById('messageInput').addEventListener('keydown', function (event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendChat();
}
});
});
function sendChat() {
const input = document.getElementById('messageInput');
const content = input.value.trim();
if (!content || !currentPartyId) return;
const chatMessage = {
partyId: currentPartyId,
senderId: senderId,
message: content
};
stompClient.send(`/pub/parties/${currentPartyId}/chat/send`, {}, JSON.stringify(chatMessage));
input.value = '';
}
function displayMessage(dto) {
const messagesDiv = document.getElementById('messages');
const messageBlock = document.createElement('div');
messageBlock.classList.add('message-block');
const senderInfo = document.createElement('div');
senderInfo.classList.add('sender-info');
const profileImg = document.createElement('img');
profileImg.src = dto.senderImageUrl || 'https://via.placeholder.com/36';
profileImg.alt = 'profile';
const nickname = document.createElement('div');
nickname.classList.add('sender-nickname');
nickname.textContent = dto.senderNickname || '알 수 없음';
senderInfo.appendChild(profileImg);
senderInfo.appendChild(nickname);
const content = document.createElement('div');
content.classList.add('message-content');
content.textContent = `${dto.message} ${dto.createdAt ? '('+dto.createdAt+')' : ''}`;
messageBlock.appendChild(senderInfo);
messageBlock.appendChild(content);
messagesDiv.appendChild(messageBlock);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function displaySystemMessage(text) {
const messagesDiv = document.getElementById('messages');
const sysMsg = document.createElement('div');
sysMsg.style.color = '#888';
sysMsg.style.fontStyle = 'italic';
sysMsg.style.marginBottom = '10px';
sysMsg.textContent = text;
messagesDiv.appendChild(sysMsg);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
</script>
</body>
</html>