From b91146e58e217d7b491f5277966566e733e8d4f6 Mon Sep 17 00:00:00 2001 From: skyla00 Date: Sat, 13 Jul 2024 17:45:54 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=20=EC=BB=A4=ED=94=BC=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=ED=8C=90=20=EA=B5=AC=ED=98=84=20v1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../advice/GlobalExceptionAdvice.java | 90 +++++++ .../java/com/springboot/audit/Auditable.java | 28 +++ .../board/controller/BoardController.java | 112 +++++++++ .../com/springboot/board/dto/BoardGetDto.java | 10 + .../springboot/board/dto/BoardPatchDto.java | 20 ++ .../springboot/board/dto/BoardPostDto.java | 22 ++ .../board/dto/BoardResponseDto.java | 28 +++ .../com/springboot/board/dto/LikePostDto.java | 15 ++ .../com/springboot/board/entity/Board.java | 116 +++++++++ .../com/springboot/board/entity/Like.java | 53 +++++ .../com/springboot/board/entity/View.java | 40 ++++ .../springboot/board/mapper/BoardMapper.java | 30 +++ .../board/repository/BoardRepository.java | 14 ++ .../board/repository/LikeRepository.java | 12 + .../board/repository/ViewRepository.java | 12 + .../board/service/BoardService.java | 221 ++++++++++++++++++ .../comment/controller/CommentController.java | 81 +++++++ .../comment/dto/CommentPatchDto.java | 15 ++ .../comment/dto/CommentPostDto.java | 20 ++ .../comment/dto/CommentResponseDto.java | 19 ++ .../springboot/comment/entity/Comment.java | 55 +++++ .../comment/mapper/CommentMapper.java | 21 ++ .../comment/repository/CommentRepository.java | 7 + .../comment/service/CommentService.java | 84 +++++++ .../com/springboot/dto/MultiResponseDto.java | 22 ++ .../java/com/springboot/dto/PageInfo.java | 16 ++ .../com/springboot/dto/SingleResponseDto.java | 10 + .../exception/BusinessLogicException.java | 13 ++ .../springboot/exception/ExceptionCode.java | 30 +++ .../member/controller/MemberController.java | 111 +++++++++ .../springboot/member/dto/MemberPatchDto.java | 27 +++ .../springboot/member/dto/MemberPostDto.java | 22 ++ .../member/dto/MemberResponseDto.java | 13 ++ .../com/springboot/member/entity/Member.java | 85 +++++++ .../member/mapper/MemberMapper.java | 17 ++ .../member/repository/MemberRepository.java | 10 + .../member/service/MemberService.java | 97 ++++++++ .../java/com/springboot/page/SortType.java | 19 ++ .../springboot/response/ErrorResponse.java | 99 ++++++++ .../java/com/springboot/utils/UriCreator.java | 12 + .../com/springboot/validator/NotSpace.java | 17 ++ .../validator/NotSpaceValidator.java | 19 ++ src/main/resources/application.yml | 11 + 43 files changed, 1775 insertions(+) create mode 100644 src/main/java/com/springboot/advice/GlobalExceptionAdvice.java create mode 100644 src/main/java/com/springboot/audit/Auditable.java create mode 100644 src/main/java/com/springboot/board/controller/BoardController.java create mode 100644 src/main/java/com/springboot/board/dto/BoardGetDto.java create mode 100644 src/main/java/com/springboot/board/dto/BoardPatchDto.java create mode 100644 src/main/java/com/springboot/board/dto/BoardPostDto.java create mode 100644 src/main/java/com/springboot/board/dto/BoardResponseDto.java create mode 100644 src/main/java/com/springboot/board/dto/LikePostDto.java create mode 100644 src/main/java/com/springboot/board/entity/Board.java create mode 100644 src/main/java/com/springboot/board/entity/Like.java create mode 100644 src/main/java/com/springboot/board/entity/View.java create mode 100644 src/main/java/com/springboot/board/mapper/BoardMapper.java create mode 100644 src/main/java/com/springboot/board/repository/BoardRepository.java create mode 100644 src/main/java/com/springboot/board/repository/LikeRepository.java create mode 100644 src/main/java/com/springboot/board/repository/ViewRepository.java create mode 100644 src/main/java/com/springboot/board/service/BoardService.java create mode 100644 src/main/java/com/springboot/comment/controller/CommentController.java create mode 100644 src/main/java/com/springboot/comment/dto/CommentPatchDto.java create mode 100644 src/main/java/com/springboot/comment/dto/CommentPostDto.java create mode 100644 src/main/java/com/springboot/comment/dto/CommentResponseDto.java create mode 100644 src/main/java/com/springboot/comment/entity/Comment.java create mode 100644 src/main/java/com/springboot/comment/mapper/CommentMapper.java create mode 100644 src/main/java/com/springboot/comment/repository/CommentRepository.java create mode 100644 src/main/java/com/springboot/comment/service/CommentService.java create mode 100644 src/main/java/com/springboot/dto/MultiResponseDto.java create mode 100644 src/main/java/com/springboot/dto/PageInfo.java create mode 100644 src/main/java/com/springboot/dto/SingleResponseDto.java create mode 100644 src/main/java/com/springboot/exception/BusinessLogicException.java create mode 100644 src/main/java/com/springboot/exception/ExceptionCode.java create mode 100644 src/main/java/com/springboot/member/controller/MemberController.java create mode 100644 src/main/java/com/springboot/member/dto/MemberPatchDto.java create mode 100644 src/main/java/com/springboot/member/dto/MemberPostDto.java create mode 100644 src/main/java/com/springboot/member/dto/MemberResponseDto.java create mode 100644 src/main/java/com/springboot/member/entity/Member.java create mode 100644 src/main/java/com/springboot/member/mapper/MemberMapper.java create mode 100644 src/main/java/com/springboot/member/repository/MemberRepository.java create mode 100644 src/main/java/com/springboot/member/service/MemberService.java create mode 100644 src/main/java/com/springboot/page/SortType.java create mode 100644 src/main/java/com/springboot/response/ErrorResponse.java create mode 100644 src/main/java/com/springboot/utils/UriCreator.java create mode 100644 src/main/java/com/springboot/validator/NotSpace.java create mode 100644 src/main/java/com/springboot/validator/NotSpaceValidator.java diff --git a/src/main/java/com/springboot/advice/GlobalExceptionAdvice.java b/src/main/java/com/springboot/advice/GlobalExceptionAdvice.java new file mode 100644 index 0000000..bd3a0fb --- /dev/null +++ b/src/main/java/com/springboot/advice/GlobalExceptionAdvice.java @@ -0,0 +1,90 @@ +package com.springboot.advice; + +import com.springboot.exception.BusinessLogicException; +import com.springboot.response.ErrorResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.validation.ConstraintViolationException; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionAdvice { + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleMethodArgumentNotValidException( + MethodArgumentNotValidException e) { + final ErrorResponse response = ErrorResponse.of(e.getBindingResult()); + + return response; + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleConstraintViolationException( + ConstraintViolationException e) { + final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations()); + + return response; + } + + @ExceptionHandler + public ResponseEntity handleBusinessLogicException(BusinessLogicException e) { + final ErrorResponse response = ErrorResponse.of(e.getExceptionCode()); + + return new ResponseEntity<>(response, HttpStatus.valueOf(e.getExceptionCode() + .getStatus())); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) + public ErrorResponse handleHttpRequestMethodNotSupportedException( + HttpRequestMethodNotSupportedException e) { + + final ErrorResponse response = ErrorResponse.of(HttpStatus.METHOD_NOT_ALLOWED); + + return response; + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleHttpMessageNotReadableException( + HttpMessageNotReadableException e) { + + final ErrorResponse response = ErrorResponse.of(HttpStatus.BAD_REQUEST, + "Required request body is missing"); + + return response; + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleMissingServletRequestParameterException( + MissingServletRequestParameterException e) { + + final ErrorResponse response = ErrorResponse.of(HttpStatus.BAD_REQUEST, + e.getMessage()); + + return response; + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorResponse handleException(Exception e) { + log.error("# handle Exception", e); + // TODO 애플리케이션의 에러는 에러 로그를 로그에 기록하고, 관리자에게 이메일이나 카카오 톡, + // 슬랙 등으로 알려주는 로직이 있는게 좋습니다. + + final ErrorResponse response = ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR); + + return response; + } +} diff --git a/src/main/java/com/springboot/audit/Auditable.java b/src/main/java/com/springboot/audit/Auditable.java new file mode 100644 index 0000000..7ca7797 --- /dev/null +++ b/src/main/java/com/springboot/audit/Auditable.java @@ -0,0 +1,28 @@ +package com.springboot.audit; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import java.time.LocalDateTime; + +@Getter +@Setter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class Auditable { + + @CreatedDate + @Column(name ="created_at", updatable = false) + private LocalDateTime createdAt = LocalDateTime.now(); + + @LastModifiedDate + @Column(name = "LAST_MODIFIED_AT") + private LocalDateTime modifiedAt = LocalDateTime.now(); +} diff --git a/src/main/java/com/springboot/board/controller/BoardController.java b/src/main/java/com/springboot/board/controller/BoardController.java new file mode 100644 index 0000000..b4741eb --- /dev/null +++ b/src/main/java/com/springboot/board/controller/BoardController.java @@ -0,0 +1,112 @@ +package com.springboot.board.controller; + +import com.springboot.board.dto.*; +import com.springboot.board.entity.Board; +import com.springboot.board.entity.Like; +import com.springboot.board.mapper.BoardMapper; +import com.springboot.board.service.BoardService; +import com.springboot.dto.MultiResponseDto; +import com.springboot.dto.SingleResponseDto; +import com.springboot.member.entity.Member; +import com.springboot.member.service.MemberService; +import com.springboot.utils.UriCreator; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import javax.validation.constraints.Positive; +import java.net.URI; +import java.util.List; + +@RestController +@RequestMapping("/v11/boards") +@Validated +public class BoardController { + private final static String BOARD_DEFAULT_URL = "/v11/boards"; + + private final BoardService boardService; + private final MemberService memberService; + private final BoardMapper mapper; + + public BoardController(BoardService boardService, MemberService memberService, BoardMapper mapper) { + this.boardService = boardService; + this.memberService = memberService; + this.mapper = mapper; + } + + @PostMapping + public ResponseEntity postBoard (@Valid @RequestBody BoardPostDto boardPostDto) { + // 메퍼로 먼저 감싼 다음에, 서비스에 적용. + + Board board = mapper.boardPostDtoToBoard(boardPostDto); + + Member member = new Member(); + member.setMemberId(boardPostDto.getMemberId()); + board.setMember(member); + + Board createdBoard = boardService.createBoard(board); + + URI location = UriCreator.createUri(BOARD_DEFAULT_URL, createdBoard.getBoardId()); + return ResponseEntity.created(location).build(); + } + + @PostMapping("/like/{board-id}") + public ResponseEntity postLike (@PathVariable("board-id") @Positive long boardId, + @Valid @RequestBody LikePostDto likePostDto) { + likePostDto.setBoardId(boardId); + Like like = mapper.likePostDtoToLike(likePostDto); + boardService.createLike(like); + + return new ResponseEntity(HttpStatus.OK); + } + + + @PatchMapping("/{board-id}") + public ResponseEntity patchBoard (@PathVariable("board-id") @Positive long boardId, + @Valid @RequestBody BoardPatchDto boardPatchDto) { + boardPatchDto.setBoardId(boardId); + Board board = boardService.updateBoard(mapper.boardPatchDtoTOBoard(boardPatchDto)); + BoardResponseDto boardResponseDto = mapper.boardToBoardResponseDto(board); + + return new ResponseEntity<>( + new SingleResponseDto<>(boardResponseDto), HttpStatus.OK); + } + + @GetMapping("/{board-id}") + public ResponseEntity getBoard ( @PathVariable("board-id") @Positive long boardId, + @Valid @RequestBody BoardGetDto boardGetDto + ) { + Board board = boardService.findBoard(boardId, boardGetDto.getMemberId()); + BoardResponseDto boardResponseDto = mapper.boardToBoardResponseDto(board); + return new ResponseEntity<>( + new SingleResponseDto<>(boardResponseDto), HttpStatus.OK); + } + + @GetMapping + public ResponseEntity getBoards (@Positive @RequestParam int page, + @Positive @RequestParam int size, + // desc, asc + @RequestParam String sort) { + Page pageBoards = boardService.findBoards(page, size, sort); + List boards = pageBoards.getContent(); + List responseDtos = mapper.boardsToBoardResponseDtos(boards); + + return new ResponseEntity<>( + new MultiResponseDto<>(responseDtos,pageBoards), HttpStatus.OK ); + } + + + // 질문 삭제 상태. + // 회원 탈퇴시 질문 비활성화. >> memberService + @DeleteMapping("{member-id}") + public ResponseEntity deleteBoard ( @PathVariable("member-id") @Positive long boardId) { + boardService.deleteBoard(boardId); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + + } + + +} diff --git a/src/main/java/com/springboot/board/dto/BoardGetDto.java b/src/main/java/com/springboot/board/dto/BoardGetDto.java new file mode 100644 index 0000000..9cdc5a2 --- /dev/null +++ b/src/main/java/com/springboot/board/dto/BoardGetDto.java @@ -0,0 +1,10 @@ +package com.springboot.board.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class BoardGetDto { + private Long memberId; +} diff --git a/src/main/java/com/springboot/board/dto/BoardPatchDto.java b/src/main/java/com/springboot/board/dto/BoardPatchDto.java new file mode 100644 index 0000000..1a2c267 --- /dev/null +++ b/src/main/java/com/springboot/board/dto/BoardPatchDto.java @@ -0,0 +1,20 @@ +package com.springboot.board.dto; + +import com.springboot.board.entity.Board; +import lombok.Getter; + +import javax.validation.constraints.NotBlank; + +@Getter +public class BoardPatchDto { + private long boardId; + private long viewMemberId; + + private String title; + private String content; + private Board.VisibilityStatus visibilityStatus; + + public void setBoardId(long boardId) { + this.boardId = boardId; + } +} diff --git a/src/main/java/com/springboot/board/dto/BoardPostDto.java b/src/main/java/com/springboot/board/dto/BoardPostDto.java new file mode 100644 index 0000000..5401ed8 --- /dev/null +++ b/src/main/java/com/springboot/board/dto/BoardPostDto.java @@ -0,0 +1,22 @@ +package com.springboot.board.dto; + +import com.springboot.board.entity.Board; +import lombok.Getter; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Positive; + +@Getter +public class BoardPostDto { + @Positive + private long memberId; + + @NotBlank + private String title; + + @NotBlank + private String content; + + private Board.VisibilityStatus visibilityStatus; + +} diff --git a/src/main/java/com/springboot/board/dto/BoardResponseDto.java b/src/main/java/com/springboot/board/dto/BoardResponseDto.java new file mode 100644 index 0000000..3d86ea5 --- /dev/null +++ b/src/main/java/com/springboot/board/dto/BoardResponseDto.java @@ -0,0 +1,28 @@ +package com.springboot.board.dto; + +import com.springboot.board.entity.Board; +import com.springboot.comment.dto.CommentResponseDto; +import lombok.*; + +import javax.persistence.Column; +import javax.validation.constraints.NotBlank; +import java.time.LocalDateTime; +import java.util.List; + +@NoArgsConstructor +@Setter +@Getter +public class BoardResponseDto { + private long boardId; + private long memberId; + private String title; + private String content; + private int likeCount ; + private int viewCount ; + private Board.QuestionStatus questionStatus; + private Board.VisibilityStatus visibilityStatus; + private LocalDateTime createdAt; + private LocalDateTime modifiedAt; + private CommentResponseDto boardComment; + +} diff --git a/src/main/java/com/springboot/board/dto/LikePostDto.java b/src/main/java/com/springboot/board/dto/LikePostDto.java new file mode 100644 index 0000000..82f4fbd --- /dev/null +++ b/src/main/java/com/springboot/board/dto/LikePostDto.java @@ -0,0 +1,15 @@ +package com.springboot.board.dto; + +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotBlank; + +@Getter +@Setter +public class LikePostDto { + + private Long memberId; + private Long boardId; + +} diff --git a/src/main/java/com/springboot/board/entity/Board.java b/src/main/java/com/springboot/board/entity/Board.java new file mode 100644 index 0000000..cd395e5 --- /dev/null +++ b/src/main/java/com/springboot/board/entity/Board.java @@ -0,0 +1,116 @@ +package com.springboot.board.entity; + +import com.springboot.audit.Auditable; +import com.springboot.comment.entity.Comment; +import com.springboot.member.entity.Member; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.*; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@Entity +public class Board extends Auditable { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long boardId; + + @Column(length = 100, nullable = false) + private String title; + + @Column(nullable = false) + private String content; + + @Column + private int likeCount = 0; + + @Column + private int viewCount = 0; + + @ManyToOne + @JoinColumn(name = "MEMBER_ID") + private Member member; + public void setMember(Member member) { + this.member = member; + if (!member.getBoards().contains(this)) { + member.setBoards(this); + } + } + + @OneToOne(mappedBy = "board") + private Comment comment; + public void setComment(Comment comment) { + this.comment = comment; + if (comment.getBoard() != this) { + comment.setBoard(this); + } + } + + @OneToOne(cascade = CascadeType.ALL) + private Like like; + public void setLike(Like like) { + this.like = like; + if (like.getBoard() != this) { + like.setBoard(this); + } + } + public void deleteLike(Like like) { + this.like = null; + if (like.getBoard() == this) { + like.deleteBoard(this); + } + } + + @OneToOne + private View view; + public void setView (View view) { + this.view = view; + if(view.getBoard() != this) { + view.setBoard(this); + } + } + + @Enumerated(value = EnumType.STRING) + @Column(nullable = false) + private QuestionStatus questionStatus = QuestionStatus.QUESTION_REGISTERED; + + @Enumerated(value = EnumType.STRING) + @Column(nullable = false) + private VisibilityStatus visibilityStatus = VisibilityStatus.PUBLIC; + + public enum QuestionStatus { + QUESTION_REGISTERED(1, "질문 등록"), + QUESTION_ANSWERED(2, "답변 완료"), + QUESTION_DELETED(3, "질문 삭제"), + // 탈퇴했을 때. + QUESTION_DEACTIVED(4, "질문 비활성화"); + + @Getter + private int QuestionNumber; + + @Getter + private String Status; + + QuestionStatus(int QuestionNumber, String questionStatus) { + this.QuestionNumber = QuestionNumber; + this.Status = Status; + } + } + + public enum VisibilityStatus { + PUBLIC("공개글"), + SECRET("비밀글"); + + @Getter + private String visibilityStatus; + + VisibilityStatus(String visibilityStatus) { + this.visibilityStatus = visibilityStatus; + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/springboot/board/entity/Like.java b/src/main/java/com/springboot/board/entity/Like.java new file mode 100644 index 0000000..9070e8e --- /dev/null +++ b/src/main/java/com/springboot/board/entity/Like.java @@ -0,0 +1,53 @@ +package com.springboot.board.entity; + +import com.springboot.audit.Auditable; +import com.springboot.comment.entity.Comment; +import com.springboot.member.entity.Member; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.mapstruct.Mapping; + +import javax.persistence.*; + +@Getter +@Setter +@NoArgsConstructor +@Entity +@Table(name = "likes") +public class Like { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long likeId; + + @OneToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "BOARD_ID") + private Board board; + public void setBoard (Board board) { + this.board = board; + if(board.getLike() != this) { + board.setLike(this); + } + } + public void deleteBoard (Board board) { + this.board = null; + if(board.getLike() == this) { + board.deleteLike(this); + } + } + @ManyToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "MEMBER_ID") + private Member member; + public void setMember (Member member) { + this.member = member; + if(!member.getLikes().contains(this)) { + member.setLikes(this); + } + } + public void deleteMember (Member member) { + this.member = null; + if(member.getLikes().contains(this)) { + member.deleteLikes(this); + } + } +} diff --git a/src/main/java/com/springboot/board/entity/View.java b/src/main/java/com/springboot/board/entity/View.java new file mode 100644 index 0000000..006652d --- /dev/null +++ b/src/main/java/com/springboot/board/entity/View.java @@ -0,0 +1,40 @@ +package com.springboot.board.entity; + +import com.springboot.audit.Auditable; +import com.springboot.member.entity.Member; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.*; + +@Getter +@Setter +@NoArgsConstructor +@Entity +public class View{ + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long viewId; + + @OneToOne(cascade = CascadeType.PERSIST) + @JoinColumn(name = "BOARD_ID") + private Board board; + public void setBoard (Board board) { + this.board = board; + if(board.getView() != this) { + board.setView(this); + } + } + + @OneToOne(cascade = CascadeType.PERSIST) + @JoinColumn(name = "MEMBER_ID") + private Member member; + public void setMember (Member member) { + this.member = member; + if(!member.getViews().contains(this)) { + member.setViews(this); + } + } +} diff --git a/src/main/java/com/springboot/board/mapper/BoardMapper.java b/src/main/java/com/springboot/board/mapper/BoardMapper.java new file mode 100644 index 0000000..a318579 --- /dev/null +++ b/src/main/java/com/springboot/board/mapper/BoardMapper.java @@ -0,0 +1,30 @@ +package com.springboot.board.mapper; + +import com.springboot.board.dto.*; +import com.springboot.board.entity.Board; +import com.springboot.board.entity.Like; +import com.springboot.board.entity.View; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import java.util.List; + +@Mapper(componentModel = "spring") +public interface BoardMapper { + Board boardPostDtoToBoard (BoardPostDto boardPostDto); + Board boardPatchDtoTOBoard (BoardPatchDto boardPatchDto); + + @Mapping(source = "member.memberId", target = "memberId") + @Mapping(source = "comment.content", target = "boardComment.content") + @Mapping(source = "comment.member.memberId", target = "boardComment.memberId") + BoardResponseDto boardToBoardResponseDto (Board board); + List boardsToBoardResponseDtos (List boards); + + @Mapping(source = "memberId", target = "member.memberId") + @Mapping(source = "boardId", target = "board.boardId") + Like likePostDtoToLike (LikePostDto likePostDto); + + @Mapping(source = "memberId", target = "member.memberId") + Board BoardGetDtoToBoard (BoardGetDto viewGetDto); + +} diff --git a/src/main/java/com/springboot/board/repository/BoardRepository.java b/src/main/java/com/springboot/board/repository/BoardRepository.java new file mode 100644 index 0000000..3d2482c --- /dev/null +++ b/src/main/java/com/springboot/board/repository/BoardRepository.java @@ -0,0 +1,14 @@ +package com.springboot.board.repository; + +import com.springboot.board.entity.Board; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; + +public interface BoardRepository extends JpaRepository { + Board findByQuestionStatusNotAndQuestionStatusNot(Board.QuestionStatus questionStatus1, Board.QuestionStatus QuestionStatus2); + Page findByQuestionStatusNotAndQuestionStatusNot(Board.QuestionStatus questionStatus1, Board.QuestionStatus QuestionStatus2, Pageable pageable); + +} diff --git a/src/main/java/com/springboot/board/repository/LikeRepository.java b/src/main/java/com/springboot/board/repository/LikeRepository.java new file mode 100644 index 0000000..81b3c46 --- /dev/null +++ b/src/main/java/com/springboot/board/repository/LikeRepository.java @@ -0,0 +1,12 @@ +package com.springboot.board.repository; + +import com.springboot.board.entity.Board; +import com.springboot.board.entity.Like; +import com.springboot.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface LikeRepository extends JpaRepository { + Optional findByMemberAndBoard(Member member, Board board); +} diff --git a/src/main/java/com/springboot/board/repository/ViewRepository.java b/src/main/java/com/springboot/board/repository/ViewRepository.java new file mode 100644 index 0000000..4b0faf8 --- /dev/null +++ b/src/main/java/com/springboot/board/repository/ViewRepository.java @@ -0,0 +1,12 @@ +package com.springboot.board.repository; + +import com.springboot.board.entity.Board; +import com.springboot.board.entity.View; +import com.springboot.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface ViewRepository extends JpaRepository { + Optional findByMemberAndBoard(Member member, Board board); +} diff --git a/src/main/java/com/springboot/board/service/BoardService.java b/src/main/java/com/springboot/board/service/BoardService.java new file mode 100644 index 0000000..3b4530c --- /dev/null +++ b/src/main/java/com/springboot/board/service/BoardService.java @@ -0,0 +1,221 @@ +package com.springboot.board.service; + +import com.springboot.board.entity.Board; +import com.springboot.board.entity.Like; +import com.springboot.board.entity.View; +import com.springboot.board.repository.BoardRepository; +import com.springboot.board.repository.LikeRepository; +import com.springboot.board.repository.ViewRepository; +import com.springboot.comment.service.CommentService; +import com.springboot.exception.BusinessLogicException; +import com.springboot.exception.ExceptionCode; +import com.springboot.member.entity.Member; +import com.springboot.member.repository.MemberRepository; +import com.springboot.member.service.MemberService; +import com.springboot.page.SortType; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static com.springboot.board.entity.Board.QuestionStatus.*; +import static com.springboot.page.SortType.*; + +@Service +public class BoardService { + private final BoardRepository boardRepository; + private final MemberService memberService; + private final CommentService commentService; + private final LikeRepository likeRepository; + private final MemberRepository memberRepository; + private final ViewRepository viewRepository; + + public BoardService(BoardRepository boardRepository, MemberService memberService, CommentService commentService, LikeRepository likeRepository, MemberRepository memberRepository, ViewRepository viewRepository) { + this.boardRepository = boardRepository; + this.memberService = memberService; + this.commentService = commentService; + this.likeRepository = likeRepository; + this.memberRepository = memberRepository; + this.viewRepository = viewRepository; + } + + public Board createBoard (Board board) { + // 회원인지 아닌지 등록함. + verifyBoardMember(board); + Board savedOrder = saveBoard(board); + // 등록시 등록 날짜 생성. + return savedOrder; + } + public void createLike (Like like) { + + // 어떤 게 null 값이 나왔을 때 어떤 exception 코드를 전해야 하는가? + // board 에 있는 like 를 확인해야 해서 그런가? + // 받아온 like 에 board 를 가지고 옴. + // jpa 가 무조건. + Optional board = boardRepository.findById(like.getBoard().getBoardId()); + Board findBoard = board.orElseThrow(() -> new BusinessLogicException(ExceptionCode.BOARD_NOT_FOUND)); + Optional member = memberRepository.findById(like.getMember().getMemberId()); + Member findMember = member.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND)); + Optional findLike = likeRepository.findByMemberAndBoard(findMember, findBoard); + + if( findLike.isPresent()) { + // delete 를 해라. + Like deleteLike = findLike.orElseThrow (() -> new BusinessLogicException(ExceptionCode.LIKE_EXISTS)); + findBoard.deleteLike(deleteLike); + findMember.deleteLikes(deleteLike); + findBoard.setLikeCount(findBoard.getLikeCount()-1); + boardRepository.save(findBoard); + likeRepository.delete(deleteLike); + } else { + Like addlike= new Like(); + addlike.setBoard(findBoard); + addlike.setMember(findMember); + likeRepository.save(addlike); + findBoard.setLikeCount(findBoard.getLikeCount() + 1); + boardRepository.save(findBoard); + } + } + + public Board updateBoard (Board board) { + // 질문을 등록한 회원만 수정할 수 있음. + Board findBoard = findVerifiedBoard(board.getBoardId()); + // 질문 수정 시 수정 날짜 업데이트. + findBoard.setModifiedAt(LocalDateTime.now()); + + // 비밀글로 변경할 경우, 상태 변경 수정. + Optional.ofNullable(board.getVisibilityStatus()) + .ifPresent(visibilityStatus -> findBoard.setVisibilityStatus(visibilityStatus)); + Optional.ofNullable(board.getTitle()) + .ifPresent(title -> findBoard.setTitle(title)); + Optional.ofNullable(board.getContent()) + .ifPresent(content -> findBoard.setContent(content)); + + // 답변 완료된 질문은 수정할 수 없음. + int questionNumber = findBoard.getQuestionStatus().getQuestionNumber(); + if (questionNumber == 2) { + throw new BusinessLogicException(ExceptionCode.CANNOT_CHANGE_BOARD); + } + + return boardRepository.save(findBoard); + // 관리자일 경우 질문 상태 대답 변경. + } + + public Board findBoard (long boardId, long memberId) { + // 비밀글이면 질문을 등록한 회원과 관리자만 조회 가능 + // 답변이 존재하면 답변도 함께 조회해야 함. + Board findBoard = findVerifiedBoard(boardId); + int questionNumber = findBoard.getQuestionStatus().getQuestionNumber(); + // 이미 삭제 상태인 질문은 조회할 수 없다. + + // view 를 하고 있는 member 를 데리고 와야 하는 것인데?????????? + // + Optional member = memberRepository.findById(memberId); + Member findMember = member.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND)); + Optional view = viewRepository.findByMemberAndBoard(findMember, findBoard); + + + + if(view.isPresent()) { + view.orElseThrow(() -> new BusinessLogicException(ExceptionCode.VIEW_NOT_FOUND)); + // delete 를 해라. + } else { + View addView = new View(); + addView.setBoard(findBoard); + addView.setMember(findMember); + viewRepository.save(addView); + findBoard.setViewCount(findBoard.getViewCount() + 1); + boardRepository.save(findBoard); + } + + if (questionNumber >= 3) { + throw new BusinessLogicException(ExceptionCode.BOARD_NOT_FOUND); + } + return findBoard; + + } + + public Page findBoards (int page, int size, String sort) { + // 여러 건의 질문 목록은 모두 조회 가능. + // 삭제 상태가 아닌 질문만 조회 가능. + // 답변도 함께 조회 해야. mapper responsedto . 를 변경. comment mapper 에서 + + // 페이지 네이션 처리. + // 최근 순, + // 오래 된 순. + // 정렬 조회. + SortType sortType = SortType.valueOf(sort); + Pageable pageable; + + switch (sortType) { + case TIME_ASC : + pageable = PageRequest.of(page - 1, size, Sort.by("createdAt").ascending()); + break; + case TIME_DESC : + pageable = PageRequest.of(page - 1, size, Sort.by("createdAt").descending()); + break; + case LIKE_ASC : + pageable = PageRequest.of(page - 1, size, Sort.by("likeCount").ascending()); + break; + case LIKE_DESC : + pageable = PageRequest.of(page - 1, size, Sort.by("likeCount").descending()); + break; + case VIEW_ASC : + pageable = PageRequest.of(page - 1, size, Sort.by("viewCount").ascending()); + break; + case VIEW_DESC : + pageable = PageRequest.of(page - 1, size, Sort.by("viewCount").descending()); + break; + default: + pageable = PageRequest.of(page -1, size, Sort.by("boardId").descending()); + } + return boardRepository + .findByQuestionStatusNotAndQuestionStatusNot(QUESTION_DELETED, QUESTION_DEACTIVED, pageable); + } + + public void deleteBoard (long boardId) { + // 질문 등록한 회원만 가능. + + + Board findBoard = findVerifiedBoard(boardId); + int questionNumber = findBoard.getQuestionStatus().getQuestionNumber(); + if( questionNumber == 1) { + findBoard.setQuestionStatus(QUESTION_DELETED); + boardRepository.save(findBoard); + } + else if( questionNumber == 2) { + // 질문 삭제하면 질문 상태만 변경. + findBoard.setQuestionStatus(QUESTION_DELETED); + // comment 는 지워져야 함. + commentService.deleteComment(findBoard.getComment().getCommentId()); + boardRepository.save(findBoard); + } + // 이미 삭제상태인 질문은 삭제 불가. + else if (questionNumber == 3) { + throw new BusinessLogicException(ExceptionCode.CANNOT_CHANGE_BOARD); + } + } + + // board 에 memberId 가 있으면, 수정, 조회, 삭제가 가능하도록 함. + // 회원인지 아닌지를 조회를 하는 것. + private void verifyBoardMember (Board board) { + // + memberService.findVerifedMember(board.getMember().getMemberId()); + } + + // board 가 있는지 확인. + public Board findVerifiedBoard (long boardId) { + Optional optionalBoard = boardRepository.findById(boardId); + Board findBoard = optionalBoard.orElseThrow(()-> + new BusinessLogicException(ExceptionCode.BOARD_NOT_FOUND)); + return findBoard; + } + + private Board saveBoard (Board board) { + return boardRepository.save(board); + } + +} diff --git a/src/main/java/com/springboot/comment/controller/CommentController.java b/src/main/java/com/springboot/comment/controller/CommentController.java new file mode 100644 index 0000000..0fc7ecc --- /dev/null +++ b/src/main/java/com/springboot/comment/controller/CommentController.java @@ -0,0 +1,81 @@ +package com.springboot.comment.controller; + +import com.springboot.board.entity.Board; +import com.springboot.board.repository.BoardRepository; +import com.springboot.board.service.BoardService; +import com.springboot.comment.dto.CommentPatchDto; +import com.springboot.comment.dto.CommentPostDto; +import com.springboot.comment.dto.CommentResponseDto; +import com.springboot.comment.entity.Comment; +import com.springboot.comment.mapper.CommentMapper; +import com.springboot.comment.repository.CommentRepository; +import com.springboot.comment.service.CommentService; +import com.springboot.dto.SingleResponseDto; +import com.springboot.member.entity.Member; +import com.springboot.member.service.MemberService; +import com.springboot.utils.UriCreator; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import javax.validation.constraints.Positive; +import java.net.URI; + +@RestController +@RequestMapping("/v11/comments") +@Validated +public class CommentController { + private final static String COMMENT_DEFAULT_URL = "/v11/comments"; + + private final CommentService commentService; + private final CommentMapper mapper; + + public CommentController(CommentService commentService, CommentMapper mapper) { + this.commentService = commentService; + this.mapper = mapper; + } + + @PostMapping + public ResponseEntity postComment (@Valid @RequestBody CommentPostDto commentPostDto) { + Comment comment = mapper.commentPostDtoToComment(commentPostDto); + Board board = new Board(); + board.setBoardId(commentPostDto.getBoardId()); + comment.setBoard(board); + + Comment createdComment = commentService.createComment(comment); + URI location = UriCreator.createUri(COMMENT_DEFAULT_URL, createdComment.getCommentId()); + return ResponseEntity.created(location).build(); + + } + + @PatchMapping("{comment-id}") + public ResponseEntity patchComment (@PathVariable("comment-id") @Positive long commentId, + @Valid @RequestBody CommentPatchDto commentPatchDto) { + commentPatchDto.setCommentId(commentId); + Comment comment = commentService.updateComment(mapper.commentPatchDtoToComment(commentPatchDto)); + CommentResponseDto commentResponseDto = mapper.commentToCommentResponseDto(comment); + return new ResponseEntity<>( + new SingleResponseDto<>(commentResponseDto), HttpStatus.OK + ); + } + @GetMapping("{comment-id}") + public ResponseEntity getComment (@PathVariable("comment-id") @Positive long commentId) { + Comment comment = commentService.findComment(commentId); + CommentResponseDto commentResponseDto = mapper.commentToCommentResponseDto(comment); + + return new ResponseEntity<>( + new SingleResponseDto<>(commentResponseDto), HttpStatus.OK + ); + } + + @DeleteMapping("{comment-id}") + public ResponseEntity deleteComment (@PathVariable("comment-id") @Positive long commentId) { + commentService.deleteComment(commentId); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + + } + +} + diff --git a/src/main/java/com/springboot/comment/dto/CommentPatchDto.java b/src/main/java/com/springboot/comment/dto/CommentPatchDto.java new file mode 100644 index 0000000..1a1fd65 --- /dev/null +++ b/src/main/java/com/springboot/comment/dto/CommentPatchDto.java @@ -0,0 +1,15 @@ +package com.springboot.comment.dto; + +import lombok.Getter; + + +@Getter +public class CommentPatchDto { + private long commentId; + + private String content; + + public void setCommentId(long commentId) { + this.commentId = commentId; + } +} diff --git a/src/main/java/com/springboot/comment/dto/CommentPostDto.java b/src/main/java/com/springboot/comment/dto/CommentPostDto.java new file mode 100644 index 0000000..0f7e0d9 --- /dev/null +++ b/src/main/java/com/springboot/comment/dto/CommentPostDto.java @@ -0,0 +1,20 @@ +package com.springboot.comment.dto; + +import com.springboot.board.entity.Board; +import lombok.Getter; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Positive; + +@Getter +public class CommentPostDto { + @Positive + private long boardId; + + @Positive + private long memberId; + + @NotBlank + private String content; + +} diff --git a/src/main/java/com/springboot/comment/dto/CommentResponseDto.java b/src/main/java/com/springboot/comment/dto/CommentResponseDto.java new file mode 100644 index 0000000..be41a70 --- /dev/null +++ b/src/main/java/com/springboot/comment/dto/CommentResponseDto.java @@ -0,0 +1,19 @@ +package com.springboot.comment.dto; + +import com.springboot.comment.entity.Comment; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Builder +@Getter +@Setter +public class CommentResponseDto { + private Long memberId; + private String content; + private LocalDateTime createdAt; + private LocalDateTime modifiedAt; + +} diff --git a/src/main/java/com/springboot/comment/entity/Comment.java b/src/main/java/com/springboot/comment/entity/Comment.java new file mode 100644 index 0000000..303290c --- /dev/null +++ b/src/main/java/com/springboot/comment/entity/Comment.java @@ -0,0 +1,55 @@ +package com.springboot.comment.entity; + +import com.springboot.audit.Auditable; +import com.springboot.board.entity.Board; +import com.springboot.member.entity.Member; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.*; + +@Getter +@Setter +@NoArgsConstructor +@Entity +public class Comment extends Auditable { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long commentId; + + @Column(nullable = false) + private String content; + + @ManyToOne + @JoinColumn(name = "MEMBER_ID") + private Member member; + + @OneToOne(cascade = CascadeType.MERGE) + //persist : 처음에 만들때 씀. + @JoinColumn(name = "BOARD_ID") + private Board board; + + public void setBoard (Board board) { + this.board = board; + if(board.getComment() != this) { + board.setComment(this); + } + } + + @Enumerated(value = EnumType.STRING) + @Column(nullable = false) + private VisibilityStatus visibilityStatus = VisibilityStatus.PUBLIC; + + public enum VisibilityStatus { + PUBLIC("공개글"), + SECRET("비밀글"); + + @Getter + private String visibilityStatus; + + VisibilityStatus(String visibilityStatus) { + this.visibilityStatus = visibilityStatus; + } + } +} diff --git a/src/main/java/com/springboot/comment/mapper/CommentMapper.java b/src/main/java/com/springboot/comment/mapper/CommentMapper.java new file mode 100644 index 0000000..7518f43 --- /dev/null +++ b/src/main/java/com/springboot/comment/mapper/CommentMapper.java @@ -0,0 +1,21 @@ +package com.springboot.comment.mapper; + +import com.springboot.comment.dto.CommentPatchDto; +import com.springboot.comment.dto.CommentPostDto; +import com.springboot.comment.dto.CommentResponseDto; +import com.springboot.comment.entity.Comment; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface CommentMapper { + + @Mapping(source = "boardId",target = "board.boardId") + @Mapping(source = "memberId", target = "member.memberId") + Comment commentPostDtoToComment (CommentPostDto commentPostDto); + Comment commentPatchDtoToComment (CommentPatchDto commentPatchDto); + + + @Mapping(source = "member.memberId", target = "memberId") + CommentResponseDto commentToCommentResponseDto (Comment comment); +} diff --git a/src/main/java/com/springboot/comment/repository/CommentRepository.java b/src/main/java/com/springboot/comment/repository/CommentRepository.java new file mode 100644 index 0000000..f48e6b6 --- /dev/null +++ b/src/main/java/com/springboot/comment/repository/CommentRepository.java @@ -0,0 +1,7 @@ +package com.springboot.comment.repository; + +import com.springboot.comment.entity.Comment; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentRepository extends JpaRepository { +} diff --git a/src/main/java/com/springboot/comment/service/CommentService.java b/src/main/java/com/springboot/comment/service/CommentService.java new file mode 100644 index 0000000..ce8f6da --- /dev/null +++ b/src/main/java/com/springboot/comment/service/CommentService.java @@ -0,0 +1,84 @@ +package com.springboot.comment.service; + +import com.springboot.board.entity.Board; +import com.springboot.board.repository.BoardRepository; +import com.springboot.board.service.BoardService; +import com.springboot.comment.entity.Comment; +import com.springboot.comment.repository.CommentRepository; +import com.springboot.exception.BusinessLogicException; +import com.springboot.exception.ExceptionCode; +import com.springboot.member.service.MemberService; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.Optional; + +@Service +public class CommentService { + private final CommentRepository commentRepository; + private final BoardRepository boardRepository; + + public CommentService(CommentRepository commentRepository, BoardRepository boardRepository) { + this.commentRepository = commentRepository; + this.boardRepository = boardRepository; + } + + public Comment createComment (Comment comment) { + // 이미 등록되어 있는지 아닌지 확인 + verifyComment(comment.getBoard().getBoardId()); + Optional findBoard = boardRepository.findById(comment.getBoard().getBoardId()); + Board board = findBoard.orElseThrow(()->new BusinessLogicException(ExceptionCode.BOARD_NOT_FOUND)); + board.setQuestionStatus(Board.QuestionStatus.QUESTION_ANSWERED); + + // 답변 등록 시 등록 날짜 생성. (이미 엔티티에 초기화 되어 있음.) + // 질문이 비밀글이면 답변 비밀글.... 같이 가야 함. + // 비공개 일때만 + if (board.getVisibilityStatus().equals(Board.VisibilityStatus.SECRET)) { + comment.setVisibilityStatus(Comment.VisibilityStatus.SECRET); + } + + //comment 를 저장해야 되는데????? 이렇게 하면 되나?????????? + return commentRepository.save(comment); + } + + public Comment updateComment (Comment comment) { + // 있는 코멘트 데리고 옴. + Comment findComment = findVerifiedComment(comment.getCommentId()); + Optional.ofNullable(comment.getContent()) + .ifPresent(content -> findComment.setContent(content)); + // 답변 수정 시 수정 날짜 생성. + findComment.setModifiedAt(LocalDateTime.now()); + + return commentRepository.save(findComment); + } + + public Comment findComment (long commentId) { + Comment findComment = findVerifiedComment(commentId); + return findComment; + } + + public void deleteComment (long commentId) { + // 삭제 시 테이블에서 Row 가 완전히 삭제될 수 있도록 함. + Comment findComment = findVerifiedComment(commentId); + commentRepository.delete(findComment); + } + + public void verifyComment (long boardId) { + Optional optionalBoard = boardRepository.findById(boardId); + Board board = optionalBoard.orElseThrow(() -> + new BusinessLogicException(ExceptionCode.BOARD_NOT_FOUND)); + if(board.getComment() != null) { + throw new BusinessLogicException(ExceptionCode.COMMENT_EXISTS); + } + } + + // comment의 id 가 있는 Id인지 확인. + public Comment findVerifiedComment (long commentId) { + Optional optionalComment = commentRepository.findById(commentId); + Comment findComment = optionalComment.orElseThrow( + () -> new BusinessLogicException(ExceptionCode.COMMENT_NOT_FOUND) + ); + return findComment; + } + +} diff --git a/src/main/java/com/springboot/dto/MultiResponseDto.java b/src/main/java/com/springboot/dto/MultiResponseDto.java new file mode 100644 index 0000000..2b59680 --- /dev/null +++ b/src/main/java/com/springboot/dto/MultiResponseDto.java @@ -0,0 +1,22 @@ +package com.springboot.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.data.domain.Page; + +import java.util.List; + +@AllArgsConstructor +@Getter +public class MultiResponseDto { + // data 랑 pageInfo 를 받는데, 이 pageInfo 는 number, size, totalElement, totalPage 를 받음. + private List data; + private PageInfo pageInfo; + + public MultiResponseDto(List data, Page page) { + this.data = data; + // page 는 인덱스처럼 0부터 시작함. pageInfo 로 response 에 담기 위해서는 + 1 이 필요함. + this.pageInfo = new PageInfo(page.getNumber() + 1, page.getSize(), + page.getTotalElements(),page.getTotalPages(), page.getSort().toString()); + } +} diff --git a/src/main/java/com/springboot/dto/PageInfo.java b/src/main/java/com/springboot/dto/PageInfo.java new file mode 100644 index 0000000..b8b1be8 --- /dev/null +++ b/src/main/java/com/springboot/dto/PageInfo.java @@ -0,0 +1,16 @@ +package com.springboot.dto; + +import com.springboot.page.SortType; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.data.domain.Sort; + +@AllArgsConstructor +@Getter +public class PageInfo { + private int page; + private int size; + private long totalElement; + private int totalPages; + private String sortType; +} diff --git a/src/main/java/com/springboot/dto/SingleResponseDto.java b/src/main/java/com/springboot/dto/SingleResponseDto.java new file mode 100644 index 0000000..770cf86 --- /dev/null +++ b/src/main/java/com/springboot/dto/SingleResponseDto.java @@ -0,0 +1,10 @@ +package com.springboot.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class SingleResponseDto { + private T data; +} diff --git a/src/main/java/com/springboot/exception/BusinessLogicException.java b/src/main/java/com/springboot/exception/BusinessLogicException.java new file mode 100644 index 0000000..770d9c7 --- /dev/null +++ b/src/main/java/com/springboot/exception/BusinessLogicException.java @@ -0,0 +1,13 @@ +package com.springboot.exception; + +import lombok.Getter; + +public class BusinessLogicException extends RuntimeException{ + @Getter + private ExceptionCode exceptionCode; + // + public BusinessLogicException(ExceptionCode exceptionCode) { + super(exceptionCode.getMessage()); + this.exceptionCode = exceptionCode; + } +} diff --git a/src/main/java/com/springboot/exception/ExceptionCode.java b/src/main/java/com/springboot/exception/ExceptionCode.java new file mode 100644 index 0000000..d0a4cb0 --- /dev/null +++ b/src/main/java/com/springboot/exception/ExceptionCode.java @@ -0,0 +1,30 @@ +package com.springboot.exception; + +import lombok.Getter; + +public enum ExceptionCode { + MEMBER_NOT_FOUND(404, "Member not found"), + MEMBER_EXISTS(409, "Member exists"), + COMMENT_NOT_FOUND(404, "Comment not found"), + COMMENT_EXISTS(409, "Comment exists"), + VIEW_NOT_FOUND(404, "View not found"), + CANNOT_CHANGE_ORDER(403, "Order can not change"), + NOT_IMPLEMENTATION(501, "Not Implementation"), + BOARD_NOT_FOUND(404, "Board not found"), + CANNOT_CHANGE_BOARD(403, "Board can not change"), + LIKE_EXISTS(404, "like exists"); + // enum 만들 때. + // 필요한 필드를 주입을 해서 + // 생성자를 만들어 주어야 함. + @Getter + private int status; + + @Getter + private String message; + // status 가 코드 임. + // + ExceptionCode(int code, String message) { + this.status = code; + this.message = message; + } +} diff --git a/src/main/java/com/springboot/member/controller/MemberController.java b/src/main/java/com/springboot/member/controller/MemberController.java new file mode 100644 index 0000000..c38aef2 --- /dev/null +++ b/src/main/java/com/springboot/member/controller/MemberController.java @@ -0,0 +1,111 @@ +package com.springboot.member.controller; + +import com.springboot.dto.MultiResponseDto; +import com.springboot.dto.SingleResponseDto; +import com.springboot.member.dto.MemberPatchDto; +import com.springboot.member.dto.MemberPostDto; +import com.springboot.member.entity.Member; +import com.springboot.member.mapper.MemberMapper; +import com.springboot.member.service.MemberService; +import com.springboot.utils.UriCreator; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import javax.validation.constraints.Positive; +import java.net.URI; +import java.util.List; + +//@ResponseBody 자바를 제이슨 형태로 전환. @Controller 로 컨트롤러 지정. +@RestController +//특정 url로 요청을 보내면 controller에서 어떠한 방식으로 처리할지 정의. +//이때 들어온 요청을 특정 메서드와 매핑하기 위해 사용되는 어노테이션 +@RequestMapping("/v11/members") +//유효성 검증 클래스에 붇임.. 메서드에는 @Valid +@Validated +public class MemberController { + private final static String MEMBER_DEFAULT_URL = "/v11/members"; + + //의존성 주입. + private final MemberService memberService; + private final MemberMapper mapper; + + public MemberController(MemberService memberService, MemberMapper mapper) { + this.memberService = memberService; + this.mapper = mapper; + } + + + @PostMapping + // @RequestBody : 제이슨을 자바로 전환 + public ResponseEntity postMember (@Valid @RequestBody MemberPostDto memberPostDto) { + // postDto 를 Member 객체로 바꾼다. + Member member = mapper.memberPostDtoToMember(memberPostDto); + // 여기에 stamp 객체를 새로 만들어준다. + // 새롭게 만든 member 를 create 한다. + Member createdMember = memberService.createMember(member); + URI location = UriCreator.createUri(MEMBER_DEFAULT_URL, createdMember.getMemberId()); + + return ResponseEntity.created(location).build(); + + } + + // patch 할 때에 member id 를 받음. + // @Validated : 객체의 검증 부분. + // @PathVariable : 경로 변수를 표시하기 위함. + // @Positive : 값이 0이 아닌 양수인지 확인. + @PatchMapping("/{member-id}") + public ResponseEntity patchMember (@PathVariable("member-id") @Positive long memberId, + @Valid @RequestBody MemberPatchDto memberPatchDto) { + + //memberId 로 원하는 멤버를 찾은 뒤에 + memberPatchDto.setMemberId(memberId); + // memberPatchDto 를 Member 로 바꿔준다. + Member updateMember = mapper.memberPatchDtoToMember(memberPatchDto); + + // 바꾼 Member 엔티티로 update 를 한다. + Member member = memberService.updateMember(updateMember); + + // 응답은 ResponseDto 로 함. OK. + return new ResponseEntity<>( + new SingleResponseDto<>(mapper.memberToMemberResponseDto(member)), HttpStatus.OK); + + } + + // get 할 때에 member id 를 받음. + @GetMapping("/{member-id}") + public ResponseEntity getMember (@PathVariable("member-id") @Positive long memberId) { + // memberId 로 member 를 찾아라. + Member member = memberService.findMember(memberId); + + // 응답은 ResponseDto 로 함. OK. + return new ResponseEntity<>( + new SingleResponseDto<>(mapper.memberToMemberResponseDto(member)), HttpStatus.OK); + + } + + // 페이지네이션 구현. + @GetMapping + public ResponseEntity getMembers (@Positive @RequestParam int page, @Positive @RequestParam int size) { + // 페이지를 먼저 구현함. 받은 페이지는 -1 을 해서 인덱스 0을 만들어 주어야 함. + Page pageMembers = memberService.findMembers(page -1, size); + // 페이지에 있는 content 만 데려와서 list를 만들어라. 이거는 밑에 있는 ResponseDto 를 만드는데 쓰일 것임. + List members = pageMembers.getContent(); + + return new ResponseEntity<>( + // public MultiResponseDto(List data, Page page) + new MultiResponseDto<>(mapper.membersToMemberResponseDtos(members), pageMembers), HttpStatus.OK + ); + } + + @DeleteMapping("/{member-id}") + public ResponseEntity deleteMember (@PathVariable("member-id") @Positive long memberId) { + // deleteMember 로직 사용. + memberService.deleteMember(memberId); + // 반환할 내용은 없고, 상태만 변환시키면 됨. + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } +} diff --git a/src/main/java/com/springboot/member/dto/MemberPatchDto.java b/src/main/java/com/springboot/member/dto/MemberPatchDto.java new file mode 100644 index 0000000..669d15e --- /dev/null +++ b/src/main/java/com/springboot/member/dto/MemberPatchDto.java @@ -0,0 +1,27 @@ +package com.springboot.member.dto; + +import com.springboot.member.entity.Member; +import com.springboot.validator.NotSpace; +import lombok.Getter; + +import javax.validation.constraints.Pattern; + +@Getter +public class MemberPatchDto { + private long memberId; + + @NotSpace(message = "회원 이름은 공백이 아니어야 합니다.") + private String name; + + @NotSpace(message = "휴대폰 번호는 공백이 아니어야 합니다.") + @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$", + message = "휴대폰 번호는 010 으로 시작하는 11자리 숫자와 '-'로 구성되엉 햡나디ㅏ.") + + private String phone; + + private Member.MemberStatus memberStatus; + + public void setMemberId(long memberId) { + this.memberId = memberId; + } +} diff --git a/src/main/java/com/springboot/member/dto/MemberPostDto.java b/src/main/java/com/springboot/member/dto/MemberPostDto.java new file mode 100644 index 0000000..eaa06e0 --- /dev/null +++ b/src/main/java/com/springboot/member/dto/MemberPostDto.java @@ -0,0 +1,22 @@ +package com.springboot.member.dto; + +import lombok.Getter; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +@Getter +public class MemberPostDto { + + @NotBlank + @Email + private String email; + + @NotBlank(message = "이름은 공백이 아니어야 합니다.") + private String name; + + @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$", + message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다.") + private String phone; +} diff --git a/src/main/java/com/springboot/member/dto/MemberResponseDto.java b/src/main/java/com/springboot/member/dto/MemberResponseDto.java new file mode 100644 index 0000000..3c5f15c --- /dev/null +++ b/src/main/java/com/springboot/member/dto/MemberResponseDto.java @@ -0,0 +1,13 @@ +package com.springboot.member.dto; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class MemberResponseDto { + private Long memberId; + private String email; + private String name; + private String phone; +} diff --git a/src/main/java/com/springboot/member/entity/Member.java b/src/main/java/com/springboot/member/entity/Member.java new file mode 100644 index 0000000..4a3215f --- /dev/null +++ b/src/main/java/com/springboot/member/entity/Member.java @@ -0,0 +1,85 @@ +package com.springboot.member.entity; + +import com.springboot.audit.Auditable; +import com.springboot.board.entity.Like; +import com.springboot.board.entity.View; +import com.springboot.comment.entity.Comment; +import com.springboot.board.entity.Board; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@Entity +public class Member extends Auditable { + @Id + @GeneratedValue (strategy = GenerationType.IDENTITY) + private Long memberId; + + @Column(length = 100, nullable = false) + private String email; + + @Column(length = 100, nullable = false) + private String name; + + @Column(length = 100, nullable = false) + private String phone; + + @Enumerated(value = EnumType.STRING) + @Column(nullable = false) + private MemberStatus memberStatus = MemberStatus.MEMBER_ACTIVE; + + @OneToMany (mappedBy = "member") + private List boards = new ArrayList<>(); + public void setBoards (Board board) { + this.boards.add(board); + if(board.getMember() != this) { + board.setMember(this); + } + } + + @OneToMany (mappedBy = "member", cascade = CascadeType.ALL) + private List likes = new ArrayList<>(); + public void setLikes(Like like) { + this.likes.add(like); + if(like.getMember() != this) { + like.setMember(this); + } + } + public void deleteLikes(Like like) { + this.likes.remove(like); + if(like.getMember() == this) { + like.deleteMember(this); + } + } + + @OneToMany (mappedBy = "member") + private List views = new ArrayList<>(); + public void setViews (View view) { + this.views.add(view); + if(view.getMember() == this) { + view.setMember(this); + } + } + + public enum MemberStatus { + MEMBER_ACTIVE("활동중"), + MEMBER_SLEEP("휴면 상태"), + MEMBER_QUIT("탈퇴 상태"); + + @Getter + private String status; + + MemberStatus(String status) { + this.status = status; + } + } + + +} diff --git a/src/main/java/com/springboot/member/mapper/MemberMapper.java b/src/main/java/com/springboot/member/mapper/MemberMapper.java new file mode 100644 index 0000000..1ba64c7 --- /dev/null +++ b/src/main/java/com/springboot/member/mapper/MemberMapper.java @@ -0,0 +1,17 @@ +package com.springboot.member.mapper; + +import com.springboot.member.dto.MemberPatchDto; +import com.springboot.member.dto.MemberPostDto; +import com.springboot.member.dto.MemberResponseDto; +import com.springboot.member.entity.Member; +import org.mapstruct.Mapper; + +import java.util.List; + +@Mapper(componentModel = "spring") +public interface MemberMapper { + Member memberPostDtoToMember (MemberPostDto memberPostDto); + Member memberPatchDtoToMember (MemberPatchDto memberPatchDto); + MemberResponseDto memberToMemberResponseDto (Member member); + List membersToMemberResponseDtos (List members); +} diff --git a/src/main/java/com/springboot/member/repository/MemberRepository.java b/src/main/java/com/springboot/member/repository/MemberRepository.java new file mode 100644 index 0000000..e504b30 --- /dev/null +++ b/src/main/java/com/springboot/member/repository/MemberRepository.java @@ -0,0 +1,10 @@ +package com.springboot.member.repository; + +import com.springboot.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + Optional findByEmail (String email); +} diff --git a/src/main/java/com/springboot/member/service/MemberService.java b/src/main/java/com/springboot/member/service/MemberService.java new file mode 100644 index 0000000..4732ea4 --- /dev/null +++ b/src/main/java/com/springboot/member/service/MemberService.java @@ -0,0 +1,97 @@ +package com.springboot.member.service; + +import com.springboot.board.entity.Board; +import com.springboot.exception.BusinessLogicException; +import com.springboot.exception.ExceptionCode; +import com.springboot.member.entity.Member; +import com.springboot.member.repository.MemberRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Slf4j +// 트랜잭션은 Spring AOP 를 통해 구현되어 있다. 선언적 트랜잭션 처리를 지원. +// 클래스, 매서드에 선언되면 해당 클래스에 트랜잭션이 적용된 프록시 객체 생성. +@Transactional +@Service +public class MemberService { + private final MemberRepository memberRepository; + + public MemberService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + // controller의 Post + public Member createMember (Member member) { + // 이메일 검증을 해야 함. 있는 이메일인지 없는 이메일인지. + verifyExistsEmail(member.getEmail()); + return memberRepository.save(member); + } + + // controller의 Patch + public Member updateMember (Member member) { + // 멤버 아이디를 보고, 있는 멤버인지 아닌지 확인함. + Member findMember = findVerifedMember(member.getMemberId()); + // 일반 객체를 optional 로 해서 null 값 을 받아주어야 update 가 가능함. + Optional.ofNullable(member.getName()) + // 만약 존재한다면, set을 사용해서 name 을 수정해라. + .ifPresent(name -> findMember.setName(name)); + Optional.ofNullable(member.getPhone()) + .ifPresent(phone -> findMember.setPhone(phone)); + Optional.ofNullable(member.getMemberStatus()) + .ifPresent(status -> findMember.setMemberStatus(status)); + findMember.setModifiedAt(LocalDateTime.now()); + + return memberRepository.save(findMember); + } + + // controller 의 Get 은 memberId 를 받아주어야 함. + public Member findMember (long memberId) { + // 있는 memberId 인지 확인하는 메서드. + return findVerifedMember(memberId); + } + + // 전체조회를 하는 것이기 때문에 페이지 네이션이 필요함. + public Page findMembers (int page, int size) { + // Repository 에서 다 찾아오는데, 페이지네이션을 해야 함. + // 입력받은 page 와 size 를 기준으로, sort.by 를 해야 함. 그리고 내림차순으로. + return memberRepository.findAll(PageRequest.of(page, size, Sort.by("memberId").descending())); + } + + // delete 는 memberId 조회를 해서 없애야 함. + public void deleteMember (long memberId) { + // 멤버 아이디로 멤버를 찾고, + Member findMember = findVerifedMember(memberId); + // member의 상태를 전환시킨다. + findMember.setMemberStatus(Member.MemberStatus.MEMBER_QUIT); + // 질문을 비활성화 한다. 질문은 boards 라서 list 임. + List boards = findMember.getBoards(); + for (Board board : boards) { + board.setQuestionStatus(Board.QuestionStatus.QUESTION_DEACTIVED); + } + memberRepository.save(findMember); + } + + private void verifyExistsEmail (String email) { + Optional member = memberRepository.findByEmail(email); + if (member.isPresent()) { + throw new BusinessLogicException(ExceptionCode.MEMBER_EXISTS); + } + } + + public Member findVerifedMember (long memberId) { + Optional member = memberRepository.findById(memberId); + // optional 일 때에는 Null 예외처리를 해 주어야 함. + // 찾은 멤버가 있으면 반환, 없으면 businessLogicException 던지기. + Member findMember = member.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND)); + return findMember; + } + +} diff --git a/src/main/java/com/springboot/page/SortType.java b/src/main/java/com/springboot/page/SortType.java new file mode 100644 index 0000000..4da8f59 --- /dev/null +++ b/src/main/java/com/springboot/page/SortType.java @@ -0,0 +1,19 @@ +package com.springboot.page; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum SortType { + TIME_ASC("오래된 글 순", "TIME_ASC"), + TIME_DESC("최신글 순", "TIME_DESC"), + LIKE_ASC("좋아요 적은 순", "LIKE_ASC"), + LIKE_DESC("좋아요 많은 순", "LIKE_DESC"), + VIEW_ASC("조회수 적은 순", "VIEW_ASC"), + VIEW_DESC("좋아요 많은 순", "VIEW_DESC"); + + @Getter + public final String korDes; + public final String engDes; +} diff --git a/src/main/java/com/springboot/response/ErrorResponse.java b/src/main/java/com/springboot/response/ErrorResponse.java new file mode 100644 index 0000000..6a77265 --- /dev/null +++ b/src/main/java/com/springboot/response/ErrorResponse.java @@ -0,0 +1,99 @@ +package com.springboot.response; + +import com.springboot.exception.ExceptionCode; +import lombok.Getter; +import org.springframework.http.HttpStatus; +import org.springframework.validation.BindingResult; + +import javax.validation.ConstraintViolation; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Getter +public class ErrorResponse { + private int status; + private String message; + private List fieldErrors; + private List violationErrors; + + private ErrorResponse(int status, String message) { + this.status = status; + this.message = message; + } + + private ErrorResponse(final List fieldErrors, + final List violationErrors) { + this.fieldErrors = fieldErrors; + this.violationErrors = violationErrors; + } + + public static ErrorResponse of(BindingResult bindingResult) { + return new ErrorResponse(FieldError.of(bindingResult), null); + } + + public static ErrorResponse of(Set> violations) { + return new ErrorResponse(null, ConstraintViolationError.of(violations)); + } + + public static ErrorResponse of(ExceptionCode exceptionCode) { + return new ErrorResponse(exceptionCode.getStatus(), exceptionCode.getMessage()); + } + + public static ErrorResponse of(HttpStatus httpStatus) { + return new ErrorResponse(httpStatus.value(), httpStatus.getReasonPhrase()); + } + + public static ErrorResponse of(HttpStatus httpStatus, String message) { + return new ErrorResponse(httpStatus.value(), message); + } + + @Getter + public static class FieldError { + private String field; + private Object rejectedValue; + private String reason; + + private FieldError(String field, Object rejectedValue, String reason) { + this.field = field; + this.rejectedValue = rejectedValue; + this.reason = reason; + } + + public static List of(BindingResult bindingResult) { + final List fieldErrors = + bindingResult.getFieldErrors(); + return fieldErrors.stream() + .map(error -> new FieldError( + error.getField(), + error.getRejectedValue() == null ? + "" : error.getRejectedValue().toString(), + error.getDefaultMessage())) + .collect(Collectors.toList()); + } + } + + @Getter + public static class ConstraintViolationError { + private String propertyPath; + private Object rejectedValue; + private String reason; + + private ConstraintViolationError(String propertyPath, Object rejectedValue, + String reason) { + this.propertyPath = propertyPath; + this.rejectedValue = rejectedValue; + this.reason = reason; + } + + public static List of( + Set> constraintViolations) { + return constraintViolations.stream() + .map(constraintViolation -> new ConstraintViolationError( + constraintViolation.getPropertyPath().toString(), + constraintViolation.getInvalidValue().toString(), + constraintViolation.getMessage() + )).collect(Collectors.toList()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/springboot/utils/UriCreator.java b/src/main/java/com/springboot/utils/UriCreator.java new file mode 100644 index 0000000..3df0164 --- /dev/null +++ b/src/main/java/com/springboot/utils/UriCreator.java @@ -0,0 +1,12 @@ +package com.springboot.utils; + +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; + +public class UriCreator { + public static URI createUri (String defaultUrl, long resourceId) { + return UriComponentsBuilder.newInstance().path(defaultUrl +"{/resource-id}") + .buildAndExpand(resourceId).toUri(); + } +} diff --git a/src/main/java/com/springboot/validator/NotSpace.java b/src/main/java/com/springboot/validator/NotSpace.java new file mode 100644 index 0000000..b592bd4 --- /dev/null +++ b/src/main/java/com/springboot/validator/NotSpace.java @@ -0,0 +1,17 @@ +package com.springboot.validator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = {NotSpaceValidator.class}) +public @interface NotSpace { + String message() default "공백이 아니어야 합니다"; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/springboot/validator/NotSpaceValidator.java b/src/main/java/com/springboot/validator/NotSpaceValidator.java new file mode 100644 index 0000000..b194a6d --- /dev/null +++ b/src/main/java/com/springboot/validator/NotSpaceValidator.java @@ -0,0 +1,19 @@ +package com.springboot.validator; + +import org.springframework.util.StringUtils; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class NotSpaceValidator implements ConstraintValidator { + @Override + public void initialize(NotSpace constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return value == null || StringUtils.hasText(value); + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 39a4a19..ebeb500 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,3 +9,14 @@ spring: hibernate: ddl-auto: create # (1) 스키마 자동 생성 show-sql: true # (2) SQL 쿼리 출력 +logging: + level: + org: + springframework: + orm: + jpa: DEBUG +server: + ssl: + key-store: classpath:localhost.p12 # 인증서 경로 작성 + key-store-type: PKCS12 # 인증서 형식 작성 + key-store-password: changeit # 인증서 비밀번호를 작성 changeit은 설정하지 않았을 때의 기본값 \ No newline at end of file From aee2eaf384181e76b61fbc2d03ffb11d80eae6a0 Mon Sep 17 00:00:00 2001 From: skyla00 Date: Tue, 16 Jul 2024 18:08:03 +0900 Subject: [PATCH 2/5] =?UTF-8?q?security=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 ++ .../com/springboot/auth/dto/LoginDto.java | 9 ++ .../auth/filter/JwtAuthenticationFilter.java | 68 ++++++++++++ .../auth/filter/JwtVerificationFilter.java | 64 +++++++++++ .../MemberAuthenticationFailureHandler.java | 32 ++++++ .../MemberAuthenticationSuccessHandler.java | 18 ++++ .../com/springboot/auth/jwt/JwtTokenizer.java | 92 ++++++++++++++++ .../userDetails/MemberDetailsService.java | 70 ++++++++++++ .../auth/utils/JwtAuthorityUtils.java | 38 +++++++ .../board/controller/BoardController.java | 5 +- .../comment/controller/CommentController.java | 4 +- .../config/SecurityConfiguration.java | 101 ++++++++++++++++++ .../member/controller/MemberController.java | 4 +- .../springboot/member/dto/MemberPostDto.java | 3 + .../com/springboot/member/entity/Member.java | 7 ++ .../member/service/MemberService.java | 15 ++- src/main/resources/application.yml | 12 ++- 17 files changed, 537 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/springboot/auth/dto/LoginDto.java create mode 100644 src/main/java/com/springboot/auth/filter/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/springboot/auth/filter/JwtVerificationFilter.java create mode 100644 src/main/java/com/springboot/auth/handler/MemberAuthenticationFailureHandler.java create mode 100644 src/main/java/com/springboot/auth/handler/MemberAuthenticationSuccessHandler.java create mode 100644 src/main/java/com/springboot/auth/jwt/JwtTokenizer.java create mode 100644 src/main/java/com/springboot/auth/userDetails/MemberDetailsService.java create mode 100644 src/main/java/com/springboot/auth/utils/JwtAuthorityUtils.java create mode 100644 src/main/java/com/springboot/config/SecurityConfiguration.java diff --git a/build.gradle b/build.gradle index 6afc82e..48421c8 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,13 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' implementation 'org.mapstruct:mapstruct:1.5.1.Final' annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.1.Final' + implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation 'com.google.code.gson:gson' + implementation 'org.springframework.boot:spring-boot-starter-security' + + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' } tasks.named('javadoc') { diff --git a/src/main/java/com/springboot/auth/dto/LoginDto.java b/src/main/java/com/springboot/auth/dto/LoginDto.java new file mode 100644 index 0000000..03c3c2c --- /dev/null +++ b/src/main/java/com/springboot/auth/dto/LoginDto.java @@ -0,0 +1,9 @@ +package com.springboot.auth.dto; + +import lombok.Getter; + +@Getter +public class LoginDto { + private String username; + private String password; +} diff --git a/src/main/java/com/springboot/auth/filter/JwtAuthenticationFilter.java b/src/main/java/com/springboot/auth/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..3c2c359 --- /dev/null +++ b/src/main/java/com/springboot/auth/filter/JwtAuthenticationFilter.java @@ -0,0 +1,68 @@ +package com.springboot.auth.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.springboot.auth.dto.LoginDto; +import com.springboot.auth.jwt.JwtTokenizer; +import com.springboot.member.entity.Member; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter { + private final AuthenticationManager authenticationManager; + private final JwtTokenizer jwtTokenizer; + + + public JwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtTokenizer jwtTokenizer) { + this.authenticationManager = authenticationManager; + this.jwtTokenizer = jwtTokenizer; + } + + public Authentication atteptAuthentication (HttpServletRequest request, HttpServletResponse response) throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + LoginDto loginDto = objectMapper.readValue(request.getInputStream(), LoginDto.class); + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword()); + return authenticationManager.authenticate(authenticationToken); + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { + Member member = (Member) authResult.getPrincipal(); + String accessToken = delegateAccessToken(member); + String refreshToken = delegateRefreshToken(member); + response.setHeader("Authorization", "Bearer " + accessToken); + response.setHeader("Refresh", refreshToken); + this.getSuccessHandler().onAuthenticationSuccess(request, response, authResult); + } + + protected String delegateAccessToken (Member member) { + Map claims = new HashMap<>(); + claims.put("username", member.getEmail()); + claims.put("roles", member.getRoles()); + String subject = member.getEmail(); + Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes()); + String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey()); + + String accessToken = jwtTokenizer.generateAccessToken(claims, subject,expiration,base64EncodedSecretKey); + return accessToken; + } + protected String delegateRefreshToken (Member member) { + String subject = member.getEmail(); + Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getRefreshTokenExpirationMinutes()); + String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey()); + + String refreshToken = jwtTokenizer.generateRefreshToken(subject, expiration, base64EncodedSecretKey); + return refreshToken; + } +} diff --git a/src/main/java/com/springboot/auth/filter/JwtVerificationFilter.java b/src/main/java/com/springboot/auth/filter/JwtVerificationFilter.java new file mode 100644 index 0000000..f473e60 --- /dev/null +++ b/src/main/java/com/springboot/auth/filter/JwtVerificationFilter.java @@ -0,0 +1,64 @@ +package com.springboot.auth.filter; + +import com.springboot.auth.jwt.JwtTokenizer; +import com.springboot.auth.utils.JwtAuthorityUtils; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class JwtVerificationFilter extends OncePerRequestFilter { + private final JwtTokenizer jwtTokenizer; + private final JwtAuthorityUtils jwtAuthorityUtils; + + public JwtVerificationFilter(JwtTokenizer jwtTokenizer, JwtAuthorityUtils jwtAuthorityUtils) { + this.jwtTokenizer = jwtTokenizer; + this.jwtAuthorityUtils = jwtAuthorityUtils; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + // 검증된 claims 를 데리고 와서 + Map claims = verifyJws(request); + // claims 를 넣고 Context 를 만들어줌. + setAuthenticationToContext(claims); + filterChain.doFilter(request, response); + } + + @Override + protected boolean shouldNotFilter (HttpServletRequest request) throws ServletException { + String authorization = request.getHeader("Authorization"); + return authorization == null || !authorization.startsWith("Bearer"); + } + + // 요청 헤더에서 Jws 를 데리고 옴. + // getClaims 로 검증을 함. + private Map verifyJws (HttpServletRequest request) { + String jws = request.getHeader("Authorization").replace("Bearer ", ""); + String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey()); + // 검증된 claims 를 받음. claims 에 있는 body 를 데리고 오기 때문에 getBody() + Map claims = jwtTokenizer.getClaims(jws, base64EncodedSecretKey).getBody(); + return claims; + } + + private void setAuthenticationToContext (Map claims) { + String username = (String) claims.get("username"); + // claims 에 있는 Roles 정보를 데리고 와서, Authorities 를 만든다. + List authorities = jwtAuthorityUtils.createAuthorities((List)claims.get("roles")); + // 이걸 Authentication 객체로 만듦. + Authentication authentication = new UsernamePasswordAuthenticationToken( + username, null, authorities); + //Security Context 에 만든 authentication 을 넣음. + SecurityContextHolder.getContext().setAuthentication(authentication); + } +} diff --git a/src/main/java/com/springboot/auth/handler/MemberAuthenticationFailureHandler.java b/src/main/java/com/springboot/auth/handler/MemberAuthenticationFailureHandler.java new file mode 100644 index 0000000..8e97442 --- /dev/null +++ b/src/main/java/com/springboot/auth/handler/MemberAuthenticationFailureHandler.java @@ -0,0 +1,32 @@ +package com.springboot.auth.handler; + +import com.google.gson.Gson; +import com.springboot.response.ErrorResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +public class MemberAuthenticationFailureHandler implements AuthenticationFailureHandler { + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { + log.info("Authenticated failed"); + log.error("Authenticated failed", exception.getMessage()); + sendErrorResponse(response); + } + + private void sendErrorResponse(HttpServletResponse response) throws IOException { + Gson gson = new Gson(); + ErrorResponse errorResponse = ErrorResponse.of(HttpStatus.UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.getWriter().write(gson.toJson(errorResponse, ErrorResponse.class)); + } +} diff --git a/src/main/java/com/springboot/auth/handler/MemberAuthenticationSuccessHandler.java b/src/main/java/com/springboot/auth/handler/MemberAuthenticationSuccessHandler.java new file mode 100644 index 0000000..012834f --- /dev/null +++ b/src/main/java/com/springboot/auth/handler/MemberAuthenticationSuccessHandler.java @@ -0,0 +1,18 @@ +package com.springboot.auth.handler; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +public class MemberAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + log.info("Authenticated Success"); + } +} diff --git a/src/main/java/com/springboot/auth/jwt/JwtTokenizer.java b/src/main/java/com/springboot/auth/jwt/JwtTokenizer.java new file mode 100644 index 0000000..263b9b2 --- /dev/null +++ b/src/main/java/com/springboot/auth/jwt/JwtTokenizer.java @@ -0,0 +1,92 @@ +package com.springboot.auth.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.security.Keys; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Calendar; +import java.util.Date; +import java.util.Map; + +@Component +public class JwtTokenizer { + @Getter + @Value("${jwt.key}") + private String secretKey; + + @Getter + @Value("${jwt.access-token-expiration-minutes}") + private int accessTokenExpirationMinutes; + + @Getter + @Value("${jwt.refresh-token-expiration-minutes}") + private int refreshTokenExpirationMinutes; + + public String encodeBase64SecretKey (String secretKey) { + // secretKey를 암호화. + return Encoders.BASE64.encode(secretKey.getBytes(StandardCharsets.UTF_8)); + } + // 해시함수로 암호화한 키. + public Key getKeyFromBase64EncodeKey (String base64EncodedSecretKey) { + // 디코딩 한 후에 + byte[] keyBytes = Decoders.BASE64URL.decode(base64EncodedSecretKey); + // 다시 해시 함수로 암호화. + Key key = Keys.hmacShaKeyFor(keyBytes); + return key; + } + //AccessToken 받기. + public String generateAccessToken (Map claims, + String subject, + Date expiration, String base64EncodedSecretKey) { + Key key = getKeyFromBase64EncodeKey(base64EncodedSecretKey); + return Jwts.builder() + .setClaims(claims) + .setSubject(subject) + .setIssuedAt(Calendar.getInstance().getTime()) + .setExpiration(expiration) + .signWith(key) + .compact(); + } + public String generateRefreshToken(String subject, + Date expiration, String base64EncodedSecretKey) { + Key key = getKeyFromBase64EncodeKey(base64EncodedSecretKey); + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(Calendar.getInstance().getTime()) + .setExpiration(expiration) + .signWith(key) + .compact(); + } + + //검증된 jwt만 받아짐. + public Jws getClaims (String jws, String baseEncodedSecretKey) { + Key key = getKeyFromBase64EncodeKey(baseEncodedSecretKey); + Jws claims = Jwts.parserBuilder() + .setSigningKey(key) + .build().parseClaimsJws(jws); + return claims; + } + + public void verifySignature (String jws, String base64EncodedSecretKey) { + Key key = getKeyFromBase64EncodeKey(base64EncodedSecretKey); + Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(jws); + } + + public Date getTokenExpiration (int expirationMinutes) { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MINUTE, expirationMinutes); + Date expiration = calendar.getTime(); + return expiration; + } +} diff --git a/src/main/java/com/springboot/auth/userDetails/MemberDetailsService.java b/src/main/java/com/springboot/auth/userDetails/MemberDetailsService.java new file mode 100644 index 0000000..4c62935 --- /dev/null +++ b/src/main/java/com/springboot/auth/userDetails/MemberDetailsService.java @@ -0,0 +1,70 @@ +package com.springboot.auth.userDetails; + +import com.springboot.auth.utils.JwtAuthorityUtils; +import com.springboot.exception.BusinessLogicException; +import com.springboot.exception.ExceptionCode; +import com.springboot.member.entity.Member; +import com.springboot.member.repository.MemberRepository; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +import java.util.Collection; +import java.util.Optional; + +public class MemberDetailsService implements UserDetailsService { + private final MemberRepository memberRepository; + private final JwtAuthorityUtils authorityUtils; + + public MemberDetailsService(MemberRepository memberRepository, JwtAuthorityUtils authorityUtils) { + this.memberRepository = memberRepository; + this.authorityUtils = authorityUtils; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + Optional optionalMember = memberRepository.findByEmail(username); + Member findMember = optionalMember.orElseThrow( () -> + new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND)); + return new MemberDetails(findMember); + } + private final class MemberDetails extends Member implements UserDetails { + public MemberDetails(Member member) { + setMemberId(member.getMemberId()); + setEmail(member.getEmail()); + setPassword(member.getPassword()); + setRoles(member.getRoles()); + } + + @Override + public Collection getAuthorities() { + return authorityUtils.createAuthorities(this.getRoles()); + } + + @Override + public String getUsername() { + return getEmail(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + } +} diff --git a/src/main/java/com/springboot/auth/utils/JwtAuthorityUtils.java b/src/main/java/com/springboot/auth/utils/JwtAuthorityUtils.java new file mode 100644 index 0000000..f3834d3 --- /dev/null +++ b/src/main/java/com/springboot/auth/utils/JwtAuthorityUtils.java @@ -0,0 +1,38 @@ +package com.springboot.auth.utils; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Component; + +import javax.print.DocFlavor; +import java.util.List; +import java.util.stream.Collectors; + +@Component +public class JwtAuthorityUtils { + @Value("${mail.address.admin}") + private String adminMailAddress; + + private final List ADMIN_ROLES = + AuthorityUtils.createAuthorityList("ROLE_ADMIN", "ROLE_USER"); + private final List USER_ROLES = + AuthorityUtils.createAuthorityList("ROLE_USER"); + private final List ADMIN_ROLES_STRING = + List.of("ADMIN", "USER"); + private final List USER_ROLES_STRING = + List.of("USER"); + + public List createAuthorities (List roles) { + return roles.stream() + .map(role -> new SimpleGrantedAuthority("ROLE_"+ role)) + .collect(Collectors.toList()); + } + public List createRoles (String email) { + if(email.equals(adminMailAddress)) { + return ADMIN_ROLES_STRING; + } + return USER_ROLES_STRING; + } + } diff --git a/src/main/java/com/springboot/board/controller/BoardController.java b/src/main/java/com/springboot/board/controller/BoardController.java index b4741eb..04ef93e 100644 --- a/src/main/java/com/springboot/board/controller/BoardController.java +++ b/src/main/java/com/springboot/board/controller/BoardController.java @@ -13,6 +13,7 @@ import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -22,10 +23,10 @@ import java.util.List; @RestController -@RequestMapping("/v11/boards") +@RequestMapping("/v2/boards") @Validated public class BoardController { - private final static String BOARD_DEFAULT_URL = "/v11/boards"; + private final static String BOARD_DEFAULT_URL = "/v2/boards"; private final BoardService boardService; private final MemberService memberService; diff --git a/src/main/java/com/springboot/comment/controller/CommentController.java b/src/main/java/com/springboot/comment/controller/CommentController.java index 0fc7ecc..5963599 100644 --- a/src/main/java/com/springboot/comment/controller/CommentController.java +++ b/src/main/java/com/springboot/comment/controller/CommentController.java @@ -24,10 +24,10 @@ import java.net.URI; @RestController -@RequestMapping("/v11/comments") +@RequestMapping("/v2/comments") @Validated public class CommentController { - private final static String COMMENT_DEFAULT_URL = "/v11/comments"; + private final static String COMMENT_DEFAULT_URL = "/v2/comments"; private final CommentService commentService; private final CommentMapper mapper; diff --git a/src/main/java/com/springboot/config/SecurityConfiguration.java b/src/main/java/com/springboot/config/SecurityConfiguration.java new file mode 100644 index 0000000..f5bc90f --- /dev/null +++ b/src/main/java/com/springboot/config/SecurityConfiguration.java @@ -0,0 +1,101 @@ +package com.springboot.config; + +import com.springboot.auth.filter.JwtAuthenticationFilter; +import com.springboot.auth.filter.JwtVerificationFilter; +import com.springboot.auth.handler.MemberAuthenticationFailureHandler; +import com.springboot.auth.jwt.JwtTokenizer; +import com.springboot.auth.utils.JwtAuthorityUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; + +import static org.springframework.security.config.Customizer.withDefaults; + +@Configuration +public class SecurityConfiguration { + private final JwtTokenizer jwtTokenizer; + private final JwtAuthorityUtils jwtAuthorityUtils; + + public SecurityConfiguration(JwtTokenizer jwtTokenizer, JwtAuthorityUtils jwtAuthorityUtils) { + this.jwtTokenizer = jwtTokenizer; + this.jwtAuthorityUtils = jwtAuthorityUtils; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // h2 웹 콘솔 화면 자체가 내부적으로 을 사용하고 있음. 이를 정상적으로 수행하도록 함. + // 동일 출처로부터 들어오는 요청만 페이지 렌더링을 허용. + http.headers().frameOptions().sameOrigin() + .and() + .csrf().disable() + .cors(withDefaults()) + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .formLogin().disable() + .httpBasic().disable() + .apply(new CustomFilterConfigurer()) + .and() + .authorizeHttpRequests(authorize -> authorize + // User 가 회원가입을 해서 로그인을 하면. + + // boards post, patch, 한 건의 get, Delete + // Get 의 경우에 모든 글은 다른 회원도 볼 수 있음. 근데, + // 비밀글 상태인 질문은 질문을 등록한 회원(고객)과 관리자만 조회할 수 있다. + // 비밀글이면 로그인 한 상태에서 MemberId 가 같은지 다른지 조건을 달아주어야 하나. + .antMatchers(HttpMethod.POST, "/*/boards").hasRole("USER") // (1) 추가 + .antMatchers(HttpMethod.PATCH, "/*/boards/**").hasRole("USER") // (2) 추가 + .antMatchers(HttpMethod.GET, "/*/boards").hasRole("ADMIN") // (3) 추가 + .antMatchers(HttpMethod.GET, "/*/boards/**").hasAnyRole("USER", "ADMIN") // (4) 추가 + .antMatchers(HttpMethod.DELETE, "/*/boards/**").hasRole("USER") // (5) 추가 + .anyRequest().permitAll()); + return http.build(); + } + + + @Bean + public PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + @Bean + CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList("*")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELETE")); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + public class CustomFilterConfigurer extends AbstractHttpConfigurer { + @Override + public void configure (HttpSecurity builder) { + AuthenticationManager authenticationManager = + builder.getSharedObject(AuthenticationManager.class); + JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer); + jwtAuthenticationFilter.setFilterProcessesUrl("v2/auth/login"); + jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler()); + jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler()); + JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenizer, jwtAuthorityUtils); + + builder.addFilter(jwtAuthenticationFilter) + // Authentication 다음에 Verification 필터를 실행해라 + .addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class); + } + } + +} diff --git a/src/main/java/com/springboot/member/controller/MemberController.java b/src/main/java/com/springboot/member/controller/MemberController.java index c38aef2..b1a3a66 100644 --- a/src/main/java/com/springboot/member/controller/MemberController.java +++ b/src/main/java/com/springboot/member/controller/MemberController.java @@ -23,11 +23,11 @@ @RestController //특정 url로 요청을 보내면 controller에서 어떠한 방식으로 처리할지 정의. //이때 들어온 요청을 특정 메서드와 매핑하기 위해 사용되는 어노테이션 -@RequestMapping("/v11/members") +@RequestMapping("/v2/members") //유효성 검증 클래스에 붇임.. 메서드에는 @Valid @Validated public class MemberController { - private final static String MEMBER_DEFAULT_URL = "/v11/members"; + private final static String MEMBER_DEFAULT_URL = "/v2/members"; //의존성 주입. private final MemberService memberService; diff --git a/src/main/java/com/springboot/member/dto/MemberPostDto.java b/src/main/java/com/springboot/member/dto/MemberPostDto.java index eaa06e0..15f5950 100644 --- a/src/main/java/com/springboot/member/dto/MemberPostDto.java +++ b/src/main/java/com/springboot/member/dto/MemberPostDto.java @@ -13,6 +13,9 @@ public class MemberPostDto { @Email private String email; + @NotBlank + private String password; + @NotBlank(message = "이름은 공백이 아니어야 합니다.") private String name; diff --git a/src/main/java/com/springboot/member/entity/Member.java b/src/main/java/com/springboot/member/entity/Member.java index 4a3215f..69c3571 100644 --- a/src/main/java/com/springboot/member/entity/Member.java +++ b/src/main/java/com/springboot/member/entity/Member.java @@ -28,9 +28,16 @@ public class Member extends Auditable { @Column(length = 100, nullable = false) private String name; + @Column(length = 100, nullable = false) + private String password; + @Column(length = 100, nullable = false) private String phone; + // 사용자 권한 등록 위한 테이블 사용. + @ElementCollection(fetch = FetchType.EAGER) + private List roles = new ArrayList<>(); + @Enumerated(value = EnumType.STRING) @Column(nullable = false) private MemberStatus memberStatus = MemberStatus.MEMBER_ACTIVE; diff --git a/src/main/java/com/springboot/member/service/MemberService.java b/src/main/java/com/springboot/member/service/MemberService.java index 4732ea4..ac990e3 100644 --- a/src/main/java/com/springboot/member/service/MemberService.java +++ b/src/main/java/com/springboot/member/service/MemberService.java @@ -1,5 +1,6 @@ package com.springboot.member.service; +import com.springboot.auth.utils.JwtAuthorityUtils; import com.springboot.board.entity.Board; import com.springboot.exception.BusinessLogicException; import com.springboot.exception.ExceptionCode; @@ -9,6 +10,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,15 +25,26 @@ @Service public class MemberService { private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + private final JwtAuthorityUtils authorityUtils; - public MemberService(MemberRepository memberRepository) { + public MemberService(MemberRepository memberRepository, PasswordEncoder passwordEncoder, JwtAuthorityUtils authorityUtils) { this.memberRepository = memberRepository; + this.passwordEncoder = passwordEncoder; + this.authorityUtils = authorityUtils; } // controller의 Post public Member createMember (Member member) { // 이메일 검증을 해야 함. 있는 이메일인지 없는 이메일인지. verifyExistsEmail(member.getEmail()); + // password 암호화 + String encryptedPassword = passwordEncoder.encode(member.getPassword()); + member.setPassword(encryptedPassword); + + // db에 userRole 저장 + List roles = authorityUtils.createRoles(member.getEmail()); + member.setRoles(roles); return memberRepository.save(member); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ebeb500..c8ff231 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -15,8 +15,10 @@ logging: springframework: orm: jpa: DEBUG -server: - ssl: - key-store: classpath:localhost.p12 # 인증서 경로 작성 - key-store-type: PKCS12 # 인증서 형식 작성 - key-store-password: changeit # 인증서 비밀번호를 작성 changeit은 설정하지 않았을 때의 기본값 \ No newline at end of file +mail: + address: + admin: admin@gmail.com +jwt: + key: ${JWT_SECRET_KEY} + access-token-expiration-minutes: 30 + refresh-token-expiration-minutes: 420 From 1b97bc78cc75fb99772303863ee419e769e3de09 Mon Sep 17 00:00:00 2001 From: skyla00 Date: Wed, 17 Jul 2024 15:13:21 +0900 Subject: [PATCH 3/5] =?UTF-8?q?security=20=EC=88=98=EC=A0=952?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../auth/filter/JwtAuthenticationFilter.java | 9 ++++++--- .../auth/userDetails/MemberDetailsService.java | 3 +++ .../com/springboot/auth/utils/JwtAuthorityUtils.java | 1 - .../com/springboot/board/service/BoardService.java | 3 --- .../springboot/comment/service/CommentService.java | 1 - .../com/springboot/config/SecurityConfiguration.java | 10 +++++----- src/main/resources/application.yml | 12 +++++++++++- 8 files changed, 26 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index 48421c8..c7e8077 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,7 @@ dependencies { annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.1.Final' implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'com.google.code.gson:gson' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' diff --git a/src/main/java/com/springboot/auth/filter/JwtAuthenticationFilter.java b/src/main/java/com/springboot/auth/filter/JwtAuthenticationFilter.java index 3c2c359..812651b 100644 --- a/src/main/java/com/springboot/auth/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/springboot/auth/filter/JwtAuthenticationFilter.java @@ -4,6 +4,7 @@ import com.springboot.auth.dto.LoginDto; import com.springboot.auth.jwt.JwtTokenizer; import com.springboot.member.entity.Member; +import lombok.SneakyThrows; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -28,11 +29,13 @@ public JwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtT this.jwtTokenizer = jwtTokenizer; } - public Authentication atteptAuthentication (HttpServletRequest request, HttpServletResponse response) throws IOException { + @Override + @SneakyThrows + public Authentication attemptAuthentication (HttpServletRequest request, HttpServletResponse response) { ObjectMapper objectMapper = new ObjectMapper(); LoginDto loginDto = objectMapper.readValue(request.getInputStream(), LoginDto.class); UsernamePasswordAuthenticationToken authenticationToken = - new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword()); + new UsernamePasswordAuthenticationToken(loginDto.getUsername(),loginDto.getPassword()); return authenticationManager.authenticate(authenticationToken); } @@ -43,7 +46,7 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR String refreshToken = delegateRefreshToken(member); response.setHeader("Authorization", "Bearer " + accessToken); response.setHeader("Refresh", refreshToken); - this.getSuccessHandler().onAuthenticationSuccess(request, response, authResult); +// this.getSuccessHandler().onAuthenticationSuccess(request, response, authResult); } protected String delegateAccessToken (Member member) { diff --git a/src/main/java/com/springboot/auth/userDetails/MemberDetailsService.java b/src/main/java/com/springboot/auth/userDetails/MemberDetailsService.java index 4c62935..b4d17bd 100644 --- a/src/main/java/com/springboot/auth/userDetails/MemberDetailsService.java +++ b/src/main/java/com/springboot/auth/userDetails/MemberDetailsService.java @@ -9,10 +9,13 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; import java.util.Collection; import java.util.Optional; + +@Component public class MemberDetailsService implements UserDetailsService { private final MemberRepository memberRepository; private final JwtAuthorityUtils authorityUtils; diff --git a/src/main/java/com/springboot/auth/utils/JwtAuthorityUtils.java b/src/main/java/com/springboot/auth/utils/JwtAuthorityUtils.java index f3834d3..28be4b0 100644 --- a/src/main/java/com/springboot/auth/utils/JwtAuthorityUtils.java +++ b/src/main/java/com/springboot/auth/utils/JwtAuthorityUtils.java @@ -6,7 +6,6 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.stereotype.Component; -import javax.print.DocFlavor; import java.util.List; import java.util.stream.Collectors; diff --git a/src/main/java/com/springboot/board/service/BoardService.java b/src/main/java/com/springboot/board/service/BoardService.java index 3b4530c..411a062 100644 --- a/src/main/java/com/springboot/board/service/BoardService.java +++ b/src/main/java/com/springboot/board/service/BoardService.java @@ -117,11 +117,8 @@ public Board findBoard (long boardId, long memberId) { Member findMember = member.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND)); Optional view = viewRepository.findByMemberAndBoard(findMember, findBoard); - - if(view.isPresent()) { view.orElseThrow(() -> new BusinessLogicException(ExceptionCode.VIEW_NOT_FOUND)); - // delete 를 해라. } else { View addView = new View(); addView.setBoard(findBoard); diff --git a/src/main/java/com/springboot/comment/service/CommentService.java b/src/main/java/com/springboot/comment/service/CommentService.java index ce8f6da..d7f38aa 100644 --- a/src/main/java/com/springboot/comment/service/CommentService.java +++ b/src/main/java/com/springboot/comment/service/CommentService.java @@ -37,7 +37,6 @@ public Comment createComment (Comment comment) { comment.setVisibilityStatus(Comment.VisibilityStatus.SECRET); } - //comment 를 저장해야 되는데????? 이렇게 하면 되나?????????? return commentRepository.save(comment); } diff --git a/src/main/java/com/springboot/config/SecurityConfiguration.java b/src/main/java/com/springboot/config/SecurityConfiguration.java index f5bc90f..6666c12 100644 --- a/src/main/java/com/springboot/config/SecurityConfiguration.java +++ b/src/main/java/com/springboot/config/SecurityConfiguration.java @@ -3,14 +3,13 @@ import com.springboot.auth.filter.JwtAuthenticationFilter; import com.springboot.auth.filter.JwtVerificationFilter; import com.springboot.auth.handler.MemberAuthenticationFailureHandler; +import com.springboot.auth.handler.MemberAuthenticationSuccessHandler; import com.springboot.auth.jwt.JwtTokenizer; import com.springboot.auth.utils.JwtAuthorityUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; @@ -39,7 +38,8 @@ public SecurityConfiguration(JwtTokenizer jwtTokenizer, JwtAuthorityUtils jwtAut public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // h2 웹 콘솔 화면 자체가 내부적으로 을 사용하고 있음. 이를 정상적으로 수행하도록 함. // 동일 출처로부터 들어오는 요청만 페이지 렌더링을 허용. - http.headers().frameOptions().sameOrigin() + http + .headers().frameOptions().sameOrigin() .and() .csrf().disable() .cors(withDefaults()) @@ -87,8 +87,8 @@ public void configure (HttpSecurity builder) { AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class); JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer); - jwtAuthenticationFilter.setFilterProcessesUrl("v2/auth/login"); - jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler()); + jwtAuthenticationFilter.setFilterProcessesUrl("/v2/auth/login"); + jwtAuthenticationFilter.setAuthenticationSuccessHandler(new MemberAuthenticationSuccessHandler()); jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler()); JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenizer, jwtAuthorityUtils); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c8ff231..1b31404 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,16 +9,26 @@ spring: hibernate: ddl-auto: create # (1) 스키마 자동 생성 show-sql: true # (2) SQL 쿼리 출력 + properties: + hibernate: + format_sql: true # (3) SQL pretty print + sql: + init: + data-locations: classpath*:db/h2/data.sql logging: level: org: springframework: orm: jpa: DEBUG +server: + servlet: + encoding: + force-response: true mail: address: admin: admin@gmail.com jwt: key: ${JWT_SECRET_KEY} access-token-expiration-minutes: 30 - refresh-token-expiration-minutes: 420 + refresh-token-expiration-minutes: 420 \ No newline at end of file From cc6d5626027d201aa7e98f630b6ebf4ad1c9d751 Mon Sep 17 00:00:00 2001 From: skyla00 Date: Thu, 18 Jul 2024 17:55:27 +0900 Subject: [PATCH 4/5] =?UTF-8?q?security=20=EC=88=98=EC=A0=95,=20=EC=9D=B8?= =?UTF-8?q?=EA=B0=80=20=EA=B6=8C=ED=95=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/springboot/auth/jwt/JwtTokenizer.java | 4 +- .../board/controller/BoardController.java | 31 ++-- .../springboot/board/dto/BoardPatchDto.java | 1 - .../springboot/board/dto/BoardPostDto.java | 4 +- .../board/service/BoardService.java | 175 +++++++++++------- .../comment/controller/CommentController.java | 40 ++-- .../comment/dto/CommentPatchDto.java | 5 + .../comment/dto/CommentPostDto.java | 7 +- .../springboot/comment/entity/Comment.java | 8 +- .../comment/mapper/CommentMapper.java | 1 - .../comment/service/CommentService.java | 85 ++++++--- .../config/SecurityConfiguration.java | 22 ++- .../springboot/exception/ExceptionCode.java | 8 +- .../com/springboot/member/entity/Member.java | 9 + .../member/service/MemberService.java | 7 +- 15 files changed, 266 insertions(+), 141 deletions(-) diff --git a/src/main/java/com/springboot/auth/jwt/JwtTokenizer.java b/src/main/java/com/springboot/auth/jwt/JwtTokenizer.java index 263b9b2..c6cfcc0 100644 --- a/src/main/java/com/springboot/auth/jwt/JwtTokenizer.java +++ b/src/main/java/com/springboot/auth/jwt/JwtTokenizer.java @@ -71,7 +71,9 @@ public Jws getClaims (String jws, String baseEncodedSecretKey) { Key key = getKeyFromBase64EncodeKey(baseEncodedSecretKey); Jws claims = Jwts.parserBuilder() .setSigningKey(key) - .build().parseClaimsJws(jws); + .build() + .parseClaimsJws(jws); + return claims; } diff --git a/src/main/java/com/springboot/board/controller/BoardController.java b/src/main/java/com/springboot/board/controller/BoardController.java index 04ef93e..ae24deb 100644 --- a/src/main/java/com/springboot/board/controller/BoardController.java +++ b/src/main/java/com/springboot/board/controller/BoardController.java @@ -39,16 +39,16 @@ public BoardController(BoardService boardService, MemberService memberService, B } @PostMapping - public ResponseEntity postBoard (@Valid @RequestBody BoardPostDto boardPostDto) { + public ResponseEntity postBoard (@Valid @RequestBody BoardPostDto boardPostDto, + Authentication authentication) { // 메퍼로 먼저 감싼 다음에, 서비스에 적용. - Board board = mapper.boardPostDtoToBoard(boardPostDto); - Member member = new Member(); - member.setMemberId(boardPostDto.getMemberId()); - board.setMember(member); - - Board createdBoard = boardService.createBoard(board); + Board createdBoard = boardService.createBoard(board,authentication); +// Member member = new Member(); +// member.setMemberId(board.getMember().getMemberId()); +// Board createdBoard = boardService.createBoard(board, authentication); +// board.setMember(member); URI location = UriCreator.createUri(BOARD_DEFAULT_URL, createdBoard.getBoardId()); return ResponseEntity.created(location).build(); @@ -67,9 +67,11 @@ public ResponseEntity postLike (@PathVariable("board-id") @Positive long boardId @PatchMapping("/{board-id}") public ResponseEntity patchBoard (@PathVariable("board-id") @Positive long boardId, - @Valid @RequestBody BoardPatchDto boardPatchDto) { + @Valid @RequestBody BoardPatchDto boardPatchDto, + Authentication authentication) { boardPatchDto.setBoardId(boardId); - Board board = boardService.updateBoard(mapper.boardPatchDtoTOBoard(boardPatchDto)); + // 질문을 등록한 회원만 수정. + Board board = boardService.updateBoard(mapper.boardPatchDtoTOBoard(boardPatchDto), authentication); BoardResponseDto boardResponseDto = mapper.boardToBoardResponseDto(board); return new ResponseEntity<>( @@ -78,9 +80,9 @@ public ResponseEntity patchBoard (@PathVariable("board-id") @Positive long board @GetMapping("/{board-id}") public ResponseEntity getBoard ( @PathVariable("board-id") @Positive long boardId, - @Valid @RequestBody BoardGetDto boardGetDto - ) { - Board board = boardService.findBoard(boardId, boardGetDto.getMemberId()); + Authentication authentication) { + String email = (String) authentication.getPrincipal(); + Board board = boardService.findBoard(boardId,authentication); BoardResponseDto boardResponseDto = mapper.boardToBoardResponseDto(board); return new ResponseEntity<>( new SingleResponseDto<>(boardResponseDto), HttpStatus.OK); @@ -103,8 +105,9 @@ public ResponseEntity getBoards (@Positive @RequestParam int page, // 질문 삭제 상태. // 회원 탈퇴시 질문 비활성화. >> memberService @DeleteMapping("{member-id}") - public ResponseEntity deleteBoard ( @PathVariable("member-id") @Positive long boardId) { - boardService.deleteBoard(boardId); + public ResponseEntity deleteBoard ( @PathVariable("member-id") @Positive long boardId, + Authentication authentication) { + boardService.deleteBoard(boardId, authentication); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } diff --git a/src/main/java/com/springboot/board/dto/BoardPatchDto.java b/src/main/java/com/springboot/board/dto/BoardPatchDto.java index 1a2c267..fabce6e 100644 --- a/src/main/java/com/springboot/board/dto/BoardPatchDto.java +++ b/src/main/java/com/springboot/board/dto/BoardPatchDto.java @@ -8,7 +8,6 @@ @Getter public class BoardPatchDto { private long boardId; - private long viewMemberId; private String title; private String content; diff --git a/src/main/java/com/springboot/board/dto/BoardPostDto.java b/src/main/java/com/springboot/board/dto/BoardPostDto.java index 5401ed8..ffb2be3 100644 --- a/src/main/java/com/springboot/board/dto/BoardPostDto.java +++ b/src/main/java/com/springboot/board/dto/BoardPostDto.java @@ -8,8 +8,8 @@ @Getter public class BoardPostDto { - @Positive - private long memberId; +// @Positive +// private long memberId; @NotBlank private String title; diff --git a/src/main/java/com/springboot/board/service/BoardService.java b/src/main/java/com/springboot/board/service/BoardService.java index 411a062..bfa98d8 100644 --- a/src/main/java/com/springboot/board/service/BoardService.java +++ b/src/main/java/com/springboot/board/service/BoardService.java @@ -17,10 +17,16 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.stereotype.Service; import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import static com.springboot.board.entity.Board.QuestionStatus.*; import static com.springboot.page.SortType.*; @@ -43,12 +49,16 @@ public BoardService(BoardRepository boardRepository, MemberService memberService this.viewRepository = viewRepository; } - public Board createBoard (Board board) { - // 회원인지 아닌지 등록함. - verifyBoardMember(board); - Board savedOrder = saveBoard(board); - // 등록시 등록 날짜 생성. - return savedOrder; + public Board createBoard (Board board, Authentication authentication) { + if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMIN"))){ + throw new BusinessLogicException(ExceptionCode.NO_AUTHORITY); + } else { + String email = (String) authentication.getPrincipal(); + Member member = memberService.findVerifiedMember(email); + board.setMember(member); + + return boardRepository.save(board); + } } public void createLike (Like like) { @@ -74,26 +84,32 @@ public void createLike (Like like) { Like addlike= new Like(); addlike.setBoard(findBoard); addlike.setMember(findMember); - likeRepository.save(addlike); findBoard.setLikeCount(findBoard.getLikeCount() + 1); - boardRepository.save(findBoard); + likeRepository.save(addlike); + +// boardRepository.save(findBoard); } } - public Board updateBoard (Board board) { + public Board updateBoard (Board board, Authentication authentication) { // 질문을 등록한 회원만 수정할 수 있음. + String email = (String) authentication.getPrincipal(); + Member member = memberService.findVerifiedMember(email); Board findBoard = findVerifiedBoard(board.getBoardId()); - // 질문 수정 시 수정 날짜 업데이트. - findBoard.setModifiedAt(LocalDateTime.now()); - - // 비밀글로 변경할 경우, 상태 변경 수정. - Optional.ofNullable(board.getVisibilityStatus()) - .ifPresent(visibilityStatus -> findBoard.setVisibilityStatus(visibilityStatus)); - Optional.ofNullable(board.getTitle()) - .ifPresent(title -> findBoard.setTitle(title)); - Optional.ofNullable(board.getContent()) - .ifPresent(content -> findBoard.setContent(content)); + if(findBoard.getMember() == member ) { + // 질문 수정 시 수정 날짜 업데이트. + findBoard.setModifiedAt(LocalDateTime.now()); + // 비밀글로 변경할 경우, 상태 변경 수정. + Optional.ofNullable(board.getVisibilityStatus()) + .ifPresent(visibilityStatus -> findBoard.setVisibilityStatus(visibilityStatus)); + Optional.ofNullable(board.getTitle()) + .ifPresent(title -> findBoard.setTitle(title)); + Optional.ofNullable(board.getContent()) + .ifPresent(content -> findBoard.setContent(content)); + } else { + new BusinessLogicException(ExceptionCode.DIFFERENT_USER); + } // 답변 완료된 질문은 수정할 수 없음. int questionNumber = findBoard.getQuestionStatus().getQuestionNumber(); if (questionNumber == 2) { @@ -101,38 +117,37 @@ public Board updateBoard (Board board) { } return boardRepository.save(findBoard); - // 관리자일 경우 질문 상태 대답 변경. + } - public Board findBoard (long boardId, long memberId) { - // 비밀글이면 질문을 등록한 회원과 관리자만 조회 가능 + public Board findBoard (long boardId, Authentication authentication) { + // 비밀글이면 질문을 등록한 회원과 관리자만 조회 가능. // 답변이 존재하면 답변도 함께 조회해야 함. Board findBoard = findVerifiedBoard(boardId); - int questionNumber = findBoard.getQuestionStatus().getQuestionNumber(); - // 이미 삭제 상태인 질문은 조회할 수 없다. + // 비밀글을 제외한 다른 글들은 회원과 관리자 모두가 봐야 함. - // view 를 하고 있는 member 를 데리고 와야 하는 것인데?????????? - // - Optional member = memberRepository.findById(memberId); - Member findMember = member.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND)); - Optional view = viewRepository.findByMemberAndBoard(findMember, findBoard); - - if(view.isPresent()) { - view.orElseThrow(() -> new BusinessLogicException(ExceptionCode.VIEW_NOT_FOUND)); + // 비밀글이면 + if(findBoard.getVisibilityStatus().equals(Board.VisibilityStatus.SECRET)) { + // 관리자와 질문을 등록한 회원은 볼 수 있음. 게시글의 작성자와 principal 이 같은지 확인. + if(isCheckBoardOwnerAndAdmin(authentication, findBoard.getMember())) { + createdView(authentication,findBoard); + return findBoard; + } else { + throw new BusinessLogicException(ExceptionCode.NO_AUTHORITY); + } } else { - View addView = new View(); - addView.setBoard(findBoard); - addView.setMember(findMember); - viewRepository.save(addView); - findBoard.setViewCount(findBoard.getViewCount() + 1); - boardRepository.save(findBoard); - } - - if (questionNumber >= 3) { - throw new BusinessLogicException(ExceptionCode.BOARD_NOT_FOUND); + // 공개 일 때에는 다른 회원의 글도 볼 수 있어야 함....... + // 그냥 조회하면 되는 건가? + // 이거는 뷰에 대한 로직. + createdView (authentication, findBoard); + // 질문삭제, 비활성화 된 글은 보이도록 하지 않음. + // 이미 삭제 상태인 질문은 조회할 수 없다. + int questionNumber = findBoard.getQuestionStatus().getQuestionNumber(); + if (questionNumber >= 3) { + throw new BusinessLogicException(ExceptionCode.BOARD_NOT_FOUND); + } + return findBoard; } - return findBoard; - } public Page findBoards (int page, int size, String sort) { @@ -173,33 +188,36 @@ public Page findBoards (int page, int size, String sort) { .findByQuestionStatusNotAndQuestionStatusNot(QUESTION_DELETED, QUESTION_DEACTIVED, pageable); } - public void deleteBoard (long boardId) { + public void deleteBoard (long boardId, Authentication authentication) { // 질문 등록한 회원만 가능. - - Board findBoard = findVerifiedBoard(boardId); - int questionNumber = findBoard.getQuestionStatus().getQuestionNumber(); - if( questionNumber == 1) { - findBoard.setQuestionStatus(QUESTION_DELETED); - boardRepository.save(findBoard); - } - else if( questionNumber == 2) { - // 질문 삭제하면 질문 상태만 변경. - findBoard.setQuestionStatus(QUESTION_DELETED); - // comment 는 지워져야 함. - commentService.deleteComment(findBoard.getComment().getCommentId()); - boardRepository.save(findBoard); - } - // 이미 삭제상태인 질문은 삭제 불가. - else if (questionNumber == 3) { - throw new BusinessLogicException(ExceptionCode.CANNOT_CHANGE_BOARD); + if(isCheckBoardOwner(authentication, findBoard.getMember())){ + int questionNumber = findBoard.getQuestionStatus().getQuestionNumber(); + if( questionNumber == 1) { + findBoard.setQuestionStatus(QUESTION_DELETED); + boardRepository.save(findBoard); + } + else if( questionNumber == 2) { + // 질문 삭제하면 질문 상태만 변경. + findBoard.setQuestionStatus(QUESTION_DELETED); + // comment 는 지워져야 함. + commentService.deleteComment(findBoard.getComment().getCommentId(), authentication); + boardRepository.save(findBoard); + } + // 이미 삭제상태인 질문은 삭제 불가. + else if (questionNumber == 3) { + throw new BusinessLogicException(ExceptionCode.CANNOT_DELETE_BOARD); + } + }else { + throw new BusinessLogicException(ExceptionCode.DIFFERENT_USER); } + } // board 에 memberId 가 있으면, 수정, 조회, 삭제가 가능하도록 함. // 회원인지 아닌지를 조회를 하는 것. private void verifyBoardMember (Board board) { - // + memberService.findVerifedMember(board.getMember().getMemberId()); } @@ -211,8 +229,35 @@ public Board findVerifiedBoard (long boardId) { return findBoard; } - private Board saveBoard (Board board) { - return boardRepository.save(board); + private boolean isCheckBoardOwnerAndAdmin(Authentication authentication, Member member) { + return member.getEmail().equals(authentication.getPrincipal()) + || authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMIN")); + } + + public boolean isCheckBoardOwner(Authentication authentication, Member member) { + return member.getEmail().equals(authentication.getPrincipal()); + } + + + + private void createdView (Authentication authentication, Board board) { + String email = (String) authentication.getPrincipal(); + Optional optionalMember = memberRepository.findByEmail(email); + Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND)); + Optional view = viewRepository.findByMemberAndBoard(findMember, board); + + if(view.isPresent()) { + view.orElseThrow(() -> new BusinessLogicException(ExceptionCode.VIEW_NOT_FOUND)); + } else { + View addView = new View(); + addView.setBoard(board); + addView.setMember(findMember); + board.setViewCount(board.getViewCount() + 1); + viewRepository.save(addView); + +// boardRepository.save(board); + } + } } diff --git a/src/main/java/com/springboot/comment/controller/CommentController.java b/src/main/java/com/springboot/comment/controller/CommentController.java index 5963599..2e51d83 100644 --- a/src/main/java/com/springboot/comment/controller/CommentController.java +++ b/src/main/java/com/springboot/comment/controller/CommentController.java @@ -16,6 +16,7 @@ import com.springboot.utils.UriCreator; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -24,10 +25,11 @@ import java.net.URI; @RestController -@RequestMapping("/v2/comments") +@RequestMapping("/v2/boards/{board-id}/comments") +// v2/{board-id}/comments/1 @Validated public class CommentController { - private final static String COMMENT_DEFAULT_URL = "/v2/comments"; + private final static String COMMENT_DEFAULT_URL = "/v2/boards/{board-id}/comments"; private final CommentService commentService; private final CommentMapper mapper; @@ -38,30 +40,33 @@ public CommentController(CommentService commentService, CommentMapper mapper) { } @PostMapping - public ResponseEntity postComment (@Valid @RequestBody CommentPostDto commentPostDto) { + public ResponseEntity postComment (@PathVariable("board-id") @Positive long boardId, + @Valid @RequestBody CommentPostDto commentPostDto, + Authentication authentication) { + commentPostDto.setBoardId(boardId); Comment comment = mapper.commentPostDtoToComment(commentPostDto); - Board board = new Board(); - board.setBoardId(commentPostDto.getBoardId()); - comment.setBoard(board); - - Comment createdComment = commentService.createComment(comment); + Comment createdComment = commentService.createComment(comment, authentication); URI location = UriCreator.createUri(COMMENT_DEFAULT_URL, createdComment.getCommentId()); return ResponseEntity.created(location).build(); } - @PatchMapping("{comment-id}") - public ResponseEntity patchComment (@PathVariable("comment-id") @Positive long commentId, - @Valid @RequestBody CommentPatchDto commentPatchDto) { + @PatchMapping("/{comment-id}") + public ResponseEntity patchComment (@PathVariable("board-id") @Positive long boardId, + @PathVariable("comment-id") @Positive long commentId, + @Valid @RequestBody CommentPatchDto commentPatchDto, + Authentication authentication) { commentPatchDto.setCommentId(commentId); - Comment comment = commentService.updateComment(mapper.commentPatchDtoToComment(commentPatchDto)); + commentPatchDto.setBoardId(boardId); + Comment comment = commentService.updateComment(mapper.commentPatchDtoToComment(commentPatchDto), authentication); CommentResponseDto commentResponseDto = mapper.commentToCommentResponseDto(comment); return new ResponseEntity<>( new SingleResponseDto<>(commentResponseDto), HttpStatus.OK ); } @GetMapping("{comment-id}") - public ResponseEntity getComment (@PathVariable("comment-id") @Positive long commentId) { + public ResponseEntity getComment (@PathVariable("board-id") @Positive long boardId, + @PathVariable("comment-id") @Positive long commentId) { Comment comment = commentService.findComment(commentId); CommentResponseDto commentResponseDto = mapper.commentToCommentResponseDto(comment); @@ -71,11 +76,10 @@ public ResponseEntity getComment (@PathVariable("comment-id") @Positive long com } @DeleteMapping("{comment-id}") - public ResponseEntity deleteComment (@PathVariable("comment-id") @Positive long commentId) { - commentService.deleteComment(commentId); + public ResponseEntity deleteComment (@PathVariable("board-id") @Positive long boardId, @PathVariable("comment-id") @Positive long commentId, Authentication authentication) { + + commentService.deleteComment(commentId, authentication); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } - -} - +} \ No newline at end of file diff --git a/src/main/java/com/springboot/comment/dto/CommentPatchDto.java b/src/main/java/com/springboot/comment/dto/CommentPatchDto.java index 1a1fd65..c3be54a 100644 --- a/src/main/java/com/springboot/comment/dto/CommentPatchDto.java +++ b/src/main/java/com/springboot/comment/dto/CommentPatchDto.java @@ -6,9 +6,14 @@ @Getter public class CommentPatchDto { private long commentId; + private long boardId; private String content; + public void setBoardId(long boardId) { + this.boardId = boardId; + } + public void setCommentId(long commentId) { this.commentId = commentId; } diff --git a/src/main/java/com/springboot/comment/dto/CommentPostDto.java b/src/main/java/com/springboot/comment/dto/CommentPostDto.java index 0f7e0d9..c5b9387 100644 --- a/src/main/java/com/springboot/comment/dto/CommentPostDto.java +++ b/src/main/java/com/springboot/comment/dto/CommentPostDto.java @@ -8,13 +8,12 @@ @Getter public class CommentPostDto { - @Positive private long boardId; - @Positive - private long memberId; - @NotBlank private String content; + public void setBoardId(long boardId) { + this.boardId = boardId; + } } diff --git a/src/main/java/com/springboot/comment/entity/Comment.java b/src/main/java/com/springboot/comment/entity/Comment.java index 303290c..66c9fb7 100644 --- a/src/main/java/com/springboot/comment/entity/Comment.java +++ b/src/main/java/com/springboot/comment/entity/Comment.java @@ -21,9 +21,15 @@ public class Comment extends Auditable { @Column(nullable = false) private String content; - @ManyToOne + @ManyToOne(cascade = CascadeType.MERGE) @JoinColumn(name = "MEMBER_ID") private Member member; + public void setMember(Member member) { + this.member = member; + if (!member.getComments().contains(this)) { + member.setComments(this); + } + } @OneToOne(cascade = CascadeType.MERGE) //persist : 처음에 만들때 씀. diff --git a/src/main/java/com/springboot/comment/mapper/CommentMapper.java b/src/main/java/com/springboot/comment/mapper/CommentMapper.java index 7518f43..bb00708 100644 --- a/src/main/java/com/springboot/comment/mapper/CommentMapper.java +++ b/src/main/java/com/springboot/comment/mapper/CommentMapper.java @@ -11,7 +11,6 @@ public interface CommentMapper { @Mapping(source = "boardId",target = "board.boardId") - @Mapping(source = "memberId", target = "member.memberId") Comment commentPostDtoToComment (CommentPostDto commentPostDto); Comment commentPatchDtoToComment (CommentPatchDto commentPatchDto); diff --git a/src/main/java/com/springboot/comment/service/CommentService.java b/src/main/java/com/springboot/comment/service/CommentService.java index d7f38aa..c1cb408 100644 --- a/src/main/java/com/springboot/comment/service/CommentService.java +++ b/src/main/java/com/springboot/comment/service/CommentService.java @@ -7,59 +7,80 @@ import com.springboot.comment.repository.CommentRepository; import com.springboot.exception.BusinessLogicException; import com.springboot.exception.ExceptionCode; +import com.springboot.member.entity.Member; import com.springboot.member.service.MemberService; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.stereotype.Service; import java.time.LocalDateTime; +import java.util.Map; import java.util.Optional; @Service public class CommentService { private final CommentRepository commentRepository; private final BoardRepository boardRepository; + private final MemberService memberService; - public CommentService(CommentRepository commentRepository, BoardRepository boardRepository) { + public CommentService(CommentRepository commentRepository, BoardRepository boardRepository, MemberService memberService) { this.commentRepository = commentRepository; this.boardRepository = boardRepository; + this.memberService = memberService; } - public Comment createComment (Comment comment) { - // 이미 등록되어 있는지 아닌지 확인 - verifyComment(comment.getBoard().getBoardId()); - Optional findBoard = boardRepository.findById(comment.getBoard().getBoardId()); - Board board = findBoard.orElseThrow(()->new BusinessLogicException(ExceptionCode.BOARD_NOT_FOUND)); - board.setQuestionStatus(Board.QuestionStatus.QUESTION_ANSWERED); - - // 답변 등록 시 등록 날짜 생성. (이미 엔티티에 초기화 되어 있음.) - // 질문이 비밀글이면 답변 비밀글.... 같이 가야 함. - // 비공개 일때만 - if (board.getVisibilityStatus().equals(Board.VisibilityStatus.SECRET)) { - comment.setVisibilityStatus(Comment.VisibilityStatus.SECRET); - } + public Comment createComment (Comment comment, Authentication authentication) { + Optional findBoard = boardRepository.findById(comment.getBoard().getBoardId()); + verifyComment(findBoard.get().getBoardId()); + Board board = findBoard.orElseThrow(()->new BusinessLogicException(ExceptionCode.BOARD_NOT_FOUND)); + board.setQuestionStatus(Board.QuestionStatus.QUESTION_ANSWERED); + // 객체 간의 entity 를 영속성을 넣을 때에는 무조건. set을 해서 상태를 넣어주어야 함. + String email = (String) authentication.getPrincipal(); + Member member = memberService.findVerifiedMember(email); + comment.setMember(member); + comment.setBoard(board); + + // 답변 등록 시 등록 날짜 생성. (이미 엔티티에 초기화 되어 있음.) + // 질문이 비밀글이면 답변 비밀글.... 같이 가야 함. + // 비공개 일때만 + if (board.getVisibilityStatus().equals(Board.VisibilityStatus.SECRET)) { + comment.setVisibilityStatus(Comment.VisibilityStatus.SECRET); + } + return commentRepository.save(comment); - return commentRepository.save(comment); } - public Comment updateComment (Comment comment) { - // 있는 코멘트 데리고 옴. + public Comment updateComment (Comment comment, Authentication authentication) { + // + String email = (String) authentication.getPrincipal(); + Member member = memberService.findVerifiedMember(email); Comment findComment = findVerifiedComment(comment.getCommentId()); - Optional.ofNullable(comment.getContent()) - .ifPresent(content -> findComment.setContent(content)); - // 답변 수정 시 수정 날짜 생성. - findComment.setModifiedAt(LocalDateTime.now()); + // admin 이고, comment 를 쓴 사람이면. + if( isCheckCommentOwner(authentication, member)) { + // 있는 코멘트 데리고 옴. +// verifyComment(comment.getBoard().getBoardId()); + Optional.ofNullable(comment.getContent()) + .ifPresent(content -> findComment.setContent(content)); + // 답변 수정 시 수정 날짜 생성. + findComment.setModifiedAt(LocalDateTime.now()); + + return commentRepository.save(findComment); - return commentRepository.save(findComment); + } else { + throw new BusinessLogicException(ExceptionCode.DIFFERENT_USER); + } } + public Comment findComment (long commentId) { Comment findComment = findVerifiedComment(commentId); return findComment; } - public void deleteComment (long commentId) { - // 삭제 시 테이블에서 Row 가 완전히 삭제될 수 있도록 함. + public void deleteComment (long commentId,Authentication authentication) { Comment findComment = findVerifiedComment(commentId); - commentRepository.delete(findComment); + // 삭제 시 테이블에서 Row 가 완전히 삭제될 수 있도록 함. + commentRepository.delete(findComment); } public void verifyComment (long boardId) { @@ -71,7 +92,7 @@ public void verifyComment (long boardId) { } } - // comment의 id 가 있는 Id인지 확인. + // comment의 id 가 있는 Id인지 확인. update 할 때 필요. public Comment findVerifiedComment (long commentId) { Optional optionalComment = commentRepository.findById(commentId); Comment findComment = optionalComment.orElseThrow( @@ -80,4 +101,16 @@ public Comment findVerifiedComment (long commentId) { return findComment; } + public boolean verifyAdmin (Authentication authentication) { + boolean isAdmin = authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMIN")); + if (!isAdmin) { + throw new BusinessLogicException(ExceptionCode.NO_AUTHORITY); + } + return isAdmin; + } + + public boolean isCheckCommentOwner (Authentication authentication, Member member) { + return member.getEmail().equals(authentication.getPrincipal()); + } + } diff --git a/src/main/java/com/springboot/config/SecurityConfiguration.java b/src/main/java/com/springboot/config/SecurityConfiguration.java index 6666c12..c86b06b 100644 --- a/src/main/java/com/springboot/config/SecurityConfiguration.java +++ b/src/main/java/com/springboot/config/SecurityConfiguration.java @@ -51,16 +51,26 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .and() .authorizeHttpRequests(authorize -> authorize // User 가 회원가입을 해서 로그인을 하면. - // boards post, patch, 한 건의 get, Delete // Get 의 경우에 모든 글은 다른 회원도 볼 수 있음. 근데, // 비밀글 상태인 질문은 질문을 등록한 회원(고객)과 관리자만 조회할 수 있다. // 비밀글이면 로그인 한 상태에서 MemberId 가 같은지 다른지 조건을 달아주어야 하나. - .antMatchers(HttpMethod.POST, "/*/boards").hasRole("USER") // (1) 추가 - .antMatchers(HttpMethod.PATCH, "/*/boards/**").hasRole("USER") // (2) 추가 - .antMatchers(HttpMethod.GET, "/*/boards").hasRole("ADMIN") // (3) 추가 - .antMatchers(HttpMethod.GET, "/*/boards/**").hasAnyRole("USER", "ADMIN") // (4) 추가 - .antMatchers(HttpMethod.DELETE, "/*/boards/**").hasRole("USER") // (5) 추가 + // 회원으로 등록한 회원만 해당 게시판 기능 이용. + .antMatchers(HttpMethod.POST, "/*/boards").hasRole("USER") + // 질문을 등록한 회원만 수정. + .antMatchers(HttpMethod.PATCH, "/*/boards/*").hasRole("USER") + .antMatchers(HttpMethod.GET, "/*/boards").hasAnyRole("USER", "ADMIN") + // 1건의 특정 질문은 질문을 등록한 회원과 관리지가 조회할 수 있음. + .antMatchers(HttpMethod.GET, "/*/boards/*").hasAnyRole("USER", "ADMIN") + // 회원만 삭제 가능. 질문을 등록한 회원만 삭제. + .antMatchers(HttpMethod.DELETE, "/*/boards/*").hasRole("USER") + // 답변 관리자만 등록 가능. 한 건만 등록 가능. + .antMatchers(HttpMethod.POST, "/*/boards/*/comments").hasRole("ADMIN") + // 답변을 등록한 관리자만 수정 가능. + .antMatchers(HttpMethod.PATCH, "/*/boards/*/comments/*").hasRole("ADMIN") + // 답변을 등록한 관리자만 삭제 가능. + .antMatchers(HttpMethod.DELETE, "/*/boards/*/comments/*").hasRole("ADMIN") + .anyRequest().permitAll()); return http.build(); } diff --git a/src/main/java/com/springboot/exception/ExceptionCode.java b/src/main/java/com/springboot/exception/ExceptionCode.java index d0a4cb0..3ba3893 100644 --- a/src/main/java/com/springboot/exception/ExceptionCode.java +++ b/src/main/java/com/springboot/exception/ExceptionCode.java @@ -8,14 +8,20 @@ public enum ExceptionCode { COMMENT_NOT_FOUND(404, "Comment not found"), COMMENT_EXISTS(409, "Comment exists"), VIEW_NOT_FOUND(404, "View not found"), - CANNOT_CHANGE_ORDER(403, "Order can not change"), + NO_AUTHORITY (403, "No Authority"), + DO_NOT_HAVE_POST_PERMISSION (403, "Do not have permission to create this post"), + DIFFERENT_USER (403, "Different User"), + DO_NOT_HAVE_GET_PERMISSION (403, "Do not have permission to look up this post"), + DO_NOT_HAVE_DELETE_PERMISSION (403, "Do not have permission to delete this post"), NOT_IMPLEMENTATION(501, "Not Implementation"), BOARD_NOT_FOUND(404, "Board not found"), CANNOT_CHANGE_BOARD(403, "Board can not change"), + CANNOT_DELETE_BOARD(403, "Board can not delete"), LIKE_EXISTS(404, "like exists"); // enum 만들 때. // 필요한 필드를 주입을 해서 // 생성자를 만들어 주어야 함. + @Getter private int status; diff --git a/src/main/java/com/springboot/member/entity/Member.java b/src/main/java/com/springboot/member/entity/Member.java index 69c3571..0cdda29 100644 --- a/src/main/java/com/springboot/member/entity/Member.java +++ b/src/main/java/com/springboot/member/entity/Member.java @@ -51,6 +51,15 @@ public void setBoards (Board board) { } } + @OneToMany (mappedBy = "member") + private List comments = new ArrayList<>(); + public void setComments (Comment comment) { + this.comments.add(comment); + if(comment.getMember() != this) { + comment.setMember(this); + } + } + @OneToMany (mappedBy = "member", cascade = CascadeType.ALL) private List likes = new ArrayList<>(); public void setLikes(Like like) { diff --git a/src/main/java/com/springboot/member/service/MemberService.java b/src/main/java/com/springboot/member/service/MemberService.java index ac990e3..58a0f28 100644 --- a/src/main/java/com/springboot/member/service/MemberService.java +++ b/src/main/java/com/springboot/member/service/MemberService.java @@ -92,12 +92,17 @@ public void deleteMember (long memberId) { memberRepository.save(findMember); } - private void verifyExistsEmail (String email) { + public void verifyExistsEmail (String email) { Optional member = memberRepository.findByEmail(email); if (member.isPresent()) { throw new BusinessLogicException(ExceptionCode.MEMBER_EXISTS); } } + public Member findVerifiedMember (String email) { + Optional member = memberRepository.findByEmail(email); + Member findMember = member.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND)); + return findMember; + } public Member findVerifedMember (long memberId) { Optional member = memberRepository.findById(memberId); From c773cffcd94fdef0305c341a9eac87ada03245aa Mon Sep 17 00:00:00 2001 From: skyla00 Date: Fri, 19 Jul 2024 17:52:17 +0900 Subject: [PATCH 5/5] =?UTF-8?q?like=20security=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/controller/BoardController.java | 11 +++++------ .../com/springboot/board/dto/BoardPostDto.java | 2 +- .../com/springboot/board/dto/LikePostDto.java | 3 --- .../java/com/springboot/board/entity/Board.java | 14 ++++++++++++++ .../java/com/springboot/board/entity/Like.java | 2 ++ .../springboot/board/mapper/BoardMapper.java | 4 ---- .../springboot/board/service/BoardService.java | 17 +++++++---------- .../com/springboot/comment/entity/Comment.java | 7 ++++++- .../comment/service/CommentService.java | 9 +++++---- .../config/SecurityConfiguration.java | 2 +- .../com/springboot/exception/ExceptionCode.java | 2 -- 11 files changed, 41 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/springboot/board/controller/BoardController.java b/src/main/java/com/springboot/board/controller/BoardController.java index ae24deb..c25a2b1 100644 --- a/src/main/java/com/springboot/board/controller/BoardController.java +++ b/src/main/java/com/springboot/board/controller/BoardController.java @@ -54,12 +54,11 @@ public ResponseEntity postBoard (@Valid @RequestBody BoardPostDto boardPostDto, return ResponseEntity.created(location).build(); } - @PostMapping("/like/{board-id}") + @PostMapping("/{board-id}/like") public ResponseEntity postLike (@PathVariable("board-id") @Positive long boardId, - @Valid @RequestBody LikePostDto likePostDto) { - likePostDto.setBoardId(boardId); - Like like = mapper.likePostDtoToLike(likePostDto); - boardService.createLike(like); + Authentication authentication) { + + boardService.createLike(boardId, authentication); return new ResponseEntity(HttpStatus.OK); } @@ -68,7 +67,7 @@ public ResponseEntity postLike (@PathVariable("board-id") @Positive long boardId @PatchMapping("/{board-id}") public ResponseEntity patchBoard (@PathVariable("board-id") @Positive long boardId, @Valid @RequestBody BoardPatchDto boardPatchDto, - Authentication authentication) { + Authentication authentication) { boardPatchDto.setBoardId(boardId); // 질문을 등록한 회원만 수정. Board board = boardService.updateBoard(mapper.boardPatchDtoTOBoard(boardPatchDto), authentication); diff --git a/src/main/java/com/springboot/board/dto/BoardPostDto.java b/src/main/java/com/springboot/board/dto/BoardPostDto.java index ffb2be3..2836d4a 100644 --- a/src/main/java/com/springboot/board/dto/BoardPostDto.java +++ b/src/main/java/com/springboot/board/dto/BoardPostDto.java @@ -17,6 +17,6 @@ public class BoardPostDto { @NotBlank private String content; - private Board.VisibilityStatus visibilityStatus; + private Board.VisibilityStatus visibilityStatus = Board.VisibilityStatus.PUBLIC; } diff --git a/src/main/java/com/springboot/board/dto/LikePostDto.java b/src/main/java/com/springboot/board/dto/LikePostDto.java index 82f4fbd..344a251 100644 --- a/src/main/java/com/springboot/board/dto/LikePostDto.java +++ b/src/main/java/com/springboot/board/dto/LikePostDto.java @@ -9,7 +9,4 @@ @Setter public class LikePostDto { - private Long memberId; - private Long boardId; - } diff --git a/src/main/java/com/springboot/board/entity/Board.java b/src/main/java/com/springboot/board/entity/Board.java index cd395e5..47a1528 100644 --- a/src/main/java/com/springboot/board/entity/Board.java +++ b/src/main/java/com/springboot/board/entity/Board.java @@ -49,6 +49,12 @@ public void setComment(Comment comment) { comment.setBoard(this); } } + public void deleteComment (Comment comment) { + this.comment = null; + if (comment.getBoard() == this) { + comment.deleteBoard(this); + } + } @OneToOne(cascade = CascadeType.ALL) private Like like; @@ -113,4 +119,12 @@ public enum VisibilityStatus { } } + public void increasedCount(){ + this.likeCount++; + } + + public void decreaseCount(){ + this.likeCount--; + } + } \ No newline at end of file diff --git a/src/main/java/com/springboot/board/entity/Like.java b/src/main/java/com/springboot/board/entity/Like.java index 9070e8e..e084351 100644 --- a/src/main/java/com/springboot/board/entity/Like.java +++ b/src/main/java/com/springboot/board/entity/Like.java @@ -35,6 +35,7 @@ public void deleteBoard (Board board) { board.deleteLike(this); } } + @ManyToOne(cascade = CascadeType.ALL) @JoinColumn(name = "MEMBER_ID") private Member member; @@ -44,6 +45,7 @@ public void setMember (Member member) { member.setLikes(this); } } + public void deleteMember (Member member) { this.member = null; if(member.getLikes().contains(this)) { diff --git a/src/main/java/com/springboot/board/mapper/BoardMapper.java b/src/main/java/com/springboot/board/mapper/BoardMapper.java index a318579..695fa9e 100644 --- a/src/main/java/com/springboot/board/mapper/BoardMapper.java +++ b/src/main/java/com/springboot/board/mapper/BoardMapper.java @@ -20,10 +20,6 @@ public interface BoardMapper { BoardResponseDto boardToBoardResponseDto (Board board); List boardsToBoardResponseDtos (List boards); - @Mapping(source = "memberId", target = "member.memberId") - @Mapping(source = "boardId", target = "board.boardId") - Like likePostDtoToLike (LikePostDto likePostDto); - @Mapping(source = "memberId", target = "member.memberId") Board BoardGetDtoToBoard (BoardGetDto viewGetDto); diff --git a/src/main/java/com/springboot/board/service/BoardService.java b/src/main/java/com/springboot/board/service/BoardService.java index bfa98d8..55c98bc 100644 --- a/src/main/java/com/springboot/board/service/BoardService.java +++ b/src/main/java/com/springboot/board/service/BoardService.java @@ -60,16 +60,17 @@ public Board createBoard (Board board, Authentication authentication) { return boardRepository.save(board); } } - public void createLike (Like like) { + public void createLike (long boardId, Authentication authentication) { // 어떤 게 null 값이 나왔을 때 어떤 exception 코드를 전해야 하는가? // board 에 있는 like 를 확인해야 해서 그런가? // 받아온 like 에 board 를 가지고 옴. // jpa 가 무조건. - Optional board = boardRepository.findById(like.getBoard().getBoardId()); + Optional board = boardRepository.findById(boardId); Board findBoard = board.orElseThrow(() -> new BusinessLogicException(ExceptionCode.BOARD_NOT_FOUND)); - Optional member = memberRepository.findById(like.getMember().getMemberId()); + Optional member = memberRepository.findByEmail((String) authentication.getPrincipal()); Member findMember = member.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND)); + Optional findLike = likeRepository.findByMemberAndBoard(findMember, findBoard); if( findLike.isPresent()) { @@ -77,17 +78,15 @@ public void createLike (Like like) { Like deleteLike = findLike.orElseThrow (() -> new BusinessLogicException(ExceptionCode.LIKE_EXISTS)); findBoard.deleteLike(deleteLike); findMember.deleteLikes(deleteLike); - findBoard.setLikeCount(findBoard.getLikeCount()-1); + findBoard.decreaseCount(); boardRepository.save(findBoard); likeRepository.delete(deleteLike); } else { Like addlike= new Like(); addlike.setBoard(findBoard); addlike.setMember(findMember); - findBoard.setLikeCount(findBoard.getLikeCount() + 1); + findBoard.increasedCount(); likeRepository.save(addlike); - -// boardRepository.save(findBoard); } } @@ -201,7 +200,7 @@ else if( questionNumber == 2) { // 질문 삭제하면 질문 상태만 변경. findBoard.setQuestionStatus(QUESTION_DELETED); // comment 는 지워져야 함. - commentService.deleteComment(findBoard.getComment().getCommentId(), authentication); + findBoard.deleteComment(findBoard.getComment()); boardRepository.save(findBoard); } // 이미 삭제상태인 질문은 삭제 불가. @@ -254,8 +253,6 @@ private void createdView (Authentication authentication, Board board) { addView.setMember(findMember); board.setViewCount(board.getViewCount() + 1); viewRepository.save(addView); - -// boardRepository.save(board); } } diff --git a/src/main/java/com/springboot/comment/entity/Comment.java b/src/main/java/com/springboot/comment/entity/Comment.java index 66c9fb7..de0a96a 100644 --- a/src/main/java/com/springboot/comment/entity/Comment.java +++ b/src/main/java/com/springboot/comment/entity/Comment.java @@ -35,13 +35,18 @@ public void setMember(Member member) { //persist : 처음에 만들때 씀. @JoinColumn(name = "BOARD_ID") private Board board; - public void setBoard (Board board) { this.board = board; if(board.getComment() != this) { board.setComment(this); } } + public void deleteBoard(Board board) { + this.board = null; + if(board.getComment() == this) { + board.deleteComment(this); + } + } @Enumerated(value = EnumType.STRING) @Column(nullable = false) diff --git a/src/main/java/com/springboot/comment/service/CommentService.java b/src/main/java/com/springboot/comment/service/CommentService.java index c1cb408..5f0a2f8 100644 --- a/src/main/java/com/springboot/comment/service/CommentService.java +++ b/src/main/java/com/springboot/comment/service/CommentService.java @@ -54,9 +54,10 @@ public Comment updateComment (Comment comment, Authentication authentication) { // String email = (String) authentication.getPrincipal(); Member member = memberService.findVerifiedMember(email); + comment.setMember(member); Comment findComment = findVerifiedComment(comment.getCommentId()); - // admin 이고, comment 를 쓴 사람이면. - if( isCheckCommentOwner(authentication, member)) { + // comment 를 쓴 사람이면. + if( isCheckCommentOwner(authentication, findComment)) { // 있는 코멘트 데리고 옴. // verifyComment(comment.getBoard().getBoardId()); Optional.ofNullable(comment.getContent()) @@ -109,8 +110,8 @@ public boolean verifyAdmin (Authentication authentication) { return isAdmin; } - public boolean isCheckCommentOwner (Authentication authentication, Member member) { - return member.getEmail().equals(authentication.getPrincipal()); + public boolean isCheckCommentOwner (Authentication authentication, Comment comment) { + return comment.getMember().getEmail().equals(authentication.getPrincipal()); } } diff --git a/src/main/java/com/springboot/config/SecurityConfiguration.java b/src/main/java/com/springboot/config/SecurityConfiguration.java index c86b06b..a12e9ac 100644 --- a/src/main/java/com/springboot/config/SecurityConfiguration.java +++ b/src/main/java/com/springboot/config/SecurityConfiguration.java @@ -58,7 +58,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 회원으로 등록한 회원만 해당 게시판 기능 이용. .antMatchers(HttpMethod.POST, "/*/boards").hasRole("USER") // 질문을 등록한 회원만 수정. - .antMatchers(HttpMethod.PATCH, "/*/boards/*").hasRole("USER") + .antMatchers(HttpMethod.PATCH, "/*/boards/**").hasRole("USER") .antMatchers(HttpMethod.GET, "/*/boards").hasAnyRole("USER", "ADMIN") // 1건의 특정 질문은 질문을 등록한 회원과 관리지가 조회할 수 있음. .antMatchers(HttpMethod.GET, "/*/boards/*").hasAnyRole("USER", "ADMIN") diff --git a/src/main/java/com/springboot/exception/ExceptionCode.java b/src/main/java/com/springboot/exception/ExceptionCode.java index 3ba3893..17ab8d5 100644 --- a/src/main/java/com/springboot/exception/ExceptionCode.java +++ b/src/main/java/com/springboot/exception/ExceptionCode.java @@ -27,8 +27,6 @@ public enum ExceptionCode { @Getter private String message; - // status 가 코드 임. - // ExceptionCode(int code, String message) { this.status = code; this.message = message;