From 3a722dbfe9eec1d1ef1b2309e7a46c21b3ddf0d2 Mon Sep 17 00:00:00 2001 From: HEX <123macanic@naver.com> Date: Wed, 9 Oct 2024 00:28:52 +0900 Subject: [PATCH 1/4] =?UTF-8?q?Refactor:=20jwt=20token=EC=97=90=EC=84=9C?= =?UTF-8?q?=202=EB=B2=88=20=EA=B2=80=EC=A6=9D=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=201=EB=B2=88=20=EA=B2=80=EC=A6=9D=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/JwtAuthenticationFilter.java | 3 +- .../mtvs/devlinkbackend/config/JwtUtil.java | 51 +++++++++++++------ .../controller/Oauth2UserController.java | 17 +++---- .../oauth2/service/EpicGamesTokenService.java | 4 +- .../oauth2/service/UserService.java | 8 +-- .../controller/QuestionController.java | 16 +++--- .../question/entity/Question.java | 12 ++++- .../reply/controller/ReplyController.java | 2 + .../reply/dto/ReplyRegistRequestDTO.java | 2 + .../reply/dto/ReplyUpdateRequestDTO.java | 2 + .../devlinkbackend/reply/entity/Reply.java | 2 + .../reply/repository/ReplyRepository.java | 2 + .../reply/service/ReplyService.java | 2 + 13 files changed, 79 insertions(+), 44 deletions(-) create mode 100644 src/main/java/com/mtvs/devlinkbackend/reply/controller/ReplyController.java create mode 100644 src/main/java/com/mtvs/devlinkbackend/reply/dto/ReplyRegistRequestDTO.java create mode 100644 src/main/java/com/mtvs/devlinkbackend/reply/dto/ReplyUpdateRequestDTO.java create mode 100644 src/main/java/com/mtvs/devlinkbackend/reply/entity/Reply.java create mode 100644 src/main/java/com/mtvs/devlinkbackend/reply/repository/ReplyRepository.java create mode 100644 src/main/java/com/mtvs/devlinkbackend/reply/service/ReplyService.java diff --git a/src/main/java/com/mtvs/devlinkbackend/config/JwtAuthenticationFilter.java b/src/main/java/com/mtvs/devlinkbackend/config/JwtAuthenticationFilter.java index b776d8f..a248ef6 100644 --- a/src/main/java/com/mtvs/devlinkbackend/config/JwtAuthenticationFilter.java +++ b/src/main/java/com/mtvs/devlinkbackend/config/JwtAuthenticationFilter.java @@ -13,7 +13,6 @@ import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; -import java.util.Map; @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @@ -53,7 +52,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse if (token != null) { // refreshToken도 없어 AccessToken이 아예 없는 경우 지나가기 try { // 토큰 검증 | 검증 성공 시 SecurityContext에 인증 정보 저장 - String userPrincipal = jwtUtil.getSubjectFromToken(token); + String userPrincipal = jwtUtil.getSubjectFromTokenWithAuth(token); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userPrincipal, null, null); diff --git a/src/main/java/com/mtvs/devlinkbackend/config/JwtUtil.java b/src/main/java/com/mtvs/devlinkbackend/config/JwtUtil.java index fb172dc..3b1987b 100644 --- a/src/main/java/com/mtvs/devlinkbackend/config/JwtUtil.java +++ b/src/main/java/com/mtvs/devlinkbackend/config/JwtUtil.java @@ -28,30 +28,33 @@ public JwtUtil(EpicGamesJWKCache jwkCache) { } // JWT 서명 및 검증을 통한 Claims 추출 - public Map getClaimsFromToken(String token) throws Exception { - SignedJWT signedJWT = SignedJWT.parse(token); - JWK jwk = jwkCache.getCachedJWKSet().getKeyByKeyId(signedJWT.getHeader().getKeyID()); - - if (jwk == null || !JWSAlgorithm.RS256.equals(jwk.getAlgorithm())) { - throw new RuntimeException("JWK key is missing or invalid algorithm"); - } + public Map getClaimsFromTokenWithAuth(String token) throws Exception { + // Claims 검증 + JWTClaimsSet claims = getClaimsFromToken(token); + validateClaims(claims); - JWSVerifier verifier = new RSASSAVerifier(jwk.toRSAKey()); - if (!signedJWT.verify(verifier)) { - throw new RuntimeException("JWT signature verification failed"); - } + // 검증이 완료되었을 경우 모든 Claims을 Map으로 변환하여 반환 + return convertClaimsToMap(claims); + } + // JWT 서명 및 검증을 통한 Claims 추출 + public Map getClaimsFromTokenWithoutAuth(String token) throws Exception { // Claims 검증 - JWTClaimsSet claims = signedJWT.getJWTClaimsSet(); - validateClaims(claims); + JWTClaimsSet claims = getClaimsFromToken(token); // 검증이 완료되었을 경우 모든 Claims을 Map으로 변환하여 반환 return convertClaimsToMap(claims); } - // 'sub' 값 검증 (예제) - public String getSubjectFromToken(String token) throws Exception { - Map claims = getClaimsFromToken(token); + // 검증된 'sub' 값, accountId 반환 + public String getSubjectFromTokenWithAuth(String token) throws Exception { + Map claims = getClaimsFromTokenWithAuth(token); + return (String) claims.get("sub"); + } + + // 검증된 'sub' 값, accountId 반환 + public String getSubjectFromTokenWithoutAuth(String token) throws Exception { + Map claims = getClaimsFromTokenWithoutAuth(token); return (String) claims.get("sub"); } @@ -79,6 +82,22 @@ private void validateClaims(JWTClaimsSet claims) throws BadJWTException { } } + private JWTClaimsSet getClaimsFromToken(String token) throws Exception { + SignedJWT signedJWT = SignedJWT.parse(token); + JWK jwk = jwkCache.getCachedJWKSet().getKeyByKeyId(signedJWT.getHeader().getKeyID()); + + if (jwk == null || !JWSAlgorithm.RS256.equals(jwk.getAlgorithm())) { + throw new RuntimeException("JWK key is missing or invalid algorithm"); + } + + JWSVerifier verifier = new RSASSAVerifier(jwk.toRSAKey()); + if (!signedJWT.verify(verifier)) { + throw new RuntimeException("JWT signature verification failed"); + } + + return signedJWT.getJWTClaimsSet(); + } + // Claims를 Map 형식으로 변환하는 메서드 private Map convertClaimsToMap(JWTClaimsSet claims) { Map claimsMap = new HashMap<>(); diff --git a/src/main/java/com/mtvs/devlinkbackend/oauth2/controller/Oauth2UserController.java b/src/main/java/com/mtvs/devlinkbackend/oauth2/controller/Oauth2UserController.java index 2abde7b..a503a8b 100644 --- a/src/main/java/com/mtvs/devlinkbackend/oauth2/controller/Oauth2UserController.java +++ b/src/main/java/com/mtvs/devlinkbackend/oauth2/controller/Oauth2UserController.java @@ -21,18 +21,13 @@ public Oauth2UserController(EpicGamesTokenService epicGamesTokenService, UserSer this.userService = userService; } - // 로컬에 저장된 user 정보 가져오는 API + // 로컬 user 정보 가져오는 API @GetMapping("/local/user-info") - public ResponseEntity getLocalUserInfo(@RequestHeader("Authorization") String accessToken) { + public ResponseEntity getLocalUserInfo(@RequestHeader("Authorization") String authorizationHeader) { try { - // JWT 토큰에서 사용자 정보 추출 - Map claims = epicGamesTokenService.validateAndParseToken(extractToken(accessToken)); + String token = extractToken(authorizationHeader); - if(userService.findUserByAccessToken(extractToken(accessToken)) != null) { - userService.registUserByAccessToken(extractToken(accessToken)); - } - - return ResponseEntity.ok().build(); + return ResponseEntity.ok(userService.findUserByAccessToken(token)); } catch (Exception e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid token"); } @@ -41,11 +36,11 @@ public ResponseEntity getLocalUserInfo(@RequestHeader("Authorization") String // epicgames 계정 정보 가져오는 API @GetMapping("/epicgames/user-info") public ResponseEntity getEpicGamesUserInfo( - @RequestHeader("Authorization") String accessToken) { + @RequestHeader("Authorization") String authorizationHeader) { try { Map userAccount = - epicGamesTokenService.getEpicGamesUserAccount(extractToken(accessToken)); + epicGamesTokenService.getEpicGamesUserAccount(extractToken(authorizationHeader)); return ResponseEntity.ok(userAccount); } catch (Exception e) { diff --git a/src/main/java/com/mtvs/devlinkbackend/oauth2/service/EpicGamesTokenService.java b/src/main/java/com/mtvs/devlinkbackend/oauth2/service/EpicGamesTokenService.java index c481234..d38da38 100644 --- a/src/main/java/com/mtvs/devlinkbackend/oauth2/service/EpicGamesTokenService.java +++ b/src/main/java/com/mtvs/devlinkbackend/oauth2/service/EpicGamesTokenService.java @@ -35,7 +35,7 @@ public EpicGamesTokenService(EpicGamesJWKCache jwkCache, JwtUtil jwtUtil) { // 오프라인 JWT 검증 및 파싱 메서드 public Map validateAndParseToken(String token) throws Exception { // JWT 토큰 검증 및 파싱하여 Claims를 추출 - return jwtUtil.getClaimsFromToken(token); + return jwtUtil.getClaimsFromTokenWithoutAuth(token); } public String getAccessTokenByRefreshToken(String refreshToken) { @@ -110,7 +110,7 @@ public Map getEpicGamesUserAccount(String accessToken) { try { response = restTemplate.exchange( - "https://api.epicgames.dev/epic/id/v2/accounts?accountId=" + jwtUtil.getClaimsFromToken(accessToken), + "https://api.epicgames.dev/epic/id/v2/accounts?accountId=" + jwtUtil.getClaimsFromTokenWithAuth(accessToken), HttpMethod.GET, request, Map.class diff --git a/src/main/java/com/mtvs/devlinkbackend/oauth2/service/UserService.java b/src/main/java/com/mtvs/devlinkbackend/oauth2/service/UserService.java index b295f0c..fc394d2 100644 --- a/src/main/java/com/mtvs/devlinkbackend/oauth2/service/UserService.java +++ b/src/main/java/com/mtvs/devlinkbackend/oauth2/service/UserService.java @@ -19,7 +19,7 @@ public UserService(UserRepository userRepository, JwtUtil jwtUtil) { public User registUserByAccessToken(String accessToken) { try { - String accountId = jwtUtil.getSubjectFromToken(accessToken); + String accountId = jwtUtil.getSubjectFromTokenWithoutAuth(accessToken); return userRepository.save(new User( accountId )); @@ -30,7 +30,7 @@ public User registUserByAccessToken(String accessToken) { public User findUserByAccessToken(String accessToken) { try { - String accountId = jwtUtil.getSubjectFromToken(accessToken); + String accountId = jwtUtil.getSubjectFromTokenWithoutAuth(accessToken); return userRepository.findUserByAccountId(accountId); } catch (Exception e) { throw new RuntimeException(e); @@ -39,7 +39,7 @@ public User findUserByAccessToken(String accessToken) { public void updateUserByAccessToken(String accessToken, UserUpdateRequestDTO userUpdateRequestDTO) { try { - String accountId = jwtUtil.getSubjectFromToken(accessToken); + String accountId = jwtUtil.getSubjectFromTokenWithoutAuth(accessToken); User user = userRepository.findUserByAccountId(accountId); if(user != null) { user.setEmail(userUpdateRequestDTO.getEmail()); @@ -53,7 +53,7 @@ public void updateUserByAccessToken(String accessToken, UserUpdateRequestDTO use public void deleteUserByAccessToken(String accessToken) { try { - String accountId = jwtUtil.getSubjectFromToken(accessToken); + String accountId = jwtUtil.getSubjectFromTokenWithAuth(accessToken); userRepository.deleteUserByAccountId(accountId); } catch (Exception e) { throw new RuntimeException(e); diff --git a/src/main/java/com/mtvs/devlinkbackend/question/controller/QuestionController.java b/src/main/java/com/mtvs/devlinkbackend/question/controller/QuestionController.java index 9a5c214..10ac287 100644 --- a/src/main/java/com/mtvs/devlinkbackend/question/controller/QuestionController.java +++ b/src/main/java/com/mtvs/devlinkbackend/question/controller/QuestionController.java @@ -1,7 +1,6 @@ package com.mtvs.devlinkbackend.question.controller; import com.mtvs.devlinkbackend.config.JwtUtil; -import com.mtvs.devlinkbackend.oauth2.service.EpicGamesTokenService; import com.mtvs.devlinkbackend.question.dto.QuestionRegistRequestDTO; import com.mtvs.devlinkbackend.question.dto.QuestionUpdateRequestDTO; import com.mtvs.devlinkbackend.question.entity.Question; @@ -31,14 +30,14 @@ public ResponseEntity createQuestion( @RequestBody QuestionRegistRequestDTO questionRegistRequestDTO, @RequestHeader(name = "Authorization") String accessToken) throws Exception { - String accountId = jwtUtil.getSubjectFromToken(accessToken); + String accountId = jwtUtil.getSubjectFromTokenWithAuth(accessToken); Question createdQuestion = questionService.registQuestion(questionRegistRequestDTO, accountId); return ResponseEntity.ok(createdQuestion); } // Retrieve a question by ID @GetMapping("/{questionId}") - public ResponseEntity getQuestionById(@PathVariable long questionId) { + public ResponseEntity getQuestionById(@PathVariable Long questionId) { Question question = questionService.findQuestionByQuestionId(questionId); if (question != null) { return ResponseEntity.ok(question); @@ -60,7 +59,7 @@ public ResponseEntity> getQuestionsByAccountIdWithPaging( @RequestParam int page, @RequestHeader(name = "Authorization") String accessToken) throws Exception { - String accountId = jwtUtil.getSubjectFromToken(accessToken); + String accountId = jwtUtil.getSubjectFromTokenWithoutAuth(accessToken); List questions = questionService.findQuestionsByAccountIdWithPaging(page, accountId); return ResponseEntity.ok(questions); } @@ -71,7 +70,7 @@ public ResponseEntity updateQuestion( @RequestBody QuestionUpdateRequestDTO questionUpdateRequestDTO, @RequestHeader(name = "Authorization") String accessToken) throws Exception { - String accountId = jwtUtil.getSubjectFromToken(accessToken); + String accountId = jwtUtil.getSubjectFromTokenWithoutAuth(accessToken); try { Question updatedQuestion = questionService.updateQuestion(questionUpdateRequestDTO, accountId); return ResponseEntity.ok(updatedQuestion); @@ -81,9 +80,10 @@ public ResponseEntity updateQuestion( } // Delete a question by ID - @DeleteMapping("/{id}") - public ResponseEntity deleteQuestion(@PathVariable long id) { - questionService.deleteQuestion(id); + @DeleteMapping("/{questionId}") + public ResponseEntity deleteQuestion(@PathVariable Long questionId) { + + questionService.deleteQuestion(questionId); return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/com/mtvs/devlinkbackend/question/entity/Question.java b/src/main/java/com/mtvs/devlinkbackend/question/entity/Question.java index ffdc1b7..911f374 100644 --- a/src/main/java/com/mtvs/devlinkbackend/question/entity/Question.java +++ b/src/main/java/com/mtvs/devlinkbackend/question/entity/Question.java @@ -1,5 +1,8 @@ package com.mtvs.devlinkbackend.question.entity; +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import com.mtvs.devlinkbackend.reply.entity.Reply; import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; @@ -7,6 +10,8 @@ import org.hibernate.annotations.UpdateTimestamp; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; @Table(name = "QUESTION") @Entity(name = "QUESTION") @@ -16,7 +21,7 @@ public class Question { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "QUESTION_ID") - private long questionId; + private Long questionId; @Column(name = "TITLE") private String title; @@ -35,7 +40,12 @@ public class Question { @Column(name = "ACCOUNT_ID") // 사용자 구분 private String accountId; + @OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List replies = new ArrayList<>(); + public Question(String title, String content, String accountId) { // Create용 생성자 + this.title = title; this.content = content; this.accountId = accountId; } diff --git a/src/main/java/com/mtvs/devlinkbackend/reply/controller/ReplyController.java b/src/main/java/com/mtvs/devlinkbackend/reply/controller/ReplyController.java new file mode 100644 index 0000000..7bae410 --- /dev/null +++ b/src/main/java/com/mtvs/devlinkbackend/reply/controller/ReplyController.java @@ -0,0 +1,2 @@ +package com.mtvs.devlinkbackend.reply.controller;public class ReplyController { +} diff --git a/src/main/java/com/mtvs/devlinkbackend/reply/dto/ReplyRegistRequestDTO.java b/src/main/java/com/mtvs/devlinkbackend/reply/dto/ReplyRegistRequestDTO.java new file mode 100644 index 0000000..15caf8a --- /dev/null +++ b/src/main/java/com/mtvs/devlinkbackend/reply/dto/ReplyRegistRequestDTO.java @@ -0,0 +1,2 @@ +package com.mtvs.devlinkbackend.reply.dto;public class ReplyRegistRequestDTO { +} diff --git a/src/main/java/com/mtvs/devlinkbackend/reply/dto/ReplyUpdateRequestDTO.java b/src/main/java/com/mtvs/devlinkbackend/reply/dto/ReplyUpdateRequestDTO.java new file mode 100644 index 0000000..3928517 --- /dev/null +++ b/src/main/java/com/mtvs/devlinkbackend/reply/dto/ReplyUpdateRequestDTO.java @@ -0,0 +1,2 @@ +package com.mtvs.devlinkbackend.reply.dto;public class ReplyUpdateRequestDTO { +} diff --git a/src/main/java/com/mtvs/devlinkbackend/reply/entity/Reply.java b/src/main/java/com/mtvs/devlinkbackend/reply/entity/Reply.java new file mode 100644 index 0000000..888fcf7 --- /dev/null +++ b/src/main/java/com/mtvs/devlinkbackend/reply/entity/Reply.java @@ -0,0 +1,2 @@ +package com.mtvs.devlinkbackend.reply.entity;public class Reply { +} diff --git a/src/main/java/com/mtvs/devlinkbackend/reply/repository/ReplyRepository.java b/src/main/java/com/mtvs/devlinkbackend/reply/repository/ReplyRepository.java new file mode 100644 index 0000000..6e9b618 --- /dev/null +++ b/src/main/java/com/mtvs/devlinkbackend/reply/repository/ReplyRepository.java @@ -0,0 +1,2 @@ +package com.mtvs.devlinkbackend.reply.repository;public class ReplyRepository { +} diff --git a/src/main/java/com/mtvs/devlinkbackend/reply/service/ReplyService.java b/src/main/java/com/mtvs/devlinkbackend/reply/service/ReplyService.java new file mode 100644 index 0000000..efc42ff --- /dev/null +++ b/src/main/java/com/mtvs/devlinkbackend/reply/service/ReplyService.java @@ -0,0 +1,2 @@ +package com.mtvs.devlinkbackend.reply.service;public class ReplyService { +} From 6e95f4fa819461bd83f7f5f021c9ccef879df20a Mon Sep 17 00:00:00 2001 From: HEX <123macanic@naver.com> Date: Wed, 9 Oct 2024 00:31:09 +0900 Subject: [PATCH 2/4] Feat: Domain Reply | repository, entity, service, dto, controller --- .../reply/controller/ReplyController.java | 79 ++++++++++++++++++- .../reply/dto/ReplyRegistRequestDTO.java | 12 ++- .../reply/dto/ReplyUpdateRequestDTO.java | 13 ++- .../devlinkbackend/reply/entity/Reply.java | 50 +++++++++++- .../reply/repository/ReplyRepository.java | 13 ++- .../reply/service/ReplyService.java | 64 ++++++++++++++- 6 files changed, 224 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/mtvs/devlinkbackend/reply/controller/ReplyController.java b/src/main/java/com/mtvs/devlinkbackend/reply/controller/ReplyController.java index 7bae410..e9d26e7 100644 --- a/src/main/java/com/mtvs/devlinkbackend/reply/controller/ReplyController.java +++ b/src/main/java/com/mtvs/devlinkbackend/reply/controller/ReplyController.java @@ -1,2 +1,77 @@ -package com.mtvs.devlinkbackend.reply.controller;public class ReplyController { -} +package com.mtvs.devlinkbackend.reply.controller; + +import com.mtvs.devlinkbackend.config.JwtUtil; +import com.mtvs.devlinkbackend.reply.dto.ReplyRegistRequestDTO; +import com.mtvs.devlinkbackend.reply.dto.ReplyUpdateRequestDTO; +import com.mtvs.devlinkbackend.reply.entity.Reply; +import com.mtvs.devlinkbackend.reply.service.ReplyService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/reply") +public class ReplyController { + + private final ReplyService replyService; + private final JwtUtil jwtUtil; + + public ReplyController(ReplyService replyService, JwtUtil jwtUtil) { + this.replyService = replyService; + this.jwtUtil = jwtUtil; + } + + @PostMapping + public ResponseEntity registReply( + @RequestBody ReplyRegistRequestDTO replyRegistRequestDTO, + @RequestHeader(name = "Authorization") String token) throws Exception { + + String accountId = jwtUtil.getSubjectFromTokenWithAuth(token); + Reply reply = replyService.registReply(replyRegistRequestDTO, accountId); + return ResponseEntity.ok(reply); + } + + @GetMapping("/{replyId}") + public ResponseEntity findReplyByReplyId(@PathVariable Long replyId) { + Reply reply = replyService.findReplyByReplyId(replyId); + return reply != null ? ResponseEntity.ok(reply) : ResponseEntity.notFound().build(); + } + + @GetMapping("/question/{questionId}") + public ResponseEntity> findRepliesByQuestionId(@PathVariable Long questionId) { + List replies = replyService.findRepliesByQuestionId(questionId); + return ResponseEntity.ok(replies); + } + + @GetMapping("/account/{accountId}") + public ResponseEntity> findRepliesByAccountId( + @RequestHeader(name = "Authorization") String token) throws Exception { + + String accountId = jwtUtil.getSubjectFromTokenWithoutAuth(token); + List replies = replyService.findRepliesByAccountId(accountId); + return ResponseEntity.ok(replies); + } + + @PutMapping + public ResponseEntity updateReply( + @RequestBody ReplyUpdateRequestDTO replyUpdateRequestDTO, + @RequestHeader(name = "Authorization") String token) throws Exception { + + String accountId = jwtUtil.getSubjectFromTokenWithoutAuth(token); + try { + Reply updatedReply = replyService.updateReply(replyUpdateRequestDTO, accountId); + return ResponseEntity.ok(updatedReply); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body(null); + } + } + + @DeleteMapping("/{replyId}") + public ResponseEntity deleteReply( + @PathVariable Long replyId) { + + replyService.deleteReply(replyId); + return ResponseEntity.noContent().build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/mtvs/devlinkbackend/reply/dto/ReplyRegistRequestDTO.java b/src/main/java/com/mtvs/devlinkbackend/reply/dto/ReplyRegistRequestDTO.java index 15caf8a..d1b1bd1 100644 --- a/src/main/java/com/mtvs/devlinkbackend/reply/dto/ReplyRegistRequestDTO.java +++ b/src/main/java/com/mtvs/devlinkbackend/reply/dto/ReplyRegistRequestDTO.java @@ -1,2 +1,12 @@ -package com.mtvs.devlinkbackend.reply.dto;public class ReplyRegistRequestDTO { +package com.mtvs.devlinkbackend.reply.dto; + +import lombok.*; + +@Getter @Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class ReplyRegistRequestDTO { + private String content; + private Long questionId; } diff --git a/src/main/java/com/mtvs/devlinkbackend/reply/dto/ReplyUpdateRequestDTO.java b/src/main/java/com/mtvs/devlinkbackend/reply/dto/ReplyUpdateRequestDTO.java index 3928517..9776523 100644 --- a/src/main/java/com/mtvs/devlinkbackend/reply/dto/ReplyUpdateRequestDTO.java +++ b/src/main/java/com/mtvs/devlinkbackend/reply/dto/ReplyUpdateRequestDTO.java @@ -1,2 +1,13 @@ -package com.mtvs.devlinkbackend.reply.dto;public class ReplyUpdateRequestDTO { +package com.mtvs.devlinkbackend.reply.dto; + +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class ReplyUpdateRequestDTO { + private Long replyId; + private String content; } diff --git a/src/main/java/com/mtvs/devlinkbackend/reply/entity/Reply.java b/src/main/java/com/mtvs/devlinkbackend/reply/entity/Reply.java index 888fcf7..b7a0ef2 100644 --- a/src/main/java/com/mtvs/devlinkbackend/reply/entity/Reply.java +++ b/src/main/java/com/mtvs/devlinkbackend/reply/entity/Reply.java @@ -1,2 +1,50 @@ -package com.mtvs.devlinkbackend.reply.entity;public class Reply { +package com.mtvs.devlinkbackend.reply.entity; + +import com.mtvs.devlinkbackend.question.entity.Question; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +@Table(name = "REPLY") +@Entity(name = "REPLY") +@Getter +@NoArgsConstructor +public class Reply { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "REPLY_ID") + private Long replyId; + + @Column(name = "CONTENT") + private String content; + + @CreationTimestamp + @Column(name = "CREATED_AT") + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "MODIFIED_AT") + private LocalDateTime modifiedAt; + + @Column(name = "ACCOUNT_ID") + private String accountId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "QUESTION_ID", nullable = false) + private Question question; + + public Reply(String content, String accountId, Question question) { + this.content = content; + this.accountId = accountId; + this.question = question; + } + + public void setContent(String content) { + this.content = content; + } } diff --git a/src/main/java/com/mtvs/devlinkbackend/reply/repository/ReplyRepository.java b/src/main/java/com/mtvs/devlinkbackend/reply/repository/ReplyRepository.java index 6e9b618..c8098a8 100644 --- a/src/main/java/com/mtvs/devlinkbackend/reply/repository/ReplyRepository.java +++ b/src/main/java/com/mtvs/devlinkbackend/reply/repository/ReplyRepository.java @@ -1,2 +1,13 @@ -package com.mtvs.devlinkbackend.reply.repository;public class ReplyRepository { +package com.mtvs.devlinkbackend.reply.repository; + +import com.mtvs.devlinkbackend.reply.entity.Reply; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ReplyRepository extends JpaRepository { + List findRepliesByAccountId(String accountId); + List findRepliesByQuestionId(Long questionId); } diff --git a/src/main/java/com/mtvs/devlinkbackend/reply/service/ReplyService.java b/src/main/java/com/mtvs/devlinkbackend/reply/service/ReplyService.java index efc42ff..d32577c 100644 --- a/src/main/java/com/mtvs/devlinkbackend/reply/service/ReplyService.java +++ b/src/main/java/com/mtvs/devlinkbackend/reply/service/ReplyService.java @@ -1,2 +1,64 @@ -package com.mtvs.devlinkbackend.reply.service;public class ReplyService { +package com.mtvs.devlinkbackend.reply.service; + +import com.mtvs.devlinkbackend.question.repository.QuestionRepository; +import com.mtvs.devlinkbackend.reply.dto.ReplyRegistRequestDTO; +import com.mtvs.devlinkbackend.reply.dto.ReplyUpdateRequestDTO; +import com.mtvs.devlinkbackend.reply.entity.Reply; +import com.mtvs.devlinkbackend.reply.repository.ReplyRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Service +public class ReplyService { + private final ReplyRepository replyRepository; + private final QuestionRepository questionRepository; + + public ReplyService(ReplyRepository replyRepository, QuestionRepository questionRepository) { + this.replyRepository = replyRepository; + this.questionRepository = questionRepository; + } + + @Transactional + public Reply registReply(ReplyRegistRequestDTO replyRegistRequestDTO, String accountId) { + return replyRepository.save( + new Reply( + replyRegistRequestDTO.getContent(), + accountId, + questionRepository.getReferenceById(replyRegistRequestDTO.getQuestionId()) + )); + } + + public Reply findReplyByReplyId(Long replyId) { + return replyRepository.findById(replyId).orElse(null); + } + + public List findRepliesByQuestionId(Long questionId) { + return replyRepository.findRepliesByQuestionId(questionId); + } + + public List findRepliesByAccountId(String accountId) { + return replyRepository.findRepliesByAccountId(accountId); + } + + @Transactional + public Reply updateReply(ReplyUpdateRequestDTO replyUpdateRequestDTO, String accountId) { + Optional reply = replyRepository.findById(replyUpdateRequestDTO.getReplyId()); + if (reply.isPresent()) { + Reply foundReply = reply.get(); + if (foundReply.getAccountId().equals(accountId)) { + foundReply.setContent(replyUpdateRequestDTO.getContent()); + return foundReply; + } else throw new IllegalArgumentException("Reply Update Error : 다른 사용자가 남의 질문 내용 변경 시도"); + } else throw new IllegalArgumentException( + "Reply not found while updating reply id : " + replyUpdateRequestDTO.getReplyId()); + } + + public void deleteReply(Long replyId) { + replyRepository.deleteById(replyId); + System.out.println("답변ID = " + replyId + " ,삭제 시간 : " + LocalDateTime.now()); + } } From 442a7d05dc79d77ca8731eb9461d900dc7f1e902 Mon Sep 17 00:00:00 2001 From: HEX <123macanic@naver.com> Date: Wed, 9 Oct 2024 20:19:09 +0900 Subject: [PATCH 3/4] =?UTF-8?q?Refactor:=20Question=20=EC=88=9C=ED=99=98?= =?UTF-8?q?=EC=B0=B8=EC=A1=B0=20=EC=98=A4=EB=A5=98=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/mtvs/devlinkbackend/config/SecurityConfig.java | 2 +- src/main/java/com/mtvs/devlinkbackend/reply/entity/Reply.java | 3 +++ .../mtvs/devlinkbackend/reply/repository/ReplyRepository.java | 2 +- .../com/mtvs/devlinkbackend/reply/service/ReplyService.java | 2 +- src/main/resources/application.yml | 2 +- 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/mtvs/devlinkbackend/config/SecurityConfig.java b/src/main/java/com/mtvs/devlinkbackend/config/SecurityConfig.java index 8ac5b20..77a8e5b 100644 --- a/src/main/java/com/mtvs/devlinkbackend/config/SecurityConfig.java +++ b/src/main/java/com/mtvs/devlinkbackend/config/SecurityConfig.java @@ -41,7 +41,7 @@ public SecurityConfig(UserService userService, OAuth2AuthorizedClientService aut public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorizeRequests -> authorizeRequests - .requestMatchers("/", "/login").permitAll() + .requestMatchers("/", "/question/**", "/login").permitAll() .anyRequest().authenticated() ) .oauth2Login(oauth2Login -> oauth2Login diff --git a/src/main/java/com/mtvs/devlinkbackend/reply/entity/Reply.java b/src/main/java/com/mtvs/devlinkbackend/reply/entity/Reply.java index b7a0ef2..5b28a9f 100644 --- a/src/main/java/com/mtvs/devlinkbackend/reply/entity/Reply.java +++ b/src/main/java/com/mtvs/devlinkbackend/reply/entity/Reply.java @@ -1,9 +1,11 @@ package com.mtvs.devlinkbackend.reply.entity; +import com.fasterxml.jackson.annotation.JsonBackReference; import com.mtvs.devlinkbackend.question.entity.Question; import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; +import net.minidev.json.annotate.JsonIgnore; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; @@ -36,6 +38,7 @@ public class Reply { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "QUESTION_ID", nullable = false) + @JsonBackReference private Question question; public Reply(String content, String accountId, Question question) { diff --git a/src/main/java/com/mtvs/devlinkbackend/reply/repository/ReplyRepository.java b/src/main/java/com/mtvs/devlinkbackend/reply/repository/ReplyRepository.java index c8098a8..49ea22e 100644 --- a/src/main/java/com/mtvs/devlinkbackend/reply/repository/ReplyRepository.java +++ b/src/main/java/com/mtvs/devlinkbackend/reply/repository/ReplyRepository.java @@ -9,5 +9,5 @@ @Repository public interface ReplyRepository extends JpaRepository { List findRepliesByAccountId(String accountId); - List findRepliesByQuestionId(Long questionId); + List findRepliesByQuestion_QuestionId(Long questionId); } diff --git a/src/main/java/com/mtvs/devlinkbackend/reply/service/ReplyService.java b/src/main/java/com/mtvs/devlinkbackend/reply/service/ReplyService.java index d32577c..5fd2cb3 100644 --- a/src/main/java/com/mtvs/devlinkbackend/reply/service/ReplyService.java +++ b/src/main/java/com/mtvs/devlinkbackend/reply/service/ReplyService.java @@ -37,7 +37,7 @@ public Reply findReplyByReplyId(Long replyId) { } public List findRepliesByQuestionId(Long questionId) { - return replyRepository.findRepliesByQuestionId(questionId); + return replyRepository.findRepliesByQuestion_QuestionId(questionId); } public List findRepliesByAccountId(String accountId) { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f0a728b..2880310 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -21,7 +21,7 @@ spring: user-name-attribute: sub datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost/devlink + url: jdbc:mysql://localhost:3306/devlink username: root password: 1234 jpa: From 319aade8aea1172bc50d50264238a891757b8b08 Mon Sep 17 00:00:00 2001 From: in seong Park <123macanic@naver.com> Date: Wed, 9 Oct 2024 22:26:31 +0900 Subject: [PATCH 4/4] =?UTF-8?q?Test:=20ReplyCRUD=20TestCode=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devlinkbackend/reply/ReplyCRUDTest.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/test/java/com/mtvs/devlinkbackend/reply/ReplyCRUDTest.java diff --git a/src/test/java/com/mtvs/devlinkbackend/reply/ReplyCRUDTest.java b/src/test/java/com/mtvs/devlinkbackend/reply/ReplyCRUDTest.java new file mode 100644 index 0000000..72f2081 --- /dev/null +++ b/src/test/java/com/mtvs/devlinkbackend/reply/ReplyCRUDTest.java @@ -0,0 +1,88 @@ +package com.mtvs.devlinkbackend.reply; + +import com.mtvs.devlinkbackend.question.dto.QuestionRegistRequestDTO; +import com.mtvs.devlinkbackend.question.dto.QuestionUpdateRequestDTO; +import com.mtvs.devlinkbackend.reply.dto.ReplyRegistRequestDTO; +import com.mtvs.devlinkbackend.reply.dto.ReplyUpdateRequestDTO; +import com.mtvs.devlinkbackend.reply.service.ReplyService; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.util.stream.Stream; + +@SpringBootTest +@Transactional +public class ReplyCRUDTest { + + @Autowired + private ReplyService replyService; + + private static Stream newReply() { + return Stream.of( + Arguments.of(new ReplyRegistRequestDTO("답변 내용 1-4", 1L), "계정0"), + Arguments.of(new ReplyRegistRequestDTO("답변 내용 1-5", 1L), "계정00") + ); + } + + private static Stream modifiedReply() { + return Stream.of( + Arguments.of(new ReplyUpdateRequestDTO(1L, "변경 답변 내용"), "계정1"), + Arguments.of(new ReplyUpdateRequestDTO(2L,"변경 답변 내용" ), "계정1") + ); + } + + @DisplayName("답변 추가 테스트") + @MethodSource("newReply") + @ParameterizedTest + public void testCreateReply(ReplyRegistRequestDTO replyRegistRequestDTO, String accountId) { + Assertions.assertDoesNotThrow(() -> + replyService.registReply(replyRegistRequestDTO, accountId)); + } + + @DisplayName("답변 단일 조회 테스트") + @ValueSource(longs = {1, 2}) + @ParameterizedTest + public void testFindReply(Long replyId) { + Assertions.assertDoesNotThrow(() -> + System.out.println(replyService.findReplyByReplyId(replyId))); + } + + @DisplayName("계정 ID에 따른 답변 조회 테스트") + @ValueSource(strings = {"계정1", "계정2"}) + @ParameterizedTest + public void testFindReplyByAccountId(String accountId) { + Assertions.assertDoesNotThrow(() -> + System.out.println(replyService.findRepliesByAccountId(accountId))); + } + + @DisplayName("질문 ID에 따른 답변 조회 테스트") + @ValueSource(longs = {1, 2}) + @ParameterizedTest + public void testFindReplyByQuestionId(Long questionId) { + Assertions.assertDoesNotThrow(() -> + System.out.println(replyService.findRepliesByQuestionId(questionId))); + } + + @DisplayName("답변 수정 테스트") + @MethodSource("modifiedReply") + @ParameterizedTest + public void testUpdateReply(ReplyUpdateRequestDTO replyUpdateRequestDTO, String accountId) { + Assertions.assertDoesNotThrow(() -> + System.out.println(replyService.updateReply(replyUpdateRequestDTO, accountId))); + } + + @DisplayName("답변 삭제 테스트") + @ValueSource(longs = {1,2}) + @ParameterizedTest + public void testDeleteReply(Long replyId) { + Assertions.assertDoesNotThrow(() -> + replyService.deleteReply(replyId)); + } +}