[SB] 21. 스프링부트 블로그 v3

최재원's avatar
May 08, 2025
[SB] 21. 스프링부트 블로그 v3
13. 스프링부트 블로그 v2
13. 스프링부트 블로그 v2
를 기준으로 v3 를 만듬. 참고해야함

모든 컨트롤러가 json 을 return 한다

이제 api 에 대한 문서를 잘 만들어야 프론트가 잘 사용한다
insert, update 는 실행한 결과를 return 해야 한다
주소에서 동사를 제거한다
json 으로 요청받고 json 으로 응답한다
 

Return json 처리⬇

1. 모든 view 파일 삭제

notion image
  • .keep 파일을 넣지 않으면 templates 폴더가 git에 저장되지 않는다
  • 내부 파일이 없는 폴더는 git이 무시한다

2. 유저

1. 회원가입

☕UserController

@PostMapping("/join") public String join(@Valid UserRequest.JoinDTO joinDTO, Errors errors) { userService.회원가입(joinDTO); return "redirect:/login-form"; }
⬇ mustache 파일이 아니라 → json 을 return 한다
@PostMapping("/join") public @ResponseBody Resp<?> join(@Valid @RequestBody UserRequest.JoinDTO reqDTO, Errors errors) { UserResponse.DTO respDTO = userService.회원가입(reqDTO); return Resp.ok(respDTO); }
  • 회원가입은 유저정보를 insert 하는 것. 따라서 insert 한 유저정보를 return 해야 한다
  • @ResponseBody → 파일이 아니라 json 을 return 한다고 알려주는 어노테이션
  • @RequestBody → mime type 을 x-www-form-urlencoded 가 아니라 application/json 으로 받겠다는 어노테이션

☕UserService

