diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f07ca97..94af24a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,7 +7,22 @@ on: pull_request: branches: - server - + workflow_dispatch: # 수동 실행 옵션 추가 + inputs: + environment: + description: 'Deployment environment' + required: false + default: 'development' + type: choice + options: + - development + - staging + - production + custom_tag: + description: 'Custom image tag (optional)' + required: false + type: string + jobs: build-and-push: runs-on: ubuntu-latest diff --git a/snapit-server/src/main/java/com/snapit/backend/snapit_server/config/SecurityConfig.java b/snapit-server/src/main/java/com/snapit/backend/snapit_server/config/SecurityConfig.java index 56e6c9a..6ad4ea7 100644 --- a/snapit-server/src/main/java/com/snapit/backend/snapit_server/config/SecurityConfig.java +++ b/snapit-server/src/main/java/com/snapit/backend/snapit_server/config/SecurityConfig.java @@ -3,6 +3,7 @@ import com.snapit.backend.snapit_server.security.jwt.JwtAuthenticationEntryPoint; import com.snapit.backend.snapit_server.security.jwt.JwtAuthenticationFilter; import com.snapit.backend.snapit_server.security.oauth2.OAuth2UserService; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; @@ -24,6 +25,9 @@ public class SecurityConfig { private final AuthenticationSuccessHandler oAuth2SuccessHandler; private final JwtAuthenticationFilter jwtAuthenticationFilter; private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + + @Value("${deployment.address}") + private String deploymentAddress; public SecurityConfig(OAuth2UserService oAuth2UserService, AuthenticationSuccessHandler oAuth2SuccessHandler, @@ -63,7 +67,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - configuration.addAllowedOrigin("http://localhost:5173"); // ✅ 요청을 허용할 Origin + configuration.addAllowedOrigin("http://localhost:5173"); // ✅ 로컬 개발 환경 + configuration.addAllowedOrigin(deploymentAddress); // ✅ 배포 환경 주소 configuration.addAllowedMethod("*"); // ✅ 모든 HTTP 메서드 허용 (GET, POST, PUT, DELETE 등) configuration.addAllowedHeader("*"); // ✅ 모든 헤더 허용 configuration.setAllowCredentials(true); // ✅ 쿠키 포함 요청 허용 (credentials: include) diff --git a/snapit-server/src/main/java/com/snapit/backend/snapit_server/controller/GameWebSocketController.java b/snapit-server/src/main/java/com/snapit/backend/snapit_server/controller/GameWebSocketController.java index b3c0a80..963d9b8 100644 --- a/snapit-server/src/main/java/com/snapit/backend/snapit_server/controller/GameWebSocketController.java +++ b/snapit-server/src/main/java/com/snapit/backend/snapit_server/controller/GameWebSocketController.java @@ -35,7 +35,7 @@ public GameWebSocketController(GameEnvService gameEnvService, GamePlayService ga @MessageMapping("/room/{roomUUID}/vote") public void vote(@DestinationVariable UUID roomUUID, @Payload VoteMessage voteMessage) { - + System.out.println("[투표 실행]-투표장소,roomUUID="+voteMessage.place()+roomUUID); gameEnvService.voteWithUUID(roomUUID, voteMessage); } @@ -45,6 +45,7 @@ public void vote(@DestinationVariable UUID roomUUID, public void score(@DestinationVariable UUID roomUUID, @Payload ScoreMessage scoreMessage, Principal principal) { + System.out.println("[점수 획득 실행]-email,roomUUID="+principal.getName()+","+roomUUID); if (GameType.PERSONAL.equals(scoreMessage.gameType())) { gamePlayService.addScore(roomUUID, principal.getName(), scoreMessage); } else if (GameType.COOPERATE.equals(scoreMessage.gameType())) { diff --git a/snapit-server/src/main/java/com/snapit/backend/snapit_server/controller/WebSocketController.java b/snapit-server/src/main/java/com/snapit/backend/snapit_server/controller/WebSocketController.java index 565dfb7..f1e56b0 100644 --- a/snapit-server/src/main/java/com/snapit/backend/snapit_server/controller/WebSocketController.java +++ b/snapit-server/src/main/java/com/snapit/backend/snapit_server/controller/WebSocketController.java @@ -2,19 +2,32 @@ import com.snapit.backend.snapit_server.domain.Room; import com.snapit.backend.snapit_server.domain.RoomCommand; +import com.snapit.backend.snapit_server.domain.enums.JoinResult; +import com.snapit.backend.snapit_server.dto.JoinMessage; import com.snapit.backend.snapit_server.dto.RoomCreateRequestDto; import com.snapit.backend.snapit_server.dto.RoomListMessage; +import com.snapit.backend.snapit_server.dto.UserListMessage; +import com.snapit.backend.snapit_server.dto.game.SimilarityResponseMessage; +import com.snapit.backend.snapit_server.dto.game.SimilarityResultMessage; import com.snapit.backend.snapit_server.service.GameEnvService; import com.snapit.backend.snapit_server.service.RoomService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.messaging.handler.annotation.DestinationVariable; -import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.messaging.handler.annotation.*; import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.security.core.Authentication; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.client.RestTemplate; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.security.Principal; +import java.util.List; +import java.util.Map; import java.util.UUID; @Controller @@ -47,15 +60,22 @@ public RoomListMessage createRoom(@Payload RoomCreateRequestDto dto, RoomCommand cmd = RoomCommand.fromRequest(dto); // 서비스 호출 및 반환 - return roomService.createRoom(cmd,principal.getName()); + return roomService.createRoom(cmd, principal.getName()); } // 방 퇴장 @MessageMapping("/room/{roomUUID}/leave") @SendTo("/topic/openrooms") public RoomListMessage leaveRoom(@DestinationVariable UUID roomUUID, - Principal principal){ - roomService.leaveRoom(roomUUID,principal.getName()); + Principal principal) { + roomService.leaveRoom(roomUUID, principal.getName()); + Room room = roomService.getRoom(roomUUID); + if(room != null) { + List userList = room.getUserList(); + messagingTemplate.convertAndSend("/topic/room/"+roomUUID, + new UserListMessage(new UserListMessage.Body(userList))); + + } return roomService.getAllRooms(); } @@ -68,5 +88,54 @@ public RoomListMessage startRoom(@DestinationVariable UUID roomUUID) { return roomService.getAllRooms(); } + // 방 입장 (MessageMapping) + @MessageMapping("/room/{roomUUID}/join") + @SendTo("/topic/openrooms") + public RoomListMessage joinRoom( + @DestinationVariable UUID roomUUID, + Principal principal) { + + String email = principal.getName(); + roomService.joinRoom(roomUUID, email); + Room room = roomService.getRoom(roomUUID); + if(room != null) { + List userList = room.getUserList(); + System.out.println("userList 보내기 전"+email); + messagingTemplate.convertAndSend("/topic/room/"+roomUUID, + new UserListMessage(new UserListMessage.Body(userList))); + + System.out.println("userList 보낸 후"+email); + } + System.out.println("room!=null 뒤 "+email); + return roomService.getAllRooms(); + } + private final RestTemplate restTemplate = new RestTemplate(); + + @MessageMapping("/room/{roomUUID}/similarity") + public void getSimilarity(@DestinationVariable UUID roomUUID, + @Header("first_word") String first_word, + @Header("second_word") String second_word, + Principal principal) { + String email = principal.getName(); + System.out.println("[유사도 계산] 이메일, UUID, first_word, second_word = " + + email + ", " + roomUUID + ", " + first_word + ", " + second_word); + + // 1. HTTP GET 요청 보내기 + String encodedFirst = URLEncoder.encode(first_word, StandardCharsets.UTF_8); + String encodedSecond = URLEncoder.encode(second_word, StandardCharsets.UTF_8); + + String url = "http://snap-it-word2vec.snapit-word2voc.svc.cluster.local:8000/similarity?first_word=" + encodedFirst + "&second_word=" + encodedSecond; + SimilarityResponseMessage similarityResult = restTemplate.getForObject(url, SimilarityResponseMessage.class); + // 2. WebSocket으로 결과 보내기 + + messagingTemplate.convertAndSend("/topic/room/" + roomUUID, + new SimilarityResultMessage( + new SimilarityResultMessage.Body( + email, + first_word, + second_word, + similarityResult.similarity() + ))); + } } \ No newline at end of file diff --git a/snapit-server/src/main/java/com/snapit/backend/snapit_server/dto/UserListMessage.java b/snapit-server/src/main/java/com/snapit/backend/snapit_server/dto/UserListMessage.java new file mode 100644 index 0000000..9486d50 --- /dev/null +++ b/snapit-server/src/main/java/com/snapit/backend/snapit_server/dto/UserListMessage.java @@ -0,0 +1,14 @@ +package com.snapit.backend.snapit_server.dto; + +import java.util.List; + +public record UserListMessage( + String header, + Body body + +) { + public UserListMessage(Body body) { + this("userList",body); + } + public record Body(List userList){} +} diff --git a/snapit-server/src/main/java/com/snapit/backend/snapit_server/dto/game/SimilarityResponseMessage.java b/snapit-server/src/main/java/com/snapit/backend/snapit_server/dto/game/SimilarityResponseMessage.java new file mode 100644 index 0000000..3232b4b --- /dev/null +++ b/snapit-server/src/main/java/com/snapit/backend/snapit_server/dto/game/SimilarityResponseMessage.java @@ -0,0 +1,4 @@ +package com.snapit.backend.snapit_server.dto.game; + +public record SimilarityResponseMessage(double similarity) { +} diff --git a/snapit-server/src/main/java/com/snapit/backend/snapit_server/dto/game/SimilarityResultMessage.java b/snapit-server/src/main/java/com/snapit/backend/snapit_server/dto/game/SimilarityResultMessage.java new file mode 100644 index 0000000..1c55ce3 --- /dev/null +++ b/snapit-server/src/main/java/com/snapit/backend/snapit_server/dto/game/SimilarityResultMessage.java @@ -0,0 +1,11 @@ +package com.snapit.backend.snapit_server.dto.game; + +public record SimilarityResultMessage( + String header, + Body body +) { + public SimilarityResultMessage(Body body) { + this("similarity",body); + } + public record Body(String email, String firstWord, String secondWord, Double similarity) {} +} diff --git a/snapit-server/src/main/java/com/snapit/backend/snapit_server/security/jwt/JwtAuthenticationFilter.java b/snapit-server/src/main/java/com/snapit/backend/snapit_server/security/jwt/JwtAuthenticationFilter.java index 88c4556..b9759cf 100644 --- a/snapit-server/src/main/java/com/snapit/backend/snapit_server/security/jwt/JwtAuthenticationFilter.java +++ b/snapit-server/src/main/java/com/snapit/backend/snapit_server/security/jwt/JwtAuthenticationFilter.java @@ -12,6 +12,8 @@ import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; /** *

