웹 게임 프로젝트 관련 코드 분석 - with GPT4
분석 프로젝트 :
https://github.com/namoldak/Backend/tree/dev/src/main/java/com/example/namoldak
Config
WebSocketConfig
package com.example.namoldak.config;
import com.example.namoldak.util.webSocket.SignalHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.*;
// 기능 : 웹소켓 사용에 필요한 설정
@RequiredArgsConstructor
@Configuration
@EnableWebSocketMessageBroker
@EnableWebSocket
@Slf4j
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer, WebSocketConfigurer {
// 웹 소켓 연결을 위한 엔드포인트 설정 및 stomp sub/pub 엔드포인트 설정
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// stomp 접속 주소 url => /ws/chat
registry.addEndpoint("/ws-stomp") // 연결될 Endpoint
.setAllowedOriginPatterns("*") // CORS 설정
.withSockJS() // SockJS 설정
.setHeartbeatTime(1000); // 연결상태 확인 주기
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/sub"); // 구독한 것에 대한 경로
config.setApplicationDestinationPrefixes("/pub"); //
}
// 웹 소켓 버퍼 사이즈 증축
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setMessageSizeLimit(160 * 64 * 1024); // default : 64 * 1024
registration.setSendTimeLimit(100 * 10000); // default : 10 * 10000
registration.setSendBufferSizeLimit(3* 512 * 1024); // default : 512 * 1024
}
@Bean
public WebSocketHandler signalHandler() {
return new SignalHandler();
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(signalHandler(), "/signal")
.setAllowedOrigins("*")
.setAllowedOrigins("http://localhost:3000", "https://namoldak.com")
.withSockJS(); // allow all origins
}
}
- Spring 프레임워크를 사용하여 웹소켓 기반의 통신을 구성하기 위한 설정 클래스
- 웹소켓 통신을 위한 라우팅, 보안, 세션 관리 등을 구성하는 중요한 역할 수행
- 클래스 선언 및 어노테이션 사용 : @Configuration, @EnableWebSocketMessageBroker, @EnableWebSocket, @Slf4j 어노테이션을 사용해 웹소켓 설정 및 로깅 기능을 활성화하고, Spring의 구성요소로 정의
- STOMP 엔드포인트 설정 : registerStompEndpoints( ) 메서드는 클라이언트가 웹소켓 서버에 연결할 수 있는 주소('/ws-stomp')를 설정하고 CORS 설정과 SockJS 지원을 통해 다양한 브라우저와의 호환성을 제공함
- 메시지 브로커 구성 : configureMessageBroker( ) 메서드는 클라이언트가 메시지를 구독할 수 있는 경로('/sub')와 메시지를 발행할 수 있는 경로('/pub')를 설정하여 메시지 브로커를 구성함
- 웹소켓 트랜스포트 설정 : configureWebSocketTransport( ) 메서드는 메시지 크기, 전송 시간 제한, 버퍼 사이즈 등 웹소켓 연결의 세부 트랜스포트 설정을 조정함
- 핸들러 정의 및 매핑 : signalHandler( ) 빈을 정의하여 웹소켓 핸들러를 구현하고, registerWebSocketHandler( ) 메서드에서 이 핸들러를 경로('/signal')에 매핑함, CORS 정책과 SockJS 설정도 이 부분에서 관리됨
WebSecurityConfig
package com.example.namoldak.config;
import com.example.namoldak.util.jwt.JwtAuthFilter;
import com.example.namoldak.util.security.SecurityExceptionFilter;
import com.example.namoldak.util.jwt.JwtUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Collections;
// 기능 : Spring Security 사용에 필요한 설정
@Configuration
@Slf4j
@RequiredArgsConstructor
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig {
private final JwtUtil jwtUtil;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
try {
// CSRF 설정
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.httpBasic().disable()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.antMatchers(HttpMethod.GET, "/posts/myPost").authenticated()
.antMatchers(HttpMethod.GET, "/**").permitAll()
.antMatchers("/ws-stomp").permitAll()
.antMatchers("/signal/**").permitAll()
.antMatchers("/signal").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(new JwtAuthFilter(jwtUtil),
UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new SecurityExceptionFilter(), JwtAuthFilter.class);
http.cors();
} catch (Exception e) {
log.info(e.getMessage());
}
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("http://localhost:3000");
config.addAllowedOrigin("https://namoldak.com.s3.ap-northeast-2.amazonaws.com");
config.addAllowedOrigin("https://namoldak.com");
config.addAllowedOrigin("https://d3j37rx7mer6cg.cloudfront.net");
// addExposedHeader : 프론트에서 헤더의 해당 값에 접근할 수 있게 허락해주는 옵션
config.addExposedHeader(JwtUtil.ACCESS_TOKEN);
config.addExposedHeader(JwtUtil.REFRESH_TOKEN);
config.addExposedHeader(JwtUtil.KAKAO_TOKEN);
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setAllowedOriginPatterns(Collections.singletonList("*"));
config.setAllowCredentials(true);
config.validateAllowCredentials();
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
- Spring Security를 활용한 보안 설정을 정의하는 클래스
- 웹 애플리케이션의 보안 관련 주요 설정을 담당하며, 사용자 인증 및 권한 부여, CORS 정책, HTTP 요청 보안 등의 기능을 구성하는 데 중요한 역할을 수행
- 보안 구성 어노테이션 사용 : @EnableWebSecurity와 @EnableGlobalMethodSecurity(securedEnabled = true) 어노테이션을 사용하여 웹 보안과 메서드 수준의 보안을 활성화함
- 비밀번호 인코더 빈 설정 : passwordEncoder( ) 메서드를 통해 BCrypt 알고리즘을 사용하는 PasswordEncoder의 인스턴스를 생성하고 빈으로 등록함
- 보안 필터 체인 정의 : securityFilterChain(HttpSecurity http) 메서드에서는 다음과 같은 보안 관련 설정을 수행함
- CSRF(Cross-Site Request Forgery) 공격 방지 기능을 비활성화함
- 세션 관리 전력을 STATELESS로 설정하여 세션을 사용하지 않고 각 요청을 독립적으로 처리함
- HTTP Basic 인증을 비활성화하고, 특정 경로에 대한 접근 권한을 설정함
- JWT(JSON Web Token) 인증 필터를 UsernamePasswordAuthenticationFilter 전에 추가하여 인증 과정을 커스텀함
- CORS(Cross-Origin Resource Sharing) 설정 : corsConfigurationSource( ) 메서드에서는 CORS 정책을 정의하여, 서로 다른 도메인 간의 리소스 공유를 허용하는 설정을 구성
- JWT 관련 필터 추가 : JwtAuthFilter와 SecurityExceptionFilter를 사용하여 JWT를 통한 인증 과정을 처리하고, 보안 관련 예외를 처리함
RedisConfig
package com.example.namoldak.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.StringRedisSerializer;
// 기능 : Redis 사용에 필요한 설정
@EnableRedisRepositories
@Configuration
public class RedisConfig {
// Redis 연결
@Bean
public RedisConnectionFactory redisConnectionFactory() {
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory();
return lettuceConnectionFactory;
}
// Redis 데이터 처리에 사용하는 템플릿
@Bean
public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
// 직렬화
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
}
- Redis 데이터베이스 연결 및 작업을 위한 설정을 제공
- Redis와의 통신을 위한 기본적인 연결 설정과 데이터 직렬화 방식을 정의하며, Spring 애플리케이션에서 Redis를 사용하기 위한 기반을 마련
- @EnableRedisRepositories 어노테이션 : Spring Data Redis의 리포지토리 지원을 활성화함, 이를 통해 Redis 연산을 수행하는 데 필요한 리포지토리 액세스를 자동으로 구성
- RedisConnectionFactory 빈 설정 : redisConnectionFactory( ) 메서드는 Redis 서버에 연결을 관리하는 RedisConnectionFactory 인스턴스를 생성함, LettuceConnectionFactory를 사용하여 연결 팩토리를 구현하며, Lettuce는 비동기 이벤트 기반의 Redis 클라이언트 라이브러리임
- RedisTemplate 빈 설정 : redisTemplate( ) 메서드는 Redis 데이터 작업을 위한 RedisTemplate 객체를 생성하고 구성함, 이 템플릿을 통해 Redis 데이터베이스와의 상호 작용이 이루어짐
- setConnectionFactory( ) : Redis 연결을 관리하기 위해 RedisConnectionFactory를 RedisTemplate에 설정함
- setKeySerializer( )와 setValueSerializer( ) : 데이터 저장 및 조회 시 사용할 키와 값의 직렬화 방식을 설정함, StringRedisSerializer를 사용하여 문자열 데이터를 처리함
Util
webSocket/SignalHandler
package com.example.namoldak.util.webSocket;
import com.example.namoldak.repository.SessionRepository;
import com.example.namoldak.dto.RequestDto.WebSocketMessage;
import com.example.namoldak.dto.ResponseDto.WebSocketResponseMessage;
import com.example.namoldak.service.GameRoomService;
import com.example.namoldak.util.GlobalResponse.CustomException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import javax.transaction.Transactional;
import java.io.IOException;
import java.util.*;
import static com.example.namoldak.util.GlobalResponse.code.StatusCode.SESSION_ROOM_NOT_FOUND;
// 기능 : WebRTC를 위한 시그널링 서버 부분으로 요청타입에 따라 분기 처리
@Slf4j
@Component
public class SignalHandler extends TextWebSocketHandler {
@Autowired
private GameRoomService gameRoomService;
private final SessionRepository sessionRepository = SessionRepository.getInstance(); // 세션 데이터 저장소
private final ObjectMapper objectMapper = new ObjectMapper();
private static final String MSG_TYPE_JOIN_ROOM = "join_room";
private static final String MSG_TYPE_OFFER = "offer";
private static final String MSG_TYPE_ANSWER = "answer";
private static final String MSG_TYPE_CANDIDATE = "candidate";
@Override
public void afterConnectionEstablished(final WebSocketSession session) {
// 웹소켓이 연결되면 실행되는 메소드
}
// 시그널링 처리 메소드
@Override
protected void handleTextMessage(final WebSocketSession session, final TextMessage textMessage) {
try {
WebSocketMessage message = objectMapper.readValue(textMessage.getPayload(), WebSocketMessage.class);
String userName = message.getSender();
Long roomId = message.getRoomId();
switch (message.getType()) {
// 처음 입장
case MSG_TYPE_JOIN_ROOM:
if (sessionRepository.hasRoom(roomId)) {
// 해당 챗룸이 존재하면
// 세션 저장 1) : 게임방 안의 session List에 새로운 Client session정보를 저장
sessionRepository.addClient(roomId, session);
} else {
// 해당 챗룸이 존재하지 않으면
// 세션 저장 1) : 새로운 게임방 정보와 새로운 Client session정보를 저장
sessionRepository.addClientInNewRoom(roomId, session);
}
// 세션 저장 2) : 이 세션이 어느 방에 들어가 있는지 저장
sessionRepository.saveRoomIdToSession(session, roomId);
// 세션 저장 3) : 방 안에 닉네임들 저장
sessionRepository.addNicknameInRoom(session.getId(), message.getNickname());
Map<String, WebSocketSession> joinClientList = sessionRepository.getClientList(roomId);
// 방안 참가자 중 자신을 제외한 나머지 사람들의 Session ID를 List로 저장
List<String> exportSessionList = new ArrayList<>();
for (Map.Entry<String, WebSocketSession> entry : joinClientList.entrySet()) {
if (entry.getValue() != session) {
exportSessionList.add(entry.getKey());
}
}
// 방안 참가자들 닉네임 List
Map<String, String> exportNicknameList = new HashMap<>();
for (Map.Entry<String, WebSocketSession> entry : joinClientList.entrySet()) {
if (entry.getValue() != session) {
exportNicknameList.put(entry.getKey(), sessionRepository.getNicknameInRoom(entry.getKey()));
}
}
// 접속한 본인에게 방안 참가자들 정보를 전송
sendMessage(session,
new WebSocketResponseMessage().builder()
.type("all_users")
.sender(userName)
.data(message.getData())
.allUsers(exportSessionList)
.allUsersNickNames(exportNicknameList)
.candidate(message.getCandidate())
.sdp(message.getSdp())
.build());
break;
case MSG_TYPE_OFFER:
case MSG_TYPE_ANSWER:
case MSG_TYPE_CANDIDATE:
if (sessionRepository.hasRoom(roomId)) {
Map<String, WebSocketSession> oacClientList = sessionRepository.getClientList(roomId);
if (oacClientList.containsKey(message.getReceiver())) {
WebSocketSession ws = oacClientList.get(message.getReceiver());
sendMessage(ws,
new WebSocketResponseMessage().builder()
.type(message.getType())
.sender(session.getId()) // 보낸사람 session Id
.senderNickName(message.getNickname())
.receiver(message.getReceiver()) // 받을사람 session Id
.data(message.getData())
.offer(message.getOffer())
.answer(message.getAnswer())
.candidate(message.getCandidate())
.sdp(message.getSdp())
.build());
}
} else {
throw new CustomException(SESSION_ROOM_NOT_FOUND);
}
break;
default:
log.info("======================================== DEFAULT");
log.info("============== 들어온 타입 : " + message.getType());
}
} catch (JsonProcessingException e) {
log.info("=================== SignalHandler Json처리 에러 : {} ", e.getMessage());
}
}
// 웹소켓 연결이 끊어지면 실행되는 메소드
@Override
@Transactional
public void afterConnectionClosed(final WebSocketSession session, final CloseStatus status) {
String nickname = sessionRepository.getNicknameInRoom(session.getId());
// 끊어진 세션이 어느방에 있었는지 조회
Long roomId = sessionRepository.getRoomId(session);
// 1) 게임방에서 나가는 멤버 정보 정리 / 방장이 나가면 방장도 바꿈
gameRoomService.exitGameRoomAboutSession(nickname, roomId);
// 2) 방 참가자들 세션 정보들 사이에서 삭제
sessionRepository.deleteClient(roomId, session);
// 3) 별도 해당 참가자 세션 정보도 삭제
sessionRepository.deleteRoomIdToSession(session);
// 4) 별도 해당 닉네임 리스트에서도 삭제
sessionRepository.deleteNicknameInRoom(session.getId());
// 본인 제외 모두에게 전달
for(Map.Entry<String, WebSocketSession> oneClient : sessionRepository.getClientList(roomId).entrySet()){
sendMessage(oneClient.getValue(),
new WebSocketResponseMessage().builder()
.type("leave")
.sender(session.getId())
.receiver(oneClient.getKey())
.build());
}
}
// 메세지 발송
private void sendMessage(final WebSocketSession session, final WebSocketResponseMessage message) {
try {
String json = objectMapper.writeValueAsString(message);
session.sendMessage(new TextMessage(json));
} catch (IOException e) {
log.info("============== 발생한 에러 메세지: {}", e.getMessage());
}
}
}
- SignalHandler 클래스는 Spring에서 제공하는 TextWebSocketHandler를 상속하여 구현한 웹소켓 핸들러로, WebRTC 시그널링 서버의 역할을 수행
- WebRTC 기반 통신을 구현하는 데 핵심적인 역할을 하며, 웹소켓을 통한 실시간 통신 환경에서의 세션 관리, 메시지 라우팅, 그리고 상태 관리 등을 책임짐
- 웹소켓 연결 관리 : afterConnectionEstablished와 afterConnectionClosed 메서드를 통해 웹소켓 연결이 시작되거나 종료될 때 필요한 로직을 실행
- 메시지 처리 : handleTextMessage 메서드는 클라이언트로부터 전달받은 텍스트 메시지를 처리함, 이 메서드는 WebRTC 시그널링 과정에서의 다양한 메시지 타입(예: join_room, offer, answer, candidate)을 처리하여 상황에 맞는 동작을 수행
- 세션 및 방 관리 : 클라이언트의 세션 정보와 방 정보를 관리함, 예를 들어, 클라이언트가 방에 입장할 때('MSG_TYPE_JOIN_ROOM'), 세션 저정소에 클라이언트의 세션을 추가하고, 방 정보를 업데이트함
- 통신 로직 구현 : WebRTC 연결을 위한 시그널링 데이터(예:SDP, ICE candidate)를 교환하는 과정에서 필요한 메시지를 생성하고, 해당 메시지를 클라이언트에게 전송
- 예외 처리 : 메시지 처리 중 발생할 수 있는 예외를 로그로 기록하고, 적절한 예외 처리를 수행
converter/GameStartSetConverter
package com.example.namoldak.util.converter;
import com.example.namoldak.util.GlobalResponse.CustomException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import java.util.Map;
import static com.example.namoldak.util.GlobalResponse.code.StatusCode.JSON_PROCESS_FAILED;
// 기능 : 객체 전환
@Component
public class GameStartSetConverter {
private ObjectMapper objectMapper = new ObjectMapper();
//GameStartSet Map <-> String
// DB에서 String 저장한 멤버와 keyword의 매칭을 Map으로 전환
public Map<String, String> getMapFromStr(String keywordToMember) {
try {
Map<String, String> map = objectMapper.readValue(keywordToMember, new TypeReference<Map<String, String>>() {});
return map;
} catch (JsonProcessingException e) {
throw new CustomException(JSON_PROCESS_FAILED);
}
}
// DB에 String으로 저장하기 위해 멤버와 keyword의 매칭을 String으로 전환
public String getStrFromMap(Map<String, String> keywordToMember) {
try {
String str = objectMapper.writeValueAsString(keywordToMember);
return str;
} catch (JsonProcessingException e) {
throw new CustomException(JSON_PROCESS_FAILED);
}
}
}
- 맵(Map) 데이터와 JSON 형태의 문자열 간 변환을 처리하는 유틸리티 컴포넌트
- JSON 문자열과 맵 데이터 간 변환을 쉽게 하기 위한 목적으로 사용되며, 주로 데이터베이스에 복잡한 구조의 데이터를 저장하거나 읽을 때 유용함
- 예외 처리를 통해 변환 과정에서 발생할 수 있는 오류를 관리하며, 오류 발생 시 적절한 예외 메시지와 함께 처리함
- 맵 데이터를 JSON 문자열로 변환 : getStrFromMap 메서드는 맵 객체를 입력받아 JSON 형태의 문자열로 변환, 이 과정에서 ObjectMapper의 writeValueAsString 메서드를 사용하여 변환을 수행하며, 변환 중 예외가 발생할 경우 CustomException을 발생시킴
- JSON 문자열을 맵 데이터로 변환 : getMapFromStr 메서드는 JSON 형태의 문자열을 입력받아 맵 객체로 변환, 이 때 ObjectMapper의 readValue 메서드를 사용하며, TypeReference를 통해 반환될 맵의 키와 값이 모두 문자열(String)임을 지정함, 변환 중 예외가 발생할 경우 CustomException을 발생 시킴
Domain
GameMessage
package com.example.namoldak.domain;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
// 기능 : 채팅에 적용되는 관리자 메세지용 Dto
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class GameMessage<T> {
private String roomId;
private String senderId;
private String sender;
private String nickname;
private T content;
private GameMessage.MessageType type;
public enum MessageType {
JOIN, OWNER, ENTER, RULE, READY, START, KEYWORD, SPOTLIGHT,
FAIL, SKIP, SUCCESS, WINNER, ENDGAME, FORCEDENDGAME,
LEAVE, NEWOWNER, END, REWARD, STUPID
}
}
- 채팅 기능에서 사용되는 메시지를 표현하는 데 사용되는 데이터 전송 객체(DTO)
- 게임 상황에 따른 다양한 메시지 유형을 관리하며, 채팅이나 게임 상태 변화에 따른 정보 전달에 사용됨, 제네릭을 활용하여 메시지 내용을 유연하게 처리할 수 있어, 다양한 타입의 데이터를 메시지로 전송할 수 있음
- 필드
- roomId : 메시지가 속한 방의 식별자
- senderId : 메시지를 보낸 사용자의 식별자
- sender : 메시지를 보낸 사용자의 이름 또는 아이디
- nickname : 메시지를 보낸 사용자의 닉네임
- content : 메시지의 내용으로, 제네릭 타입 'T'를 사용하여 다양한 타입의 내용을 지원함
- type : 메시지의 종류를 나타내는 MessageType 열거형
- 메서드
- 클래스에는 생성자를 포함한 메서드는 Lombok 어노테이션을 통해 자동으로 생성 됨
- MessageType 열거형
- MessageType은 메시지 유형을 나타내며, 여러 가지 게임 상황에 맞는 상태 값들(JOIN, READY, START, END, LEAVE)을 포함, 이를 통해 메시지의 목적과 상황을 구분할 수 있음
GameRoom
package com.example.namoldak.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
// 기능 : 게임룸 Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Builder
public class GameRoom extends Timestamped{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long gameRoomId;
@Column(nullable = false)
private String gameRoomName;
@Column
private String gameRoomPassword;
@Column(nullable = false)
private String owner;
@Column(nullable = false)
private boolean status;
public void setOwner(String owner){
this.owner = owner;
}
public void setStatus(boolean status) {
this.status = status;
}
}
- 게임룸에 대한 엔티티(Entity)를 정의하며, 데이터베이스 테이블과 매핑됨
- 게임룸의 기본적인 정보를 저장하고 관리하는 데 사용되며, 데이터베이스와의 상호작용을 위한 JPA 엔티티로서의 역할을 수행
- 필드 정의
- gameRoomId : 게임룸의 고유 식별자로, 자동으로 생성되는 값, @GeneratedValue 어노테이션을 사용해 ID가자동으로 생성되도록 설정
- gameRoomName : 게임룸의 이름으로, nullable = false 속성을 통해 반드시 값을 가지도록 설정
- gameRoomPassword : 게임룸의 비밀번호로, 비공개 방을 만들 때 사용할 수 있음
- owner : 게임룸의 소유자(방장)를 나타내며, 이 필드 역시 반드시 값을 가지도록 설정
- status : 게임룸의 상태(예: 활성화/비활성화)를 나타내며, nullable = false 속성을 통해 반드시 값이 설정되어야 함
- Lombok 어노테이션 사용
- 메소드 정의
- setOwner : 게임룸의 소유자를 설정하는 메소드
- setStatus : 게임룸의 상태를 설정하는 메소드
GameRoomAttendee
package com.example.namoldak.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
// 기능 : 게임룸과 유저를 연결하는 중간 Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Builder
public class GameRoomAttendee extends Timestamped{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long gameRoomMemberId;
@JoinColumn(name="gameroomid")
@OneToOne(fetch = FetchType.LAZY)
private GameRoom gameRoom;
@JoinColumn(name="memberid")
@OneToOne(fetch = FetchType.LAZY)
private Member member;
@Column(nullable = false)
private String memberNickname;
public GameRoomAttendee(GameRoom gameRoom, Member member){
this.gameRoom = gameRoom;
this.member = member;
this.memberNickname = member.getNickname();
}
}
- 게임룸과 유저 간의 관계를 나타내는 중간 엔티티로서, 게임룸에 참여하는 각 유저의 정보를 매핑하는 데 사용됨
- 게임룸과 유저 사이의 관계를 나타내며, 각 게임룸에 참여하는 유저들의 정보를 관리하는 중요한 역할을 함
- 필드 정의
- gameRoomMemberId : 참가자의 고유 식별자로, 데이터베이스에서 자동으로 생성되는 값
- gameRoom : 게임룸 엔티티에 대한 참조로, @OneToOne 관계를 통해 게임룸 객체와 연결됨
- member : 유저(멤버) 엔티티에 대한 참조로, @OneToOne 관계를 통해 유저 객체와 연결됨
- memberNickname : 참가하는 멤버의 닉네임을 저장합니다. 이 필드는 nullable = false 속성을 가지므로 반드시 값을 설정해야 함
- JPA 관계 매핑 : @JoinColumn(name = "gameroomid")과 @JoinColumn(name = "memberid") 어노테이션을 통해 외래 키 관계를 명시적으로 정의
- 생성자 : GameRoomAttendee(GameRoom gameRoom, Member member)
- 주요 정보를 받아 객체를 초기화하는 생성자로 게임룸 객체와 멤버 객체를 인자로 받아, 해당 엔티티 인스턴스의 필드를 설정
- Lombok 어노테이션 사용
GameStartSet
package com.example.namoldak.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
// 기능 : 게임에 필요한 세트 설정
@Getter
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GameStartSet{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long gameSetId;
@Column(nullable = false)
private Long roomId;
@Column(nullable = false)
private String category;
@Column
private String keywordToMember; // JSON화
@Column(nullable = false)
private Integer round;
@Column(nullable = false)
private Integer spotNum;
@Column
private String winner;
@Column
private Long gameStartTime;
public void setSpotNum(Integer num) {
this.spotNum = num;
}
public void setRound(Integer round) {
this.round = round;
}
public void setWinner(String winner) {
this.winner = winner;
}
}
- 게임 시작 시 필요한 설정 정보를 담는 엔티티로 게임의 세션 설정을 데이터베이스에 저장하고 관리하기 위한 구조를 제공함
- 게임 설정에 필요한 다양한 정보를 관리하는 중요한 역할을 수행하며, 게임 로직을 실행하는 데 필요한 기본 데이터를 제공
- 필드 정의
- gameSetId : 게임 세트의 고유 식별자로, 데이터베이스에서 자동으로 생성되는 값
- roomId : 게임이 진행되는 방의 식별자
- category : 게임의 카테고리를 나타내며, 이는 게임의 종류나 분류를 구분하는 데 사용됨
- keywordToMember : 게임에 사용되는 키워드와 멤버 간의 매칭 정보를 JSON 문자열 형태로 저장함
- round : 게임의 라운드 수를 나타냄
- spotNum : 게임에서 중요한 위치나 역할을 나타내는 번호임
- winner : 게임의 승자를 나타냄
- gameStartTime : 게임의 시작 시간을 나타냄
- gameSetId : 게임 세트의 고유 식별자로, 데이터베이스에서 자동으로 생성되는 값
- 메서드 정의
- setSpotNum, setRound, setWinner : 각각 게임의 중요 번호, 라운드 수, 승자를 설정하는 메서드임
- JPA 설정
- Lombok 어노테이션 사용
Member
package com.example.namoldak.domain;
import com.example.namoldak.dto.RequestDto.SignupRequestDto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
// 기능 : 유저 정보 Entity
@Entity
@Table(name = "MEMBER")
@Getter
@NoArgsConstructor
public class Member extends Timestamped{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false, unique = true)
private String nickname;
@Column(nullable = false)
private String password;
@Column
private Long kakaoId;
@Column
private Long winNum = 0L;
@Column
private Long loseNum = 0L;
@Column
private Long totalGameNum = 0L;
@Column
private Long enterGameNum = 0L;
@Column
private Long soloExitNum = 0L;
@Column
private Long makeRoomNum = 0L;
@Column
private Long playTime = 0L;
public Member(String email, String nickname, String password) {
this.email = email;
this.nickname = nickname;
this.password = password;
}
public Member(String email, String password, Long kakaoId, String kakaoNickname){
this.email = email;
this.password = password;
this.kakaoId = kakaoId;
this.nickname = kakaoNickname;
}
public void update(SignupRequestDto signupRequestDto) {
this.nickname = signupRequestDto.getNickname();
}
public void updateWinNum(Long num) {
this.winNum += num;
}
public void updateLoseNum(Long num) {
this.loseNum += num;
}
public void updateTotalGame(Long num) {
this.totalGameNum += num;
}
public void updateSoloExit(Long num) {
this.soloExitNum += num;
}
public void updateMakeRoom(Long num) {
this.makeRoomNum += num;
}
public void updateEnterGame(Long num) {
this.enterGameNum += num;
}
public void updatePlayTime(Long num) {
this.playTime += num;
}
}
- 유저 정보를 관리하는 엔티티로서, 사용자의 기본 정보 및 게임 관련 통계 데이터를 저장함
- 유저의 인증 정보와 게임 내 활동을 추적하는 데 필요한 데이터를 포함하며, 애플리케이션 내에서 유저의 중심적인 역할을 수행함
- 필드 정의
- id : 유저의 고유 식별자로, 데이터베이스에서 자동으로 생성되는 값
- email, nickname, password : 유저의 이메일, 닉네임, 비밀번호를 저장합니다. 이메일과 닉네임은 고유해야 하므로 unique = true 설정이 추가됨
- kakaoId : 카카오 로그인을 통해 얻은 유저 식별자
- winNum, loseNum, totalGameNum, enterGameNum, soloExitNum, makeRoomNum, playTime : 유저의 게임 승리 횟수, 패배 횟수, 총 게임 수, 참가한 게임 수, 독자적으로 게임을 나간 횟수, 방을 만든 횟수, 총 플레이 시간 등의 게임 관련 통계 데이터를 저장함
- 메서드 정의
- 생성자를 통해 유저 정보를 초기화
- update... 메서드들을 통해 유저의 게임 통계 데이터를 업데이트함
- JPA 설정
domainModel
GameCommand
package com.example.namoldak.domainModel;
import com.example.namoldak.domain.*;
import com.example.namoldak.repository.GameRoomAttendeeRepository;
import com.example.namoldak.repository.GameRoomRepository;
import com.example.namoldak.repository.GameStartSetRepository;
import com.example.namoldak.repository.RewardReposiroty;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
// 기능 : 게임 도메인 관련 DB CUD 관리
@Service
@RequiredArgsConstructor
public class GameCommand {
private final GameRoomRepository gameRoomRepository;
private final GameRoomAttendeeRepository gameRoomAttendeeRepository;
private final GameStartSetRepository gameStartSetRepository;
private final RewardReposiroty rewardReposiroty;
//////////////TODO GameRoom 관련
// 게임방 저장하기
public void saveGameRoom(GameRoom gameRoom) {
gameRoomRepository.save(gameRoom);
}
// 게임방 삭제하기
public void deleteGameRoom(GameRoom gameRoom) {
gameRoomRepository.delete(gameRoom);
}
//////////////TODO GameRoomAttendee 관련
// 참가자 저장
public void saveGameRoomAttendee(GameRoomAttendee gameRoomAttendee) {
gameRoomAttendeeRepository.save(gameRoomAttendee);
}
// 참가자 삭제
public void deleteGameRoomAttendee(GameRoomAttendee gameRoomAttendee) {
gameRoomAttendeeRepository.delete(gameRoomAttendee);
}
//////////////TODO GameStartSet 관련
// GameStartSet 저장하기
public void saveGameStartSet(GameStartSet gameStartSet) {
gameStartSetRepository.save(gameStartSet);
}
// GameStartSet 객체로 DB에서 삭제하기
public void deleteGameStartSetByRoomId(Long roomId) {
gameStartSetRepository.deleteByRoomId(roomId);
}
//////////////TODO Reward 관련
// 리워드 저장하기
public void saveReward(Reward reward) {
rewardReposiroty.save(reward);
}
}
- 게임 도메인에 대한 데이터베이스의 생성(Create), 갱신(Update), 삭제(Delete) 작업을 관리함
- @Service 어노테이션을 사용하여 정의되어 있으며, 의존성 주입을 통해 필요한 레포지토리 객체들을 주입받음
- 이러한 구조는 각 엔티티에 대한 데이터베이스 작업을 명확하게 분리하고, 도메인 로직을 캡슐화하여 애플리케이션의 유지보수성과 확장성을 향상시킴
- 필드 정의
- gameRoomRepository : 게임방 데이터를 관리하는 레포지토리
- gameRoomAttendeeRepository : 게임방 참가자 데이터를 관리하는 레포지토리
- gameStartSetRepository : 게임 설정 데이터를 관리하는 레포지토리
- rewardReposiroty : 게임의 보상 데이터를 관리하는 레포지토리
- gameRoomRepository : 게임방 데이터를 관리하는 레포지토리
- 기능 제공
- saveGameRoom : 게임방 엔티티를 저장
- deleteGameRoom : 게임방 엔티티를 삭제
- saveGameRoomAttendee : 게임방 참가자 정보를 저장
- deleteGameRoomAttendee : 게임방 참가자 정보를 삭제
- saveGameStartSet : 게임 설정 정보를 저장
- deleteGameStartSetByRoomId : 특정 게임방의 게임 설정 정보를 삭제
- saveReward : 게임의 보상 정보를 저장
GameQuery
package com.example.namoldak.domainModel;
import com.example.namoldak.domain.*;
import com.example.namoldak.repository.*;
import com.example.namoldak.util.GlobalResponse.CustomException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import java.util.List;
import static com.example.namoldak.util.GlobalResponse.code.StatusCode.*;
import static com.example.namoldak.util.GlobalResponse.code.StatusCode.NOT_EXIST_ROOMS;
// 기능 : 게임 도메인 관련 DB Read 관리
@Service
@RequiredArgsConstructor
public class GameQuery {
private final GameStartSetRepository gameStartSetRepository;
private final KeywordRepository keywordRepository;
private final GameRoomAttendeeRepository gameRoomAttendeeRepository;
private final GameRoomRepository gameRoomRepository;
private final RewardReposiroty rewardReposiroty;
//////////////TODO GameRoom 관련
// 게임룸 Id로 객체 찾아오기
public GameRoom findGameRoomByRoomId(Long roomId) {
return gameRoomRepository.findByGameRoomId(roomId).orElseThrow(
()-> new CustomException(NOT_EXIST_ROOMS)
);
}
public GameRoom findGameRoomByRoomIdLock(Long roomId) {
return gameRoomRepository.findByGameRoomId2(roomId).orElseThrow(
()-> new CustomException(NOT_EXIST_ROOMS)
);
}
// 페이징 처리해서 모든 게임방 갖고 오기
public Page<GameRoom> findGameRoomByPageable(Pageable pageable) {
Page<GameRoom> gameRoomPage = gameRoomRepository.findAll(pageable);
return gameRoomPage;
}
// 특정 키워드로 검색해서 게임방 가져오기
public Page<GameRoom> findGameRoomByContainingKeyword(Pageable pageable, String keyword) {
Page<GameRoom> gameRoomPage = gameRoomRepository.findByGameRoomNameContaining(pageable, keyword);
return gameRoomPage;
}
// 게임방 리스트 형식으로 갖고 오기
public List<GameRoom> findAllGameRoomList() {
List<GameRoom> gameRoomList = gameRoomRepository.findAll();
return gameRoomList;
}
//////////////TODO GameRoomAttendee 관련
// 멤버 객체로 참가자 정보 조회
public GameRoomAttendee findAttendeeByMember(Member member) {
GameRoomAttendee gameRoomAttendee = gameRoomAttendeeRepository.findByMember(member).orElseThrow(
()-> new CustomException(NOT_FOUND_ATTENDEE)
);
return gameRoomAttendee;
}
// 게임룸 객체로 참가자 찾아오기
public List<GameRoomAttendee> findAttendeeByGameRoom(GameRoom gameRoom) {
List<GameRoomAttendee> gameRoomAttendeeList = gameRoomAttendeeRepository.findByGameRoom(gameRoom);
return gameRoomAttendeeList;
}
public List<GameRoomAttendee> findAttendeeByRoomId(Long roomId) {
return gameRoomAttendeeRepository.findByGameRoom_GameRoomId(roomId);
}
// 멤버 Id로 참가자 객체 가져오기
public GameRoomAttendee findAttendeeByMemberId(Long memberId) {
return gameRoomAttendeeRepository.findByMember_Id(memberId).orElseThrow(
()-> new CustomException(NOT_FOUND_ATTENDEE)
);
}
//////////////TODO GameStartSet 관련
// RoomId로 GameStartSet 객체 찾아오기
public GameStartSet findGameStartSetByRoomId(Long roomId) {
return gameStartSetRepository.findByRoomId(roomId).orElseThrow(
()-> new CustomException(GAME_SET_NOT_FOUND)
);
}
//////////////TODO 댓글 관련
// 키워드 랜덤으로 4개 가지고 오기
public List<Keyword> findTop4KeywordByCategory(String category) {
List<Keyword> keywordList = keywordRepository.findTop4ByCategory(category);
return keywordList;
}
// 키워드 랜덤으로 3개 가지고 오기
public List<Keyword> findTop3KeywordByCategory(String category) {
List<Keyword> keywordList = keywordRepository.findTop3ByCategory(category);
return keywordList;
}
//////////////TODO Reward 관련
public List<Reward> findAllReward(Member member) {
List<Reward> rewardList = rewardReposiroty.findByMember(member);
return rewardList;
}
}
- 게임 도메인 관련 데이터를 조회(Read)하는 데 사용되는 서비스로, 데이터베이스에서 정보를 읽어오는 기능을 제공함
- 게임방 정보 조회
- findGameRoomByRoomId : 특정 ID를 가진 게임방을 찾아 반환함
- findGameRoomByRoomIdLock : 특정 ID를 가진 게임방을 찾되, 데이터베이스 락을 적용하여 동시성을 관리할 때 사용할 수 있음
- findGameRoomByPageable : 페이지 처리를 적용하여 게임방 목록을 조회함
- findGameRoomByContainingKeyword : 특정 키워드를 포함하는 게임방 이름으로 게임방을 조회함
- 게임방 참가자 정보 조회
- findAttendeeByMember : 특정 멤버 객체를 기반으로 게임방 참가자 정보를 조회함
- findAttendeeByGameRoom : 특정 게임방 객체를 기반으로 해당 게임방의 참가자 목록을 조회함
- findAttendeeByRoomId : 특정 게임방 ID를 기반으로 참가자 목록을 조회함
- findAttendeeByMemberId : 특정 멤버 ID로 게임방 참가자 객체를 조회함
- findAttendeeByMember : 특정 멤버 객체를 기반으로 게임방 참가자 정보를 조회함
- 게임 설정 정보 조회
- findGameStartSetByRoomId : 특정 게임방 ID로 게임 설정 정보(GameStartSet)를 조회함
- 키워드 정보 조회
- findTop4KeywordByCategory : 특정 카테고리에 대해 랜덤으로 4개의 키워드를 조회함
- findTop3KeywordByCategory : 특정 카테고리에 대해 랜덤으로 3개의 키워드를 조회함
- 보상 정보 조회
- findAllReward : 특정 멤버에 대한 모든 보상 정보를 조회함
Dto
RequestDto/GameDto
package com.example.namoldak.dto.RequestDto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
// 기능 : 게임 정답 Dto
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class GameDto {
private String answer;
private String nickname;
}
- 게임에서 사용자가 제출한 정답과 닉네임을 전달하기 위한 데이터 전송 객체
- 클라이언트로부터 게임과 관련된 요청을 처리할 때 사용되며, 요청 데이터를 담아 애플리케이션 내부로 전달하는 역할을 수행함
- 이 구조는 게임 로직 처리에 필요한 사용자 데이터를 명확하고 효율적으로 전달하기 위해 설계 됨
- 필드 정의
- answer : 사용자가 게임에서 입력한 정답 문자열을 저장함
- nickname : 사용자의 닉네임을 저장함
- Lombok 어노테이션 사용
RequestDto/GameRoomRequestDto
package com.example.namoldak.dto.RequestDto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
// 기능 : 게임룸 생성시 사용하는 Dto
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class GameRoomRequestDto {
private String gameRoomName;
private String gameRoomPassword;
}
- 게임룸 생성 시 클라이언트로부터 받는 데이터를 담는 역할을 하는 데이터 전송 객체(DTO)임, 이 클래스는 게임룸 생성에 필요한 정보를 구조화하여 관리함
- 사용자의 요청을 애플리케이션 서버에서 처리할 때 필요한 데이터를 캡슐화하여 전달하는데 사용되며, 이를 통해 게임룸 생성 과정에서 요구되는 정보를 효율적으로 관리하고 처리할 수 있음
- 필드 정의
- gameRoomName : 생성할 게임룸의 이름을 나타냄
gameRoomPassword : 게임룸에 접근하기 위해 필요한 비밀번호를 나타내며, 이는 옵셔널하게 사용될 수 있어 비공개 방을 생성할 때 필요함
- gameRoomName : 생성할 게임룸의 이름을 나타냄
- Lombok 어노테이션 사용
RequestDto/WebSocketMessage
package com.example.namoldak.dto.RequestDto;
import lombok.Getter;
// 기능 : 프론트에서 받는 시그널링용 Message
@Getter
public class WebSocketMessage {
private String sender;
private String type;
private String data;
private Long roomId;
private String nickname;
private String receiver;
private Object offer;
private Object answer;
private Object candidate;
private Object sdp;
}
- 프론트엔드로부터 받는 시그널링 정보를 처리하기 위한 DTO(Data Transfer Object)임, 이 클래스는 웹소켓 통신 중에 교환되는 메시지의 구조를 정의하며, WebRTC 연결 설정과 관련된 다양한 데이터를 포함함
- 웹소켓을 통한 실시간 통신에서 중요한 역할을 수행하며, WebRTC 기반의 어플리케이션에서 피어 간의 연결 설정과 데이터 교환을 가능하게 하는 데 사용됨
- 필드 정의
- sender : 메시지를 보낸 사용자의 식별자
- type : 메시지의 유형을 나타내며, 예를 들어 offer, answer, candidate 등이 있음
- data : 메시지와 관련된 추가 데이터를 나타냄
- roomId : 메시지가 속한 방의 식별자
- nickname : 메시지를 보낸 사용자의 닉네임
- receiver : 메시지를 받을 대상의 식별
- offer : WebRTC 연결에서 사용하는 SDP(Session Description Protocol) 초기 제안을 나타냄
- answer : offer에 대한 응답으로, WebRTC 연결에서 사용하는 SDP 정보
- candidate : ICE(Interactive Connectivity Establishment) 후보자 정보를 나타냄
- sdp : 세션 기술 프로토콜 관련 정보
- sender : 메시지를 보낸 사용자의 식별자
ResponseDto/GameRoomResponseDto
package com.example.namoldak.dto.ResponseDto;
import lombok.Builder;
import lombok.Getter;
import java.util.List;
// 기능 : 게임룸 정보 Response Dto
@Builder
@Getter
public class GameRoomResponseDto {
private Long id;
private String roomName;
private String roomPassword;
private String owner;
private boolean status;
private int memberCnt;
private List<MemberResponseDto> member;
}
- 게임룸 정보를 클라이언트에 응답할 대 사용되는 데이터 전송 객체(DTO)임, 이 클래스는 게임룸의 상세 정보를 구조화하여 클라이언트에 전달하는 역할을 수행함
- 게임룸 관련 요청에 대한 응답 데이터를 캡슐화하여, 클라이언트에 필요한 게임룸의 상태 및 참가 멤버 정보를 효과적으로 전달함
- 프론트엔드에서 게임룸의 상태를 사용자에게 표시하거나, 게임룸 관련 정보를 처리하는 데 사용될 수 있ㅇ므
- 필드 정의
- id : 게임룸의 고유 식별자
- roomName : 게임룸의 이름
- roomPassword : 게임룸의 비밀번호로, 비공개 방인 경우 사용
- owner : 게임룸의 소유자(방장)입니다
- status : 게임룸의 현재 상태(예: 활성화/비활성화)를 나타냄
- memberCnt : 게임룸에 참가한 멤버의 수
- member : 게임룸에 참가한 멤버들의 정보 목록으로, MemberResponseDto의 리스트 형태임
- id : 게임룸의 고유 식별자
- Lombok 어노테이션 사용
ResponseDto/GameRoomResponseListDto
package com.example.namoldak.dto.ResponseDto;
import lombok.Getter;
import java.util.List;
// 기능 : 게임룸 List 응답 Dto
@Getter
public class GameRoomResponseListDto {
private int totalPage;
List<GameRoomResponseDto> gameRoomResponseDtoList;
public GameRoomResponseListDto(int totalPage, List<GameRoomResponseDto> gameRoomResponseDtoList) {
this.totalPage = totalPage;
this.gameRoomResponseDtoList = gameRoomResponseDtoList;
}
}
- 클라이언트에 전달되는 게임룸 목록 정보를 담는 응답 DOT(Data Transfer Object)임, 이 클래스는 게임룸 리스트와 관련된 데이터를 구조화하여 전달하는 역할을 수행함
- 게임룸 정보를 페이지화하여 관리하고, 이를 클라이언트에게 효율적으로 전달하기 위한 목적으로 사용됨, 클라이언트는 게임룸의 리스트와 전체 페이지 수를 알 수 있으며, 사용자 인터페이스에서 이 정보를 바탕으로 사용자에게 게임룸 데이터를 표시할 수 있음
- 필드 정의
- totalPage : 전체 페이지 수를 나타냄, 페이징 처리된 응답에서 사용자에게 전체 데이터의 페이지 수를 알려주는 데 사용
- gameRoomResponseDtoList : GameRoomResponseDto 객체의 리스트로, 각 게임룸의 상세 정보를 포함한다
- 생성자
- GameRoomResponseListDto(int totalPage, List<GameRoomResponseDto> gameRoomResponseDtoList) : 전체 페이지 수와 게임룸 상세 정보 리스트를 매개변수로 받아 객체를 초기화함
ResponseDto/WebSocketResponseMessage
package com.example.namoldak.dto.ResponseDto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
// 기능 : 프론트에 응답하는 시그널링용 Message
@Builder
@Getter
@JsonInclude(JsonInclude.Include.NON_NULL)
@NoArgsConstructor
@AllArgsConstructor
public class WebSocketResponseMessage {
private String sender;
private String senderNickName;
private String type;
private String data;
private Long roomId;
private List<String> allUsers;
private Map<String, String> allUsersNickNames;
private String receiver;
private Object offer;
private Object answer;
private Object candidate;
private Object sdp;
}
- 웹소켓 통신을 통해 프론트엔드에 응답하는 시그널링 메시지를 정의하는 DTO(Data Transfer Object)임, 이 클래스는 WebRTC 통신을 시그널링 정보와 그외 필요한 데이터를 전달하는 데 사용됨
- 필드 정의
- sender : 메시지를 보낸 사용자의 식별자
- senderNickName : 메시지를 보낸 사용자의 닉네임
- type : 메시지의 유형(예: offer, answer, candidate)임
- data : 메시지와 관련된 추가 데이터
- roomId : 메시지가 속한 방의 식별자
- allUsers : 방에 있는 모든 사용자의 식별자 리스트
- allUsersNickNames : 방에 있는 모든 사용자의 닉네임과 식별자를 매핑한 정보
- receiver : 메시지를 받을 대상의 식별자
- offer, answer, candidate, sdp: WebRTC 연결에 사용되는 각종 시그널링 데이터
- sender : 메시지를 보낸 사용자의 식별자
- Lombok 어노테이션 사용
- JsonInclude 어노테이션 사용
- @JsonInclude(JsonInclude.Include.NON_NULL): JSON으로 직렬화할 때 null 값이 아닌 필드만 포함시키도록 설정함, 이를 통해 불필요한 null 값의 전송을 방지하고, 데이터 전송을 최적화함
ResponseDto/VictoryDto
package com.example.namoldak.dto.ResponseDto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
// 기능 : 위너와 루저 정보를 전달하는 Dto
@Getter
@NoArgsConstructor
public class VictoryDto {
List<String> winner = new ArrayList<>();
List<String> loser = new ArrayList<>();
public void setWinner(String winner){
this.winner.add(winner);
}
public void setLoser(String loser){
this.loser.add(loser);
- 게임 결과에서 승리자(winner)와 패배자(loser)의 정보를 전달하기 위한 데이터 전송 객체(DTO)
- 게임 종료 시 승리자와 패배자 목록을 쉽게 관리하고 전달하기 위해 사용됨, 이 DTO를 통해 게임 결과 관련 정보를 클라이언트나 다른 서비스 컴포넌트에 효과적으로 전달할 수 있음
- 필드 정의
- winner : 승리자 목록을 나타내는 String 타입의 리스트입니다. 여러 승리자를 관리할 수 있도록 ArrayList로 초기화되어 있습니다.
- loser : 패배자 목록을 나타내는 String 타입의 리스트입니다. 여러 패배자를 관리할 수 있도록 ArrayList로 초기화되어 있습니다.
- winner : 승리자 목록을 나타내는 String 타입의 리스트입니다. 여러 승리자를 관리할 수 있도록 ArrayList로 초기화되어 있습니다.
- 메서드 정의
- setWinner(String winner) : 승리자 리스트에 새로운 승리자를 추가합니다. 파라미터로 받은 승리자의 이름을 리스트에 추가하는 기능을 수행합니다.
- setLoser(String loser) : 패배자 리스트에 새로운 패배자를 추가합니다. 파라미터로 받은 패배자의 이름을 리스트에 추가하는 기능을 수행합니다.
- setWinner(String winner) : 승리자 리스트에 새로운 승리자를 추가합니다. 파라미터로 받은 승리자의 이름을 리스트에 추가하는 기능을 수행합니다.
- Lombok 어노테이션 사용
Repository
GameRoomAttendeeRepository
package com.example.namoldak.repository;
import com.example.namoldak.domain.GameRoom;
import com.example.namoldak.domain.GameRoomAttendee;
import com.example.namoldak.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import javax.transaction.Transactional;
import java.util.List;
import java.util.Optional;
// 기능 : 게임룸에 들어간 유저 정보 레포
public interface GameRoomAttendeeRepository extends JpaRepository<GameRoomAttendee, Long> {
List<GameRoomAttendee> findByGameRoom(GameRoom gameRoom); // 게임룸 객체로 안에 있는 멤버들 전부 조회
Optional<GameRoomAttendee> findByMember(Member member); // 멤버 객체로 참가자 정보 조회
Optional<GameRoomAttendee> findByMember_Id(Long memberId); // 멤버 ID로 참가자 정보 조회
List<GameRoomAttendee> findByGameRoom_GameRoomId(Long gameRoomId); // 게임룸 ID로 안에 있는 멤버 전부 조회
@Transactional
void deleteAllByMember(Member member); // 멤버 객체로 삭제
boolean existsByMember(Member member); // 멤버 존재 여부 확인
}
- 위 인터페이스는 GameRoomAttendee 엔티티에 대한 데이터 접근을 위한 JPA 레포지토리
- JpaRepository 인터페이스를 상속받아 기본적인 CRUD 작업을 위한 메서드뿐만 아니라, 필요한 커스텀 쿼리 메서드를 추가로 제공함
- 이를 통해 게임룸과 관련된 참가자 데이터에 대한 관리가 용이해짐
- 게임룸 참가자 조회
- findByGameRoom(GameRoom gameRoom) : 특정 게임룸에 참여하고 있는 모든 멤버를 조회
- findByMember(Member member) : 특정 멤버에 해당하는 게임룸 참가자 정보를 조회
- findByMember_Id(Long memberId) : 멤버 ID를 기준으로 게임룸 참가자 정보를 조회
- findByGameRoom_GameRoomId(Long gameRoomId) : 게임룸 ID를 통해 해당 게임룸의 모든 참가자 정보를 조회
- findByGameRoom(GameRoom gameRoom) : 특정 게임룸에 참여하고 있는 모든 멤버를 조회
- 참가자 정보 삭제
- deleteAllByMember(Member member) : 특정 멤버와 관련된 게임룸 참가자 정보를 삭제함, @Transactional 어노테이션을 사용하여 데이터의 일관성을 유지
- 참가자 존재 여부 확인
- existsByMember(Member member) : 특정 멤버가 게임룸 참가자로 존재하는지 여부를 확인
GameRoomRepository
package com.example.namoldak.repository;
import com.example.namoldak.domain.GameRoom;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import javax.persistence.LockModeType;
import java.util.Optional;
// 기능 : 게임룸 레포
public interface GameRoomRepository extends JpaRepository<GameRoom, Long> {
Page<GameRoom> findAll(Pageable pageable); // 게임룸 전체 조회 페이징처리
Page<GameRoom> findByGameRoomNameContaining(Pageable pageable, String keyword); // 게임룸 페이징 처리 + 검색 기능
Optional<GameRoom> findByGameRoomId(Long gameRoomId); // 게임룸 단건 조회
@Lock(LockModeType.PESSIMISTIC_WRITE) // 동시 접속에 대한 충돌 방지용 비관적 락
@Query("select b from GameRoom b where b.gameRoomId = :gameRoomId")
Optional<GameRoom> findByGameRoomId2(Long gameRoomId); // 게임룸 단건 조회
}
- GameRoom 엔티티에 대한 데이터 접근을 위한 JPA 레포지토리, 이 인터페이스는 게임룸 데이터에 대한 기본적인 CRUD 작업 및 특정 기능을 위한 메서드를 제공
- 게임룸 데이터에 대한 접근 및 관리가 용이해지며, 페이징 처리, 검색 기능, 동시성 제어 등의 추가적인 요구 사항을 충족할 수 있음
- 기본 조회 기능
- findAll(Pageable pageable) : 게임룸 전체를 페이지 처리하여 조회함, 이 메소드는 대규모 데이터셋을 효과적으로 처리하기 위해 페이징을 사용함
- findByGameRoomNameContaining(Pageable pageable, String keyword) : 게임룸 이름에 특정 키워드가 포함되어 있는지 검색하고, 결과를 페이지 처리하여 반환함
- findAll(Pageable pageable) : 게임룸 전체를 페이지 처리하여 조회함, 이 메소드는 대규모 데이터셋을 효과적으로 처리하기 위해 페이징을 사용함
- 단건 조회 기능
- findByGameRoomId(Long gameRoomId) : 특정 ID를 가진 게임룸을 조회하고, 결과는 Optional로 반환되어, 해당 게임룸이 존재하지 않을 경우 쉽게 처리할 수 있음
- 비관적 락을 통한 동시성 제어
- findByGameRoomId2(Long gameRoomId) :
- 특정 ID를 가진 게임룸을 비관적 락(PESSIMISTIC_WRITE)을 사용하여 조회합니다.
- 이는 동시에 여러 트랜잭션이 같은 레코드에 접근할 때 발생할 수 있는 충돌을 방지하기 위해 사용됩니다.
- 쿼리 메소드에 @Lock 어노테이션을 사용하여 비관적 락을 설정합니다.
- findByGameRoomId2(Long gameRoomId) :
GameStartSetRepository
package com.example.namoldak.repository;
import com.example.namoldak.domain.GameStartSet;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
// 기능 : 게임 시작시 저장되는 스타트셋 레포
public interface GameStartSetRepository extends JpaRepository<GameStartSet, Long> {
Optional<GameStartSet> findByRoomId(Long roomId); // 게임룸 ID로 스타트 셋 찾기
void deleteByRoomId(Long roomId); // 게임룸 ID로 스타트 셋 지우기
}
- GameStartSet 엔티티에 대한 데이터 접근을 위한 JPA 레포지토리, 게임 시작 시 사용되는 설정 정보(스타트겟)에 대한 데이터베이스 작업을 수행함
- 게임 관련 설정 정보의 저장, 조회, 삭제 등의 데이터베이스 작업을 쉽게 처리할 수 있으며, 게임룸과 관련된 로직을 구현하는 데 필요한 핵심 데이터 작업을 지원함
- 스타트셋 조회
- findByRoomId(Long roomId) : 특정 게임룸 ID에 해당하는 게임 스타트셋 정보를 조회합니다. 조회 결과는 Optional로 반환되어, 해당 스타트셋이 존재하지 않을 경우 쉽게 처리할 수 있습니다.
- 스타트셋 삭제
- deleteByRoomId(Long roomId) : 특정 게임룸 ID에 해당하는 게임 스타트셋 정보를 삭제합니다. 이 메소드는 게임룸이 종료되거나 스타트셋 정보가 더 이상 필요하지 않을 때 사용됩니다.
SessionRepository
package com.example.namoldak.repository;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
// 기능 : 웹소켓에 필요한 세션 정보를 저장, 관리 (싱글톤)
@Slf4j
@Component
@NoArgsConstructor
public class SessionRepository {
private static SessionRepository sessionRepository;
// 세션 저장 1) clientsInRoom : 방 Id를 key 값으로 하여 방마다 가지고 있는 Client들의 session Id 와 session 객체를 저장
private final Map<Long, Map<String, WebSocketSession>> clientsInRoom = new ConcurrentHashMap<>();
// 세션 저장 2) roomIdToSession : 참가자들 각각의 데이터로 session 객체를 key 값으로 하여 해당 객체가 어느방에 속해있는지를 저장
private final Map<WebSocketSession, Long> roomIdToSession = new HashMap<>();
// 세션 저장 3) nicknamesInRoom : 참가자들의 세션 Id와 닉네임을 저장
private final Map<String, String> nicknamesInRoom = new HashMap<>();
// Session 데이터를 공통으로 사용하기 위해 싱글톤으로 구현
public static SessionRepository getInstance(){
if(sessionRepository == null){
synchronized (SessionRepository.class){
sessionRepository = new SessionRepository();
}
}
return sessionRepository;
}
// 해당 방의 ClientList 조회
public Map<String, WebSocketSession> getClientList(Long roomId) {
return clientsInRoom.get(roomId);
}
// 해당 방 존재 유무 조회
public boolean hasRoom(Long roomId){
return clientsInRoom.containsKey(roomId);
}
// 해당 session이 어느방에 있는지 조회
public Long getRoomId(WebSocketSession session){
return roomIdToSession.get(session);
}
// 세션을 저장 및 수정해서 저장
public void addClientInNewRoom(Long roomId, WebSocketSession session) {
Map<String, WebSocketSession> newClient = new HashMap<>();
newClient.put(session.getId(), session);
clientsInRoom.put(roomId, newClient);
}
// 끊어진 Client session 하나만 지우고 다시 저장
public void deleteClient(Long roomId, WebSocketSession session) {
Map<String, WebSocketSession> clientList = clientsInRoom.get(roomId);
String removeKey = "";
for(Map.Entry<String, WebSocketSession> oneClient : clientList.entrySet()){
if(oneClient.getKey().equals(session.getId())){
removeKey = oneClient.getKey();
}
}
clientList.remove(removeKey);
// 끊어진 세션을 제외한 나머지 세션들을 다시 저장
clientsInRoom.put(roomId, clientList);
}
// 하나의 Client session 정보만 추가하기
public void addClient(Long roomId, WebSocketSession session) {
clientsInRoom.get(roomId).put(session.getId(), session);
}
// 방 정보 모두 삭제 (방 폭파시 연계 작동)
public void deleteAllclientsInRoom(Long roomId){
clientsInRoom.remove(roomId);
}
// roomIdToSession에서 동일한 roomId에 해당하는 session을 모두 조회
public Map<WebSocketSession, Long> searchRooIdToSessionList(Long roomId){
return roomIdToSession.entrySet()
.stream()
.filter(entry -> entry.getValue() == roomId)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
// session을 key로 roomIdToSession에 이 세션이 어느방에 속해 있는 지 저장
public void saveRoomIdToSession(WebSocketSession session, Long roomId) {
roomIdToSession.put(session, roomId);
}
// session을 key로 roomIdToSession에서 해당 세션 정보 삭제
public void deleteRoomIdToSession(WebSocketSession session) {
roomIdToSession.remove(session);
}
// session Id로 닉네임 정보 조회
public String getNicknameInRoom(String sessionId) {
return this.nicknamesInRoom.get(sessionId);
}
// session Id를 키로 닉네임 정보 저장
public void addNicknameInRoom(String sessionId, String nickname) {
this.nicknamesInRoom.put(sessionId, nickname);
}
// session Id를 키로 닉네임 정보 삭제
public void deleteNicknameInRoom(String sessionId) {
this.nicknamesInRoom.remove(sessionId);
}
}
- 웹소켓 세션 정보를 저장하고 관리하는 싱글톤 구조의 레포지토리, 웹 소켓 통신 중에 사용자 세션을 효과적으로 관리하기 위해 사용 됨
- 웹소켓 통신 과정에서 다루어야 하는 다양한 세션 정보를 효율적으로 관리할 수 있으며, 실시간 통신 어플리케이션에서 필요한 세션 관리 로직을 캡슐화함
- 세션 및 방 정보 관리
- 각 방에 참여하는 클라이언트의 세션 정보와, 세션 별로 어느 방에 속해 있는지의 정보를 관리함
- 사용자의 닉네임과 세션 ID를 연결하는 맵을 통해 사용자 정보를 관리함
- 싱글톤 구현
- getInstance( ) : SessionRepository 인스턴스의 글로벌 접근 지점, 인스턴스가 없으면 생성하고, 있으면 기존 인스턴스를 반환
- 세션 정보 조작 메서드
- addClientInNewRoom, deleteClient, addClient : 특정 방에 클라이언트 세션을 추가하거나 삭제하는 기능을 제공
- deleteAllclientsInRoom : 특정 방의 모든 클라이언트 세션 정보를 삭제
- 조회 메서드
- getClientList, hasRoom, getRoomId, searchRooIdToSessionList : 특정 조건에 따라 저장된 세션 정보를 조회
- 세션 및 닉네임 정보 관리
- saveRoomIdToSession, deleteRoomIdToSession, getNicknameInRoom, addNicknameInRoom, deleteNicknameInRoom : 세션에 해당하는 방 ID를 저장하거나 삭제하고, 세션 ID에 해당하는 닉네임 정보를 관리
Controller
GameController
package com.example.namoldak.controller;
import com.example.namoldak.dto.RequestDto.GameDto;
import com.example.namoldak.service.GameService;
import com.example.namoldak.util.GlobalResponse.GlobalResponseDto;
import com.example.namoldak.util.GlobalResponse.ResponseUtil;
import com.example.namoldak.util.GlobalResponse.code.StatusCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
// 기능 : 게임 진행 관련 주요 서비스들을 컨트롤
@Slf4j
@RequiredArgsConstructor
@RestController
public class GameController {
private final GameService gameService;
// 게임 시작
@MessageMapping("/game/{roomId}/start")
public ResponseEntity<GlobalResponseDto> gameStart(@DestinationVariable Long roomId,
GameDto gameDto) {
gameService.gameStart(roomId, gameDto);
return ResponseUtil.response(StatusCode.GAME_START);
}
// 건너뛰기
@MessageMapping("/game/{roomId}/skip")
public void gameSkip(GameDto gameDto,
@DestinationVariable Long roomId) {
gameService.gameSkip(gameDto, roomId);
}
// 발언권 부여
@MessageMapping("/game/{roomId}/spotlight")
public void spotlight(
@DestinationVariable Long roomId) {
log.info("스포트라이트 - 게임방 아이디 : {}", roomId);
gameService.spotlight(roomId);
}
// 정답
@MessageMapping("/game/{roomId}/answer")
public void gameAnswer(@DestinationVariable Long roomId,
@RequestBody GameDto gameDto) {
gameService.gameAnswer(roomId, gameDto);
}
// 게임 끝내기
@MessageMapping("/game/{roomId}/endGame")
public void endGame(@DestinationVariable Long roomId) {
gameService.endGame(roomId);
}
}
- 게임 진행과 관련된 주요 서비스를 제어하는 컨트롤러, Spring의 메시지 매핑을 활용하여 웹소켓을 통한 실시간 게임 서비스를 제공함
- @MessageMapping 어노테이션을 사용하여 웹소켓을 통한 메시지 라우팅을 구현하며, 각 게임 액션에 대한 요청을 적절한 서비스 로직으로 연결하여 처리함, 이를 통해 사용자는 웹소켓 기반의 인터렉티브한 게임 경험을 할 수 있음
- 게임 시작(gameStart)
- /game/{roomId}/start 엔드포인트를 통해 게임 시작 명령을 처리합니다.
- roomId를 URL 경로 변수로 받고, GameDto를 메시지 본문으로 받아 게임 서비스의 gameStart 메소드를 호출합니다.
- /game/{roomId}/start 엔드포인트를 통해 게임 시작 명령을 처리합니다.
- 게임 건너뛰기(gameSkip)
- /game/{roomId}/skip 엔드포인트에서 건너뛰기 액션을 처리합니다.
- roomId와 GameDto를 받아 게임 서비스의 gameSkip 메소드를 호출합니다.
- 발언권 부여(spotlight)
- /game/{roomId}/spotlight 엔드포인트를 통해 특정 사용자에게 발언권을 부여하는 기능을 처리합니다.
- roomId만 받고, 게임 서비스의 spotlight 메소드를 호출합니다.
- 정답 제출(gameAnswer)
- /game/{roomId}/answer 엔드포인트를 통해 사용자의 정답을 처리합니다.
- roomId와 GameDto를 받아 게임 서비스의 gameAnswer 메소드를 호출합니다.
- 게임 종료(endGame)
- /game/{roomId}/endGame 엔드포인트에서 게임 종료 처리를 합니다.
- roomId를 받아 게임 서비스의 endGame 메소드를 호출합니다.
GameRoomController
package com.example.namoldak.controller;
import com.example.namoldak.dto.RequestDto.GameRoomRequestDto;
import com.example.namoldak.dto.ResponseDto.GameRoomResponseListDto;
import com.example.namoldak.service.GameRoomService;
import com.example.namoldak.util.GlobalResponse.GlobalResponseDto;
import com.example.namoldak.util.GlobalResponse.ResponseUtil;
import com.example.namoldak.util.GlobalResponse.code.StatusCode;
import com.example.namoldak.util.security.UserDetailsImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
// 기능 : 게임룸 관련 CRUD 컨트롤
@Slf4j
@RequiredArgsConstructor
@RestController
public class GameRoomController {
private final GameRoomService gameRoomService;
// 게임룸 생성
@PostMapping("/rooms")
public ResponseEntity<Map<String, String>> makeGameRoom(@AuthenticationPrincipal UserDetailsImpl userDetails,
@RequestBody GameRoomRequestDto gameRoomRequestDto) {
return ResponseUtil.response(gameRoomService.makeGameRoom(userDetails.getMember(), gameRoomRequestDto));
}
// 게임룸 전체조회 (페이징 처리)
@GetMapping("/rooms") // '/rooms?page=1'
public ResponseEntity<GameRoomResponseListDto> mainPage(@PageableDefault(size = 4, sort = "gameRoomId", direction = Sort.Direction.DESC) Pageable pageable) {
return ResponseUtil.response(gameRoomService.mainPage(pageable));
}
// 게임룸 키워드 조회
@GetMapping("/rooms/search") // '/rooms/search?keyword=검색어'
public GameRoomResponseListDto searchGame(@PageableDefault(size = 4, sort = "gameRoomId", direction = Sort.Direction.DESC) Pageable pageable, String keyword) {
return gameRoomService.searchGame(pageable, keyword);
}
// 게임룸 입장
@PostMapping("/rooms/{roomId}")
public ResponseEntity<Map<String, String>> enterGame(@PathVariable Long roomId,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
return ResponseUtil.response(gameRoomService.enterGame(roomId, userDetails.getMember()));
}
// 게임룸 입장 검증
@PostMapping("/rooms/{roomId}/verify")
public void enterVerify(@PathVariable Long roomId,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
gameRoomService.enterVerify(roomId, userDetails);
}
// 게임룸 나가기
@DeleteMapping("/rooms/{roomId}/exit")
public ResponseEntity<GlobalResponseDto> roomExit(@PathVariable Long roomId,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
gameRoomService.roomExit(roomId, userDetails.getMember());
return ResponseUtil.response(StatusCode.EXIT_SUCCESS);
}
// 게임룸 방장 조회하기
@GetMapping("/rooms/{roomId}/ownerInfo")
public ResponseEntity<Map<String, String>> ownerInfo(@PathVariable Long roomId) {
return ResponseUtil.response(gameRoomService.ownerInfo(roomId));
}
}
- 게임룸 관련 CRUD 작업을 수행하는 REST 컨트롤러, 각 메서드는 HTTP 요청을 처리하고, GameRoomService를 통해 비즈니스 로직을 실행한 후 응답을 반환함
- 컨트롤러는 RESTful 원칙을 따르며, 각 엔드포인트는 게임룸 관련 작업을 명확하게 수행하고 ResponseUtil.response() 메소드를 통해 일관된 응답 구조를 제공합니다.
- 게임룸 생성(makeGameRoom)
- @PostMapping("/rooms") : 사용자로부터 게임룸 생성 요청을 받아 처리합니다. 사용자의 상세 정보와 게임룸 정보가 담긴 DTO를 매개변수로 받습니다.
- 게임룸 전체 조회(mainPage)
- @GetMapping("/rooms") : 페이징 처리된 게임룸 목록을 조회합니다. Pageable 객체를 사용하여 페이징과 정렬을 처리합니다.
- 게임룸 키워드 조회(searchGame)
- @GetMapping("/rooms/search") : 키워드를 포함하는 게임룸을 조회합니다. 검색어와 페이징 정보를 매개변수로 받습니다.
- 게임룸 입장(enterGame)
- @PostMapping("/rooms/{roomId}") : 특정 게임룸에 사용자를 입장시킵니다. 게임룸 ID와 사용자의 상세 정보를 매개변수로 받습니다.
- 게임룸 입장 검증(enterVerify)
- @PostMapping("/rooms/{roomId}/verify") : 특정 게임룸에 사용자의 입장을 검증합니다. 게임룸 ID와 사용자의 상세 정보를 매개변수로 받습니다.
- 게임룸 나가기 (roomExit)
- @DeleteMapping("/rooms/{roomId}/exit") : 사용자가 특정 게임룸에서 나가는 작업을 처리합니다. 게임룸 ID와 사용자의 상세 정보를 매개변수로 받습니다.
- 게임룸 방장 정보 조회 (ownerInfo)
- @GetMapping("/rooms/{roomId}/ownerInfo") : 특정 게임룸의 방장 정보를 조회합니다. 게임룸 ID를 매개변수로 받습니다.
Service
GameRoomService
package com.example.namoldak.service;
import com.example.namoldak.domain.*;
import com.example.namoldak.domainModel.GameCommand;
import com.example.namoldak.domainModel.GameQuery;
import com.example.namoldak.domainModel.MemberCommand;
import com.example.namoldak.domainModel.MemberQuery;
import com.example.namoldak.dto.RequestDto.GameRoomRequestDto;
import com.example.namoldak.dto.ResponseDto.GameRoomResponseDto;
import com.example.namoldak.dto.ResponseDto.GameRoomResponseListDto;
import com.example.namoldak.dto.ResponseDto.MemberResponseDto;
import com.example.namoldak.repository.SessionRepository;
import com.example.namoldak.util.GlobalResponse.CustomException;
import com.example.namoldak.util.security.UserDetailsImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import static com.example.namoldak.util.GlobalResponse.code.StatusCode.*;
// 기능 : 게임룸 서비스
@Slf4j
@RequiredArgsConstructor
@Service
public class GameRoomService {
// 의존성 주입
private final GameService gameService;
private final MemberQuery memberQuery;
private final MemberCommand memberCommand;
private final GameQuery gameQuery;
private final GameCommand gameCommand;
private final SessionRepository sessionRepository = SessionRepository.getInstance();
// 게임룸 전체 조회
@Transactional
public GameRoomResponseListDto mainPage(Pageable pageable) {
// DB에 저장된 모든 Room들을 리스트형으로 저장 + 페이징 처리
Page<GameRoom> rooms = gameQuery.findGameRoomByPageable(pageable);
// DB에 저장된 모든 Room들을 리스트로 가져와
List<GameRoom> roomList = gameQuery.findAllGameRoomList();
// 필요한 키값들을 반환하기 위해서 미리 Dto 리스트 선언
List<GameRoomResponseDto> gameRoomList = new ArrayList<>();
for (GameRoom room : rooms){
// 모든 Room들이 모여있는 rooms에서 하나씩 추출 -> Room 객체 활용해서 GameRoomMember DB에서 찾은 후 리스트에 저장
List<GameRoomAttendee> gameRoomAttendeeList = gameQuery.findAttendeeByGameRoom(room);
// 필요한 키값들을 반환하기 위해서 미리 Dto 리스트 선언
List<MemberResponseDto> memberList = new ArrayList<>();
for (GameRoomAttendee gameRoomAttendee : gameRoomAttendeeList) {
// GameRoomMember에 저장된 멤버 아이디로 DB 조회 후 데이터 저장
Member eachMember = memberQuery.findMemberById(gameRoomAttendee.getMember().getId());
// MemberResponseDto에 빌더 방식으로 각각의 데이터 값 넣어주기
MemberResponseDto memberResponseDto = MemberResponseDto.builder()
.memberId(eachMember.getId())
.email(eachMember.getEmail())
.nickname(eachMember.getNickname())
.build();
// 완성된 Dto 리스트에 추가
memberList.add(memberResponseDto);
}
// 그냥 Member가 아니라 GameRoomMember를 담아야 하기 때문에 GameRoomResponseDto로 다시 한번 감쌈
GameRoomResponseDto gameRoomResponseDto = GameRoomResponseDto.builder()
.id(room.getGameRoomId())
.roomName(room.getGameRoomName())
.roomPassword(room.getGameRoomPassword())
.member(memberList)
.memberCnt(memberList.size())
.owner(room.getOwner())
.status(room.isStatus())
.build();
// memberList에 데이터가 있다면 gameRoomList에 gameRoomResponseDto 추가
// for문 끝날 때 까지 반복
if (!memberList.isEmpty()) {
gameRoomList.add(gameRoomResponseDto);
}
}
int totalPage = rooms.getTotalPages();
return new GameRoomResponseListDto(totalPage, gameRoomList);
}
// 게임룸 생성
@Transactional
public Map<String, String> makeGameRoom(Member member, GameRoomRequestDto gameRoomRequestDto) {
// 게임방 만든 횟수 추가
member.updateMakeRoom(1L);
memberCommand.saveMember(member);
// 빌더 활용해서 GameRoom 엔티티 데이터 채워주기
GameRoom gameRoom = GameRoom.builder()
.gameRoomName(gameRoomRequestDto.getGameRoomName())
.gameRoomPassword(gameRoomRequestDto.getGameRoomPassword())
.owner(member.getNickname())
.status(true)
.build();
// DB에 데이터 저장
gameCommand.saveGameRoom(gameRoom);
// 생성자로 gameRoom, member 데이터를 담은 GameRoomMember 객체 완성
GameRoomAttendee gameRoomAttendee = new GameRoomAttendee(gameRoom, member);
// GameRoomMember DB에 해당 데이터 저장
gameCommand.saveGameRoomAttendee(gameRoomAttendee);
// data에 데이터를 담아주기 위해 HashMap 생성
Map<String, String> roomInfo = new HashMap<>();
// 앞에 키 값에 뒤에 밸류 값을 넣어줌
roomInfo.put("gameRoomName", gameRoom.getGameRoomName());
roomInfo.put("roomId", Long.toString(gameRoom.getGameRoomId()));
roomInfo.put("gameRoomPassword", gameRoom.getGameRoomPassword());
roomInfo.put("owner", gameRoom.getOwner());
roomInfo.put("status", String.valueOf(gameRoom.isStatus()));
return roomInfo;
}
// 게임룸 입장
@Transactional
public Map<String, String> enterGame(Long roomId, Member member) {
// roomId로 DB에서 데이터 찾아와서 담음
GameRoom enterGameRoom = gameQuery.findGameRoomByRoomIdLock(roomId);
// 방의 상태가 false면 게임이 시작 중이거나 가득 찬 상태이기 때문에 출입이 불가능
if (!enterGameRoom.isStatus()) {
// 뒤로 넘어가면 안 되니까 return으로 호다닥 끝내버림
throw new CustomException(ALREADY_PLAYING);
}
// 입장하려는 게임방을 이용해서 GameRoomMember DB에서 유저 정보 전부 빼와서 리스트형에 저장 (입장 정원 확인 용도)
List<GameRoomAttendee> gameRoomAttendeeList = gameQuery.findAttendeeByGameRoom(enterGameRoom);
// 만약 방에 4명이 넘어가면
if (gameRoomAttendeeList.size() > 3) {
// 입장 안 된다고 입구컷
throw new CustomException(CANT_ENTER);
}
// 멤버가 방에 입장한 횟수 1개 증가
member.updateEnterGame(1L);
memberCommand.saveMember(member);
// for문으로 리스트에서 gameRoomMember 하나씩 빼주기
for (GameRoomAttendee gameRoomAttendee : gameRoomAttendeeList) {
// gameRoomMember에서 얻은 유저 아이디로 Member 객체 저장
Member member1 = memberQuery.findMemberById(gameRoomAttendee.getMember().getId());
// 현재 들어가려는 유저의 ID와 게임에 들어가있는 멤버의 ID가 똑같으면 입구컷 해버림
if (member.getId().equals(member1.getId())) {
// return new PrivateResponseBody(StatusCode.MEMBER_DUPLICATED, "이미 입장해있닭!!");
throw new CustomException(MEMBER_DUPLICATED);
}
}
GameRoomAttendee gameRoomAttendee = new GameRoomAttendee(enterGameRoom, member);
// DB에 데이터 저장
gameCommand.saveGameRoomAttendee(gameRoomAttendee);
Map<String, Object> contentSet = new HashMap<>();
contentSet.put("owner", enterGameRoom.getOwner());
contentSet.put("memberCnt", gameRoomAttendeeList.size());
contentSet.put("enterComment", roomId + "번 방에" + String.valueOf(member.getId()) + "님이 입장하셨습니닭!");
// 게임 메세지 전송
gameService.sendGameMessage(roomId, GameMessage.MessageType.ENTER, contentSet, null, member.getNickname());
// 해시맵으로 데이터 정리해서 보여주기
Map<String, String> roomInfo = new HashMap<>();
roomInfo.put("gameRoomName", enterGameRoom.getGameRoomName());
roomInfo.put("roomId", String.valueOf(enterGameRoom.getGameRoomId()));
roomInfo.put("owner", enterGameRoom.getOwner());
roomInfo.put("status", String.valueOf(enterGameRoom.isStatus()));
return roomInfo;
}
// 비정상 게임룸 접속자 방지
public void enterVerify(Long roomId, UserDetailsImpl userDetails) {
// 비회원일 경우 에러 메세지 보내기
if (userDetails == null) {
throw new CustomException(INVALID_TOKEN);
}
// 검증을 위한 카운트 미리 선언
int cnt = 0;
// 해당 방의 모든 참가자들 리스트로 저장
List<GameRoomAttendee> gameRoomAttendeeList = gameQuery.findAttendeeByRoomId(roomId);
// 참가자의 닉네임과 접속한 사람의 닉네임이 동일하면 cnt 1개씩 올림
for (GameRoomAttendee gameRoomAttendee : gameRoomAttendeeList) {
if (userDetails.getMember().getNickname().equals(gameRoomAttendee.getMemberNickname())){
cnt++;
}
}
// cnt가 1이 아닐 경우 뭔가가 오류가 있기 때문에 들어갈 수 없다고 에러 메세지 띄워줌
if (cnt != 1) {
throw new CustomException(BAD_REQUEST);
}
}
// 게임룸 키워드 조회
public GameRoomResponseListDto searchGame(Pageable pageable, String keyword) {
// 게임룸 이름을 keyword(검색어)로 잡고 조회 + 페이징 처리
Page<GameRoom> rooms = gameQuery.findGameRoomByContainingKeyword(pageable, keyword);
if(rooms.isEmpty()){
throw new CustomException(NOT_EXIST_ROOMS);
}
List<GameRoomResponseDto> gameRoomList = new ArrayList<>();
for (GameRoom room : rooms) {
// 게임룸에 입장해 있는 멤버 조회
List<GameRoomAttendee> gameRoomAttendeeList = gameQuery.findAttendeeByGameRoom(room);
List<MemberResponseDto> memberList = new ArrayList<>();
for (GameRoomAttendee gameRoomAttendee : gameRoomAttendeeList){
Member eachMember = memberQuery.findMemberById(gameRoomAttendee.getMember().getId());
// 멤버로부터 필요한 정보인 id, email, nickname만 Dto에 담아주기
MemberResponseDto memberResponseDto = MemberResponseDto.builder()
.memberId(eachMember.getId())
.email(eachMember.getEmail())
.nickname(eachMember.getNickname())
.build();
// 담긴 정보 저장
memberList.add(memberResponseDto);
}
// 게임룸에 필요한 정보를 Dto에 담아주기
GameRoomResponseDto gameRoomResponseDto = GameRoomResponseDto.builder()
.id(room.getGameRoomId())
.roomName(room.getGameRoomName())
.roomPassword(room.getGameRoomPassword())
.member(memberList)
.memberCnt(memberList.size())
.owner(room.getOwner())
.status(room.isStatus())
.build();
// 방에 멤버가 1명 이상이라면, 담아줬던 데이터 저장하기
if (!memberList.isEmpty()) {
gameRoomList.add(gameRoomResponseDto);
}
}
// 저장된 정보가 담긴 리스트를 반환
int totalPage = rooms.getTotalPages();
return new GameRoomResponseListDto(totalPage, gameRoomList);
}
// 방 나가기
@Transactional
public void roomExit(Long roomId, Member member) {
// 나가려고 하는 방 정보 DB에서 불러오기
GameRoom enterGameRoom = gameQuery.findGameRoomByRoomId(roomId);
// 나가려고 하는 GameRoomMember를 member 객체로 DB에서 조회
GameRoomAttendee gameRoomAttendee = gameQuery.findAttendeeByMember(member);
// 위에서 구한 GameRoomMemeber 객체로 DB 데이터 삭제
gameCommand.deleteGameRoomAttendee(gameRoomAttendee);
// 게임방에 남아있는 유저들 구하기
List<GameRoomAttendee> existGameRoomAttendee = gameQuery.findAttendeeByGameRoom(enterGameRoom);
// 남아있는 유저의 수가 0명이라면 게임방 DB에서 데이터 삭제
if (existGameRoomAttendee.size() == 0) {
// 혼자 있을 때 방에서 나간 횟수 증가
member.updateSoloExit(1L);
memberCommand.saveMember(member);
gameCommand.deleteGameRoom(enterGameRoom);
// 게임 채팅방도 삭제해줌
sessionRepository.deleteAllclientsInRoom(roomId);
}
// 게임이 시잓된 상태에서 나갔을 경우
if (!enterGameRoom.isStatus()){
// 게임을 끝내버림
gameService.forcedEndGame(roomId, member.getNickname());
}
// 방을 나갈 경우의 알림 문구와 나간 이후의 방 인원 수를 저장하기 위한 해시맵
Map<String, Object> contentSet = new HashMap<>();
contentSet.put("memberCnt", existGameRoomAttendee.size());
contentSet.put("alert", member.getNickname() + " 님이 방을 나가셨습니닭!");
// 누가 나갔는지 알려줄 메세지 정보 세팅
gameService.sendGameMessage(roomId, GameMessage.MessageType.LEAVE, contentSet, null, member.getNickname());
// 만약에 나간 사람이 그 방의 방장이고 남은 인원이 0명이 아닐 경우에
if (member.getNickname().equals(enterGameRoom.getOwner()) && !existGameRoomAttendee.isEmpty()){
// 남은 사람들의 수 만큼 랜덤으로 돌려서 나온 멤버 ID
String nextOwner = existGameRoomAttendee.get((int) (Math.random() * existGameRoomAttendee.size())).getMemberNickname();
enterGameRoom.setOwner(nextOwner);
gameService.sendGameMessage(roomId, GameMessage.MessageType.NEWOWNER, null, null, nextOwner);
}
}
// 방장 정보 조회
public Map<String, String> ownerInfo(Long roomId) {
// 전달받은 roomId로 DB 조회 후 저장
GameRoom enterRoom = gameQuery.findGameRoomByRoomId(roomId);
// 방에서 방장의 닉네임을 저장
String ownerNickname = enterRoom.getOwner();
// 닉네임을 통해서 유저 객체를 불러온 후에 ID를 저장
Member member = memberQuery.findMemberByNickname(ownerNickname);
String ownerId = member.getId().toString();
// 데이터를 전달할 해시맵 생성 후 넣어주기
Map<String, String> ownerInfo = new HashMap<>();
ownerInfo.put("ownerId", ownerId);
ownerInfo.put("ownerNickname", ownerNickname);
return ownerInfo;
}
// signalHandler에서 세션 끊김을 감지했을 때 게임방에서 참가자 정보를 정리
public void exitGameRoomAboutSession(String nickname, Long roomId) {
Member member = memberQuery.findMemberByNickname(nickname);
List<GameRoomAttendee> gameRoomAttendeeList = gameQuery.findAttendeeByRoomId(roomId);
for(GameRoomAttendee gameRoomAttendee : gameRoomAttendeeList) {
if(nickname.equals(gameRoomAttendee.getMemberNickname())){
roomExit(roomId, member);
}
}
}
}
- 게임룸 관련 서비스 로직을 담당하는 스프링 서비스 컴포넌트, 이 클래스는 게임룸의 생성, 조회, 입장, 퇴장 및 관련 정보 처리를 위한 다양한 메서드를 제공함
- 게임룸 생성(makeGameRoom)
- 새 게임룸을 생성하고, 생성 정보를 데이터베이스에 저장합니다.
- 게임룸 전체 조회 (mainPage)
- 페이징 처리된 게임룸 목록을 조회하고, 관련 정보를 DTO로 변환하여 반환합니다.
- 게임룸 키워드 조회 (searchGame)
- 특정 키워드를 포함하는 게임룸을 페이징 처리하여 조회합니다.
- 게임룸 입장 (enterGame)
- 사용자를 게임룸에 입장시키며, 게임룸이 가득 찼거나 게임이 이미 시작된 경우 예외 처리를 수행합니다.
- 게임룸 입장 검증 (enterVerify)
- 사용자의 게임룸 입장을 검증합니다.
- 게임룸 퇴장 (roomExit)
- 사용자를 게임룸에서 퇴장시키고, 필요한 데이터 조정 및 클린업을 수행합니다.
- 방장 정보 조회 (ownerInfo)
- 게임룸의 방장 정보를 조회합니다.
- 세션 끊김 시 게임룸 퇴장 처리 (exitGameRoomAboutSession)
- 웹소켓 세션 종료 시 해당 사용자를 게임룸에서 자동으로 퇴장 처리합니다.
- 각 메서드는 비즈니스 로직을 수행하기 위해 GameQuery, GameCommand, MemberQuery, MemberCommand, GameService, SessionRepository 등의 다른 컴포넌트와 협력하고 트랜잭션 관리는 @Transactional 어노테이션을 통해 수행되며, 이는 메서드 실행 도중 예외 발생 시 변경사항을 롤백하여 데이터 일관성을 보장함
GameService
package com.example.namoldak.service;
import com.example.namoldak.domain.*;
import com.example.namoldak.domainModel.GameCommand;
import com.example.namoldak.domainModel.GameQuery;
import com.example.namoldak.domainModel.MemberCommand;
import com.example.namoldak.domainModel.MemberQuery;
import com.example.namoldak.dto.RequestDto.GameDto;
import com.example.namoldak.dto.ResponseDto.VictoryDto;
import com.example.namoldak.util.GlobalResponse.CustomException;
import com.example.namoldak.util.GlobalResponse.code.StatusCode;
import com.example.namoldak.util.converter.GameStartSetConverter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import static com.example.namoldak.util.GlobalResponse.code.StatusCode.*;
// 기능 : 게임 진행 서비스
@Slf4j
@RequiredArgsConstructor
@Service
public class GameService {
private final SimpMessageSendingOperations messagingTemplate;
private final RewardService rewardService;
private final MemberQuery memberQuery;
private final MemberCommand memberCommand;
private final GameQuery gameQuery;
private final GameCommand gameCommand;
private final GameStartSetConverter gameStartSetConverter;
// 게임 시작
@Transactional
public void gameStart(Long roomId, GameDto gameDto) {
// 현재 입장한 게임방의 정보를 가져옴
GameRoom gameRoom = gameQuery.findGameRoomByRoomId(roomId);
// 게임 시작은 방장만이 할 수 있음
if (!gameDto.getNickname().equals(gameRoom.getOwner())) {
throw new CustomException(StatusCode.UNAUTHORIZE);
}
// 게임방에 입장한 멤버들 DB(GameRoomMember)에서 가져오기
List<GameRoomAttendee> gameRoomAttendees = gameQuery.findAttendeeByGameRoom(gameRoom);
// 게임방의 상태를 start 상태로 업데이트
gameRoom.setStatus(false);
// 랜덤으로 뽑은 키워드의 카테고리
String category = Category.getRandom().name();
// 같은 카테고리를 가진 키워드 리스트 만들기
List<Keyword> keywordList = getRandomKeyword(gameRoomAttendees.size(), category);
// 웹소켓으로 방에 참가한 인원 리스트 전달을 위한 리스트 (닉네임만 필요하기에 닉네임만 담음)
List<String> memberNicknameList = getNicknameList(gameRoomAttendees);
//게임룸 멤버한테 키워드 배당
Map<String, String> keywordToMember = matchKeywordToMember(keywordList, memberNicknameList);
GameStartSet gameStartSet = GameStartSet.builder()
.roomId(roomId)
.category(category)
.keywordToMember(gameStartSetConverter.getStrFromMap(keywordToMember))
.round(0)
.spotNum(0)
.winner("")
.gameStartTime(System.currentTimeMillis())
.build();
// StartSet 저장
gameCommand.saveGameStartSet(gameStartSet);
GameStartSet searchOneGameStartSet = gameQuery.findGameStartSetByRoomId(roomId);
log.info("카테고리 : " + searchOneGameStartSet.getCategory());
for (String memberNick : memberNicknameList) {
log.info("키워드 : " + keywordToMember.get(memberNick));
}
// 웹소켓으로 전달드릴 content 내용
Map<String, Object> startSet = new HashMap<>();
startSet.put("category", gameStartSet.getCategory()); // 카테고리
startSet.put("keyword", gameStartSetConverter.getMapFromStr(gameStartSet.getKeywordToMember())); // 키워드
startSet.put("memberList", memberNicknameList); // 방에 존재하는 모든 유저들
startSet.put("startAlert", "총 8라운드닭! 초록색으로 하이라이트된 사람만 말할 수 있고 다른 사람들은 마이크 기능이 제한되니까 채팅으로 알려주면 된닭!");
sendGameMessage(roomId, GameMessage.MessageType.START, startSet, null, null);
}
// 건너뛰기
@Transactional
public void gameSkip(GameDto gameDto, Long roomId) {
String msg = gameDto.getNickname() + "님이 건너뛰기를 선택하셨습니다.";
sendGameMessage(roomId, GameMessage.MessageType.SKIP, msg, null, gameDto.getNickname());
}
@Transactional
public void spotlight(Long roomId) {
GameRoom playRoom = gameQuery.findGameRoomByRoomId(roomId);
// 해당 게임룸의 게임셋을 조회
GameStartSet gameStartSet = gameQuery.findGameStartSetByRoomId(roomId);
// 게임이 진행이 불가한 상태라면 초기화 시켜야 함
if (playRoom.isStatus()) { // false : 게임이 진행 중, true : 게임 시작 전
gameStartSet.setRound(0);
gameStartSet.setSpotNum(0);
gameCommand.saveGameStartSet(gameStartSet);
}
// 유저들 정보 조회
List<GameRoomAttendee> memberListInGame = gameQuery.findAttendeeByGameRoom(playRoom);
// 라운드 진행 중
if (gameStartSet.getSpotNum() < memberListInGame.size()) {
// 현재 스포트라이트 받는 멤버
Member spotMember = memberQuery.findMemberById(memberListInGame.get(gameStartSet.getSpotNum()).getMember().getId());
// 메세지 알림
String msg = spotMember.getNickname() + "님의 차례입니닭!";
sendGameMessage(roomId, GameMessage.MessageType.SPOTLIGHT, msg, spotMember.getNickname(), spotMember.getNickname());
// 다음 차례로!
gameStartSet.setSpotNum(gameStartSet.getSpotNum() +1);
gameCommand.saveGameStartSet(gameStartSet);
} else if (gameStartSet.getSpotNum() == memberListInGame.size()) {
if (gameStartSet.getRound() < 7) {
// 한 라운드 종료, 라운드 +1 , 위치 정보 초기화
gameStartSet.setRound(gameStartSet.getRound() +1);
gameStartSet.setSpotNum(0);
gameCommand.saveGameStartSet(gameStartSet);
spotlight(roomId);
// 0번부터 시작이다
} else if (gameStartSet.getRound() == 7) {
// 메세지 알림
String msg = "너흰 전부 바보닭!!!";
sendGameMessage(roomId, GameMessage.MessageType.STUPID, msg, null, null);
forcedEndGame(roomId, null);
}
}
}
// 정답
@Transactional
public void gameAnswer(Long roomId, GameDto gameDto) {
// 모달창에 작성한 정답
String answer = gameDto.getAnswer().replaceAll(" ", "");
// gameStartSet 불러오기
GameStartSet gameStartSet = gameQuery.findGameStartSetByRoomId(roomId);
// 정답을 맞추면 게임 끝
if (gameStartSetConverter.getMapFromStr(gameStartSet.getKeywordToMember()).get(gameDto.getNickname()).equals(answer)){
// 정답자
gameStartSet.setWinner(gameDto.getNickname());
gameCommand.saveGameStartSet(gameStartSet);
// 메세지 알림
String msg = gameDto.getNickname() + "님이 작성하신" + answer + "은(는) 정답입니닭!";
sendGameMessage(roomId, GameMessage.MessageType.SUCCESS, msg, gameDto.getNickname(), null);
} else {
// 메세지 알림
String msg = gameDto.getNickname() + "님이 작성하신" + answer + "은(는) 정답이 아닙니닭!";
sendGameMessage(roomId, GameMessage.MessageType.FAIL, msg, gameDto.getNickname(), null);
}
}
// 게임 강제 종료
@Transactional
public void forcedEndGame(Long roomId, String nickname){
// 현재 게임방 정보 불러오기
GameRoom enterGameRoom = gameQuery.findGameRoomByRoomId(roomId);
// 메세지 알림
String msg = nickname == null ? "게임이 종료되었닭!!" : nickname + " 님이 방에서 탈주해서 강제 종료되었닭!!";
sendGameMessage(roomId, GameMessage.MessageType.FORCEDENDGAME, msg, null, null);
// DB에서 게임 셋팅 삭제
gameCommand.deleteGameStartSetByRoomId(roomId);
// 현재 방 상태 정보를 true로 변경
enterGameRoom.setStatus(true);
}
// 게임 정상 종료
@Transactional
public void endGame(Long roomId){
// 승리자와 패배자를 list로 반환할 DTO 생성
VictoryDto victoryDto = new VictoryDto();
// 방 게임셋 정보 불러오기
GameStartSet gameStartSet = gameQuery.findGameStartSetByRoomId(roomId);
// 현재 게임룸 데이터 불러오기
GameRoom enterGameRoom = gameQuery.findGameRoomByRoomId(roomId);
// 불러온 게임룸으로 들어간 GameRoomMember들 구하기
List<GameRoomAttendee> gameRoomAttendeeList = gameQuery.findAttendeeByGameRoom(enterGameRoom);
// 닉네임을 구하기 위해서 멤버 객체를 담을 리스트 선언
List<Member> memberList = new ArrayList<>();
// for문으로 하나씩 빼서 DB 조회 후 List에 넣어주기
for (GameRoomAttendee gameRoomAttendee : gameRoomAttendeeList){
Member member = memberQuery.findMemberById(gameRoomAttendee.getMember().getId());
// 멤버 총 게임 횟수 증가
member.updateTotalGame(1L);
memberCommand.saveMember(member);
memberList.add(member);
rewardService.createTotalGameReward(member);
}
Long startTime = gameStartSet.getGameStartTime();
Long currentTime = System.currentTimeMillis();
Long playTime = (currentTime - startTime) / 1000;
// member의 닉네임이 정답자와 같지 않을 경우 전부 Loser에 저장하고 같을 경우 Winner에 저장
for (Member member : memberList){
if (!member.getNickname().equals(gameStartSet.getWinner())){
victoryDto.setLoser(member.getNickname());
// 멤버 패배 기록 추가
member.updateLoseNum(1L);
member.updatePlayTime(playTime);
memberCommand.saveMember(member);
rewardService.createLoseReward(member);
} else {
victoryDto.setWinner(member.getNickname());
// 멤버 승리 기록 추가
member.updateWinNum(1L);
member.updatePlayTime(playTime);
memberCommand.saveMember(member);
rewardService.createWinReward(member);
}
}
// 메세지 알림
sendGameMessage(roomId, GameMessage.MessageType.ENDGAME, gameStartSet.getKeywordToMember(), null, null);
// DB에서 게임 셋팅 삭제
gameCommand.deleteGameStartSetByRoomId(roomId);
// 현재 방 상태 정보를 true로 변경
enterGameRoom.setStatus(true);
}
// 게임 운영자 메세지 전송
public <T> void sendGameMessage(Long roomId, GameMessage.MessageType type, T Content, String nickname, String sender) {
String senderName = sender == null ? "양계장 주인" : sender;
GameMessage<T> gameMessage = new GameMessage<>();
gameMessage.setRoomId(Long.toString(roomId));
gameMessage.setType(type);
gameMessage.setSender(senderName);
gameMessage.setContent(Content);
gameMessage.setNickname(nickname);
messagingTemplate.convertAndSend("/sub/gameRoom/" + roomId, gameMessage);
}
// 키워드 생성
public List<Keyword> getRandomKeyword(int size, String category) {
// 같은 카테고리를 가진 키워드 리스트 만들기
List<Keyword> keywordList;
if (size == 4) {
// 참여 멤버가 4명 이라면, 랜덤으로 키워드 4장이 담긴 리스트를 만들어 준다.
keywordList = gameQuery.findTop4KeywordByCategory(category);
} else if (size == 3) {
// 참여 멤버가 3명 이라면, 랜덤으로 키워드 3장이 담긴 리스트를 만들어 준다.
keywordList = gameQuery.findTop3KeywordByCategory(category);
} else {
throw new CustomException(NOT_ENOUGH_MEMBER);
}
return keywordList;
}
// 키워드와 참가자 매칭
public Map<String, String> matchKeywordToMember(List<Keyword> keywordList, List<String> memberNicknameList) {
Map<String, String> keywordToMember = new HashMap<>();
//게임룸 멤버한테 키워드 배당
for (int i = 0; i < keywordList.size(); i++) {
keywordToMember.put(memberNicknameList.get(i), keywordList.get(i).getWord());
}
return keywordToMember;
}
// 방의 참가자들 닉네임
public List<String> getNicknameList(List<GameRoomAttendee> gameRoomAttendees) {
// 웹소켓으로 방에 참가한 인원 리스트 전달을 위한 리스트 (닉네임만 필요하기에 닉네임만 담음)
List<String> memberNicknameList = new ArrayList<>();
for (GameRoomAttendee gameRoomAttendee : gameRoomAttendees) {
memberNicknameList.add(gameRoomAttendee.getMemberNickname());
}
return memberNicknameList;
}
}
- 게임의 진행에 필요한 주요 서비스 로직을 처리함, 이 클래스에서는 게임의 시작, 진행, 종료, 메시지 전송 등 다양한 기능을 제공하며, 게임룸 관리와 플레이어 상호작용을 중개함
- 게임 시작 (gameStart)
게임 시작 조건을 확인하고, 게임룸의 상태를 업데이트합니다.
랜덤 키워드 배정 및 게임 시작 정보를 게임룸 참가자에게 전송합니다. - 건너뛰기 (gameSkip)
플레이어가 게임 내에서 건너뛰기 액션을 선택할 때 처리합니다. - 발언권 부여 (spotlight)
특정 플레이어에게 게임 내 발언권을 부여하고 게임 진행 상태를 관리합니다. - 정답 처리 (gameAnswer)
플레이어의 정답 제출을 처리하고, 정답 여부에 따라 게임 상태를 업데이트합니다. - 게임 강제 종료 (forcedEndGame)
게임 강제 종료 시 필요한 처리를 수행합니다. - 게임 정상 종료 (endGame)
게임의 정상 종료 시 점수 집계 및 보상 처리를 수행합니다. - 메시지 전송 (sendGameMessage)
게임 상태 변경, 플레이어 액션, 시스템 메시지 등을 웹소켓을 통해 참가자에게 전송합니다. - 키워드 생성 및 매칭 (getRandomKeyword, matchKeywordToMember)
게임룸 참가자에게 배정될 랜덤 키워드를 생성하고 매칭합니다. - 닉네임 리스트 생성 (getNicknameList)
게임룸 참가자의 닉네임 리스트를 생성합니다.
'항해 99 > Spring' 카테고리의 다른 글
Call by Reference (0) | 2024.04.04 |
---|---|
WebSocket 활용 웹 게임 구현 (0) | 2024.04.03 |
WebSocket - STOMP 2 (0) | 2024.03.30 |
WebSocket - STOMP 1 (1) | 2024.03.29 |
WebSocket - SockJS (0) | 2024.03.28 |