diff --git a/src/main/java/com/Alchive/backend/config/SwaggerConfig.java b/src/main/java/com/Alchive/backend/config/SwaggerConfig.java index 9516445..a0445a8 100644 --- a/src/main/java/com/Alchive/backend/config/SwaggerConfig.java +++ b/src/main/java/com/Alchive/backend/config/SwaggerConfig.java @@ -43,7 +43,7 @@ public OpenAPI openAPI() { private Info apiInfo() { return new Info() .title("Alchive API") - .description("[ Base URL: http://localhost:8080/api/v1]\n\nAlchive의 API 문서") + .description("[ Base URL: http://localhost:8080/api/v2]\n\nAlchive의 API 문서") .version("1.0.0"); } } \ No newline at end of file diff --git a/src/main/java/com/Alchive/backend/config/auth/SecurityConfig.java b/src/main/java/com/Alchive/backend/config/auth/SecurityConfig.java index c0d4181..c1c7223 100644 --- a/src/main/java/com/Alchive/backend/config/auth/SecurityConfig.java +++ b/src/main/java/com/Alchive/backend/config/auth/SecurityConfig.java @@ -3,6 +3,9 @@ import com.Alchive.backend.config.auth.handler.OAuth2FailureHandler; import com.Alchive.backend.config.auth.handler.OAuth2SuccessHandler; import com.Alchive.backend.config.auth.service.CustomOAuth2UserService; +import com.Alchive.backend.config.jwt.JwtAuthenticationFilter; +import com.Alchive.backend.config.jwt.JwtTokenProvider; +import com.Alchive.backend.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -12,6 +15,7 @@ import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @RequiredArgsConstructor @@ -21,20 +25,26 @@ public class SecurityConfig { private final CustomOAuth2UserService customOAuth2UserService; private final OAuth2SuccessHandler oAuth2SuccessHandler; private final OAuth2FailureHandler oAuth2FailureHandler; + private final JwtTokenProvider jwtTokenProvider; + private final UserService userService; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .csrf( - AbstractHttpConfigurer::disable - ) - .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) // H2 콘솔 사용을 위한 설정 + // CSRF 보호 비활성화 + .csrf(AbstractHttpConfigurer::disable) + // H2 콘솔 사용을 위한 Frame-Options 설정 + .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) .authorizeHttpRequests(requests -> requests.anyRequest().permitAll() // 모든 요청을 모든 사용자에게 허용 ) + .addFilterBefore( + new JwtAuthenticationFilter(jwtTokenProvider, userService), + UsernamePasswordAuthenticationFilter.class + ) .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS) - ) // 세션을 사용하지 않으므로 STATELESS 설정 + ) // 세션을 사용하지 않으므로 STATELESS 설정 .logout( // 로그아웃 성공 시 / 주소로 이동 (logoutConfig) -> logoutConfig.logoutSuccessUrl("/") ) diff --git a/src/main/java/com/Alchive/backend/config/auth/handler/OAuth2SuccessHandler.java b/src/main/java/com/Alchive/backend/config/auth/handler/OAuth2SuccessHandler.java index 6782d89..f7d0dcf 100644 --- a/src/main/java/com/Alchive/backend/config/auth/handler/OAuth2SuccessHandler.java +++ b/src/main/java/com/Alchive/backend/config/auth/handler/OAuth2SuccessHandler.java @@ -1,6 +1,6 @@ package com.Alchive.backend.config.auth.handler; -import com.Alchive.backend.config.jwt.TokenService; +import com.Alchive.backend.config.jwt.JwtTokenProvider; import com.Alchive.backend.domain.user.User; import com.Alchive.backend.repository.UserRepository; import jakarta.servlet.http.HttpServletRequest; @@ -20,8 +20,8 @@ @RequiredArgsConstructor @Component public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { - private final TokenService tokenService; private final UserRepository userRepository; + private final JwtTokenProvider jwtTokenProvider; // 검증 완료된 유저의 정보를 가져와서 토큰 생성, 로그인/회원가입 요청에 맞게 리다이렉트 @Override @@ -32,20 +32,19 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo String email = oAuth2User.getAttribute("email"); Optional user = userRepository.findByEmail(email); String targetUrl; - Long userId; + String userEmail; - if ( user.isPresent() ) { // 로그인인 경우 - userId = user.get().getId(); - String accessToken = tokenService.generateAccessToken(userId); - String refreshToken = tokenService.generateRefreshToken(); + if (user.isPresent()) { // 로그인인 경우 + userEmail = user.get().getEmail(); + String accessToken = jwtTokenProvider.createAccessToken(userEmail); + String refreshToken = jwtTokenProvider.createRefreshToken(userEmail); targetUrl = UriComponentsBuilder.fromUriString("/") .queryParam("access", accessToken) .queryParam("refresh", refreshToken) .build().toUriString(); - } - else { // 회원가입인 경우 + } else { // 회원가입인 경우 targetUrl = UriComponentsBuilder.fromUriString("http://localhost:5173/sign") - .queryParam("email",email) + .queryParam("email", email) .build().toUriString(); } getRedirectStrategy().sendRedirect(request, response, targetUrl); diff --git a/src/main/java/com/Alchive/backend/config/error/GlobalExceptionHandler.java b/src/main/java/com/Alchive/backend/config/error/GlobalExceptionHandler.java index afcac59..ea12e2b 100644 --- a/src/main/java/com/Alchive/backend/config/error/GlobalExceptionHandler.java +++ b/src/main/java/com/Alchive/backend/config/error/GlobalExceptionHandler.java @@ -12,7 +12,7 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(RuntimeException.class) public ResponseEntity handleRuntimeException(RuntimeException exception) { - log.info(exception.getMessage() + ", " + exception.getClass()); + log.error(exception.getMessage() + ", " + exception.getClass()); ErrorCode errorCode = ErrorCode._INTERNAL_SERVER_ERROR; return ResponseEntity.status(errorCode.getHttpStatus()) .body(ErrorResponse.builder() diff --git a/src/main/java/com/Alchive/backend/config/jwt/JwtAuthenticationFilter.java b/src/main/java/com/Alchive/backend/config/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..cfef4b8 --- /dev/null +++ b/src/main/java/com/Alchive/backend/config/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,122 @@ +package com.Alchive.backend.config.jwt; + +import com.Alchive.backend.config.error.ErrorCode; +import com.Alchive.backend.config.error.ErrorResponse; +import com.Alchive.backend.config.error.exception.BusinessException; +import com.Alchive.backend.config.error.exception.token.TokenNotExistsException; +import com.Alchive.backend.domain.user.User; +import com.Alchive.backend.service.UserService; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtTokenProvider jwtTokenProvider; + private final UserService userService; + private final AntPathMatcher pathMatcher = new AntPathMatcher(); + + + // 인증에서 제외할 URL과 메서드 + private static final Map> EXCLUDE_URL = Map.ofEntries( + Map.entry("/api/v2", List.of("GET")), + Map.entry("/api/v2/api-docs/**", List.of("GET")), + Map.entry("/api/swagger-ui/**", List.of("GET")), + Map.entry("/api/v2/boards", List.of("GET")), + Map.entry("/api/v2/boards/{boardId}", List.of("GET")), + Map.entry("/api/v2/users", List.of("GET", "POST")), + Map.entry("/api/v2/users/{userId}", List.of("GET")), + Map.entry("/api/v2/users/username/{name}", List.of("GET")), + Map.entry("/api/v2/sns/{snsId}", List.of("GET")), + Map.entry("/api/v2/slack/reminder", List.of("GET")), + Map.entry("/api/v2/slack/added", List.of("GET")) + ); + + // EXCLUDE_URL과 메서드에 일치할 경우 현재 필터를 진행하지 않고 다음 필터 진행 + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + String path = request.getServletPath(); + String method = request.getMethod(); + return EXCLUDE_URL.entrySet().stream() + .anyMatch(entry -> pathMatches(entry.getKey(), path) && entry.getValue().contains(method)); + } + + // 경로 패턴을 비교하는 유틸리티 메서드 (단순히 String 비교를 넘어 패턴 매칭이 필요할 경우 활용) + private boolean pathMatches(String pattern, String path) { + return pathMatcher.match(pattern, path); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + try { + // 액세스 토큰 추출 및 검증 + String accessToken = jwtTokenProvider.resolveAccessToken(request); + if (accessToken != null && jwtTokenProvider.validateToken(accessToken)) { + authenticateWithToken(accessToken); + } + // 액세스 토큰이 없거나 만료된 경우 리프레시 토큰 확인 + else { + // 리프레시 토큰 추출 및 검증 + String refreshToken = jwtTokenProvider.resolveRefreshToken(request); + if (refreshToken != null && jwtTokenProvider.validateToken(refreshToken)) { + String email = jwtTokenProvider.getEmailFromToken(refreshToken); + // 새로운 액세스, 리프레시 토큰 발급 + String newAccessToken = jwtTokenProvider.createAccessToken(email); + String newRefreshToken = jwtTokenProvider.createRefreshToken(email); + response.setHeader("Authorization", "Bearer " + newAccessToken); + response.setHeader("Refresh-Token", newRefreshToken); + // 새로 발급된 액세스 토큰으로 인증 처리 + authenticateWithToken(newAccessToken); + } else { + // 토큰이 없는 경우 + throw new TokenNotExistsException(); + } + } + + filterChain.doFilter(request, response); + } catch (BusinessException e) { + handleException(response, e); + } + } + + private void authenticateWithToken(String token) { + // 토큰에서 이메일 추출 후 이메일로 사용자 조회 + String email = jwtTokenProvider.getEmailFromToken(token); + User user = userService.findByEmail(email); + // 인증 객체 생성 + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(user, null); + // SecurityContext에 인증 정보 설정 + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + // 토큰 검증 중 토큰이 없거나 유효하지 않은 경우 예외 처리 + private void handleException(HttpServletResponse response, BusinessException exception) throws IOException { + ErrorCode errorCode = exception.getErrorCode(); + ErrorResponse errorResponse = ErrorResponse.builder() + .code(String.valueOf(errorCode.getCode())) + .message(errorCode.getMessage()) + .build(); + response.setStatus(errorCode.getHttpStatus()); + response.setContentType("application/json; charset=UTF-8"); // 한글을 위해 UTF-8 인코딩 설정 + + // ErrorResponse를 JSON 형식으로 변환하여 응답 + ObjectMapper objectMapper = new ObjectMapper(); + String jsonResponse = objectMapper.writeValueAsString(errorResponse); + + response.getWriter().write(jsonResponse); + } +} diff --git a/src/main/java/com/Alchive/backend/config/jwt/JwtController.java b/src/main/java/com/Alchive/backend/config/jwt/JwtController.java new file mode 100644 index 0000000..8eef923 --- /dev/null +++ b/src/main/java/com/Alchive/backend/config/jwt/JwtController.java @@ -0,0 +1,28 @@ +package com.Alchive.backend.config.jwt; + +import com.Alchive.backend.config.result.ResultResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static com.Alchive.backend.config.result.ResultCode.TOKEN_ACCESS_SUCCESS; + +@Tag(name = "JWT", description = "[Test] JWT 관련 api입니다.") +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v2/jwt") // 공통 url +public class JwtController { + private final JwtTokenProvider jwtTokenProvider; + + @Operation(summary = "토큰 재발급 메서드", description = "액세스 토큰을 재발급하는 메서드입니다.") + @GetMapping("") + public ResponseEntity createToken(String email) { + String accessToken = jwtTokenProvider.createAccessToken(email); + return ResponseEntity.ok(ResultResponse.of(TOKEN_ACCESS_SUCCESS, accessToken)); + } +} diff --git a/src/main/java/com/Alchive/backend/config/jwt/JwtTokenProvider.java b/src/main/java/com/Alchive/backend/config/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..6f9b074 --- /dev/null +++ b/src/main/java/com/Alchive/backend/config/jwt/JwtTokenProvider.java @@ -0,0 +1,95 @@ +package com.Alchive.backend.config.jwt; + +import com.Alchive.backend.config.error.exception.token.TokenExpiredException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Date; + +@Component +@RequiredArgsConstructor +public class JwtTokenProvider { + // JWT 토큰 생성 + private Key secretKey; + @Value("${jwt.token.secret-key}") + private String SECRET_KEY; + @Value("${jwt.token.access-expire-length}") + private Long ACCESS_EXPIRE_LENGTH; + @Value("${jwt.token.refresh-expire-length}") + private Long REFRESH_EXPIRE_LENGTH; + + @PostConstruct + protected void init() { + secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET_KEY)); + } + + // 액세스 및 리프레시 토큰 생성 + public String createAccessToken(String email) { + return createToken(email, ACCESS_EXPIRE_LENGTH); + } + + public String createRefreshToken(String email) { + return createToken(email, REFRESH_EXPIRE_LENGTH); + } + + private String createToken(String email, Long expireLength) { + Claims claims = Jwts.claims().setSubject(email); + return Jwts.builder().setClaims(claims) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + expireLength)) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + // 액세스 토큰 추출 + public String resolveAccessToken(HttpServletRequest request) { + return resolveToken(request, "Authorization", "Bearer "); + } + + // 리프레시 토큰 추출 + public String resolveRefreshToken(HttpServletRequest request) { + return resolveToken(request, "Refresh-Token", ""); + } + + // HTTP 요청 헤더에서 토큰 추출 + private String resolveToken(HttpServletRequest request, String headerName, String prefix) { + try { + String header = request.getHeader(headerName); + return prefix.isEmpty() ? header : header.substring(prefix.length()); + } catch (NullPointerException | IllegalArgumentException e) { + return null; + } + } + + // 토큰 검증 + public boolean validateToken(String token) { + try { + Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token); + return true; + } catch (Exception e) { + throw new TokenExpiredException(); + } + } + + // 이메일 추출 + public String getEmailFromToken(String token) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody(); + return claims.getSubject(); + } +} diff --git a/src/main/java/com/Alchive/backend/config/jwt/TokenService.java b/src/main/java/com/Alchive/backend/config/jwt/TokenService.java deleted file mode 100644 index a1b30e4..0000000 --- a/src/main/java/com/Alchive/backend/config/jwt/TokenService.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.Alchive.backend.config.jwt; - -import com.Alchive.backend.config.error.exception.token.TokenExpiredException; -import com.Alchive.backend.config.error.exception.token.TokenNotExistsException; -import com.Alchive.backend.config.error.exception.token.UnmatchedUserIdException; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.security.Keys; -import jakarta.annotation.PostConstruct; -import jakarta.servlet.http.HttpServletRequest; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import java.security.Key; -import java.util.Date; - -@Slf4j -@Service -public class TokenService { - // JWT 토큰 생성 - private Key secretKey; - - @Value("${jwt.token.secret-key}") - private String SECRET_KEY; - - @Value("${jwt.token.access-expire-length}") - private Long ACCESS_EXPIRE_LENGTH; - @Value("${jwt.token.refresh-expire-length}") - private Long REFRESH_EXPIRE_LENGTH; - - @PostConstruct - protected void init() { - secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET_KEY)); - } - - private String generateToken(Long expireLength, Claims claims) { - return Jwts.builder().setClaims(claims) - .setIssuedAt(new Date(System.currentTimeMillis())) - .setExpiration(new Date(System.currentTimeMillis() + expireLength)) - .signWith(secretKey, SignatureAlgorithm.HS256) - .compact(); - } - - public String generateAccessToken(Long userId) { - Claims claims = Jwts.claims().setSubject(String.valueOf(userId)); - return generateToken(ACCESS_EXPIRE_LENGTH, claims); - } - - public String generateRefreshToken() { - return generateToken(REFRESH_EXPIRE_LENGTH, Jwts.claims()); - } - - private String resolveToken(HttpServletRequest request, String headerName, String prefix) { - try { - String header = request.getHeader(headerName); - return prefix.isEmpty() ? header : header.substring(prefix.length()); - } catch (NullPointerException | IllegalArgumentException e) { - throw new TokenNotExistsException(); - } - } - - // JWT 검증 로직에서 userId 반환 - private Claims validateToken(String token) { - try { - return Jwts.parserBuilder().setSigningKey(secretKey) - .build().parseClaimsJws(token).getBody(); - } catch (ExpiredJwtException exception) { - throw new TokenExpiredException(); - } catch (IllegalArgumentException e) { - throw new TokenNotExistsException(); - } - } - - // 액세스 토큰 검증 후 userId 반환 - public Long validateAccessToken(HttpServletRequest request) { - String token = resolveToken(request, "AUTHORIZATION", "Bearer "); - Claims claims = validateToken(token); // JWT 검증 및 claims 반환 - return Long.parseLong(claims.getSubject()); // userId 반환 - } - - public void validateRefreshToken(HttpServletRequest request) { - String token = resolveToken(request, "REFRESH-TOKEN", ""); - validateToken(token); // 검증만 진행 - } - - private Claims getClaimsWithoutExpirationCheck(String token) { - try { - return Jwts.parserBuilder() - .setSigningKey(secretKey) - .build() - .parseClaimsJws(token) - .getBody(); - } catch (ExpiredJwtException e) { // 만료된 토큰이라도 Claims를 반환 - return e.getClaims(); - } - } - - // 리프레시 토큰으로 새로운 액세스 토큰 발급 - public String refreshAccessToken(HttpServletRequest request) { - validateRefreshToken(request); // 리프레시 토큰 검증 - String token = resolveToken(request, "AUTHORIZATION", "Bearer "); - Claims claims = getClaimsWithoutExpirationCheck(token); - return generateAccessToken(Long.parseLong(claims.getSubject())); - } -} diff --git a/src/main/java/com/Alchive/backend/controller/BoardController.java b/src/main/java/com/Alchive/backend/controller/BoardController.java index fb9786f..f18abb0 100644 --- a/src/main/java/com/Alchive/backend/controller/BoardController.java +++ b/src/main/java/com/Alchive/backend/controller/BoardController.java @@ -1,6 +1,7 @@ package com.Alchive.backend.controller; import com.Alchive.backend.config.result.ResultResponse; +import com.Alchive.backend.domain.user.User; import com.Alchive.backend.dto.request.BoardCreateRequest; import com.Alchive.backend.dto.request.BoardMemoUpdateRequest; import com.Alchive.backend.dto.request.BoardUpdateRequest; @@ -8,15 +9,15 @@ import com.Alchive.backend.dto.response.BoardDetailResponseDTO; import com.Alchive.backend.dto.response.BoardResponseDTO; import com.Alchive.backend.service.BoardService; -import com.Alchive.backend.sns.SlackService; +import com.Alchive.backend.service.SlackService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -27,15 +28,15 @@ @Tag(name = "게시물", description = "게시물 관련 api입니다. ") @RequiredArgsConstructor @RestController -@RequestMapping("/api/v1/boards") +@RequestMapping("/api/v2/boards") public class BoardController { private final BoardService boardService; private final SlackService slackService; @Operation(summary = "게시물 저장 여부 조회", description = "게시물의 저장 여부를 조회하는 메서드입니다. ") @PostMapping("/saved") - public ResponseEntity isBoardSaved(HttpServletRequest tokenRequest, @RequestBody @Valid ProblemNumberRequest problemNumberRequest) { - BoardDetailResponseDTO board = boardService.isBoardSaved(tokenRequest, problemNumberRequest); + public ResponseEntity isBoardSaved(@AuthenticationPrincipal User user, @RequestBody @Valid ProblemNumberRequest problemNumberRequest) { + BoardDetailResponseDTO board = boardService.isBoardSaved(user, problemNumberRequest); if (board != null) { return ResponseEntity.ok(ResultResponse.of(BOARD_INFO_SUCCESS, board)); } else { @@ -53,8 +54,8 @@ public ResponseEntity getBoardList(@RequestParam(value = "offset @Operation(summary = "게시물 생성", description = "새로운 게시물을 생성하는 메서드입니다. ") @PostMapping("") - public ResponseEntity createBoard(HttpServletRequest tokenRequest, @RequestBody @Valid BoardCreateRequest boardCreateRequest) { - BoardResponseDTO board = boardService.createBoard(tokenRequest, boardCreateRequest); + public ResponseEntity createBoard(@AuthenticationPrincipal User user, @RequestBody @Valid BoardCreateRequest boardCreateRequest) { + BoardResponseDTO board = boardService.createBoard(user, boardCreateRequest); // slackService.sendMessageCreateBoard(boardCreateRequest, board); return ResponseEntity.ok(ResultResponse.of(BOARD_CREATE_SUCCESS, board)); } @@ -68,22 +69,22 @@ public ResponseEntity getBoard(@PathVariable Long boardId) { @Operation(summary = "게시물 업데이트", description = "게시물 설명을 수정하는 메서드입니다. ") @PatchMapping("/{boardId}") - public ResponseEntity updateBoard(HttpServletRequest tokenRequest, @PathVariable Long boardId, @RequestBody BoardUpdateRequest updateRequest) { - BoardResponseDTO board = boardService.updateBoard(tokenRequest, boardId, updateRequest); + public ResponseEntity updateBoard(@AuthenticationPrincipal User user, @PathVariable Long boardId, @RequestBody BoardUpdateRequest updateRequest) { + BoardResponseDTO board = boardService.updateBoard(user, boardId, updateRequest); return ResponseEntity.ok(ResultResponse.of(BOARD_MEMO_UPDATE_SUCCESS, board)); } @Operation(summary = "게시물 삭제", description = "게시물을 삭제하는 메서드입니다. ") @DeleteMapping("/{boardId}") - public ResponseEntity deleteBoard(HttpServletRequest tokenRequest, @PathVariable Long boardId) { - boardService.deleteBoard(tokenRequest, boardId); + public ResponseEntity deleteBoard(@AuthenticationPrincipal User user, @PathVariable Long boardId) { + boardService.deleteBoard(user, boardId); return ResponseEntity.ok(ResultResponse.of(BOARD_DELETE_SUCCESS)); } @Operation(summary = "게시물 메모 업데이트", description = "게시물 메모를 수정하는 메서드입니다. ") @PatchMapping("/memo/{boardId}") - public ResponseEntity updateBoardMemo(HttpServletRequest tokenRequest, @PathVariable Long boardId, @RequestBody BoardMemoUpdateRequest updateRequest) { - BoardResponseDTO board = boardService.updateBoardMemo(tokenRequest, boardId, updateRequest); + public ResponseEntity updateBoardMemo(@AuthenticationPrincipal User user, @PathVariable Long boardId, @RequestBody BoardMemoUpdateRequest updateRequest) { + BoardResponseDTO board = boardService.updateBoardMemo(user, boardId, updateRequest); return ResponseEntity.ok(ResultResponse.of(BOARD_MEMO_UPDATE_SUCCESS, board)); } } \ No newline at end of file diff --git a/src/main/java/com/Alchive/backend/sns/DiscordController.java b/src/main/java/com/Alchive/backend/controller/DiscordController.java similarity index 76% rename from src/main/java/com/Alchive/backend/sns/DiscordController.java rename to src/main/java/com/Alchive/backend/controller/DiscordController.java index a7796f6..2e849ec 100644 --- a/src/main/java/com/Alchive/backend/sns/DiscordController.java +++ b/src/main/java/com/Alchive/backend/controller/DiscordController.java @@ -1,16 +1,19 @@ -package com.Alchive.backend.sns; +package com.Alchive.backend.controller; import com.Alchive.backend.config.result.ResultResponse; +import com.Alchive.backend.domain.sns.Sns; import com.Alchive.backend.domain.sns.SnsCategory; +import com.Alchive.backend.domain.user.User; import com.Alchive.backend.dto.request.SnsCreateRequest; import com.Alchive.backend.service.SnsService; +import com.Alchive.backend.service.DiscordService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.*; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import static com.Alchive.backend.config.result.ResultCode.DISCORD_DM_SEND_SUCCESS; @@ -19,14 +22,14 @@ @RequiredArgsConstructor @RestController @Slf4j -@RequestMapping("/api/v1/discord") +@RequestMapping("/api/v2/discord") public class DiscordController { private final SnsService snsService; private final DiscordService discordService; @Operation(summary = "디스코드 봇 연결", description = "디스코드 액세스 토큰을 요청하고 DM 채널을 연결하는 api입니다. ") @GetMapping("/dm/open") - public ResponseEntity openDiscordDm(HttpServletRequest tokenRequest, @RequestParam String code) { + public ResponseEntity openDiscordDm(@AuthenticationPrincipal User user, @RequestParam String code) { // Access Token 요청 String accessToken = discordService.getAccessToken(code); log.info("Access Token 반환 완료: " + accessToken); @@ -46,7 +49,7 @@ public ResponseEntity openDiscordDm(HttpServletRequest tokenRequ .channel_id(channelId) // Discord Channel Id .time("0 0 18 ? * MON") .build(); - snsService.createSns(tokenRequest, snsCreateRequest); + snsService.createSns(user, snsCreateRequest); log.info("SNS 정보 저장 완료"); // DM 전송 요청 @@ -57,9 +60,9 @@ public ResponseEntity openDiscordDm(HttpServletRequest tokenRequ @Operation(summary = "디스코드 DM 전송", description = "디스코드 DM으로 메시지를 전송하는 api입니다. ") @PostMapping("dm/send") - public ResponseEntity sendDiscordDm(HttpServletRequest tokenRequest, @RequestParam String message) { - String discordUserId = discordService.getDiscordUserId(tokenRequest); - discordService.sendDmJda(discordUserId, message); + public ResponseEntity sendDiscordDm(@AuthenticationPrincipal User user, @RequestParam String message) { + Sns discordInfo = discordService.getDiscordInfo(user); + discordService.sendDmJda(discordInfo.getSns_id(), message); return ResponseEntity.ok(ResultResponse.of(DISCORD_DM_SEND_SUCCESS)); } } diff --git a/src/main/java/com/Alchive/backend/sns/SlackController.java b/src/main/java/com/Alchive/backend/controller/SlackController.java similarity index 56% rename from src/main/java/com/Alchive/backend/sns/SlackController.java rename to src/main/java/com/Alchive/backend/controller/SlackController.java index b7a3a3f..d3d9083 100644 --- a/src/main/java/com/Alchive/backend/sns/SlackController.java +++ b/src/main/java/com/Alchive/backend/controller/SlackController.java @@ -1,19 +1,17 @@ -package com.Alchive.backend.sns; +package com.Alchive.backend.controller; -import com.Alchive.backend.config.error.exception.sns.NoSuchSnsIdException; -import com.Alchive.backend.config.jwt.TokenService; import com.Alchive.backend.config.result.ResultResponse; import com.Alchive.backend.domain.sns.Sns; -import com.Alchive.backend.domain.sns.SnsCategory; +import com.Alchive.backend.domain.user.User; import com.Alchive.backend.dto.request.SnsCreateRequest; -import com.Alchive.backend.repository.SnsReporitory; import com.Alchive.backend.service.SnsService; +import com.Alchive.backend.service.SlackService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import static com.Alchive.backend.config.result.ResultCode.SLACK_DM_SEND_SUCCESS; @@ -21,23 +19,21 @@ @Slf4j @Tag(name = "슬랙", description = "슬랙 관련 API입니다. ") @RestController -@RequestMapping("/api/v1/slack") +@RequestMapping("/api/v2/slack") @RequiredArgsConstructor public class SlackController { private final SlackService slackService; private final SnsService snsService; - private final SnsReporitory snsReporitory; - private final TokenService tokenService; @Operation(summary = "슬랙 봇 연결", description = "슬랙 액세스 토큰을 요청하고 DM 채널을 연결하는 api입니다. ") @GetMapping("/dm/open") - public ResponseEntity openSlackDm(HttpServletRequest tokenRequest, @RequestParam String code) { + public ResponseEntity openSlackDm(@AuthenticationPrincipal User user, @RequestParam String code) { // Bot Access Token, User Access Token, Slack User Id 요청 SnsCreateRequest snsCreateRequest = slackService.getSlackInfo(code); log.info("사용자 slack 정보를 불러왔습니다. "); // Slack SNS 정보 저장 - snsService.createSns(tokenRequest, snsCreateRequest); + snsService.createSns(user, snsCreateRequest); log.info("사용자 slack 정보를 저장했습니다. "); // DM 전송 요청 @@ -51,24 +47,9 @@ public ResponseEntity openSlackDm(HttpServletRequest tokenReques @Operation(summary = "슬랙 DM 전송", description = "슬랙 DM으로 메시지를 전송하는 api입니다. ") @PostMapping("dm/send") - public ResponseEntity sendSlackDm(HttpServletRequest tokenRequest, @RequestParam String message) { - Long userId = tokenService.validateAccessToken(tokenRequest); - Sns sns = snsReporitory.findByUser_IdAndCategory(userId, SnsCategory.SLACK) - .orElseThrow(NoSuchSnsIdException::new); + public ResponseEntity sendSlackDm(@AuthenticationPrincipal User user, @RequestParam String message) { + Sns sns = slackService.getSlackInfo(user); slackService.sendDm(sns.getSns_id(), sns.getBot_token(), message); return ResponseEntity.ok(ResultResponse.of(SLACK_DM_SEND_SUCCESS)); } - -// @Operation(summary = "슬랙 봇 설정", description = "슬랙 봇 추가 시 메시지를 전송하는 메서드입니다. ") -// @GetMapping("/added") -// public void addedSlackBot() { -// slackService.sendMessage(":wave: Hi from a bot written in Alchive!"); -// log.info("Slack Test"); -// } -// -// @Operation(summary = "문제 리마인더", description = "30분마다 해결하지 못한 문제를 리마인드해주는 메서드입니다. ") -// @GetMapping("/reminder") -// public void sendReminder(HttpServletRequest tokenRequest) { -// slackService.sendMessageReminderBoard(tokenRequest); -// } } \ No newline at end of file diff --git a/src/main/java/com/Alchive/backend/controller/SnsController.java b/src/main/java/com/Alchive/backend/controller/SnsController.java index 99965ce..c11ed3b 100644 --- a/src/main/java/com/Alchive/backend/controller/SnsController.java +++ b/src/main/java/com/Alchive/backend/controller/SnsController.java @@ -1,23 +1,24 @@ package com.Alchive.backend.controller; import com.Alchive.backend.config.result.ResultResponse; -import com.Alchive.backend.domain.sns.Sns; +import com.Alchive.backend.domain.user.User; import com.Alchive.backend.dto.request.SnsCreateRequest; import com.Alchive.backend.dto.response.SnsResponseDTO; import com.Alchive.backend.service.SnsService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import static com.Alchive.backend.config.result.ResultCode.*; +import static com.Alchive.backend.config.result.ResultCode.SNS_CREATE_SUCCESS; +import static com.Alchive.backend.config.result.ResultCode.SNS_INFO_SUCCESS; @Tag(name = "소셜", description = "소셜 관련 api입니다. ") @RequiredArgsConstructor @RestController -@RequestMapping("/api/v1/sns") +@RequestMapping("/api/v2/sns") public class SnsController { private final SnsService snsService; @@ -30,8 +31,8 @@ public ResponseEntity getSns(@PathVariable Long snsId) { @Operation(summary = "소셜 정보 생성", description = "소셜 정보를 생성하는 메서드입니다. ") @PostMapping("") - public ResponseEntity createSns(HttpServletRequest tokenRequest, SnsCreateRequest request) { - snsService.createSns(tokenRequest, request); + public ResponseEntity createSns(@AuthenticationPrincipal User user, SnsCreateRequest request) { + snsService.createSns(user, request); return ResponseEntity.ok(ResultResponse.of(SNS_CREATE_SUCCESS)); } } diff --git a/src/main/java/com/Alchive/backend/controller/SolutionController.java b/src/main/java/com/Alchive/backend/controller/SolutionController.java index 3a30433..61771ce 100644 --- a/src/main/java/com/Alchive/backend/controller/SolutionController.java +++ b/src/main/java/com/Alchive/backend/controller/SolutionController.java @@ -1,16 +1,17 @@ package com.Alchive.backend.controller; import com.Alchive.backend.config.result.ResultResponse; +import com.Alchive.backend.domain.user.User; import com.Alchive.backend.dto.request.SolutionCreateRequest; import com.Alchive.backend.dto.request.SolutionUpdateRequest; import com.Alchive.backend.dto.response.SolutionDetailResponseDTO; import com.Alchive.backend.service.SolutionService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import static com.Alchive.backend.config.result.ResultCode.*; @@ -18,28 +19,28 @@ @Tag(name = "풀이", description = "풀이 관련 api입니다.") @RequiredArgsConstructor @RestController -@RequestMapping("/api/v1/solutions") +@RequestMapping("/api/v2/solutions") public class SolutionController { private final SolutionService solutionService; @Operation(summary = "풀이 생성", description = "새로운 풀이를 생성하는 메서드입니다.") @PostMapping("/{boardId}") - public ResponseEntity createSolution(HttpServletRequest tokenRequest, @PathVariable Long boardId, @RequestBody @Valid SolutionCreateRequest solutionRequest) { - SolutionDetailResponseDTO solution = solutionService.createSolution(tokenRequest, boardId, solutionRequest); + public ResponseEntity createSolution(@AuthenticationPrincipal User user, @PathVariable Long boardId, @RequestBody @Valid SolutionCreateRequest solutionRequest) { + SolutionDetailResponseDTO solution = solutionService.createSolution(user, boardId, solutionRequest); return ResponseEntity.ok(ResultResponse.of(SOLUTION_CREATE_SUCCESS, solution)); } @Operation(summary = "풀이 수정", description = "풀이 내용을 수정하는 메서드입니다. ") @PatchMapping("/{solutionId}") - public ResponseEntity updateSolution(HttpServletRequest tokenRequest, @PathVariable Long solutionId, @RequestBody @Valid SolutionUpdateRequest solutionRequest) { - SolutionDetailResponseDTO solution = solutionService.updateSolution(tokenRequest, solutionId, solutionRequest); + public ResponseEntity updateSolution(@AuthenticationPrincipal User user, @PathVariable Long solutionId, @RequestBody @Valid SolutionUpdateRequest solutionRequest) { + SolutionDetailResponseDTO solution = solutionService.updateSolution(user, solutionId, solutionRequest); return ResponseEntity.ok(ResultResponse.of(SOLUTION_UPDATE_SUCCESS, solution)); } @Operation(summary = "풀이 삭제", description = "풀이를 삭제하는 메서드입니다.") @DeleteMapping("/{solutionId}") - public ResponseEntity deleteSolution(HttpServletRequest tokenRequest, @PathVariable Long solutionId) { - solutionService.deleteSolution(tokenRequest, solutionId); + public ResponseEntity deleteSolution(@AuthenticationPrincipal User user, @PathVariable Long solutionId) { + solutionService.deleteSolution(user, solutionId); return ResponseEntity.ok(ResultResponse.of(SOLUTION_DELETE_SUCCESS)); } } diff --git a/src/main/java/com/Alchive/backend/controller/UserController.java b/src/main/java/com/Alchive/backend/controller/UserController.java index 55f93a9..394f422 100644 --- a/src/main/java/com/Alchive/backend/controller/UserController.java +++ b/src/main/java/com/Alchive/backend/controller/UserController.java @@ -1,6 +1,5 @@ package com.Alchive.backend.controller; -import com.Alchive.backend.config.jwt.TokenService; import com.Alchive.backend.config.result.ResultResponse; import com.Alchive.backend.domain.user.User; import com.Alchive.backend.dto.request.UserCreateRequest; @@ -10,9 +9,9 @@ import com.Alchive.backend.service.UserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import static com.Alchive.backend.config.result.ResultCode.*; @@ -21,16 +20,15 @@ @Tag(name = "사용자", description = "사용자 관련 api입니다.") @RequiredArgsConstructor @RestController -@RequestMapping("/api/v1/users") // 공통 url +@RequestMapping("/api/v2/users") // 공통 url public class UserController { private final UserService userService; - private final TokenService tokenService; @Operation(summary = "사용자 생성 메서드", description = "user를 생성하는 메서드입니다.") @PostMapping public ResponseEntity createUser(@RequestBody UserCreateRequest createRequest) { - UserResponseDTO newUser = userService.createUser(createRequest); - return ResponseEntity.ok(ResultResponse.of(USER_CREATE_SUCCESS, newUser)); + UserResponseDTO user = userService.createUser(createRequest); + return ResponseEntity.ok(ResultResponse.of(USER_CREATE_SUCCESS, user)); } @Operation(summary = "username 중복 확인 메서드", description = "username 중복을 검사하는 메서드입니다.") @@ -43,30 +41,24 @@ public ResponseEntity isDuplicateUsername(@PathVariable String n } @Operation(summary = "프로필 조회 메서드", description = "특정 사용자의 프로필 정보를 조회하는 메서드입니다.") - @GetMapping - public ResponseEntity findUser(HttpServletRequest request) { - User user = userService.getUserDetail(request); + @GetMapping("/{userId}") + public ResponseEntity getUserDetail(@PathVariable Long userId) { + User user = userService.getUserDetail(userId); return ResponseEntity.ok(ResultResponse.of(USER_DETAIL_INFO_SUCCESS, new UserDetailResponseDTO(user))); } @Operation(summary = "프로필 수정 메서드", description = "특정 사용자의 프로필 정보를 수정하는 메서드입니다.") @PutMapping - public ResponseEntity updateUser(HttpServletRequest request, @RequestBody UserUpdateRequest updateRequest) { - User user = userService.updateUserDetail(request, updateRequest); - return ResponseEntity.ok(ResultResponse.of(USER_UPDATE_SUCCESS, new UserDetailResponseDTO(user))); + public ResponseEntity updateUser(@AuthenticationPrincipal User user, @RequestBody UserUpdateRequest updateRequest) { + User newUser = userService.updateUserDetail(user, updateRequest); + return ResponseEntity.ok(ResultResponse.of(USER_UPDATE_SUCCESS, new UserDetailResponseDTO(newUser))); } @Operation(summary = "사용자 삭제 메서드", description = "특정 사용자를 삭제하는 메서드입니다.") @DeleteMapping - public ResponseEntity deleteUser(HttpServletRequest request) { - userService.deleteUserDetail(request); + public ResponseEntity deleteUser(@AuthenticationPrincipal User user) { + userService.deleteUserDetail(user); return ResponseEntity.ok(ResultResponse.of(USER_DELETE_SUCCESS)); } - @Operation(summary = "액세스 토큰 재발급 메서드", description = "리프레시 토큰으로 액세스 토큰을 재발급하는 메서드입니다.") - @GetMapping("/auth/token") - public ResponseEntity refreshAccessToken(HttpServletRequest request) { - String accessToken = tokenService.refreshAccessToken(request); - return ResponseEntity.ok(ResultResponse.of(TOKEN_ACCESS_SUCCESS, accessToken)); - } } diff --git a/src/main/java/com/Alchive/backend/domain/board/Board.java b/src/main/java/com/Alchive/backend/domain/board/Board.java index abf3217..2a1dca2 100644 --- a/src/main/java/com/Alchive/backend/domain/board/Board.java +++ b/src/main/java/com/Alchive/backend/domain/board/Board.java @@ -62,4 +62,8 @@ public Board updateDescription(String description) { return this; } + public Board updateStatus(BoardStatus status) { + this.status = status; + return this; + } } \ No newline at end of file diff --git a/src/main/java/com/Alchive/backend/service/BoardService.java b/src/main/java/com/Alchive/backend/service/BoardService.java index 6a91311..48bd9d5 100644 --- a/src/main/java/com/Alchive/backend/service/BoardService.java +++ b/src/main/java/com/Alchive/backend/service/BoardService.java @@ -2,8 +2,6 @@ import com.Alchive.backend.config.error.exception.board.NotFoundBoardException; import com.Alchive.backend.config.error.exception.problem.NotFoundProblemException; -import com.Alchive.backend.config.error.exception.user.NoSuchUserIdException; -import com.Alchive.backend.config.jwt.TokenService; import com.Alchive.backend.domain.algorithm.Algorithm; import com.Alchive.backend.domain.algorithmProblem.AlgorithmProblem; import com.Alchive.backend.domain.board.Board; @@ -16,7 +14,6 @@ import com.Alchive.backend.dto.response.ProblemResponseDTO; import com.Alchive.backend.dto.response.SolutionResponseDTO; import com.Alchive.backend.repository.*; -import jakarta.servlet.http.HttpServletRequest; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -34,12 +31,10 @@ @Service public class BoardService { private final BoardRepository boardRepository; - private final UserRepository userRepository; private final ProblemRepository problemRepository; private final AlgorithmRepository algorithmRepository; private final AlgorithmProblemRepository algorithmProblemRepository; private final SolutionRepository solutionRepository; - private final TokenService tokenService; private final UserService userService; private BoardDetailResponseDTO toBoardDetailResponseDTO(Board board) { @@ -59,9 +54,8 @@ private BoardDetailResponseDTO toBoardDetailResponseDTO(Board board) { } // Board 저장 여부 구현 - public BoardDetailResponseDTO isBoardSaved(HttpServletRequest tokenRequest, ProblemNumberRequest problemNumberRequest) { - Long userId = tokenService.validateAccessToken(tokenRequest); - Optional board = boardRepository.findByProblem_PlatformAndProblem_NumberAndUser_Id(problemNumberRequest.getPlatform(), problemNumberRequest.getProblemNumber(), userId); + public BoardDetailResponseDTO isBoardSaved(User user, ProblemNumberRequest problemNumberRequest) { + Optional board = boardRepository.findByProblem_PlatformAndProblem_NumberAndUser_Id(problemNumberRequest.getPlatform(), problemNumberRequest.getProblemNumber(), user.getId()); return board.map(this::toBoardDetailResponseDTO).orElse(null); } @@ -80,10 +74,7 @@ public Page> getBoardList(int offset, int limit) { // 게시물 메서드 @Transactional - public BoardResponseDTO createBoard(HttpServletRequest tokenRequest, BoardCreateRequest boardCreateRequest) { - Long userId = tokenService.validateAccessToken(tokenRequest); - User user = userRepository.findById(userId) - .orElseThrow(NoSuchUserIdException::new); + public BoardResponseDTO createBoard(User user, BoardCreateRequest boardCreateRequest) { ProblemCreateRequest problemCreateRequest = boardCreateRequest.getProblemCreateRequest(); // 문제 정보 저장 여부 확인 if (!problemRepository.existsByNumberAndPlatform(problemCreateRequest.getNumber(), problemCreateRequest.getPlatform())) { @@ -103,29 +94,26 @@ public BoardDetailResponseDTO getBoardDetail(Long boardId) { } @Transactional - public BoardResponseDTO updateBoard(HttpServletRequest tokenRequest, Long boardId, BoardUpdateRequest updateRequest) { - Long userId = tokenService.validateAccessToken(tokenRequest); + public BoardResponseDTO updateBoard(User user, Long boardId, BoardUpdateRequest updateRequest) { Board board = boardRepository.findById(boardId) .orElseThrow(NotFoundBoardException::new); - userService.validateUser(userId, board.getUser().getId()); + userService.validateUser(user.getId(), board.getUser().getId()); return new BoardResponseDTO(board.updateDescription(updateRequest.getDescription())); } @Transactional - public void deleteBoard(HttpServletRequest tokenRequest, Long boardId) { - Long userId = tokenService.validateAccessToken(tokenRequest); + public void deleteBoard(User user, Long boardId) { Board board = boardRepository.findById(boardId) .orElseThrow(NotFoundBoardException::new); - userService.validateUser(userId, board.getUser().getId()); + userService.validateUser(user.getId(), board.getUser().getId()); boardRepository.delete(board); } @Transactional - public BoardResponseDTO updateBoardMemo(HttpServletRequest tokenRequest, Long boardId, BoardMemoUpdateRequest updateRequest) { - Long userId = tokenService.validateAccessToken(tokenRequest); + public BoardResponseDTO updateBoardMemo(User user, Long boardId, BoardMemoUpdateRequest updateRequest) { Board board = boardRepository.findById(boardId) .orElseThrow(NotFoundBoardException::new); - userService.validateUser(userId, board.getUser().getId()); + userService.validateUser(user.getId(), board.getUser().getId()); return new BoardResponseDTO(board.updateMemo(updateRequest.getMemo())); } diff --git a/src/main/java/com/Alchive/backend/sns/DiscordService.java b/src/main/java/com/Alchive/backend/service/DiscordService.java similarity index 84% rename from src/main/java/com/Alchive/backend/sns/DiscordService.java rename to src/main/java/com/Alchive/backend/service/DiscordService.java index 7e4907e..a3a0ce9 100644 --- a/src/main/java/com/Alchive/backend/sns/DiscordService.java +++ b/src/main/java/com/Alchive/backend/service/DiscordService.java @@ -1,33 +1,23 @@ -package com.Alchive.backend.sns; +package com.Alchive.backend.service; import com.Alchive.backend.config.error.exception.sns.InvalidGrantException; import com.Alchive.backend.config.error.exception.sns.NoSuchDiscordUserException; import com.Alchive.backend.config.error.exception.sns.NoSuchSnsIdException; -import com.Alchive.backend.config.jwt.TokenService; import com.Alchive.backend.domain.board.Board; import com.Alchive.backend.domain.sns.Sns; import com.Alchive.backend.domain.sns.SnsCategory; import com.Alchive.backend.repository.BoardRepository; import com.Alchive.backend.repository.SnsReporitory; -import com.slack.api.Slack; -import com.slack.api.methods.MethodsClient; -import com.slack.api.methods.request.chat.ChatPostMessageRequest; -import jakarta.annotation.PostConstruct; -import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.entities.User; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.client.RestTemplate; import java.time.LocalDateTime; @@ -43,7 +33,6 @@ public class DiscordService { private final JDA jda; private final BoardRepository boardRepository; private final SnsReporitory snsReporitory; - private final TokenService tokenService; @Value("${DISCORD_CLIENT_ID}") private String clientId; @@ -95,12 +84,11 @@ public String getDiscordUserIdFromAccessToken(String accessToken) { return discordUserId; } - public String getDiscordUserId(HttpServletRequest tokenRequest) { - Long userId = tokenService.validateAccessToken(tokenRequest); - Sns snsInfo = snsReporitory.findByUser_IdAndCategory(userId, SnsCategory.DISCORD) + public Sns getDiscordInfo(com.Alchive.backend.domain.user.User user) { + Long userId = user.getId(); + Sns discordInfo = snsReporitory.findByUser_IdAndCategory(userId, SnsCategory.DISCORD) .orElseThrow(NoSuchSnsIdException::new); - String discordUserId = snsInfo.getSns_id(); - return discordUserId; + return discordInfo; } public void sendDm(String channelId, String message) { @@ -146,20 +134,19 @@ public void sendDmJda(String discordUserId, String message) { } // @Scheduled(cron = "0 */1 * * * *") // todo: Quartz로 동적 스케줄링 작성하기 - public void sendMessageReminderBoard(HttpServletRequest tokenRequest) { + public void sendMessageReminderBoard(com.Alchive.backend.domain.user.User user) { LocalDateTime threeDaysAgo = LocalDateTime.now().minusDays(1); - Long userId = tokenService.validateAccessToken(tokenRequest); - - Board unSolvedBoard = boardRepository.findUnsolvedBoardAddedBefore(threeDaysAgo, userId); + Board unSolvedBoard = boardRepository.findUnsolvedBoardAddedBefore(threeDaysAgo, user.getId()); if (unSolvedBoard != null) { String message = String.format(":star-struck: %d일 전 도전했던 %d. %s 문제를 아직 풀지 못했어요. \n \n다시 도전해보세요! :facepunch: \n \n<%s|:link: 문제 풀러가기>", ChronoUnit.DAYS.between(unSolvedBoard.getCreatedAt(), LocalDateTime.now()), unSolvedBoard.getProblem().getNumber(), unSolvedBoard.getProblem().getTitle(), unSolvedBoard.getProblem().getUrl()); - String discordUserId = getDiscordUserId(tokenRequest); - sendDmJda(discordUserId,message); + + Sns discordInfo = getDiscordInfo(user); + sendDmJda(discordInfo.getSns_id(), message); } else { log.info("풀지 못한 문제가 존재하지 않습니다. "); } diff --git a/src/main/java/com/Alchive/backend/sns/SlackService.java b/src/main/java/com/Alchive/backend/service/SlackService.java similarity index 70% rename from src/main/java/com/Alchive/backend/sns/SlackService.java rename to src/main/java/com/Alchive/backend/service/SlackService.java index 5230bf8..88e9239 100644 --- a/src/main/java/com/Alchive/backend/sns/SlackService.java +++ b/src/main/java/com/Alchive/backend/service/SlackService.java @@ -1,22 +1,14 @@ -package com.Alchive.backend.sns; +package com.Alchive.backend.service; import com.Alchive.backend.config.error.exception.sns.InvalidGrantException; -import com.Alchive.backend.config.jwt.TokenService; +import com.Alchive.backend.config.error.exception.sns.NoSuchSnsIdException; import com.Alchive.backend.domain.board.Board; -import com.Alchive.backend.domain.board.BoardStatus; import com.Alchive.backend.domain.sns.Sns; import com.Alchive.backend.domain.sns.SnsCategory; -import com.Alchive.backend.dto.request.BoardCreateRequest; +import com.Alchive.backend.domain.user.User; import com.Alchive.backend.dto.request.SnsCreateRequest; -import com.Alchive.backend.dto.response.BoardResponseDTO; import com.Alchive.backend.repository.BoardRepository; import com.Alchive.backend.repository.SnsReporitory; -import com.Alchive.backend.service.SnsService; -import com.slack.api.Slack; -import com.slack.api.bolt.App; -import com.slack.api.methods.MethodsClient; -import com.slack.api.methods.request.chat.ChatPostMessageRequest; -import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -55,6 +47,8 @@ public class SlackService { private String slackBotToken; private RestTemplate restTemplate = new RestTemplate(); + private BoardRepository boardRepository; + private SnsReporitory snsReporitory; public SnsCreateRequest getSlackInfo(String code) { String getTokenUrl = "https://slack.com/api/oauth.v2.access"; @@ -92,6 +86,13 @@ public SnsCreateRequest getSlackInfo(String code) { return snsCreateRequest; } + public Sns getSlackInfo(User user) { + Long userId = user.getId(); + Sns slackInfo = snsReporitory.findByUser_IdAndCategory(userId, SnsCategory.SLACK) + .orElseThrow(NoSuchSnsIdException::new); + return slackInfo; + } + public void sendDm(String slackUserId, String botAccessToken, String message) { String sendDmUrl = "https://slack.com/api/chat.postMessage"; HttpHeaders sendDmHeaders = new HttpHeaders(); @@ -105,4 +106,23 @@ public void sendDm(String slackUserId, String botAccessToken, String message) { HttpEntity> request = new HttpEntity<>(sendDmParams, sendDmHeaders); restTemplate.postForEntity(sendDmUrl, request, Map.class); } + + // @Scheduled(cron = "0 0 * * * *") // todo: Quartz로 동적 스케줄링 작성하기 + public void sendMessageReminderBoard(User user) { + LocalDateTime threeDaysAgo = LocalDateTime.now().minusDays(1); + + Board unSolvedBoard = boardRepository.findUnsolvedBoardAddedBefore(threeDaysAgo, user.getId()); + if (unSolvedBoard != null) { + String message = String.format(":star-struck: %d일 전 도전했던 %d. %s 문제를 아직 풀지 못했어요. \n \n다시 도전해보세요! :facepunch: \n \n<%s|:link: 문제 풀러가기>", + ChronoUnit.DAYS.between(unSolvedBoard.getCreatedAt(), LocalDateTime.now()), + unSolvedBoard.getProblem().getNumber(), + unSolvedBoard.getProblem().getTitle(), + unSolvedBoard.getProblem().getUrl()); + + Sns slackInfo = getSlackInfo(user); + sendDm(slackInfo.getSns_id(), slackInfo.getBot_token(), message); + } else { + log.info("풀지 못한 문제가 존재하지 않습니다. "); + } + } } diff --git a/src/main/java/com/Alchive/backend/service/SnsService.java b/src/main/java/com/Alchive/backend/service/SnsService.java index 24f80ac..8dce77d 100644 --- a/src/main/java/com/Alchive/backend/service/SnsService.java +++ b/src/main/java/com/Alchive/backend/service/SnsService.java @@ -1,27 +1,21 @@ package com.Alchive.backend.service; -import com.Alchive.backend.config.error.exception.user.NoSuchUserIdException; -import com.Alchive.backend.config.jwt.TokenService; +import com.Alchive.backend.config.error.exception.sns.NoSuchSnsIdException; import com.Alchive.backend.domain.sns.Sns; import com.Alchive.backend.domain.user.User; import com.Alchive.backend.dto.request.SnsCreateRequest; import com.Alchive.backend.dto.response.SnsResponseDTO; import com.Alchive.backend.repository.SnsReporitory; -import com.Alchive.backend.repository.UserRepository; -import jakarta.servlet.http.HttpServletRequest; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import com.Alchive.backend.config.error.exception.sns.NoSuchSnsIdException; @RequiredArgsConstructor @Service @Slf4j public class SnsService { - private final TokenService tokenService; private final SnsReporitory snsReporitory; - private final UserRepository userRepository; public SnsResponseDTO getSns(Long snsId) { return new SnsResponseDTO(snsReporitory.findById(snsId) @@ -29,16 +23,7 @@ public SnsResponseDTO getSns(Long snsId) { } @Transactional - public void createSns(HttpServletRequest tokenRequest, SnsCreateRequest request) { - Long userId = tokenService.validateAccessToken(tokenRequest); - User user = userRepository.findById(userId) - .orElseThrow(NoSuchUserIdException::new); - - if (snsReporitory.existsByUser_IdAndCategory(userId, request.getCategory())) { - log.info("해당 카테고리의 소셜 정보가 이미 존재합니다. "); - return; - } - + public void createSns(User user, SnsCreateRequest request) { Sns sns = Sns.of(user, request); snsReporitory.save(sns); } diff --git a/src/main/java/com/Alchive/backend/service/SolutionService.java b/src/main/java/com/Alchive/backend/service/SolutionService.java index 09a1fa6..33b5f07 100644 --- a/src/main/java/com/Alchive/backend/service/SolutionService.java +++ b/src/main/java/com/Alchive/backend/service/SolutionService.java @@ -2,15 +2,17 @@ import com.Alchive.backend.config.error.exception.board.NotFoundBoardException; import com.Alchive.backend.config.error.exception.solution.NotFoundSolutionException; -import com.Alchive.backend.config.jwt.TokenService; +import com.Alchive.backend.config.error.exception.token.UnmatchedUserIdException; import com.Alchive.backend.domain.board.Board; +import com.Alchive.backend.domain.board.BoardStatus; import com.Alchive.backend.domain.solution.Solution; +import com.Alchive.backend.domain.solution.SolutionStatus; +import com.Alchive.backend.domain.user.User; import com.Alchive.backend.dto.request.SolutionCreateRequest; import com.Alchive.backend.dto.request.SolutionUpdateRequest; import com.Alchive.backend.dto.response.SolutionDetailResponseDTO; import com.Alchive.backend.repository.BoardRepository; import com.Alchive.backend.repository.SolutionRepository; -import jakarta.servlet.http.HttpServletRequest; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -20,29 +22,31 @@ public class SolutionService { private final SolutionRepository solutionRepository; private final BoardRepository boardRepository; - private final TokenService tokenService; private final UserService userService; - public SolutionDetailResponseDTO createSolution(HttpServletRequest tokenRequest, Long boardId, SolutionCreateRequest solutionRequest) { - tokenService.validateAccessToken(tokenRequest); + public SolutionDetailResponseDTO createSolution(User user, Long boardId, SolutionCreateRequest solutionRequest) { Board board = boardRepository.findById(boardId).orElseThrow(NotFoundBoardException::new); + if (board.getUser().getId() != user.getId()) { + throw new UnmatchedUserIdException(); + } + if (solutionRequest.getStatus() == SolutionStatus.CORRECT) { + board.updateStatus(BoardStatus.CORRECT); + } Solution solution = Solution.of(board, solutionRequest); return new SolutionDetailResponseDTO(solutionRepository.save(solution)); } @Transactional - public SolutionDetailResponseDTO updateSolution(HttpServletRequest tokenRequest, Long solutionId, SolutionUpdateRequest solutionRequest) { - Long userId = tokenService.validateAccessToken(tokenRequest); + public SolutionDetailResponseDTO updateSolution(User user, Long solutionId, SolutionUpdateRequest solutionRequest) { Solution solution = solutionRepository.findById(solutionId).orElseThrow(NotFoundSolutionException::new); - userService.validateUser(userId, solution.getBoard().getUser().getId()); + userService.validateUser(user.getId(), solution.getBoard().getUser().getId()); return new SolutionDetailResponseDTO(solution.update(solutionRequest)); } @Transactional - public void deleteSolution(HttpServletRequest tokenRequest, Long solutionId) { - Long userId = tokenService.validateAccessToken(tokenRequest); + public void deleteSolution(User user, Long solutionId) { Solution solution = solutionRepository.findById(solutionId).orElseThrow(NotFoundSolutionException::new); - userService.validateUser(userId, solution.getBoard().getUser().getId()); + userService.validateUser(user.getId(), solution.getBoard().getUser().getId()); solutionRepository.delete(solution); } } diff --git a/src/main/java/com/Alchive/backend/service/UserService.java b/src/main/java/com/Alchive/backend/service/UserService.java index 9253c63..38a3ff9 100644 --- a/src/main/java/com/Alchive/backend/service/UserService.java +++ b/src/main/java/com/Alchive/backend/service/UserService.java @@ -4,41 +4,42 @@ import com.Alchive.backend.config.error.exception.user.NoSuchUserIdException; import com.Alchive.backend.config.error.exception.user.UserEmailExistException; import com.Alchive.backend.config.error.exception.user.UserNameExistException; -import com.Alchive.backend.config.jwt.TokenService; +import com.Alchive.backend.config.jwt.JwtTokenProvider; import com.Alchive.backend.domain.user.User; import com.Alchive.backend.dto.request.UserCreateRequest; import com.Alchive.backend.dto.request.UserUpdateRequest; import com.Alchive.backend.dto.response.UserResponseDTO; import com.Alchive.backend.repository.UserRepository; -import jakarta.servlet.http.HttpServletRequest; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.Objects; + @RequiredArgsConstructor @Service public class UserService { private final UserRepository userRepository; - private final TokenService tokenService; + private final JwtTokenProvider jwtTokenProvider; @Transactional public UserResponseDTO createUser(UserCreateRequest request) { String email = request.getEmail(); String username = request.getName(); - if (userRepository.existsByEmail(email)) { // 중복 이메일 검사 + // 중복 이메일 검사 + if (userRepository.existsByEmail(email)) { throw new UserEmailExistException(); } - if (userRepository.existsByName(username)) { // 중복 유저 이름 검사 + // 중복 유저 이름 검사 + if (userRepository.existsByName(username)) { throw new UserNameExistException(); } - + // db에 유저 저장 - 회원 가입 User user = new User(email, username); - user = userRepository.save(user); // db에 유저 저장 - 회원 가입 - + user = userRepository.save(user); // 토큰 생성 후 전달 - Long userId = user.getId(); - String accessToken = tokenService.generateAccessToken(userId); - String refreshToken = tokenService.generateRefreshToken(); + String accessToken = jwtTokenProvider.createAccessToken(email); + String refreshToken = jwtTokenProvider.createRefreshToken(email); return new UserResponseDTO(user, accessToken, refreshToken); } @@ -46,31 +47,28 @@ public boolean isDuplicateUsername(String name) { return userRepository.existsByName(name); } - public User getUserDetail(HttpServletRequest tokenRequest) { - Long userId = tokenService.validateAccessToken(tokenRequest); + public User getUserDetail(Long userId) { return userRepository.findById(userId) .orElseThrow(NoSuchUserIdException::new); } @Transactional - public User updateUserDetail(HttpServletRequest tokenRequest, UserUpdateRequest updateRequest) { - Long userId = tokenService.validateAccessToken(tokenRequest); - User user = userRepository.findById(userId) - .orElseThrow(NoSuchUserIdException::new); + public User updateUserDetail(User user, UserUpdateRequest updateRequest) { return user.update(updateRequest.getDescription(), updateRequest.getAutoSave()); } @Transactional - public void deleteUserDetail(HttpServletRequest tokenRequest) { - Long userId = tokenService.validateAccessToken(tokenRequest); - User user = userRepository.findById(userId) - .orElseThrow(NoSuchUserIdException::new); + public void deleteUserDetail(User user) { userRepository.delete(user); } public void validateUser(Long userId, Long requestedId) { - if (requestedId != userId) { + if (!Objects.equals(requestedId, userId)) { throw new UnmatchedUserIdException(); } } + + public User findByEmail(String email) { + return userRepository.findByEmail(email).orElseThrow(NoSuchUserIdException::new); + } } \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index eb1afe2..ea16d00 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -19,20 +19,20 @@ spring: springdoc: swagger-ui: - path: /api/v1 # 접속 경로 + path: /api/v2 # 접속 경로 groups-order: DESC # 내림차순 tags-sorter: alpha # 알파벳순 정렬 operations-sorter: method # 메소드 별 정렬 disable-swagger-default-url: true display-request-duration: true api-docs: - path: /api/v1/api-docs + path: /api/v2/api-docs show-actuator: true default-consumes-media-type: application/json default-produces-media-type: application/json writer-with-default-pretty-printer: true # 예쁘게 paths-to-match: - - /api/v1/** + - /api/v2/** management: endpoints: diff --git a/src/test/java/com/Alchive/backend/service/SolutionServiceTest.java b/src/test/java/com/Alchive/backend/service/SolutionServiceTest.java index f87f64d..59fd6b6 100644 --- a/src/test/java/com/Alchive/backend/service/SolutionServiceTest.java +++ b/src/test/java/com/Alchive/backend/service/SolutionServiceTest.java @@ -1,146 +1,146 @@ -package com.Alchive.backend.service; - -import com.Alchive.backend.config.error.exception.board.NotFoundBoardException; -import com.Alchive.backend.config.error.exception.solution.NotFoundSolutionException; -import com.Alchive.backend.domain.board.Board; -import com.Alchive.backend.domain.solution.Solution; -import com.Alchive.backend.domain.solution.SolutionLanguage; -import com.Alchive.backend.domain.solution.SolutionStatus; -import com.Alchive.backend.dto.request.SolutionCreateRequest; -import com.Alchive.backend.dto.request.SolutionUpdateRequest; -import com.Alchive.backend.dto.response.SolutionDetailResponseDTO; -import com.Alchive.backend.repository.BoardRepository; -import com.Alchive.backend.repository.SolutionRepository; -import jakarta.servlet.http.HttpServletRequest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.time.LocalDateTime; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -class SolutionServiceTest { - @InjectMocks - private SolutionService solutionService; - - @Mock - private SolutionRepository solutionRepository; - - @Mock - private BoardRepository boardRepository; - - @Mock - private HttpServletRequest request; - - private Board mockBoard; - private Solution mockSolution; - private SolutionCreateRequest mockCreateRequest; - private SolutionUpdateRequest mockUpdateRequest; - - @BeforeEach - void setUp() { - // Mockito 초기화 - MockitoAnnotations.openMocks(this); - - // Mock 객체 초기화 - mockBoard = Board.builder() - .id(1L) - .build(); - - mockSolution = Solution.builder() - .id(1L) - .board(mockBoard) - .build(); - - mockCreateRequest = SolutionCreateRequest.builder() - .content("Sample content") - .language(SolutionLanguage.JAVA) - .description("Sample description") - .status(SolutionStatus.CORRECT) - .memory(128) - .time(200) - .submitAt(LocalDateTime.now()) - .build(); - - mockUpdateRequest = SolutionUpdateRequest.builder() - .description("update description") - .build(); - } - - @Test - @DisplayName("풀이 생성 성공") - void testCreateSolution() { - when(boardRepository.findById(1L)).thenReturn(Optional.of(mockBoard)); // 게시물이 존재하는 경우 - when(solutionRepository.save(any(Solution.class))).thenReturn(mockSolution); // Solution 객체 저장 시 Mock 객체 반환 - - SolutionDetailResponseDTO response = solutionService.createSolution(request, 1L, mockCreateRequest); - - assertNotNull(response); // 응답이 null이 아님을 확인 - assertEquals(mockSolution.getId(), response.getId()); // ID가 일치하는지 확인 - verify(boardRepository, times(1)).findById(1L); // 게시물 조회가 1회 발생했는지 확인 - verify(solutionRepository, times(1)).save(any(Solution.class)); // 저장 메서드가 1회 호출되었는지 확인 - } - - @Test - @DisplayName("풀이 생성 실패 - 존재하지 않는 게시물") - void testCreateSolution_BoardNotFound() { - when(boardRepository.findById(1L)).thenReturn(Optional.empty()); // 게시물이 존재하지 않는 경우 - - assertThrows(NotFoundBoardException.class, () -> { // NotFoundBoardException이 발생하는지 검증 - solutionService.createSolution(request, 1L, mockCreateRequest); - }); - } - - @Test - @DisplayName("풀이 수정 성공") - void testUpdateSolution() { - when(solutionRepository.findById(1L)).thenReturn(Optional.of(mockSolution)); - - SolutionDetailResponseDTO response = solutionService.updateSolution(request, 1L, mockUpdateRequest); - - assertNotNull(response); - assertEquals(mockSolution.getId(), response.getId()); - verify(solutionRepository, times(1)).findById(1L); - verify(solutionRepository, times(1)).save(any(Solution.class)); - } - - @Test - @DisplayName("풀이 수정 실패 - 존재하지 않는 풀이") - void testUpdateSolution_NotFound() { - when(solutionRepository.findById(1L)).thenReturn(Optional.empty()); - - assertThrows(NotFoundSolutionException.class, () -> { - solutionService.updateSolution(request, 1L, mockUpdateRequest); - }); - } - - @Test - @DisplayName("풀이 삭제 성공") - void testDeleteSolution() { - when(solutionRepository.findById(1L)).thenReturn(Optional.of(mockSolution)); - when(solutionRepository.save(any(Solution.class))).thenReturn(mockSolution); - - solutionService.deleteSolution(request, 1L); - - assertTrue(mockSolution.getIsDeleted()); // softDelete가 제대로 동작했는지 확인 - verify(solutionRepository, times(1)).findById(1L); - verify(solutionRepository, times(1)).save(any(Solution.class)); - } - - @Test - @DisplayName("풀이 삭제 실패 - 존재하지 않는 풀이") - void testDeleteSolution_NotFound() { - when(solutionRepository.findById(1L)).thenReturn(Optional.empty()); - - assertThrows(NotFoundSolutionException.class, () -> { - solutionService.deleteSolution(request, 1L); - }); - } -} \ No newline at end of file +//package com.Alchive.backend.service; +// +//import com.Alchive.backend.config.error.exception.board.NotFoundBoardException; +//import com.Alchive.backend.config.error.exception.solution.NotFoundSolutionException; +//import com.Alchive.backend.domain.board.Board; +//import com.Alchive.backend.domain.solution.Solution; +//import com.Alchive.backend.domain.solution.SolutionLanguage; +//import com.Alchive.backend.domain.solution.SolutionStatus; +//import com.Alchive.backend.dto.request.SolutionCreateRequest; +//import com.Alchive.backend.dto.request.SolutionUpdateRequest; +//import com.Alchive.backend.dto.response.SolutionDetailResponseDTO; +//import com.Alchive.backend.repository.BoardRepository; +//import com.Alchive.backend.repository.SolutionRepository; +//import jakarta.servlet.http.HttpServletRequest; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.DisplayName; +//import org.junit.jupiter.api.Test; +//import org.mockito.InjectMocks; +//import org.mockito.Mock; +//import org.mockito.MockitoAnnotations; +// +//import java.time.LocalDateTime; +//import java.util.Optional; +// +//import static org.junit.jupiter.api.Assertions.*; +//import static org.mockito.ArgumentMatchers.any; +//import static org.mockito.Mockito.*; +// +//class SolutionServiceTest { +// @InjectMocks +// private SolutionService solutionService; +// +// @Mock +// private SolutionRepository solutionRepository; +// +// @Mock +// private BoardRepository boardRepository; +// +// @Mock +// private HttpServletRequest request; +// +// private Board mockBoard; +// private Solution mockSolution; +// private SolutionCreateRequest mockCreateRequest; +// private SolutionUpdateRequest mockUpdateRequest; +// +// @BeforeEach +// void setUp() { +// // Mockito 초기화 +// MockitoAnnotations.openMocks(this); +// +// // Mock 객체 초기화 +// mockBoard = Board.builder() +// .id(1L) +// .build(); +// +// mockSolution = Solution.builder() +// .id(1L) +// .board(mockBoard) +// .build(); +// +// mockCreateRequest = SolutionCreateRequest.builder() +// .content("Sample content") +// .language(SolutionLanguage.JAVA) +// .description("Sample description") +// .status(SolutionStatus.CORRECT) +// .memory(128) +// .time(200) +// .submitAt(LocalDateTime.now()) +// .build(); +// +// mockUpdateRequest = SolutionUpdateRequest.builder() +// .description("update description") +// .build(); +// } +// +// @Test +// @DisplayName("풀이 생성 성공") +// void testCreateSolution() { +// when(boardRepository.findById(1L)).thenReturn(Optional.of(mockBoard)); // 게시물이 존재하는 경우 +// when(solutionRepository.save(any(Solution.class))).thenReturn(mockSolution); // Solution 객체 저장 시 Mock 객체 반환 +// +// SolutionDetailResponseDTO response = solutionService.createSolution(request, 1L, mockCreateRequest); +// +// assertNotNull(response); // 응답이 null이 아님을 확인 +// assertEquals(mockSolution.getId(), response.getId()); // ID가 일치하는지 확인 +// verify(boardRepository, times(1)).findById(1L); // 게시물 조회가 1회 발생했는지 확인 +// verify(solutionRepository, times(1)).save(any(Solution.class)); // 저장 메서드가 1회 호출되었는지 확인 +// } +// +// @Test +// @DisplayName("풀이 생성 실패 - 존재하지 않는 게시물") +// void testCreateSolution_BoardNotFound() { +// when(boardRepository.findById(1L)).thenReturn(Optional.empty()); // 게시물이 존재하지 않는 경우 +// +// assertThrows(NotFoundBoardException.class, () -> { // NotFoundBoardException이 발생하는지 검증 +// solutionService.createSolution(request, 1L, mockCreateRequest); +// }); +// } +// +// @Test +// @DisplayName("풀이 수정 성공") +// void testUpdateSolution() { +// when(solutionRepository.findById(1L)).thenReturn(Optional.of(mockSolution)); +// +// SolutionDetailResponseDTO response = solutionService.updateSolution(request, 1L, mockUpdateRequest); +// +// assertNotNull(response); +// assertEquals(mockSolution.getId(), response.getId()); +// verify(solutionRepository, times(1)).findById(1L); +// verify(solutionRepository, times(1)).save(any(Solution.class)); +// } +// +// @Test +// @DisplayName("풀이 수정 실패 - 존재하지 않는 풀이") +// void testUpdateSolution_NotFound() { +// when(solutionRepository.findById(1L)).thenReturn(Optional.empty()); +// +// assertThrows(NotFoundSolutionException.class, () -> { +// solutionService.updateSolution(request, 1L, mockUpdateRequest); +// }); +// } +// +// @Test +// @DisplayName("풀이 삭제 성공") +// void testDeleteSolution() { +// when(solutionRepository.findById(1L)).thenReturn(Optional.of(mockSolution)); +// when(solutionRepository.save(any(Solution.class))).thenReturn(mockSolution); +// +// solutionService.deleteSolution(request, 1L); +// +// assertTrue(mockSolution.getIsDeleted()); // softDelete가 제대로 동작했는지 확인 +// verify(solutionRepository, times(1)).findById(1L); +// verify(solutionRepository, times(1)).save(any(Solution.class)); +// } +// +// @Test +// @DisplayName("풀이 삭제 실패 - 존재하지 않는 풀이") +// void testDeleteSolution_NotFound() { +// when(solutionRepository.findById(1L)).thenReturn(Optional.empty()); +// +// assertThrows(NotFoundSolutionException.class, () -> { +// solutionService.deleteSolution(request, 1L); +// }); +// } +//} \ No newline at end of file diff --git a/src/test/java/com/Alchive/backend/service/UserServiceTest.java b/src/test/java/com/Alchive/backend/service/UserServiceTest.java index 7abacc2..ac5415f 100644 --- a/src/test/java/com/Alchive/backend/service/UserServiceTest.java +++ b/src/test/java/com/Alchive/backend/service/UserServiceTest.java @@ -1,176 +1,175 @@ -package com.Alchive.backend.service; - -import com.Alchive.backend.config.error.exception.user.NoSuchUserIdException; -import com.Alchive.backend.config.error.exception.user.UserEmailExistException; -import com.Alchive.backend.config.jwt.TokenService; -import com.Alchive.backend.domain.user.User; -import com.Alchive.backend.dto.request.UserCreateRequest; -import com.Alchive.backend.dto.request.UserUpdateRequest; -import com.Alchive.backend.dto.response.UserResponseDTO; -import com.Alchive.backend.repository.UserRepository; -import jakarta.servlet.http.HttpServletRequest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.Optional; - -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.when; - -import static org.mockito.ArgumentMatchers.any; -import static org.junit.jupiter.api.Assertions.*; - -public class UserServiceTest { - @InjectMocks - private UserService userService; - - @Mock - private UserRepository userRepository; - - @Mock - private TokenService tokenService; - - @Mock - private HttpServletRequest request; - - private User user; - - @BeforeEach - void setUp() { - // Mockito 초기화 - MockitoAnnotations.openMocks(this); - - // 테스트용 Builder 패턴을 이용해 userId까지 직접 입력 - user = User.userTestBuilder() - .id(1L) - .email("user1@test.com") - .name("user1") - .build(); - } - - @DisplayName("사용자 생성 - 성공") - @Test - void createUser_success() { - // given - UserCreateRequest createRequest = UserCreateRequest.builder() - .email(user.getEmail()) - .name(user.getName()) - .build(); - - when(userRepository.existsByEmail(createRequest.getEmail())).thenReturn(false); - when(userRepository.save(any(User.class))).thenReturn(user); - when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); - doNothing().when(tokenService).validateAccessToken(request); - when(tokenService.validateAccessToken(request)).thenReturn(user.getId()); - - // when - UserResponseDTO returnedUser = userService.createUser(createRequest); - - // then - assertNotNull(returnedUser); - assertEquals(user.getId(), returnedUser.getUserId()); - assertEquals(user.getName(), returnedUser.getUserName()); - assertEquals(user.getEmail(), returnedUser.getUserEmail()); - } - - @DisplayName("사용자 생성 - 유저 닉네임 중복") - @Test - void createUser_username_exists() { - // given - UserCreateRequest createRequest = UserCreateRequest.builder() - .email("testEmail@test.com") - .name("duplicatedUserName") - .build(); - when(userService.isDuplicateUsername(createRequest.getName())).thenReturn(true); - - // when - boolean returnedAnswer = userService.isDuplicateUsername(createRequest.getName()); - - // then - assertTrue(returnedAnswer); - } - - @DisplayName("사용자 생성 - 유저 이메일 중복") - @Test - void createUser_userEmail_exists() { - // given - UserCreateRequest createRequest = UserCreateRequest.builder() - .email("duplicatedEmail@test.com") - .name("user1") - .build(); - when(userRepository.existsByEmail(createRequest.getEmail())).thenReturn(true); - - // when, then - assertThrows(UserEmailExistException.class, () -> userService.createUser(createRequest)); - } - - @DisplayName("프로필 조회 - 성공") - @Test - void getUser_success() { - // given - doNothing().when(tokenService).validateAccessToken(request); - when(tokenService.validateAccessToken(request)).thenReturn(user.getId()); - when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); - - // when - User returnedUser = userService.getUserDetail(request); - - // then - assertNotNull(returnedUser); - assertEquals(user.getId(), returnedUser.getId()); - assertEquals(user.getEmail(), returnedUser.getEmail()); - assertEquals(user.getName(), returnedUser.getName()); - } - - @DisplayName("프로필 조회 - 존재하지 않는 유저 아이디") - @Test - void getUser_userNotFound() { - // given - doNothing().when(tokenService).validateAccessToken(request); - when(tokenService.validateAccessToken(request)).thenReturn(user.getId()); - when(userRepository.findById(user.getId())).thenReturn(Optional.empty()); - - // when, then - assertThrows(NoSuchUserIdException.class, () -> userService.getUserDetail(request)); - } - - @DisplayName("프로필 수정 - 성공") - @Test - void updateUser_success() { - // given - UserUpdateRequest updateRequest = UserUpdateRequest.builder() - .description("updatedUserDescription") - .build(); - doNothing().when(tokenService).validateAccessToken(request); - when(tokenService.validateAccessToken(request)).thenReturn(user.getId()); - when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); - when(userRepository.save(any(User.class))).thenReturn(user); - - // when - userService.updateUserDetail(request, updateRequest); - - // then - assertNotNull(user); - assertEquals(user.getDescription(), "updatedUserDescription"); - } - - @DisplayName("사용자 삭제 - 성공") - @Test - void deleteUser_success() { - // given - doNothing().when(tokenService).validateAccessToken(request); - when(tokenService.validateAccessToken(request)).thenReturn(user.getId()); - when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); - - // when - userService.deleteUserDetail(request); - when(userRepository.findById(user.getId())).thenReturn(Optional.empty()); - - // then - assertThrows(NoSuchUserIdException.class, () -> userService.getUserDetail(request)); - } -} +//package com.Alchive.backend.service; +// +//import com.Alchive.backend.config.error.exception.user.NoSuchUserIdException; +//import com.Alchive.backend.config.error.exception.user.UserEmailExistException; +//import com.Alchive.backend.domain.user.User; +//import com.Alchive.backend.dto.request.UserCreateRequest; +//import com.Alchive.backend.dto.request.UserUpdateRequest; +//import com.Alchive.backend.dto.response.UserResponseDTO; +//import com.Alchive.backend.repository.UserRepository; +//import jakarta.servlet.http.HttpServletRequest; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.DisplayName; +//import org.junit.jupiter.api.Test; +//import org.mockito.InjectMocks; +//import org.mockito.Mock; +//import org.mockito.MockitoAnnotations; +//import org.springframework.security.core.token.TokenService; +// +//import java.util.Optional; +// +//import static org.junit.jupiter.api.Assertions.*; +//import static org.mockito.ArgumentMatchers.any; +//import static org.mockito.Mockito.doNothing; +//import static org.mockito.Mockito.when; +// +//public class UserServiceTest { +// @InjectMocks +// private UserService userService; +// +// @Mock +// private UserRepository userRepository; +// +// @Mock +// private TokenService tokenService; +// +// @Mock +// private HttpServletRequest request; +// +// private User user; +// +// @BeforeEach +// void setUp() { +// // Mockito 초기화 +// MockitoAnnotations.openMocks(this); +// +// // 테스트용 Builder 패턴을 이용해 userId까지 직접 입력 +// user = User.userTestBuilder() +// .id(1L) +// .email("user1@test.com") +// .name("user1") +// .build(); +// } +// +// @DisplayName("사용자 생성 - 성공") +// @Test +// void createUser_success() { +// // given +// UserCreateRequest createRequest = UserCreateRequest.builder() +// .email(user.getEmail()) +// .name(user.getName()) +// .build(); +// +// when(userRepository.existsByEmail(createRequest.getEmail())).thenReturn(false); +// when(userRepository.save(any(User.class))).thenReturn(user); +// when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); +// doNothing().when(tokenService).validateAccessToken(request); +// when(tokenService.validateAccessToken(request)).thenReturn(user.getId()); +// +// // when +// UserResponseDTO returnedUser = userService.createUser(createRequest); +// +// // then +// assertNotNull(returnedUser); +// assertEquals(user.getId(), returnedUser.getUserId()); +// assertEquals(user.getName(), returnedUser.getUserName()); +// assertEquals(user.getEmail(), returnedUser.getUserEmail()); +// } +// +// @DisplayName("사용자 생성 - 유저 닉네임 중복") +// @Test +// void createUser_username_exists() { +// // given +// UserCreateRequest createRequest = UserCreateRequest.builder() +// .email("testEmail@test.com") +// .name("duplicatedUserName") +// .build(); +// when(userService.isDuplicateUsername(createRequest.getName())).thenReturn(true); +// +// // when +// boolean returnedAnswer = userService.isDuplicateUsername(createRequest.getName()); +// +// // then +// assertTrue(returnedAnswer); +// } +// +// @DisplayName("사용자 생성 - 유저 이메일 중복") +// @Test +// void createUser_userEmail_exists() { +// // given +// UserCreateRequest createRequest = UserCreateRequest.builder() +// .email("duplicatedEmail@test.com") +// .name("user1") +// .build(); +// when(userRepository.existsByEmail(createRequest.getEmail())).thenReturn(true); +// +// // when, then +// assertThrows(UserEmailExistException.class, () -> userService.createUser(createRequest)); +// } +// +// @DisplayName("프로필 조회 - 성공") +// @Test +// void getUser_success() { +// // given +// doNothing().when(tokenService).validateAccessToken(request); +// when(tokenService.validateAccessToken(request)).thenReturn(user.getId()); +// when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); +// +// // when +// User returnedUser = userService.getUserDetail(request); +// +// // then +// assertNotNull(returnedUser); +// assertEquals(user.getId(), returnedUser.getId()); +// assertEquals(user.getEmail(), returnedUser.getEmail()); +// assertEquals(user.getName(), returnedUser.getName()); +// } +// +// @DisplayName("프로필 조회 - 존재하지 않는 유저 아이디") +// @Test +// void getUser_userNotFound() { +// // given +// doNothing().when(tokenService).validateAccessToken(request); +// when(tokenService.validateAccessToken(request)).thenReturn(user.getId()); +// when(userRepository.findById(user.getId())).thenReturn(Optional.empty()); +// +// // when, then +// assertThrows(NoSuchUserIdException.class, () -> userService.getUserDetail(request)); +// } +// +// @DisplayName("프로필 수정 - 성공") +// @Test +// void updateUser_success() { +// // given +// UserUpdateRequest updateRequest = UserUpdateRequest.builder() +// .description("updatedUserDescription") +// .build(); +// doNothing().when(tokenService).validateAccessToken(request); +// when(tokenService.validateAccessToken(request)).thenReturn(user.getId()); +// when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); +// when(userRepository.save(any(User.class))).thenReturn(user); +// +// // when +// userService.updateUserDetail(request, updateRequest); +// +// // then +// assertNotNull(user); +// assertEquals(user.getDescription(), "updatedUserDescription"); +// } +// +// @DisplayName("사용자 삭제 - 성공") +// @Test +// void deleteUser_success() { +// // given +// doNothing().when(tokenService).validateAccessToken(request); +// when(tokenService.validateAccessToken(request)).thenReturn(user.getId()); +// when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); +// +// // when +// userService.deleteUserDetail(request); +// when(userRepository.findById(user.getId())).thenReturn(Optional.empty()); +// +// // then +// assertThrows(NoSuchUserIdException.class, () -> userService.getUserDetail(request)); +// } +//}