Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
75bfb14
feat: CI 워크플로우에 수동 실행 옵션 추가
chabinhwang May 26, 2025
0206af0
fetch: WebHook 오류 전달 기능 도입
chabinhwang May 27, 2025
18b1a99
revert: Webhook 기능 제거
chabinhwang May 27, 2025
7fafb15
feat: GeminiService와 GeminiApiIntegrationTest에서 공간 및 물건 목록 반환 형식 수정
chabinhwang May 28, 2025
5aaef22
feat: 배포 환경 주소 및 JWT 필터 디버깅 로깅 추가
chabinhwang May 28, 2025
b5aa2e1
feat: 테스트 환경 설정 파일에 배포 주소 추가
chabinhwang May 28, 2025
b49da8f
feat: CORS 설정 및 OAuth2SuccessHandler에서 배포 주소 제거
chabinhwang May 28, 2025
b3b4ee1
feat: CORS 설정 수정 및 로컬 개발 환경 주소 추가
chabinhwang May 28, 2025
1714d38
feat: OAuth2SuccessHandler에서 이메일 쿠키의 보안 설정 수정
chabinhwang May 29, 2025
668efc0
fix: RoomService에서 방의 현재 용량 조건 수정
chabinhwang Jun 4, 2025
09a6554
feat: WebSocketController에 방 입장 API 추가
chabinhwang Jun 4, 2025
5c38ee7
feat: JWT 만료 시간 및 갱신 시간 수정
chabinhwang Jun 4, 2025
b56ebfd
feat: WebSocketController 방 입장 API 경로 수정 및 쿠키 유효기간 변경
chabinhwang Jun 4, 2025
51528e5
feat: GameEnvService에서 투표 로직에 디버깅 로그 추가
chabinhwang Jun 6, 2025
aeab2bd
feat: GameEnvService의 디버깅 로그를 한국어로 변경
chabinhwang Jun 6, 2025
9de9e8d
feat: RoomService에 방 진입 로직에 디버깅 로그 추가
chabinhwang Jun 6, 2025
8872a6f
feat: 유사도 계산 기능 추가 및 관련 DTO 클래스 생성
chabinhwang Jun 6, 2025
aadddd4
feat: WebSocketController에서 유사도 계산을 위한 URL 인코딩 추가
chabinhwang Jun 8, 2025
db2a763
feat: 유사도 계산 API의 URL을 로컬 주소에서 클러스터 주소로 변경
chabinhwang Jun 8, 2025
1d1ac91
fix: SimilarityResultMessage 클래스의 필드 이름 수정
chabinhwang Jun 8, 2025
07d2565
feat: WebSocketController에 사용자 목록 전송 기능 추가 및 UserListMessage DTO 생성
chabinhwang Jun 8, 2025
4a254d3
feat: GamePlayService에 점수 획득 실행 시 디버깅 로그 추가
chabinhwang Jun 9, 2025
a12fe0d
feat: RequiredArgsConstructor 적용
chabinhwang Jun 9, 2025
30d8ff4
feat: UserListMessage 보내는 엔드포인트 경로 오류 수정
chabinhwang Jun 10, 2025
8137fca
feat: UserListMessage header-body 구조 변경
chabinhwang Jun 10, 2025
c9ca3dc
feat: WebSocketController 디버그 출력 추가
chabinhwang Jun 10, 2025
eadccde
feat: WebSocketController 엔드포인트 수정
chabinhwang Jun 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

}
Expand All @@ -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())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String> userList = room.getUserList();
messagingTemplate.convertAndSend("/topic/room/"+roomUUID,
new UserListMessage(new UserListMessage.Body(userList)));

}
return roomService.getAllRooms();
}

Expand All @@ -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<String> 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()
)));
}
}
Original file line number Diff line number Diff line change
@@ -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<String> userList){}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.snapit.backend.snapit_server.dto.game;

public record SimilarityResponseMessage(double similarity) {
}
Original file line number Diff line number Diff line change
@@ -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) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* <h4>JWT 토큰 부분 검증</h4>
* HTTP 요청 헤더에서 JWT 토큰 추출
Expand All @@ -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);
}
Expand All @@ -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();
}
}
Expand All @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 제거
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

Expand Down Expand Up @@ -73,23 +71,22 @@ 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();

// Refresh Token도 쿠키로 설정
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());
Expand Down
Loading