Spring Lv3 과제
회원 가입 API
최소 4자 이상, 10자 이하이며 알파벳 소문자(a~z), 숫자(0~9)
로 구성되어야 한다.최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z), 숫자(0~9), 특수문자
로 구성되어야 한다.로그인 API
댓글 작성 API
댓글 수정 API
댓글 삭제 API
예외 처리
전체 게시글 목록 조회 API
게시글 작성 API
선택한 게시글 조회 API
선택한 게시글 수정 API
선택한 게시글 삭제 API
기본적으로는 '다대일 단방향' 위주로 구성했다.
게시판 : 사용자
= N : 1 다대일 단방향
댓글 : 사용자
= N : 1 다대일 단방향
댓글 : 게시판
: N : 1 다대일 양방향
Lv2 와 동일하다
Lv2 와 동일하다
id 이름 수정
게시판 id : 댓글 id 와의 구분을 위해 모두 boardId 로 수정
댓글 id : cmtId
그에 따라, @PathVariable 를 사용한 URL 도 수정
게시글 작성, 전체/선택 조회, 수정은 Lv2 와 동일하다.
// 게시글 삭제
@DeleteMapping("/board/{boardId}")
public ResponseEntity<MsgResponseDto> deleteBoard(@PathVariable Long boardId, @RequestBody BoardRequestDto requestDto, HttpServletRequest request) {
return ResponseEntity.ok(boardService.deleteBoard(boardId, requestDto, request));
}
@RequestBody BoardRequestDto requestDto
기본적인 틀은 BoardController 과 동일하다
// 댓글 작성
@PostMapping("/{boardId}")
public ResponseEntity<CommentResponseDto> createComment(@PathVariable Long boardId, @RequestBody CommentRequestDto commentRequestDto, HttpServletRequest request) {
return ResponseEntity.ok(commentService.createComment(boardId, commentRequestDto, request));
}
@PostMapping("/{boardId}")
// 댓글 수정
@PutMapping("/{boardId}/{cmtId}") // 해당 게시글 번호, 댓글 번호가 필요
public ResponseEntity<CommentResponseDto> updateComment(@PathVariable Long boardId, @PathVariable Long cmtId, @RequestBody CommentRequestDto commentRequestDto, HttpServletRequest request) {
return ResponseEntity.ok(commentService.updateComment(boardId, cmtId, commentRequestDto, request));
}
// 댓글 삭제
@DeleteMapping("/{boardId}/{cmtId}")
public ResponseEntity<MsgResponseDto> deleteComment(@PathVariable Long boardId, @PathVariable Long cmtId, @RequestBody CommentRequestDto commentRequestDto, HttpServletRequest request) {
return ResponseEntity.ok(commentService.deleteComment(boardId, cmtId, commentRequestDto, request));
}
@PutMapping("/{boardId}/{cmtId}")
ResponseEntity<MsgResponseDto>
Lv2 와 동일하다.
@Getter
@NoArgsConstructor
public class SignupRequestDto {
@NotBlank
private String username;
@NotBlank
private String password;
private boolean admin = false; // 디폴트 값은 false. 관리자 권한일 경우 true 로 변한다
private String adminToken = "";
}
username 과 password 로 회원가입을 한다. (Lv2 와 동일)
회원 가입 시, 관리자(admin) 일 경우
Lv2 와 동일하다
@Getter
@NoArgsConstructor
public class BoardRequestDto {
private String username;
private String title;
private String contents;
}
username
@Getter
@NoArgsConstructor
public class CommentRequestDto {
private String username;
private String comment;
}
username
@Getter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class BoardResponseDto {
private Long id;
private String title;
private String username;
private String contents;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
private List<CommentResponseDto> commentList = new ArrayList<>(); // 게시글 조회 시, 댓글 목록도 함께 조회
// 게시글 작성
public BoardResponseDto(Board board) {
this.id = board.getId();
this.title = board.getTitle();
this.username = board.getUsername();
this.contents = board.getContents();
this.createdAt = board.getCreatedAt();
this.modifiedAt = board.getModifiedAt();
}
게시글 전체/선택 조회, 수정 (+ 댓글 목록이 함께 조회)
public BoardResponseDto(Board board, List<CommentResponseDto> commentList) {
this.id = board.getId();
this.title = board.getTitle();
this.username = board.getUsername();
this.contents = board.getContents();
this.createdAt = board.getCreatedAt();
this.modifiedAt = board.getModifiedAt();
this.commentList = commentList;
}
}
private List<CommentResponseDto> commentList = new ArrayList<>()
public BoardResponseDto(Board board, List<CommentResponseDto> commentList)
@Getter
public class CommentResponseDto {
private Long id;
private String username;
private String comment;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
// 게시글 전체/선택 조회, 수정 및 댓글 작성, 수정
public CommentResponseDto(Comment comment) {
this.id = comment.getId();
this.username = comment.getUsername();
this.comment = comment.getComment();
this.createdAt = comment.getCreatedAt();
this.modifiedAt = comment.getModifiedAt();
}
}
public CommentResponseDto(Comment comment)
Lv2 와 동일하다
// 게시글 전체 조회
@Transactional(readOnly = true)
public List<BoardResponseDto> getBoardList() {
List<Board> boardList = boardRepository.findAllByOrderByCreatedAtDesc(); // 1.
List<BoardResponseDto> boardResponseDtoList = new ArrayList<>(); // 2.
for (Board board : boardList) { // 3.
List<CommentResponseDto> commentList = new ArrayList<>();
for (Comment comment : board.getCommentList()) {
commentList.add(new CommentResponseDto(comment));
}
boardResponseDtoList.add(new BoardResponseDto(board, commentList)); // 4.
}
return boardResponseDtoList; // 5.
}
게시글을 조회하면서, 댓글 목록도 해당 게시글과 함께 조회되도록 했다.
DB 에 저장된 게시글을 생성시간 기준 내림차순으로 조회한 후, boardList 에 담는다.
게시글 + 댓글이 함께 조회될 새로운 boardResponseDtoList 배열 생성
boardList 배열을 돌면서 (= 게시글 목록에 담긴 게시글들을 돌면서), 새롭게 생성한 댓글 목록 commentList 배열에 댓글들을 담는다
boardResponseDtoList 배열(게시글+댓글)에 게시글 board 과 댓글 목록 commentList 을 담는다
boardResponseDtoList 배열을 반환
// 게시글 선택 조회
public BoardResponseDto getBoard(Long boardId) {
Board board = boardRepository.findById(boardId).orElseThrow(
() -> new CustomException(NOT_FOUND_BOARD)
);
List<CommentResponseDto> commentList = new ArrayList<>();
for (Comment comment : board.getCommentList()) {
commentList.add(new CommentResponseDto(comment));
}
return new BoardResponseDto(board, commentList);
}
// 게시글 수정
@Transactional
public BoardResponseDto updateBoard(Long boardId, BoardRequestDto requestDto, HttpServletRequest request) {
User user = getUserFromToken(request); // getUserFromToken 메서드를 호출
Board board;
// 사용자의 권한 확인
if (user.getRole().equals(UserRoleEnum.ADMIN)) {
board = boardRepository.findById(boardId).orElseThrow (
() -> new CustomException(NOT_FOUND_BOARD)
);
} else {
board = boardRepository.findByIdAndUserId(boardId, user.getId()).orElseThrow (
() -> new CustomException(NOT_FOUND_BOARD)
);
}
// username 일치 여부 확인
if (requestDto.getUsername().equals(user.getUsername())) {
board.update(requestDto);
} else {
throw new CustomException(AUTHORIZATION);
}
List<CommentResponseDto> commentList = new ArrayList<>();
for (Comment comment : board.getCommentList()) {
commentList.add(new CommentResponseDto(comment));
}
return new BoardResponseDto(board, commentList);
}
사용자의 권한 확인
username 일치 여부 확인
commentList
// 게시글 삭제
public MsgResponseDto deleteBoard(Long boardId, BoardRequestDto requestDto, HttpServletRequest request) {
User user = getUserFromToken(request); // getUserFromToken 메서드를 호출
Board board;
// 토큰을 가진 사용자가 '관리자'라면
if (user.getRole().equals(UserRoleEnum.ADMIN)) {
board = boardRepository.findById(boardId).orElseThrow(
() -> new CustomException(NOT_FOUND_BOARD)
);
} else {
board = boardRepository.findByIdAndUserId(boardId, user.getId()).orElseThrow (
() -> new CustomException(NOT_FOUND_BOARD)
);
}
if (requestDto.getUsername().equals(user.getUsername())) {
boardRepository.delete(board);
} else {
throw new CustomException(AUTHORIZATION);
}
return new MsgResponseDto("게시글을 삭제했습니다.", HttpStatus.OK.value());
}
조건 충족 시 게시글을 삭제하고 삭제 완료 메시지를 반환하는 부분을 제외하고는 게시글 수정과 동일하다
@Service
@RequiredArgsConstructor
public class CommentService {
private final UserRepository userRepository;
private final BoardRepository boardRepository;
private final CommentRepository commentRepository;
private final JwtUtil jwtUtil;
...
}
기본적인 틀은 지금껏 사용한 service 단의 것들과 동일하다
// 댓글 작성
public CommentResponseDto createComment(Long boardId, CommentRequestDto commentRequestDto, HttpServletRequest request) {
Board board = boardRepository.findById(boardId).orElseThrow (
() -> new CustomException(NOT_FOUND_COMMENT)
);
User user = getUserFromToken(request); // getUserFromToken 메서드를 호출
Comment comment = new Comment(commentRequestDto, board, user);
Comment saveComment = commentRepository.save(comment);
return new CommentResponseDto(saveComment);
}
Comment comment = new Comment(commentRequestDto, board, user)
// 댓글 수정
@Transactional
public CommentResponseDto updateComment(Long boardId, Long cmtId, CommentRequestDto commentRequestDto, HttpServletRequest request) {
// 해당 사용자가 토큰을 가진 사용자인지 확인
User user = getUserFromToken(request);
// 게시글이 있는지
Board board = boardRepository.findById(boardId).orElseThrow (
() -> new CustomException(NOT_FOUND_BOARD)
);
Comment comment;
// 사용자의 권한 확인
if (user.getRole().equals(UserRoleEnum.ADMIN)) {
comment = commentRepository.findById(cmtId).orElseThrow (
() -> new CustomException(NOT_FOUND_COMMENT)
);
} else {
comment = commentRepository.findByIdAndUserId(cmtId, user.getId()).orElseThrow (
() -> new CustomException(NOT_FOUND_COMMENT)
);
}
// username 일치 여부 확인
if (commentRequestDto.getUsername().equals(user.getUsername())) {
comment.update(commentRequestDto);
} else {
throw new CustomException(AUTHORIZATION);
}
return new CommentResponseDto(comment);
}
// 댓글 삭제
public MsgResponseDto deleteComment(Long boardId, Long cmtId, CommentRequestDto commentRequestDto, HttpServletRequest request) {
// 해당 사용자가 토큰을 가진 사용자인지 확인
User user = getUserFromToken(request);
// 게시글이 있는지
Board board = boardRepository.findById(boardId).orElseThrow (
() -> new CustomException(NOT_FOUND_BOARD)
);
Comment comment;
// 토큰을 가진 사용자가 '관리자'라면
if (user.getRole().equals(UserRoleEnum.ADMIN)) {
comment = commentRepository.findById(cmtId).orElseThrow ( // 관리자는 모든 댓글을 삭제할 권한 있으므로, userId 와 일치여부까지 비교할 필요는 없다
() -> new CustomException(NOT_FOUND_COMMENT)
);
} else {
comment = commentRepository.findByIdAndUserId(cmtId, user.getId()).orElseThrow (
() -> new CustomException(NOT_FOUND_COMMENT)
);
}
if (commentRequestDto.getUsername().equals(user.getUsername())) {
commentRepository.deleteById(cmtId);
} else {
throw new CustomException(AUTHORIZATION);
}
return new MsgResponseDto("댓글을 삭제했습니다.", HttpStatus.OK.value());
}
댓글 삭제도 게시글 삭제와 기본적인 틀은 동일하다
// 회원 가입
public void signup(SignupRequestDto requestDto) {
String username = requestDto.getUsername();
String password = requestDto.getPassword();
// 회원 중복 확인
Optional<User> checkUsername = userRepository.findByUsername(username);
if (checkUsername.isPresent()) {
throw new CustomException(DUPLICATED_USERNAME);
}
// 사용자 ROLE 확인 (admin = true 일 경우 아래 코드 수행)
UserRoleEnum role = UserRoleEnum.USER;
if (requestDto.isAdmin()) {
if (!ADMIN_TOKEN.equals(requestDto.getAdminToken())) {
throw new CustomException(NOT_MATCH_ADMIN_TOKEN);
}
role = UserRoleEnum.ADMIN;
}
// 사용자 등록 (admin = false 일 경우 아래 코드 수행)
User user = new User(username, password, role);
userRepository.save(user);
}
로그인의 경우, custom 예외 처리를 제외하곤 Lv2 와 동일하다
public interface BoardRepository extends JpaRepository<Board, Long> {
List<Board> findAllByOrderByCreatedAtDesc();
Optional<Board> findByIdAndUserId(Long boardId, Long userId);
}
public interface CommentRepository extends JpaRepository<Comment, Long> {
Optional<Comment> findByIdAndUserId(Long cmtId, Long userId);
}
댓글 아이디를 cmtId 로 나타냈다.
Admin 이 아닌 일반 User 가 댓글 수정/삭제 시, id 일치 여부를 확인하기 위함이다.
Lv2 와 동일하다
public class Board extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "title", nullable = false)
private String title;
@Column(name = "username", nullable = false)
private String username;
@Column(name = "contents", nullable = false, length = 500)
private String contents;
@ManyToOne(fetch = FetchType.LAZY) // board : user = N : 1 다대일 단방향
@JoinColumn(name = "user_id", nullable = false)
private User user;
@OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE) // comment : board = N : 1 다대일 양방향
private List<Comment> commentList = new ArrayList<>();
...
}
@OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE)
cascade = CascadeType.REMOVE
private List<Comment> commentList = new ArrayList<>();
나머진 Lv2 와 동일하다
public class Comment extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", nullable = false)
private String username;
@Column(name = "comment", nullable = false)
private String comment;
@ManyToOne // comment : user = N : 1 다대일 단방향
@JoinColumn(name = "user_id", nullable = false)
private User user;
@ManyToOne(fetch = FetchType.LAZY) // comment : board = N : 1 다대일 양방향
@JoinColumn(name = "board_id", nullable = false)
private Board board;
...
}
// 댓글 작성
public Comment(CommentRequestDto commentRequestDto, Board board, User user) {
this.comment = commentRequestDto.getComment();
this.username = user.getUsername();
this.board = board;
this.user = user;
}
// 댓글 수정
public void update(CommentRequestDto commentRequestDto) {
this.comment = commentRequestDto.getComment();
}
댓글 작성 시
단. 댓글 수정 시에는
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
@Size(min = 4,max = 10, message ="작성자명은 4자 이상 10자 이하만 가능합니다.")
@Pattern(regexp = "^[a-z0-9]*$", message = "작성자명은 알파벳 소문자, 숫자만 사용 가능합니다.")
private String username;
@Column(nullable = false)
@Size(min = 8,max = 15, message ="비밀번호는 8자 이상 15자 이하만 가능합니다.")
@Pattern(regexp = "^[a-zA-Z_0-9`~!@#$%^&*()+|={};:,.<>/?]*$", message = "비밀번호는 알파벳 대소문자, 숫자, 특수문자만 사용 가능합니다.")
private String password;
@Column(nullable = false)
@Enumerated(value = EnumType.STRING)
private UserRoleEnum role;
@Enumerated(value = EnumType.STRING)
Enum 타입을 저장할 때 사용
value = EnumType.STRING 을 사용해서 Enum 의 이름 그대로 DB 에 저장
Lv2 와 달리 권한(role)이 추가됐다.
*만약, 해당 사용자가 작성한 게시글 & 댓글 목록을 조회하고 싶다면
@OneToMany(mappedBy = "user") private List<Board> boardList = new ArrayList<>(); @OneToMany(mappedBy = "user") private List<Comment> commentList = new ArrayList<>();
양방향 연관관계를 지어줄 수도 있다.
이렇게 될 수 있는 이유는 여기에 적힌 코드들이 연관관계의 주인이 아니기 때문이다.(mappedBy 가 사용됐음)
따라서, 이들은 오로지 '조회'만 가능하게 된다.
참고: 기본 키 PK vs 외래 키 FK
public User(String username, String password, UserRoleEnum role) {
this.username = username;
this.password = password;
this.role = role;
}
public enum UserRoleEnum {
USER, // 사용자 권한
ADMIN // 관리자 권한
}
Lv1 과 동일하다
Lv1 과 동일하다
Lv2 와 동일하다
참고: @RestControllerAdvice 예외 처리 구현하기
예외 처리에 대한 부분은 따로 정리해두었다.