diff --git a/src/main/java/com/example/eatmate/app/domain/meeting/domain/MeetingStatus.java b/src/main/java/com/example/eatmate/app/domain/meeting/domain/MeetingStatus.java index 995e947..cc2b70b 100644 --- a/src/main/java/com/example/eatmate/app/domain/meeting/domain/MeetingStatus.java +++ b/src/main/java/com/example/eatmate/app/domain/meeting/domain/MeetingStatus.java @@ -7,7 +7,7 @@ public enum MeetingStatus { ACTIVE("활성화"), INACTIVE("비활성화"), ; - + private final String text; MeetingStatus(String text) { diff --git a/src/main/java/com/example/eatmate/global/auth/jwt/JwtService.java b/src/main/java/com/example/eatmate/global/auth/jwt/JwtService.java index db982c4..297c83b 100644 --- a/src/main/java/com/example/eatmate/global/auth/jwt/JwtService.java +++ b/src/main/java/com/example/eatmate/global/auth/jwt/JwtService.java @@ -1,5 +1,13 @@ package com.example.eatmate.global.auth.jwt; +import java.util.Arrays; +import java.util.Date; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; @@ -8,18 +16,12 @@ import com.auth0.jwt.exceptions.TokenExpiredException; import com.example.eatmate.app.domain.member.domain.repository.MemberRepository; import com.example.eatmate.global.config.error.exception.custom.UserNotFoundException; + import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Arrays; -import java.util.Date; -import java.util.Optional; @Service @RequiredArgsConstructor @@ -27,135 +29,131 @@ @Slf4j public class JwtService { - @Value("${jwt.secretKey}") - private String secretKey; - - @Value("${jwt.access.expiration}") - private Long accessTokenExpirationPeriod; - - @Value("${jwt.refresh.expiration}") - private Long refreshTokenExpirationPeriod; - - private static final String ACCESS_TOKEN_SUBJECT = "AccessToken"; - private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken"; - private static final String EMAIL_CLAIM = "email"; - private static final String ROLE_CLAIM = "role"; - - private final MemberRepository memberRepository; - - /** - * 토큰 생성 메서드 - */ - private String createToken(String subject, long expirationTime, String email, String role) { - Date now = new Date(); - var jwtBuilder = JWT.create() - .withSubject(subject) - .withExpiresAt(new Date(now.getTime() + expirationTime)) - .withClaim(EMAIL_CLAIM, email); - - if (role != null) { - jwtBuilder.withClaim(ROLE_CLAIM, role); // Role 클레임 추가 - } - - return jwtBuilder.sign(Algorithm.HMAC512(secretKey)); - } - - /** - * Access Token 생성 (Role 포함) - */ - public String createAccessToken(String email, String role) { - return createToken(ACCESS_TOKEN_SUBJECT, accessTokenExpirationPeriod, email, role); - } - - /** - * Refresh Token 생성 (Role 정보 없음) - */ - public String createRefreshToken() { - return createToken(REFRESH_TOKEN_SUBJECT, refreshTokenExpirationPeriod, null, null); - } - - /** - * 공통: 쿠키에서 토큰 추출 - */ - private Optional extractTokenFromCookie(HttpServletRequest request, String cookieName) { - return Arrays.stream(Optional.ofNullable(request.getCookies()).orElse(new Cookie[0])) - .filter(cookie -> cookie.getName().equals(cookieName)) - .map(Cookie::getValue) - .findFirst(); - } - - /** - * 쿠키에서 Access Token 추출 - */ - public Optional extractAccessTokenFromCookie(HttpServletRequest request) { - return extractTokenFromCookie(request, "AccessToken"); - } - - /** - * 쿠키에서 Refresh Token 추출 - */ - public Optional extractRefreshTokenFromCookie(HttpServletRequest request) { - return extractTokenFromCookie(request, "RefreshToken"); - } - - /** - * Access Token에서 Email 추출 - */ - public Optional extractEmail(String accessToken) { - return extractClaim(accessToken, EMAIL_CLAIM); - } - - /** - * Access Token에서 Role 추출 - */ - public Optional extractRole(String accessToken) { - return extractClaim(accessToken, ROLE_CLAIM); - } - - /** - * 공통: 토큰에서 Claim 추출 - */ - private Optional extractClaim(String token, String claim) { - try { - JWTVerifier verifier = JWT.require(Algorithm.HMAC512(secretKey)).build(); - return Optional.ofNullable(verifier.verify(token).getClaim(claim).asString()); - } catch (Exception e) { - log.error("토큰에서 {} 추출 실패: {}", claim, e.getMessage()); - return Optional.empty(); - } - } - - /** - * 토큰 유효성 검증 - */ - public boolean isTokenValid(String token) { - try { - JWT.require(Algorithm.HMAC512(secretKey)).build().verify(token); - return true; - } catch (TokenExpiredException e) { - log.warn("토큰 만료: {}", e.getMessage()); - } catch (SignatureVerificationException e) { - log.warn("서명 검증 실패: {}", e.getMessage()); - } catch (JWTDecodeException e) { - log.warn("토큰 디코딩 실패: {}", e.getMessage()); - } catch (Exception e) { - log.error("알 수 없는 토큰 검증 오류: {}", e.getMessage()); - } - return false; - } - - /** - * Refresh Token 업데이트 - */ - @Transactional - public void updateRefreshToken(String email, String refreshToken) { - memberRepository.findByEmail(email).ifPresentOrElse( - member -> { - member.updateRefreshToken(refreshToken); - memberRepository.saveAndFlush(member); - }, - () -> { - throw new UserNotFoundException(); - }); - } + private static final String ACCESS_TOKEN_SUBJECT = "AccessToken"; + private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken"; + private static final String EMAIL_CLAIM = "email"; + private static final String ROLE_CLAIM = "role"; + private final MemberRepository memberRepository; + @Value("${jwt.secretKey}") + private String secretKey; + @Value("${jwt.access.expiration}") + private Long accessTokenExpirationPeriod; + @Value("${jwt.refresh.expiration}") + private Long refreshTokenExpirationPeriod; + + /** + * 토큰 생성 메서드 + */ + private String createToken(String subject, long expirationTime, String email, String role) { + Date now = new Date(); + var jwtBuilder = JWT.create() + .withSubject(subject) + .withExpiresAt(new Date(now.getTime() + expirationTime)) + .withClaim(EMAIL_CLAIM, email); + + if (role != null) { + jwtBuilder.withClaim(ROLE_CLAIM, role); // Role 클레임 추가 + } + + return jwtBuilder.sign(Algorithm.HMAC512(secretKey)); + } + + /** + * Access Token 생성 (Role 포함) + */ + public String createAccessToken(String email, String role) { + return createToken(ACCESS_TOKEN_SUBJECT, accessTokenExpirationPeriod, email, role); + } + + /** + * Refresh Token 생성 (Role 정보 없음) + */ + public String createRefreshToken() { + return createToken(REFRESH_TOKEN_SUBJECT, refreshTokenExpirationPeriod, null, null); + } + + /** + * 공통: 쿠키에서 토큰 추출 + */ + private Optional extractTokenFromCookie(HttpServletRequest request, String cookieName) { + return Arrays.stream(Optional.ofNullable(request.getCookies()).orElse(new Cookie[0])) + .filter(cookie -> cookie.getName().equals(cookieName)) + .map(Cookie::getValue) + .findFirst(); + } + + /** + * 쿠키에서 Access Token 추출 + */ + public Optional extractAccessTokenFromCookie(HttpServletRequest request) { + return extractTokenFromCookie(request, "AccessToken"); + } + + /** + * 쿠키에서 Refresh Token 추출 + */ + public Optional extractRefreshTokenFromCookie(HttpServletRequest request) { + return extractTokenFromCookie(request, "RefreshToken"); + } + + /** + * Access Token에서 Email 추출 + */ + public Optional extractEmail(String accessToken) { + return extractClaim(accessToken, EMAIL_CLAIM); + } + + /** + * Access Token에서 Role 추출 + */ + public Optional extractRole(String accessToken) { + return extractClaim(accessToken, ROLE_CLAIM); + } + + /** + * 공통: 토큰에서 Claim 추출 + */ + private Optional extractClaim(String token, String claim) { + try { + JWTVerifier verifier = JWT.require(Algorithm.HMAC512(secretKey)).build(); + return Optional.ofNullable(verifier.verify(token).getClaim(claim).asString()); + } catch (Exception e) { + log.error("토큰에서 {} 추출 실패: {}", claim, e.getMessage()); + return Optional.empty(); + } + } + + /** + * 토큰 유효성 검증 + */ + public boolean isTokenValid(String token) { + try { + JWT.require(Algorithm.HMAC512(secretKey)).build().verify(token); + return true; + } catch (TokenExpiredException e) { + log.warn("토큰 만료: {}", e.getMessage()); + } catch (SignatureVerificationException e) { + log.warn("서명 검증 실패: {}", e.getMessage()); + } catch (JWTDecodeException e) { + log.warn("토큰 디코딩 실패: {}", e.getMessage()); + } catch (Exception e) { + log.error("알 수 없는 토큰 검증 오류: {}", e.getMessage()); + } + return false; + } + + /** + * Refresh Token 업데이트 + */ + @Transactional + public void updateRefreshToken(String email, String refreshToken) { + memberRepository.findByEmail(email).ifPresentOrElse( + member -> { + member.updateRefreshToken(refreshToken); + memberRepository.saveAndFlush(member); + }, + () -> { + throw new UserNotFoundException(); + }); + } } diff --git a/src/main/java/com/example/eatmate/global/auth/login/controller/AuthController.java b/src/main/java/com/example/eatmate/global/auth/login/controller/AuthController.java index f5103c7..1da4dd3 100644 --- a/src/main/java/com/example/eatmate/global/auth/login/controller/AuthController.java +++ b/src/main/java/com/example/eatmate/global/auth/login/controller/AuthController.java @@ -1,21 +1,24 @@ package com.example.eatmate.global.auth.login.controller; -import com.example.eatmate.global.auth.jwt.JwtService; -import com.example.eatmate.global.auth.login.dto.UserLoginResponseDto; -import com.example.eatmate.global.auth.login.service.LoginService; -import com.example.eatmate.global.config.error.exception.CommonException; -import jakarta.servlet.http.HttpServletRequest; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import com.example.eatmate.app.domain.member.dto.MemberSignUpRequestDto; import com.example.eatmate.app.domain.member.service.MemberService; +import com.example.eatmate.global.auth.jwt.JwtService; +import com.example.eatmate.global.auth.login.dto.UserLoginResponseDto; +import com.example.eatmate.global.auth.login.service.LoginService; import com.example.eatmate.global.response.GlobalResponseDto; import io.swagger.v3.oas.annotations.Operation; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -50,6 +53,4 @@ public ResponseEntity> getUserInfo(HttpS return ResponseEntity.ok(GlobalResponseDto.success(userInfo, HttpStatus.OK.value())); } - - } diff --git a/src/main/java/com/example/eatmate/global/auth/login/dto/UserLoginResponseDto.java b/src/main/java/com/example/eatmate/global/auth/login/dto/UserLoginResponseDto.java index 16c785b..abdbac5 100644 --- a/src/main/java/com/example/eatmate/global/auth/login/dto/UserLoginResponseDto.java +++ b/src/main/java/com/example/eatmate/global/auth/login/dto/UserLoginResponseDto.java @@ -1,6 +1,7 @@ package com.example.eatmate.global.auth.login.dto; import com.example.eatmate.app.domain.member.domain.Role; + import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -10,6 +11,6 @@ @NoArgsConstructor public class UserLoginResponseDto { - private String email; - private Role role; + private String email; + private Role role; } diff --git a/src/main/java/com/example/eatmate/global/auth/login/service/LoginService.java b/src/main/java/com/example/eatmate/global/auth/login/service/LoginService.java index bf47643..45336b4 100644 --- a/src/main/java/com/example/eatmate/global/auth/login/service/LoginService.java +++ b/src/main/java/com/example/eatmate/global/auth/login/service/LoginService.java @@ -2,6 +2,10 @@ import static org.springframework.security.core.userdetails.User.*; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Service; + import com.example.eatmate.app.domain.member.domain.Member; import com.example.eatmate.app.domain.member.domain.Role; import com.example.eatmate.app.domain.member.domain.repository.MemberRepository; @@ -9,58 +13,50 @@ import com.example.eatmate.global.auth.login.dto.UserLoginResponseDto; import com.example.eatmate.global.config.error.ErrorCode; import com.example.eatmate.global.config.error.exception.CommonException; -import com.example.eatmate.global.config.error.exception.custom.InvalidTokenException; import com.example.eatmate.global.config.error.exception.custom.UserNotFoundException; + import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - @Service @RequiredArgsConstructor public class LoginService implements UserDetailsService { - private final MemberRepository memberRepository; - private final JwtService jwtService; - - @Override - public UserDetails loadUserByUsername(String email) throws UserNotFoundException { - Member member = memberRepository.findByEmail(email) - .orElseThrow(UserNotFoundException::new); - - - return builder() - .username(member.getEmail()) - .build(); - } - - public UserLoginResponseDto getUserInfoFromRequest(HttpServletRequest request) { - // 쿠키에서 AccessToken 추출 - String accessToken = jwtService.extractAccessTokenFromCookie(request) - .orElseThrow(() -> new CommonException(ErrorCode.TOKEN_NOT_FOUND)); - - // AccessToken 유효성 검증 및 사용자 정보 조회 - return getUserInfo(accessToken); - } - - public UserLoginResponseDto getUserInfo(String accessToken) { - // AccessToken 유효성 검증 - if (!jwtService.isTokenValid(accessToken)) { - throw new CommonException(ErrorCode.INVALID_TOKEN); - } - // AccessToken에서 이메일과 역할(Role) 추출 - String email = jwtService.extractEmail(accessToken) - .orElseThrow(() -> new CommonException(ErrorCode.INVALID_TOKEN)); - String role = jwtService.extractRole(accessToken) - .orElseThrow(() -> new CommonException(ErrorCode.INVALID_TOKEN)); - - // 사용자 정보 반환 - return new UserLoginResponseDto(email, Role.valueOf(role)); - } - - + private final MemberRepository memberRepository; + private final JwtService jwtService; + + @Override + public UserDetails loadUserByUsername(String email) throws UserNotFoundException { + Member member = memberRepository.findByEmail(email) + .orElseThrow(UserNotFoundException::new); + + return builder() + .username(member.getEmail()) + .build(); + } + + public UserLoginResponseDto getUserInfoFromRequest(HttpServletRequest request) { + // 쿠키에서 AccessToken 추출 + String accessToken = jwtService.extractAccessTokenFromCookie(request) + .orElseThrow(() -> new CommonException(ErrorCode.TOKEN_NOT_FOUND)); + + // AccessToken 유효성 검증 및 사용자 정보 조회 + return getUserInfo(accessToken); + } + + public UserLoginResponseDto getUserInfo(String accessToken) { + // AccessToken 유효성 검증 + if (!jwtService.isTokenValid(accessToken)) { + throw new CommonException(ErrorCode.INVALID_TOKEN); + } + // AccessToken에서 이메일과 역할(Role) 추출 + String email = jwtService.extractEmail(accessToken) + .orElseThrow(() -> new CommonException(ErrorCode.INVALID_TOKEN)); + String role = jwtService.extractRole(accessToken) + .orElseThrow(() -> new CommonException(ErrorCode.INVALID_TOKEN)); + + // 사용자 정보 반환 + return new UserLoginResponseDto(email, Role.valueOf(role)); + } } diff --git a/src/main/java/com/example/eatmate/global/config/RestTemplateConfig.java b/src/main/java/com/example/eatmate/global/config/RestTemplateConfig.java new file mode 100644 index 0000000..2b96309 --- /dev/null +++ b/src/main/java/com/example/eatmate/global/config/RestTemplateConfig.java @@ -0,0 +1,14 @@ +package com.example.eatmate.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/src/main/java/com/example/eatmate/global/config/error/ErrorCode.java b/src/main/java/com/example/eatmate/global/config/error/ErrorCode.java index fff25f7..10c3508 100644 --- a/src/main/java/com/example/eatmate/global/config/error/ErrorCode.java +++ b/src/main/java/com/example/eatmate/global/config/error/ErrorCode.java @@ -10,7 +10,7 @@ public enum ErrorCode { //공통 INTERNAL_SERVER_ERROR(500, "INTERNAL_SERVER_ERROR", "서버 내부 에러"), VALIDATION_FAILED(400, "VALIDATION_FAILED", "요청 값이 올바르지 않습니다."), - + JSON_PARSING_ERROR(400, "JSON_PARSING_ERROR", "JSON 데이터 처리 중 오류가 발생했습니다"), //회원 USER_NOT_FOUND(404, "USER_NOT_FOUND", "유저를 찾을 수 없습니다."), TOKEN_NOT_FOUND(404, "TOKEN_NOT_FOUND", "토큰를 찾을 수 없습니다."), diff --git a/src/main/java/com/example/eatmate/global/config/error/exception/GithubIssueGenerator.java b/src/main/java/com/example/eatmate/global/config/error/exception/GithubIssueGenerator.java new file mode 100644 index 0000000..fa76ef8 --- /dev/null +++ b/src/main/java/com/example/eatmate/global/config/error/exception/GithubIssueGenerator.java @@ -0,0 +1,77 @@ +package com.example.eatmate.global.config.error.exception; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import com.example.eatmate.global.config.error.ErrorCode; +import com.example.eatmate.global.config.error.exception.dto.IssueCreateRequestDto; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class GithubIssueGenerator { + private static final String REPO_URL = "https://api.github.com/repos/Leets-Official/EatMate-BE/issues"; + private static final List ASSIGNEES = List.of("ehs208", "seokjun01", "jj0526", "dyk-im"); + private static final List LABELS = List.of("fix"); + private static final MediaType JSON_MEDIA_TYPE = new MediaType("application", "json", StandardCharsets.UTF_8); + private final ObjectMapper objectMapper; + private final RestTemplate restTemplate; + @Value("${github.access-token}") + private String accessToken; + + public void create(Exception exception) { + // api 요청 + restTemplate.exchange( + REPO_URL, + HttpMethod.POST, + new HttpEntity<>(createRequestJson(exception), setAuthorization()), + String.class + ); + } + + // DTO 객체를 JSON 문자열로 변환 + private String createRequestJson(Exception exception) { + return parseJsonString(new IssueCreateRequestDto( + "[Fix] 서버 장애 발생 " + exception.getMessage(), + createIssueBody(exception), + ASSIGNEES, + LABELS + )); + } + + private String parseJsonString(IssueCreateRequestDto request) { + try { + return objectMapper.writeValueAsString(request); + } catch (JsonProcessingException e) { + throw new CommonException(ErrorCode.JSON_PARSING_ERROR); + } + } + + // 발생한 예외의 stackTrace를 마크다운 코드 블럭으로 감싸서 작성 + private String createIssueBody(Exception exception) { + StringWriter stringWriter = new StringWriter(); + exception.printStackTrace(new PrintWriter(stringWriter)); + return "```\n" + stringWriter + "\n```"; + } + + // Authorization 헤더에 accessToken 입력 + private HttpHeaders setAuthorization() { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.set("Authorization", "token " + accessToken); + httpHeaders.setContentType(JSON_MEDIA_TYPE); + return httpHeaders; + } +} diff --git a/src/main/java/com/example/eatmate/global/config/error/exception/GlobalExceptionHandler.java b/src/main/java/com/example/eatmate/global/config/error/exception/GlobalExceptionHandler.java index d4b0e53..4faa7f5 100644 --- a/src/main/java/com/example/eatmate/global/config/error/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/eatmate/global/config/error/exception/GlobalExceptionHandler.java @@ -1,7 +1,9 @@ package com.example.eatmate.global.config.error.exception; +import java.util.Arrays; import java.util.stream.Collectors; +import org.springframework.core.env.Environment; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -14,17 +16,18 @@ import com.example.eatmate.global.response.GlobalResponseDto; import jakarta.validation.ConstraintViolationException; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @ControllerAdvice +@RequiredArgsConstructor public class GlobalExceptionHandler { + private static final String ISSUE_CREATE_ENV = "dev"; private final View error; - - public GlobalExceptionHandler(View error) { - this.error = error; - } + private final GithubIssueGenerator githubIssueGenerator; + private final Environment environment; private static void showErrorLog(ErrorCode errorCode) { log.error("errorCode: {}, message: {}", errorCode.getCode(), errorCode.getMessage()); @@ -35,6 +38,7 @@ public ResponseEntity handleGenericException(Exception ex) { ErrorCode errorCode = ErrorCode.INTERNAL_SERVER_ERROR; ErrorResponse errorResponse = new ErrorResponse(errorCode); log.error(ex.getMessage()); + handleUnexpectedError(ex); log.error(ex.getClass().getSimpleName()); return ResponseEntity.status(HttpStatus.valueOf(errorCode.getStatus())) .body(GlobalResponseDto.fail(errorCode, errorResponse.getMessage())); @@ -78,4 +82,14 @@ public ResponseEntity> handleConstraintViolationExcept return ResponseEntity.status(HttpStatus.valueOf(errorCode.getStatus())) .body(GlobalResponseDto.fail(errorCode, errorMessage)); } + + public void handleUnexpectedError(Exception ex) { + if (isProd(environment.getActiveProfiles())) { + githubIssueGenerator.create(ex); + } + } + + private boolean isProd(String[] activeProfiles) { + return Arrays.stream(activeProfiles).anyMatch(profile -> profile.equalsIgnoreCase(ISSUE_CREATE_ENV)); + } } diff --git a/src/main/java/com/example/eatmate/global/config/error/exception/dto/IssueCreateRequestDto.java b/src/main/java/com/example/eatmate/global/config/error/exception/dto/IssueCreateRequestDto.java new file mode 100644 index 0000000..e812c3b --- /dev/null +++ b/src/main/java/com/example/eatmate/global/config/error/exception/dto/IssueCreateRequestDto.java @@ -0,0 +1,15 @@ +package com.example.eatmate.global.config.error.exception.dto; + +import java.util.List; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class IssueCreateRequestDto { + private final String title; // 이슈 제목 + private final String body; // 이슈 본문 + private final List assignees; // 할당할 담당자들 + private final List labels; // 이슈 라벨들 +} diff --git a/src/test/java/com/example/eatmate/global/config/error/exception/GlobalExceptionHandlerTest.java b/src/test/java/com/example/eatmate/global/config/error/exception/GlobalExceptionHandlerTest.java new file mode 100644 index 0000000..fe273f0 --- /dev/null +++ b/src/test/java/com/example/eatmate/global/config/error/exception/GlobalExceptionHandlerTest.java @@ -0,0 +1,46 @@ +package com.example.eatmate.global.config.error.exception; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@SpringBootTest +@AutoConfigureMockMvc +class GlobalExceptionHandlerTest { + + @Autowired + private MockMvc mockMvc; + + @Test + @DisplayName("예외 발생시 GlobalExceptionHandler가 처리하여 깃허브 이슈를 생성한다") + void handleGenericException() throws Exception { + // given + String url = "/api/test-exception"; + + // when & then + mockMvc.perform(get(url)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.code").value(500)) + .andExpect(jsonPath("$.message").exists()) + .andDo(print()); + } +} + +@RestController +class TestExceptionController { + + @GetMapping("/api/test-exception") + public void throwException() { + throw new RuntimeException("테스트용 예외 발생!"); + } +}