Skip to content

Commit 0eaeff1

Browse files
committed
refact: 채팅방 버그로인한 roomId 추가, read-only websocket 연결유지
1 parent 90ebe64 commit 0eaeff1

6 files changed

Lines changed: 91 additions & 21 deletions

File tree

src/main/java/com/knoc/chat/dto/ChatMessageResponse.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,17 @@ public class ChatMessageResponse {
2929
// PAYMENT_REQUESTED 같은 금액 기반 시스템 메시지에서 결제 버튼 렌더링에 사용. 그 외엔 null
3030
private Integer amount;
3131

32-
public ChatMessageResponse(Long id, String senderNickname, String content, LocalDateTime createdAt, MessageType messageType, Long referenceId, Integer amount) {
32+
// roomId를 통해 프론트에서 어느 방 메시지인지 구분에 사용
33+
private Long roomId;
34+
35+
public ChatMessageResponse(Long id, String senderNickname, String content, LocalDateTime createdAt, MessageType messageType, Long referenceId, Integer amount, Long roomId) {
3336
this.id = id;
3437
this.senderNickname = senderNickname;
3538
this.content = content;
3639
this.createdAt = createdAt;
3740
this.messageType = messageType;
3841
this.referenceId = referenceId;
3942
this.amount = amount;
43+
this.roomId = roomId;
4044
}
4145
}

src/main/java/com/knoc/chat/service/ChatMessageService.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ public void sendMessage(Long roomId, String email, String content) {
8787
.messageType(savedMessage.getMessageType())
8888
.referenceId(savedMessage.getReferenceId()) // USER 메시지는 null
8989
// amount는 생략 -> null (PAYMENT_REQUESTED에서만 의미 있음)
90+
.roomId(roomId)
9091
.build();
9192

9293
// 6. 수신자/발신자 양쪽에 1:1 queue 전송

src/main/java/com/knoc/event/listener/ChatEventListener.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public void handleChatEventSystem(ChatSystemEvent event) {
6565
.messageType(event.type())
6666
.referenceId(event.referenceId())
6767
.amount(amount)
68+
.roomId(event.roomId())
6869
.build();
6970

7071
// 4. 1:1 Queue 방식으로 조건에 맞게 전송

src/main/resources/static/css/chatrooms.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,25 @@
119119
text-overflow: ellipsis;
120120
}
121121

122+
.room-meta {
123+
display: flex;
124+
flex-direction: column;
125+
align-items: flex-end;
126+
gap: 4px;
127+
flex-shrink: 0;
128+
}
129+
122130
.room-time {
123131
font-size: 0.7rem;
124132
color: #555;
133+
}
134+
135+
.unread-badge {
136+
width: 8px;
137+
height: 8px;
138+
border-radius: 50%;
139+
background-color: #00CFE8;
140+
display: none;
125141
flex-shrink: 0;
126142
}
127143

src/main/resources/static/js/chat.js

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ const SENIOR_PRICE_PER_REVIEW = window.SENIOR_PRICE_PER_REVIEW ?? 0;
1616
const chatContainer = document.getElementById('messageList');
1717
let stompClient = null;
1818

19+
// 현재 방 마감 여부 (페이지 로드 시 초기값 + ROOM_CLOSE 이벤트 수신 시 true로 전환)
20+
// ROOM_STATUS 상수는 변경이 안 되므로 별도 변수로 관리
21+
let currentRoomClosed = ROOM_STATUS === 'CLOSED';
22+
1923
// 결제 상세 모달에서 결제 진행 시 사용할 현재 주문 정보
2024
// (GET /orders/{orderId}/prepare 응답으로 채워짐)
2125
let paymentDetailOrderId = null; // Toss orderId로 사용할 orderNumber(예: "ORD-...")
@@ -157,8 +161,8 @@ function renderUserMessage(msg, options = {}) {
157161
}
158162

159163
// 사이드바 미리보기 실시간 업데이트
160-
function updateSidebarPreview(content) {
161-
const roomItem = document.querySelector(`.room-item[data-room-id="${ROOM_ID}"]`);
164+
function updateSidebarPreview(content, roomId = ROOM_ID) {
165+
const roomItem = document.querySelector(`.room-item[data-room-id="${roomId}"]`);
162166
if (!roomItem) return;
163167
const preview = roomItem.querySelector('.room-preview');
164168
if (preview) preview.textContent = content;
@@ -169,6 +173,34 @@ function updateSidebarPreview(content) {
169173
}
170174
}
171175

176+
// 읽지 않은 메시지 점 표시 관리 (localStorage로 새로고침 후에도 유지)
177+
function updateUnreadBadge(roomId) {
178+
localStorage.setItem('unread_' + roomId, 'true');
179+
showUnreadDot(roomId);
180+
}
181+
182+
function showUnreadDot(roomId) {
183+
const roomItem = document.querySelector(`.room-item[data-room-id="${roomId}"]`);
184+
if (!roomItem) return;
185+
const dot = roomItem.querySelector('.unread-badge');
186+
if (dot) dot.style.display = 'block';
187+
}
188+
189+
// 페이지 로드 시: 현재 방은 읽음 처리, 나머지는 localStorage에서 복원
190+
document.addEventListener('DOMContentLoaded', function () {
191+
// 현재 보고 있는 방은 읽음 처리
192+
if (ROOM_ID) localStorage.removeItem('unread_' + ROOM_ID);
193+
194+
// 다른 방들의 미확인 점 복원
195+
document.querySelectorAll('.room-item').forEach(function (item) {
196+
const roomId = item.getAttribute('data-room-id');
197+
if (roomId && localStorage.getItem('unread_' + roomId)) {
198+
const dot = item.querySelector('.unread-badge');
199+
if (dot) dot.style.display = 'block';
200+
}
201+
});
202+
});
203+
172204
// 사이드바 토글 (모바일 대응)
173205
function toggleSidebar() {
174206
const sidebar = document.getElementById('chatSidebar');
@@ -193,7 +225,8 @@ function disableChatUI() {
193225
// ==========================================
194226
// 5. STOMP 연결 및 구독 로직 (1:1 Queue)
195227
// ==========================================
196-
if (ROOM_ID && ROOM_STATUS !== 'CLOSED') {
228+
// CLOSED 방을 보고 있어도 다른 방의 메시지(배지/사이드바 업데이트)를 받기 위해 항상 연결한다.
229+
if (ROOM_ID) {
197230
const socket = new SockJS('/ws');
198231
stompClient = Stomp.over(socket);
199232
stompClient.debug = null;
@@ -205,28 +238,43 @@ if (ROOM_ID && ROOM_STATUS !== 'CLOSED') {
205238
stompClient.connect(connectHeaders, function () {
206239
console.log('✅ STOMP 서버 연결 완료');
207240
const statusEl = document.getElementById('connectionStatus');
208-
if(statusEl) {
209-
statusEl.textContent = '연결됨';
210-
statusEl.className = 'connection-status status-connected';
241+
if (statusEl) {
242+
if (currentRoomClosed) {
243+
statusEl.textContent = '마감됨';
244+
statusEl.className = 'connection-status status-disconnected';
245+
} else {
246+
statusEl.textContent = '연결됨';
247+
statusEl.className = 'connection-status status-connected';
248+
}
211249
}
212250

213251
// 💡 1:1 큐 구독 방식으로 통신
214252
stompClient.subscribe('/user/queue/chat', function (message) {
215253
const data = JSON.parse(message.body);
216254
const type = data.messageType || data.type;
217255

256+
// 다른 방 메시지: 사이드바 미리보기 + 배지만 업데이트하고 렌더링은 스킵
257+
if (data.roomId && data.roomId !== ROOM_ID) {
258+
if (type === 'USER') {
259+
updateSidebarPreview(data.content, data.roomId);
260+
updateUnreadBadge(data.roomId);
261+
}
262+
return;
263+
}
264+
265+
// 현재 방이 마감 상태면 새 메시지 렌더링 스킵
266+
if (currentRoomClosed) return;
267+
218268
// 실시간 마감 이벤트 감지
219269
if (type === 'ROOM_CLOSE') {
270+
currentRoomClosed = true; // 이후 이 방의 메시지는 렌더링하지 않음
220271
renderSystemMessage(data);
221272
disableChatUI();
222-
223-
stompClient.disconnect(function() {
224-
console.log("🔒 채팅방 마감: 소켓 연결이 안전하게 종료되었습니다.");
225-
if(statusEl) {
226-
statusEl.textContent = '마감됨';
227-
statusEl.className = 'connection-status status-disconnected';
228-
}
229-
});
273+
// WebSocket은 끊지 않음 → 다른 방 메시지(배지/사이드바)를 계속 받기 위해
274+
if (statusEl) {
275+
statusEl.textContent = '마감됨';
276+
statusEl.className = 'connection-status status-disconnected';
277+
}
230278
return;
231279
}
232280

@@ -256,9 +304,6 @@ if (ROOM_ID && ROOM_STATUS !== 'CLOSED') {
256304
statusEl.className = 'connection-status status-disconnected';
257305
}
258306
});
259-
} else if (ROOM_STATUS === 'CLOSED') {
260-
console.warn("🔒 이미 마감된 채팅방입니다. 소켓 연결을 차단합니다.");
261-
setTimeout(scrollToBottom, 100);
262307
}
263308

264309
// ==========================================

src/main/resources/templates/chat/chatrooms.html

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,12 @@
4747
</div>
4848
</div>
4949

50-
<div class="room-time"
51-
th:if="${latestMessages[room.id] != null}"
52-
th:text="${#temporals.format(latestMessages[room.id].createdAt, 'MM/dd')}">
50+
<div class="room-meta">
51+
<div class="room-time"
52+
th:if="${latestMessages[room.id] != null}"
53+
th:text="${#temporals.format(latestMessages[room.id].createdAt, 'MM/dd')}">
54+
</div>
55+
<span class="unread-badge" style="display:none;"></span>
5356
</div>
5457

5558
</a>

0 commit comments

Comments
 (0)