From b8e547adcb53636a57f48080a26b6ba96f14c26a Mon Sep 17 00:00:00 2001 From: hanjunLee00 Date: Wed, 26 Mar 2025 13:41:01 +0900 Subject: [PATCH 01/12] =?UTF-8?q?chore:=20=EB=B0=B0=ED=8F=AC=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20=ED=8A=B8=EB=A6=AC?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b8e3de2..0ab0a89 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: Deploy to EC2 on: push: - branches: [ dev ] # dev 브랜치에 푸시될 때만 실행 + branches: [ dev ] jobs: deploy: From 2257b78945b70cb23974c8a1dfe0162301a55031 Mon Sep 17 00:00:00 2001 From: hanjunLee00 Date: Wed, 26 Mar 2025 13:45:50 +0900 Subject: [PATCH 02/12] =?UTF-8?q?chore:=20=EB=B0=B0=ED=8F=AC=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20=ED=8A=B8=EB=A6=AC?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0ab0a89..d1eb37d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: Deploy to EC2 on: push: - branches: [ dev ] + branches: [ dev ] # dev에 변경사항이 있을 때 자동 배포 jobs: deploy: From 7b1841627914cf56619bfd7465723667da7bd343 Mon Sep 17 00:00:00 2001 From: hanjunLee00 Date: Wed, 26 Mar 2025 13:59:50 +0900 Subject: [PATCH 03/12] =?UTF-8?q?chore:=20=EB=B0=B0=ED=8F=AC=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20=ED=8A=B8=EB=A6=AC?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d1eb37d..0ab0a89 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: Deploy to EC2 on: push: - branches: [ dev ] # dev에 변경사항이 있을 때 자동 배포 + branches: [ dev ] jobs: deploy: From fb9b1abd883c71cd6717efcd59a2edefaca7bf3c Mon Sep 17 00:00:00 2001 From: geg222 Date: Fri, 28 Mar 2025 16:38:38 +0900 Subject: [PATCH 04/12] =?UTF-8?q?[FIX]=20refreshToken=20=EB=B0=9C=EA=B8=89?= =?UTF-8?q?=20=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [FIX] refreshToken 발급 방식으로 변경 --- .../member/controller/MemberController.java | 31 ++++-- .../back/member/entity/RefreshToken.java | 29 ++++++ .../repository/RefreshTokenRepository.java | 13 +++ .../back/member/service/MemberService.java | 99 +++++++++++++++++-- .../filter/JwtAuthenticationFilter.java | 28 ++++-- .../back/security/util/JwtUtil.java | 11 +++ 6 files changed, 187 insertions(+), 24 deletions(-) create mode 100644 src/main/java/pawparazzi/back/member/entity/RefreshToken.java create mode 100644 src/main/java/pawparazzi/back/member/repository/RefreshTokenRepository.java diff --git a/src/main/java/pawparazzi/back/member/controller/MemberController.java b/src/main/java/pawparazzi/back/member/controller/MemberController.java index c338e1a..827766f 100644 --- a/src/main/java/pawparazzi/back/member/controller/MemberController.java +++ b/src/main/java/pawparazzi/back/member/controller/MemberController.java @@ -2,14 +2,12 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import pawparazzi.back.S3.service.S3AsyncService; import pawparazzi.back.member.dto.request.LoginRequestDto; import pawparazzi.back.member.dto.request.SignUpRequestDto; import pawparazzi.back.member.dto.request.UpdateMemberRequestDto; @@ -19,7 +17,6 @@ import pawparazzi.back.member.service.MemberService; import pawparazzi.back.security.util.JwtUtil; -import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -33,7 +30,6 @@ public class MemberController { private final MemberService memberService; private final ObjectMapper objectMapper; - /** * 회원 가입 */ @@ -60,8 +56,8 @@ public CompletableFuture> registerUser( */ @PostMapping("/login") public ResponseEntity> login(@Valid @RequestBody LoginRequestDto request) { - String token = memberService.login(request); - return ResponseEntity.ok(Map.of("token", token)); + Map tokenMap = memberService.login(request); + return ResponseEntity.ok(tokenMap); } /** @@ -100,7 +96,7 @@ public CompletableFuture> updateMember( } /** - * 회원 탙퇴 + * 회원 탈퇴 */ @DeleteMapping("/delete") public ResponseEntity deleteMember(@RequestHeader("Authorization") String token) { @@ -110,7 +106,6 @@ public ResponseEntity deleteMember(@RequestHeader("Authorization") Strin return ResponseEntity.ok("회원 탈퇴 완료"); } - /** * 전체 회원 목록 조회 API */ @@ -119,4 +114,24 @@ public ResponseEntity> getAllMembers() { List members = memberService.getAllMembers(); return ResponseEntity.ok(members); } + + /** + * 로그아웃 + */ + @PostMapping("/logout") + public ResponseEntity logout(@RequestHeader("Authorization") String accessToken, + @RequestBody Map body) { + String refreshToken = body.get("refreshToken"); + accessToken = accessToken.replace("Bearer ", ""); + Long memberId = jwtUtil.extractMemberId(accessToken); + memberService.logout(memberId, refreshToken); + return ResponseEntity.ok("로그아웃 완료"); + } + + @PostMapping("/reissue") + public ResponseEntity> reissue(@RequestBody Map request) { + String refreshToken = request.get("refreshToken"); + Map tokenMap = memberService.reissueAccessToken(refreshToken); + return ResponseEntity.ok(tokenMap); + } } \ No newline at end of file diff --git a/src/main/java/pawparazzi/back/member/entity/RefreshToken.java b/src/main/java/pawparazzi/back/member/entity/RefreshToken.java new file mode 100644 index 0000000..cfbb3f3 --- /dev/null +++ b/src/main/java/pawparazzi/back/member/entity/RefreshToken.java @@ -0,0 +1,29 @@ +package pawparazzi.back.member.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RefreshToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @Column(nullable = false, length = 512) + private String token; + + @Column(nullable = false) + private LocalDateTime expiryDate; +} \ No newline at end of file diff --git a/src/main/java/pawparazzi/back/member/repository/RefreshTokenRepository.java b/src/main/java/pawparazzi/back/member/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..d66fa3c --- /dev/null +++ b/src/main/java/pawparazzi/back/member/repository/RefreshTokenRepository.java @@ -0,0 +1,13 @@ +package pawparazzi.back.member.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import pawparazzi.back.member.entity.RefreshToken; +import pawparazzi.back.member.entity.Member; + +import java.util.Optional; + +public interface RefreshTokenRepository extends JpaRepository { + Optional findByMember(Member member); + Optional findByToken(String token); + void deleteByMember(Member member); +} \ No newline at end of file diff --git a/src/main/java/pawparazzi/back/member/service/MemberService.java b/src/main/java/pawparazzi/back/member/service/MemberService.java index 195ff1f..c5afee7 100644 --- a/src/main/java/pawparazzi/back/member/service/MemberService.java +++ b/src/main/java/pawparazzi/back/member/service/MemberService.java @@ -19,16 +19,20 @@ import pawparazzi.back.member.dto.response.MemberResponseDto; import pawparazzi.back.member.dto.response.UpdateMemberResponseDto; import pawparazzi.back.member.entity.Member; +import pawparazzi.back.member.entity.RefreshToken; import pawparazzi.back.member.repository.MemberRepository; +import pawparazzi.back.member.repository.RefreshTokenRepository; import pawparazzi.back.security.util.JwtUtil; import java.io.IOException; +import java.time.LocalDateTime; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; - +import java.time.temporal.ChronoUnit; @Service @RequiredArgsConstructor @@ -41,6 +45,7 @@ public class MemberService { private final BoardMongoRepository boardMongoRepository; private final S3AsyncService s3AsyncService; private final S3UploadUtil s3UploadUtil; + private final RefreshTokenRepository refreshTokenRepository; /** * 회원가입 @@ -72,19 +77,48 @@ public CompletableFuture registerUser(SignUpRequestDto request, MultipartF /** * 로그인 */ - public String login(LoginRequestDto request) { - Optional memberOptional = memberRepository.findByEmail(request.getEmail()); - if (memberOptional.isEmpty()) { + public Map login(LoginRequestDto request) { + Member member = memberRepository.findByEmail(request.getEmail()) + .orElseThrow(() -> new BadCredentialsException("이메일 또는 비밀번호가 잘못되었습니다.")); + + if (!passwordEncoder.matches(request.getPassword(), member.getPassword())) { throw new BadCredentialsException("이메일 또는 비밀번호가 잘못되었습니다."); } - Member member = memberOptional.get(); + String accessToken = jwtUtil.generateIdToken(member.getId()); - if (!passwordEncoder.matches(request.getPassword(), member.getPassword())) { - throw new BadCredentialsException("이메일 또는 비밀번호가 잘못되었습니다."); + Optional existingTokenOpt = refreshTokenRepository.findByMember(member); + String refreshToken; + + if (existingTokenOpt.isPresent()) { + RefreshToken existingToken = existingTokenOpt.get(); + long remainingDays = ChronoUnit.DAYS.between(LocalDateTime.now(), existingToken.getExpiryDate()); + + if (remainingDays <= 1) { + // 만료 임박 → 새로 발급 + refreshToken = jwtUtil.generateRefreshToken(); + existingToken.setToken(refreshToken); + existingToken.setExpiryDate(jwtUtil.getRefreshTokenExpiryDate()); + refreshTokenRepository.save(existingToken); + } else { + // 기존 토큰 사용 + refreshToken = existingToken.getToken(); + } + } else { + // 존재하지 않으면 새로 발급 + refreshToken = jwtUtil.generateRefreshToken(); + RefreshToken newToken = RefreshToken.builder() + .member(member) + .token(refreshToken) + .expiryDate(jwtUtil.getRefreshTokenExpiryDate()) + .build(); + refreshTokenRepository.save(newToken); } - return jwtUtil.generateIdToken(member.getId()); + return Map.of( + "accessToken", accessToken, + "refreshToken", refreshToken + ); } /** @@ -222,4 +256,53 @@ public Long handleKakaoLogin(KakaoUserDto kakaoUser) { return newMember.getId(); } } + + /** + * 로그아웃 + */ + @Transactional + public void logout(Long memberId, String refreshToken) { + RefreshToken savedToken = refreshTokenRepository.findByToken(refreshToken) + .orElseThrow(() -> new IllegalArgumentException("이미 만료되었거나 존재하지 않는 토큰입니다.")); + + if (!savedToken.getMember().getId().equals(memberId)) { + throw new SecurityException("토큰의 사용자 정보가 일치하지 않습니다."); + } + + refreshTokenRepository.delete(savedToken); + } + + + @Transactional + public Map reissueAccessToken(String refreshToken) { + RefreshToken savedToken = refreshTokenRepository.findByToken(refreshToken) + .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 리프레시 토큰입니다.")); + + if (savedToken.getExpiryDate().isBefore(LocalDateTime.now())) { + refreshTokenRepository.delete(savedToken); + throw new IllegalArgumentException("리프레시 토큰이 만료되었습니다. 다시 로그인 해주세요."); + } + + Member member = savedToken.getMember(); + String newAccessToken = jwtUtil.generateIdToken(member.getId()); + + long remainingDays = ChronoUnit.DAYS.between(LocalDateTime.now(), savedToken.getExpiryDate()); + + if (remainingDays <= 1) { + // RefreshToken 재발급 + String newRefreshToken = jwtUtil.generateRefreshToken(); + savedToken.setToken(newRefreshToken); + savedToken.setExpiryDate(jwtUtil.getRefreshTokenExpiryDate()); + refreshTokenRepository.save(savedToken); + + return Map.of( + "accessToken", newAccessToken, + "refreshToken", newRefreshToken + ); + } else { + return Map.of( + "accessToken", newAccessToken + ); + } + } } \ No newline at end of file diff --git a/src/main/java/pawparazzi/back/security/filter/JwtAuthenticationFilter.java b/src/main/java/pawparazzi/back/security/filter/JwtAuthenticationFilter.java index 2b8f0e8..7c2a9a3 100644 --- a/src/main/java/pawparazzi/back/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/pawparazzi/back/security/filter/JwtAuthenticationFilter.java @@ -1,5 +1,7 @@ package pawparazzi.back.security.filter; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -28,16 +30,26 @@ protected void doFilterInternal(HttpServletRequest request, if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); - - if (jwtUtil.validateToken(token)) { - Long memberId = jwtUtil.extractMemberId(token); - UserDetails userDetails = userDetailsService.loadUserById(memberId); - - JwtAuthenticationToken authentication = - new JwtAuthenticationToken(userDetails, token, userDetails.getAuthorities()); - SecurityContextHolder.getContext().setAuthentication(authentication); + try { + if (jwtUtil.validateToken(token)) { + Long memberId = jwtUtil.extractMemberId(token); + UserDetails userDetails = userDetailsService.loadUserById(memberId); + + JwtAuthenticationToken authentication = + new JwtAuthenticationToken(userDetails, token, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (ExpiredJwtException e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401로 응답 + response.getWriter().write("Access Token Expired"); + return; + } catch (JwtException e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("Invalid JWT"); + return; } } + chain.doFilter(request, response); } } \ No newline at end of file diff --git a/src/main/java/pawparazzi/back/security/util/JwtUtil.java b/src/main/java/pawparazzi/back/security/util/JwtUtil.java index 50ba695..81949e7 100644 --- a/src/main/java/pawparazzi/back/security/util/JwtUtil.java +++ b/src/main/java/pawparazzi/back/security/util/JwtUtil.java @@ -6,7 +6,9 @@ import org.springframework.stereotype.Component; import java.security.Key; +import java.time.LocalDateTime; import java.util.Date; +import java.util.UUID; @Component public class JwtUtil { @@ -30,6 +32,15 @@ public String generateIdToken(Long memberId) { .compact(); } + public String generateRefreshToken() { + return UUID.randomUUID().toString(); + } + + public LocalDateTime getRefreshTokenExpiryDate() { + return LocalDateTime.now().plusDays(7); // 7일 + } + + // 토큰에서 사용자 ID 추출 public Long extractMemberId(String token) { return Long.parseLong(Jwts.parserBuilder() From 2ea547ad5a348ad52433a998f913118e86b9bf1b Mon Sep 17 00:00:00 2001 From: geg222 Date: Fri, 28 Mar 2025 16:48:53 +0900 Subject: [PATCH 05/12] =?UTF-8?q?[FIX]=20=ED=9A=8C=EC=9B=90=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=88=98=EC=A0=95=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [FIX] 회원 정보 수정 오류 수정 --- .../pawparazzi/back/member/service/MemberService.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/pawparazzi/back/member/service/MemberService.java b/src/main/java/pawparazzi/back/member/service/MemberService.java index c5afee7..51c4cc9 100644 --- a/src/main/java/pawparazzi/back/member/service/MemberService.java +++ b/src/main/java/pawparazzi/back/member/service/MemberService.java @@ -152,6 +152,17 @@ public CompletableFuture updateMember(Long memberId, Up String pathPrefix = "profile_images/" + member.getNickName(); String defaultImageUrl = "https://default-image-url.com/default-profile.png"; + + if (newProfileImage == null || newProfileImage.isEmpty()) { + return CompletableFuture.completedFuture(new UpdateMemberResponseDto( + member.getId(), + member.getEmail(), + member.getNickName(), + member.getName(), + member.getProfileImageUrl() + )); + } + String oldProfileImageUrl = member.getProfileImageUrl(); // 새로운 프로필 이미지 업로드 From 688484b7515c45db9f5c7735f2257a7ca285ff92 Mon Sep 17 00:00:00 2001 From: geg222 Date: Fri, 28 Mar 2025 19:29:01 +0900 Subject: [PATCH 06/12] =?UTF-8?q?[FIX]=20401=20=EC=98=A4=EB=A5=98=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [FIX] 401 오류로 변경 --- .../board/controller/BoardController.java | 23 +++++++++++-- .../comment/controller/CommentController.java | 32 ++++++++++++++++--- .../follow/controller/FollowController.java | 18 ++++++++--- .../back/likes/controller/LikeController.java | 9 +++++- .../member/controller/MemberController.java | 31 +++++++++++++++--- .../back/member/service/MemberService.java | 1 - .../back/security/config/SecurityConfig.java | 17 +++++++++- .../filter/JwtAuthenticationFilter.java | 13 ++++++-- .../back/security/util/JwtUtil.java | 19 +++++++---- .../back/walk/controller/WalkController.java | 17 ++++++++-- 10 files changed, 149 insertions(+), 31 deletions(-) diff --git a/src/main/java/pawparazzi/back/board/controller/BoardController.java b/src/main/java/pawparazzi/back/board/controller/BoardController.java index 22b8d89..77dfa02 100644 --- a/src/main/java/pawparazzi/back/board/controller/BoardController.java +++ b/src/main/java/pawparazzi/back/board/controller/BoardController.java @@ -2,7 +2,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.JwtException; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -36,7 +38,12 @@ public ResponseEntity createBoard( @RequestPart(value = "titleImage", required = false) MultipartFile titleImageFile, @RequestPart(value = "titleContent", required = false) String titleContent) { - Long memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + Long memberId; + try { + memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } BoardCreateRequestDto requestDto; try { @@ -81,9 +88,14 @@ public ResponseEntity updateBoard( @RequestPart(value = "titleImage", required = false) MultipartFile titleImageFile, @RequestPart(value = "titleContent", required = false) String titleContent) { + Long memberId; try { - Long memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + try { BoardUpdateRequestDto requestDto = objectMapper.readValue(userDataJson, BoardUpdateRequestDto.class); requestDto.setTitleContent(titleContent); @@ -110,7 +122,12 @@ public ResponseEntity> getBoardsByMember(@PathVariabl */ @DeleteMapping("/{boardId}") public ResponseEntity deleteBoard(@PathVariable Long boardId, @RequestHeader("Authorization") String token) { - Long userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + Long userId; + try { + userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } boardService.deleteBoard(boardId, userId).join(); diff --git a/src/main/java/pawparazzi/back/comment/controller/CommentController.java b/src/main/java/pawparazzi/back/comment/controller/CommentController.java index d4929bf..305f428 100644 --- a/src/main/java/pawparazzi/back/comment/controller/CommentController.java +++ b/src/main/java/pawparazzi/back/comment/controller/CommentController.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; import pawparazzi.back.comment.dto.request.CommentRequestDto; import pawparazzi.back.comment.dto.response.CommentLikeResponseDto; import pawparazzi.back.comment.dto.response.CommentLikesResponseDto; @@ -13,6 +14,9 @@ import pawparazzi.back.comment.service.CommentService; import pawparazzi.back.security.util.JwtUtil; +import io.jsonwebtoken.JwtException; +import org.springframework.http.HttpStatus; + import java.util.Map; @RestController @@ -33,7 +37,12 @@ public ResponseEntity createComment( @RequestHeader("Authorization") String token, @RequestBody @Valid CommentRequestDto requestDto) { // DTO 적용 - Long memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + Long memberId; + try { + memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + } catch (JwtException e) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Access Token Expired or Invalid"); + } CommentResponseDto response = commentService.createComment(boardId, memberId, requestDto); return ResponseEntity.ok(response); } @@ -47,7 +56,12 @@ public ResponseEntity updateComment( @RequestHeader("Authorization") String token, @RequestBody Map request) { - Long memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + Long memberId; + try { + memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + } catch (JwtException e) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Access Token Expired or Invalid"); + } String content = request.get("content"); CommentResponseDto response = commentService.updateComment(commentId, memberId, content); return ResponseEntity.ok(response); @@ -61,7 +75,12 @@ public ResponseEntity> deleteComment( @PathVariable Long commentId, @RequestHeader("Authorization") String token) { - Long memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + Long memberId; + try { + memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + } catch (JwtException e) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Access Token Expired or Invalid"); + } commentService.deleteComment(commentId, memberId); return ResponseEntity.ok(Map.of("message", "댓글이 삭제되었습니다.")); @@ -83,7 +102,12 @@ public ResponseEntity toggleCommentLike( @PathVariable Long commentId, @RequestHeader("Authorization") String token) { - Long memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + Long memberId; + try { + memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + } catch (JwtException e) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Access Token Expired or Invalid"); + } CommentLikeResponseDto response = commentLikeService.toggleCommentLike(commentId, memberId); return ResponseEntity.ok(response); diff --git a/src/main/java/pawparazzi/back/follow/controller/FollowController.java b/src/main/java/pawparazzi/back/follow/controller/FollowController.java index 4dd2a33..ce5e7e3 100644 --- a/src/main/java/pawparazzi/back/follow/controller/FollowController.java +++ b/src/main/java/pawparazzi/back/follow/controller/FollowController.java @@ -1,6 +1,8 @@ package pawparazzi.back.follow.controller; +import io.jsonwebtoken.JwtException; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import pawparazzi.back.follow.dto.FollowResponseDto; @@ -22,16 +24,24 @@ public class FollowController { public ResponseEntity follow( @PathVariable Long targetId, @RequestHeader("Authorization") String token){ - FollowResponseDto response = followService.follow(targetId, token); - return ResponseEntity.ok(response); + try { + FollowResponseDto response = followService.follow(targetId, token); + return ResponseEntity.ok(response); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } } @DeleteMapping("/{targetId}") public ResponseEntity unfollow( @PathVariable Long targetId, @RequestHeader("Authorization") String token){ - UnfollowResponseDto response = followService.unfollow(targetId, token); - return ResponseEntity.ok(response); + try { + UnfollowResponseDto response = followService.unfollow(targetId, token); + return ResponseEntity.ok(response); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } } @GetMapping("/followers/{targetId}") diff --git a/src/main/java/pawparazzi/back/likes/controller/LikeController.java b/src/main/java/pawparazzi/back/likes/controller/LikeController.java index c19c2d8..185ba47 100644 --- a/src/main/java/pawparazzi/back/likes/controller/LikeController.java +++ b/src/main/java/pawparazzi/back/likes/controller/LikeController.java @@ -1,6 +1,8 @@ package pawparazzi.back.likes.controller; +import io.jsonwebtoken.JwtException; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -28,7 +30,12 @@ public ResponseEntity toggleLike( @PathVariable Long boardId, @RequestHeader("Authorization") String token) { - Long memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + Long memberId; + try { + memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } LikeToggleResponseDto response = likeService.toggleLike(boardId, memberId); return ResponseEntity.ok(response); diff --git a/src/main/java/pawparazzi/back/member/controller/MemberController.java b/src/main/java/pawparazzi/back/member/controller/MemberController.java index 827766f..8b8fef6 100644 --- a/src/main/java/pawparazzi/back/member/controller/MemberController.java +++ b/src/main/java/pawparazzi/back/member/controller/MemberController.java @@ -2,8 +2,11 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.JwtException; import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -66,7 +69,12 @@ public ResponseEntity> login(@Valid @RequestBody LoginReques @GetMapping("/me") public ResponseEntity getCurrentUser(@RequestHeader("Authorization") String token) { token = token.replace("Bearer ", ""); - Long memberId = jwtUtil.extractMemberId(token); + Long memberId; + try { + memberId = jwtUtil.extractMemberId(token); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } Member member = memberService.findById(memberId); return ResponseEntity.ok(member); } @@ -81,7 +89,12 @@ public CompletableFuture> updateMember( @RequestPart(value = "userData", required = false) String userDataJson) { token = token.replace("Bearer ", ""); - Long memberId = jwtUtil.extractMemberId(token); + Long memberId; + try { + memberId = jwtUtil.extractMemberId(token); + } catch (JwtException e) { + return CompletableFuture.completedFuture(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()); + } try { UpdateMemberRequestDto request = (userDataJson == null || userDataJson.isBlank()) @@ -101,7 +114,12 @@ public CompletableFuture> updateMember( @DeleteMapping("/delete") public ResponseEntity deleteMember(@RequestHeader("Authorization") String token) { token = token.replace("Bearer ", ""); - Long memberId = jwtUtil.extractMemberId(token); + Long memberId; + try { + memberId = jwtUtil.extractMemberId(token); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } memberService.deleteMember(memberId); return ResponseEntity.ok("회원 탈퇴 완료"); } @@ -123,7 +141,12 @@ public ResponseEntity logout(@RequestHeader("Authorization") String acce @RequestBody Map body) { String refreshToken = body.get("refreshToken"); accessToken = accessToken.replace("Bearer ", ""); - Long memberId = jwtUtil.extractMemberId(accessToken); + Long memberId; + try { + memberId = jwtUtil.extractMemberId(accessToken); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } memberService.logout(memberId, refreshToken); return ResponseEntity.ok("로그아웃 완료"); } diff --git a/src/main/java/pawparazzi/back/member/service/MemberService.java b/src/main/java/pawparazzi/back/member/service/MemberService.java index 51c4cc9..8495eab 100644 --- a/src/main/java/pawparazzi/back/member/service/MemberService.java +++ b/src/main/java/pawparazzi/back/member/service/MemberService.java @@ -24,7 +24,6 @@ import pawparazzi.back.member.repository.RefreshTokenRepository; import pawparazzi.back.security.util.JwtUtil; -import java.io.IOException; import java.time.LocalDateTime; import java.util.List; import java.util.Map; diff --git a/src/main/java/pawparazzi/back/security/config/SecurityConfig.java b/src/main/java/pawparazzi/back/security/config/SecurityConfig.java index 74b9710..9698d17 100644 --- a/src/main/java/pawparazzi/back/security/config/SecurityConfig.java +++ b/src/main/java/pawparazzi/back/security/config/SecurityConfig.java @@ -1,5 +1,7 @@ package pawparazzi.back.security.config; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -27,6 +29,7 @@ public class SecurityConfig { private final JwtUtil jwtUtil; private final CustomUserDetailsService userDetailsService; + private final ObjectMapper objectMapper; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { @@ -55,7 +58,19 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers(HttpMethod.DELETE, "/api/replies/**").authenticated() .anyRequest().authenticated()) // 나머지는 인증 필요 - .addFilterBefore(new JwtAuthenticationFilter(jwtUtil, userDetailsService), + .exceptionHandling(exception -> exception + .authenticationEntryPoint((request, response, authException) -> { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json"); + response.getWriter().write("{\"status\":401, \"message\":\"Unauthorized - 인증 실패 또는 토큰 만료\"}"); + }) + .accessDeniedHandler((request, response, accessDeniedException) -> { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.setContentType("application/json"); + response.getWriter().write("{\"status\":403, \"message\":\"Forbidden - 접근 권한이 없습니다.\"}"); + }) + ) + .addFilterBefore(new JwtAuthenticationFilter(jwtUtil, userDetailsService, objectMapper), UsernamePasswordAuthenticationFilter.class); return http.build(); diff --git a/src/main/java/pawparazzi/back/security/filter/JwtAuthenticationFilter.java b/src/main/java/pawparazzi/back/security/filter/JwtAuthenticationFilter.java index 7c2a9a3..03ca077 100644 --- a/src/main/java/pawparazzi/back/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/pawparazzi/back/security/filter/JwtAuthenticationFilter.java @@ -1,5 +1,6 @@ package pawparazzi.back.security.filter; +import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; import jakarta.servlet.FilterChain; @@ -15,12 +16,14 @@ import pawparazzi.back.security.token.JwtAuthenticationToken; import java.io.IOException; +import java.util.Map; @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final CustomUserDetailsService userDetailsService; + private final ObjectMapper objectMapper; @Override protected void doFilterInternal(HttpServletRequest request, @@ -40,12 +43,16 @@ protected void doFilterInternal(HttpServletRequest request, SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (ExpiredJwtException e) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401로 응답 - response.getWriter().write("Access Token Expired"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json"); + objectMapper.writeValue(response.getWriter(), + Map.of("status", 401, "message", "Access Token Expired")); return; } catch (JwtException e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.getWriter().write("Invalid JWT"); + response.setContentType("application/json"); + objectMapper.writeValue(response.getWriter(), + Map.of("status", 401, "message", "Invalid JWT")); return; } } diff --git a/src/main/java/pawparazzi/back/security/util/JwtUtil.java b/src/main/java/pawparazzi/back/security/util/JwtUtil.java index 81949e7..c65b8b9 100644 --- a/src/main/java/pawparazzi/back/security/util/JwtUtil.java +++ b/src/main/java/pawparazzi/back/security/util/JwtUtil.java @@ -40,15 +40,20 @@ public LocalDateTime getRefreshTokenExpiryDate() { return LocalDateTime.now().plusDays(7); // 7일 } - // 토큰에서 사용자 ID 추출 public Long extractMemberId(String token) { - return Long.parseLong(Jwts.parserBuilder() - .setSigningKey(key) - .build() - .parseClaimsJws(token) - .getBody() - .getSubject()); + try { + return Long.parseLong(Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody() + .getSubject()); + } catch (ExpiredJwtException e) { + throw new JwtException("Access Token Expired", e); + } catch (JwtException | IllegalArgumentException e) { + throw new JwtException("Invalid JWT", e); + } } // 토큰 유효성 검증 diff --git a/src/main/java/pawparazzi/back/walk/controller/WalkController.java b/src/main/java/pawparazzi/back/walk/controller/WalkController.java index 4be6668..f952d7e 100644 --- a/src/main/java/pawparazzi/back/walk/controller/WalkController.java +++ b/src/main/java/pawparazzi/back/walk/controller/WalkController.java @@ -1,5 +1,6 @@ package pawparazzi.back.walk.controller; +import io.jsonwebtoken.JwtException; import lombok.RequiredArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.HttpStatus; @@ -41,10 +42,12 @@ public ResponseEntity createWalk( public ResponseEntity getWalk( @PathVariable Long walkId, @RequestHeader("Authorization") String token) { - try{ + try { Long userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); WalkResponseDto responseDto = walkService.getWalkById(walkId, userId); return ResponseEntity.ok(responseDto); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } catch (NoSuchElementException e) { return ResponseEntity.notFound().build(); } @@ -55,10 +58,12 @@ public ResponseEntity getWalk( public ResponseEntity deleteWalk( @PathVariable Long walkId, @RequestHeader("Authorization") String token) { - try{ + try { Long userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); walkService.deleteWalk(walkId, userId); return ResponseEntity.noContent().build(); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } catch (NoSuchElementException e) { return ResponseEntity.notFound().build(); } @@ -73,6 +78,8 @@ public ResponseEntity> getWalkByPet( Long userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); List walks = walkService.getWalksByPetId(petId, userId); return ResponseEntity.ok(walks); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } catch (NoSuchElementException e) { return ResponseEntity.notFound().build(); } @@ -83,10 +90,12 @@ public ResponseEntity> getWalkByPet( public ResponseEntity> getWalkByPetDate( @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime date, @RequestHeader("Authorization") String token){ - try{ + try { Long userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); List walks = walkService.getWalksByDate(date, userId); return ResponseEntity.ok(walks); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } catch (NoSuchElementException e) { return ResponseEntity.notFound().build(); } @@ -102,6 +111,8 @@ public ResponseEntity> getWalkByPetAndDate( Long userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); List walks = walkService.getWalksByPetIdAndDate(petId, date, userId); return ResponseEntity.ok(walks); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } catch (NoSuchElementException e) { return ResponseEntity.notFound().build(); } From 287c1426e3865f8ef4fb526ba01ccd00babc1050 Mon Sep 17 00:00:00 2001 From: geg222 Date: Fri, 28 Mar 2025 19:36:11 +0900 Subject: [PATCH 07/12] =?UTF-8?q?[FIX]=20=EB=8C=80=EB=8C=93=EA=B8=80=20401?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [FIX] 대댓글 401 에러 --- .../comment/controller/ReplyController.java | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/main/java/pawparazzi/back/comment/controller/ReplyController.java b/src/main/java/pawparazzi/back/comment/controller/ReplyController.java index 7514fa5..61b66a3 100644 --- a/src/main/java/pawparazzi/back/comment/controller/ReplyController.java +++ b/src/main/java/pawparazzi/back/comment/controller/ReplyController.java @@ -2,6 +2,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import pawparazzi.back.comment.dto.request.ReplyRequestDto; @@ -13,6 +14,7 @@ import pawparazzi.back.comment.service.ReplyService; import pawparazzi.back.security.util.JwtUtil; +import io.jsonwebtoken.JwtException; import java.util.Map; @@ -33,10 +35,13 @@ public ResponseEntity createReply( @PathVariable Long commentId, @RequestHeader("Authorization") String token, @RequestBody @Valid ReplyRequestDto requestDto) { - - Long memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - ReplyResponseDto response = replyService.createReply(commentId, memberId, requestDto); - return ResponseEntity.ok(response); + try { + Long memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + ReplyResponseDto response = replyService.createReply(commentId, memberId, requestDto); + return ResponseEntity.ok(response); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } } /** @@ -47,11 +52,14 @@ public ResponseEntity updateReply( @PathVariable Long replyId, @RequestHeader("Authorization") String token, @RequestBody Map request) { - - Long memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - String content = request.get("content"); - ReplyResponseDto response = replyService.updateReply(replyId, memberId, content); - return ResponseEntity.ok(response); + try { + Long memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + String content = request.get("content"); + ReplyResponseDto response = replyService.updateReply(replyId, memberId, content); + return ResponseEntity.ok(response); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } } /** @@ -61,11 +69,13 @@ public ResponseEntity updateReply( public ResponseEntity> deleteReply( @PathVariable Long replyId, @RequestHeader("Authorization") String token) { - - Long memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - replyService.deleteReply(replyId, memberId); - - return ResponseEntity.ok(Map.of("message", "대댓글이 삭제되었습니다.")); + try { + Long memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + replyService.deleteReply(replyId, memberId); + return ResponseEntity.ok(Map.of("message", "대댓글이 삭제되었습니다.")); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } } /** @@ -76,7 +86,6 @@ public ResponseEntity getReplies(@PathVariable Long commen return ResponseEntity.ok(replyService.getRepliesByComment(commentId)); } - /** * 대댓글 좋아요 등록/삭제 (토글) */ @@ -84,11 +93,13 @@ public ResponseEntity getReplies(@PathVariable Long commen public ResponseEntity toggleReplyLike( @PathVariable Long replyId, @RequestHeader("Authorization") String token) { - - Long memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - ReplyLikeResponseDto response = replyLikeService.toggleReplyLike(replyId, memberId); - - return ResponseEntity.ok(response); + try { + Long memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + ReplyLikeResponseDto response = replyLikeService.toggleReplyLike(replyId, memberId); + return ResponseEntity.ok(response); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } } /** From 5265e20a16656243adeb26d0d97f8a1a55ed0f98 Mon Sep 17 00:00:00 2001 From: geg222 Date: Fri, 28 Mar 2025 20:00:21 +0900 Subject: [PATCH 08/12] =?UTF-8?q?[FIX]=20401=20UNAUTHORIZED=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [FIX] 401 UNAUTHORIZED 수정 --- .../back/pet/controller/PetController.java | 37 ++++++++++++++++--- .../back/walk/controller/WalkController.java | 10 +++-- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/main/java/pawparazzi/back/pet/controller/PetController.java b/src/main/java/pawparazzi/back/pet/controller/PetController.java index 8f52f10..d3a9888 100644 --- a/src/main/java/pawparazzi/back/pet/controller/PetController.java +++ b/src/main/java/pawparazzi/back/pet/controller/PetController.java @@ -2,7 +2,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.JwtException; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -35,7 +37,12 @@ public CompletableFuture> registerPet( @RequestPart("petData") String petDataJson, @RequestPart(value = "petImage", required = false) MultipartFile petImage) { - Long userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + Long userId; + try { + userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + } catch (JwtException e) { + return CompletableFuture.completedFuture(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()); + } PetRegisterRequestDto registerDto; try { @@ -53,7 +60,12 @@ public CompletableFuture> registerPet( */ @GetMapping("/all") public ResponseEntity> getAllPets(@RequestHeader("Authorization") String token) { - Long userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + Long userId; + try { + userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } List pets = petService.getPetsByMember(userId); return ResponseEntity.ok(pets); } @@ -63,7 +75,12 @@ public ResponseEntity> getAllPets(@RequestHeader("Authoriza */ @GetMapping("/{petId}") public ResponseEntity getPet(@PathVariable Long petId, @RequestHeader("Authorization") String token) { - Long userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + Long userId; + try { + userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } return ResponseEntity.ok(petService.getPetById(petId, userId)); } @@ -77,7 +94,12 @@ public CompletableFuture> updatePet( @RequestPart(value = "petData", required = false) String petDataJson, @RequestPart(value = "petImage", required = false) MultipartFile petImage) { - Long userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + Long userId; + try { + userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + } catch (JwtException e) { + return CompletableFuture.completedFuture(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()); + } PetUpdateDto updateDto; try { @@ -100,7 +122,12 @@ public CompletableFuture>> deletePet( @PathVariable Long petId, @RequestHeader("Authorization") String token) { - Long userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + Long userId; + try { + userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + } catch (JwtException e) { + return CompletableFuture.completedFuture(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()); + } return petService.deletePet(petId, userId) .thenApply(ignored -> ResponseEntity.ok(Map.of("message", "반려동물이 삭제되었습니다."))); diff --git a/src/main/java/pawparazzi/back/walk/controller/WalkController.java b/src/main/java/pawparazzi/back/walk/controller/WalkController.java index f952d7e..5782edd 100644 --- a/src/main/java/pawparazzi/back/walk/controller/WalkController.java +++ b/src/main/java/pawparazzi/back/walk/controller/WalkController.java @@ -32,9 +32,13 @@ public ResponseEntity createWalk( @RequestBody WalkRequestDto requestDto, @RequestHeader("Authorization") String token ) { - Long userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - WalkResponseDto responseDto = walkService.createWalk(requestDto, userId); - return ResponseEntity.status(HttpStatus.CREATED).body(responseDto); + try { + Long userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + WalkResponseDto responseDto = walkService.createWalk(requestDto, userId); + return ResponseEntity.status(HttpStatus.CREATED).body(responseDto); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } } //산책 기록 조회 (산책 기록 아이디로) From acabf0b5708d82804ae258d17c1f78d10e6c992c Mon Sep 17 00:00:00 2001 From: geg222 Date: Sun, 30 Mar 2025 16:49:02 +0900 Subject: [PATCH 09/12] =?UTF-8?q?[FIX]=20kakao=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20refresh=ED=86=A0=ED=81=B0=20=EB=B0=9C=EA=B8=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [FIX] kakao 로그인 refresh토큰 발급 --- .../member/controller/AuthController.java | 24 ++++++++++++------- .../member/controller/MemberController.java | 1 - .../back/member/service/MemberService.java | 22 +++++++++-------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/main/java/pawparazzi/back/member/controller/AuthController.java b/src/main/java/pawparazzi/back/member/controller/AuthController.java index 866b98e..6904e31 100644 --- a/src/main/java/pawparazzi/back/member/controller/AuthController.java +++ b/src/main/java/pawparazzi/back/member/controller/AuthController.java @@ -4,15 +4,17 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import pawparazzi.back.member.dto.JwtResponseDto; import pawparazzi.back.member.dto.KakaoUserDto; import pawparazzi.back.member.service.KakaoAuthService; import pawparazzi.back.member.service.MemberService; +import pawparazzi.back.member.entity.Member; import pawparazzi.back.security.util.JwtUtil; import java.net.URI; +import java.util.Map; @RestController @RequestMapping("/api/auth") @@ -50,19 +52,25 @@ public ResponseEntity redirectToKakaoLogin() { * 카카오 로그인 콜백 (인가 코드 → JWT 발급) */ @GetMapping("/kakao/callback") - public ResponseEntity kakaoLogin(@RequestParam String code) { + public ResponseEntity> kakaoLogin(@RequestParam String code) { try { String accessToken = kakaoAuthService.getAccessToken(code); KakaoUserDto kakaoUser = kakaoAuthService.getUserInfo(accessToken); - Long memberId = memberService.handleKakaoLogin(kakaoUser); - String jwtToken = jwtUtil.generateIdToken(memberId); + Member member = memberService.handleKakaoLogin(kakaoUser); - String frontendRedirectUrl = "http://localhost:8082/auth/success?token=" + jwtToken; + String jwtToken = jwtUtil.generateIdToken(member.getId()); + String refreshToken = memberService.generateOrUpdateRefreshToken(member); - HttpHeaders headers = new HttpHeaders(); - headers.setLocation(URI.create(frontendRedirectUrl)); - return ResponseEntity.status(302).headers(headers).build(); + Map tokenMap = Map.of( + "accessToken", jwtToken, + "refreshToken", refreshToken + ); + return ResponseEntity.ok(tokenMap); + + } catch (IllegalStateException e) { + log.error("카카오 인증 처리 중 오류 발생: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } catch (Exception e) { log.error("카카오 로그인 처리 중 오류 발생: {}", e.getMessage()); return ResponseEntity.internalServerError().build(); diff --git a/src/main/java/pawparazzi/back/member/controller/MemberController.java b/src/main/java/pawparazzi/back/member/controller/MemberController.java index 8b8fef6..99220df 100644 --- a/src/main/java/pawparazzi/back/member/controller/MemberController.java +++ b/src/main/java/pawparazzi/back/member/controller/MemberController.java @@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.JwtException; import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; diff --git a/src/main/java/pawparazzi/back/member/service/MemberService.java b/src/main/java/pawparazzi/back/member/service/MemberService.java index 8495eab..b76938b 100644 --- a/src/main/java/pawparazzi/back/member/service/MemberService.java +++ b/src/main/java/pawparazzi/back/member/service/MemberService.java @@ -85,7 +85,15 @@ public Map login(LoginRequestDto request) { } String accessToken = jwtUtil.generateIdToken(member.getId()); + String refreshToken = generateOrUpdateRefreshToken(member); + return Map.of( + "accessToken", accessToken, + "refreshToken", refreshToken + ); + } + + public String generateOrUpdateRefreshToken(Member member) { Optional existingTokenOpt = refreshTokenRepository.findByMember(member); String refreshToken; @@ -94,17 +102,14 @@ public Map login(LoginRequestDto request) { long remainingDays = ChronoUnit.DAYS.between(LocalDateTime.now(), existingToken.getExpiryDate()); if (remainingDays <= 1) { - // 만료 임박 → 새로 발급 refreshToken = jwtUtil.generateRefreshToken(); existingToken.setToken(refreshToken); existingToken.setExpiryDate(jwtUtil.getRefreshTokenExpiryDate()); refreshTokenRepository.save(existingToken); } else { - // 기존 토큰 사용 refreshToken = existingToken.getToken(); } } else { - // 존재하지 않으면 새로 발급 refreshToken = jwtUtil.generateRefreshToken(); RefreshToken newToken = RefreshToken.builder() .member(member) @@ -114,10 +119,7 @@ public Map login(LoginRequestDto request) { refreshTokenRepository.save(newToken); } - return Map.of( - "accessToken", accessToken, - "refreshToken", refreshToken - ); + return refreshToken; } /** @@ -247,11 +249,11 @@ public List getAllMembers() { * 카카오 로그인 회원 처리 */ @Transactional - public Long handleKakaoLogin(KakaoUserDto kakaoUser) { + public Member handleKakaoLogin(KakaoUserDto kakaoUser) { Optional existingMember = memberRepository.findByEmail(kakaoUser.getEmail()); if (existingMember.isPresent()) { - return existingMember.get().getId(); + return existingMember.get(); } else { String randomPassword = passwordEncoder.encode(UUID.randomUUID().toString()); @@ -263,7 +265,7 @@ public Long handleKakaoLogin(KakaoUserDto kakaoUser) { kakaoUser.getNickname() ); memberRepository.save(newMember); - return newMember.getId(); + return newMember; } } From 9747e75c8b9f03ca36ae1955e26402afa15215b7 Mon Sep 17 00:00:00 2001 From: geg222 Date: Mon, 31 Mar 2025 11:34:38 +0900 Subject: [PATCH 10/12] =?UTF-8?q?[Refactor]=20=EB=A9=A4=EB=B2=84,=20?= =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EB=AC=BC,=20=EC=A2=8B=EC=95=84=EC=9A=94=20co?= =?UTF-8?q?ntroller,=20service=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Refactor] 멤버, 게시물, 좋아요 controller, service 분리 --- .../board/controller/BoardController.java | 63 +++------------ .../back/board/service/BoardService.java | 26 ++++-- .../back/likes/controller/LikeController.java | 14 +--- .../member/controller/MemberController.java | 79 ++++--------------- .../back/member/service/MemberService.java | 24 +++++- .../back/security/config/SecurityConfig.java | 4 +- .../back/security/user/CustomUserDetails.java | 4 + 7 files changed, 80 insertions(+), 134 deletions(-) diff --git a/src/main/java/pawparazzi/back/board/controller/BoardController.java b/src/main/java/pawparazzi/back/board/controller/BoardController.java index 77dfa02..176cf4b 100644 --- a/src/main/java/pawparazzi/back/board/controller/BoardController.java +++ b/src/main/java/pawparazzi/back/board/controller/BoardController.java @@ -1,20 +1,15 @@ package pawparazzi.back.board.controller; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.jsonwebtoken.JwtException; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import pawparazzi.back.board.dto.BoardCreateRequestDto; import pawparazzi.back.board.dto.BoardListResponseDto; import pawparazzi.back.board.dto.BoardDetailDto; -import pawparazzi.back.board.dto.BoardUpdateRequestDto; import pawparazzi.back.board.service.BoardService; -import pawparazzi.back.security.util.JwtUtil; +import pawparazzi.back.security.user.CustomUserDetails; import java.util.List; @@ -24,37 +19,21 @@ public class BoardController { private final BoardService boardService; - private final JwtUtil jwtUtil; - private final ObjectMapper objectMapper; /** * 게시물 등록 */ @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity createBoard( - @RequestHeader("Authorization") String token, + @AuthenticationPrincipal CustomUserDetails userDetails, @RequestPart("userData") String userDataJson, @RequestPart(value = "mediaFiles", required = false) List mediaFiles, @RequestPart(value = "titleImage", required = false) MultipartFile titleImageFile, @RequestPart(value = "titleContent", required = false) String titleContent) { - Long memberId; - try { - memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - } catch (JwtException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } + Long memberId = userDetails.getId(); - BoardCreateRequestDto requestDto; - try { - requestDto = objectMapper.readValue(userDataJson, BoardCreateRequestDto.class); - requestDto.setMediaFiles(mediaFiles); - requestDto.setTitleContent(titleContent); - } catch (JsonProcessingException e) { - return ResponseEntity.badRequest().body(null); - } - - BoardDetailDto response = boardService.createBoard(requestDto, memberId, titleImageFile); + BoardDetailDto response = boardService.createBoard(userDataJson, memberId, titleImageFile, mediaFiles, titleContent); return ResponseEntity.ok(response); } @@ -82,30 +61,16 @@ public ResponseEntity> getBoardList() { @PutMapping(value = "/{boardId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity updateBoard( @PathVariable Long boardId, - @RequestHeader("Authorization") String token, + @AuthenticationPrincipal CustomUserDetails userDetails, @RequestPart("userData") String userDataJson, @RequestPart(value = "mediaFiles", required = false) List mediaFiles, @RequestPart(value = "titleImage", required = false) MultipartFile titleImageFile, @RequestPart(value = "titleContent", required = false) String titleContent) { - Long memberId; - try { - memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - } catch (JwtException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - - try { - BoardUpdateRequestDto requestDto = objectMapper.readValue(userDataJson, BoardUpdateRequestDto.class); - - requestDto.setTitleContent(titleContent); + Long memberId = userDetails.getId(); - BoardDetailDto updatedBoard = boardService.updateBoard(boardId, memberId, requestDto, mediaFiles, titleImageFile).join(); - - return ResponseEntity.ok(updatedBoard); - } catch (JsonProcessingException e) { - return ResponseEntity.badRequest().body(null); - } + BoardDetailDto updatedBoard = boardService.updateBoard(boardId, memberId, userDataJson, mediaFiles, titleImageFile, titleContent).join(); + return ResponseEntity.ok(updatedBoard); } /** @@ -121,16 +86,10 @@ public ResponseEntity> getBoardsByMember(@PathVariabl * 게시물 삭제 */ @DeleteMapping("/{boardId}") - public ResponseEntity deleteBoard(@PathVariable Long boardId, @RequestHeader("Authorization") String token) { - Long userId; - try { - userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - } catch (JwtException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } + public ResponseEntity deleteBoard(@PathVariable Long boardId, @AuthenticationPrincipal CustomUserDetails userDetails) { + Long userId = userDetails.getId(); boardService.deleteBoard(boardId, userId).join(); - return ResponseEntity.noContent().build(); } } \ No newline at end of file diff --git a/src/main/java/pawparazzi/back/board/service/BoardService.java b/src/main/java/pawparazzi/back/board/service/BoardService.java index e91b861..eb1b09d 100644 --- a/src/main/java/pawparazzi/back/board/service/BoardService.java +++ b/src/main/java/pawparazzi/back/board/service/BoardService.java @@ -1,5 +1,7 @@ package pawparazzi.back.board.service; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -48,7 +50,16 @@ public class BoardService { * 게시물 등록 */ @Transactional - public BoardDetailDto createBoard(BoardCreateRequestDto requestDto, Long userId, MultipartFile titleImageFile) { + public BoardDetailDto createBoard(String userDataJson, Long userId, MultipartFile titleImageFile, List mediaFiles, String titleContent) { + BoardCreateRequestDto requestDto; + try { + requestDto = new ObjectMapper().readValue(userDataJson, BoardCreateRequestDto.class); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Invalid JSON format", e); + } + requestDto.setMediaFiles(mediaFiles); + requestDto.setTitleContent(titleContent); + Member member = memberRepository.findById(userId) .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); @@ -131,10 +142,15 @@ private String getTitleImageUrl(MultipartFile titleImageFile, List uploa * 게시물 수정 */ @Transactional - public CompletableFuture updateBoard(Long boardId, Long userId, - BoardUpdateRequestDto requestDto, - List mediaFiles, - MultipartFile titleImageFile) { + public CompletableFuture updateBoard(Long boardId, Long userId, String userDataJson, List mediaFiles, MultipartFile titleImageFile, String titleContent) { + BoardUpdateRequestDto requestDto; + try { + requestDto = new ObjectMapper().readValue(userDataJson, BoardUpdateRequestDto.class); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Invalid JSON format", e); + } + requestDto.setTitleContent(titleContent); + Board board = boardRepository.findById(boardId) .orElseThrow(() -> new EntityNotFoundException("게시물을 찾을 수 없습니다.")); diff --git a/src/main/java/pawparazzi/back/likes/controller/LikeController.java b/src/main/java/pawparazzi/back/likes/controller/LikeController.java index 185ba47..2afd27b 100644 --- a/src/main/java/pawparazzi/back/likes/controller/LikeController.java +++ b/src/main/java/pawparazzi/back/likes/controller/LikeController.java @@ -9,7 +9,7 @@ import pawparazzi.back.likes.dto.LikeResponseDto; import pawparazzi.back.likes.dto.LikeToggleResponseDto; import pawparazzi.back.likes.service.LikeService; -import pawparazzi.back.security.util.JwtUtil; +import pawparazzi.back.security.user.CustomUserDetails; import java.util.List; import java.util.Map; @@ -20,7 +20,6 @@ public class LikeController { private final LikeService likeService; - private final JwtUtil jwtUtil; /** * 좋아요 등록/삭제 @@ -28,14 +27,9 @@ public class LikeController { @PostMapping("/{boardId}/like") public ResponseEntity toggleLike( @PathVariable Long boardId, - @RequestHeader("Authorization") String token) { - - Long memberId; - try { - memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - } catch (JwtException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } + @AuthenticationPrincipal CustomUserDetails userDetails) { + + Long memberId = userDetails.getId(); LikeToggleResponseDto response = likeService.toggleLike(boardId, memberId); return ResponseEntity.ok(response); diff --git a/src/main/java/pawparazzi/back/member/controller/MemberController.java b/src/main/java/pawparazzi/back/member/controller/MemberController.java index 99220df..13b9754 100644 --- a/src/main/java/pawparazzi/back/member/controller/MemberController.java +++ b/src/main/java/pawparazzi/back/member/controller/MemberController.java @@ -1,23 +1,18 @@ package pawparazzi.back.member.controller; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.jsonwebtoken.JwtException; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import pawparazzi.back.member.dto.request.LoginRequestDto; -import pawparazzi.back.member.dto.request.SignUpRequestDto; -import pawparazzi.back.member.dto.request.UpdateMemberRequestDto; import pawparazzi.back.member.dto.response.MemberResponseDto; import pawparazzi.back.member.dto.response.UpdateMemberResponseDto; import pawparazzi.back.member.entity.Member; import pawparazzi.back.member.service.MemberService; -import pawparazzi.back.security.util.JwtUtil; +import pawparazzi.back.security.user.CustomUserDetails; import java.util.List; import java.util.Map; @@ -28,9 +23,7 @@ @RequiredArgsConstructor public class MemberController { - private final JwtUtil jwtUtil; private final MemberService memberService; - private final ObjectMapper objectMapper; /** * 회원 가입 @@ -40,17 +33,10 @@ public CompletableFuture> registerUser( @RequestPart(value = "profileImage", required = false) MultipartFile profileImage, @RequestPart("userData") String userDataJson) { - // JSON 데이터를 DTO로 변환 - SignUpRequestDto request; - try { - request = objectMapper.readValue(userDataJson, SignUpRequestDto.class); - } catch (JsonProcessingException e) { - return CompletableFuture.completedFuture(ResponseEntity.badRequest().body("Invalid JSON format")); - } - // 비동기 회원가입 처리 후 응답 반환 - return memberService.registerUser(request, profileImage) - .thenApply(unused -> ResponseEntity.ok("회원가입 성공")); + return memberService.registerUser(userDataJson, profileImage) + .thenApply(unused -> ResponseEntity.ok("회원가입 성공")) + .exceptionally(ex -> ResponseEntity.badRequest().body("Invalid JSON format")); } /** @@ -66,14 +52,8 @@ public ResponseEntity> login(@Valid @RequestBody LoginReques * 사용자 정보 조회 */ @GetMapping("/me") - public ResponseEntity getCurrentUser(@RequestHeader("Authorization") String token) { - token = token.replace("Bearer ", ""); - Long memberId; - try { - memberId = jwtUtil.extractMemberId(token); - } catch (JwtException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } + public ResponseEntity getCurrentUser(@AuthenticationPrincipal CustomUserDetails userDetails) { + Long memberId = userDetails.getId(); Member member = memberService.findById(memberId); return ResponseEntity.ok(member); } @@ -83,43 +63,23 @@ public ResponseEntity getCurrentUser(@RequestHeader("Authorization") Str */ @PatchMapping(value = "/me", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public CompletableFuture> updateMember( - @RequestHeader("Authorization") String token, + @AuthenticationPrincipal CustomUserDetails userDetails, @RequestPart(value = "profileImage", required = false) MultipartFile profileImage, @RequestPart(value = "userData", required = false) String userDataJson) { - token = token.replace("Bearer ", ""); - Long memberId; - try { - memberId = jwtUtil.extractMemberId(token); - } catch (JwtException e) { - return CompletableFuture.completedFuture(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()); - } - - try { - UpdateMemberRequestDto request = (userDataJson == null || userDataJson.isBlank()) - ? new UpdateMemberRequestDto() - : objectMapper.readValue(userDataJson, UpdateMemberRequestDto.class); + Long memberId = userDetails.getId(); - return memberService.updateMember(memberId, request, profileImage) - .thenApply(ResponseEntity::ok); - } catch (JsonProcessingException e) { - return CompletableFuture.completedFuture(ResponseEntity.badRequest().body(null)); - } + return memberService.updateMember(memberId, userDataJson, profileImage) + .thenApply(ResponseEntity::ok) + .exceptionally(ex -> ResponseEntity.badRequest().body(null)); } /** * 회원 탈퇴 */ @DeleteMapping("/delete") - public ResponseEntity deleteMember(@RequestHeader("Authorization") String token) { - token = token.replace("Bearer ", ""); - Long memberId; - try { - memberId = jwtUtil.extractMemberId(token); - } catch (JwtException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - memberService.deleteMember(memberId); + public ResponseEntity deleteMember(@AuthenticationPrincipal CustomUserDetails userDetails) { + memberService.deleteMember(userDetails.getId()); return ResponseEntity.ok("회원 탈퇴 완료"); } @@ -136,17 +96,10 @@ public ResponseEntity> getAllMembers() { * 로그아웃 */ @PostMapping("/logout") - public ResponseEntity logout(@RequestHeader("Authorization") String accessToken, + public ResponseEntity logout(@AuthenticationPrincipal CustomUserDetails userDetails, @RequestBody Map body) { String refreshToken = body.get("refreshToken"); - accessToken = accessToken.replace("Bearer ", ""); - Long memberId; - try { - memberId = jwtUtil.extractMemberId(accessToken); - } catch (JwtException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - memberService.logout(memberId, refreshToken); + memberService.logout(userDetails.getId(), refreshToken); return ResponseEntity.ok("로그아웃 완료"); } diff --git a/src/main/java/pawparazzi/back/member/service/MemberService.java b/src/main/java/pawparazzi/back/member/service/MemberService.java index b76938b..fde334b 100644 --- a/src/main/java/pawparazzi/back/member/service/MemberService.java +++ b/src/main/java/pawparazzi/back/member/service/MemberService.java @@ -1,5 +1,7 @@ package pawparazzi.back.member.service; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.EntityNotFoundException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; @@ -45,12 +47,20 @@ public class MemberService { private final S3AsyncService s3AsyncService; private final S3UploadUtil s3UploadUtil; private final RefreshTokenRepository refreshTokenRepository; + private final ObjectMapper objectMapper; /** * 회원가입 */ @Transactional - public CompletableFuture registerUser(SignUpRequestDto request, MultipartFile profileImage) { + public CompletableFuture registerUser(String userDataJson, MultipartFile profileImage) { + SignUpRequestDto request; + try { + request = objectMapper.readValue(userDataJson, SignUpRequestDto.class); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Invalid JSON format"); + } + if (memberRepository.existsByEmail(request.getEmail())) { throw new IllegalArgumentException("이미 가입된 이메일입니다."); } @@ -133,7 +143,16 @@ public Member findById(Long id) { /** * 회원 정보 수정 */ - public CompletableFuture updateMember(Long memberId, UpdateMemberRequestDto request, MultipartFile newProfileImage) { + public CompletableFuture updateMember(Long memberId, String userDataJson, MultipartFile newProfileImage) { + UpdateMemberRequestDto request; + try { + request = (userDataJson == null || userDataJson.isBlank()) + ? new UpdateMemberRequestDto() + : objectMapper.readValue(userDataJson, UpdateMemberRequestDto.class); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Invalid JSON format"); + } + Member member = memberRepository.findById(memberId) .orElseThrow(() -> new EntityNotFoundException("회원 정보를 찾을 수 없습니다.")); @@ -151,6 +170,7 @@ public CompletableFuture updateMember(Long memberId, Up member.setName(request.getName()); } + // 프로필 사진 수정 String pathPrefix = "profile_images/" + member.getNickName(); String defaultImageUrl = "https://default-image-url.com/default-profile.png"; diff --git a/src/main/java/pawparazzi/back/security/config/SecurityConfig.java b/src/main/java/pawparazzi/back/security/config/SecurityConfig.java index 9698d17..6da69d8 100644 --- a/src/main/java/pawparazzi/back/security/config/SecurityConfig.java +++ b/src/main/java/pawparazzi/back/security/config/SecurityConfig.java @@ -62,12 +62,12 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .authenticationEntryPoint((request, response, authException) -> { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); - response.getWriter().write("{\"status\":401, \"message\":\"Unauthorized - 인증 실패 또는 토큰 만료\"}"); + response.getWriter().write("{\"status\":401, \"message\":\"Unauthorized\"}"); }) .accessDeniedHandler((request, response, accessDeniedException) -> { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType("application/json"); - response.getWriter().write("{\"status\":403, \"message\":\"Forbidden - 접근 권한이 없습니다.\"}"); + response.getWriter().write("{\"status\":403, \"message\":\"Forbidden\"}"); }) ) .addFilterBefore(new JwtAuthenticationFilter(jwtUtil, userDetailsService, objectMapper), diff --git a/src/main/java/pawparazzi/back/security/user/CustomUserDetails.java b/src/main/java/pawparazzi/back/security/user/CustomUserDetails.java index 13c361f..8c1211d 100644 --- a/src/main/java/pawparazzi/back/security/user/CustomUserDetails.java +++ b/src/main/java/pawparazzi/back/security/user/CustomUserDetails.java @@ -13,6 +13,10 @@ public class CustomUserDetails implements UserDetails { private final Member member; + public Long getId() { + return member.getId(); + } + public CustomUserDetails(Member member) { this.member = member; } From 2acd88e362c05e2fcf0c2cfa57a2aa80b77dd13b Mon Sep 17 00:00:00 2001 From: geg222 Date: Mon, 31 Mar 2025 19:30:40 +0900 Subject: [PATCH 11/12] =?UTF-8?q?[Refactor]=20Comment,=20pet=20controller?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Refactor] Comment, pet controller 로직 분리 --- .../comment/controller/CommentController.java | 51 ++++------- .../comment/controller/ReplyController.java | 58 +++++-------- .../comment/service/CommentLikeService.java | 1 - .../back/pet/controller/PetController.java | 84 +++++-------------- .../back/pet/service/PetService.java | 29 ++----- .../back/pet/util/PetImageHelper.java | 39 +++++++++ 6 files changed, 101 insertions(+), 161 deletions(-) create mode 100644 src/main/java/pawparazzi/back/pet/util/PetImageHelper.java diff --git a/src/main/java/pawparazzi/back/comment/controller/CommentController.java b/src/main/java/pawparazzi/back/comment/controller/CommentController.java index 305f428..5a8ccc8 100644 --- a/src/main/java/pawparazzi/back/comment/controller/CommentController.java +++ b/src/main/java/pawparazzi/back/comment/controller/CommentController.java @@ -4,7 +4,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import org.springframework.web.server.ResponseStatusException; import pawparazzi.back.comment.dto.request.CommentRequestDto; import pawparazzi.back.comment.dto.response.CommentLikeResponseDto; import pawparazzi.back.comment.dto.response.CommentLikesResponseDto; @@ -12,10 +11,9 @@ import pawparazzi.back.comment.dto.response.CommentListResponseDto; import pawparazzi.back.comment.service.CommentLikeService; import pawparazzi.back.comment.service.CommentService; -import pawparazzi.back.security.util.JwtUtil; +import pawparazzi.back.security.user.CustomUserDetails; +import org.springframework.security.core.annotation.AuthenticationPrincipal; -import io.jsonwebtoken.JwtException; -import org.springframework.http.HttpStatus; import java.util.Map; @@ -26,7 +24,6 @@ public class CommentController { private final CommentService commentService; private final CommentLikeService commentLikeService; - private final JwtUtil jwtUtil; /** * 댓글 작성 @@ -34,15 +31,10 @@ public class CommentController { @PostMapping("/{boardId}") public ResponseEntity createComment( @PathVariable Long boardId, - @RequestHeader("Authorization") String token, - @RequestBody @Valid CommentRequestDto requestDto) { // DTO 적용 - - Long memberId; - try { - memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - } catch (JwtException e) { - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Access Token Expired or Invalid"); - } + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestBody @Valid CommentRequestDto requestDto) { + + Long memberId = userDetails.getId(); CommentResponseDto response = commentService.createComment(boardId, memberId, requestDto); return ResponseEntity.ok(response); } @@ -53,15 +45,10 @@ public ResponseEntity createComment( @PutMapping("/{commentId}") public ResponseEntity updateComment( @PathVariable Long commentId, - @RequestHeader("Authorization") String token, + @AuthenticationPrincipal CustomUserDetails userDetails, @RequestBody Map request) { - Long memberId; - try { - memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - } catch (JwtException e) { - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Access Token Expired or Invalid"); - } + Long memberId = userDetails.getId(); String content = request.get("content"); CommentResponseDto response = commentService.updateComment(commentId, memberId, content); return ResponseEntity.ok(response); @@ -73,14 +60,9 @@ public ResponseEntity updateComment( @DeleteMapping("/{commentId}") public ResponseEntity> deleteComment( @PathVariable Long commentId, - @RequestHeader("Authorization") String token) { - - Long memberId; - try { - memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - } catch (JwtException e) { - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Access Token Expired or Invalid"); - } + @AuthenticationPrincipal CustomUserDetails userDetails) { + + Long memberId = userDetails.getId(); commentService.deleteComment(commentId, memberId); return ResponseEntity.ok(Map.of("message", "댓글이 삭제되었습니다.")); @@ -100,14 +82,9 @@ public ResponseEntity getComments(@PathVariable Long boa @PostMapping("/{commentId}/like") public ResponseEntity toggleCommentLike( @PathVariable Long commentId, - @RequestHeader("Authorization") String token) { - - Long memberId; - try { - memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - } catch (JwtException e) { - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Access Token Expired or Invalid"); - } + @AuthenticationPrincipal CustomUserDetails userDetails) { + + Long memberId = userDetails.getId(); CommentLikeResponseDto response = commentLikeService.toggleCommentLike(commentId, memberId); return ResponseEntity.ok(response); diff --git a/src/main/java/pawparazzi/back/comment/controller/ReplyController.java b/src/main/java/pawparazzi/back/comment/controller/ReplyController.java index 61b66a3..e50fe05 100644 --- a/src/main/java/pawparazzi/back/comment/controller/ReplyController.java +++ b/src/main/java/pawparazzi/back/comment/controller/ReplyController.java @@ -2,9 +2,9 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import pawparazzi.back.comment.dto.request.ReplyRequestDto; import pawparazzi.back.comment.dto.response.ReplyLikeResponseDto; import pawparazzi.back.comment.dto.response.ReplyLikesResponseDto; @@ -12,9 +12,7 @@ import pawparazzi.back.comment.dto.response.ReplyListResponseDto; import pawparazzi.back.comment.service.ReplyLikeService; import pawparazzi.back.comment.service.ReplyService; -import pawparazzi.back.security.util.JwtUtil; - -import io.jsonwebtoken.JwtException; +import pawparazzi.back.security.user.CustomUserDetails; import java.util.Map; @@ -25,7 +23,6 @@ public class ReplyController { private final ReplyService replyService; private final ReplyLikeService replyLikeService; - private final JwtUtil jwtUtil; /** * 대댓글 작성 @@ -33,15 +30,11 @@ public class ReplyController { @PostMapping("/{commentId}") public ResponseEntity createReply( @PathVariable Long commentId, - @RequestHeader("Authorization") String token, + @AuthenticationPrincipal CustomUserDetails userDetails, @RequestBody @Valid ReplyRequestDto requestDto) { - try { - Long memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - ReplyResponseDto response = replyService.createReply(commentId, memberId, requestDto); - return ResponseEntity.ok(response); - } catch (JwtException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } + Long memberId = userDetails.getId(); + ReplyResponseDto response = replyService.createReply(commentId, memberId, requestDto); + return ResponseEntity.ok(response); } /** @@ -50,16 +43,12 @@ public ResponseEntity createReply( @PutMapping("/{replyId}") public ResponseEntity updateReply( @PathVariable Long replyId, - @RequestHeader("Authorization") String token, + @AuthenticationPrincipal CustomUserDetails userDetails, @RequestBody Map request) { - try { - Long memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - String content = request.get("content"); - ReplyResponseDto response = replyService.updateReply(replyId, memberId, content); - return ResponseEntity.ok(response); - } catch (JwtException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } + Long memberId = userDetails.getId(); + String content = request.get("content"); + ReplyResponseDto response = replyService.updateReply(replyId, memberId, content); + return ResponseEntity.ok(response); } /** @@ -68,14 +57,10 @@ public ResponseEntity updateReply( @DeleteMapping("/{replyId}") public ResponseEntity> deleteReply( @PathVariable Long replyId, - @RequestHeader("Authorization") String token) { - try { - Long memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - replyService.deleteReply(replyId, memberId); - return ResponseEntity.ok(Map.of("message", "대댓글이 삭제되었습니다.")); - } catch (JwtException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } + @AuthenticationPrincipal CustomUserDetails userDetails) { + Long memberId = userDetails.getId(); + replyService.deleteReply(replyId, memberId); + return ResponseEntity.ok(Map.of("message", "대댓글이 삭제되었습니다.")); } /** @@ -92,14 +77,11 @@ public ResponseEntity getReplies(@PathVariable Long commen @PostMapping("/{replyId}/like") public ResponseEntity toggleReplyLike( @PathVariable Long replyId, - @RequestHeader("Authorization") String token) { - try { - Long memberId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - ReplyLikeResponseDto response = replyLikeService.toggleReplyLike(replyId, memberId); - return ResponseEntity.ok(response); - } catch (JwtException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } + @AuthenticationPrincipal CustomUserDetails userDetails) { + + Long memberId = userDetails.getId(); + ReplyLikeResponseDto response = replyLikeService.toggleReplyLike(replyId, memberId); + return ResponseEntity.ok(response); } /** diff --git a/src/main/java/pawparazzi/back/comment/service/CommentLikeService.java b/src/main/java/pawparazzi/back/comment/service/CommentLikeService.java index b7be5f0..c2f8841 100644 --- a/src/main/java/pawparazzi/back/comment/service/CommentLikeService.java +++ b/src/main/java/pawparazzi/back/comment/service/CommentLikeService.java @@ -14,7 +14,6 @@ import pawparazzi.back.member.repository.MemberRepository; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; @Service diff --git a/src/main/java/pawparazzi/back/pet/controller/PetController.java b/src/main/java/pawparazzi/back/pet/controller/PetController.java index d3a9888..ca5a98d 100644 --- a/src/main/java/pawparazzi/back/pet/controller/PetController.java +++ b/src/main/java/pawparazzi/back/pet/controller/PetController.java @@ -1,19 +1,16 @@ package pawparazzi.back.pet.controller; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.jsonwebtoken.JwtException; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import pawparazzi.back.pet.dto.PetRegisterRequestDto; import pawparazzi.back.pet.dto.PetResponseDto; import pawparazzi.back.pet.dto.PetUpdateDto; import pawparazzi.back.pet.service.PetService; -import pawparazzi.back.security.util.JwtUtil; +import pawparazzi.back.security.user.CustomUserDetails; import java.util.List; import java.util.Map; @@ -25,47 +22,27 @@ public class PetController { private final PetService petService; - private final JwtUtil jwtUtil; - private final ObjectMapper objectMapper; /** * 반려동물 등록 */ @PostMapping(value = "/register", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public CompletableFuture> registerPet( - @RequestHeader("Authorization") String token, - @RequestPart("petData") String petDataJson, + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestPart("petData") PetRegisterRequestDto requestDto, @RequestPart(value = "petImage", required = false) MultipartFile petImage) { - Long userId; - try { - userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - } catch (JwtException e) { - return CompletableFuture.completedFuture(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()); - } - - PetRegisterRequestDto registerDto; - try { - registerDto = objectMapper.readValue(petDataJson, PetRegisterRequestDto.class); - } catch (JsonProcessingException e) { - return CompletableFuture.completedFuture(ResponseEntity.badRequest().build()); - } - - return petService.registerPet(userId, registerDto, petImage) - .thenApply(ResponseEntity::ok); + return petService.registerPet(userDetails.getId(), requestDto, petImage) + .thenApply(ResponseEntity::ok) + .exceptionally(ex -> ResponseEntity.badRequest().build()); } /** * 회원별 반려동물 목록 조회 */ @GetMapping("/all") - public ResponseEntity> getAllPets(@RequestHeader("Authorization") String token) { - Long userId; - try { - userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - } catch (JwtException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } + public ResponseEntity> getAllPets(@AuthenticationPrincipal CustomUserDetails userDetails) { + Long userId = userDetails.getId(); List pets = petService.getPetsByMember(userId); return ResponseEntity.ok(pets); } @@ -74,13 +51,8 @@ public ResponseEntity> getAllPets(@RequestHeader("Authoriza * 반려동물 상세 조회 */ @GetMapping("/{petId}") - public ResponseEntity getPet(@PathVariable Long petId, @RequestHeader("Authorization") String token) { - Long userId; - try { - userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - } catch (JwtException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } + public ResponseEntity getPet(@PathVariable Long petId, @AuthenticationPrincipal CustomUserDetails userDetails) { + Long userId = userDetails.getId(); return ResponseEntity.ok(petService.getPetById(petId, userId)); } @@ -90,28 +62,15 @@ public ResponseEntity getPet(@PathVariable Long petId, @RequestH @PatchMapping(value = "/{petId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public CompletableFuture> updatePet( @PathVariable Long petId, - @RequestHeader("Authorization") String token, - @RequestPart(value = "petData", required = false) String petDataJson, + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestPart(value = "petData", required = false) PetUpdateDto petData, @RequestPart(value = "petImage", required = false) MultipartFile petImage) { - Long userId; - try { - userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - } catch (JwtException e) { - return CompletableFuture.completedFuture(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()); - } - - PetUpdateDto updateDto; - try { - updateDto = (petDataJson != null && !petDataJson.isBlank()) - ? objectMapper.readValue(petDataJson, PetUpdateDto.class) - : new PetUpdateDto(); - } catch (JsonProcessingException e) { - return CompletableFuture.completedFuture(ResponseEntity.badRequest().build()); - } + Long userId = userDetails.getId(); - return petService.updatePet(petId, userId, updateDto, petImage) - .thenApply(ResponseEntity::ok); + return petService.updatePet(petId, userId, petData, petImage) + .thenApply(ResponseEntity::ok) + .exceptionally(ex -> ResponseEntity.badRequest().build()); } /** @@ -120,14 +79,9 @@ public CompletableFuture> updatePet( @DeleteMapping("/{petId}") public CompletableFuture>> deletePet( @PathVariable Long petId, - @RequestHeader("Authorization") String token) { + @AuthenticationPrincipal CustomUserDetails userDetails) { - Long userId; - try { - userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); - } catch (JwtException e) { - return CompletableFuture.completedFuture(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()); - } + Long userId = userDetails.getId(); return petService.deletePet(petId, userId) .thenApply(ignored -> ResponseEntity.ok(Map.of("message", "반려동물이 삭제되었습니다."))); diff --git a/src/main/java/pawparazzi/back/pet/service/PetService.java b/src/main/java/pawparazzi/back/pet/service/PetService.java index 4d558e8..e43b815 100644 --- a/src/main/java/pawparazzi/back/pet/service/PetService.java +++ b/src/main/java/pawparazzi/back/pet/service/PetService.java @@ -15,6 +15,7 @@ import pawparazzi.back.pet.dto.PetUpdateDto; import pawparazzi.back.pet.entity.Pet; import pawparazzi.back.pet.repository.PetRepository; +import pawparazzi.back.pet.util.PetImageHelper; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -38,15 +39,15 @@ public CompletableFuture registerPet(Long userId, PetRegisterReq Member member = memberRepository.findById(userId) .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); - String pathPrefix = "pet_images/" + member.getNickName(); - String defaultImageUrl = "https://default-image-url.com/default-pet.png"; + String pathPrefix = PetImageHelper.getPathPrefix(member.getNickName()); + String defaultImageUrl = PetImageHelper.getDefaultImageUrl(); // S3 이미지 업로드 (비동기 처리) CompletableFuture petImageUrlFuture = s3UploadUtil.uploadImageAsync(petImage, pathPrefix, defaultImageUrl); return petImageUrlFuture.thenApply(petImageUrl -> { Pet pet = new Pet(registerDto.getName(), registerDto.getType(), registerDto.getBirthDate(), petImageUrl, member); - member.addPet(pet); // Member에 반려동물 추가 + member.addPet(pet); petRepository.save(pet); return new PetResponseDto(pet); }); @@ -92,8 +93,8 @@ public CompletableFuture updatePet(Long petId, Long userId, PetU throw new IllegalArgumentException("해당 반려동물을 수정할 권한이 없습니다."); } - String pathPrefix = "pet_images/" + pet.getMember().getNickName(); - String defaultImageUrl = "https://default-image-url.com/default-pet.png"; + String pathPrefix = PetImageHelper.getPathPrefix(pet.getMember().getNickName()); + String defaultImageUrl = PetImageHelper.getDefaultImageUrl(); String oldImageUrl = pet.getPetImg(); // S3 이미지 업로드 (petImage가 있을 때만 비동기 처리) @@ -112,7 +113,7 @@ public CompletableFuture updatePet(Long petId, Long userId, PetU return new PetResponseDto(pet); }).thenCombineAsync( // 기존 S3 이미지 삭제를 비동기 병렬 실행 (oldImageUrl != null && !oldImageUrl.equals(defaultImageUrl)) - ? s3AsyncService.deleteFile("pet_images/" + extractFileName(oldImageUrl)) + ? s3AsyncService.deleteFile("pet_images/" + PetImageHelper.extractFileName(oldImageUrl)) : CompletableFuture.completedFuture(null), (updatedPet, ignored) -> updatedPet // 이미지 삭제 완료 여부와 상관없이 업데이트된 Pet 반환 ); @@ -131,14 +132,14 @@ public CompletableFuture deletePet(Long petId, Long userId) { } String petImageUrl = pet.getPetImg(); - String defaultImageUrl = "https://default-image-url.com/default-pet.png"; + String defaultImageUrl = PetImageHelper.getDefaultImageUrl(); // 먼저 DB에서 반려동물 삭제 petRepository.delete(pet); // S3에서 기존 반려동물 이미지 삭제 if (petImageUrl != null && !petImageUrl.equals(defaultImageUrl)) { - String fileName = extractFileName(petImageUrl); + String fileName = PetImageHelper.extractFileName(petImageUrl); return s3AsyncService.deleteFile("pet_images/" + fileName) .exceptionally(ex -> { System.err.println("S3 이미지 삭제 실패: " + ex.getMessage()); @@ -148,16 +149,4 @@ public CompletableFuture deletePet(Long petId, Long userId) { return CompletableFuture.completedFuture(null); } - - private String extractFileName(String imageUrl) { - if (imageUrl == null || imageUrl.isBlank()) { - return null; - } - - if (imageUrl.contains("/pet_images/")) { - return imageUrl.substring(imageUrl.lastIndexOf("/pet_images/") + "/pet_images/".length()); - } - - return imageUrl.substring(imageUrl.lastIndexOf("/") + 1); - } } \ No newline at end of file diff --git a/src/main/java/pawparazzi/back/pet/util/PetImageHelper.java b/src/main/java/pawparazzi/back/pet/util/PetImageHelper.java new file mode 100644 index 0000000..c277ce0 --- /dev/null +++ b/src/main/java/pawparazzi/back/pet/util/PetImageHelper.java @@ -0,0 +1,39 @@ +package pawparazzi.back.pet.util; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +public class PetImageHelper { + + private static final String DEFAULT_IMAGE_URL = "https://your-bucket.s3.region.amazonaws.com/pet_images/default_pet.png"; + private static final String IMAGE_FOLDER = "pet_images/"; + + /** + * S3에 저장될 경로 prefix 생성 + */ + public static String getPathPrefix(String nickname) { + return IMAGE_FOLDER + encode(nickname) + "/"; + } + + /** + * 기본 이미지 URL 반환 + */ + public static String getDefaultImageUrl() { + return DEFAULT_IMAGE_URL; + } + + /** + * URL에서 파일명 추출 + */ + public static String extractFileName(String imageUrl) { + if (imageUrl == null || imageUrl.isBlank()) return null; + return imageUrl.substring(imageUrl.lastIndexOf("/") + 1); + } + + /** + * 경로에 사용할 닉네임 인코딩 + */ + private static String encode(String text) { + return URLEncoder.encode(text, StandardCharsets.UTF_8); + } +} \ No newline at end of file From 5dd557669e1dc2cf27665ea41d077a471a9fb1fb Mon Sep 17 00:00:00 2001 From: geg222 Date: Wed, 2 Apr 2025 15:04:57 +0900 Subject: [PATCH 12/12] =?UTF-8?q?[FIX]=20member,=20pet=20=EC=9D=B4?= =?UTF-8?q?=EC=A0=84=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [FIX] member, pet 이전 코드로 변경 --- .../member/controller/MemberController.java | 79 +++++++++++++---- .../back/member/service/MemberService.java | 24 +----- .../back/pet/controller/PetController.java | 84 ++++++++++++++----- .../back/pet/service/PetService.java | 29 +++++-- .../back/pet/util/PetImageHelper.java | 39 --------- 5 files changed, 150 insertions(+), 105 deletions(-) delete mode 100644 src/main/java/pawparazzi/back/pet/util/PetImageHelper.java diff --git a/src/main/java/pawparazzi/back/member/controller/MemberController.java b/src/main/java/pawparazzi/back/member/controller/MemberController.java index 13b9754..99220df 100644 --- a/src/main/java/pawparazzi/back/member/controller/MemberController.java +++ b/src/main/java/pawparazzi/back/member/controller/MemberController.java @@ -1,18 +1,23 @@ package pawparazzi.back.member.controller; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.JwtException; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import pawparazzi.back.member.dto.request.LoginRequestDto; +import pawparazzi.back.member.dto.request.SignUpRequestDto; +import pawparazzi.back.member.dto.request.UpdateMemberRequestDto; import pawparazzi.back.member.dto.response.MemberResponseDto; import pawparazzi.back.member.dto.response.UpdateMemberResponseDto; import pawparazzi.back.member.entity.Member; import pawparazzi.back.member.service.MemberService; -import pawparazzi.back.security.user.CustomUserDetails; +import pawparazzi.back.security.util.JwtUtil; import java.util.List; import java.util.Map; @@ -23,7 +28,9 @@ @RequiredArgsConstructor public class MemberController { + private final JwtUtil jwtUtil; private final MemberService memberService; + private final ObjectMapper objectMapper; /** * 회원 가입 @@ -33,10 +40,17 @@ public CompletableFuture> registerUser( @RequestPart(value = "profileImage", required = false) MultipartFile profileImage, @RequestPart("userData") String userDataJson) { + // JSON 데이터를 DTO로 변환 + SignUpRequestDto request; + try { + request = objectMapper.readValue(userDataJson, SignUpRequestDto.class); + } catch (JsonProcessingException e) { + return CompletableFuture.completedFuture(ResponseEntity.badRequest().body("Invalid JSON format")); + } + // 비동기 회원가입 처리 후 응답 반환 - return memberService.registerUser(userDataJson, profileImage) - .thenApply(unused -> ResponseEntity.ok("회원가입 성공")) - .exceptionally(ex -> ResponseEntity.badRequest().body("Invalid JSON format")); + return memberService.registerUser(request, profileImage) + .thenApply(unused -> ResponseEntity.ok("회원가입 성공")); } /** @@ -52,8 +66,14 @@ public ResponseEntity> login(@Valid @RequestBody LoginReques * 사용자 정보 조회 */ @GetMapping("/me") - public ResponseEntity getCurrentUser(@AuthenticationPrincipal CustomUserDetails userDetails) { - Long memberId = userDetails.getId(); + public ResponseEntity getCurrentUser(@RequestHeader("Authorization") String token) { + token = token.replace("Bearer ", ""); + Long memberId; + try { + memberId = jwtUtil.extractMemberId(token); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } Member member = memberService.findById(memberId); return ResponseEntity.ok(member); } @@ -63,23 +83,43 @@ public ResponseEntity getCurrentUser(@AuthenticationPrincipal CustomUser */ @PatchMapping(value = "/me", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public CompletableFuture> updateMember( - @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestHeader("Authorization") String token, @RequestPart(value = "profileImage", required = false) MultipartFile profileImage, @RequestPart(value = "userData", required = false) String userDataJson) { - Long memberId = userDetails.getId(); + token = token.replace("Bearer ", ""); + Long memberId; + try { + memberId = jwtUtil.extractMemberId(token); + } catch (JwtException e) { + return CompletableFuture.completedFuture(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()); + } + + try { + UpdateMemberRequestDto request = (userDataJson == null || userDataJson.isBlank()) + ? new UpdateMemberRequestDto() + : objectMapper.readValue(userDataJson, UpdateMemberRequestDto.class); - return memberService.updateMember(memberId, userDataJson, profileImage) - .thenApply(ResponseEntity::ok) - .exceptionally(ex -> ResponseEntity.badRequest().body(null)); + return memberService.updateMember(memberId, request, profileImage) + .thenApply(ResponseEntity::ok); + } catch (JsonProcessingException e) { + return CompletableFuture.completedFuture(ResponseEntity.badRequest().body(null)); + } } /** * 회원 탈퇴 */ @DeleteMapping("/delete") - public ResponseEntity deleteMember(@AuthenticationPrincipal CustomUserDetails userDetails) { - memberService.deleteMember(userDetails.getId()); + public ResponseEntity deleteMember(@RequestHeader("Authorization") String token) { + token = token.replace("Bearer ", ""); + Long memberId; + try { + memberId = jwtUtil.extractMemberId(token); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + memberService.deleteMember(memberId); return ResponseEntity.ok("회원 탈퇴 완료"); } @@ -96,10 +136,17 @@ public ResponseEntity> getAllMembers() { * 로그아웃 */ @PostMapping("/logout") - public ResponseEntity logout(@AuthenticationPrincipal CustomUserDetails userDetails, + public ResponseEntity logout(@RequestHeader("Authorization") String accessToken, @RequestBody Map body) { String refreshToken = body.get("refreshToken"); - memberService.logout(userDetails.getId(), refreshToken); + accessToken = accessToken.replace("Bearer ", ""); + Long memberId; + try { + memberId = jwtUtil.extractMemberId(accessToken); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + memberService.logout(memberId, refreshToken); return ResponseEntity.ok("로그아웃 완료"); } diff --git a/src/main/java/pawparazzi/back/member/service/MemberService.java b/src/main/java/pawparazzi/back/member/service/MemberService.java index fde334b..b76938b 100644 --- a/src/main/java/pawparazzi/back/member/service/MemberService.java +++ b/src/main/java/pawparazzi/back/member/service/MemberService.java @@ -1,7 +1,5 @@ package pawparazzi.back.member.service; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.EntityNotFoundException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; @@ -47,20 +45,12 @@ public class MemberService { private final S3AsyncService s3AsyncService; private final S3UploadUtil s3UploadUtil; private final RefreshTokenRepository refreshTokenRepository; - private final ObjectMapper objectMapper; /** * 회원가입 */ @Transactional - public CompletableFuture registerUser(String userDataJson, MultipartFile profileImage) { - SignUpRequestDto request; - try { - request = objectMapper.readValue(userDataJson, SignUpRequestDto.class); - } catch (JsonProcessingException e) { - throw new IllegalArgumentException("Invalid JSON format"); - } - + public CompletableFuture registerUser(SignUpRequestDto request, MultipartFile profileImage) { if (memberRepository.existsByEmail(request.getEmail())) { throw new IllegalArgumentException("이미 가입된 이메일입니다."); } @@ -143,16 +133,7 @@ public Member findById(Long id) { /** * 회원 정보 수정 */ - public CompletableFuture updateMember(Long memberId, String userDataJson, MultipartFile newProfileImage) { - UpdateMemberRequestDto request; - try { - request = (userDataJson == null || userDataJson.isBlank()) - ? new UpdateMemberRequestDto() - : objectMapper.readValue(userDataJson, UpdateMemberRequestDto.class); - } catch (JsonProcessingException e) { - throw new IllegalArgumentException("Invalid JSON format"); - } - + public CompletableFuture updateMember(Long memberId, UpdateMemberRequestDto request, MultipartFile newProfileImage) { Member member = memberRepository.findById(memberId) .orElseThrow(() -> new EntityNotFoundException("회원 정보를 찾을 수 없습니다.")); @@ -170,7 +151,6 @@ public CompletableFuture updateMember(Long memberId, St member.setName(request.getName()); } - // 프로필 사진 수정 String pathPrefix = "profile_images/" + member.getNickName(); String defaultImageUrl = "https://default-image-url.com/default-profile.png"; diff --git a/src/main/java/pawparazzi/back/pet/controller/PetController.java b/src/main/java/pawparazzi/back/pet/controller/PetController.java index ca5a98d..d3a9888 100644 --- a/src/main/java/pawparazzi/back/pet/controller/PetController.java +++ b/src/main/java/pawparazzi/back/pet/controller/PetController.java @@ -1,16 +1,19 @@ package pawparazzi.back.pet.controller; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.JwtException; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import pawparazzi.back.pet.dto.PetRegisterRequestDto; import pawparazzi.back.pet.dto.PetResponseDto; import pawparazzi.back.pet.dto.PetUpdateDto; import pawparazzi.back.pet.service.PetService; -import pawparazzi.back.security.user.CustomUserDetails; +import pawparazzi.back.security.util.JwtUtil; import java.util.List; import java.util.Map; @@ -22,27 +25,47 @@ public class PetController { private final PetService petService; + private final JwtUtil jwtUtil; + private final ObjectMapper objectMapper; /** * 반려동물 등록 */ @PostMapping(value = "/register", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public CompletableFuture> registerPet( - @AuthenticationPrincipal CustomUserDetails userDetails, - @RequestPart("petData") PetRegisterRequestDto requestDto, + @RequestHeader("Authorization") String token, + @RequestPart("petData") String petDataJson, @RequestPart(value = "petImage", required = false) MultipartFile petImage) { - return petService.registerPet(userDetails.getId(), requestDto, petImage) - .thenApply(ResponseEntity::ok) - .exceptionally(ex -> ResponseEntity.badRequest().build()); + Long userId; + try { + userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + } catch (JwtException e) { + return CompletableFuture.completedFuture(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()); + } + + PetRegisterRequestDto registerDto; + try { + registerDto = objectMapper.readValue(petDataJson, PetRegisterRequestDto.class); + } catch (JsonProcessingException e) { + return CompletableFuture.completedFuture(ResponseEntity.badRequest().build()); + } + + return petService.registerPet(userId, registerDto, petImage) + .thenApply(ResponseEntity::ok); } /** * 회원별 반려동물 목록 조회 */ @GetMapping("/all") - public ResponseEntity> getAllPets(@AuthenticationPrincipal CustomUserDetails userDetails) { - Long userId = userDetails.getId(); + public ResponseEntity> getAllPets(@RequestHeader("Authorization") String token) { + Long userId; + try { + userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } List pets = petService.getPetsByMember(userId); return ResponseEntity.ok(pets); } @@ -51,8 +74,13 @@ public ResponseEntity> getAllPets(@AuthenticationPrincipal * 반려동물 상세 조회 */ @GetMapping("/{petId}") - public ResponseEntity getPet(@PathVariable Long petId, @AuthenticationPrincipal CustomUserDetails userDetails) { - Long userId = userDetails.getId(); + public ResponseEntity getPet(@PathVariable Long petId, @RequestHeader("Authorization") String token) { + Long userId; + try { + userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + } catch (JwtException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } return ResponseEntity.ok(petService.getPetById(petId, userId)); } @@ -62,15 +90,28 @@ public ResponseEntity getPet(@PathVariable Long petId, @Authenti @PatchMapping(value = "/{petId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public CompletableFuture> updatePet( @PathVariable Long petId, - @AuthenticationPrincipal CustomUserDetails userDetails, - @RequestPart(value = "petData", required = false) PetUpdateDto petData, + @RequestHeader("Authorization") String token, + @RequestPart(value = "petData", required = false) String petDataJson, @RequestPart(value = "petImage", required = false) MultipartFile petImage) { - Long userId = userDetails.getId(); + Long userId; + try { + userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + } catch (JwtException e) { + return CompletableFuture.completedFuture(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()); + } + + PetUpdateDto updateDto; + try { + updateDto = (petDataJson != null && !petDataJson.isBlank()) + ? objectMapper.readValue(petDataJson, PetUpdateDto.class) + : new PetUpdateDto(); + } catch (JsonProcessingException e) { + return CompletableFuture.completedFuture(ResponseEntity.badRequest().build()); + } - return petService.updatePet(petId, userId, petData, petImage) - .thenApply(ResponseEntity::ok) - .exceptionally(ex -> ResponseEntity.badRequest().build()); + return petService.updatePet(petId, userId, updateDto, petImage) + .thenApply(ResponseEntity::ok); } /** @@ -79,9 +120,14 @@ public CompletableFuture> updatePet( @DeleteMapping("/{petId}") public CompletableFuture>> deletePet( @PathVariable Long petId, - @AuthenticationPrincipal CustomUserDetails userDetails) { + @RequestHeader("Authorization") String token) { - Long userId = userDetails.getId(); + Long userId; + try { + userId = jwtUtil.extractMemberId(token.replace("Bearer ", "")); + } catch (JwtException e) { + return CompletableFuture.completedFuture(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()); + } return petService.deletePet(petId, userId) .thenApply(ignored -> ResponseEntity.ok(Map.of("message", "반려동물이 삭제되었습니다."))); diff --git a/src/main/java/pawparazzi/back/pet/service/PetService.java b/src/main/java/pawparazzi/back/pet/service/PetService.java index e43b815..4d558e8 100644 --- a/src/main/java/pawparazzi/back/pet/service/PetService.java +++ b/src/main/java/pawparazzi/back/pet/service/PetService.java @@ -15,7 +15,6 @@ import pawparazzi.back.pet.dto.PetUpdateDto; import pawparazzi.back.pet.entity.Pet; import pawparazzi.back.pet.repository.PetRepository; -import pawparazzi.back.pet.util.PetImageHelper; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -39,15 +38,15 @@ public CompletableFuture registerPet(Long userId, PetRegisterReq Member member = memberRepository.findById(userId) .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); - String pathPrefix = PetImageHelper.getPathPrefix(member.getNickName()); - String defaultImageUrl = PetImageHelper.getDefaultImageUrl(); + String pathPrefix = "pet_images/" + member.getNickName(); + String defaultImageUrl = "https://default-image-url.com/default-pet.png"; // S3 이미지 업로드 (비동기 처리) CompletableFuture petImageUrlFuture = s3UploadUtil.uploadImageAsync(petImage, pathPrefix, defaultImageUrl); return petImageUrlFuture.thenApply(petImageUrl -> { Pet pet = new Pet(registerDto.getName(), registerDto.getType(), registerDto.getBirthDate(), petImageUrl, member); - member.addPet(pet); + member.addPet(pet); // Member에 반려동물 추가 petRepository.save(pet); return new PetResponseDto(pet); }); @@ -93,8 +92,8 @@ public CompletableFuture updatePet(Long petId, Long userId, PetU throw new IllegalArgumentException("해당 반려동물을 수정할 권한이 없습니다."); } - String pathPrefix = PetImageHelper.getPathPrefix(pet.getMember().getNickName()); - String defaultImageUrl = PetImageHelper.getDefaultImageUrl(); + String pathPrefix = "pet_images/" + pet.getMember().getNickName(); + String defaultImageUrl = "https://default-image-url.com/default-pet.png"; String oldImageUrl = pet.getPetImg(); // S3 이미지 업로드 (petImage가 있을 때만 비동기 처리) @@ -113,7 +112,7 @@ public CompletableFuture updatePet(Long petId, Long userId, PetU return new PetResponseDto(pet); }).thenCombineAsync( // 기존 S3 이미지 삭제를 비동기 병렬 실행 (oldImageUrl != null && !oldImageUrl.equals(defaultImageUrl)) - ? s3AsyncService.deleteFile("pet_images/" + PetImageHelper.extractFileName(oldImageUrl)) + ? s3AsyncService.deleteFile("pet_images/" + extractFileName(oldImageUrl)) : CompletableFuture.completedFuture(null), (updatedPet, ignored) -> updatedPet // 이미지 삭제 완료 여부와 상관없이 업데이트된 Pet 반환 ); @@ -132,14 +131,14 @@ public CompletableFuture deletePet(Long petId, Long userId) { } String petImageUrl = pet.getPetImg(); - String defaultImageUrl = PetImageHelper.getDefaultImageUrl(); + String defaultImageUrl = "https://default-image-url.com/default-pet.png"; // 먼저 DB에서 반려동물 삭제 petRepository.delete(pet); // S3에서 기존 반려동물 이미지 삭제 if (petImageUrl != null && !petImageUrl.equals(defaultImageUrl)) { - String fileName = PetImageHelper.extractFileName(petImageUrl); + String fileName = extractFileName(petImageUrl); return s3AsyncService.deleteFile("pet_images/" + fileName) .exceptionally(ex -> { System.err.println("S3 이미지 삭제 실패: " + ex.getMessage()); @@ -149,4 +148,16 @@ public CompletableFuture deletePet(Long petId, Long userId) { return CompletableFuture.completedFuture(null); } + + private String extractFileName(String imageUrl) { + if (imageUrl == null || imageUrl.isBlank()) { + return null; + } + + if (imageUrl.contains("/pet_images/")) { + return imageUrl.substring(imageUrl.lastIndexOf("/pet_images/") + "/pet_images/".length()); + } + + return imageUrl.substring(imageUrl.lastIndexOf("/") + 1); + } } \ No newline at end of file diff --git a/src/main/java/pawparazzi/back/pet/util/PetImageHelper.java b/src/main/java/pawparazzi/back/pet/util/PetImageHelper.java deleted file mode 100644 index c277ce0..0000000 --- a/src/main/java/pawparazzi/back/pet/util/PetImageHelper.java +++ /dev/null @@ -1,39 +0,0 @@ -package pawparazzi.back.pet.util; - -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; - -public class PetImageHelper { - - private static final String DEFAULT_IMAGE_URL = "https://your-bucket.s3.region.amazonaws.com/pet_images/default_pet.png"; - private static final String IMAGE_FOLDER = "pet_images/"; - - /** - * S3에 저장될 경로 prefix 생성 - */ - public static String getPathPrefix(String nickname) { - return IMAGE_FOLDER + encode(nickname) + "/"; - } - - /** - * 기본 이미지 URL 반환 - */ - public static String getDefaultImageUrl() { - return DEFAULT_IMAGE_URL; - } - - /** - * URL에서 파일명 추출 - */ - public static String extractFileName(String imageUrl) { - if (imageUrl == null || imageUrl.isBlank()) return null; - return imageUrl.substring(imageUrl.lastIndexOf("/") + 1); - } - - /** - * 경로에 사용할 닉네임 인코딩 - */ - private static String encode(String text) { - return URLEncoder.encode(text, StandardCharsets.UTF_8); - } -} \ No newline at end of file