diff --git a/README.md b/README.md index cd28b96c4..768f691c4 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ - [x] 게시글 목록 구현하기 - [x] 게시글 상세보기 구현하기 - [x] 사용자 정보 DB에 저장 -- [ ] 배포 +- [x] 배포 ### 4단계 @@ -33,3 +33,19 @@ - [x] 세션을 통한 로그인 검증 - [x] 로그인된 유저만이 각 작성한 게시글 수정 및 삭제 기능 +### 6단계 + +- [x] 게시글을 볼 때, 댓글까지 함께 표시 +- [x] 로그인한 유저는 댓글 작성 가능 +- [x] 자신의 작성한 댓글만 삭제 가능 +- [x] 다른 유저의 댓글이 포함되어있는 게시글은 삭제 불가능하도록 구현 +- [ ] 댓글 수정기능 + +> 댓글 수정 기능의 경우, softDeletion 혹은 수정하는 상태에 대한 필드를 추가해서 쿼리 및 로직을 수정해야 깔끔하게 구현할 수 있을 듯하다. + +## 테스트 + +스크린샷 2022-07-28 오후 6 41 21 + +스크린샷 2022-07-28 오후 6 45 58 + diff --git a/src/main/java/com/kakao/cafe/config/MvcConfig.java b/src/main/java/com/kakao/cafe/config/MvcConfig.java index 7c8ddc6f0..2706d6001 100644 --- a/src/main/java/com/kakao/cafe/config/MvcConfig.java +++ b/src/main/java/com/kakao/cafe/config/MvcConfig.java @@ -12,8 +12,11 @@ public class MvcConfig implements WebMvcConfigurer { public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .order(1) - .addPathPatterns("/users/{id}", "/users/{id}/update", - "/qna/write-qna", "/qna/show/{id}", - "qna/update/{id}", "qna/delete/{id}" ); + .addPathPatterns("/users/{id}", "/users/{id}/update") + .addPathPatterns("/qna/write-qna", "/qna/show/{id}") + .addPathPatterns("qna/update/{id}", "qna/delete/{id}") + .addPathPatterns("/qna/{articleId}/reply/write", "/qna/{articleId}/reply/{id}/delete"); + + } } diff --git a/src/main/java/com/kakao/cafe/domain/article/Article.java b/src/main/java/com/kakao/cafe/domain/article/Article.java index 921575f4e..f730f2a0a 100644 --- a/src/main/java/com/kakao/cafe/domain/article/Article.java +++ b/src/main/java/com/kakao/cafe/domain/article/Article.java @@ -12,22 +12,23 @@ public class Article { private final String contents; private final LocalDateTime writtenTime; - public Article(Integer id, String writer, String title, String contents) { + private Article(Integer id, String writer, String title, String contents, LocalDateTime writtenTime) { this.id = id; this.writer = writer; this.title = title; this.contents = contents; - this.writtenTime = LocalDateTime.now(); + this.writtenTime = writtenTime; } - public Article(Integer id, String writer, String title, String contents, LocalDateTime writtenTime) { - this.id = id; - this.writer = writer; - this.title = title; - this.contents = contents; - this.writtenTime = writtenTime; + public static Article newInstance(Integer id, String writer, String title, String contents) { + return new Article(id, writer, title, contents, LocalDateTime.now()); + } + + public static Article of(Integer id, String writer, String title, String contents, LocalDateTime writtenTime){ + return new Article(id, writer, title, contents, writtenTime); } + public boolean hasId() { return this.id != null; } @@ -61,7 +62,7 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Article article = (Article) o; - return Objects.equals(getId(), article.getId()); + return this.id.equals(article.getId()); } @Override diff --git a/src/main/java/com/kakao/cafe/domain/article/JdbcTemplateArticleRepository.java b/src/main/java/com/kakao/cafe/domain/article/JdbcTemplateArticleRepository.java index b49c32530..5e24dbb43 100644 --- a/src/main/java/com/kakao/cafe/domain/article/JdbcTemplateArticleRepository.java +++ b/src/main/java/com/kakao/cafe/domain/article/JdbcTemplateArticleRepository.java @@ -62,15 +62,11 @@ public void clear() { @Override public boolean deleteOne(Integer id) { String sql = "delete from cafe_article where id = :id"; - int result = jdbcTemplate.update(sql, new MapSqlParameterSource("id", id)); - if(result == 1) { - return true; - } - return false; + return jdbcTemplate.update(sql, new MapSqlParameterSource("id", id)) == 1; } private RowMapper
articleRowMapper() { - return (rs, rowNum) -> new Article(rs.getInt("id"), + return (rs, rowNum) -> Article.of(rs.getInt("id"), rs.getString("writer"), rs.getString("title"), rs.getString("contents"), diff --git a/src/main/java/com/kakao/cafe/domain/reply/JdbcTemplateReplyRepository.java b/src/main/java/com/kakao/cafe/domain/reply/JdbcTemplateReplyRepository.java new file mode 100644 index 000000000..97d553a7f --- /dev/null +++ b/src/main/java/com/kakao/cafe/domain/reply/JdbcTemplateReplyRepository.java @@ -0,0 +1,88 @@ +package com.kakao.cafe.domain.reply; + +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.*; + +@Repository +public class JdbcTemplateReplyRepository implements ReplyRepository{ + + private final NamedParameterJdbcTemplate jdbcTemplate; + + public JdbcTemplateReplyRepository(NamedParameterJdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public Reply save(Reply reply) { + if (!reply.hasId()) { + return insert(reply); + } + return update(reply); + } + + @Override + public Optional findWriterById(Integer id) { + String sql = "select writer from cafe_reply where id = :id"; + try { + String writer = jdbcTemplate.queryForObject(sql, new MapSqlParameterSource("id", id), String.class); + return Optional.ofNullable(writer); + + } catch (EmptyResultDataAccessException e) { + return Optional.empty(); + } + } + + @Override + public List findAllByArticleId(Integer articleId) { + String sql ="select id, articleId, writer, contents, writtenTime from cafe_reply where articleId = :articleId"; + return jdbcTemplate.query(sql, new MapSqlParameterSource("articleId", articleId), replyRowMapper()); + } + + @Override + public boolean deleteOne(Integer id) { + String sql = "delete from cafe_reply where id = :id"; + return jdbcTemplate.update(sql, new MapSqlParameterSource("id", id)) == 1; + } + + @Override + public boolean hasReplyOfAnotherWriter(Integer articleId, String writer) { + String sql = "select count(id) from cafe_reply where articleId = :articleId and writer != :writer"; + + MapSqlParameterSource mapSqlParameterSource = new MapSqlParameterSource(); + mapSqlParameterSource.addValue("articleId", articleId); + mapSqlParameterSource.addValue("writer", writer); + + return jdbcTemplate.queryForObject(sql, mapSqlParameterSource, Integer.class) > 0; + } + + private Reply insert(Reply reply) { + KeyHolder keyHolder = new GeneratedKeyHolder(); + String sql = "insert into cafe_reply (articleId, writer, contents, writtenTime) values (:articleId, :writer, :contents, :writtenTime);"; + jdbcTemplate.update(sql, new BeanPropertySqlParameterSource(reply), keyHolder); + reply.setId(Objects.requireNonNull(keyHolder.getKey()).intValue()); + return reply; + } + + private Reply update(Reply reply) { + String sql = "update cafe_reply set contents = :contents where id = :id"; + jdbcTemplate.update(sql, new BeanPropertySqlParameterSource(reply)); + return reply; + } + + private RowMapper replyRowMapper() { + return (rs, rowNum) -> Reply.of(rs.getInt("id"), + rs.getInt("articleId"), + rs.getString("writer"), + rs.getString("contents"), + rs.getObject("writtenTime", LocalDateTime.class)); + } +} diff --git a/src/main/java/com/kakao/cafe/domain/reply/Reply.java b/src/main/java/com/kakao/cafe/domain/reply/Reply.java new file mode 100644 index 000000000..cf7340a09 --- /dev/null +++ b/src/main/java/com/kakao/cafe/domain/reply/Reply.java @@ -0,0 +1,69 @@ +package com.kakao.cafe.domain.reply; + +import java.time.LocalDateTime; +import java.util.Objects; + +public class Reply { + + private Integer id; + private final Integer ArticleId; + private final String writer; + private final String contents; + private final LocalDateTime writtenTime; + + private Reply(Integer id, Integer articleId, String writer, String contents, LocalDateTime writtenTime) { + this.id = id; + ArticleId = articleId; + this.writer = writer; + this.contents = contents; + this.writtenTime = writtenTime; + } + public static Reply newInstance(Integer articleId, String writer, String contents) { + return new Reply(null, articleId, writer, contents, LocalDateTime.now()); + } + + public static Reply of(Integer id, Integer articleId, String writer, String contents, LocalDateTime writtenTime) { + return new Reply(id, articleId, writer, contents, writtenTime); + } + + public boolean hasId() { + return id != null; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getArticleId() { + return ArticleId; + } + + public String getWriter() { + return writer; + } + + public String getContents() { + return contents; + } + + public LocalDateTime getWrittenTime() { + return writtenTime; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Reply reply = (Reply) o; + return getId().equals(reply.getId()); + } + + @Override + public int hashCode() { + return Objects.hash(getId()); + } +} diff --git a/src/main/java/com/kakao/cafe/domain/reply/ReplyRepository.java b/src/main/java/com/kakao/cafe/domain/reply/ReplyRepository.java new file mode 100644 index 000000000..30d3b6660 --- /dev/null +++ b/src/main/java/com/kakao/cafe/domain/reply/ReplyRepository.java @@ -0,0 +1,13 @@ +package com.kakao.cafe.domain.reply; + +import java.util.List; +import java.util.Optional; + +public interface ReplyRepository { + + Reply save(Reply reply); + Optional findWriterById(Integer id); + List findAllByArticleId(Integer articleId); + boolean deleteOne(Integer id); + boolean hasReplyOfAnotherWriter(Integer articleId, String writer); +} diff --git a/src/main/java/com/kakao/cafe/domain/user/User.java b/src/main/java/com/kakao/cafe/domain/user/User.java index b6cde7d8f..f266bec45 100644 --- a/src/main/java/com/kakao/cafe/domain/user/User.java +++ b/src/main/java/com/kakao/cafe/domain/user/User.java @@ -47,7 +47,7 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; - return Objects.equals(getUserId(), user.getUserId()) && Objects.equals(getPassword(), user.getPassword()); + return getUserId().equals(user.getUserId()) && getPassword().equals(user.getPassword()); } @Override diff --git a/src/main/java/com/kakao/cafe/service/ArticleService.java b/src/main/java/com/kakao/cafe/service/ArticleService.java index 239855514..ff296e98a 100644 --- a/src/main/java/com/kakao/cafe/service/ArticleService.java +++ b/src/main/java/com/kakao/cafe/service/ArticleService.java @@ -5,6 +5,7 @@ import com.kakao.cafe.exception.ClientException; import com.kakao.cafe.web.dto.ArticleDto; import com.kakao.cafe.web.dto.ArticleResponseDto; +import com.kakao.cafe.web.dto.ArticleUpdateDto; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; @@ -42,13 +43,12 @@ public void clearRepository() { public boolean deleteOne(Integer articleId, String writer) { checkAuthorized(articleId, writer); - boolean deleted = articleRepository.deleteOne(articleId); - return deleted; + return articleRepository.deleteOne(articleId); } - public Article updateOne(String userId, Integer articleId, ArticleDto articleDto) { - checkAuthorized(articleId, userId); - return articleRepository.save(articleDto.toUpdateEntity(articleId, userId)); + public Article updateOne(String userId, ArticleUpdateDto articleUpdateDto) { + checkAuthorized(articleUpdateDto.getId(), userId); + return articleRepository.save(articleUpdateDto.toEntityWithWriter(userId)); } private void checkAuthorized(Integer articleId, String writer) { diff --git a/src/main/java/com/kakao/cafe/service/ReplyService.java b/src/main/java/com/kakao/cafe/service/ReplyService.java new file mode 100644 index 000000000..140d25439 --- /dev/null +++ b/src/main/java/com/kakao/cafe/service/ReplyService.java @@ -0,0 +1,61 @@ +package com.kakao.cafe.service; + +import com.kakao.cafe.domain.reply.Reply; +import com.kakao.cafe.domain.reply.ReplyRepository; +import com.kakao.cafe.exception.ClientException; +import com.kakao.cafe.web.dto.ReplyResponseDto; +import com.kakao.cafe.web.dto.ReplyUpdateDto; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class ReplyService { + + private final ReplyRepository replyRepository; + + public ReplyService(ReplyRepository replyRepository) { + this.replyRepository = replyRepository; + } + + public ReplyResponseDto write(Integer articleId, String writer, String contents) { + Reply reply = replyRepository.save(Reply.newInstance(articleId, writer, contents)); + return ReplyResponseDto.from(reply); + } + + public List showAllInArticle(Integer articleId) { + return replyRepository.findAllByArticleId(articleId) + .stream() + .map(ReplyResponseDto::from) + .collect(Collectors.toList()); + } + + public boolean delete(String writer, Integer id) { + if(checkWriter(writer, id)) { + return replyRepository.deleteOne(id); + } + throw new ClientException(HttpStatus.FORBIDDEN, "접근 권한이 없습니다."); + } + + public boolean isDeletableArticle(Integer articleId, String writer) { + return !replyRepository.hasReplyOfAnotherWriter(articleId, writer); + } + + public ReplyResponseDto update(ReplyUpdateDto replyUpdateDto, String writer) { + if(!replyUpdateDto.hasSameWriter(writer)) { + throw new ClientException(HttpStatus.FORBIDDEN, "접근 권한이 없습니다."); + }; + return ReplyResponseDto.from(replyRepository.save(replyUpdateDto.toEntity())); + } + + private boolean checkWriter(String writer, Integer id) { + String targetWriter = replyRepository.findWriterById(id) + .orElseThrow(() -> new ClientException(HttpStatus.NOT_FOUND, "댓글을 찾을 수 없습니다.")); + + return targetWriter.equals(writer); + } + + +} diff --git a/src/main/java/com/kakao/cafe/service/UserService.java b/src/main/java/com/kakao/cafe/service/UserService.java index 667e376f2..ab470cb9f 100644 --- a/src/main/java/com/kakao/cafe/service/UserService.java +++ b/src/main/java/com/kakao/cafe/service/UserService.java @@ -4,6 +4,7 @@ import com.kakao.cafe.domain.user.UserRepository; import com.kakao.cafe.exception.ClientException; import com.kakao.cafe.web.dto.LoginDto; +import com.kakao.cafe.web.dto.SessionUser; import com.kakao.cafe.web.dto.UserDto; import com.kakao.cafe.web.dto.UserResponseDto; import org.springframework.beans.factory.annotation.Autowired; @@ -58,10 +59,10 @@ public User updateUserInfo(UserDto userDto) { return user; } - public Optional login(LoginDto loginDto) { + public Optional login(LoginDto loginDto) { return userRepository.findById(loginDto.getUserId()) .filter(user -> user.isSamePassword(loginDto.getPassword())) - .map(UserResponseDto::new); + .map(SessionUser::from); } public void logout(HttpSession httpSession) { diff --git a/src/main/java/com/kakao/cafe/web/ArticleController.java b/src/main/java/com/kakao/cafe/web/ArticleController.java index a600ed555..1bd72bc09 100644 --- a/src/main/java/com/kakao/cafe/web/ArticleController.java +++ b/src/main/java/com/kakao/cafe/web/ArticleController.java @@ -1,12 +1,9 @@ package com.kakao.cafe.web; - -import com.kakao.cafe.constants.LoginConstants; import com.kakao.cafe.exception.ClientException; import com.kakao.cafe.service.ArticleService; -import com.kakao.cafe.web.dto.ArticleDto; -import com.kakao.cafe.web.dto.ArticleResponseDto; -import com.kakao.cafe.web.dto.UserResponseDto; +import com.kakao.cafe.service.ReplyService; +import com.kakao.cafe.web.dto.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; @@ -15,6 +12,7 @@ import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpSession; +import java.util.List; @Controller @RequestMapping("/qna") @@ -23,9 +21,11 @@ public class ArticleController { private final Logger logger = LoggerFactory.getLogger(ArticleController.class); private final ArticleService articleService; + private final ReplyService replyService; - public ArticleController(ArticleService articleService) { + public ArticleController(ArticleService articleService, ReplyService replyService) { this.articleService = articleService; + this.replyService = replyService; } @GetMapping("/write-qna") @@ -37,10 +37,10 @@ public String writeForm() { @PostMapping("/write-qna") public String write(ArticleDto articleDto, HttpSession httpSession) { - UserResponseDto sessionedUser = (UserResponseDto) httpSession.getAttribute(LoginConstants.SESSIONED_USER); - String sessionedUserUserId = sessionedUser.getUserId(); - logger.info("[{}] writing qna{}", sessionedUserUserId, articleDto); - articleService.write(sessionedUserUserId, articleDto); + SessionUser sessionedUser = SessionUser.from(httpSession); + String sessionedUserId = sessionedUser.getUserId(); + logger.info("[{}] writing qna{}", sessionedUserId, articleDto); + articleService.write(sessionedUserId, articleDto); return "redirect:/qna/all"; } @@ -55,30 +55,37 @@ public String showAll(Model model) { @GetMapping("/show/{id}") public String showArticle(@PathVariable Integer id, Model model) { - logger.info("Search for articleId{} to show client", id); + logger.info("Search for articleId[{}] to show client", id); ArticleResponseDto result = articleService.findOne(id); model.addAttribute("article", result); - logger.info("Show article{}", result); + + List replyResponseDtos = replyService.showAllInArticle(id); + model.addAttribute("size", replyResponseDtos.size()); + if(!replyResponseDtos.isEmpty()) { + model.addAttribute("replies", replyResponseDtos); + } + + logger.info("Show article[{}] with [{}] replies ", result, replyResponseDtos.size()); return "qna/show"; } @DeleteMapping("/delete/{id}") public String deleteArticle(@PathVariable Integer id, HttpSession httpSession) { - UserResponseDto sessionedUser = (UserResponseDto) httpSession.getAttribute(LoginConstants.SESSIONED_USER); - String sessionedUserUserId = sessionedUser.getUserId(); - - articleService.deleteOne(id, sessionedUserUserId); - logger.info("[{}] delete qna{}", sessionedUserUserId, id); + SessionUser sessionedUser = SessionUser.from(httpSession); + String sessionedUserId = sessionedUser.getUserId(); + checkDeletable(id, sessionedUserId); + articleService.deleteOne(id, sessionedUserId); + logger.info("[{}] delete qna[{}]", sessionedUserId, id); return "redirect:/qna/all"; } @GetMapping("/update/{id}") public String updateForm(@PathVariable Integer id, HttpSession httpSession, Model model) { - UserResponseDto sessionedUser = (UserResponseDto) httpSession.getAttribute(LoginConstants.SESSIONED_USER); - logger.info("[{}] request updateForm qna{}", sessionedUser.getUserId(), id); + SessionUser sessionedUser = SessionUser.from(httpSession); + logger.info("[{}] request updateForm qna[{}]", sessionedUser.getUserId(), id); ArticleResponseDto result = articleService.findOne(id); checkAccessPermission(result, sessionedUser); @@ -88,20 +95,28 @@ public String updateForm(@PathVariable Integer id, HttpSession httpSession, Mode } @PutMapping("/update/{id}") - public String updateArticle(@PathVariable Integer id, ArticleDto articleDto, HttpSession httpSession) { - UserResponseDto sessionedUser = (UserResponseDto) httpSession.getAttribute(LoginConstants.SESSIONED_USER); - articleService.updateOne(sessionedUser.getUserId(), id, articleDto); + public String updateArticle(@PathVariable Integer id, ArticleUpdateDto articleUpdateDto, HttpSession httpSession) { + SessionUser sessionedUser = SessionUser.from(httpSession); + articleService.updateOne(sessionedUser.getUserId(), articleUpdateDto); + logger.info("[{}] update qna[{}]", sessionedUser.getUserId(), id); return "redirect:/qna/show/" + id; } // session정보와 pathArticleId 확인 - private void checkAccessPermission(ArticleResponseDto articleResponseDto, UserResponseDto sessionedUser) { + private void checkAccessPermission(ArticleResponseDto articleResponseDto, SessionUser sessionedUser) { String writer = articleResponseDto.getWriter(); Integer id = articleResponseDto.getId(); if(!sessionedUser.hasSameId(writer)){ - logger.info("[{}] tries access [{}]'s article[{}]", sessionedUser.getUserId(), writer, id); + logger.error("[{}] tries access [{}]'s article[{}]", sessionedUser.getUserId(), writer, id); throw new ClientException(HttpStatus.FORBIDDEN, "접근 권한이 없습니다."); } } + + private void checkDeletable(Integer id, String userId) { + if(!replyService.isDeletableArticle(id, userId)) { + logger.error("[{}] failed to delete article[{}] with other user's replies", userId, id); + throw new ClientException(HttpStatus.CONFLICT, "다른 유저의 답변이 포함되어 있어서 질문을 삭제할 수 없습니다."); + } + } } diff --git a/src/main/java/com/kakao/cafe/web/ReplyController.java b/src/main/java/com/kakao/cafe/web/ReplyController.java new file mode 100644 index 000000000..cf7af1e35 --- /dev/null +++ b/src/main/java/com/kakao/cafe/web/ReplyController.java @@ -0,0 +1,53 @@ +package com.kakao.cafe.web; + +import com.kakao.cafe.service.ReplyService; +import com.kakao.cafe.web.dto.ReplyResponseDto; +import com.kakao.cafe.web.dto.ReplyUpdateDto; +import com.kakao.cafe.web.dto.SessionUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpSession; + +@Controller +@RequestMapping("/qna/{articleId}/reply") +public class ReplyController { + + private final Logger logger = LoggerFactory.getLogger(ReplyController.class); + + private final ReplyService replyService; + + public ReplyController(ReplyService replyService) { + this.replyService = replyService; + } + + @PostMapping("/write") + public String write(@PathVariable Integer articleId, String contents, HttpSession httpSession) { + SessionUser sessionedUser = SessionUser.from(httpSession); + String sessionedUserId = sessionedUser.getUserId(); + logger.info("[{}] write reply[{}] in [article{}]", sessionedUserId, contents, articleId); + + replyService.write(articleId, sessionedUserId, contents); + + return "redirect:/qna/show/" + articleId; + } + + @DeleteMapping("/{id}/delete") + public String delete(@PathVariable Integer articleId, @PathVariable Integer id, HttpSession httpSession) { + SessionUser sessionedUser = SessionUser.from(httpSession); + replyService.delete(sessionedUser.getUserId(), id); + logger.info("[{}] delete reply[{}] in [article{}]", sessionedUser.getUserId(), id, articleId); + + return "redirect:/qna/show/" + articleId; + } + + @PostMapping("/{id}/update") + public String update(ReplyUpdateDto replyUpdateDto, HttpSession httpSession) { + SessionUser sessionedUser = SessionUser.from(httpSession); + replyService.update(replyUpdateDto, sessionedUser.getUserId()); + + return "redirect:/qna/show/" + replyUpdateDto.getArticleId(); + } +} diff --git a/src/main/java/com/kakao/cafe/web/UserController.java b/src/main/java/com/kakao/cafe/web/UserController.java index 68ef5558e..61164fdde 100644 --- a/src/main/java/com/kakao/cafe/web/UserController.java +++ b/src/main/java/com/kakao/cafe/web/UserController.java @@ -4,6 +4,7 @@ import com.kakao.cafe.exception.ClientException; import com.kakao.cafe.service.UserService; import com.kakao.cafe.web.dto.LoginDto; +import com.kakao.cafe.web.dto.SessionUser; import com.kakao.cafe.web.dto.UserDto; import com.kakao.cafe.web.dto.UserResponseDto; import org.slf4j.Logger; @@ -71,7 +72,7 @@ public String showProfile(@PathVariable String id, Model model) { @GetMapping("/users/{id}/update") public String updateForm(@PathVariable String id, Model model, HttpSession httpSession) { - UserResponseDto sessionedUser = (UserResponseDto) httpSession.getAttribute(LoginConstants.SESSIONED_USER); + SessionUser sessionedUser = (SessionUser) httpSession.getAttribute(LoginConstants.SESSIONED_USER); checkAccessPermission(id, sessionedUser); logger.info("[{}] in updateForm for update info", id); @@ -82,7 +83,7 @@ public String updateForm(@PathVariable String id, Model model, HttpSession httpS @PutMapping("/users/{id}/update") public String updateInfo(@PathVariable String id, UserDto userDto, HttpSession httpSession) { - UserResponseDto sessionedUser = (UserResponseDto) httpSession.getAttribute(LoginConstants.SESSIONED_USER); + SessionUser sessionedUser = (SessionUser) httpSession.getAttribute(LoginConstants.SESSIONED_USER); checkAccessPermission(id, sessionedUser); logger.info("[{}] updated info [{}]", id, userDto); @@ -101,7 +102,7 @@ public String loginForm() { @PostMapping("/user/login") public String loginUser(LoginDto loginDto, HttpSession httpSession, HttpServletResponse httpServletResponse, RedirectAttributes redirectAttributes) { logger.info("[{}] request login", loginDto.getUserId()); - Optional loginUserOptional = userService.login(loginDto); + Optional loginUserOptional = userService.login(loginDto); if(loginUserOptional.isPresent()) { logger.info("[{}] succeded login ", loginDto.getUserId()); @@ -123,9 +124,9 @@ public String logout(HttpSession httpSession) { } // session정보와 pathID 확인 - private void checkAccessPermission(String id, UserResponseDto sessionedUser) { + private void checkAccessPermission(String id, SessionUser sessionedUser) { if(!sessionedUser.hasSameId(id)){ - logger.info("[{}] tries access [{}]'s info", sessionedUser.getUserId(), id); + logger.error("[{}] tries access [{}]'s info", sessionedUser.getUserId(), id); throw new ClientException(HttpStatus.FORBIDDEN, "접근 권한이 없습니다."); } } diff --git a/src/main/java/com/kakao/cafe/web/dto/ArticleDto.java b/src/main/java/com/kakao/cafe/web/dto/ArticleDto.java index fe2c8f482..d990873f4 100644 --- a/src/main/java/com/kakao/cafe/web/dto/ArticleDto.java +++ b/src/main/java/com/kakao/cafe/web/dto/ArticleDto.java @@ -26,24 +26,7 @@ public String getContents() { } public Article toEntityWithWriter(String writer) { - return new Article(null, writer, this.title, this.contents); - } - - public Article toUpdateEntity(Integer articleId, String writer) { - return new Article(articleId, writer, this.title, this.contents); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ArticleDto that = (ArticleDto) o; - return Objects.equals(getTitle(), that.getTitle()) && Objects.equals(getContents(), that.getContents()); - } - - @Override - public int hashCode() { - return Objects.hash(getTitle(), getContents()); + return Article.newInstance(null, writer, this.title, this.contents); } @Override diff --git a/src/main/java/com/kakao/cafe/web/dto/ArticleResponseDto.java b/src/main/java/com/kakao/cafe/web/dto/ArticleResponseDto.java index 814c68494..672c9b8b8 100644 --- a/src/main/java/com/kakao/cafe/web/dto/ArticleResponseDto.java +++ b/src/main/java/com/kakao/cafe/web/dto/ArticleResponseDto.java @@ -50,12 +50,12 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ArticleResponseDto that = (ArticleResponseDto) o; - return Objects.equals(getId(), that.getId()) && Objects.equals(getWriter(), that.getWriter()) && Objects.equals(getTitle(), that.getTitle()) && Objects.equals(getContents(), that.getContents()) && Objects.equals(getWrittenTime(), that.getWrittenTime()); + return getId().equals(that.getId()) && getWriter().equals(that.getWriter()) && getTitle().equals(that.getTitle()) && getContents().equals(that.getContents()) && getWrittenTime().equals(that.getWrittenTime()); } @Override public int hashCode() { - return Objects.hash(id, writer, title, contents, writtenTime); + return Objects.hash(getId(), getWriter(), getTitle(), getContents(), getWrittenTime()); } @Override diff --git a/src/main/java/com/kakao/cafe/web/dto/ArticleUpdateDto.java b/src/main/java/com/kakao/cafe/web/dto/ArticleUpdateDto.java new file mode 100644 index 000000000..47e514209 --- /dev/null +++ b/src/main/java/com/kakao/cafe/web/dto/ArticleUpdateDto.java @@ -0,0 +1,32 @@ +package com.kakao.cafe.web.dto; + +import com.kakao.cafe.domain.article.Article; + +public class ArticleUpdateDto { + + private final Integer id; + private final String title; + private final String contents; + + public ArticleUpdateDto(Integer id, String title, String contents) { + this.id = id; + this.title = title; + this.contents = contents; + } + + public Article toEntityWithWriter(String writer) { + return Article.newInstance(this.id, writer, this.title, this.contents); + } + + public Integer getId() { + return id; + } + + public String getTitle() { + return title; + } + + public String getContents() { + return contents; + } +} diff --git a/src/main/java/com/kakao/cafe/web/dto/ReplyResponseDto.java b/src/main/java/com/kakao/cafe/web/dto/ReplyResponseDto.java new file mode 100644 index 000000000..e4a8964cc --- /dev/null +++ b/src/main/java/com/kakao/cafe/web/dto/ReplyResponseDto.java @@ -0,0 +1,46 @@ +package com.kakao.cafe.web.dto; + +import com.kakao.cafe.domain.reply.Reply; + +import java.time.LocalDateTime; + +public class ReplyResponseDto { + + private final Integer id; + private final Integer articleId; + private final String writer; + private final String contents; + private final LocalDateTime writtenTime; + + public static ReplyResponseDto from(Reply reply) { + return new ReplyResponseDto(reply.getId(), reply.getArticleId(), reply.getWriter(), reply.getContents(), reply.getWrittenTime()); + } + + public ReplyResponseDto(Integer id, Integer articleId, String writer, String contents, LocalDateTime writtenTime) { + this.id = id; + this.articleId = articleId; + this.writer = writer; + this.contents = contents; + this.writtenTime = writtenTime; + } + + public Integer getId() { + return id; + } + + public Integer getArticleId() { + return articleId; + } + + public String getWriter() { + return writer; + } + + public String getContents() { + return contents; + } + + public LocalDateTime getWrittenTime() { + return writtenTime; + } +} diff --git a/src/main/java/com/kakao/cafe/web/dto/ReplyUpdateDto.java b/src/main/java/com/kakao/cafe/web/dto/ReplyUpdateDto.java new file mode 100644 index 000000000..4fdfec5cf --- /dev/null +++ b/src/main/java/com/kakao/cafe/web/dto/ReplyUpdateDto.java @@ -0,0 +1,50 @@ +package com.kakao.cafe.web.dto; + +import com.kakao.cafe.domain.reply.Reply; + +import java.time.LocalDateTime; + +public class ReplyUpdateDto { + + private final Integer id; + private final Integer articleId; + private final String writer; + private final String contents; + private final LocalDateTime writtenTime; + + public ReplyUpdateDto(Integer id, Integer articleId, String writer, String contents, LocalDateTime writtenTime) { + this.id = id; + this.articleId = articleId; + this.writer = writer; + this.contents = contents; + this.writtenTime = writtenTime; + } + + public Reply toEntity() { + return Reply.of(this.id, this.articleId, this.writer, this.contents, this.writtenTime); + } + + public boolean hasSameWriter(String writer) { + return this.writer.equals(writer); + } + + public Integer getId() { + return id; + } + + public Integer getArticleId() { + return articleId; + } + + public String getWriter() { + return writer; + } + + public String getContents() { + return contents; + } + + public LocalDateTime getWrittenTime() { + return writtenTime; + } +} diff --git a/src/main/java/com/kakao/cafe/web/dto/SessionUser.java b/src/main/java/com/kakao/cafe/web/dto/SessionUser.java new file mode 100644 index 000000000..0e437d99c --- /dev/null +++ b/src/main/java/com/kakao/cafe/web/dto/SessionUser.java @@ -0,0 +1,58 @@ +package com.kakao.cafe.web.dto; + +import com.kakao.cafe.constants.LoginConstants; +import com.kakao.cafe.domain.user.User; + +import javax.servlet.http.HttpSession; +import java.util.Objects; +import java.util.Optional; + +public class SessionUser { + + private final String userId; + private final String name; + private final String email; + + public static SessionUser from(User user) { + return new SessionUser(user.getUserId(), user.getName(), user.getEmail()); + } + + public static SessionUser from(HttpSession httpSession) { + return (SessionUser) httpSession.getAttribute(LoginConstants.SESSIONED_USER); + } + + public SessionUser(String userId, String name, String email) { + this.userId = userId; + this.name = name; + this.email = email; + } + + public boolean hasSameId(String userId) { + return this.userId.equals(userId); + } + + public String getUserId() { + return userId; + } + + public String getName() { + return name; + } + + public String getEmail() { + return email; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SessionUser that = (SessionUser) o; + return getUserId().equals(that.getUserId()) && getName().equals(that.getName()) && getEmail().equals(that.getEmail()); + } + + @Override + public int hashCode() { + return Objects.hash(getUserId(), getName(), getEmail()); + } +} diff --git a/src/main/java/com/kakao/cafe/web/dto/UserResponseDto.java b/src/main/java/com/kakao/cafe/web/dto/UserResponseDto.java index beff45d38..01a7fdc2b 100644 --- a/src/main/java/com/kakao/cafe/web/dto/UserResponseDto.java +++ b/src/main/java/com/kakao/cafe/web/dto/UserResponseDto.java @@ -14,10 +14,6 @@ public UserResponseDto(User user) { this.email = user.getEmail(); } - public boolean hasSameId(String userId) { - return this.userId.equals(userId); - } - public String getUserId() { return userId; } diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 000000000..1c00cc1e1 --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,2 @@ +insert into cafe_article (writer, title, contents, writtenTime) values ('writer1', 'title1', 'contents1', '2022-03-26 12:30'); +insert into cafe_article (writer, title, contents, writtenTime) values ('writer2', 'title2', 'contents2', '2022-03-26 12:31'); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index c1a95f08b..5475d3a6c 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -14,3 +14,13 @@ create table cafe_article ( writtenTime timestamp ); +create table cafe_reply ( + id int auto_increment, + articleId int, + writer varchar(255), + contents varchar(255), + writtenTime timestamp, + primary key (id), + foreign key (articleId) references cafe_article (id) on delete cascade +) + diff --git a/src/main/resources/templates/qna/show.html b/src/main/resources/templates/qna/show.html index e76f4a431..ad114d3b8 100644 --- a/src/main/resources/templates/qna/show.html +++ b/src/main/resources/templates/qna/show.html @@ -43,63 +43,33 @@

{{title}}

{{/article}} -
-

2개의 의견

+

{{size}}개의 의견

- + {{#replies}}
- - - 2016-01-12 14:06 - -
-
-
-

이 글만으로는 원인 파악하기 힘들겠다. 소스 코드와 설정을 단순화해서 공유해 주면 같이 디버깅해줄 수도 있겠다.

-
-
-
    -
  • - 수정 -
  • -
  • -
    - - -
    -
  • -
-
-
- -
-
- + {{/replies}} + {{#article}} + +
+
- +
+ {{/article}} +
diff --git a/src/test/java/com/kakao/cafe/domain/article/JdbcTemplateArticleRepositoryTest.java b/src/test/java/com/kakao/cafe/domain/article/JdbcTemplateArticleRepositoryTest.java index 3f0dba3dc..83ed9f985 100644 --- a/src/test/java/com/kakao/cafe/domain/article/JdbcTemplateArticleRepositoryTest.java +++ b/src/test/java/com/kakao/cafe/domain/article/JdbcTemplateArticleRepositoryTest.java @@ -30,7 +30,7 @@ public JdbcTemplateArticleRepositoryTest(NamedParameterJdbcTemplate namedParamet @BeforeEach void setUp() { - article = new Article(null,"writer","title","contents"); + article = Article.newInstance(null,"writer","title","contents"); } @Test @@ -88,7 +88,7 @@ void findAllTest() { void updateTest() { Article previousArticle = jdbcTemplateArticleRepository.save(article); - Article updateArticle = new Article(previousArticle.getId(), "writer", "newTitle", "newContents", LocalDateTime.now()); + Article updateArticle = Article.of(previousArticle.getId(), "writer", "newTitle", "newContents", LocalDateTime.now()); Article updated = jdbcTemplateArticleRepository.save(updateArticle); Optional
updatedArticleOptional = jdbcTemplateArticleRepository.findById(previousArticle.getId()); @@ -104,7 +104,7 @@ void updateTest() { } @Test - @DisplayName("delte 시 해당 게시글을 삭제한다.") + @DisplayName("delete 시 해당 게시글을 삭제한다.") void deleteTest() { Article saved = jdbcTemplateArticleRepository.save(article); diff --git a/src/test/java/com/kakao/cafe/domain/article/MemoryArticleRepositoryTest.java b/src/test/java/com/kakao/cafe/domain/article/MemoryArticleRepositoryTest.java index e12dd6c0f..64709a36e 100644 --- a/src/test/java/com/kakao/cafe/domain/article/MemoryArticleRepositoryTest.java +++ b/src/test/java/com/kakao/cafe/domain/article/MemoryArticleRepositoryTest.java @@ -19,7 +19,7 @@ class MemoryArticleRepositoryTest { @BeforeEach void setUp() { articleRepository = new MemoryArticleRepository(); - article = new Article(null,"작성자", "제목", "본문"); + article = Article.newInstance(null,"작성자", "제목", "본문"); } @AfterEach diff --git a/src/test/java/com/kakao/cafe/domain/reply/JdbcTemplateReplyRepositoryTest.java b/src/test/java/com/kakao/cafe/domain/reply/JdbcTemplateReplyRepositoryTest.java new file mode 100644 index 000000000..e7168ec89 --- /dev/null +++ b/src/test/java/com/kakao/cafe/domain/reply/JdbcTemplateReplyRepositoryTest.java @@ -0,0 +1,117 @@ +package com.kakao.cafe.domain.reply; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; + +@JdbcTest +class JdbcTemplateReplyRepositoryTest { + + private final JdbcTemplateReplyRepository jdbcTemplateReplyRepository; + private Reply reply; + + @Autowired + public JdbcTemplateReplyRepositoryTest(NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + this.jdbcTemplateReplyRepository = new JdbcTemplateReplyRepository(namedParameterJdbcTemplate); + } + + @BeforeEach + void setUp() { + reply = Reply.newInstance(1, "writer1", "contents"); + } + + @Test + @DisplayName("기존에 저장되어 있지 않은 답변이라면, 저장하고, id를 부여한다.") + void save_new_Reply_test() { + + Reply saved = jdbcTemplateReplyRepository.save(reply); + + assertThat(saved.hasId()).isTrue(); + assertThat(saved.getId()).isEqualTo(1); + assertThat(saved.getArticleId()).isEqualTo(reply.getArticleId()); + assertThat(saved.getWriter()).isEqualTo(reply.getWriter()); + assertThat(saved.getContents()).isEqualTo(reply.getContents()); + assertThat(saved.getWrittenTime()).isEqualTo(reply.getWrittenTime()); + } + + @Test + @DisplayName("id를 가지고 있는 (이미 저장한)답변이라면, 정보를 업데이트한다.") + void save_reply_update_test() { + + Reply saved = jdbcTemplateReplyRepository.save(reply); + Reply updateReply = Reply.of(1, 1, "writer", "updatedContents", reply.getWrittenTime()); + + Reply updated = jdbcTemplateReplyRepository.save(updateReply); + + assertThat(updated).isEqualTo(saved); + assertThat(updated.getContents()).isNotEqualTo(saved.getContents()); + } + + @Test + @DisplayName("reply의 id로 해당 writer를 찾아 optional로 반환한다.") + void findWriterByIdTest() { + + Reply saved = jdbcTemplateReplyRepository.save(reply); + + Optional writerById = jdbcTemplateReplyRepository.findWriterById(saved.getId()); + + assertThat(writerById).isNotEmpty(); + assertThat(writerById.get()).isEqualTo(saved.getWriter()); + + } + + @Test + @DisplayName("찾는 id의 reply가 없으면, optional.empty를 반환한다.") + void findWriterByIdTest_not_found() { + + jdbcTemplateReplyRepository.save(reply); + + Optional writerById = jdbcTemplateReplyRepository.findWriterById(2); + + assertThat(writerById).isEmpty(); + + } + + @Test + @DisplayName("하나의 질문 게시글에 포함된 답변을 모두 반환한다.") + void findAllByArticleIdTest() { + Reply saved = jdbcTemplateReplyRepository.save(reply); + Reply replyOfAnotherArticle = jdbcTemplateReplyRepository.save(Reply.newInstance(2, "anotherWriter", "contents")); + + List all = jdbcTemplateReplyRepository.findAllByArticleId(1); + + assertThat(all.size()).isEqualTo(1); + assertThat(all).contains(saved); + assertThat(all).doesNotContain(replyOfAnotherArticle); + } + + @Test + @DisplayName("id로 해당하는 답변을 삭제한다.") + void deleteOne() { + Reply saved = jdbcTemplateReplyRepository.save(reply); + + boolean isDeleted = jdbcTemplateReplyRepository.deleteOne(saved.getId()); + + assertThat(isDeleted).isTrue(); + assertThat(jdbcTemplateReplyRepository.findWriterById(saved.getId())).isEmpty(); + } + + @Test + void hasReplyOfAnotherWriter() { + jdbcTemplateReplyRepository.save(reply); + Reply replyOfAnotherWriter = Reply.newInstance(1, "AnotherWriter", "contents"); + jdbcTemplateReplyRepository.save(replyOfAnotherWriter); + + boolean hasReplyOfAnotherWriter = jdbcTemplateReplyRepository.hasReplyOfAnotherWriter(1, "writer1"); + + assertThat(hasReplyOfAnotherWriter).isTrue(); + } +} diff --git a/src/test/java/com/kakao/cafe/service/ArticleServiceTest.java b/src/test/java/com/kakao/cafe/service/ArticleServiceTest.java index 1024f6a56..1e7b4ba49 100644 --- a/src/test/java/com/kakao/cafe/service/ArticleServiceTest.java +++ b/src/test/java/com/kakao/cafe/service/ArticleServiceTest.java @@ -37,7 +37,7 @@ class ArticleServiceTest { @BeforeEach void setUp() { - article = new Article(1, "writer", "title", "contents", LocalDateTime.of(2022,03,11,11,25)); + article = Article.of(1, "writer", "title", "contents", LocalDateTime.of(2022,03,11,11,25)); } @Test @@ -84,7 +84,7 @@ void findOneTest_error(int id) { } @Test - @DisplayName("모든 게시글을 조회하면 ArticleDto로 변환해서 반환한다.") + @DisplayName("모든 게시글을 조회하면 ArticleResponseDto로 변환해서 반환한다.") void findAllTest() { given(articleRepository.findAll()).willReturn(List.of(article)); diff --git a/src/test/java/com/kakao/cafe/service/ReplyServiceTest.java b/src/test/java/com/kakao/cafe/service/ReplyServiceTest.java new file mode 100644 index 000000000..eeca77557 --- /dev/null +++ b/src/test/java/com/kakao/cafe/service/ReplyServiceTest.java @@ -0,0 +1,116 @@ +package com.kakao.cafe.service; + +import com.kakao.cafe.domain.reply.Reply; +import com.kakao.cafe.domain.reply.ReplyRepository; +import com.kakao.cafe.exception.ClientException; +import com.kakao.cafe.web.dto.ReplyResponseDto; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +class ReplyServiceTest { + + @InjectMocks + private ReplyService replyService; + + @Mock + private ReplyRepository replyRepository; + + private String writer; + private Reply reply; + + @BeforeEach + void setUp() { + writer = "writer"; + reply = Reply.of(1, 1, writer, "contents", LocalDateTime.now()); + } + + @Test + @DisplayName("writer와 replyDto를 매개변수로, 저장한 후 replyResponseDto를 반환한다.") + void writeTest() { + //given + given(replyRepository.save(any())).willReturn(reply); + + //when + ReplyResponseDto replyResponseDto = replyService.write(1, writer, "contents"); + + //then + assertThat(replyResponseDto.getId()).isEqualTo(reply.getId()); + assertThat(replyResponseDto.getArticleId()).isEqualTo(reply.getArticleId()); + assertThat(replyResponseDto.getWriter()).isEqualTo(reply.getWriter()); + assertThat(replyResponseDto.getContents()).isEqualTo(reply.getContents()); + assertThat(replyResponseDto.getWrittenTime()).isEqualTo(reply.getWrittenTime()); + } + + @Test + @DisplayName("articleId로 해당하는 article의 답변들을 검색하면, 해당 articleId를 가지는 reply들을 replyResponseDto로 변환해서 리스트로 반환한다.") + void showAllInArticleTest() { + //given + given(replyRepository.findAllByArticleId(1)).willReturn(List.of(reply)); + + //when + List replyResponseDtos = replyService.showAllInArticle(1); + + //then + ReplyResponseDto replyResponseDto = replyResponseDtos.get(0); + assertThat(replyResponseDtos.size()).isEqualTo(1); + assertThat(replyResponseDto.getId()).isEqualTo(reply.getId()); + assertThat(replyResponseDto.getArticleId()).isEqualTo(reply.getArticleId()); + assertThat(replyResponseDto.getWriter()).isEqualTo(reply.getWriter()); + assertThat(replyResponseDto.getContents()).isEqualTo(reply.getContents()); + assertThat(replyResponseDto.getWrittenTime()).isEqualTo(reply.getWrittenTime()); + } + + @Test + @DisplayName("작성자는 직접 작성한 답변이라면, 해당하는 reply의 id로 답변을 삭제할 수 있다.") + void delete_owned_reply_test() { + //given + given(replyRepository.deleteOne(1)).willReturn(true); + given(replyRepository.findWriterById(1)).willReturn(Optional.of(writer)); + + //when + boolean isDeleted = replyService.delete(writer, 1); + + //then + assertThat(isDeleted).isTrue(); + } + + @Test + @DisplayName("다른 작성자의 답변을 삭제하려하면, ClientException이 발생한다") + void delete() { + //given + given(replyRepository.findWriterById(1)).willReturn(Optional.of(writer)); + + //when & then + assertThatThrownBy(()->replyService.delete("AnotherWriter", 1)) + .isInstanceOf(ClientException.class) + .hasMessage("접근 권한이 없습니다."); + + } + + @Test + void isDeletableArticle() { + //given + given(replyRepository.hasReplyOfAnotherWriter(1, writer)).willReturn(false); + + //when + boolean isDeletable = replyService.isDeletableArticle(1, writer); + + //then + assertThat(isDeletable).isTrue(); + + } +} diff --git a/src/test/java/com/kakao/cafe/service/UserServiceTest.java b/src/test/java/com/kakao/cafe/service/UserServiceTest.java index 86cde2f5e..23e1ee3d1 100644 --- a/src/test/java/com/kakao/cafe/service/UserServiceTest.java +++ b/src/test/java/com/kakao/cafe/service/UserServiceTest.java @@ -4,6 +4,7 @@ import com.kakao.cafe.domain.user.UserRepository; import com.kakao.cafe.exception.ClientException; import com.kakao.cafe.web.dto.LoginDto; +import com.kakao.cafe.web.dto.SessionUser; import com.kakao.cafe.web.dto.UserDto; import com.kakao.cafe.web.dto.UserResponseDto; import org.junit.jupiter.api.BeforeEach; @@ -138,7 +139,7 @@ void loginTest() { given(userRepository.findById("ron2")).willReturn(Optional.of(user)); //when - UserResponseDto login = userService.login(loginUser).orElse(null); + SessionUser login = userService.login(loginUser).orElse(null); //then assertThat(login.getUserId()).isEqualTo(loginUser.getUserId()); @@ -156,7 +157,7 @@ void login_not_signup_user_throw_test() { given(userRepository.findById(any())).willReturn(Optional.empty()); //when - Optional loginUserOptional = userService.login(loginUser); + Optional loginUserOptional = userService.login(loginUser); //then assertThat(loginUserOptional).isEmpty(); @@ -170,7 +171,7 @@ void login_wrong_password_throw_test() { given(userRepository.findById("ron2")).willReturn(Optional.of(user)); //when - Optional loginUserOptional = userService.login(loginUser); + Optional loginUserOptional = userService.login(loginUser); //then assertThat(loginUserOptional).isEmpty(); diff --git a/src/test/java/com/kakao/cafe/web/ArticleControllerTest.java b/src/test/java/com/kakao/cafe/web/ArticleControllerTest.java index 8bd20ab7f..cc9b8e91b 100644 --- a/src/test/java/com/kakao/cafe/web/ArticleControllerTest.java +++ b/src/test/java/com/kakao/cafe/web/ArticleControllerTest.java @@ -4,8 +4,10 @@ import com.kakao.cafe.domain.article.Article; import com.kakao.cafe.domain.user.User; import com.kakao.cafe.service.ArticleService; +import com.kakao.cafe.service.ReplyService; import com.kakao.cafe.web.dto.ArticleResponseDto; -import com.kakao.cafe.web.dto.UserResponseDto; +import com.kakao.cafe.web.dto.ReplyResponseDto; +import com.kakao.cafe.web.dto.SessionUser; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -16,6 +18,7 @@ import org.springframework.mock.web.MockHttpSession; import org.springframework.test.web.servlet.MockMvc; +import java.time.LocalDateTime; import java.util.List; import static org.mockito.ArgumentMatchers.any; @@ -35,12 +38,15 @@ class ArticleControllerTest { @MockBean ArticleService articleService; + @MockBean + ReplyService replyService; + private MockHttpSession httpSession = new MockHttpSession(); @BeforeEach void SetUp() { User user = new User("ron2", "1234", "로니", "ron2@gmail.com"); - httpSession.setAttribute(LoginConstants.SESSIONED_USER, new UserResponseDto(user)); + httpSession.setAttribute(LoginConstants.SESSIONED_USER, SessionUser.from(user)); } @Test @@ -88,7 +94,7 @@ void write_not_logined_user_redirect_test() throws Exception { @DisplayName("/qna/all로 get 요청시 /qna/list 호출해서 게시글 목록을 보여준다.") void showAll() throws Exception { - Article article = new Article(1,"작성자","제목","본문"); + Article article = Article.newInstance(1,"작성자","제목","본문"); List articleResponseDtos = List.of(new ArticleResponseDto(article)); given(articleService.findAll()).willReturn(articleResponseDtos); @@ -101,28 +107,34 @@ void showAll() throws Exception { } @Test - @DisplayName("/qna/show/{id} get 요청시 해당 id를 가지고 있는 게시글을 /qna/show에서 보여준다.") + @DisplayName("/qna/show/{id} get 요청시 해당 id를 가지고 있는 게시글을 /qna/show에서 댓글들과 함께 보여준다.") void showArticle() throws Exception { - Article article = new Article(1,"작성자","제목","본문"); + Article article = Article.newInstance(1,"작성자","제목","본문"); ArticleResponseDto articleResponseDto = new ArticleResponseDto(article); + ReplyResponseDto replyResponseDto = new ReplyResponseDto(1,1,"작성자", "본문", LocalDateTime.now()); + List replyResponseDtos = List.of(replyResponseDto); given(articleService.findOne(anyInt())).willReturn(articleResponseDto); + given(replyService.showAllInArticle(anyInt())).willReturn(replyResponseDtos); mockMvc.perform(get("/qna/show/"+anyInt()).session(httpSession)) .andExpect(status().isOk()) .andExpect(model().attributeExists("article")) .andExpect(model().attribute("article", articleResponseDto)) + .andExpect(model().attribute("size", 1)) + .andExpect(model().attribute("replies", replyResponseDtos)) .andExpect(view().name("qna/show")); } @Test - @DisplayName("/qna/delete/{id} DELETE 요청시 로그인된 해당유저가 작성한 글이면 삭제하고, /qna/all로 리다이렉션한다.") + @DisplayName("/qna/delete/{id} DELETE 요청시 로그인된 해당유저가 작성한 글이고, 다른 사용자의 댓글이 없다면 삭제하고, /qna/all로 리다이렉션한다.") void deleteArticleTest() throws Exception { //given Integer articleId = 1; String writer = "ron2"; - Article article = new Article(articleId, writer, "제목","본문"); + Article article = Article.newInstance(articleId, writer, "제목","본문"); given(articleService.findOne(any())).willReturn(new ArticleResponseDto(article)); + given(replyService.isDeletableArticle(anyInt(), any())).willReturn(true); //when mockMvc.perform(delete("/qna/delete/"+articleId) @@ -140,7 +152,7 @@ void deleteArticleTest() throws Exception { void updateFormTest() throws Exception { //given Integer articleId = 1; - Article article = new Article(articleId, "ron2", "제목","본문"); + Article article = Article.newInstance(articleId, "ron2", "제목","본문"); ArticleResponseDto articleResponseDto = new ArticleResponseDto(article); given(articleService.findOne(any())).willReturn(articleResponseDto); diff --git a/src/test/java/com/kakao/cafe/web/HomeControllerTest.java b/src/test/java/com/kakao/cafe/web/HomeControllerTest.java index 69cd73caf..1d73ca0ed 100644 --- a/src/test/java/com/kakao/cafe/web/HomeControllerTest.java +++ b/src/test/java/com/kakao/cafe/web/HomeControllerTest.java @@ -6,8 +6,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @WebMvcTest(HomeController.class) @@ -20,8 +19,8 @@ class HomeControllerTest { @DisplayName("GetMapping index.html 테스트") void homeTest() throws Exception { mockMvc.perform(get("/")) - .andExpect(status().isOk()) - .andExpect(view().name("index")); + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/qna/all")); } diff --git a/src/test/java/com/kakao/cafe/web/ReplyControllerTest.java b/src/test/java/com/kakao/cafe/web/ReplyControllerTest.java new file mode 100644 index 000000000..872d43a07 --- /dev/null +++ b/src/test/java/com/kakao/cafe/web/ReplyControllerTest.java @@ -0,0 +1,78 @@ +package com.kakao.cafe.web; + +import com.kakao.cafe.constants.LoginConstants; +import com.kakao.cafe.domain.reply.Reply; +import com.kakao.cafe.domain.user.User; +import com.kakao.cafe.service.ReplyService; +import com.kakao.cafe.web.dto.ReplyResponseDto; +import com.kakao.cafe.web.dto.SessionUser; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDateTime; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.only; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(ReplyController.class) +class ReplyControllerTest { + + @Autowired + MockMvc mockMvc; + + @MockBean + ReplyService replyService; + + private MockHttpSession httpSession = new MockHttpSession(); + + @BeforeEach + void setUp() { + SessionUser sessionUser = SessionUser.from(new User("ron2", "1234", "로니", "ron2@gmail.com")); + httpSession.setAttribute(LoginConstants.SESSIONED_USER, sessionUser); + } + + @Test + @DisplayName("/qna/{articleId}/reply/write post 요청시 contents를 받아서 댓글을 작성하고, /qna/show/{articleId}로 리다이렉션한다.") + void writeTest() throws Exception { + //given + Reply reply = Reply.of(1,1,"ron2","답변", LocalDateTime.now()); + given(replyService.write(1,"ron2","답변")).willReturn(ReplyResponseDto.from(reply)); + + //when + mockMvc.perform(post("/qna/1/reply/write") + .session(httpSession) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .content("contents=답변")) + //then + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/qna/show/1")); + + } + + @Test + @DisplayName("/qna/{articleId}/reply/{id}/delete delete 요청시 해당 댓글을 삭제하고 /qna/show/{articleId}로 리다이렉션한다.") + void deleteTest() throws Exception { + + //when + mockMvc.perform(delete("/qna/1/reply/1/delete") + .session(httpSession)) + //then + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/qna/show/1")); + + verify(replyService, only()).delete("ron2", 1); + + } + +} diff --git a/src/test/java/com/kakao/cafe/web/UserControllerTest.java b/src/test/java/com/kakao/cafe/web/UserControllerTest.java index 659037776..9ec14d28f 100644 --- a/src/test/java/com/kakao/cafe/web/UserControllerTest.java +++ b/src/test/java/com/kakao/cafe/web/UserControllerTest.java @@ -3,6 +3,7 @@ import com.kakao.cafe.constants.LoginConstants; import com.kakao.cafe.domain.user.User; import com.kakao.cafe.service.UserService; +import com.kakao.cafe.web.dto.SessionUser; import com.kakao.cafe.web.dto.UserResponseDto; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -35,11 +36,13 @@ class UserControllerTest { private MockHttpSession httpSession = new MockHttpSession(); private UserResponseDto userResponseDto; + private SessionUser sessionUser; @BeforeEach void setUp() { User user = new User("ron2", "1234", "ron2", "ron2@gmail.com"); userResponseDto = new UserResponseDto(user); + sessionUser = new SessionUser(user.getUserId(), user.getName(), user.getEmail()); } @AfterEach @@ -96,11 +99,11 @@ void showProfile() throws Exception { @DisplayName("GetMapping userId를 @PathVariable로 받아서 해당 유저 정보를 /user/update_form으로 넘겨준다.") void updateFormTest() throws Exception { - httpSession.setAttribute("sessionedUser",userResponseDto); + httpSession.setAttribute("sessionedUser", sessionUser); mockMvc.perform(get("/users/ron2/update").session(httpSession)) .andExpect(status().isOk()) - .andExpect(model().attribute("user", userResponseDto)) + .andExpect(model().attribute("user", sessionUser)) .andExpect(view().name("user/update_form")) .andDo(print()); } @@ -120,7 +123,7 @@ void updateForm_notLoggedIn_Test() throws Exception { @DisplayName("updatform get 요청시, session과 id가 일치하지않으면 error-page/4xx을 반환한다.") void updateForm_another_user_access_Test() throws Exception { //given - httpSession.setAttribute("sessionedUser",userResponseDto); + httpSession.setAttribute("sessionedUser", sessionUser); //when mockMvc.perform(get("/users/anotherId/update").session(httpSession)) @@ -135,7 +138,7 @@ void updateForm_another_user_access_Test() throws Exception { @DisplayName("PutMapping 수정정보를 받아서 회원정보를 수정 후 /users로 리다이렉션한다.") void updateInfoTest() throws Exception { //given - httpSession.setAttribute("sessionedUser", userResponseDto); + httpSession.setAttribute("sessionedUser", sessionUser); //when mockMvc.perform(put("/users/ron2/update") @@ -163,7 +166,7 @@ void updateInfo_notLoggedIn_User_test() throws Exception { @DisplayName("updateInfo put 요청시, 로그인된 유저가 아닌 다른 유저의 정보를 변경하려고 하면 ClientException이 발생한다. ") void updateInfo_another_user_access_Test() throws Exception { //given - httpSession.setAttribute("sessionedUser", userResponseDto); + httpSession.setAttribute("sessionedUser", sessionUser); //when mockMvc.perform(put("/users/anotherId/update") @@ -189,7 +192,7 @@ void loginFormGetTest() throws Exception { @DisplayName("/user/login post 요청시 user/login을 반환한다.") void loginPostTest() throws Exception { //given - given(userService.login(any())).willReturn(Optional.ofNullable(userResponseDto)); + given(userService.login(any())).willReturn(Optional.ofNullable(sessionUser)); //when mockMvc.perform(post("/user/login") @@ -198,7 +201,7 @@ void loginPostTest() throws Exception { //then .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl("/qna/all")) - .andExpect(request().sessionAttribute("sessionedUser", userResponseDto)); + .andExpect(request().sessionAttribute("sessionedUser", sessionUser)); } @Test