Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public void removeChatRoom(ChatRoomDeleteRequest request, String email) {

ChatRoom removedRoom = chattingDomainService.getChatRoom(request.roomId());

chattingDomainService.isChatRoomOwner(removedRoom, user.getId());
chattingDomainService.checkChatRoomOwnerOrAdmin(removedRoom, user);

chattingDomainService.removeChatRoom(removedRoom);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
import org.ezcode.codetest.domain.game.model.item.Item;
import org.ezcode.codetest.domain.game.model.skill.Skill;
import org.ezcode.codetest.domain.game.service.GameManagementDomainService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
@PreAuthorize("hasRole('ADMIN')")
public class GameAdminUseCase {

private final GameManagementDomainService managementService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ public interface ChatRepository {

Chat save(Chat chat);

List<Chat> findAll();

List<Chat> findChatsFromLastHour(Long roomId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import org.ezcode.codetest.domain.chat.model.ChatRoom;
import org.ezcode.codetest.domain.chat.repository.ChatRepository;
import org.ezcode.codetest.domain.chat.repository.ChatRoomRepository;
import org.ezcode.codetest.domain.user.model.entity.User;
import org.ezcode.codetest.domain.user.model.enums.UserRole;
import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;
Expand All @@ -29,9 +31,9 @@ public void removeChatRoom(ChatRoom room) {
chatRoomRepository.delete(room);
}

public void isChatRoomOwner(ChatRoom room, Long userId) {
public void checkChatRoomOwnerOrAdmin(ChatRoom room, User user) {

if(!room.isOwner(userId)) {
if (!room.isOwner(user.getId()) && user.getRole() != UserRole.ADMIN) {
throw new ChattingException(ChattingExceptionCode.CHATROOM_NOT_OWNER);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,17 @@ protected Principal determineUser(
String query = uri.getQuery();
String tokenParam = null;

if (query != null && query.startsWith("token=")) {
if (query == null) {
throw new IllegalArgumentException("WebSocket 연결에 필요한 토큰이 없습니다.");
}

if (query.startsWith("token=")) {
tokenParam = query.substring(6);
} else if (query.startsWith("chat-token=")) {
tokenParam = query.substring(11);
attributes.put("isChattingWebsocket", true);
} else {
throw new IllegalArgumentException("허용되지 않은 토큰 파라미터: " + query);
}
Comment on lines +35 to 42
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

토큰 파라미터 처리 로직의 확장성과 유지보수성을 개선해주세요.

현재 구현에서 몇 가지 개선 사항이 필요합니다:

  • 매직 넘버(6, 11) 사용으로 인한 유지보수성 저하
  • 문자열 리터럴 중복
  • 새로운 토큰 타입 추가 시 확장성 부족

다음과 같이 리팩토링을 제안합니다:

+	private static final String TOKEN_PREFIX = "token=";
+	private static final String CHAT_TOKEN_PREFIX = "chat-token=";
+
 	@Override
 	protected Principal determineUser(
 		@NonNull ServerHttpRequest request,
 		@NonNull WebSocketHandler wsHandler,
 		@NonNull Map<String, Object> attributes
 	) {
 		URI uri = request.getURI();
 		String query = uri.getQuery();
 		String tokenParam = null;

 		if (query == null) {
 			throw new IllegalArgumentException("WebSocket 연결에 필요한 토큰이 없습니다.");
 		}

-		if (query.startsWith("token=")) {
-			tokenParam = query.substring(6);
-		} else if (query.startsWith("chat-token=")) {
-			tokenParam = query.substring(11);
+		if (query.startsWith(TOKEN_PREFIX)) {
+			tokenParam = query.substring(TOKEN_PREFIX.length());
+		} else if (query.startsWith(CHAT_TOKEN_PREFIX)) {
+			tokenParam = query.substring(CHAT_TOKEN_PREFIX.length());
 			attributes.put("isChattingWebsocket", true);
 		} else {
 			throw new IllegalArgumentException("허용되지 않은 토큰 파라미터: " + query);
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (query.startsWith("token=")) {
tokenParam = query.substring(6);
} else if (query.startsWith("chat-token=")) {
tokenParam = query.substring(11);
attributes.put("isChattingWebsocket", true);
} else {
throw new IllegalArgumentException("허용되지 않은 토큰 파라미터: " + query);
}
// In CustomHandShakeHandler.java, add at the top of the class:
private static final String TOKEN_PREFIX = "token=";
private static final String CHAT_TOKEN_PREFIX = "chat-token=";
@Override
protected Principal determineUser(
@NonNull ServerHttpRequest request,
@NonNull WebSocketHandler wsHandler,
@NonNull Map<String, Object> attributes
) {
URI uri = request.getURI();
String query = uri.getQuery();
String tokenParam = null;
if (query == null) {
throw new IllegalArgumentException("WebSocket 연결에 필요한 토큰이 없습니다.");
}
if (query.startsWith(TOKEN_PREFIX)) {
tokenParam = query.substring(TOKEN_PREFIX.length());
} else if (query.startsWith(CHAT_TOKEN_PREFIX)) {
tokenParam = query.substring(CHAT_TOKEN_PREFIX.length());
attributes.put("isChattingWebsocket", true);
} else {
throw new IllegalArgumentException("허용되지 않은 토큰 파라미터: " + query);
}
// … continue processing tokenParam …
}
🤖 Prompt for AI Agents
In
src/main/java/org/ezcode/codetest/infrastructure/event/config/CustomHandShakeHandler.java
around lines 35 to 42, the token parameter handling uses hardcoded substring
indices and duplicated string literals, which reduces maintainability and
extensibility. Refactor by defining constants for token prefixes and their
lengths, and use these constants to extract token values. Consider using a map
or enum to associate token prefixes with their handling logic, so adding new
token types requires minimal code changes.


Claims claims = jwtUtil.extractClaims(tokenParam);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.ezcode.codetest.infrastructure.event.dto;

public record GameLevelUpEvent(

Long userId,

boolean isProblemSolved,

String problemCategory

) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import org.ezcode.codetest.infrastructure.event.dto.ChatRoomParticipantCountChangeEvent;
import org.ezcode.codetest.infrastructure.event.dto.ChatRoomEntryExitMessageEvent;
import org.ezcode.codetest.infrastructure.event.dto.ChatRoomHistoryLoadEvent;
import org.ezcode.codetest.infrastructure.event.service.StompMessageService;
import org.ezcode.codetest.infrastructure.event.publisher.StompMessageService;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.ezcode.codetest.infrastructure.event.listener;

import org.ezcode.codetest.domain.game.exception.GameException;
import org.ezcode.codetest.domain.game.service.CharacterStatusDomainService;
import org.ezcode.codetest.infrastructure.event.dto.GameLevelUpEvent;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.transaction.interceptor.TransactionAspectSupport;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
public class GameLevelUpListener {

private final CharacterStatusDomainService characterDomainService;

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleGameCharacterLevelUp(GameLevelUpEvent event) {

try {
characterDomainService.gameCharacterLevelUp(event.userId(), event.isProblemSolved(),
event.problemCategory());
} catch (GameException ge) {
log.info("현재 해당 사용자는 게임 캐릭터를 생성한 상태가 아닙니다. {}", ge.getMessage());
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
} catch (Exception e) {
log.warn("문제 풀이로 인한 레벨업 실패 Unknown Error 발생", e);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import org.ezcode.codetest.application.notification.event.NotificationReadEvent;
import org.ezcode.codetest.infrastructure.event.dto.NotificationRecord;
import org.ezcode.codetest.infrastructure.event.dto.NotificationResponse;
import org.ezcode.codetest.infrastructure.event.service.StompMessageService;
import org.ezcode.codetest.infrastructure.event.publisher.StompMessageService;
import org.ezcode.codetest.infrastructure.persistence.repository.notification.NotificationRepository;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import org.ezcode.codetest.domain.chat.model.ChatRoom;
import org.ezcode.codetest.domain.chat.service.ChattingDomainService;
import org.ezcode.codetest.domain.user.service.UserDomainService;
import org.ezcode.codetest.infrastructure.event.service.StompMessageService;
import org.ezcode.codetest.infrastructure.event.publisher.StompMessageService;
import org.ezcode.codetest.infrastructure.session.service.RedisSessionCountService;
import org.springframework.context.ApplicationListener;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
Expand All @@ -31,11 +31,24 @@ public class WebSocketEventListener implements ApplicationListener<SessionDiscon
@Override
public void onApplicationEvent(SessionDisconnectEvent event) {

StompHeaderAccessor header = StompHeaderAccessor.wrap(event.getMessage());

Map<String, Object> attrs = header.getSessionAttributes();

if (attrs == null) {
return;
}

Boolean isChatWs = (Boolean)attrs.get("isChattingWebsocket");

if (!Boolean.TRUE.equals(isChatWs)) {
return;
}

try {
StompHeaderAccessor h = StompHeaderAccessor.wrap(event.getMessage());
String sessionId = h.getSessionId();
String sessionId = header.getSessionId();

String email = h.getUser().getName();
String email = header.getUser().getName();
String nickName = userDomainService.getUser(email).getNickname();

RoomSessionInfo roomData = sessionService.removeSessionCount(sessionId);
Expand All @@ -51,7 +64,7 @@ public void onApplicationEvent(SessionDisconnectEvent event) {
messageService.handleChatRoomEntryExitMessage(ChatMessageTemplate.CHAT_ROOM_LEFT.format(nickName),
chatRoom.getId());
} catch (Exception e) {
log.info("SessionDisconnectEvent 처리 중 예외 발생, 채팅 관련 웹소켓 세션이 아닙니다.", e);
log.info("SessionDisconnectEvent 채팅 세션 정리중 예외 발생", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.ezcode.codetest.infrastructure.event.service;
package org.ezcode.codetest.infrastructure.event.publisher;

import org.ezcode.codetest.application.chatting.port.event.ChatEventService;
import org.ezcode.codetest.infrastructure.event.dto.ChatMessageBroadcastEvent;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.ezcode.codetest.infrastructure.event.service;
package org.ezcode.codetest.infrastructure.event.publisher;

import java.time.Instant;
import java.util.List;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.ezcode.codetest.infrastructure.event.service;
package org.ezcode.codetest.infrastructure.event.publisher;

import org.ezcode.codetest.application.notification.event.NotificationCreateEvent;
import org.ezcode.codetest.application.notification.event.NotificationListRequestEvent;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.ezcode.codetest.infrastructure.event.publisher;

import org.ezcode.codetest.domain.submission.model.entity.UserProblemResult;
import org.ezcode.codetest.infrastructure.event.dto.GameLevelUpEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class ProblemEventPublisher {

private final ApplicationEventPublisher publisher;

public void publishProblemSolveEvent(UserProblemResult event) {

Long userId = event.getUser().getId();
boolean isCorrect = event.isCorrect();
String problemCategory = event.getProblem().getCategory().getDescription();

publisher.publishEvent(new GameLevelUpEvent(userId, isCorrect, problemCategory));
}
Comment on lines +16 to +23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

널 안전성 검증 추가 필요

현재 구현에서는 event, event.getUser(), event.getProblem(), event.getProblem().getCategory() 등의 null 체크가 누락되어 있어 NullPointerException이 발생할 수 있습니다.

다음과 같이 수정하여 null 안전성을 보장해주세요:

 public void publishProblemSolveEvent(UserProblemResult event) {
+    if (event == null || event.getUser() == null || event.getProblem() == null 
+        || event.getProblem().getCategory() == null) {
+        throw new IllegalArgumentException("UserProblemResult와 관련 객체들은 null일 수 없습니다");
+    }

     Long userId = event.getUser().getId();
     boolean isCorrect = event.isCorrect();
     String problemCategory = event.getProblem().getCategory().getDescription();

     publisher.publishEvent(new GameLevelUpEvent(userId, isCorrect, problemCategory));
 }
🤖 Prompt for AI Agents
In
src/main/java/org/ezcode/codetest/infrastructure/event/publisher/ProblemEventPublisher.java
between lines 16 and 23, add null checks for event, event.getUser(),
event.getProblem(), and event.getProblem().getCategory() before accessing their
methods to prevent NullPointerException. Implement conditional checks that
return early or handle null cases safely to ensure null safety in the
publishProblemSolveEvent method.

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.ezcode.codetest.infrastructure.event.service;
package org.ezcode.codetest.infrastructure.event.publisher;

import java.util.Map;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.ezcode.codetest.infrastructure.event.service;
package org.ezcode.codetest.infrastructure.event.publisher;

import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,6 @@ public Chat save(Chat chat) {
return chatRepository.save(chat);
}

@Override
public List<Chat> findAll() {

return chatRepository.findAll();
}

@Override
public List<Chat> findChatsFromLastHour(Long roomId) {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.ezcode.codetest.presentation.chattingmanagement.chatting.config;
package org.ezcode.codetest.presentation.chatting.chatting.config;

import org.ezcode.codetest.presentation.chattingmanagement.chatting.interceptor.ChatLimitInterceptor;
import org.ezcode.codetest.presentation.chatting.chatting.interceptor.ChatLimitInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
Expand All @@ -16,6 +16,6 @@ public class ChatWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(chatSpamInterceptor)
.addPathPatterns("/api/room/*/chat");
.addPathPatterns("/api/rooms/*/chat");
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.ezcode.codetest.presentation.chattingmanagement.chatting.controller;
package org.ezcode.codetest.presentation.chatting.chatting.controller;

import org.ezcode.codetest.application.chatting.dto.request.ChatSaveRequest;
import org.ezcode.codetest.application.chatting.service.ChattingUseCase;
Expand All @@ -14,7 +14,7 @@
import lombok.RequiredArgsConstructor;

@Controller
@RequestMapping("/api/room/{roomId}/chat")
@RequestMapping("/api/rooms/{roomId}/chat")
@RequiredArgsConstructor
public class ChatController {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.ezcode.codetest.presentation.chattingmanagement.chatting.controller;
package org.ezcode.codetest.presentation.chatting.chatting.controller;

import org.ezcode.codetest.application.chatting.dto.request.ChatRoomDeleteRequest;
import org.ezcode.codetest.application.chatting.dto.request.ChatRoomSaveRequest;
Expand All @@ -21,7 +21,7 @@

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/chatrooms")
@RequestMapping("/api/rooms")
public class ChatRoomController {

private final ChattingUseCase chatUseCase;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.ezcode.codetest.presentation.chattingmanagement.chatting.interceptor;
package org.ezcode.codetest.presentation.chatting.chatting.interceptor;

import java.util.Map;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.ezcode.codetest.presentation.chattingmanagement.event;
package org.ezcode.codetest.presentation.chatting.event;

import java.security.Principal;

Expand All @@ -25,7 +25,7 @@ public void handleGetChatRoomList(
chatUseCase.getChatRoomList(principal.getName(), sessionId);
}

@MessageMapping("/room/{roomId}/enter")
@MessageMapping("/rooms/{roomId}/enter")
public void handleGetChattingHistory(
Principal principal,
@DestinationVariable Long roomId,
Expand All @@ -35,7 +35,7 @@ public void handleGetChattingHistory(
chatUseCase.getChattingHistory(sessionId, principal.getName(), principal.getName(), roomId);
}

@MessageMapping("/room/{roomId}/left")
@MessageMapping("/rooms/{roomId}/left")
public void handleChatRoomLeft(
Principal principal,
@DestinationVariable Long roomId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.ezcode.codetest.presentation.chattingmanagement.view;
package org.ezcode.codetest.presentation.chatting.view;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
Expand Down
8 changes: 4 additions & 4 deletions src/main/resources/templates/chat-page.html
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ <h2>채팅방 목록</h2>
});

function initChat() {
const socket = new SockJS('/ws?token=' + encodeURIComponent(token));
const socket = new SockJS('/ws?chat-token=' + encodeURIComponent(token));
stompClient = Stomp.over(socket);

// ─── Heartbeat 5분(300,000ms) 설정 ───
Expand Down Expand Up @@ -308,7 +308,7 @@ <h2>채팅방 목록</h2>
const nr = li.dataset.roomId;
if (!nr || currentRoomId === nr) return;
if (chatSubscription && currentRoomId) {
stompClient.send(`/chat/room/${currentRoomId}/left`, {}, currentRoomId);
stompClient.send(`/chat/rooms/${currentRoomId}/left`, {}, currentRoomId);
chatSubscription.unsubscribe();
}
currentRoomId = nr;
Expand Down Expand Up @@ -337,7 +337,7 @@ <h2>채팅방 목록</h2>
}, 100);
});
setTimeout(() => {
stompClient.send(`/chat/room/${nr}/enter`, {}, nr);
stompClient.send(`/chat/rooms/${nr}/enter`, {}, nr);
}, 100);
}, 100);
});
Expand All @@ -352,7 +352,7 @@ <h2>채팅방 목록</h2>
if (!currentRoomId) return;
const m = msgInput.value.trim();
if (!m) return;
fetch(`/api/room/${currentRoomId}/chat`, {
fetch(`/api/rooms/${currentRoomId}/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import static org.mockito.Mockito.*;

import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import org.ezcode.codetest.application.chatting.dto.request.ChatRoomDeleteRequest;
Expand Down Expand Up @@ -168,7 +167,7 @@ void removeChatRoom() {
// then
verify(userDomainService).getUser(TEST_EMAIL);
verify(chattingDomainService).getChatRoom(TEST_ROOM_ID);
verify(chattingDomainService).isChatRoomOwner(chatRoom1, user.getId());
verify(chattingDomainService).checkChatRoomOwnerOrAdmin(chatRoom1, user);
verify(chattingDomainService).removeChatRoom(chatRoom1);

ArgumentCaptor<ChatRoomCache> cacheCaptor = ArgumentCaptor.forClass(ChatRoomCache.class);
Expand Down
Loading