From 3e4b35d52a802ec1485fa93b6a58eaa18d63e995 Mon Sep 17 00:00:00 2001 From: YoungJunRoh Date: Mon, 15 Jul 2024 10:03:57 +0900 Subject: [PATCH 1/3] "featured changed" --- build.gradle | 25 ++-- .../springboot/SpringStartApplication.java | 4 + .../advice/GlobalExceptionAdvice.java | 89 ++++++++++++++ .../answer/controller/AnswerController.java | 100 +++++++++++++++ .../springboot/answer/dto/AnswerPatchDto.java | 17 +++ .../springboot/answer/dto/AnswerPostDto.java | 16 +++ .../answer/dto/AnswerResponseDto.java | 30 +++++ .../com/springboot/answer/entity/Answer.java | 46 +++++++ .../answer/mapper/AnswerMapper.java | 27 ++++ .../answer/repository/AnswerRepository.java | 7 ++ .../answer/service/AnswerService.java | 105 ++++++++++++++++ .../com/springboot/auth/dto/LoginDto.java | 9 ++ .../auth/filter/JwtAuthenticationFilter.java | 85 +++++++++++++ .../auth/filter/JwtVerificationFilter.java | 66 ++++++++++ .../handler/MemberAccessDeniedHandler.java | 23 ++++ .../MemberAuthenticationEntryPoint.java | 33 +++++ .../MemberAuthenticationFailureHandler.java | 36 ++++++ .../MemberAuthenticationSuccessHandler.java | 23 ++++ .../com/springboot/auth/jwt/JwtTokenizer.java | 92 ++++++++++++++ .../userdetails/MemberDetailsService.java | 78 ++++++++++++ .../springboot/auth/utils/ErrorResponder.java | 22 ++++ .../auth/utils/JwtAuthorityUtils.java | 44 +++++++ .../board/controller/BoardController.java | 97 +++++++++++++++ .../springboot/board/dto/BoardPatchDto.java | 15 +++ .../springboot/board/dto/BoardPostDto.java | 18 +++ .../board/dto/BoardResponseDto.java | 34 +++++ .../com/springboot/board/entity/Board.java | 86 +++++++++++++ .../springboot/board/mapper/BoardMapper.java | 57 +++++++++ .../board/repository/BoardRepository.java | 7 ++ .../board/service/BoardService.java | 109 ++++++++++++++++ .../config/SecurityConfiguration.java | 103 ++++++++++++++++ .../com/springboot/dto/MultiResponseDto.java | 18 +++ .../java/com/springboot/dto/PageInfo.java | 14 +++ .../com/springboot/dto/SingleResponseDto.java | 11 ++ .../exception/BusinessLogicException.java | 13 ++ .../springboot/exception/ExceptionCode.java | 26 ++++ .../helper/email/EmailConfiguration.java | 12 ++ .../helper/email/EmailSendable.java | 8 ++ .../springboot/helper/email/EmailSender.java | 18 +++ .../helper/email/MockEmailSendable.java | 8 ++ .../email/MockExceptionEmailSendable.java | 11 ++ .../helper/email/SimpleEmailSendable.java | 8 ++ .../helper/email/TemplateEmailSendable.java | 8 ++ .../MemberRegistrationApplicationEvent.java | 16 +++ .../MemberRegistrationEventListener.java | 40 ++++++ .../like/controller/LikeController.java | 51 ++++++++ .../com/springboot/like/dto/LikePostDto.java | 14 +++ .../java/com/springboot/like/entity/Like.java | 40 ++++++ .../springboot/like/mapper/LikeMapper.java | 18 +++ .../like/repository/LikeRepository.java | 7 ++ .../springboot/like/service/LikeService.java | 51 ++++++++ .../com/springboot/member/dto/MemberDto.java | 66 ++++++++++ .../member/mapper/MemberMapper.java | 17 +++ .../member/repository/MemberRepository.java | 10 ++ .../member/service/MemberService.java | 116 ++++++++++++++++++ .../springboot/response/ErrorResponse.java | 102 +++++++++++++++ .../java/com/springboot/utils/UriCreator.java | 15 +++ .../com/springboot/validator/NotSpace.java | 17 +++ .../validator/NotSpaceValidator.java | 19 +++ src/main/resources/application.yml | 23 ++++ 60 files changed, 2269 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/springboot/advice/GlobalExceptionAdvice.java create mode 100644 src/main/java/com/springboot/answer/controller/AnswerController.java create mode 100644 src/main/java/com/springboot/answer/dto/AnswerPatchDto.java create mode 100644 src/main/java/com/springboot/answer/dto/AnswerPostDto.java create mode 100644 src/main/java/com/springboot/answer/dto/AnswerResponseDto.java create mode 100644 src/main/java/com/springboot/answer/entity/Answer.java create mode 100644 src/main/java/com/springboot/answer/mapper/AnswerMapper.java create mode 100644 src/main/java/com/springboot/answer/repository/AnswerRepository.java create mode 100644 src/main/java/com/springboot/answer/service/AnswerService.java 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/MemberAccessDeniedHandler.java create mode 100644 src/main/java/com/springboot/auth/handler/MemberAuthenticationEntryPoint.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/ErrorResponder.java create mode 100644 src/main/java/com/springboot/auth/utils/JwtAuthorityUtils.java create mode 100644 src/main/java/com/springboot/board/controller/BoardController.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/entity/Board.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/service/BoardService.java create mode 100644 src/main/java/com/springboot/config/SecurityConfiguration.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/helper/email/EmailConfiguration.java create mode 100644 src/main/java/com/springboot/helper/email/EmailSendable.java create mode 100644 src/main/java/com/springboot/helper/email/EmailSender.java create mode 100644 src/main/java/com/springboot/helper/email/MockEmailSendable.java create mode 100644 src/main/java/com/springboot/helper/email/MockExceptionEmailSendable.java create mode 100644 src/main/java/com/springboot/helper/email/SimpleEmailSendable.java create mode 100644 src/main/java/com/springboot/helper/email/TemplateEmailSendable.java create mode 100644 src/main/java/com/springboot/helper/event/MemberRegistrationApplicationEvent.java create mode 100644 src/main/java/com/springboot/helper/event/MemberRegistrationEventListener.java create mode 100644 src/main/java/com/springboot/like/controller/LikeController.java create mode 100644 src/main/java/com/springboot/like/dto/LikePostDto.java create mode 100644 src/main/java/com/springboot/like/entity/Like.java create mode 100644 src/main/java/com/springboot/like/mapper/LikeMapper.java create mode 100644 src/main/java/com/springboot/like/repository/LikeRepository.java create mode 100644 src/main/java/com/springboot/like/service/LikeService.java create mode 100644 src/main/java/com/springboot/member/dto/MemberDto.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/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/build.gradle b/build.gradle index 6afc82e..c5be42b 100644 --- a/build.gradle +++ b/build.gradle @@ -4,11 +4,9 @@ plugins { id 'java' } -javadoc.options.encoding = 'UTF-8' - -tasks.withType(JavaCompile) { - options.encoding = 'UTF-8' -} +group = 'com.springboot' +version = '0.0.1-SNAPSHOT' +sourceCompatibility = '11' configurations { compileOnly { @@ -28,12 +26,17 @@ dependencies { runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' 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.mapstruct:mapstruct:1.4.2.Final' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation 'com.google.code.gson:gson' + // (2) JWT 기능을 위한 jjwt 라이브러리 + 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') { - source = sourceSets.main.allJava - options.memberLevel = JavadocMemberLevel.PRIVATE - destinationDir = file("build/docs/javadoc") +tasks.named('test') { + useJUnitPlatform() } diff --git a/src/main/java/com/springboot/SpringStartApplication.java b/src/main/java/com/springboot/SpringStartApplication.java index 0094952..13c59f0 100644 --- a/src/main/java/com/springboot/SpringStartApplication.java +++ b/src/main/java/com/springboot/SpringStartApplication.java @@ -1,11 +1,15 @@ package com.springboot; + import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class SpringStartApplication { public static void main(String[] args) { SpringApplication.run(SpringStartApplication.class, args); } + } 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..bdae572 --- /dev/null +++ b/src/main/java/com/springboot/advice/GlobalExceptionAdvice.java @@ -0,0 +1,89 @@ +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/answer/controller/AnswerController.java b/src/main/java/com/springboot/answer/controller/AnswerController.java new file mode 100644 index 0000000..fd6e6b9 --- /dev/null +++ b/src/main/java/com/springboot/answer/controller/AnswerController.java @@ -0,0 +1,100 @@ +package com.springboot.answer.controller; + + +import com.springboot.answer.dto.AnswerPatchDto; +import com.springboot.answer.dto.AnswerPostDto; +import com.springboot.answer.entity.Answer; +import com.springboot.answer.mapper.AnswerMapper; +import com.springboot.answer.service.AnswerService; +import com.springboot.dto.MultiResponseDto; +import com.springboot.dto.SingleResponseDto; +import com.springboot.member.entity.Member; +import com.springboot.utils.UriCreator; +import lombok.extern.slf4j.Slf4j; +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.security.core.GrantedAuthority; +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.Collection; +import java.util.List; + +@RestController +@RequestMapping("/v11/answers") +@Validated +public class AnswerController { + private final static String ANSWER_DEFAULT_URL = "/v11/answers"; + private final AnswerService answerService; + private final AnswerMapper mapper; + + + public AnswerController(AnswerService answerService, AnswerMapper mapper) { + this.answerService = answerService; + this.mapper = mapper; + } + + @PostMapping + public ResponseEntity postAnswer(@Valid @RequestBody AnswerPostDto requestBody, + Authentication authentication){ + String email = (String) authentication.getPrincipal(); + + + Answer answer = mapper.answerPostDtoToAnswer(requestBody); + Answer createdAnswer = answerService.createAnswer(answer,email); + URI location = UriCreator.createUri(ANSWER_DEFAULT_URL, createdAnswer.getAnswerId()); + return ResponseEntity.created(location).build(); + } + + @PatchMapping("/{answer-id}") + public ResponseEntity patchAnswer(@PathVariable("answer-id") @Positive long answerId, + @Valid @RequestBody AnswerPatchDto requestBody, + Authentication authentication){ + requestBody.setAnswerId(answerId); + String email = (String) authentication.getPrincipal(); + + Answer answer = answerService.updateAnswer(mapper.answerPatchDtoToAnswer(requestBody),email); + + return new ResponseEntity( + new SingleResponseDto<>(mapper.answerToAnswerResponseDto(answer)),HttpStatus.OK + ); + } + + @GetMapping("/{answer-id}") + public ResponseEntity getAnswer(@PathVariable("answer-id") @Positive long answerId){ + Answer answer = answerService.findAnswer(answerId); + + return new ResponseEntity( + new SingleResponseDto<>(mapper.answerToAnswerResponseDto(answer)),HttpStatus.OK + ); + } + + @GetMapping + public ResponseEntity getAnswers(@Positive @RequestParam int page, + @Positive @RequestParam int size) { + Page pageAnswers = answerService.findAnswers(page - 1, size); + List answers = pageAnswers.getContent(); + return new ResponseEntity<>( + new MultiResponseDto<>(mapper.answersToAnswerResponseDtos(answers), + pageAnswers), + HttpStatus.OK); + } + + @DeleteMapping("/{answer-id}") + public ResponseEntity deleteAnswer(@PathVariable("answer-id") @Positive long answerId, + Authentication authentication){ + + String email = (String) authentication.getPrincipal(); + answerService.deleteAnswer(answerId, email); + + return new ResponseEntity(HttpStatus.NO_CONTENT); + } + + + +} diff --git a/src/main/java/com/springboot/answer/dto/AnswerPatchDto.java b/src/main/java/com/springboot/answer/dto/AnswerPatchDto.java new file mode 100644 index 0000000..dfea087 --- /dev/null +++ b/src/main/java/com/springboot/answer/dto/AnswerPatchDto.java @@ -0,0 +1,17 @@ +package com.springboot.answer.dto; + +import lombok.Getter; + +import javax.validation.constraints.Positive; + +@Getter +public class AnswerPatchDto { + private long answerId; + + private String content; + + public void setAnswerId(long answerId){ + this.answerId = answerId; + } + +} diff --git a/src/main/java/com/springboot/answer/dto/AnswerPostDto.java b/src/main/java/com/springboot/answer/dto/AnswerPostDto.java new file mode 100644 index 0000000..3ae26a9 --- /dev/null +++ b/src/main/java/com/springboot/answer/dto/AnswerPostDto.java @@ -0,0 +1,16 @@ +package com.springboot.answer.dto; + + +import lombok.Getter; + +import javax.validation.constraints.Positive; + +@Getter +public class AnswerPostDto { + + @Positive + private long boardId; + + private String content; + +} diff --git a/src/main/java/com/springboot/answer/dto/AnswerResponseDto.java b/src/main/java/com/springboot/answer/dto/AnswerResponseDto.java new file mode 100644 index 0000000..3fcb217 --- /dev/null +++ b/src/main/java/com/springboot/answer/dto/AnswerResponseDto.java @@ -0,0 +1,30 @@ +package com.springboot.answer.dto; + + +import com.springboot.board.entity.Board; +import com.springboot.member.entity.Member; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +public class AnswerResponseDto { + private long answerId; + + @Setter(AccessLevel.NONE) + private long memberId; + private long boardId; + private String content; + private LocalDateTime createdAt; + + public void setMember(Member member){ + this.memberId = member.getMemberId(); + } + public void setBoard(Board board){ + this.boardId = board.getBoardId(); + } + +} diff --git a/src/main/java/com/springboot/answer/entity/Answer.java b/src/main/java/com/springboot/answer/entity/Answer.java new file mode 100644 index 0000000..7b7ebf6 --- /dev/null +++ b/src/main/java/com/springboot/answer/entity/Answer.java @@ -0,0 +1,46 @@ +package com.springboot.answer.entity; + + +import com.springboot.board.entity.Board; +import com.springboot.member.entity.Member; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.*; +import java.time.LocalDateTime; + +@Getter +@Setter +@NoArgsConstructor +@Entity +public class Answer { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long answerId; + + @Column(nullable = false) + private String content = ""; + + @Column(nullable = false) + private LocalDateTime createdAt = LocalDateTime.now(); + + @Column(nullable = false, name = "LAST_MODIFIED_AT") + private LocalDateTime modifiedAt = LocalDateTime.now(); + + @OneToOne + @JoinColumn(name = "BOARD_ID") + private Board board; + + @ManyToOne + @JoinColumn(name = "MEMBER_ID") + private Member member; + + public void setBoard(Board board){ + this.board = board; + if(board.getAnswer() != this){ + board.setAnswer(this); + } + } + +} diff --git a/src/main/java/com/springboot/answer/mapper/AnswerMapper.java b/src/main/java/com/springboot/answer/mapper/AnswerMapper.java new file mode 100644 index 0000000..0c02b51 --- /dev/null +++ b/src/main/java/com/springboot/answer/mapper/AnswerMapper.java @@ -0,0 +1,27 @@ +package com.springboot.answer.mapper; + + +import com.springboot.answer.dto.AnswerPatchDto; +import com.springboot.answer.dto.AnswerPostDto; +import com.springboot.answer.dto.AnswerResponseDto; +import com.springboot.answer.entity.Answer; +import com.springboot.board.entity.Board; +import com.springboot.member.entity.Member; +import org.mapstruct.Mapper; + +import java.util.List; + +@Mapper(componentModel = "spring") +public interface AnswerMapper { + default Answer answerPostDtoToAnswer(AnswerPostDto answerPostDto){ + Answer answer = new Answer(); + Board board = new Board(); + board.setBoardId(answerPostDto.getBoardId()); + answer.setBoard(board); + answer.setContent(answerPostDto.getContent()); + return answer; + } + Answer answerPatchDtoToAnswer(AnswerPatchDto answerPatchDto); + AnswerResponseDto answerToAnswerResponseDto(Answer answer); + List answersToAnswerResponseDtos(List answers); +} diff --git a/src/main/java/com/springboot/answer/repository/AnswerRepository.java b/src/main/java/com/springboot/answer/repository/AnswerRepository.java new file mode 100644 index 0000000..f538022 --- /dev/null +++ b/src/main/java/com/springboot/answer/repository/AnswerRepository.java @@ -0,0 +1,7 @@ +package com.springboot.answer.repository; + +import com.springboot.answer.entity.Answer; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AnswerRepository extends JpaRepository { +} diff --git a/src/main/java/com/springboot/answer/service/AnswerService.java b/src/main/java/com/springboot/answer/service/AnswerService.java new file mode 100644 index 0000000..115b9ef --- /dev/null +++ b/src/main/java/com/springboot/answer/service/AnswerService.java @@ -0,0 +1,105 @@ +package com.springboot.answer.service; + + +import com.springboot.answer.entity.Answer; +import com.springboot.answer.repository.AnswerRepository; +import com.springboot.board.entity.Board; +import com.springboot.board.repository.BoardRepository; +import com.springboot.board.service.BoardService; +import com.springboot.exception.BusinessLogicException; +import com.springboot.exception.ExceptionCode; +import com.springboot.member.entity.Member; +import com.springboot.member.service.MemberService; +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.util.Optional; + +@Transactional +@Service +@Slf4j +public class AnswerService { + private final AnswerRepository answerRepository; + private final BoardService boardService; + private final MemberService memberService; + + public AnswerService(AnswerRepository answerRepository, BoardService boardService, MemberService memberService) { + this.answerRepository = answerRepository; + this.boardService = boardService; + this.memberService = memberService; + } + + public Answer createAnswer(Answer answer, String email){ + verifiedAnswer(answer); + + Member member = memberService.findVerifiedMember(email); + answer.setMember(member); + updateBoard(answer); + + Answer answer1 =answerRepository.save(answer); + Board board = boardService.findVerifiedBoard(answer.getBoard().getBoardId()); + board.setBoardStatus(Board.BoardStatus.QUESTION_REGISTERED); + boardService.updateBoard(board); + + return answerRepository.save(answer); + } + public Answer updateAnswer(Answer answer, String email){ + Answer findAnswer = findVerifiedAnswer(answer.getAnswerId()); + + if(!findAnswer.getMember().getEmail().equals(email)){ + throw new BusinessLogicException(ExceptionCode.NOT_YOUR_ANSWER); + } + + Optional.ofNullable(answer.getContent()) + .ifPresent(content -> findAnswer.setContent(content)); + + return answerRepository.save(findAnswer); + } + + public Answer findAnswer(long answerId){ + return findVerifiedAnswer(answerId); + } + + public Page findAnswers(int page, int size){ + return answerRepository.findAll(PageRequest.of(page,size, Sort.by("answerId").descending())); + } + public void deleteAnswer(long answerId, String email){ + Answer answer = findVerifiedAnswer(answerId); + Board findBoard = boardService.findVerifiedBoard(answer.getBoard().getBoardId()); + + Answer findAnswer = findVerifiedAnswer(answer.getAnswerId()); + + if(!findAnswer.getMember().getEmail().equals(email)){ + throw new BusinessLogicException(ExceptionCode.NOT_YOUR_ANSWER); + } + + findBoard.setAnswer(null); + boardService.updateBoard(findBoard); + answerRepository.delete(findVerifiedAnswer(answerId)); + } + + + + private Answer findVerifiedAnswer(long answerId){ + Optional optionalAnswer = answerRepository.findById(answerId); + Answer answer = optionalAnswer.orElseThrow(()-> + new BusinessLogicException(ExceptionCode.ANSWER_NOT_FOUND)); + + return answer; + } + + private void verifiedAnswer(Answer answer){ + boardService.findVerifiedBoard(answer.getBoard().getBoardId()); + } + private void updateBoard(Answer answer){ + Board board = boardService.findBoard(answer.getBoard().getBoardId()); + + board.setAnswer(answer); + boardService.updateBoard(board); + } + +} 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..b7dea86 --- /dev/null +++ b/src/main/java/com/springboot/auth/filter/JwtAuthenticationFilter.java @@ -0,0 +1,85 @@ +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 lombok.SneakyThrows; +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; + } + + @SneakyThrows + @Override + 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()); + + return authenticationManager.authenticate(authenticationToken); + + } + protected void successfulAuthentication(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain, + Authentication authentication) throws ServletException, IOException { + Member member = (Member) authentication.getPrincipal(); + String accessToken = delegateAccessToken(member); + String refreshToken = delegateRefreshToken(member); + response.setHeader("Authorization", "Bearer " + accessToken); + response.setHeader("Refresh", refreshToken); + this.getSuccessHandler().onAuthenticationSuccess(request,response,authentication); + + } + + + 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..cdac55b --- /dev/null +++ b/src/main/java/com/springboot/auth/filter/JwtVerificationFilter.java @@ -0,0 +1,66 @@ +package com.springboot.auth.filter; + +import com.springboot.auth.jwt.JwtTokenizer; +import com.springboot.auth.utils.JwtAuthorityUtils; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.security.SignatureException; +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; + +public class JwtVerificationFilter extends OncePerRequestFilter { + private final JwtTokenizer jwtTokenizer; + private final JwtAuthorityUtils authorityUtils; + + public JwtVerificationFilter(JwtTokenizer jwtTokenizer, JwtAuthorityUtils authorityUtils) { + this.jwtTokenizer = jwtTokenizer; + this.authorityUtils = authorityUtils; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + try { + Map claims = verifyJws(request); + setAuthenticationToContext(claims); + }catch (SignatureException se){ + request.setAttribute("exception", se); + }catch (ExpiredJwtException ee){ + request.setAttribute("exception", ee); + }catch(Exception e) { + request.setAttribute("exception", e); + } + filterChain.doFilter(request, response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String authorization = request.getHeader("Authorization"); + + return authorization == null || !authorization.startsWith("Bearer"); + } + + private Map verifyJws(HttpServletRequest request){ + String jws = request.getHeader("Authorization").replace("Bearer ", ""); // (3-1) + String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey()); // (3-2) + Map claims = jwtTokenizer.getClaims(jws, base64EncodedSecretKey).getBody(); + + return claims; + } + private void setAuthenticationToContext(Map claims){ + String username = (String) claims.get("username"); + List authorities = authorityUtils.createAuthorities((List)claims.get("roles")); + Authentication authentication = new UsernamePasswordAuthenticationToken( + username, null, authorities); + SecurityContextHolder.getContext().setAuthentication(authentication); + } +} diff --git a/src/main/java/com/springboot/auth/handler/MemberAccessDeniedHandler.java b/src/main/java/com/springboot/auth/handler/MemberAccessDeniedHandler.java new file mode 100644 index 0000000..723222b --- /dev/null +++ b/src/main/java/com/springboot/auth/handler/MemberAccessDeniedHandler.java @@ -0,0 +1,23 @@ +package com.springboot.auth.handler; + +import com.springboot.auth.utils.ErrorResponder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +@Component +public class MemberAccessDeniedHandler implements AccessDeniedHandler { + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { + ErrorResponder.sendErrorResponse(response, HttpStatus.FORBIDDEN); + log.warn("Forbidden error happened: {}", accessDeniedException.getMessage()); + } +} diff --git a/src/main/java/com/springboot/auth/handler/MemberAuthenticationEntryPoint.java b/src/main/java/com/springboot/auth/handler/MemberAuthenticationEntryPoint.java new file mode 100644 index 0000000..a901cba --- /dev/null +++ b/src/main/java/com/springboot/auth/handler/MemberAuthenticationEntryPoint.java @@ -0,0 +1,33 @@ +package com.springboot.auth.handler; + +import com.springboot.auth.utils.ErrorResponder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +@Component +public class MemberAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + Exception exception = (Exception) request.getAttribute("exception"); + ErrorResponder.sendErrorResponse(response, HttpStatus.UNAUTHORIZED); + + logExceptionMessage(authException, exception); + } + + + private void logExceptionMessage(AuthenticationException authException, + Exception e){ + String message = e != null ? e.getMessage() : authException.getMessage(); + log.warn("Unauthorized error : {}", message); + } + +} 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..0cd5255 --- /dev/null +++ b/src/main/java/com/springboot/auth/handler/MemberAuthenticationFailureHandler.java @@ -0,0 +1,36 @@ +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.error("Authenticated filed", 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..c37125b --- /dev/null +++ b/src/main/java/com/springboot/auth/handler/MemberAuthenticationSuccessHandler.java @@ -0,0 +1,23 @@ +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..e636b1c --- /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){ + return Encoders.BASE64.encode(secretKey.getBytes(StandardCharsets.UTF_8)); + } + public String generateAccessToken(Map claims, + String subject, + Date expiration, + String base64EncodedSecretKey){ + Key key = getKeyFromBase64EncodedKey(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 = getKeyFromBase64EncodedKey(base64EncodedSecretKey); + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(Calendar.getInstance().getTime()) + .setExpiration(expiration) + .signWith(key) + .compact(); + } + + private Key getKeyFromBase64EncodedKey(String base64EncodedSecretKey){ + byte[] keyBytes = Decoders.BASE64URL.decode(base64EncodedSecretKey); + Key key = Keys.hmacShaKeyFor(keyBytes); + return key; + } + public Date getTokenExpiration(int expirationMinutes){ + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MINUTE, expirationMinutes); + Date expiration = calendar.getTime(); + return expiration; + } + public Jws getClaims(String jws, String base64EncodedSecretKey){ + Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey); + Jws claims = Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(jws); + return claims; + } + public void verifySignature(String jws, String base64EncodedSecretKey){ + Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey); + Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(jws); + } + + + +} 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..88e1da5 --- /dev/null +++ b/src/main/java/com/springboot/auth/userdetails/MemberDetailsService.java @@ -0,0 +1,78 @@ +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 org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +@Component +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 this.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/ErrorResponder.java b/src/main/java/com/springboot/auth/utils/ErrorResponder.java new file mode 100644 index 0000000..6085f51 --- /dev/null +++ b/src/main/java/com/springboot/auth/utils/ErrorResponder.java @@ -0,0 +1,22 @@ +package com.springboot.auth.utils; + +import com.google.gson.Gson; +import com.springboot.response.ErrorResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class ErrorResponder { + public static void sendErrorResponse(HttpServletResponse response, + HttpStatus status) throws IOException { + Gson gson = new Gson(); + ErrorResponse errorResponse = ErrorResponse.of(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(status.value()); + response.getWriter().write(gson.toJson(errorResponse,ErrorResponse.class)); + + + } +} 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..8770bbb --- /dev/null +++ b/src/main/java/com/springboot/auth/utils/JwtAuthorityUtils.java @@ -0,0 +1,44 @@ +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 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(String email){ + if(email.equals(adminMailAddress)){ + return ADMIN_ROLES; + } + return USER_ROLES; + } + public ListcreateAuthorities(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 new file mode 100644 index 0000000..fc6dd78 --- /dev/null +++ b/src/main/java/com/springboot/board/controller/BoardController.java @@ -0,0 +1,97 @@ +package com.springboot.board.controller; + + +import com.springboot.answer.entity.Answer; +import com.springboot.board.dto.BoardPatchDto; +import com.springboot.board.dto.BoardPostDto; +import com.springboot.board.entity.Board; +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.utils.UriCreator; +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.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.context.SecurityContextHolder; +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 BoardMapper mapper; + + public BoardController(BoardService boardService, BoardMapper mapper) { + this.boardService = boardService; + this.mapper = mapper; + + } + + @PostMapping + public ResponseEntity postBoard(@Valid @RequestBody BoardPostDto boardPostDto, + Authentication authentication){ + String email = (String) authentication.getPrincipal(); + + Board board = boardService.createBoard(mapper.boardPostDtoToBoard(boardPostDto),email); + + URI location = UriCreator.createUri(BOARD_DEFAULT_URL, board.getBoardId()); + + return ResponseEntity.created(location).build(); + } + + @PatchMapping("/{board-id}") + public ResponseEntity patchBoard(@PathVariable("board-id") @Positive long boardId, + @Valid @RequestBody BoardPatchDto boardPatchDto, + Authentication authentication){ + boardPatchDto.setBoardId(boardId); + if (authentication == null) { + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } + + String email = (String) authentication.getPrincipal(); + + System.out.println("Email: " + email); + + Board board = + boardService.updateBoard(mapper.boardPatchDtoToBoard(boardPatchDto),email); + return new ResponseEntity( + new SingleResponseDto<>(mapper.boardToBoardResponseDto(board)), HttpStatus.OK); + } + + @GetMapping("/{board-id}") + public ResponseEntity patchBoard(@PathVariable("board-id") @Positive long boardId){ + Board board = boardService.findBoard(boardId); + + return new ResponseEntity(new SingleResponseDto<>(mapper.boardToBoardResponseDto(board)), HttpStatus.OK); + } + + @GetMapping + public ResponseEntity getBoards(@Positive @RequestParam int page, + @Positive @RequestParam int size){ + Page pageBoards = boardService.findBoards(page -1, size); + List boards = pageBoards.getContent(); + return new ResponseEntity( + new MultiResponseDto<>(mapper.boardsToBoardResponseDtos(boards),pageBoards), HttpStatus.OK + ); + } + + @DeleteMapping("/{board-id}") + public ResponseEntity cancleBoard(@PathVariable("board-id") @Positive long boardId, + Authentication authentication){ + String email = (String) authentication.getPrincipal(); + boardService.deleteBoard(boardId,email); + 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 new file mode 100644 index 0000000..2de945f --- /dev/null +++ b/src/main/java/com/springboot/board/dto/BoardPatchDto.java @@ -0,0 +1,15 @@ +package com.springboot.board.dto; + +import com.springboot.board.entity.Board; +import lombok.Getter; + +@Getter +public class BoardPatchDto { + private long boardId; + private String title; + private String content; + + 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..a2bc7ed --- /dev/null +++ b/src/main/java/com/springboot/board/dto/BoardPostDto.java @@ -0,0 +1,18 @@ +package com.springboot.board.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Positive; + +@Getter +@AllArgsConstructor +public class BoardPostDto { + + @NotBlank + private String title; + + @NotBlank + private String content; +} 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..98e0ddb --- /dev/null +++ b/src/main/java/com/springboot/board/dto/BoardResponseDto.java @@ -0,0 +1,34 @@ +package com.springboot.board.dto; + +import com.springboot.answer.dto.AnswerResponseDto; +import com.springboot.answer.entity.Answer; +import com.springboot.board.entity.Board; +import com.springboot.member.entity.Member; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + + +@Setter +@Getter +public class BoardResponseDto { + private long boardId; + + @Setter(AccessLevel.NONE) + private long memberId; + private Board.BoardStatus boardStatus; + private String title; + private String content; + private LocalDateTime createdAt; + + private AnswerResponseDto answerResponseDto; + + public void setMember(Member member){ + this.memberId = member.getMemberId(); + } + + +} 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..1eb4e74 --- /dev/null +++ b/src/main/java/com/springboot/board/entity/Board.java @@ -0,0 +1,86 @@ +package com.springboot.board.entity; + + +import com.springboot.answer.entity.Answer; +import com.springboot.like.entity.Like; +import com.springboot.member.entity.Member; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.*; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@NoArgsConstructor +@Getter +@Setter +@Entity(name = "BOARDS") +public class Board { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long boardId; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String content; + + @Enumerated(value = EnumType.STRING) + @Column(length = 20, nullable = false) + private BoardStatus boardStatus = BoardStatus.QUESTION_REGISTERED; + + @Column(nullable = false) + private LocalDateTime createdAt = LocalDateTime.now(); + + @Column(nullable = false, name = "LAST_MODIFIED_AT") + private LocalDateTime modifiedAt = LocalDateTime.now(); + + @ManyToOne + @JoinColumn(name = "MEMBER_ID") + private Member member; + + @OneToOne + @JoinColumn(name = "ANSWER_ID") + private Answer answer; + + @OneToMany(mappedBy = "board", cascade = CascadeType.ALL) + List likes = new ArrayList<>(); + + public void setMember(Member member) { + this.member = member; + } + + public void setAnswer(Answer answer){ + if(answer == null){ + this.answer =null; + return; + } + this.answer = answer; + if(answer.getBoard() != this){ + answer.setBoard(this); + } + } + + public enum BoardStatus{ + QUESTION_REGISTERED(1, "질문 등록 상태"), + QUESTION_ANSWERED(2, "답변 완료 상태"), + QUESTION_DELETED(3, "질문 삭제 상태"), + QUESTION_DEACTIVED(4, "질문 비활성화 상태"); + + @Getter + private int stepNumber; + + @Getter + private String stepDescription; + + BoardStatus(int stepNumber, String stepDescription) { + this.stepNumber = stepNumber; + this.stepDescription = stepDescription; + } + } + + +} 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..4c56987 --- /dev/null +++ b/src/main/java/com/springboot/board/mapper/BoardMapper.java @@ -0,0 +1,57 @@ +package com.springboot.board.mapper; + +import com.springboot.answer.dto.AnswerResponseDto; +import com.springboot.answer.entity.Answer; +import com.springboot.answer.mapper.AnswerMapper; +import com.springboot.board.dto.BoardPatchDto; +import com.springboot.board.dto.BoardPostDto; +import com.springboot.board.dto.BoardResponseDto; +import com.springboot.board.entity.Board; +import com.springboot.member.entity.Member; +import com.springboot.member.repository.MemberRepository; +import lombok.AccessLevel; +import lombok.Setter; +import org.mapstruct.Mapper; +import org.springframework.security.core.Authentication; + +import java.time.LocalDateTime; +import java.util.List; + +@Mapper(componentModel = "spring") +public interface BoardMapper { + + Board boardPatchDtoToBoard(BoardPatchDto boardPatchDto); + default Board boardPostDtoToBoard(BoardPostDto boardPostDto){ + Board board = new Board(); + + board.setTitle(boardPostDto.getTitle()); + board.setContent(boardPostDto.getContent()); + + return board; + } + default BoardResponseDto boardToBoardResponseDto(Board board){ + BoardResponseDto boardResponseDto = new BoardResponseDto(); + + boardResponseDto.setBoardId(board.getBoardId()); + boardResponseDto.setBoardStatus(board.getBoardStatus()); + boardResponseDto.setTitle(board.getTitle()); + boardResponseDto.setContent(board.getContent()); + boardResponseDto.setCreatedAt(board.getCreatedAt()); + boardResponseDto.setMember(board.getMember()); + + AnswerResponseDto answerResponseDto = new AnswerResponseDto(); + if(board.getAnswer() == null){ + answerResponseDto = null; + }else { + answerResponseDto.setAnswerId(board.getAnswer().getAnswerId()); + answerResponseDto.setMember(board.getMember()); + answerResponseDto.setContent(board.getContent()); + answerResponseDto.setCreatedAt(board.getCreatedAt()); + answerResponseDto.setBoardId(board.getBoardId()); + boardResponseDto.setAnswerResponseDto(answerResponseDto); + } + + return boardResponseDto; + } + List boardsToBoardResponseDtos(List boards); +} 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..a855ba0 --- /dev/null +++ b/src/main/java/com/springboot/board/repository/BoardRepository.java @@ -0,0 +1,7 @@ +package com.springboot.board.repository; + +import com.springboot.board.entity.Board; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BoardRepository extends JpaRepository { +} 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..eaddc22 --- /dev/null +++ b/src/main/java/com/springboot/board/service/BoardService.java @@ -0,0 +1,109 @@ +package com.springboot.board.service; + + +import com.springboot.board.entity.Board; +import com.springboot.board.repository.BoardRepository; +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.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.util.Objects; +import java.util.Optional; + +@Transactional +@Service +public class BoardService { + private final BoardRepository boardRepository; + private final MemberService memberService; + + + public BoardService(BoardRepository boardRepository, MemberService memberService) { + this.boardRepository = boardRepository; + this.memberService = memberService; + } + + public Board createBoard(Board board){ + verifyBoard(board); + + return boardRepository.save(board); + } + + public Board createBoard(Board board, String email){ + + Member member = memberService.findVerifiedMember(email); + + board.setMember(member); + + return boardRepository.save(board); + } + + public Board updateBoard(Board board){ + Board findBoard = findVerifiedBoard(board.getBoardId()); + + Optional.ofNullable(board.getTitle()) + .ifPresent(title -> findBoard.setTitle(title)); + Optional.ofNullable(board.getContent()) + .ifPresent(content -> findBoard.setContent(content)); + + return boardRepository.save(findBoard); + } + + public Board updateBoard(Board board, String email){ + Board findBoard = findVerifiedBoard(board.getBoardId()); + + if(!findBoard.getMember().getEmail().equals(email)){ + throw new BusinessLogicException(ExceptionCode.NOT_YOUR_BOARD); + } + + Optional.ofNullable(board.getTitle()) + .ifPresent(title -> findBoard.setTitle(title)); + Optional.ofNullable(board.getContent()) + .ifPresent(content -> findBoard.setContent(content)); + + return boardRepository.save(findBoard); + } + + public Board findBoard(long boardId){ + return findVerifiedBoard(boardId); + } + public Page findBoards(int page, int size){ + return boardRepository.findAll(PageRequest.of(page,size, Sort.by("boardId").descending())); + } + + public void deleteBoard(long boardId, String email){ + Board findBoard = findVerifiedBoard(boardId); + + if(!findBoard.getMember().getEmail().equals(email)){ + throw new BusinessLogicException(ExceptionCode.NOT_YOUR_BOARD); + } + + findBoard.setBoardStatus(Board.BoardStatus.QUESTION_DELETED); + } + + 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); + } + + private void verifyBoard(Board board){ + memberService.findVerifiedMember(board.getMember().getMemberId()); + } + + + + +} 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..a6713d7 --- /dev/null +++ b/src/main/java/com/springboot/config/SecurityConfiguration.java @@ -0,0 +1,103 @@ +package com.springboot.config; + + +import com.springboot.auth.filter.JwtAuthenticationFilter; +import com.springboot.auth.filter.JwtVerificationFilter; +import com.springboot.auth.handler.MemberAccessDeniedHandler; +import com.springboot.auth.handler.MemberAuthenticationEntryPoint; +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.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; + +@Configuration +public class SecurityConfiguration { + private final JwtTokenizer jwtTokenizer; + private final JwtAuthorityUtils authorityUtils; + + public SecurityConfiguration(JwtTokenizer jwtTokenizer, JwtAuthorityUtils authorityUtils) { + this.jwtTokenizer = jwtTokenizer; + this.authorityUtils = authorityUtils; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ + http + .headers().frameOptions().sameOrigin() + .and() + .csrf().disable() + .cors(Customizer.withDefaults()) + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .formLogin().disable() + .httpBasic().disable() + .exceptionHandling() + .authenticationEntryPoint(new MemberAuthenticationEntryPoint()) + .accessDeniedHandler(new MemberAccessDeniedHandler()) + .and() + .apply(new CustomFilterConfigurer()) + .and() + .authorizeHttpRequests(authorize -> authorize + .antMatchers(HttpMethod.POST, "/*/members").permitAll() + .antMatchers(HttpMethod.PATCH, "/*/members/**").hasRole("USER") + .antMatchers(HttpMethod.GET, "/*/members").hasRole("ADMIN") + .antMatchers(HttpMethod.GET, "/*/members/**").hasAnyRole("USER", "ADMIN") + .antMatchers(HttpMethod.DELETE, "/*/members/**").hasRole("USER") + .antMatchers(HttpMethod.POST, "/*/answers").hasRole("ADMIN") + .antMatchers(HttpMethod.PATCH, "/*/answers/**").hasRole("ADMIN") + .antMatchers(HttpMethod.DELETE, "/*/answers/**").hasRole("ADMIN") + .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) throws Exception{ + AuthenticationManager authenticationManager = + builder.getSharedObject(AuthenticationManager.class); + + JwtAuthenticationFilter jwtAuthenticationFilter = + new JwtAuthenticationFilter(authenticationManager, jwtTokenizer); + jwtAuthenticationFilter.setFilterProcessesUrl("/v11/auth/login"); + jwtAuthenticationFilter.setAuthenticationSuccessHandler(new MemberAuthenticationSuccessHandler()); + jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler()); + JwtVerificationFilter jwtVerificationFilter = + new JwtVerificationFilter(jwtTokenizer,authorityUtils); + builder.addFilter(jwtAuthenticationFilter) + .addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class); + } + } +} 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..cae8f9b --- /dev/null +++ b/src/main/java/com/springboot/dto/MultiResponseDto.java @@ -0,0 +1,18 @@ +package com.springboot.dto; + +import lombok.Getter; +import org.springframework.data.domain.Page; + +import java.util.List; + +@Getter +public class MultiResponseDto { + private List data; + private PageInfo pageInfo; + + public MultiResponseDto(List data, Page page){ + this.data = data; + this.pageInfo = new PageInfo(page.getNumber() + 1, + page.getSize(), page.getTotalElements(), page.getTotalPages()); + } +} 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..7517803 --- /dev/null +++ b/src/main/java/com/springboot/dto/PageInfo.java @@ -0,0 +1,14 @@ +package com.springboot.dto; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class PageInfo { + private int page; + private int size; + private long totalElements; + private int totalPages; +} 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..e5f3d74 --- /dev/null +++ b/src/main/java/com/springboot/dto/SingleResponseDto.java @@ -0,0 +1,11 @@ +package com.springboot.dto; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +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..09de085 --- /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..f2f6100 --- /dev/null +++ b/src/main/java/com/springboot/exception/ExceptionCode.java @@ -0,0 +1,26 @@ +package com.springboot.exception; + +import lombok.Getter; + +public enum ExceptionCode { + MEMBER_NOT_FOUND(404, "Member not found"), + MEMBER_EXISTS(409, "Member exists"), + NOT_IMPLEMENTATION(501, "Not Implementation"), + INVALID_MEMBER_STATUS(400, "Invalid member status"), + BOARD_NOT_FOUND(404, "Board not found"), + ANSWER_NOT_FOUND(404, "Answer not found"), + NOT_YOUR_BOARD(409, "NOT_YOUR_BOARD"), + NOT_YOUR_ANSWER(409, "NOT_YOUR_ANSWER"), + LIKE_NOT_FOUND(404,"LIKE_NOT_FOUND"); + + @Getter + private int status; + + @Getter + private String message; + + ExceptionCode(int code, String message) { + this.status = code; + this.message = message; + } +} diff --git a/src/main/java/com/springboot/helper/email/EmailConfiguration.java b/src/main/java/com/springboot/helper/email/EmailConfiguration.java new file mode 100644 index 0000000..dfe1ba4 --- /dev/null +++ b/src/main/java/com/springboot/helper/email/EmailConfiguration.java @@ -0,0 +1,12 @@ +package com.springboot.helper.email; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class EmailConfiguration { + @Bean + public EmailSendable emailSendable() { + return new MockEmailSendable(); + } +} diff --git a/src/main/java/com/springboot/helper/email/EmailSendable.java b/src/main/java/com/springboot/helper/email/EmailSendable.java new file mode 100644 index 0000000..0102850 --- /dev/null +++ b/src/main/java/com/springboot/helper/email/EmailSendable.java @@ -0,0 +1,8 @@ +package com.springboot.helper.email; + +import org.springframework.stereotype.Component; + +@Component +public interface EmailSendable { + void send(String message) throws InterruptedException; +} diff --git a/src/main/java/com/springboot/helper/email/EmailSender.java b/src/main/java/com/springboot/helper/email/EmailSender.java new file mode 100644 index 0000000..67565ae --- /dev/null +++ b/src/main/java/com/springboot/helper/email/EmailSender.java @@ -0,0 +1,18 @@ +package com.springboot.helper.email; + +import org.springframework.mail.MailSendException; +import org.springframework.stereotype.Service; + +@Service +public class EmailSender { + private final EmailSendable emailSendable; + + public EmailSender(EmailSendable emailSendable) { + this.emailSendable = emailSendable; + } + + public void sendEmail(String message) throws MailSendException, + InterruptedException { + emailSendable.send(message); + } +} diff --git a/src/main/java/com/springboot/helper/email/MockEmailSendable.java b/src/main/java/com/springboot/helper/email/MockEmailSendable.java new file mode 100644 index 0000000..5ce64e3 --- /dev/null +++ b/src/main/java/com/springboot/helper/email/MockEmailSendable.java @@ -0,0 +1,8 @@ +package com.springboot.helper.email; + +public class MockEmailSendable implements EmailSendable { + @Override + public void send(String message) { + System.out.println("sent mock email!"); + } +} diff --git a/src/main/java/com/springboot/helper/email/MockExceptionEmailSendable.java b/src/main/java/com/springboot/helper/email/MockExceptionEmailSendable.java new file mode 100644 index 0000000..95cc3b5 --- /dev/null +++ b/src/main/java/com/springboot/helper/email/MockExceptionEmailSendable.java @@ -0,0 +1,11 @@ +package com.springboot.helper.email; + +import org.springframework.mail.MailSendException; + +public class MockExceptionEmailSendable implements EmailSendable { + @Override + public void send(String message) throws InterruptedException { + Thread.sleep(5000L); + throw new MailSendException("error while send email"); + } +} diff --git a/src/main/java/com/springboot/helper/email/SimpleEmailSendable.java b/src/main/java/com/springboot/helper/email/SimpleEmailSendable.java new file mode 100644 index 0000000..14c92f3 --- /dev/null +++ b/src/main/java/com/springboot/helper/email/SimpleEmailSendable.java @@ -0,0 +1,8 @@ +package com.springboot.helper.email; + +public class SimpleEmailSendable implements EmailSendable { + @Override + public void send(String message) { + // TODO 간단한 문자열 형태의 이메일 구현에 대한 로직을 구성할 수 있습니다. + } +} diff --git a/src/main/java/com/springboot/helper/email/TemplateEmailSendable.java b/src/main/java/com/springboot/helper/email/TemplateEmailSendable.java new file mode 100644 index 0000000..894cf21 --- /dev/null +++ b/src/main/java/com/springboot/helper/email/TemplateEmailSendable.java @@ -0,0 +1,8 @@ +package com.springboot.helper.email; + +public class TemplateEmailSendable implements EmailSendable { + @Override + public void send(String message) { + // TODO 템플릿을 사용한 이메일을 보낼 수 있습니다. + } +} diff --git a/src/main/java/com/springboot/helper/event/MemberRegistrationApplicationEvent.java b/src/main/java/com/springboot/helper/event/MemberRegistrationApplicationEvent.java new file mode 100644 index 0000000..cb49062 --- /dev/null +++ b/src/main/java/com/springboot/helper/event/MemberRegistrationApplicationEvent.java @@ -0,0 +1,16 @@ +package com.springboot.helper.event; + +import com.springboot.member.entity.Member; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class MemberRegistrationApplicationEvent extends ApplicationEvent { + private Member member; + public MemberRegistrationApplicationEvent(Object source, Member member){ + super(source); + this.member = member; + } + + +} diff --git a/src/main/java/com/springboot/helper/event/MemberRegistrationEventListener.java b/src/main/java/com/springboot/helper/event/MemberRegistrationEventListener.java new file mode 100644 index 0000000..49dcedb --- /dev/null +++ b/src/main/java/com/springboot/helper/event/MemberRegistrationEventListener.java @@ -0,0 +1,40 @@ +package com.springboot.helper.event; + +import com.springboot.helper.email.EmailSender; +import com.springboot.member.entity.Member; +import com.springboot.member.service.MemberService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; +import org.springframework.mail.MailSendException; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.stereotype.Component; + + +@EnableAsync +@Configuration +@Component +@Slf4j +public class MemberRegistrationEventListener { + private final EmailSender emailSender; + private final MemberService memberService; + + public MemberRegistrationEventListener(EmailSender emailSender, MemberService memberService) { + this.emailSender = emailSender; + this.memberService = memberService; + } + @Async + @EventListener + public void listen(MemberRegistrationApplicationEvent event) throws Exception{ + try { + String message = "any email message"; + emailSender.sendEmail(message); + }catch (MailSendException e){ + e.printStackTrace(); + log.error("MailSendException: rollback for Member Registration:"); + Member member = event.getMember(); + memberService.deleteMember(member.getMemberId()); + } + } +} diff --git a/src/main/java/com/springboot/like/controller/LikeController.java b/src/main/java/com/springboot/like/controller/LikeController.java new file mode 100644 index 0000000..be48cd2 --- /dev/null +++ b/src/main/java/com/springboot/like/controller/LikeController.java @@ -0,0 +1,51 @@ +package com.springboot.like.controller; + +import com.springboot.like.dto.LikePostDto; +import com.springboot.like.entity.Like; +import com.springboot.like.mapper.LikeMapper; +import com.springboot.like.service.LikeService; +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.*; + +import javax.validation.Valid; +import javax.validation.constraints.Positive; +import java.net.URI; + + +@RestController +@RequestMapping("/v11/likes") +@Validated +public class LikeController { + private final static String LIKES_DEFAULT_URL = "/v11/likes"; + private final LikeService likeService; + private final LikeMapper mapper; + + public LikeController(LikeService likeService, LikeMapper mapper) { + this.likeService = likeService; + this.mapper = mapper; + } + + @PostMapping + public ResponseEntity postLike(@Valid @RequestBody LikePostDto likePostDto, + Authentication authentication) { + String email = (String) authentication.getPrincipal(); + Like like = likeService.createLike(mapper.likePostDtoToLike(likePostDto),email); + + URI location = UriCreator.createUri(LIKES_DEFAULT_URL, like.getLikeId()); + return ResponseEntity.created(location).build(); + } + + @DeleteMapping("/{like-id}") + public ResponseEntity deleteLike(@PathVariable("like-id") @Positive long likeId, + Authentication authentication){ + String email = (String) authentication.getPrincipal(); + likeService.deleteLike(likeId, email); + + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + +} diff --git a/src/main/java/com/springboot/like/dto/LikePostDto.java b/src/main/java/com/springboot/like/dto/LikePostDto.java new file mode 100644 index 0000000..002ee95 --- /dev/null +++ b/src/main/java/com/springboot/like/dto/LikePostDto.java @@ -0,0 +1,14 @@ +package com.springboot.like.dto; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import javax.validation.constraints.Positive; + +@Getter +public class LikePostDto { + @Positive + private long boardId; + +} diff --git a/src/main/java/com/springboot/like/entity/Like.java b/src/main/java/com/springboot/like/entity/Like.java new file mode 100644 index 0000000..a9185bd --- /dev/null +++ b/src/main/java/com/springboot/like/entity/Like.java @@ -0,0 +1,40 @@ +package com.springboot.like.entity; + + +import com.springboot.board.entity.Board; +import com.springboot.member.entity.Member; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.*; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@NoArgsConstructor +@Getter +@Setter +@Entity(name = "LIKES") +public class Like { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long likeId; + + @ManyToOne + @JoinColumn(name = "MEMBER_ID") + private Member member; + + @ManyToOne + @JoinColumn(name = "BOARD_ID") + private Board board; + + @Column(nullable = false) + private LocalDateTime createdAt = LocalDateTime.now(); + + + public void setBoard(Board board){ + this.board = board; + } + +} \ No newline at end of file diff --git a/src/main/java/com/springboot/like/mapper/LikeMapper.java b/src/main/java/com/springboot/like/mapper/LikeMapper.java new file mode 100644 index 0000000..5448e49 --- /dev/null +++ b/src/main/java/com/springboot/like/mapper/LikeMapper.java @@ -0,0 +1,18 @@ +package com.springboot.like.mapper; + +import com.springboot.board.entity.Board; +import com.springboot.like.dto.LikePostDto; +import com.springboot.like.entity.Like; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface LikeMapper { + default Like likePostDtoToLike(LikePostDto likePostDto){ + Like like = new Like(); + Board board = new Board(); + board.setBoardId(likePostDto.getBoardId()); + like.setBoard(board); + + return like; + } +} diff --git a/src/main/java/com/springboot/like/repository/LikeRepository.java b/src/main/java/com/springboot/like/repository/LikeRepository.java new file mode 100644 index 0000000..50246f5 --- /dev/null +++ b/src/main/java/com/springboot/like/repository/LikeRepository.java @@ -0,0 +1,7 @@ +package com.springboot.like.repository; + +import com.springboot.like.entity.Like; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LikeRepository extends JpaRepository { +} diff --git a/src/main/java/com/springboot/like/service/LikeService.java b/src/main/java/com/springboot/like/service/LikeService.java new file mode 100644 index 0000000..eedc1e6 --- /dev/null +++ b/src/main/java/com/springboot/like/service/LikeService.java @@ -0,0 +1,51 @@ +package com.springboot.like.service; + + +import com.springboot.board.entity.Board; +import com.springboot.board.service.BoardService; +import com.springboot.exception.BusinessLogicException; +import com.springboot.exception.ExceptionCode; +import com.springboot.like.entity.Like; +import com.springboot.like.repository.LikeRepository; +import com.springboot.member.entity.Member; +import com.springboot.member.service.MemberService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Transactional +@Service +public class LikeService { + private final LikeRepository likeRepository; + private final MemberService memberService; + private final BoardService boardService; + + public LikeService(LikeRepository likeRepository, MemberService memberService, BoardService boardService) { + this.likeRepository = likeRepository; + this.memberService = memberService; + this.boardService = boardService; + } + + + public Like createLike(Like like, String email){ + Member findMember = memberService.findVerifiedMember(email); + Board board = boardService.findVerifiedBoard(like.getBoard().getBoardId()); + like.setMember(findMember); + like.setBoard(board); + + return likeRepository.save(like); + } + + public void deleteLike(long likeId, String email){ + memberService.findVerifiedMember(email); + Optional optionalLike = likeRepository.findById(likeId); + Like like = optionalLike.orElseThrow(() -> + new BusinessLogicException(ExceptionCode.LIKE_NOT_FOUND)); + boardService.findVerifiedBoard(like.getBoard().getBoardId()); + + likeRepository.delete(like); + } + + +} diff --git a/src/main/java/com/springboot/member/dto/MemberDto.java b/src/main/java/com/springboot/member/dto/MemberDto.java new file mode 100644 index 0000000..8565264 --- /dev/null +++ b/src/main/java/com/springboot/member/dto/MemberDto.java @@ -0,0 +1,66 @@ +package com.springboot.member.dto; + +import com.springboot.member.entity.Member; +import com.springboot.validator.NotSpace; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +public class MemberDto { + @Getter + @AllArgsConstructor + public static class Post{ + @NotBlank + @Email + private String email; + + @NotBlank + private String password; + + @NotBlank + private String name; + + @Pattern(regexp = "^010-\\d{3,4}-\\d{4}$", + message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다.") + private String phone; + } + + @Getter + @AllArgsConstructor + public static class Patch{ + 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; + } + + } + @AllArgsConstructor + @Getter + public static class Response{ + private long memberId; + private String email; + private String name; + private String phone; + private Member.MemberStatus memberStatus; + + public String getMemberStatus() { + return memberStatus.getStatus(); + } + + } + +} 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..32aea40 --- /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.MemberDto; +import com.springboot.member.entity.Member; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +import java.util.List; + +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface MemberMapper { + Member memberPostToMember(MemberDto.Post requestBody); + Member memberPatchToMember(MemberDto.Patch requestBody); + MemberDto.Response memberToMemberResponse(Member member); + List membersToMemberResponses(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..3ae2c75 --- /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..d88e7cc --- /dev/null +++ b/src/main/java/com/springboot/member/service/MemberService.java @@ -0,0 +1,116 @@ +package com.springboot.member.service; + + +import com.springboot.auth.utils.JwtAuthorityUtils; +import com.springboot.board.entity.Board; +import com.springboot.board.repository.BoardRepository; +import com.springboot.board.service.BoardService; +import com.springboot.exception.BusinessLogicException; +import com.springboot.exception.ExceptionCode; +import com.springboot.helper.event.MemberRegistrationApplicationEvent; +import com.springboot.like.service.LikeService; +import com.springboot.member.entity.Member; +import com.springboot.member.repository.MemberRepository; +import org.springframework.context.ApplicationEventPublisher; +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.Isolation; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Transactional +@Service +public class MemberService { + private final MemberRepository memberRepository; + private final ApplicationEventPublisher publisher; + private final PasswordEncoder passwordEncoder; + private final JwtAuthorityUtils authorityUtils; + + public MemberService(MemberRepository memberRepository, ApplicationEventPublisher publisher, PasswordEncoder passwordEncoder, JwtAuthorityUtils authorityUtils) { + this.memberRepository = memberRepository; + this.publisher = publisher; + this.passwordEncoder = passwordEncoder; + this.authorityUtils = authorityUtils; + } + + public Member createMember(Member member){ + verifyExistsEmail(member.getEmail()); + + String encryptedPassword = passwordEncoder.encode(member.getPassword()); + member.setPassword(encryptedPassword); + + List roles = authorityUtils.createRoles(member.getEmail()); + member.setRoles(roles); + + Member savedMember = memberRepository.save(member); + + publisher.publishEvent(new MemberRegistrationApplicationEvent(this, savedMember)); + return savedMember; + } + + @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.SERIALIZABLE) + public Member updateMember(Member member){ + Member findMember = findVerifiedMember(member.getMemberId()); + + Optional.ofNullable(member.getName()) + .ifPresent(name -> findMember.setName(name)); + Optional.ofNullable(member.getPhone()) + .ifPresent(phone -> findMember.setPhone(phone)); + Optional.ofNullable(member.getMemberStatus()) + .ifPresent(memberStatus -> findMember.setMemberStatus(memberStatus)); + + return memberRepository.save(findMember); + } + @Transactional(readOnly = true) + public Member findMember(long memberId) { + return findVerifiedMember(memberId); + } + + public Page findMembers(int page, int size){ + return memberRepository + .findAll(PageRequest.of(page, size, Sort.by("memberId") + .descending())); + } + + + public void deleteMember(long memberId) { + Member findMember = findVerifiedMember(memberId); + findMember.setMemberStatus(Member.MemberStatus.MEMBER_QUIT); + for(Board board : findMember.getBoards()){ + board.setBoardStatus(Board.BoardStatus.QUESTION_DEACTIVED); + board.setMember(null); + } + + memberRepository.save(findMember); + } + + private void verifyExistsEmail(String email){ + Optional member = memberRepository.findByEmail(email); + if(member.isPresent()) + throw new BusinessLogicException(ExceptionCode.MEMBER_EXISTS); + } + @Transactional(readOnly = true) + public Member findVerifiedMember(long memberId){ + Optional optionalMember = memberRepository.findById(memberId); + Member findMember = optionalMember.orElseThrow(() -> + new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND)); + return findMember; + } + + @Transactional(readOnly = true) + public Member findVerifiedMember(String email){ + Optional optionalMember = memberRepository.findByEmail(email); + Member findMember = optionalMember.orElseThrow(() -> + new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND)); + return findMember; + } + + +} 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..627053a --- /dev/null +++ b/src/main/java/com/springboot/response/ErrorResponse.java @@ -0,0 +1,102 @@ +package com.springboot.response; + +import com.springboot.exception.ExceptionCode; +import lombok.Getter; +import org.springframework.http.HttpStatus; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; + +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; + + public ErrorResponse(int status, String message) { + this.status = status; + this.message = message; + } + + public ErrorResponse(List fieldErrors, 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()); + } + } + +} 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..2dc3833 --- /dev/null +++ b/src/main/java/com/springboot/utils/UriCreator.java @@ -0,0 +1,15 @@ +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..611913c --- /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..1b31404 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,3 +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 \ No newline at end of file From 90a3607a299c1db64b2f7c7cd60683928e40d728 Mon Sep 17 00:00:00 2001 From: YoungJunRoh Date: Thu, 18 Jul 2024 12:41:59 +0900 Subject: [PATCH 2/3] "featured changed" --- .../answer/service/AnswerService.java | 2 +- .../board/controller/BoardController.java | 5 ++-- .../springboot/board/dto/BoardPostDto.java | 7 ++++++ .../com/springboot/board/entity/Board.java | 24 +++++++++++++++++++ .../springboot/board/mapper/BoardMapper.java | 1 + .../board/service/BoardService.java | 20 ++++++++++++++-- .../springboot/exception/ExceptionCode.java | 3 ++- .../like/controller/LikeController.java | 4 ++++ .../like/repository/LikeRepository.java | 5 ++++ .../springboot/like/service/LikeService.java | 16 +++++++++++++ .../member/service/MemberService.java | 2 +- 11 files changed, 82 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/springboot/answer/service/AnswerService.java b/src/main/java/com/springboot/answer/service/AnswerService.java index 115b9ef..2e12455 100644 --- a/src/main/java/com/springboot/answer/service/AnswerService.java +++ b/src/main/java/com/springboot/answer/service/AnswerService.java @@ -96,7 +96,7 @@ private void verifiedAnswer(Answer answer){ boardService.findVerifiedBoard(answer.getBoard().getBoardId()); } private void updateBoard(Answer answer){ - Board board = boardService.findBoard(answer.getBoard().getBoardId()); + Board board = boardService.findVerifiedBoard(answer.getBoard().getBoardId()); board.setAnswer(answer); boardService.updateBoard(board); diff --git a/src/main/java/com/springboot/board/controller/BoardController.java b/src/main/java/com/springboot/board/controller/BoardController.java index fc6dd78..2950bdb 100644 --- a/src/main/java/com/springboot/board/controller/BoardController.java +++ b/src/main/java/com/springboot/board/controller/BoardController.java @@ -70,8 +70,9 @@ public ResponseEntity patchBoard(@PathVariable("board-id") @Positive long boardI } @GetMapping("/{board-id}") - public ResponseEntity patchBoard(@PathVariable("board-id") @Positive long boardId){ - Board board = boardService.findBoard(boardId); + public ResponseEntity patchBoard(@PathVariable("board-id") @Positive long boardId, + Authentication authentication){ + Board board = boardService.findBoard(boardId, authentication); return new ResponseEntity(new SingleResponseDto<>(mapper.boardToBoardResponseDto(board)), HttpStatus.OK); } diff --git a/src/main/java/com/springboot/board/dto/BoardPostDto.java b/src/main/java/com/springboot/board/dto/BoardPostDto.java index a2bc7ed..fc3be0e 100644 --- a/src/main/java/com/springboot/board/dto/BoardPostDto.java +++ b/src/main/java/com/springboot/board/dto/BoardPostDto.java @@ -1,9 +1,13 @@ package com.springboot.board.dto; +import com.springboot.board.entity.Board; import lombok.AllArgsConstructor; import lombok.Getter; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; import javax.validation.constraints.Positive; @Getter @@ -15,4 +19,7 @@ public class BoardPostDto { @NotBlank private String content; + + @Enumerated(value = EnumType.STRING) + private Board.BoardSecret boardSecret; } diff --git a/src/main/java/com/springboot/board/entity/Board.java b/src/main/java/com/springboot/board/entity/Board.java index 1eb4e74..ff9b650 100644 --- a/src/main/java/com/springboot/board/entity/Board.java +++ b/src/main/java/com/springboot/board/entity/Board.java @@ -38,6 +38,11 @@ public class Board { @Column(nullable = false, name = "LAST_MODIFIED_AT") private LocalDateTime modifiedAt = LocalDateTime.now(); + + @Enumerated(value = EnumType.STRING) + @Column(length = 20, nullable = false) + private BoardSecret boardSecret = BoardSecret.PUBLIC_BOARD; + @ManyToOne @JoinColumn(name = "MEMBER_ID") private Member member; @@ -49,6 +54,9 @@ public class Board { @OneToMany(mappedBy = "board", cascade = CascadeType.ALL) List likes = new ArrayList<>(); + @Column + private Integer likeCount = 0; + public void setMember(Member member) { this.member = member; } @@ -82,5 +90,21 @@ public enum BoardStatus{ } } + public enum BoardSecret{ + SECRET_BOARD(1, "비밀 글"), + PUBLIC_BOARD(2, "공개 글"); + + @Getter + private int stepNumber; + + @Getter + private String stepDescription; + + BoardSecret(int stepNumber, String stepDescription) { + this.stepNumber = stepNumber; + this.stepDescription = stepDescription; + } + } + } diff --git a/src/main/java/com/springboot/board/mapper/BoardMapper.java b/src/main/java/com/springboot/board/mapper/BoardMapper.java index 4c56987..4b01263 100644 --- a/src/main/java/com/springboot/board/mapper/BoardMapper.java +++ b/src/main/java/com/springboot/board/mapper/BoardMapper.java @@ -26,6 +26,7 @@ default Board boardPostDtoToBoard(BoardPostDto boardPostDto){ board.setTitle(boardPostDto.getTitle()); board.setContent(boardPostDto.getContent()); + board.setBoardSecret(boardPostDto.getBoardSecret()); return board; } diff --git a/src/main/java/com/springboot/board/service/BoardService.java b/src/main/java/com/springboot/board/service/BoardService.java index eaddc22..f09ebae 100644 --- a/src/main/java/com/springboot/board/service/BoardService.java +++ b/src/main/java/com/springboot/board/service/BoardService.java @@ -10,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.core.Authentication; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -69,7 +70,19 @@ public Board updateBoard(Board board, String email){ return boardRepository.save(findBoard); } - public Board findBoard(long boardId){ + public Board findBoard(long boardId,Authentication authentication){ + Board findBoard = findVerifiedBoard(boardId); + if(findBoard.getBoardSecret().equals(Board.BoardSecret.SECRET_BOARD)){ + boolean isAdmin = authentication.getAuthorities().stream() + .anyMatch(authority -> authority.getAuthority().equals("ROLE_ADMIN")); + if(isAdmin){ + return findBoard; + } + else{ + throw new BusinessLogicException(ExceptionCode.ONLY_ADMIN); + } + } + return findVerifiedBoard(boardId); } public Page findBoards(int page, int size){ @@ -100,7 +113,10 @@ private Board saveBoard(Board board){ } private void verifyBoard(Board board){ - memberService.findVerifiedMember(board.getMember().getMemberId()); + Member findMember = memberService.findVerifiedMember(board.getMember().getMemberId()); + if(findMember.getMemberStatus().equals(Member.MemberStatus.MEMBER_QUIT)){ + throw new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND); + } } diff --git a/src/main/java/com/springboot/exception/ExceptionCode.java b/src/main/java/com/springboot/exception/ExceptionCode.java index f2f6100..e7e6a10 100644 --- a/src/main/java/com/springboot/exception/ExceptionCode.java +++ b/src/main/java/com/springboot/exception/ExceptionCode.java @@ -11,7 +11,8 @@ public enum ExceptionCode { ANSWER_NOT_FOUND(404, "Answer not found"), NOT_YOUR_BOARD(409, "NOT_YOUR_BOARD"), NOT_YOUR_ANSWER(409, "NOT_YOUR_ANSWER"), - LIKE_NOT_FOUND(404,"LIKE_NOT_FOUND"); + LIKE_NOT_FOUND(404,"LIKE_NOT_FOUND"), + ONLY_ADMIN(401, "ONLY_ADMIN"); @Getter private int status; diff --git a/src/main/java/com/springboot/like/controller/LikeController.java b/src/main/java/com/springboot/like/controller/LikeController.java index be48cd2..0c00872 100644 --- a/src/main/java/com/springboot/like/controller/LikeController.java +++ b/src/main/java/com/springboot/like/controller/LikeController.java @@ -35,6 +35,10 @@ public ResponseEntity postLike(@Valid @RequestBody LikePostDto likePostDto, String email = (String) authentication.getPrincipal(); Like like = likeService.createLike(mapper.likePostDtoToLike(likePostDto),email); + if(like == null){ + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + URI location = UriCreator.createUri(LIKES_DEFAULT_URL, like.getLikeId()); return ResponseEntity.created(location).build(); } diff --git a/src/main/java/com/springboot/like/repository/LikeRepository.java b/src/main/java/com/springboot/like/repository/LikeRepository.java index 50246f5..205db10 100644 --- a/src/main/java/com/springboot/like/repository/LikeRepository.java +++ b/src/main/java/com/springboot/like/repository/LikeRepository.java @@ -1,7 +1,12 @@ package com.springboot.like.repository; +import com.springboot.board.entity.Board; import com.springboot.like.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 findByBoardAndMember(Board board, Member member); } diff --git a/src/main/java/com/springboot/like/service/LikeService.java b/src/main/java/com/springboot/like/service/LikeService.java index eedc1e6..30f15ff 100644 --- a/src/main/java/com/springboot/like/service/LikeService.java +++ b/src/main/java/com/springboot/like/service/LikeService.java @@ -14,6 +14,7 @@ import java.util.Optional; + @Transactional @Service public class LikeService { @@ -34,6 +35,19 @@ public Like createLike(Like like, String email){ like.setMember(findMember); like.setBoard(board); + Optional optionalLike = likeRepository.findByBoardAndMember(board,findMember); + + Integer count = like.getBoard().getLikeCount(); + if(optionalLike.isPresent()){ + likeRepository.delete(optionalLike.get()); + like.getBoard().setLikeCount(count -1); + return null; + } + + like.getBoard().setLikeCount(count + 1 + ); + + return likeRepository.save(like); } @@ -48,4 +62,6 @@ public void deleteLike(long likeId, String email){ } + + } diff --git a/src/main/java/com/springboot/member/service/MemberService.java b/src/main/java/com/springboot/member/service/MemberService.java index d88e7cc..5e71db3 100644 --- a/src/main/java/com/springboot/member/service/MemberService.java +++ b/src/main/java/com/springboot/member/service/MemberService.java @@ -85,7 +85,7 @@ public void deleteMember(long memberId) { findMember.setMemberStatus(Member.MemberStatus.MEMBER_QUIT); for(Board board : findMember.getBoards()){ board.setBoardStatus(Board.BoardStatus.QUESTION_DEACTIVED); - board.setMember(null); + } memberRepository.save(findMember); From 604764af6378755bf1a5c2ad57d4d87e717c5e53 Mon Sep 17 00:00:00 2001 From: YoungJunRoh Date: Fri, 19 Jul 2024 17:49:44 +0900 Subject: [PATCH 3/3] "featured changed" --- .../board/controller/BoardController.java | 3 ++ .../board/dto/BoardResponseDto.java | 2 + .../com/springboot/board/entity/Board.java | 16 +++++++ .../com/springboot/board/entity/View.java | 32 +++++++++++++ .../springboot/board/mapper/BoardMapper.java | 3 ++ .../board/repository/ViewRepository.java | 12 +++++ .../board/service/BoardService.java | 47 +++++++++++++++++-- .../springboot/like/service/LikeService.java | 3 +- 8 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/springboot/board/entity/View.java create mode 100644 src/main/java/com/springboot/board/repository/ViewRepository.java diff --git a/src/main/java/com/springboot/board/controller/BoardController.java b/src/main/java/com/springboot/board/controller/BoardController.java index 2950bdb..b62b9d1 100644 --- a/src/main/java/com/springboot/board/controller/BoardController.java +++ b/src/main/java/com/springboot/board/controller/BoardController.java @@ -72,6 +72,9 @@ public ResponseEntity patchBoard(@PathVariable("board-id") @Positive long boardI @GetMapping("/{board-id}") public ResponseEntity patchBoard(@PathVariable("board-id") @Positive long boardId, Authentication authentication){ + if (authentication == null) { + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } Board board = boardService.findBoard(boardId, authentication); return new ResponseEntity(new SingleResponseDto<>(mapper.boardToBoardResponseDto(board)), HttpStatus.OK); diff --git a/src/main/java/com/springboot/board/dto/BoardResponseDto.java b/src/main/java/com/springboot/board/dto/BoardResponseDto.java index 98e0ddb..3ef5cfa 100644 --- a/src/main/java/com/springboot/board/dto/BoardResponseDto.java +++ b/src/main/java/com/springboot/board/dto/BoardResponseDto.java @@ -23,6 +23,8 @@ public class BoardResponseDto { private String title; private String content; private LocalDateTime createdAt; + private Integer viewCount; + private Integer likeCount; private AnswerResponseDto answerResponseDto; diff --git a/src/main/java/com/springboot/board/entity/Board.java b/src/main/java/com/springboot/board/entity/Board.java index ff9b650..b57bef4 100644 --- a/src/main/java/com/springboot/board/entity/Board.java +++ b/src/main/java/com/springboot/board/entity/Board.java @@ -47,6 +47,9 @@ public class Board { @JoinColumn(name = "MEMBER_ID") private Member member; + @OneToMany(mappedBy = "board") + List views = new ArrayList<>(); + @OneToOne @JoinColumn(name = "ANSWER_ID") private Answer answer; @@ -57,6 +60,9 @@ public class Board { @Column private Integer likeCount = 0; + @Column + private Integer viewCount = 0; + public void setMember(Member member) { this.member = member; } @@ -72,6 +78,16 @@ public void setAnswer(Answer answer){ } } + public void addView(View view){ + this.getViews().add(view); + if(view.getBoard() != this){ + view.setBoard(this); + } + } + + + + public enum BoardStatus{ QUESTION_REGISTERED(1, "질문 등록 상태"), QUESTION_ANSWERED(2, "답변 완료 상태"), 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..5347dc8 --- /dev/null +++ b/src/main/java/com/springboot/board/entity/View.java @@ -0,0 +1,32 @@ +package com.springboot.board.entity; + +import com.springboot.member.entity.Member; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.*; + +@NoArgsConstructor +@Getter +@Setter +@Entity(name = "VIEWS") +public class View { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long viewId; + + @ManyToOne + @JoinColumn(name = "BOARD_ID") + private Board board; + + @OneToOne + @JoinColumn(name = "MEMBER_ID") + private Member member; + + public void setBoard(Board board){ + this.board = board; + } + + +} diff --git a/src/main/java/com/springboot/board/mapper/BoardMapper.java b/src/main/java/com/springboot/board/mapper/BoardMapper.java index 4b01263..70aa87e 100644 --- a/src/main/java/com/springboot/board/mapper/BoardMapper.java +++ b/src/main/java/com/springboot/board/mapper/BoardMapper.java @@ -39,6 +39,9 @@ default BoardResponseDto boardToBoardResponseDto(Board board){ boardResponseDto.setContent(board.getContent()); boardResponseDto.setCreatedAt(board.getCreatedAt()); boardResponseDto.setMember(board.getMember()); + boardResponseDto.setViewCount(board.getViewCount()); + boardResponseDto.setLikeCount(board.getLikeCount()); + AnswerResponseDto answerResponseDto = new AnswerResponseDto(); if(board.getAnswer() == null){ 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..f3b313b --- /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 findByBoardAndMember(Board board, Member member); +} diff --git a/src/main/java/com/springboot/board/service/BoardService.java b/src/main/java/com/springboot/board/service/BoardService.java index f09ebae..5a08dfe 100644 --- a/src/main/java/com/springboot/board/service/BoardService.java +++ b/src/main/java/com/springboot/board/service/BoardService.java @@ -2,7 +2,9 @@ import com.springboot.board.entity.Board; +import com.springboot.board.entity.View; import com.springboot.board.repository.BoardRepository; +import com.springboot.board.repository.ViewRepository; import com.springboot.exception.BusinessLogicException; import com.springboot.exception.ExceptionCode; import com.springboot.member.entity.Member; @@ -22,11 +24,15 @@ public class BoardService { private final BoardRepository boardRepository; private final MemberService memberService; + private final ViewRepository viewRepository; - public BoardService(BoardRepository boardRepository, MemberService memberService) { + + public BoardService(BoardRepository boardRepository, MemberService memberService, ViewRepository viewRepository) { this.boardRepository = boardRepository; this.memberService = memberService; + + this.viewRepository = viewRepository; } public Board createBoard(Board board){ @@ -72,18 +78,36 @@ public Board updateBoard(Board board, String email){ public Board findBoard(long boardId,Authentication authentication){ Board findBoard = findVerifiedBoard(boardId); + Member member = memberService.findVerifiedMember((String)authentication.getPrincipal()); + + Optional optionalView = viewRepository.findByBoardAndMember(findBoard, member); + Integer count = findBoard.getViews().size(); if(findBoard.getBoardSecret().equals(Board.BoardSecret.SECRET_BOARD)){ boolean isAdmin = authentication.getAuthorities().stream() .anyMatch(authority -> authority.getAuthority().equals("ROLE_ADMIN")); - if(isAdmin){ + if(isAdmin || Objects.equals((String) authentication.getPrincipal(), findBoard.getMember().getEmail())){ + if(optionalView.isEmpty()){ + View view = createView(boardId, authentication); + findBoard.addView(view); + findBoard.setViewCount(findBoard.getViews().size()); + }else{ + findBoard.setViewCount(findBoard.getViews().size()); + } return findBoard; } else{ throw new BusinessLogicException(ExceptionCode.ONLY_ADMIN); } + } else{ + if (optionalView.isEmpty()) { + View view = createView(boardId, authentication); + findBoard.addView(view); + findBoard.setViewCount(findBoard.getViews().size()); + } else { + findBoard.setViewCount(findBoard.getViews().size()); + } } - - return findVerifiedBoard(boardId); + return findBoard; } public Page findBoards(int page, int size){ return boardRepository.findAll(PageRequest.of(page,size, Sort.by("boardId").descending())); @@ -119,7 +143,22 @@ private void verifyBoard(Board board){ } } + private View createView(long boardId, Authentication authentication) { + Board board = findVerifiedBoard(boardId); + Member member = memberService.findVerifiedMember((String) authentication.getPrincipal()); + View view = new View(); + view.setMember(member); + view.setBoard(board); + + if (viewRepository.findByBoardAndMember(board, member).isPresent()) { + return null; + } else { + return viewRepository.save(view); + + } + + } } diff --git a/src/main/java/com/springboot/like/service/LikeService.java b/src/main/java/com/springboot/like/service/LikeService.java index 30f15ff..2162013 100644 --- a/src/main/java/com/springboot/like/service/LikeService.java +++ b/src/main/java/com/springboot/like/service/LikeService.java @@ -44,8 +44,7 @@ public Like createLike(Like like, String email){ return null; } - like.getBoard().setLikeCount(count + 1 - ); + like.getBoard().setLikeCount(count + 1); return likeRepository.save(like);