JWT 토큰 부분 검증

* HTTP 요청 헤더에서 JWT 토큰 추출 @@ -23,31 +25,51 @@ @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtProvider jwtProvider; + private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); public JwtAuthenticationFilter(JwtProvider jwtProvider) { this.jwtProvider = jwtProvider; } + + private String getTimePrefix() { + return "[" + LocalDateTime.now().format(formatter) + "] "; + } @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException { String requestURI = req.getRequestURI(); - System.out.println("JwtAuthenticationFilter - 요청 URL: " + requestURI); + System.out.println(getTimePrefix() + "JwtAuthenticationFilter - 요청 URL: " + requestURI); + + // 쿠키 디버깅 + Cookie[] cookies = req.getCookies(); + if (cookies != null) { + System.out.println(getTimePrefix() + "JwtAuthenticationFilter - 쿠키 개수: " + cookies.length); + for (Cookie cookie : cookies) { + System.out.println(getTimePrefix() + "JwtAuthenticationFilter - 쿠키 발견: " + cookie.getName() + " (도메인: " + cookie.getDomain() + ", 경로: " + cookie.getPath() + ")"); + } + } else { + System.out.println(getTimePrefix() + "JwtAuthenticationFilter - 쿠키 없음"); + } + + // 헤더 디버깅 + String authHeader = req.getHeader("Authorization"); + System.out.println(getTimePrefix() + "JwtAuthenticationFilter - Authorization 헤더: " + authHeader); String token = resolveToken(req); if (token != null) { - System.out.println("JwtAuthenticationFilter - 토큰 발견, 검증 시작"); + System.out.println(getTimePrefix() + "JwtAuthenticationFilter - 토큰 발견, 검증 시작: " + token.substring(0, Math.min(10, token.length())) + "..."); if (jwtProvider.validateToken(token)) { // Spring Security 내부의 인증·인가 흐름을 단일 타입으로 일원화하기 위해 // UsernamePasswordAuthenticationToken을 사용 Authentication auth = jwtProvider.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(auth); - System.out.println("JwtAuthenticationFilter - 인증 성공: " + auth.getName()); + System.out.println(getTimePrefix() + "JwtAuthenticationFilter - 인증 성공: " + auth.getName()); } else { - System.out.println("JwtAuthenticationFilter - 토큰 검증 실패"); + System.out.println(getTimePrefix() + "JwtAuthenticationFilter - 토큰 검증 실패: 유효하지 않은 토큰"); } } else { - System.out.println("JwtAuthenticationFilter - 토큰 없음"); + System.out.println(getTimePrefix() + "JwtAuthenticationFilter - 토큰 없음"); } chain.doFilter(req, res); } @@ -58,6 +80,7 @@ private String resolveToken(HttpServletRequest req) { if (cookies != null) { for (Cookie cookie : cookies) { if ("accessToken".equals(cookie.getName())) { + System.out.println(getTimePrefix() + "JwtAuthenticationFilter - 쿠키에서 토큰 발견"); return cookie.getValue(); } } @@ -66,13 +89,14 @@ private String resolveToken(HttpServletRequest req) { // 2) URL 쿼리 파라미터에서 토큰 추출 시도 String tokenParam = req.getParameter("token"); if (tokenParam != null && !tokenParam.isEmpty()) { - System.out.println("JwtAuthenticationFilter - 토큰 파라미터 발견: " + tokenParam.substring(0, Math.min(10, tokenParam.length())) + "..."); + System.out.println(getTimePrefix() + "JwtAuthenticationFilter - 토큰 파라미터 발견: " + tokenParam.substring(0, Math.min(10, tokenParam.length())) + "..."); return tokenParam; } // 3) 헤더의 Bearer 토큰도 함께 처리하려면 아래 로직 유지 String bearer = req.getHeader("Authorization"); if (bearer != null && bearer.startsWith("Bearer ")) { + System.out.println(getTimePrefix() + "JwtAuthenticationFilter - 헤더에서 Bearer 토큰 발견"); return bearer.substring(7); } diff --git a/snapit-server/src/main/java/com/snapit/backend/snapit_server/security/jwt/principal/JwtProvider.java b/snapit-server/src/main/java/com/snapit/backend/snapit_server/security/jwt/principal/JwtProvider.java index 93014b6..167099a 100644 --- a/snapit-server/src/main/java/com/snapit/backend/snapit_server/security/jwt/principal/JwtProvider.java +++ b/snapit-server/src/main/java/com/snapit/backend/snapit_server/security/jwt/principal/JwtProvider.java @@ -17,8 +17,8 @@ public class JwtProvider { private final SecretKey secretKey ; - private final long expirationTime = 1000L * 60 * 60; // 1시간. - private final long refreshExpirationTime = 1000L * 60 * 60 * 24 * 7; + private final long expirationTime = 1000L * 60 * 60 * 24 * 30; // 30일 + private final long refreshExpirationTime = 1000L * 60 * 60 * 24 * 30; // 30일 // UserDetailsService 제거 diff --git a/snapit-server/src/main/java/com/snapit/backend/snapit_server/security/oauth2/handler/OAuth2SuccessHandler.java b/snapit-server/src/main/java/com/snapit/backend/snapit_server/security/oauth2/handler/OAuth2SuccessHandler.java index cab011c..fb42b80 100644 --- a/snapit-server/src/main/java/com/snapit/backend/snapit_server/security/oauth2/handler/OAuth2SuccessHandler.java +++ b/snapit-server/src/main/java/com/snapit/backend/snapit_server/security/oauth2/handler/OAuth2SuccessHandler.java @@ -10,6 +10,8 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; + +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseCookie; @@ -28,16 +30,12 @@ public class OAuth2SuccessHandler implements AuthenticationSuccessHandler { private final JwtProvider jwtProvider; - private final OAuth2UserService oauth2UserService; private final TokenRepository tokenRepository; - // public OAuth2SuccessHandler(JwtProvider jwtProvider, UserRepository userRepository) { - // this.jwtProvider = jwtProvider; - // this.userRepository = userRepository; - // } - public OAuth2SuccessHandler(JwtProvider jwtProvider, OAuth2UserService oauth2UserService, TokenRepository tokenRepository) { + + + public OAuth2SuccessHandler(JwtProvider jwtProvider, TokenRepository tokenRepository) { this.jwtProvider = jwtProvider; - this.oauth2UserService = oauth2UserService; this.tokenRepository = tokenRepository; } @@ -73,7 +71,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo ResponseCookie accessTokenCookie = ResponseCookie.from("accessToken", accessToken) .httpOnly(false) .path("/") - .maxAge(60 * 60) // 1시간 + .maxAge(60 * 60 * 24 * 30) // 30일 .sameSite("Strict") .build(); @@ -81,15 +79,14 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo ResponseCookie refreshTokenCookie = ResponseCookie.from("refreshToken", refreshToken) .httpOnly(false) .path("/api/token/refresh") // Refresh 엔드포인트에서만 사용 가능 - .maxAge(60 * 60 * 24 * 7) // 7일 + .maxAge(60 * 60 * 24 * 30) // 30일 .sameSite("Strict") .build(); // [쿠키] 이메일도 넣게끔 설정 ResponseCookie emailCookie = ResponseCookie.from("email", email) .httpOnly(false) // JS에서 접근 가능하게 하려면 false - .secure(true) .path("/") - .maxAge(Duration.ofDays(14)) + .maxAge(Duration.ofDays(30)) .build(); // [쿠키] 응답에 담기 response.addHeader(HttpHeaders.SET_COOKIE, accessTokenCookie.toString()); diff --git a/snapit-server/src/main/java/com/snapit/backend/snapit_server/service/GameEnvService.java b/snapit-server/src/main/java/com/snapit/backend/snapit_server/service/GameEnvService.java index 71742a4..edf9364 100644 --- a/snapit-server/src/main/java/com/snapit/backend/snapit_server/service/GameEnvService.java +++ b/snapit-server/src/main/java/com/snapit/backend/snapit_server/service/GameEnvService.java @@ -3,6 +3,7 @@ import com.snapit.backend.snapit_server.domain.Room; import com.snapit.backend.snapit_server.domain.enums.GameType; import com.snapit.backend.snapit_server.dto.game.*; +import lombok.RequiredArgsConstructor; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; @@ -15,6 +16,7 @@ import java.util.stream.Collectors; @Service +@RequiredArgsConstructor public class GameEnvService { private final SimpMessagingTemplate messagingTemplate; @@ -23,11 +25,6 @@ public class GameEnvService { private final Map> votes = new ConcurrentHashMap<>(); private final Map roomInfo = new ConcurrentHashMap<>(); - public GameEnvService(SimpMessagingTemplate messagingTemplate, GeminiService geminiService) { - this.messagingTemplate = messagingTemplate; - this.geminiService = geminiService; - } - // 게임 시작 public void gameInitiate(UUID roomUUID, Room room) { // 게임 시작 알림 @@ -47,16 +44,31 @@ public void makePlaceListAndSend(UUID roomUUID, int round) { // UUID 기반 투표 + 결과 전송/협동전도 한번에 처리 public void voteWithUUID(UUID roomUUID, VoteMessage voteMessage) { + System.out.println("[IF문 전]"); String place = voteMessage.place(); int round = voteMessage.round(); GameType gameType = voteMessage.gameType(); + System.out.println("roomUUID = " + roomUUID); votes.computeIfAbsent(roomUUID, k -> new CopyOnWriteArrayList<>()).add(place); + System.out.println("현재 투표 인원 = " + votes.get(roomUUID).size()); + System.out.println("최대 투표 인원 = " + roomInfo.get(roomUUID).getCurrentCapacity()); + System.out.println("게임 타입 = " + gameType); + System.out.println("라운드 = " + round); + if (GameType.PERSONAL.equals(gameType)) { if (round == 1 && votes.get(roomUUID).size() == roomInfo.get(roomUUID).getCurrentCapacity()) { + System.out.println("[IF문 후]"); String mostVoted = calculateVoteResultAndSend(roomUUID, round); + System.out.println("최다 투표 장소 = " + mostVoted); sendVoteResult(roomUUID, round, mostVoted); + + System.out.println("현재 투표 인원 = " + votes.get(roomUUID).size()); + System.out.println("최대 투표 인원 = " + roomInfo.get(roomUUID).getCurrentCapacity()); + System.out.println("게임 타입 = " + gameType); + System.out.println("라운드 = " + round); + } else if (round == 2) { String mostVoted = calculateVoteResultAndSend(roomUUID, round); sendVoteResult(roomUUID, round, mostVoted); diff --git a/snapit-server/src/main/java/com/snapit/backend/snapit_server/service/GamePlayService.java b/snapit-server/src/main/java/com/snapit/backend/snapit_server/service/GamePlayService.java index d6fb4b4..c66c2a6 100644 --- a/snapit-server/src/main/java/com/snapit/backend/snapit_server/service/GamePlayService.java +++ b/snapit-server/src/main/java/com/snapit/backend/snapit_server/service/GamePlayService.java @@ -7,6 +7,7 @@ import com.snapit.backend.snapit_server.dto.game.GameScoreInfoMessage; import com.snapit.backend.snapit_server.dto.game.ScoreMessage; import com.snapit.backend.snapit_server.dto.game.TimeOverMessage; +import lombok.RequiredArgsConstructor; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; @@ -17,6 +18,7 @@ import java.util.concurrent.ConcurrentHashMap; @Service +@RequiredArgsConstructor public class GamePlayService { private final Map> gameScoreInfo = new ConcurrentHashMap<>(); @@ -27,21 +29,25 @@ public class GamePlayService { private final SimpMessagingTemplate messagingTemplate; private final GameEnvService gameEnvService; - public GamePlayService(SimpMessagingTemplate messagingTemplate, GameEnvService gameEnvService) { - this.messagingTemplate = messagingTemplate; - this.gameEnvService = gameEnvService; - } // [PERSONAL] UUID 기준으로 gameInfo에서 GameScore를 찾아서 해당 유저 점수를 더함. 없으면 생성 후 더함. public void addScore(UUID roomUUID, String email, ScoreMessage scoreMessage) { + System.out.println("[점수 획득 실행]-email,roomUUID="+email+","+roomUUID); int round = scoreMessage.round(); int score = scoreMessage.score(); - + System.out.println("[점수 획득 실행]-round,score="+round+","+score); List scores = gameScoreInfo.get(roomUUID); if (scores == null) { + System.out.println("[점수 획득 실행]-scores가 null이므로 초기화"); addGameInfo(roomUUID); endCount.put(roomUUID, new ArrayList<>()); + scores = gameScoreInfo.get(roomUUID); + System.out.println("[점수 획득 실행]- gameScoreInfo 초기화 후 값 확인 = "+gameScoreInfo.get(roomUUID)); + for (GameScore gs : scores) { + System.out.println("[점수 획득 실행]- gameScoreInfo 초기화 후 값 확인 = "+gs.getEmail()+","+gs.getScore()+","+gs.getScore2()); + } + } // UUID가 키로 존재한다면, 모든 유저가 등록되어있음. @@ -50,10 +56,16 @@ public void addScore(UUID roomUUID, String email, ScoreMessage scoreMessage) { .findFirst() .orElse(null); + System.out.println("[점수 획득 실행]-UUID, gameScore Null 여부 = "+roomUUID+","+gameScore); + if (round == 1) { + System.out.println("[점수 획득 실행]-round가 1이므로 점수 업데이트"); gameScore.setScore(Math.max(score, gameScore.getScore())); + System.out.println("[점수 획득 실행]-획득 점수 score, 현재 최고 점수 = "+score+","+gameScore.getScore()); } else if (round == 2) { + System.out.println("[점수 획득 실행]-round가 2이므로 점수 업데이트"); gameScore.setScore2(Math.max(score, gameScore.getScore2())); + System.out.println("[점수 획득 실행]-획득 점수 score, 현재 최고 점수 = "+score+","+gameScore.getScore2()); } broadcastGameInfo(roomUUID, round, false, convertGameScoreListToUserInfoList(scores)); @@ -70,16 +82,19 @@ public void addCount(UUID roomUUID, ScoreMessage scoreMessage) { public void timeOver(UUID roomUUID, String email, TimeOverMessage timeOverMessage) { int round = timeOverMessage.round(); GameType gameType = timeOverMessage.gameType(); + System.out.println("[라운드 종료 실행]-round,email,gameType="+round+","+email+","+gameType); endCount.get(roomUUID).add(email); if (GameType.PERSONAL.equals(gameType)) { // 1라운드 종료시, 게임결과 발송 후 2번째 게임 투표 발송 if (round == 1 && endCount.get(roomUUID).size() == gameScoreInfo.get(roomUUID).size()) { + System.out.println("[라운드 종료 실행]-종료된 사람, 총 인원 = "+endCount.get(roomUUID).size()+","+gameScoreInfo.get(roomUUID).size()); broadcastGameInfo(roomUUID, round, true, convertGameScoreListToUserInfoList(gameScoreInfo.get(roomUUID))); gameEnvService.makePlaceListAndSend(roomUUID, 2); } // 2라운드 종료시, 게임결과 발송 후 키-벨류 정리 if (round == 2 && endCount.get(roomUUID).size() == 2 * gameScoreInfo.get(roomUUID).size()) { + System.out.println("[라운드 종료 실행]-종료된 사람, 총 인원 = "+endCount.get(roomUUID).size()+","+gameScoreInfo.get(roomUUID).size()); broadcastGameInfo(roomUUID, round, true, convertGameScoreListToUserInfoList(gameScoreInfo.get(roomUUID))); gameScoreInfo.remove(roomUUID); endCount.remove(roomUUID); @@ -89,12 +104,14 @@ public void timeOver(UUID roomUUID, String email, TimeOverMessage timeOverMessag // 1라운드 종료시, 게임결과 발송 후 counts 초기화 후 2번째 게임 투표 발송 if (round == 1 && endCount.get(roomUUID).size() == gameScoreInfo.get(roomUUID).size()) { + System.out.println("[라운드 종료 실행]-종료된 사람, 총 인원 = "+endCount.get(roomUUID).size()+","+gameScoreInfo.get(roomUUID).size()); broadcastFoundStuffAndRemain(roomUUID, round, true, "gameEnd"); counts.put(roomUUID, 0); gameEnvService.makePlaceListAndSend(roomUUID, 2); } // 2라운드 종료시, 게임결과 발송 후 키-벨류 정리. counts 초기화 if (round == 2 && endCount.get(roomUUID).size() == 2 * gameScoreInfo.get(roomUUID).size()) { + System.out.println("[라운드 종료 실행]-종료된 사람, 총 인원 = "+endCount.get(roomUUID).size()+","+gameScoreInfo.get(roomUUID).size()); broadcastFoundStuffAndRemain(roomUUID, round, true, "gameEnd"); counts.remove(roomUUID); gameScoreInfo.remove(roomUUID); @@ -115,6 +132,7 @@ private void broadcastFoundStuffAndRemain(UUID roomUUID, int round, boolean isEn // [PERSONAL] 특정 UUID로 gameScoreInfo broadcast private void broadcastGameInfo(UUID roomUUID, int round, boolean isEnd, List userInfoList) { + System.out.println("[게임 정보 발송 실행]-roomUUID,round,isEnd="+roomUUID+","+round+","+isEnd); messagingTemplate.convertAndSend("/topic/room/" + roomUUID, new GameScoreInfoMessage( new GameScoreInfoMessage.Body(round, isEnd, userInfoList) )); @@ -123,6 +141,7 @@ private void broadcastGameInfo(UUID roomUUID, int round, boolean isEnd, // [PERSONAL]scors->UserInfos private List convertGameScoreListToUserInfoList(List scores) { + System.out.println("[게임 정보 발송 실행]-score에서 UserInfo로 변환"); return scores.stream() .map(gs -> new GameScoreInfoMessage.UserInfo( gs.getEmail(), gs.getScore(), gs.getScore2())) diff --git a/snapit-server/src/main/java/com/snapit/backend/snapit_server/service/GeminiService.java b/snapit-server/src/main/java/com/snapit/backend/snapit_server/service/GeminiService.java index 4479176..ae2bec2 100644 --- a/snapit-server/src/main/java/com/snapit/backend/snapit_server/service/GeminiService.java +++ b/snapit-server/src/main/java/com/snapit/backend/snapit_server/service/GeminiService.java @@ -23,10 +23,10 @@ public List generatePlaceList(){ String placeListPrompt = "10~20대가 접근하기 쉬운 공간(현실에 존재하는 공간)을 6개 랜덤하게 선택해서, 아래 예시와 같은 json형식으로 반환해줘." + " 이때, 공간은 단순히 공간명을 의미해. 공간은 특정성(스타벅스, 맥도날드)를 띄지 말고, 추상적으로 표현(카페, 음식점) 해줘. " + - "학교, 대학교 안에 존재하는 공간을 2개 이상 포함해줘.\n" + + "학교, 대학교 안에 존재하는 공간을 2개 이상 포함해줘.답변은 한글단어-영어단어 쌍인 예시와 같은 형식으로 보내줘.\n" + "예시:\n" + "{\n" + - " \"placeList\": [\"카페\",\"마트\",\"영화관\",\"강의실\",\"거리\",\"도서관\"]\n" + + " \"placeList\": [\"카페-cafe\",\"마트-market\",\"영화관-theater\",\"강의실-classroom\",\"거리-street\",\"도서관-library\"]\n" + "}"; String url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite:generateContent?key=" + apiKey; @@ -111,7 +111,7 @@ public List generatePlaceList(){ public List generateStuffList(String place){ String stuffListPrompt = place +"에 있을법한 물건 10개를 랜덤하게 골라서\n" + - "key는 stuffList, value는 물건 이름 String 배열로 JSON형식으로 답해줘"; + "key는 stuffList, value는 물건 이름 한글-영어를 하나의 String으로 배열로 JSON형식으로 답해줘"; String url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite:generateContent?key=" + apiKey; // 1) 스키마 정의 (stuffList) diff --git a/snapit-server/src/main/java/com/snapit/backend/snapit_server/service/RoomService.java b/snapit-server/src/main/java/com/snapit/backend/snapit_server/service/RoomService.java index 8d9d021..b724c24 100644 --- a/snapit-server/src/main/java/com/snapit/backend/snapit_server/service/RoomService.java +++ b/snapit-server/src/main/java/com/snapit/backend/snapit_server/service/RoomService.java @@ -27,14 +27,15 @@ public RoomListMessage getAllRooms() { // 방 생성하기 @Transactional public RoomListMessage createRoom(RoomCommand cmd,String email) { + System.out.println("[방 생성]이메일, UUID 값 : " + email + ", " + cmd.roomUUID()); Room room = new Room( cmd.roomUUID(), cmd.title(), cmd.maxCapacity(), cmd.gameType() ); - room.getUserList().add(email); // 방 생성한 유저를 해당 방 멤버로 저장 rooms.put(cmd.roomUUID(), room); + System.out.println("[방 생성]이메일, UUID 값 : " + email + ", " + cmd.roomUUID() + " 방 생성 완료"); return getAllRoomListMessage(); } @@ -43,17 +44,21 @@ public RoomListMessage createRoom(RoomCommand cmd,String email) { @Transactional public JoinResult joinRoom(UUID roomUUID, String email) { AtomicReference res = new AtomicReference<>(JoinResult.ROOM_NOT_FOUND); - + System.out.println("[방 진입]이메일, UUID 값 : " + email + ", " + roomUUID); rooms.compute(roomUUID, (id, room) -> { if (room == null) {// 키가 애초에 없던 경우 + System.out.println("[방 진입]UUID 값 : " + roomUUID + " 방이 없습니다."); return null; } - if (room.isFull()) {// 꽉 찬 경우 + if (room.isFull()) {// 꽉 찬 경우 + System.out.println("[방 진입]UUID 값 : " + roomUUID + " 방이 꽉 찼습니다."); res.set(JoinResult.FULL_CAPACITY); return room; } room.upCurrentCapacity();// 정상 입장 + System.out.println("[방 진입]UUID 값 : " + roomUUID + " 방 인원 증가. 현재 인원 : " + room.getCurrentCapacity()); room.getUserList().add(email); + System.out.println("[방 진입]UUID 값 : " + roomUUID + " 방 인원 증가. 현재 인원 : " + room.getCurrentCapacity()); res.set(JoinResult.SUCCESS); return room; }); @@ -69,7 +74,7 @@ public void leaveRoom(UUID roomUUID,String email) { if (room == null) { return null; } - if (room.getCurrentCapacity() == 1) { + if (room.getCurrentCapacity() <= 1) { return null; // 방 삭제 } // 그 외 경우엔 그대로 유지 또는 수정 @@ -85,6 +90,12 @@ public Room gameStartandDeleteRoom(UUID roomUUID) { return rooms.remove(roomUUID); } + // UUID로 Room 찾아서 반환 + @Transactional + public Room getRoom(UUID roomUUID) { + return rooms.get(roomUUID); + } + private RoomListMessage getAllRoomListMessage() { var roomDtos = rooms.values().stream() .map(RoomDto::from) diff --git a/snapit-server/src/main/java/com/snapit/backend/snapit_server/service/TokenService.java b/snapit-server/src/main/java/com/snapit/backend/snapit_server/service/TokenService.java index 18ca3e5..936aa48 100644 --- a/snapit-server/src/main/java/com/snapit/backend/snapit_server/service/TokenService.java +++ b/snapit-server/src/main/java/com/snapit/backend/snapit_server/service/TokenService.java @@ -2,18 +2,16 @@ import com.snapit.backend.snapit_server.security.jwt.principal.JwtProvider; import com.snapit.backend.snapit_server.security.jwt.principal.TokenRepository; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service +@RequiredArgsConstructor public class TokenService { private final JwtProvider jwtProvider; private final TokenRepository tokenRepository; - public TokenService(JwtProvider jwtProvider, TokenRepository tokenRepository) { - this.jwtProvider = jwtProvider; - this.tokenRepository = tokenRepository; - } public TokenRefreshResult refreshToken(String refreshToken) { // Refresh Token이 없는 경우 diff --git a/snapit-server/src/main/resources/application.properties b/snapit-server/src/main/resources/application.properties index 224f339..380ee6e 100644 --- a/snapit-server/src/main/resources/application.properties +++ b/snapit-server/src/main/resources/application.properties @@ -23,3 +23,6 @@ gemini.api.key = ${GEMINI_API_KEY} # JWT secret secret.key = ${SECRET_KEY} + +# Deployment +deployment.address = ${DEPLOYMENT_ADDRESS} \ No newline at end of file diff --git a/snapit-server/src/test/java/com/snapit/backend/snapit_server/service/GeminiApiIntegrationTest.java b/snapit-server/src/test/java/com/snapit/backend/snapit_server/service/GeminiApiIntegrationTest.java index fab767e..167372c 100644 --- a/snapit-server/src/test/java/com/snapit/backend/snapit_server/service/GeminiApiIntegrationTest.java +++ b/snapit-server/src/test/java/com/snapit/backend/snapit_server/service/GeminiApiIntegrationTest.java @@ -61,7 +61,7 @@ void testGeminiApiCallWithMock() throws Exception { List> parts = new ArrayList<>(); Map part = new HashMap<>(); - part.put("text", "{\"placeList\":[\"카페\",\"도서관\",\"강의실\",\"학생회관\",\"공원\",\"영화관\"]}"); + part.put("text", "{\"placeList\":[\"카페-cafe\",\"도서관-library\",\"강의실-classroom\",\"학생회관-student's union\",\"공원-park\",\"영화관-theater\"]}"); parts.add(part); content.put("parts", parts); candidate.put("content", content); @@ -80,10 +80,10 @@ void testGeminiApiCallWithMock() throws Exception { String placeListPrompt = "10~20대가 접근하기 쉬운 공간(현실에 존재하는 공간)을 6개 랜덤하게 선택해서, 아래 예시와 같은 json형식으로 반환해줘." + " 이때, 공간은 단순히 공간명을 의미해. 공간은 특정성(스타벅스, 맥도날드)를 띄지 말고, 추상적으로 표현(카페, 음식점) 해줘. " + - "학교, 대학교 안에 존재하는 공간을 2개 이상 포함해줘.\n" + + "학교, 대학교 안에 존재하는 공간을 2개 이상 포함해줘.답변은 한글단어-영어단어 쌍인 예시와 같은 형식으로 보내줘.\n" + "예시:\n" + "{\n" + - " \"placeList\": [\"카페\",\"마트\",\"영화관\",\"강의실\",\"거리\",\"도서관\"]\n" + + " \"placeList\": [\"카페-cafe\",\"마트-market\",\"영화관-theater\",\"강의실-classroom\",\"거리-street\",\"도서관-library\"]\n" + "}"; String url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite:generateContent?key=" + apiKey; @@ -288,10 +288,10 @@ void testRealGeminiApiCall() throws Exception { String placeListPrompt = "10~20대가 접근하기 쉬운 공간(현실에 존재하는 공간)을 6개 랜덤하게 선택해서, 아래 예시와 같은 json형식으로 반환해줘." + " 이때, 공간은 단순히 공간명을 의미해. 공간은 특정성(스타벅스, 맥도날드)를 띄지 말고, 추상적으로 표현(카페, 음식점) 해줘. " + - "학교, 대학교 안에 존재하는 공간을 2개 이상 포함해줘.\n" + + "학교, 대학교 안에 존재하는 공간을 2개 이상 포함해줘. 답변은 한글단어-영어단어 쌍인 예시와 같은 형식으로 보내줘.\n" + "예시:\n" + "{\n" + - " \"placeList\": [\"카페\",\"마트\",\"영화관\",\"강의실\",\"거리\",\"도서관\"]\n" + + " \"placeList\": [\"카페-cafe\",\"마트-market\",\"영화관-theater\",\"강의실-classroom\",\"거리-street\",\"도서관-library\"]\n" + "}"; String url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite:generateContent?key=" + apiKey; @@ -382,8 +382,8 @@ void testRealGeminiApiCall2() throws Exception { // 이 테스트는 실제 API를 호출하므로 API 키가 유효한 경우에만 실행합니다. org.junit.jupiter.api.Assumptions.assumeTrue(!apiKey.equals("TEST_GEMINI_API_KEY"), "실제 API 키가 필요합니다"); - String stuffListPrompt = "대학교 강의실에 있을법한 물건 10개를 랜덤하게 골라서\n" + - "key는 stuffList, value는 물건 이름 String 배열로 JSON형식으로 답해줘"; + String stuffListPrompt ="대학교 강의실에 있을법한 물건 10개를 랜덤하게 골라서\n" + + "key는 stuffList, value는 물건 이름 한글-영어를 하나의 String으로 배열로 JSON형식으로 답해줘"; String url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite:generateContent?key=" + apiKey; // 1) 스키마 정의 (stuffList) diff --git a/snapit-server/src/test/resources/application-test.properties b/snapit-server/src/test/resources/application-test.properties index a2b83fe..cf29d71 100644 --- a/snapit-server/src/test/resources/application-test.properties +++ b/snapit-server/src/test/resources/application-test.properties @@ -25,4 +25,7 @@ spring.security.oauth2.client.registration.google.scope=profile,email gemini.api.key=test-dummy-api-key-for-testing-not-real # JWT secret -secret.key=testsecretkeytestsecretkeytestsecretkeytestsecretkeytestsecretkeytestsecretkey \ No newline at end of file +secret.key=testsecretkeytestsecretkeytestsecretkeytestsecretkeytestsecretkeytestsecretkey + +# Deployment +deployment.address=http://localhost:8080 \ No newline at end of file