@Transactional public void 회원가입(UserRequest.JoinDTO joinDTO) { // 1. 해당 username이 사용 중인지 확인 User alreadyUser = userRepository.findByUsername(joinDTO.getUsername()); // 2. 사용 중이면 예외! if (alreadyUser != null) { throw new Exception400("해당 username은 이미 사용중 입니다"); } // 3. 아니면 회원가입 성공 userRepository.save(joinDTO.toEntity()); }
⬇ insert 된 user 를 DTO 에 담아 return 한다
// RestAPI 규칙 1 -> insert 는 그 행을 DTO 에 담아 돌려줘야 한다 @Transactional public UserResponse.DTO 회원가입(UserRequest.JoinDTO reqDTO) { try { User userPS = userRepository.save(reqDTO.toEntity()); return new UserResponse.DTO(userPS); } catch (Exception e) { throw new Exception400("잘못된 요청입니다"); } }
  • RestAPI 규칙 1 -> insert 는 그 행을 DTO 에 담아 돌려줘야 한다

☕UserResponse

package shop.mtcoding.blog.user; import lombok.Data; public class UserResponse { // 가장 기본이 되는 DTO 는 그냥 DTO 로 만든다(User -> DTO) // RestAPI 규칙 2 -> DTO 에 민감 정보 삭제, 날짜는 String (날짜타입을 공부할 때까지) @Data public static class DTO { private Integer id; private String username; private String email; private String createdAt; public DTO(User user) { this.id = user.getId(); this.username = user.getUsername(); this.email = user.getEmail(); this.createdAt = user.getCreatedAt().toString(); } } }
  • 유저 정보를 return 하기 위한 DTO
  • 가장 기본적인 정보를 가진 DTO 는 이름을 그냥 DTO 라고 적는다
  • RestAPI 규칙 2 -> DTO 에 민감 정보 삭제
notion image
 

2. 로그인(JWT 배우기 까지 미완)

☕UserController

@PostMapping("/login") public String login(@Valid UserRequest.LoginDTO loginDTO, Errors errors, HttpServletResponse response) { User sessionUser = userService.로그인(loginDTO); session.setAttribute("sessionUser", sessionUser); if (loginDTO.getRememberMe() == null) { Cookie cookie = new Cookie("username", null); cookie.setMaxAge(0); // 브라우저가 MaxAge가 0인 쿠키는 자동 삭제함 response.addCookie(cookie); } else { Cookie cookie = new Cookie("username", loginDTO.getUsername()); cookie.setMaxAge(24 * 60 * 60 * 7); // MaxAge에 값을 넣으면 브라우저를 새로 켜도 유지됨 response.addCookie(cookie); } return "redirect:/"; }
@PostMapping("/login") // password랑 id가 노출되면 안되기 때문에 post로 받는다 public String login(@Valid @RequestBody UserRequest.LoginDTO loginDTO, Errors errors, HttpServletResponse response) { //System.out.println(loginDTO); User sessionUser = userService.로그인(loginDTO); session.setAttribute("sessionUser", sessionUser); if (loginDTO.getRememberMe() == null) { Cookie cookie = new Cookie("username", null); cookie.setMaxAge(0); // 즉시 만료 response.addCookie(cookie); } else { Cookie cookie = new Cookie("username", loginDTO.getUsername()); cookie.setMaxAge(60 * 60 * 24 * 7); response.addCookie(cookie); } return "redirect:/"; }
  • 로그인이 post 맵핑을 사용하는 이유
    • 로그인에는 사용자의 민감한 정보(비밀번호)가 들어있기 때문에 조회 기능 이긴 하나 post 맵핑을 사용해 body 로 데이터를 받음

☕UserService

public User 로그인(UserRequest.LoginDTO loginDTO) { // 1. username에 대한 데이터가 있는지 확인 User user = userRepository.findByUsername(loginDTO.getUsername()); // 2. 없으면 예외! if (user == null) { throw new Exception401("username 혹은 password가 맞지 않습니다"); } // 3. 있으면 password 비교 if (!(user.getPassword().equals(loginDTO.getPassword()))) { throw new Exception401("username 혹은 password가 맞지 않습니다"); } return user; }
// TODO -> A4용지에다가 id, username 적어, A4용지를 서명함, A4용지를 돌려주기 public User 로그인(UserRequest.LoginDTO loginDTO) { User user = userRepository.findByUsername(loginDTO.getUsername()); if (user == null) throw new Exception401("유저네임 혹은 비밀번호가 틀렸습니다"); if (!user.getPassword().equals(loginDTO.getPassword())) { throw new Exception401("유저네임 혹은 비밀번호가 틀렸습니다"); } return user; }

☕UserResponse

 

3. 회원정보수정(JWT 배우기 까지 미완)

☕UserController

@PostMapping("/user/update") public String update(@Valid UserRequest.UpdateDTO updateDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); User userPS = userService.회원정보수정(updateDTO, sessionUser.getId()); // 세션 동기화 session.setAttribute("sessionUser", userPS); return "redirect:/"; }
⬇ post → put 으로 맴핑 수정
// TODO -> JWT 이후에 하기 @PutMapping("/user") // 인증에 관련된 id 는 주소로 받는게 아닌 session 에서 가져온다 public String update(@Valid @RequestBody UserRequest.UpdateDTO updateDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); User userPS = userService.회원정보수정(updateDTO, sessionUser.getId()); // 토큰도 새로 만들어 줘야 한다 // 세션 동기화 session.setAttribute("sessionUser", userPS); return "redirect:/"; }

☕UserService

@Transactional public User 회원정보수정(UserRequest.UpdateDTO updateDTO, Integer userId) { User userPS = userRepository.findByUserId(userId); if (userPS == null) throw new Exception404("회원을 찾을 수 없습니다"); userPS.update(updateDTO.getPassword(), updateDTO.getEmail()); // 영속화 된 객체(db에서 조회한 것)의 상태변경 return userPS; } // 더티체킹 -> 상태가 변경되면 update를 날린다
// TODO RestAPI 규칙 3 -> update 된 data 도 돌려줘야 한다 @Transactional public User 회원정보수정(UserRequest.UpdateDTO updateDTO, Integer userId) { User userPS = userRepository.findById(userId); // Exception404 if (userPS == null) throw new Exception404("자원을 찾을 수 없습니다"); userPS.update(updateDTO.getPassword(), updateDTO.getEmail()); // 영속화된 객체의 상태변경 return userPS; // 리턴한 이유는 세션을 동기화해야해서!! } // 더티체킹 -> 상태가 변경되면 update을 날려요!!
  • TODO RestAPI 규칙 3 -> update 된 data 도 돌려줘야 한다

☕UserResponse

 

3. 게시글

1. 글상세보기(댓글,좋아요 포함)

☕BoardController

@GetMapping("/board/{id}") public String detail(@PathVariable("id") Integer id, HttpServletRequest request) { User sessionUser = (User) session.getAttribute("sessionUser"); Integer sessionUserId = sessionUser == null ? null : sessionUser.getId(); BoardResponse.DetailDTO detailDTO = boardService.상세보기(id, sessionUserId); request.setAttribute("model", detailDTO); return "board/detail"; }
⬇ 주소에 detail 이 추가된 이유 → 상세보기 화면에는 기존 board 보다 더 추가된 데이터가 있기 때문
@GetMapping("/board/{id}/detail") // 전달하는 데이터를 보고 주소를 설계함 public @ResponseBody Resp<?> getBoardDetail(@PathVariable("id") Integer id) { User sessionUser = (User) session.getAttribute("sessionUser"); Integer sessionUserId = (sessionUser == null ? null : sessionUser.getId()); BoardResponse.DetailDTO detailDTO = boardService.글상세보기(id, sessionUserId); return Resp.ok(detailDTO); }
  • 게시물 상세화면에 필요한 컨트롤러이기 때문에 이름을 getBoardDetail 로 작성하였다

☕BoardService

@Transactional public BoardResponse.DetailDTO 글상세보기(Integer id, Integer userId) { Board boardPS = boardRepository.findByIdJoinUserAndReplies(id); Love love = loveRepository.findByUserIdAndBoardId(userId, id); Long loveCount = loveRepository.findByBoardId(id); Integer loveId = love == null ? null : love.getId(); Boolean isLove = love == null ? false : true; BoardResponse.DetailDTO detailDTO = new BoardResponse.DetailDTO(boardPS, userId, isLove, loveCount.intValue(), loveId); return detailDTO; }

☕BoardResponse

// 상세보기 화면에 필요한 데이터 @Data public static class DetailDTO { private Integer id; private String title; private String content; private Boolean isPublic; private Boolean isOwner; private Boolean isLove; private Integer loveCount; private String username; private Timestamp createdAt; private Integer loveId; private List<ReplyDTO> replies; @Data public class ReplyDTO { private Integer id; private String content; private String username; private Boolean isOwner; public ReplyDTO(Reply reply, Integer sessionUserId) { this.id = reply.getId(); this.content = reply.getContent(); this.username = reply.getUser().getUsername(); this.isOwner = reply.getUser().getId().equals(sessionUserId); } } public DetailDTO(Board board, Integer sessionUserId, Boolean isLove, Integer loveCount, Integer loveId) { this.id = board.getId(); this.title = board.getTitle(); this.content = board.getContent(); this.isPublic = board.getIsPublic(); this.isOwner = sessionUserId == board.getUser().getId(); this.username = board.getUser().getUsername(); this.createdAt = board.getCreatedAt(); this.isLove = isLove; this.loveCount = loveCount; this.loveId = loveId; List<ReplyDTO> repliesDTO = new ArrayList<>(); for (Reply reply : board.getReplies()) { ReplyDTO replyDTO = new ReplyDTO(reply, sessionUserId); repliesDTO.add(replyDTO); } this.replies = repliesDTO; } }
notion image

2. 글보기(api용 주소 추가)

☕BoardController

@GetMapping("/board/{id}") // 전달하는 데이터를 보고 주소를 설계함 public @ResponseBody Resp<?> getBoardOne(@PathVariable("id") int id) { User sessionUser = (User) session.getAttribute("sessionUser"); BoardResponse.DTO respDTO = boardService.글보기(id, sessionUser.getId()); return Resp.ok(respDTO); }
  • 전달하는 데이터를 보고 주소를 설계
  • getBoardOne → api 용 board 를 단 건으로 조회할 수 있는 주소

☕BoardService

// 규칙4 -> 화면에 보이는 데이터 + 반드시 PK 를 넣어야 한다 public BoardResponse.DTO 글보기(int id, Integer sessionUserId) { Board boardPS = boardRepository.findById(id); if (boardPS == null) throw new Exception404("자원을 찾을 수 없습니다"); if (!boardPS.getUser().getId().equals(sessionUserId)) { throw new Exception403("권한이 없습니다"); } return new BoardResponse.DTO(boardPS); }
  • 규칙4 -> 화면에 보이는 데이터 + 반드시 PK 를 넣어야 한다

☕BoardResponse

@Data public static class DTO { private Integer id; private String title; private String content; private Boolean isPublic; private Integer userId; private String createdAt; public DTO(Board board) { this.id = board.getId(); this.title = board.getTitle(); this.content = board.getContent(); this.isPublic = board.getIsPublic(); this.userId = board.getUser().getId(); this.createdAt = board.getCreatedAt().toString(); } }
  • 가장 기본적인 board 와 같은 DTO
notion image

3. 글수정하기

☕BoardController

@PostMapping("/board/{id}/update") public String update(@PathVariable("id") Integer id, @Valid BoardRequest.UpdateDTO reqDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); boardService.글수정(id, reqDTO, sessionUser.getId()); return "redirect:/board/" + id; }
⬇ post → put 맵핑 변경
@PutMapping("/board/{id}") public @ResponseBody Resp<?> update(@PathVariable("id") Integer id, @Valid @RequestBody BoardRequest.UpdateDTO reqDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); BoardResponse.DTO respDTO = boardService.글수정하기(reqDTO, id, sessionUser.getId()); return Resp.ok(respDTO); }
  • 수정된 결과를 DTO 에 넣어 return

☕BoardService

@Transactional public User 회원정보수정(UserRequest.UpdateDTO updateDTO, Integer userId) { User userPS = userRepository.findByUserId(userId); if (userPS == null) throw new Exception404("회원을 찾을 수 없습니다"); userPS.update(updateDTO.getPassword(), updateDTO.getEmail()); // 영속화 된 객체(db에서 조회한 것)의 상태변경 return userPS; } // 더티체킹 -> 상태가 변경되면 update를 날린다
// TODO RestAPI 규칙 3 -> update 된 data 도 돌려줘야 한다 @Transactional public User 회원정보수정(UserRequest.UpdateDTO updateDTO, Integer userId) { User userPS = userRepository.findById(userId); // Exception404 if (userPS == null) throw new Exception404("자원을 찾을 수 없습니다"); userPS.update(updateDTO.getPassword(), updateDTO.getEmail()); // 영속화된 객체의 상태변경 return userPS; // 리턴한 이유는 세션을 동기화해야해서!! } // 더티체킹 -> 상태가 변경되면 update을 날려요!!
  • TODO RestAPI 규칙 3 -> update 된 data 도 돌려줘야 한다

☕BoardResponse

@Data public static class DTO { private Integer id; private String title; private String content; private Boolean isPublic; private Integer userId; private String createdAt; public DTO(Board board) { this.id = board.getId(); this.title = board.getTitle(); this.content = board.getContent(); this.isPublic = board.getIsPublic(); this.userId = board.getUser().getId(); this.createdAt = board.getCreatedAt().toString(); } }
  • Board 엔티티와 동일한 DTO
notion image

4. 글쓰기

☕BoardController

@PostMapping("/board/save") public String save(@Valid BoardRequest.SaveDTO reqDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); boardService.글쓰기(reqDTO, sessionUser); return "redirect:/"; }
⬇ 주소에 save 동사 삭제
@PostMapping("/board") public @ResponseBody Resp<?> save(@Valid @RequestBody BoardRequest.SaveDTO reqDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); BoardResponse.DTO respDTO = boardService.글쓰기(reqDTO, sessionUser); return Resp.ok(respDTO); }
  • insert 한 board 를 DTO 에 담아서 return

☕BoardService

@Transactional public void 글쓰기(BoardRequest.SaveDTO saveDTO, User sessionUser) { Board board = saveDTO.toEntity(sessionUser); boardRepository.save(board); }
@Transactional public BoardResponse.DTO 글쓰기(BoardRequest.SaveDTO reqDTO, User sessionUser) { Board board = reqDTO.toEntity(sessionUser); Board boardPS = boardRepository.save(board); return new BoardResponse.DTO(boardPS); }
  • insert 된 board 엔티티를 DTO 에 담아서 return

☕BoardResponse

@Data public static class DTO { private Integer id; private String title; private String content; private Boolean isPublic; private Integer userId; private String createdAt; public DTO(Board board) { this.id = board.getId(); this.title = board.getTitle(); this.content = board.getContent(); this.isPublic = board.getIsPublic(); this.userId = board.getUser().getId(); this.createdAt = board.getCreatedAt().toString(); } }
  • Board 엔티티와 동일한 DTO
 

4. 댓글

1. 댓글쓰기

☕ReplyController

@PostMapping("/reply/save") public String saveReply(@Valid ReplyRequest.SaveDTO reqDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); replyService.댓글등록(reqDTO, sessionUser.getId()); return "redirect:/board/" + reqDTO.getBoardId(); }
@PostMapping("/reply") public @ResponseBody Resp<?> save(@Valid @RequestBody ReplyRequest.SaveDTO reqDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); ReplyResponse.DTO respDTO = replyService.댓글쓰기(reqDTO, sessionUser); return Resp.ok(respDTO); }

☕ReplyService

@Transactional public void 댓글등록(ReplyRequest.SaveDTO reqDTO, Integer sessionUserId) { replyRepository.save(reqDTO.toEntity(sessionUserId)); }
@Transactional public ReplyResponse.DTO 댓글쓰기(ReplyRequest.SaveDTO reqDTO, User sessionUser) { Reply replyPS = replyRepository.save(reqDTO.toEntity(sessionUser)); return new ReplyResponse.DTO(replyPS); }

☕ReplyResponse

@Data public static class DTO { private Integer id; private String content; // 댓글 내용 private Integer userId; private Integer boardId; private String createdAt; public DTO(Reply reply) { this.id = reply.getId(); this.content = reply.getContent(); this.userId = reply.getUser().getId(); this.boardId = reply.getBoard().getId(); this.createdAt = reply.getCreatedAt().toString(); } }

2. 댓글삭제

☕ReplyController

@PostMapping("/reply/{id}/delete") public String deleteReply(@PathVariable Integer id) { User sessionUser = (User) session.getAttribute("sessionUser"); Integer boardId = replyService.댓글삭제(id, sessionUser.getId()); return "redirect:/board/" + boardId; }
@DeleteMapping("/reply/{id}") public @ResponseBody Resp<?> delete(@PathVariable("id") Integer id) { User sessionUser = (User) session.getAttribute("sessionUser"); replyService.댓글삭제(id, sessionUser.getId()); return Resp.ok(null); }
  • 삭제한 후에는 null 을 넣어 return 한다

☕ReplyService

@Transactional public Integer 댓글삭제(Integer id, Integer sessionUserId) { Reply replyPS = replyRepository.findById(id); if (replyPS == null) throw new Exception404("해당 댓글이 없습니다"); if (!(replyPS.getUser().getId().equals(sessionUserId))) throw new Exception403("권한이 없습니다"); replyRepository.deleteById(id); // 삭제 쿼리를 날리기만 한다 return replyPS.getBoard().getId(); // pc 객체는 남아 있다 }
// TODO -> 2단계 RestAPI 주소 변경 json 돌려주기할때 void로 변경하기 // 규칙 5 -> 삭제는 응답할 데이터가 없다. void를 사용한다 @Transactional public void 댓글삭제(Integer id, Integer sessionUserId) { Reply replyPS = replyRepository.findById(id); // Exception404 if (replyPS == null) throw new Exception404("자원을 찾을 수 없습니다"); if (!replyPS.getUser().getId().equals(sessionUserId)) { throw new Exception403("권한이 없습니다"); } replyRepository.deleteById(id); }
  • 규칙 5 -> 삭제는 응답할 데이터가 없다. void 를 사용한다
notion image

5. 좋아요

1. 댓글쓰기

☕ReplyController

@PostMapping("/reply/save") public String saveReply(@Valid ReplyRequest.SaveDTO reqDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); replyService.댓글등록(reqDTO, sessionUser.getId()); return "redirect:/board/" + reqDTO.getBoardId(); }
@PostMapping("/reply") public @ResponseBody Resp<?> save(@Valid @RequestBody ReplyRequest.SaveDTO reqDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); ReplyResponse.DTO respDTO = replyService.댓글쓰기(reqDTO, sessionUser); return Resp.ok(respDTO); }

☕ReplyService

@Transactional public void 댓글등록(ReplyRequest.SaveDTO reqDTO, Integer sessionUserId) { replyRepository.save(reqDTO.toEntity(sessionUserId)); }
@Transactional public ReplyResponse.DTO 댓글쓰기(ReplyRequest.SaveDTO reqDTO, User sessionUser) { Reply replyPS = replyRepository.save(reqDTO.toEntity(sessionUser)); return new ReplyResponse.DTO(replyPS); }

☕ReplyResponse

@Data public static class DTO { private Integer id; private String content; // 댓글 내용 private Integer userId; private Integer boardId; private String createdAt; public DTO(Reply reply) { this.id = reply.getId(); this.content = reply.getContent(); this.userId = reply.getUser().getId(); this.boardId = reply.getBoard().getId(); this.createdAt = reply.getCreatedAt().toString(); } }

2. 댓글삭제

☕ReplyController

@PostMapping("/reply/{id}/delete") public String deleteReply(@PathVariable Integer id) { User sessionUser = (User) session.getAttribute("sessionUser"); Integer boardId = replyService.댓글삭제(id, sessionUser.getId()); return "redirect:/board/" + boardId; }
@DeleteMapping("/reply/{id}") public @ResponseBody Resp<?> delete(@PathVariable("id") Integer id) { User sessionUser = (User) session.getAttribute("sessionUser"); replyService.댓글삭제(id, sessionUser.getId()); return Resp.ok(null); }
  • 삭제한 후에는 null 을 넣어 return 한다

☕ReplyService

@Transactional public Integer 댓글삭제(Integer id, Integer sessionUserId) { Reply replyPS = replyRepository.findById(id); if (replyPS == null) throw new Exception404("해당 댓글이 없습니다"); if (!(replyPS.getUser().getId().equals(sessionUserId))) throw new Exception403("권한이 없습니다"); replyRepository.deleteById(id); // 삭제 쿼리를 날리기만 한다 return replyPS.getBoard().getId(); // pc 객체는 남아 있다 }
// TODO -> 2단계 RestAPI 주소 변경 json 돌려주기할때 void로 변경하기 // 규칙 5 -> 삭제는 응답할 데이터가 없다. void를 사용한다 @Transactional public void 댓글삭제(Integer id, Integer sessionUserId) { Reply replyPS = replyRepository.findById(id); // Exception404 if (replyPS == null) throw new Exception404("자원을 찾을 수 없습니다"); if (!replyPS.getUser().getId().equals(sessionUserId)) { throw new Exception403("권한이 없습니다"); } replyRepository.deleteById(id); }
  • 규칙 5 -> 삭제는 응답할 데이터가 없다. void 를 사용한다
notion image

Optional 처리⬇

22. 옵셔널(Optional)

1. 유저

UserRepository

findById

public User findById(Integer id) { return em.find(User.class, id); }
// Optional<User> -> 이거 null 일 수 있다. 알려줌 public Optional<User> findById(Integer id) { return Optional.ofNullable(em.find(User.class, id)); }

findByUsername

public User findByUsername(String username) { try { return em.createQuery("select u from User u where u.username = :username", User.class) .setParameter("username", username) .getSingleResult(); } catch (Exception e) { return null; } }
public Optional<User> findByUsername(String username) { try { User userPS = em.createQuery("select u from User u where u.username = :username", User.class) .setParameter("username", username) .getSingleResult(); return Optional.of(userPS); } catch (Exception e) { return Optional.ofNullable(null); } }

UserService

로그인

public User 로그인(UserRequest.LoginDTO loginDTO) { User user = userRepository.findByUsername(loginDTO.getUsername()); if (user == null) throw new Exception401("유저네임 혹은 비밀번호가 틀렸습니다"); if (!user.getPassword().equals(loginDTO.getPassword())) { throw new Exception401("유저네임 혹은 비밀번호가 틀렸습니다"); } return user; }
public User 로그인(UserRequest.LoginDTO loginDTO) { User userPS = userRepository.findByUsername(loginDTO.getUsername()) .orElseThrow(() -> new Exception401("유저네임 혹은 비밀번호가 틀렸습니다")); if (!userPS.getPassword().equals(loginDTO.getPassword())) { throw new Exception401("유저네임 혹은 비밀번호가 틀렸습니다"); } return userPS; }

중복체크

public Map<String, Object> 유저네임중복체크(String username) { User user = userRepository.findByUsername(username); Map<String, Object> dto = new HashMap<>(); if (user == null) { dto.put("available", true); } else { dto.put("available", false); } return dto; }
public Map<String, Object> 유저네임중복체크(String username) { Optional<User> userOP = userRepository.findByUsername(username); Map<String, Object> dto = new HashMap<>(); if (userOP.isPresent()) { dto.put("available", false); } else { dto.put("available", true); } return dto; }

회원정보수정

@Transactional public User 회원정보수정(UserRequest.UpdateDTO updateDTO, Integer userId) { User userPS = userRepository.findById(userId); // Exception404 if (userPS == null) throw new Exception404("자원을 찾을 수 없습니다"); userPS.update(updateDTO.getPassword(), updateDTO.getEmail()); // 영속화된 객체의 상태변경 return userPS; // 리턴한 이유는 세션을 동기화해야해서!! } // 더티체킹 -> 상태가 변경되면 update을 날려요!!
@Transactional public User 회원정보수정(UserRequest.UpdateDTO updateDTO, Integer userId) { User userPS = userRepository.findById(userId) .orElseThrow(() -> new Exception404("자원을 찾을 수 없습니다")); userPS.update(updateDTO.getPassword(), updateDTO.getEmail()); // 영속화된 객체의 상태변경 return userPS; // 리턴한 이유는 세션을 동기화해야해서!! } // 더티체킹 -> 상태가 변경되면 update을 날려요!!

2. 댓글

ReplyRepository

findById

public Reply findById(Integer id) { return em.find(Reply.class, id); }
public Optional<Reply> findById(Integer id) { return Optional.ofNullable(em.find(Reply.class, id)); }

ReplyService

댓글삭제

@Transactional public void 댓글삭제(Integer id, Integer sessionUserId) { Reply replyPS = replyRepository.findById(id); // Exception404 if (replyPS == null) throw new Exception404("자원을 찾을 수 없습니다"); if (!replyPS.getUser().getId().equals(sessionUserId)) { throw new Exception403("권한이 없습니다"); } replyRepository.deleteById(id); }
@Transactional public void 댓글삭제(Integer id, Integer sessionUserId) { Reply replyPS = replyRepository.findById(id) .orElseThrow(() -> new Exception404("자원을 찾을 수 없습니다")); if (!replyPS.getUser().getId().equals(sessionUserId)) { throw new Exception403("권한이 없습니다"); } replyRepository.deleteById(id); }

3. 좋아요

LoveRepository

findByUserIdAndBoardId

public Love findByUserIdAndBoardId(Integer userId, Integer boardId) { Query query = em.createQuery("select lo from Love lo where lo.user.id = :userId and lo.board.id = :boardId", Love.class); query.setParameter("userId", userId); query.setParameter("boardId", boardId); try { return (Love) query.getSingleResult(); } catch (Exception e) { return null; } }
public Optional<Love> findByUserIdAndBoardId(Integer userId, Integer boardId) { Query query = em.createQuery("select lo from Love lo where lo.user.id = :userId and lo.board.id = :boardId", Love.class); query.setParameter("userId", userId); query.setParameter("boardId", boardId); try { return Optional.of((Love) query.getSingleResult()); } catch (Exception e) { return Optional.ofNullable(null); } }

findById

public Love findById(Integer id) { return em.find(Love.class, id); }
public Optional<Love> findById(Integer id) { return Optional.ofNullable(em.find(Love.class, id)); }

LoveService

좋아요취소

@Transactional public LoveResponse.DeleteDTO 좋아요취소(Integer id, Integer sessionUserId) { Love lovePS = loveRepository.findById(id); if (lovePS == null) throw new ExceptionApi404("자원을 찾을 수 없습니다"); if (!lovePS.getUser().getId().equals(sessionUserId)) { throw new ExceptionApi403("권한이 없습니다"); } Integer boardId = lovePS.getBoard().getId(); loveRepository.deleteById(id); Long loveCount = loveRepository.findByBoardId(boardId); return new LoveResponse.DeleteDTO(loveCount.intValue()); }
@Transactional public LoveResponse.DeleteDTO 좋아요취소(Integer id, Integer sessionUserId) { Love lovePS = loveRepository.findById(id) .orElseThrow(() -> new ExceptionApi404("자원을 찾을 수 없습니다")); if (!lovePS.getUser().getId().equals(sessionUserId)) { throw new ExceptionApi403("권한이 없습니다"); } Integer boardId = lovePS.getBoard().getId(); loveRepository.deleteById(id); Long loveCount = loveRepository.findByBoardId(boardId); return new LoveResponse.DeleteDTO(loveCount.intValue()); }

4. 게시글

BoardRepository

findByIdJoinUserAndReplies

public Board findByIdJoinUserAndReplies(Integer id) { Query query = em.createQuery("select b from Board b join fetch b.user left join fetch b.replies r left join fetch r.user where b.id = :id order by r.id desc", Board.class); query.setParameter("id", id); return (Board) query.getSingleResult(); }
public Optional<Board> findByIdJoinUserAndReplies(Integer id) { Query query = em.createQuery("select b from Board b join fetch b.user left join fetch b.replies r left join fetch r.user where b.id = :id order by r.id desc", Board.class); query.setParameter("id", id); try { return Optional.of((Board) query.getSingleResult()); } catch (Exception e) { return Optional.ofNullable(null); } }

findByIdJoinUser

public Board findByIdJoinUser(Integer id) { Query query = em.createQuery("select b from Board b join fetch b.user where b.id = :id", Board.class); query.setParameter("id", id); return (Board) query.getSingleResult(); }
public Optional<Board> findByIdJoinUser(Integer id) { Query query = em.createQuery("select b from Board b join fetch b.user where b.id = :id", Board.class); query.setParameter("id", id); try { return Optional.of((Board) query.getSingleResult()); } catch (Exception e) { return Optional.ofNullable(null); } }

findById

public Board findById(Integer id) { return em.find(Board.class, id); }
public Optional<Board> findById(Integer id) { return Optional.ofNullable(em.find(Board.class, id)); }

BoardService

글수정하기

@Transactional public BoardResponse.DTO 글수정하기(BoardRequest.UpdateDTO reqDTO, Integer boardId, Integer sessionUserId) { Board boardPS = boardRepository.findById(boardId); if (boardPS == null) throw new Exception404("자원을 찾을 수 없습니다"); if (!boardPS.getUser().getId().equals(sessionUserId)) { throw new Exception403("권한이 없습니다"); } boardPS.update(reqDTO.getTitle(), reqDTO.getContent(), reqDTO.getIsPublic()); return new BoardResponse.DTO(boardPS); } // 더티 체킹 (상태 변경해서 update)
@Transactional public BoardResponse.DTO 글수정하기(BoardRequest.UpdateDTO reqDTO, Integer id, Integer sessionUserId) { Board boardPS = boardRepository.findById(id) .orElseThrow(() -> new Exception404("자원을 찾을 수 없습니다")); if (!boardPS.getUser().getId().equals(sessionUserId)) { throw new Exception403("권한이 없습니다"); } boardPS.update(reqDTO.getTitle(), reqDTO.getContent(), reqDTO.getIsPublic()); return new BoardResponse.DTO(boardPS); } // 더티 체킹 (상태 변경해서 update)

글삭제

@Transactional public void 글삭제(Integer id, Integer sessionUserId) { Board boardPS = boardRepository.findById(id); if (boardPS == null) throw new Exception404("자원을 찾을 수 없습니다"); if (!boardPS.getUser().getId().equals(sessionUserId)) { throw new Exception403("권한이 없습니다"); } boardRepository.deleteById(id); }
@Transactional public void 글삭제(Integer id, Integer sessionUserId) { Board boardPS = boardRepository.findById(id) .orElseThrow(() -> new Exception404("자원을 찾을 수 없습니다")); if (!boardPS.getUser().getId().equals(sessionUserId)) { throw new Exception403("권한이 없습니다"); } boardRepository.deleteById(id); }

글상세보기

@Transactional public BoardResponse.DetailDTO 글상세보기(Integer id, Integer userId) { Board boardPS = boardRepository.findByIdJoinUserAndReplies(id); Love love = loveRepository.findByUserIdAndBoardId(userId, id); Long loveCount = loveRepository.findByBoardId(id); Integer loveId = love == null ? null : love.getId(); Boolean isLove = love == null ? false : true; BoardResponse.DetailDTO detailDTO = new BoardResponse.DetailDTO(boardPS, userId, isLove, loveCount.intValue(), loveId); return detailDTO; }
@Transactional public BoardResponse.DetailDTO 글상세보기(Integer id, Integer userId) { Board boardPS = boardRepository.findByIdJoinUserAndReplies(id) .orElseThrow(() -> new Exception404("자원을 찾을 수 없습니다")); Love lovePS = loveRepository.findByUserIdAndBoardId(userId, id) .orElseThrow(() -> new Exception404("자원을 찾을 수 없습니다")); Long loveCount = loveRepository.findByBoardId(id); Integer loveId = lovePS.getId(); Boolean isLoved = lovePS == null ? false : true; BoardResponse.DetailDTO detailDTO = new BoardResponse.DetailDTO(boardPS, userId, isLoved, loveCount.intValue(), loveId); return detailDTO; }

글보기

public BoardResponse.DTO 글보기(int id, Integer sessionUserId) { Board boardPS = boardRepository.findById(id); if (boardPS == null) throw new Exception404("자원을 찾을 수 없습니다"); if (!boardPS.getUser().getId().equals(sessionUserId)) { throw new Exception403("권한이 없습니다"); } return new BoardResponse.DTO(boardPS); }
public BoardResponse.DTO 글보기(int id, Integer sessionUserId) { Board boardPS = boardRepository.findById(id) .orElseThrow(() -> new Exception404("자원을 찾을 수 없습니다")); if (!boardPS.getUser().getId().equals(sessionUserId)) { throw new Exception403("권한이 없습니다"); } return new BoardResponse.DTO(boardPS); }

주소 처리⬇

  • s → 인증 필요
  • api → restAPI 리턴

1. 유저

유저인증관련은 api 주소를 안 붙인다

UserController

// TODO -> JWT 이후에 하기 @PutMapping("/s/api/user") // 인증에 관련된 id 는 주소로 받는게 아닌 session 에서 가져온다 public String update(@Valid @RequestBody UserRequest.UpdateDTO updateDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); User userPS = userService.회원정보수정(updateDTO, sessionUser.getId()); // 토큰도 새로 만들어 줘야 한다 // 세션 동기화 session.setAttribute("sessionUser", userPS); return "redirect:/"; }
@GetMapping("/api/check-username-available/{username}") public @ResponseBody Resp<?> checkUsernameAvailable(@PathVariable("username") String username) { Map<String, Object> dto = userService.유저네임중복체크(username); return Resp.ok(dto); }
@PostMapping("/join") public @ResponseBody Resp<?> join(@Valid @RequestBody UserRequest.JoinDTO reqDTO, Errors errors) { UserResponse.DTO respDTO = userService.회원가입(reqDTO); return Resp.ok(respDTO); }
  • join 은 단독으로 사용 api 를 붙이지 않음
@PostMapping("/login") // password랑 id가 노출되면 안되기 때문에 post로 받는다 public String login(@Valid @RequestBody UserRequest.LoginDTO loginDTO, Errors errors, HttpServletResponse response) { //System.out.println(loginDTO); User sessionUser = userService.로그인(loginDTO); session.setAttribute("sessionUser", sessionUser); if (loginDTO.getRememberMe() == null) { Cookie cookie = new Cookie("username", null); cookie.setMaxAge(0); // 즉시 만료 response.addCookie(cookie); } else { Cookie cookie = new Cookie("username", loginDTO.getUsername()); cookie.setMaxAge(60 * 60 * 24 * 7); response.addCookie(cookie); } return "redirect:/"; }
  • login 은 단독으로 사용 api 를 붙이지 않음
@GetMapping("/logout") public String logout() { session.invalidate(); return "redirect:/login-form"; }
  • logout 은 단독으로 사용 api 를 붙이지 않음

2. 댓글

ReplyController

@DeleteMapping("/s/api/reply/{id}") public @ResponseBody Resp<?> delete(@PathVariable("id") Integer id) { User sessionUser = (User) session.getAttribute("sessionUser"); replyService.댓글삭제(id, sessionUser.getId()); return Resp.ok(null); }
@PostMapping("/s/api/reply") public @ResponseBody Resp<?> save(@Valid @RequestBody ReplyRequest.SaveDTO reqDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); ReplyResponse.DTO respDTO = replyService.댓글쓰기(reqDTO, sessionUser); return Resp.ok(respDTO); }

3. 좋아요

LoveController

@PostMapping("/s/api/love") public Resp<?> saveLove(@RequestBody @Valid LoveRequest.SaveDTO reqDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); LoveResponse.SaveDTO respDTO = loveService.좋아요(reqDTO, sessionUser.getId()); return Resp.ok(respDTO); }
@DeleteMapping("/s/api/love/{id}") public Resp<?> deleteLove(@PathVariable("id") Integer id) { User sessionUser = (User) session.getAttribute("sessionUser"); LoveResponse.DeleteDTO respDTO = loveService.좋아요취소(id, sessionUser.getId()); return Resp.ok(respDTO); }

4. 게시글

BoardController

@PutMapping("/s/api/board/{id}") public @ResponseBody Resp<?> update(@PathVariable("id") Integer id, @Valid @RequestBody BoardRequest.UpdateDTO reqDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); BoardResponse.DTO respDTO = boardService.글수정하기(reqDTO, id, sessionUser.getId()); return Resp.ok(respDTO); }
@GetMapping("/api/board/{id}") // 전달하는 데이터를 보고 주소를 설계함 public @ResponseBody Resp<?> getBoardOne(@PathVariable("id") int id) { User sessionUser = (User) session.getAttribute("sessionUser"); BoardResponse.DTO respDTO = boardService.글보기(id, sessionUser.getId()); return Resp.ok(respDTO); }
@GetMapping("/api/board/{id}/detail") // 전달하는 데이터를 보고 주소를 설계함 public @ResponseBody Resp<?> getBoardDetail(@PathVariable("id") Integer id) { User sessionUser = (User) session.getAttribute("sessionUser"); Integer sessionUserId = (sessionUser == null ? null : sessionUser.getId()); BoardResponse.DetailDTO detailDTO = boardService.글상세보기(id, sessionUserId); return Resp.ok(detailDTO); }
@GetMapping({"/", "/api/board"}) public @ResponseBody Resp<?> list(@RequestParam(required = false, value = "page", defaultValue = "0") Integer page, @RequestParam(required = false, value = "keyword", defaultValue = "") String keyword) { User sessionUser = (User) session.getAttribute("sessionUser"); BoardResponse.ListDTO respDTO; if (sessionUser == null) { respDTO = boardService.글목록보기(null, page, keyword); } else { respDTO = boardService.글목록보기(sessionUser.getId(), page, keyword); } return Resp.ok(respDTO); }
  • 메인 화면은 2가지 주소를 주어 사용할 수 있다
@PostMapping("/s/api/board") public @ResponseBody Resp<?> save(@Valid @RequestBody BoardRequest.SaveDTO reqDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); BoardResponse.DTO respDTO = boardService.글쓰기(reqDTO, sessionUser); return Resp.ok(respDTO); }

인터셉터 수정

package shop.mtcoding.blog._core.interceptor; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import org.springframework.web.servlet.HandlerInterceptor; import shop.mtcoding.blog._core.error.ex.ExceptionApi401; import shop.mtcoding.blog.user.User; public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String uri = request.getRequestURI(); System.out.println("uri: " + uri); HttpSession session = request.getSession(); User sessionUser = (User) session.getAttribute("sessionUser"); if (uri.startsWith("/s") && sessionUser == null) { throw new ExceptionApi401("인증이 필요합니다"); } return true; } }
  • /s 로 시작하는 주소는 인증이 필요하기 때문에 변경

공통 레포지토리 만드는 법

Share article

jjack1