diff --git a/src/main/java/com/nooblol/board/controller/ArticleController.java b/src/main/java/com/nooblol/board/controller/ArticleController.java new file mode 100644 index 00000000..8b5d0403 --- /dev/null +++ b/src/main/java/com/nooblol/board/controller/ArticleController.java @@ -0,0 +1,114 @@ +package com.nooblol.board.controller; + +import com.nooblol.board.dto.ArticleDto; +import com.nooblol.board.dto.ArticleInsertRequestDto; +import com.nooblol.board.dto.ArticleUpdateRequestDto; +import com.nooblol.board.service.ArticleService; +import com.nooblol.global.annotation.UserLoginCheck; +import com.nooblol.global.dto.ResponseDto; +import com.nooblol.global.utils.ResponseUtils; +import com.nooblol.global.utils.SessionUtils; +import javax.servlet.http.HttpSession; +import javax.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequestMapping("/article") +@RequiredArgsConstructor +public class ArticleController { + + private final ArticleService articleService; + + /** + * articleId의 게시물 정보와 현재 요청한 사용자의 권한을 같이 Return한다 Session에 로그인정보가 없는 경우에는 게시물에 대한 정보 수정을 주는 권한을 + * Guest로 설정한다 + * + * @param articleId + * @param session + * @return + */ + @GetMapping("/{articleId}") + public ResponseDto getArticle( + @PathVariable int articleId, HttpSession session + ) { + ArticleDto article = articleService.getArticleInfo( + articleId, SessionUtils.getSessionUserId(session) + ); + + return ResponseUtils.makeToResponseOkDto(article); + } + + /** + * 게시물 등록 + * + * @param articleDto + * @return + */ + @UserLoginCheck + @PostMapping("/") + public ResponseDto insertArticle( + @Valid @RequestBody ArticleInsertRequestDto articleDto, HttpSession session + ) { + ArticleDto upsertArticle = new ArticleDto().builder() + .bbsId(articleDto.getBbsId()) + .articleTitle(articleDto.getArticleTitle()) + .articleContent(articleDto.getArticleContent()) + .articleReadCount(articleDto.getArticleReadCount()) + .status(articleDto.getStatus()) + .createdUserId(SessionUtils.getSessionUserId(session)) + .createdAt(articleDto.getCreatedAt()) + .updatedAt(articleDto.getUpdatedAt()) + .build(); + + boolean upsertResult = articleService.upsertArticle(upsertArticle, session, true); + + return ResponseUtils.makeToResponseOkDto(upsertResult); + } + + /** + * 게시물 수정 + * + * @param articleDto + * @return + */ + @UserLoginCheck + @PutMapping("/") + public ResponseDto updateArticle( + @Valid @RequestBody ArticleUpdateRequestDto articleDto, HttpSession session + ) { + ArticleDto upsertArticle = new ArticleDto().builder() + .articleId(articleDto.getArticleId()) + .bbsId(articleDto.getBbsId()) + .articleTitle(articleDto.getArticleTitle()) + .articleContent(articleDto.getArticleContent()) + .status(articleDto.getStatus()) + .build(); + + boolean upsertResult = articleService.upsertArticle(upsertArticle, session, false); + + return ResponseUtils.makeToResponseOkDto(upsertResult); + } + + /** + * 게시물 삭제 + * + * @param articleId + * @param session + * @return + */ + @UserLoginCheck + @DeleteMapping("/{articleId}") + public ResponseDto deleteArticle(@PathVariable int articleId, HttpSession session) { + return ResponseUtils.makeToResponseOkDto(articleService.deleteArticle(articleId, session)); + } +} diff --git a/src/main/java/com/nooblol/board/controller/ArticleStatusController.java b/src/main/java/com/nooblol/board/controller/ArticleStatusController.java new file mode 100644 index 00000000..f1f04e61 --- /dev/null +++ b/src/main/java/com/nooblol/board/controller/ArticleStatusController.java @@ -0,0 +1,61 @@ +package com.nooblol.board.controller; + +import com.nooblol.board.service.ArticleStatusService; +import com.nooblol.global.annotation.UserLoginCheck; +import com.nooblol.global.dto.ResponseDto; +import com.nooblol.global.utils.ResponseUtils; +import javax.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequestMapping("/article/status") +@RequiredArgsConstructor +public class ArticleStatusController { + + private final ArticleStatusService articleStatusService; + + /** + * 파라미터로 제공한 게시물의 추천, 비추천 갯수를 Return한다 + * + * @param articleId + * @return + */ + @GetMapping("/{articleId}") + public ResponseDto likeAndNotLikeArticle(@PathVariable int articleId) { + return ResponseUtils.makeToResponseOkDto(articleStatusService.likeAndNotListStatus(articleId)); + } + + /** + * 게시물 추천 + * + * @param articleId + * @param session + * @return + */ + @UserLoginCheck + @PostMapping("/like/{articleId}") + public ResponseDto likeArticle(@PathVariable int articleId, HttpSession session) { + return ResponseUtils.makeToResponseOkDto(articleStatusService.likeArticle(articleId, session)); + } + + /** + * 게시물 비추천 + * + * @param articleId + * @param session + * @return + */ + @UserLoginCheck + @PostMapping("/notLike/{articleId}") + public ResponseDto notLikeArticle(@PathVariable int articleId, HttpSession session) { + return ResponseUtils.makeToResponseOkDto( + articleStatusService.notLikeArticle(articleId, session)); + } +} diff --git a/src/main/java/com/nooblol/board/dto/ArticleDto.java b/src/main/java/com/nooblol/board/dto/ArticleDto.java new file mode 100644 index 00000000..ba072b55 --- /dev/null +++ b/src/main/java/com/nooblol/board/dto/ArticleDto.java @@ -0,0 +1,32 @@ +package com.nooblol.board.dto; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ArticleDto { + + /* + Integer로 변경하는 이유는 기본값 0이 들어감으로 인해서 Mybatis에서 NullCheck를 못하는 경우를 제외하기 위해 변경 + */ + + private Integer articleId; + private Integer bbsId; + private String articleTitle; + private int articleReadCount; + private String articleContent; + private Integer status; + private String createdUserId; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private String authMessage; +} diff --git a/src/main/java/com/nooblol/board/dto/ArticleInsertRequestDto.java b/src/main/java/com/nooblol/board/dto/ArticleInsertRequestDto.java new file mode 100644 index 00000000..17b26933 --- /dev/null +++ b/src/main/java/com/nooblol/board/dto/ArticleInsertRequestDto.java @@ -0,0 +1,23 @@ +package com.nooblol.board.dto; + +import java.time.LocalDateTime; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ArticleInsertRequestDto extends ArticleRequestBaseDto { + + private String createdUserId; + private Integer articleReadCount = 0; + private LocalDateTime createdAt = LocalDateTime.now(); + private LocalDateTime updatedAt = LocalDateTime.now(); +} + diff --git a/src/main/java/com/nooblol/board/dto/ArticleRequestBaseDto.java b/src/main/java/com/nooblol/board/dto/ArticleRequestBaseDto.java new file mode 100644 index 00000000..b6f2ab73 --- /dev/null +++ b/src/main/java/com/nooblol/board/dto/ArticleRequestBaseDto.java @@ -0,0 +1,29 @@ +package com.nooblol.board.dto; + + +import com.nooblol.board.utils.ArticleMessage; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class ArticleRequestBaseDto { + + @NotNull(message = ArticleMessage.BBS_ID_NULL) + private Integer bbsId; + + @NotBlank(message = ArticleMessage.ARTICLE_TITLE_NULL) + private String articleTitle; + + @NotBlank(message = ArticleMessage.ARTICLE_CONTENT_NULL) + private String articleContent; + + @NotNull(message = ArticleMessage.ARTICLE_STATUS_NULL) + private Integer status; +} diff --git a/src/main/java/com/nooblol/board/dto/ArticleStatusDto.java b/src/main/java/com/nooblol/board/dto/ArticleStatusDto.java new file mode 100644 index 00000000..e1866831 --- /dev/null +++ b/src/main/java/com/nooblol/board/dto/ArticleStatusDto.java @@ -0,0 +1,26 @@ +package com.nooblol.board.dto; + +import com.nooblol.board.utils.ArticleLikeStatusEnum; +import com.nooblol.board.utils.ArticleStatusEnum; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ArticleStatusDto { + + private int articleId; + + private String userId; + + private ArticleLikeStatusEnum likeType; + + private LocalDateTime createdAt = LocalDateTime.now(); +} diff --git a/src/main/java/com/nooblol/board/dto/ArticleUpdateRequestDto.java b/src/main/java/com/nooblol/board/dto/ArticleUpdateRequestDto.java new file mode 100644 index 00000000..08519aa7 --- /dev/null +++ b/src/main/java/com/nooblol/board/dto/ArticleUpdateRequestDto.java @@ -0,0 +1,25 @@ +package com.nooblol.board.dto; + +import com.nooblol.board.utils.ArticleMessage; +import java.time.LocalDateTime; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class ArticleUpdateRequestDto extends ArticleRequestBaseDto { + + @NotNull(message = ArticleMessage.ARTICLE_ID_NULL) + private Integer articleId; + + + private LocalDateTime updatedAt = LocalDateTime.now(); +} + diff --git a/src/main/java/com/nooblol/board/dto/LikeAndNotLikeResponseDto.java b/src/main/java/com/nooblol/board/dto/LikeAndNotLikeResponseDto.java new file mode 100644 index 00000000..af589573 --- /dev/null +++ b/src/main/java/com/nooblol/board/dto/LikeAndNotLikeResponseDto.java @@ -0,0 +1,19 @@ +package com.nooblol.board.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class LikeAndNotLikeResponseDto { + + private int likeCnt; + private int notLikeCnt; + +} diff --git a/src/main/java/com/nooblol/board/mapper/ArticleMapper.java b/src/main/java/com/nooblol/board/mapper/ArticleMapper.java new file mode 100644 index 00000000..f879a5e6 --- /dev/null +++ b/src/main/java/com/nooblol/board/mapper/ArticleMapper.java @@ -0,0 +1,22 @@ +package com.nooblol.board.mapper; + +import com.nooblol.board.dto.ArticleDto; +import com.nooblol.board.dto.ArticleStatusDto; +import com.nooblol.board.dto.LikeAndNotLikeResponseDto; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ArticleMapper { + + ArticleDto selectArticleByArticleId(int articleId); + + void addReadCount(int articleId); + + int selectUserAuth(String userId); + + int upsertArticle(ArticleDto articleDto); + + String selectCreatedUserId(int articleId); + + int deleteArticleByArticleId(int articleId); +} diff --git a/src/main/java/com/nooblol/board/mapper/ArticleStatusMapper.java b/src/main/java/com/nooblol/board/mapper/ArticleStatusMapper.java new file mode 100644 index 00000000..2b744dd8 --- /dev/null +++ b/src/main/java/com/nooblol/board/mapper/ArticleStatusMapper.java @@ -0,0 +1,18 @@ +package com.nooblol.board.mapper; + +import com.nooblol.board.dto.ArticleStatusDto; +import com.nooblol.board.dto.LikeAndNotLikeResponseDto; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ArticleStatusMapper { + + ArticleStatusDto selectArticleStatusByArticleIdAndUserId(ArticleStatusDto articleStatusDto); + + LikeAndNotLikeResponseDto selectArticleAllStatusByArticleId(int articleId); + + int insertArticleStatus(ArticleStatusDto articleStatusDto); + + int deleteArticleStatus(ArticleStatusDto articleStatusDto); + +} diff --git a/src/main/java/com/nooblol/board/service/ArticleService.java b/src/main/java/com/nooblol/board/service/ArticleService.java new file mode 100644 index 00000000..22e8c18f --- /dev/null +++ b/src/main/java/com/nooblol/board/service/ArticleService.java @@ -0,0 +1,54 @@ +package com.nooblol.board.service; + +import com.nooblol.board.dto.ArticleDto; +import com.nooblol.board.dto.LikeAndNotLikeResponseDto; +import javax.servlet.http.HttpSession; + +public interface ArticleService { + + /** + * 받은 articleId로 조회수를 증가한 이후 게시물정보를 반환한다. + * + * @param articleId 조회해야 하는 게시물 ID + * @param userId 로그인을 한 경우, Session에 저장된 UserId + * @return + */ + ArticleDto getArticleInfo(int articleId, String userId); + + /** + * 해당 사용자가 실제 사용자인지확인후, 게시물에서 행동할 수 있는 권한을 Return한다 + * + * @param userId + * @return + */ + String getUserArticleAuth(String userId); + + /** + * 받은 articleId의 조회수를 1 증가시킨다 + * + * @param articleId + */ + void addReadCount(int articleId); + + /** + * 게시물의 삽입 또는 수정을 진행한다. + * + * @param articleDto + * @param session + * @param isInsert 삽입인 경우 True, Update인 경우 False + * @return + */ + boolean upsertArticle(ArticleDto articleDto, HttpSession session, boolean isInsert); + + + /** + * 게시물 삭제, 요청자가 관리자인 경우 또는 글 작성자인 경우에만 삭제를 진행한다. Delete를 진행하는 경우 관련 테이블에 대해서도 삭제가 이뤄지다 보니, + * Transaction으로 묶어 처리를 진행한다 + * + * @param articleId + * @param session + * @return + */ + boolean deleteArticle(int articleId, HttpSession session); + +} diff --git a/src/main/java/com/nooblol/board/service/ArticleStatusService.java b/src/main/java/com/nooblol/board/service/ArticleStatusService.java new file mode 100644 index 00000000..7ca2571d --- /dev/null +++ b/src/main/java/com/nooblol/board/service/ArticleStatusService.java @@ -0,0 +1,33 @@ +package com.nooblol.board.service; + +import com.nooblol.board.dto.LikeAndNotLikeResponseDto; +import javax.servlet.http.HttpSession; + +public interface ArticleStatusService { + + /** + * 게시물 추천 + * + * @param articleId + * @param session 한개의 Article에 한번만 추천또는 비추천이 가능하며, 재요청이 들어온 경우 Delete처리를 하기위함. + * @return + */ + boolean likeArticle(int articleId, HttpSession session); + + /** + * 게시물 비추천 + * + * @param articleId + * @param session 한개의 Article에 한번만 추천또는 비추천이 가능하며, 재요청이 들어온 경우 Delete처리를 하기위함. + * @return + */ + boolean notLikeArticle(int articleId, HttpSession session); + + /** + * 해당 ArticleId의 좋아요 갯수와 싫어요 갯수를 Return한다. + * + * @param articleId + * @return + */ + LikeAndNotLikeResponseDto likeAndNotListStatus(int articleId); +} diff --git a/src/main/java/com/nooblol/board/service/impl/ArticleServiceImpl.java b/src/main/java/com/nooblol/board/service/impl/ArticleServiceImpl.java new file mode 100644 index 00000000..dc9d0f2a --- /dev/null +++ b/src/main/java/com/nooblol/board/service/impl/ArticleServiceImpl.java @@ -0,0 +1,148 @@ +package com.nooblol.board.service.impl; + +import com.nooblol.board.dto.ArticleDto; +import com.nooblol.board.dto.ArticleStatusDto; +import com.nooblol.board.mapper.ArticleStatusMapper; +import com.nooblol.board.service.ArticleService; +import com.nooblol.board.mapper.ArticleMapper; +import com.nooblol.board.utils.ArticleAuthMessage; +import com.nooblol.global.exception.ExceptionMessage; +import com.nooblol.global.utils.SessionUtils; +import com.nooblol.user.utils.UserRoleStatus; +import javax.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.ObjectUtils; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ArticleServiceImpl implements ArticleService { + + private final ArticleMapper articleMapper; + + private final ArticleStatusMapper articleStatusMapper; + + @Override + public ArticleDto getArticleInfo(int articleId, String userId) { + addReadCount(articleId); + ArticleDto result = articleMapper.selectArticleByArticleId(articleId); + + if (ObjectUtils.isEmpty(result)) { + throw new IllegalArgumentException(ExceptionMessage.NO_DATA); + } + + result.setAuthMessage(getUserArticleAuth(userId)); + return result; + } + + @Override + public String getUserArticleAuth(String userId) { + if (StringUtils.isBlank(userId)) { + return ArticleAuthMessage.GUEST.name(); + } + + int userAuthData = articleMapper.selectUserAuth(userId); + if (userAuthData == UserRoleStatus.ADMIN.getRoleValue()) { + return ArticleAuthMessage.ADMIN.name(); + } + + return ArticleAuthMessage.USER.name(); + } + + @Override + public void addReadCount(int articleId) { + articleMapper.addReadCount(articleId); + } + + @Override + public boolean upsertArticle(ArticleDto articleDto, HttpSession session, boolean isInsert) { + //UserLoginCheck의 Annotation을 통해 무조건 Session로그인이 확인된 상황이기에, Role이 Null이 올 수 없음 + + boolean isUserAdminOrInsertArticle = + UserRoleStatus.isUserRoleAdmin(SessionUtils.getSessionUserRole(session)) || isInsert; + + if (isUserAdminOrInsertArticle) { + return isArticleUpsertSuccess(articleDto); + } + + //일반 사용자이면서, 게시물의 원작자 여부 확인 + boolean isNotCreatedUser = isNotArticleCreatedUser( + articleMapper.selectCreatedUserId(articleDto.getArticleId()), + SessionUtils.getSessionUserId(session) + ); + + if (isNotCreatedUser) { + throw new IllegalArgumentException(ExceptionMessage.FORBIDDEN); + } + + return isArticleUpsertSuccess(articleDto); + } + + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public boolean deleteArticle(int articleId, HttpSession session) { + ArticleDto haveArticleData = articleMapper.selectArticleByArticleId(articleId); + + if (ObjectUtils.isEmpty(haveArticleData)) { + throw new IllegalArgumentException(ExceptionMessage.NO_DATA); + } + + boolean isUserAdmin = UserRoleStatus.isUserRoleAdmin(SessionUtils.getSessionUserRole(session)); + if (isUserAdmin) { + return isArticleDeleteSuccess(articleId); + } + + boolean isNotCreatedUser = isNotArticleCreatedUser( + haveArticleData.getCreatedUserId(), SessionUtils.getSessionUserId(session) + ); + + if (isNotCreatedUser) { + throw new IllegalArgumentException(ExceptionMessage.FORBIDDEN); + } + + return isArticleDeleteSuccess(articleId); + } + + /** + * Upsert가 정상적으로 진행된 경우 True를 Return한다. + * + * @param articleDto + * @return + */ + private boolean isArticleUpsertSuccess(ArticleDto articleDto) { + return articleMapper.upsertArticle(articleDto) > 0; + } + + /** + * 게시글을 작성한 사용자가 Session에 저장된 사용자가 아닌 경우 True를 Return한다. + * + * @param dbCreatedUserId 데이터가 없는 경우 빈값이 올 수 있기에 무조건 첫번쨰 파라미터는 DB의 CreatedUserId를 넣어야 한다. + * @param sessionUserId Session에 존재하는 로그인된 사용자 Id + * @return + */ + private boolean isNotArticleCreatedUser(String dbCreatedUserId, String sessionUserId) { + return StringUtils.isBlank(dbCreatedUserId) || !dbCreatedUserId.equals(sessionUserId); + } + + + /** + * 먼저 추천, 비추천에 대한 기록도 모두 삭제하며, + *

+ * 이후 게시글을 삭제하며, 정상적으로 삭제가 되면 True, 삭제된 건수가 없으면 False를 Return한다 + * + * @param articleId + * @return + */ + private boolean isArticleDeleteSuccess(int articleId) { + articleStatusMapper.deleteArticleStatus( + new ArticleStatusDto().builder().articleId(articleId).build() + ); + + return articleMapper.deleteArticleByArticleId(articleId) > 0; + } +} diff --git a/src/main/java/com/nooblol/board/service/impl/ArticleStatusServiceImpl.java b/src/main/java/com/nooblol/board/service/impl/ArticleStatusServiceImpl.java new file mode 100644 index 00000000..62fc9990 --- /dev/null +++ b/src/main/java/com/nooblol/board/service/impl/ArticleStatusServiceImpl.java @@ -0,0 +1,98 @@ +package com.nooblol.board.service.impl; + + +import com.nooblol.board.dto.ArticleStatusDto; +import com.nooblol.board.dto.LikeAndNotLikeResponseDto; +import com.nooblol.board.mapper.ArticleMapper; +import com.nooblol.board.mapper.ArticleStatusMapper; +import com.nooblol.board.service.ArticleStatusService; +import com.nooblol.board.utils.ArticleLikeStatusEnum; +import com.nooblol.global.exception.ExceptionMessage; +import com.nooblol.global.utils.SessionUtils; +import javax.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.ObjectUtils; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ArticleStatusServiceImpl implements ArticleStatusService { + + private final ArticleMapper articleMapper; + + private final ArticleStatusMapper articleStatusMapper; + + @Override + public boolean likeArticle(int articleId, HttpSession session) { + validatedNotHaveArticle(articleId); + + ArticleStatusDto requestArticleStatusDto = + createArticleStatusDto( + articleId, SessionUtils.getSessionUserId(session), true + ); + + return statusProcess(requestArticleStatusDto); + } + + @Override + public boolean notLikeArticle(int articleId, HttpSession session) { + validatedNotHaveArticle(articleId); + + ArticleStatusDto requestArticleStatusDto = + createArticleStatusDto( + articleId, SessionUtils.getSessionUserId(session), false + ); + + return statusProcess(requestArticleStatusDto); + } + + @Override + public LikeAndNotLikeResponseDto likeAndNotListStatus(int articleId) { + return articleStatusMapper.selectArticleAllStatusByArticleId(articleId); + } + + private boolean isNotArticleInDb(int articleId) { + return ObjectUtils.isEmpty(articleMapper.selectArticleByArticleId(articleId)); + } + + private void validatedNotHaveArticle(int articleId) { + if (isNotArticleInDb(articleId)) { + throw new IllegalArgumentException(ExceptionMessage.BAD_REQUEST); + } + } + + private ArticleStatusDto createArticleStatusDto(int articleId, String userId, boolean type) { + ArticleStatusDto articleStatusDto = new ArticleStatusDto().builder() + .articleId(articleId) + .userId(userId) + .likeType(ArticleLikeStatusEnum.findLikeStatusType(type)) + .build(); + + return articleStatusDto; + } + + /** + * 추천, 비추천에 대한 프로세스, 해당 게시물에 대해 사용자가 좋아요가 없는 경우 Insert 이미 같은 타입(추천, 비추천)을 한경우는 삭제, 다른 타입인 경우는 + * Exception이 발생한다 + * + * @param requestArticleStatusDto + * @return + */ + private boolean statusProcess(ArticleStatusDto requestArticleStatusDto) { + ArticleStatusDto IsHaveStatusData = articleStatusMapper.selectArticleStatusByArticleIdAndUserId( + requestArticleStatusDto); + + if (ObjectUtils.isEmpty(IsHaveStatusData)) { + return articleStatusMapper.insertArticleStatus(requestArticleStatusDto) > 0; + } + + if (IsHaveStatusData.getLikeType().isLikeStatus() != + requestArticleStatusDto.getLikeType().isLikeStatus()) { + throw new IllegalArgumentException(ExceptionMessage.BAD_REQUEST); + } + + return articleStatusMapper.deleteArticleStatus(requestArticleStatusDto) > 0; + } +} diff --git a/src/main/java/com/nooblol/board/utils/ArticleAuthMessage.java b/src/main/java/com/nooblol/board/utils/ArticleAuthMessage.java new file mode 100644 index 00000000..1f548ba6 --- /dev/null +++ b/src/main/java/com/nooblol/board/utils/ArticleAuthMessage.java @@ -0,0 +1,6 @@ +package com.nooblol.board.utils; + + +public enum ArticleAuthMessage { + GUEST, USER, ADMIN; +} diff --git a/src/main/java/com/nooblol/board/utils/ArticleLikeStatusEnum.java b/src/main/java/com/nooblol/board/utils/ArticleLikeStatusEnum.java new file mode 100644 index 00000000..7a3ae323 --- /dev/null +++ b/src/main/java/com/nooblol/board/utils/ArticleLikeStatusEnum.java @@ -0,0 +1,31 @@ +package com.nooblol.board.utils; + +import lombok.Getter; +import lombok.Setter; + +@Getter +public enum ArticleLikeStatusEnum { + + LIKE(true), NOT_LIKE(false); + + ArticleLikeStatusEnum(boolean likeStatus) { + this.likeStatus = likeStatus; + } + + boolean likeStatus; + + public static ArticleLikeStatusEnum findLikeStatusType(boolean likeStatus) { + if (likeStatus) { + return LIKE; + } + return NOT_LIKE; + } + + + public static ArticleLikeStatusEnum findLikeStatusByInt(int num) { + if (num == 1) { + return LIKE; + } + return NOT_LIKE; + } +} diff --git a/src/main/java/com/nooblol/board/utils/ArticleLikeStatusEnumTypeHandler.java b/src/main/java/com/nooblol/board/utils/ArticleLikeStatusEnumTypeHandler.java new file mode 100644 index 00000000..13770203 --- /dev/null +++ b/src/main/java/com/nooblol/board/utils/ArticleLikeStatusEnumTypeHandler.java @@ -0,0 +1,43 @@ +package com.nooblol.board.utils; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.TypeHandler; + + +/* +ReferenceList + https://umbum.dev/1122 + https://exchangetuts.com/java-mybatis-enum-string-value-1640689684810781 + */ +public class ArticleLikeStatusEnumTypeHandler implements TypeHandler { + + + @Override + public void setParameter(PreparedStatement ps, int i, ArticleLikeStatusEnum parameter, + JdbcType jdbcType) throws SQLException { + ps.setBoolean(i, parameter.likeStatus); + } + + @Override + public ArticleLikeStatusEnum getResult(ResultSet rs, String columnName) throws SQLException { +// return ArticleLikeStatusEnum.findLikeStatusType(rs.getBoolean(columnName)); + return ArticleLikeStatusEnum.findLikeStatusByInt(rs.getInt(columnName)); + } + + @Override + public ArticleLikeStatusEnum getResult(ResultSet rs, int columnIndex) throws SQLException { +// return ArticleLikeStatusEnum.findLikeStatusType(rs.getBoolean(columnIndex)); + return ArticleLikeStatusEnum.findLikeStatusByInt(rs.getInt(columnIndex)); + } + + @Override + public ArticleLikeStatusEnum getResult(CallableStatement cs, int columnIndex) + throws SQLException { +// return ArticleLikeStatusEnum.findLikeStatusType(cs.getBoolean(columnIndex)); + return ArticleLikeStatusEnum.findLikeStatusByInt(cs.getInt(columnIndex)); + } +} diff --git a/src/main/java/com/nooblol/board/utils/ArticleMessage.java b/src/main/java/com/nooblol/board/utils/ArticleMessage.java new file mode 100644 index 00000000..2d11a46e --- /dev/null +++ b/src/main/java/com/nooblol/board/utils/ArticleMessage.java @@ -0,0 +1,18 @@ +package com.nooblol.board.utils; + +public class ArticleMessage { + + private ArticleMessage() { + } + + public static final String BBS_ID_NULL = "게시판ID가 입력되지 않았습니다."; + + public static final String ARTICLE_ID_NULL = "게시글ID가 입력되지 않았습니다."; + + public static final String ARTICLE_TITLE_NULL = "게시글 제목이 입력되지 않았습니다."; + + public static final String ARTICLE_CONTENT_NULL = "게시글 제목이 입력되지 않았습니다."; + + public static final String ARTICLE_STATUS_NULL = "해당 게시물의 타입이 입력되지 않았습니다."; + +} diff --git a/src/main/java/com/nooblol/board/utils/ArticleStatusEnum.java b/src/main/java/com/nooblol/board/utils/ArticleStatusEnum.java new file mode 100644 index 00000000..f8be4683 --- /dev/null +++ b/src/main/java/com/nooblol/board/utils/ArticleStatusEnum.java @@ -0,0 +1,17 @@ +package com.nooblol.board.utils; + +import lombok.Getter; + +@Getter +public enum ArticleStatusEnum { + ACTIVE(1), SECRET(2); + + + ArticleStatusEnum(int status) { + this.status = status; + } + + int status; + + +} diff --git a/src/main/java/com/nooblol/board/utils/BoardStatusEnum.java b/src/main/java/com/nooblol/board/utils/BoardStatusEnum.java index 5e0a7809..618a9bf2 100644 --- a/src/main/java/com/nooblol/board/utils/BoardStatusEnum.java +++ b/src/main/java/com/nooblol/board/utils/BoardStatusEnum.java @@ -7,13 +7,13 @@ public enum BoardStatusEnum { ACTIVE(1), DEACTIVE(2); + BoardStatusEnum(int status) { this.status = status; } int status; - public static boolean isExistStatus(int statusType) { return Arrays.stream(BoardStatusEnum.values()) .anyMatch(status -> status.getStatus() == statusType); diff --git a/src/main/java/com/nooblol/global/config/AuthCheckAspect.java b/src/main/java/com/nooblol/global/config/AuthCheckAspect.java index a4442963..a5480732 100644 --- a/src/main/java/com/nooblol/global/config/AuthCheckAspect.java +++ b/src/main/java/com/nooblol/global/config/AuthCheckAspect.java @@ -6,7 +6,6 @@ import java.util.Optional; import javax.servlet.http.HttpSession; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @@ -21,6 +20,7 @@ public class AuthCheckAspect { /** * 사용자로그인이 필요한 기능에서 AOP를 활용하여 사전에 로그인이 안된 사용자를 거름 + *

* * @param jp */ @@ -49,8 +49,13 @@ public void memberLoginAndRoleAdminCheck(JoinPoint jp) throws Throwable { HttpSession session = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest() .getSession(); - if (Optional.ofNullable(SessionUtils.getSessionUserRole(session)).isEmpty()) { + //사용자 정보가 존재하지 않는 경우 GUEST로 설정 + Integer userRole = Optional + .ofNullable(SessionUtils.getSessionUserRole(session)) + .orElse(UserRoleStatus.GUEST.getRoleValue()); + + if (userRole.equals(UserRoleStatus.ADMIN.getRoleValue())) { throw new IllegalArgumentException(ExceptionMessage.UNAUTHORIZED); } } -} \ No newline at end of file +} diff --git a/src/main/java/com/nooblol/global/utils/ResponseUtils.java b/src/main/java/com/nooblol/global/utils/ResponseUtils.java index b31d12dc..dce9ba33 100644 --- a/src/main/java/com/nooblol/global/utils/ResponseUtils.java +++ b/src/main/java/com/nooblol/global/utils/ResponseUtils.java @@ -1,8 +1,10 @@ package com.nooblol.global.utils; import com.nooblol.global.dto.ResponseDto; +import com.nooblol.global.exception.ExceptionMessage; import java.util.List; import org.springframework.http.HttpStatus; +import org.springframework.util.ObjectUtils; public class ResponseUtils { @@ -20,5 +22,20 @@ public static ResponseDto makeListToResponseDto(List list) { return new ResponseDto(HttpStatus.OK.value(), list); } + + /** + * Object를 받아 ResponseDto를 반환한다. + * + * @param obj + * @return + */ + public static ResponseDto makeToResponseOkDto(Object obj) { + if (ObjectUtils.isEmpty(obj)) { + throw new IllegalArgumentException(ExceptionMessage.NO_DATA); + } + ResponseDto result = ResponseEnum.OK.getResponse(); + result.setResult(obj); + return result; + } } diff --git a/src/main/java/com/nooblol/global/utils/SessionUtils.java b/src/main/java/com/nooblol/global/utils/SessionUtils.java index 5b3e3555..d1613a7d 100644 --- a/src/main/java/com/nooblol/global/utils/SessionUtils.java +++ b/src/main/java/com/nooblol/global/utils/SessionUtils.java @@ -25,4 +25,4 @@ public static Integer getSessionUserRole(HttpSession session) { } return userAttribute.getUserRole(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/nooblol/user/utils/UserRoleStatus.java b/src/main/java/com/nooblol/user/utils/UserRoleStatus.java index cab7c41a..203e8a37 100644 --- a/src/main/java/com/nooblol/user/utils/UserRoleStatus.java +++ b/src/main/java/com/nooblol/user/utils/UserRoleStatus.java @@ -1,5 +1,7 @@ package com.nooblol.user.utils; +import java.util.Arrays; + /** * Value 분류 *

@@ -21,4 +23,8 @@ public enum UserRoleStatus { public int getRoleValue() { return roleValue; } + + public static boolean isUserRoleAdmin(int roleValue) { + return ADMIN.getRoleValue() == roleValue; + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3194b8fb..67f9f034 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -24,6 +24,13 @@ spring: starttls: enable: true + #NoHandlerException Setting + mvc: + throw-exception-if-no-handler-found: true + web: + resources: + add-mappings: false + #Embedded Tomcat Session TimeOut server: servlet: @@ -37,6 +44,7 @@ mybatis: jdbc-type-for-null: null call-setters-on-nulls: true multiple-result-sets-enabled: false + default-enum-type-handler: com.nooblol.board.utils.ArticleLikeStatusEnumTypeHandler type-aliases-package: com.nooblol.*.* mapper-locations: classpath:mybatis/mapper/*/*.xml diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 00000000..8344e15e --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,41 @@ +/*password : abc*/ +INSERT INTO users(user_id, user_email, user_name, user_password_hash, user_role, level, exp, + created_at, updated_at) +VALUES ('test', 'test@test.com', 'test', + '3a81oZNherrMQXNJriBBMRLm+k6JqX6iCp7u5ktV05ohkpkqJ0/BqDa6PCOj/uu9RU1EI2Q86A4qmslPpUyknw==', + 1, 1, 0, now(), now()); + +INSERT INTO users(user_id, user_email, user_name, user_password_hash, user_role, level, exp, + created_at, updated_at) +VALUES ('test-admin-user', 'admin@test.com', 'admin', + '3a81oZNherrMQXNJriBBMRLm+k6JqX6iCp7u5ktV05ohkpkqJ0/BqDa6PCOj/uu9RU1EI2Q86A4qmslPpUyknw==', + 9, 1, 0, now(), now()); + +INSERT INTO bbs_category(category_name, status, created_user_id, created_at, updated_user_id, + updated_at) +VALUES ('Test Active Category1', 1, 'test-admin-user', now(), 'test-admin-user', now()); + +INSERT INTO bbs (category_id, bbs_name, status, created_user_id, created_at, updated_user_id, + updated_at) +VALUES (1, 'Test Active BBS', 1, 'test-admin-user', now(), 'test-admin-user', now()); + +INSERT INTO bbs_articles(article_id, bbs_id, article_title, article_read_count, article_content, + status, + created_user_id, created_at, updated_at) +VALUES (1, 1, 'Test Article Title - 1', 0, '내용이웨요', 1, 'test-admin-user', now(), now()); + +INSERT INTO bbs_articles(article_id, bbs_id, article_title, article_read_count, article_content, + status, + created_user_id, created_at, updated_at) +VALUES (2, 1, 'Test Article Title - 22', 0, '내용이웨요22', 1, 'test', now(), now()); + + + +INSERT INTO bbs_articles_status (article_id, user_id, type, created_at) +VALUES (1, 'test', true, now()); + +INSERT INTO bbs_articles_status (article_id, user_id, type, created_at) +VALUES (1, 'test2', true, now()); + +INSERT INTO bbs_articles_status (article_id, user_id, type, created_at) +VALUES (1, 'test3', false, now()); \ No newline at end of file diff --git a/src/main/resources/mybatis/mapper/board/ArticleMapper.xml b/src/main/resources/mybatis/mapper/board/ArticleMapper.xml new file mode 100644 index 00000000..1fe3d247 --- /dev/null +++ b/src/main/resources/mybatis/mapper/board/ArticleMapper.xml @@ -0,0 +1,63 @@ + + + + + + + + + UPDATE bbs_articles + SET article_read_count = article_read_count + 1 + WHERE article_id = #{articleId} + + + + + INSERT INTO bbs_articles(article_id, bbs_id, article_title, article_read_count, article_content, + status, created_user_id, created_at, updated_at) + VALUES (IFNULL(#{articleId}, IFNULL((SELECT article_id + 1 + FROM bbs_articles + ORDER BY article_id DESC limit 1), 1)), + #{bbsId}, #{articleTitle}, + #{articleReadCount}, #{articleContent}, + #{status}, #{createdUserId}, #{createdAt}, #{updatedAt}) ON DUPLICATE KEY + UPDATE + updated_at = IFNULL(VALUES (updated_at), updated_at) + , bbs_id = IFNULL(VALUES (bbs_id), bbs_id) + , article_title = IFNULL(VALUES (article_title), article_title) + , article_content = IFNULL(VALUES (article_content), article_content) + , status = IFNULL(VALUES (status), status) + + + + + + DELETE + FROM bbs_articles + WHERE article_id = #{articleId} + + \ No newline at end of file diff --git a/src/main/resources/mybatis/mapper/board/ArticleStatusMapper.xml b/src/main/resources/mybatis/mapper/board/ArticleStatusMapper.xml new file mode 100644 index 00000000..9d35d30f --- /dev/null +++ b/src/main/resources/mybatis/mapper/board/ArticleStatusMapper.xml @@ -0,0 +1,45 @@ + + + + + + + + + + INSERT INTO bbs_articles_status(article_id, user_id, type, created_at) + VALUES (#{articleId}, #{userId}, + #{likeType, typeHandler=com.nooblol.board.utils.ArticleLikeStatusEnumTypeHandler}, + #{createdAt}) + + + + + DELETE + FROM bbs_articles_status + WHERE article_id = #{articleId} + + AND user_id = #{userId} + + + + \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 8487886b..557605a4 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -137,9 +137,10 @@ CREATE TABLE `bbs` `updated_at` datetime ); +/* Upsert시 자동증가인 경우 Update도 값을 증기사키는 문제로 인한 수정*/ CREATE TABLE `bbs_articles` ( - `article_id` int PRIMARY KEY AUTO_INCREMENT, + `article_id` int PRIMARY KEY, `bbs_id` int, `article_title` varchar(255), `article_read_count` int, @@ -150,12 +151,14 @@ CREATE TABLE `bbs_articles` `updated_at` datetime ); +/* + 22. 09. 09 BBSID컬럼 삭제 : articleId로 추적이 가능하기 떄문에 해당 테이블에서는 꼭 필요하지 않다 판단. + */ CREATE TABLE `bbs_articles_status` ( `article_id` int, - `bbs_id` int, - `user_id` int, - `type` tinyint, + `user_id` varchar(255), + `type` tinyint(1), `created_at` datetime DEFAULT (now()) ); diff --git a/src/test/java/com/nooblol/board/service/impl/ArticleServiceImplTest.java b/src/test/java/com/nooblol/board/service/impl/ArticleServiceImplTest.java new file mode 100644 index 00000000..3eda97d2 --- /dev/null +++ b/src/test/java/com/nooblol/board/service/impl/ArticleServiceImplTest.java @@ -0,0 +1,353 @@ +package com.nooblol.board.service.impl; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import com.nooblol.board.dto.ArticleDto; +import com.nooblol.board.mapper.ArticleMapper; +import com.nooblol.board.mapper.ArticleStatusMapper; +import com.nooblol.board.utils.ArticleAuthMessage; +import com.nooblol.global.exception.ExceptionMessage; +import com.nooblol.global.utils.SessionEnum; +import com.nooblol.user.dto.UserDto; +import com.nooblol.user.utils.UserRoleStatus; +import javax.servlet.http.HttpSession; +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 org.springframework.mock.web.MockHttpSession; + +@ExtendWith(MockitoExtension.class) +class ArticleServiceImplTest { + + @Mock + private ArticleMapper articleMapper; + + @Mock + private ArticleStatusMapper articleStatusMapper; + + @InjectMocks + private ArticleServiceImpl articleService; + + @Test + @DisplayName("UserId가 공백인 경우, 결과값이 GUEST로 반환된다") + void getUserArticleAuth_WhenUserIdIsBlankThenReturnGuest() { + //given + + //when + String result = articleService.getUserArticleAuth(""); + + //then + assertEquals(result, ArticleAuthMessage.GUEST.name()); + } + + @Test + @DisplayName("UserId가 관리자인 경우, 결과값이 ADMIN으로 반환된다.") + void getUserArticleAuth_WhenUserIdAdminThenReturnAdmin() { + //given + String adminUserId = "test-admin-user"; + + //mock + when(articleMapper.selectUserAuth(adminUserId)).thenReturn(UserRoleStatus.ADMIN.getRoleValue()); + + //when + String result = articleService.getUserArticleAuth(adminUserId); + + //then + assertEquals(result, ArticleAuthMessage.ADMIN.name()); + } + + @Test + @DisplayName("UserId가 일반 사용자 경우, 결과값이 USER로 반환된다.") + void getUserArticleAuth_WhenUserIdUserThenReturnUser() { + //given + String userId = "test-user"; + + //mock + when(articleMapper.selectUserAuth(userId)).thenReturn(UserRoleStatus.AUTH_USER.getRoleValue()); + + //when + String result = articleService.getUserArticleAuth(userId); + + //then + assertEquals(result, ArticleAuthMessage.USER.name()); + } + + @Test + @DisplayName("articleId가 실제로 존재하지 않는 경우 Exception이 발생한다") + void getArticleInfo_WhenArticleIsNullThenNoDataException() { + //given + int emptyArticleId = 99999; + + //mock + when(articleMapper.selectArticleByArticleId(emptyArticleId)).thenReturn(null); + + //when + Exception e = assertThrows(IllegalArgumentException.class, () -> { + articleService.getArticleInfo(emptyArticleId, ""); + }); + + //then + assertEquals(e.getMessage(), ExceptionMessage.NO_DATA); + } + + @Test + @DisplayName("articleId가 실제로 존재하는 경우, 게시물 정보를 반환한다.") + void getArticleInfo_WhenArticleIdHaveDataThenReturnArticleDto() { + //given + int articleId = 1; + + ArticleDto mockData = new ArticleDto().builder() + .articleId(articleId) + .bbsId(1) + .articleTitle("Test Title") + .status(1) + .build(); + + //mock + when(articleMapper.selectArticleByArticleId(articleId)).thenReturn(mockData); + + //when + ArticleDto result = articleService.getArticleInfo(articleId, ""); + + //then + assertEquals(result, mockData); + } + + @Test + @DisplayName("일반 사용자이며 게시물을 등록하는 경우에 DB에 정상적으로 데이터가 삽입되면 결과로 True를 Return받는다") + void upsertArticle_WhenUserIsAuthUserAndInsertIsSuccessThenReturnTrue() { + //given + UserDto sessionUserData = new UserDto().builder() + .userId("test") + .userEmail("test@test.com") + .userName("test") + .userRole(UserRoleStatus.AUTH_USER.getRoleValue()) + .level(1) + .exp(0) + .build(); + + HttpSession session = new MockHttpSession(); + session.setAttribute(SessionEnum.USER_LOGIN.getValue(), sessionUserData); + + ArticleDto mockArticleDto = new ArticleDto(); + + //mock + when(articleMapper.upsertArticle(mockArticleDto)).thenReturn(1); + + //when + boolean result = articleService.upsertArticle(mockArticleDto, session, true); + + //then + assertEquals(result, true); + } + + @Test + @DisplayName("사용자 관리자이며 게시물을 등록이나 수정을 하는 경우에 DB에 정상적으로 데이터가 삽입되면 결과로 True를 Return받는다") + void upsertArticle_WhenUserIsAdminAndUpsertIsSuccessThenReturnTrue() { + //given + UserDto sessionUserData = new UserDto().builder() + .userId("test") + .userEmail("test@test.com") + .userName("test") + .userRole(UserRoleStatus.ADMIN.getRoleValue()) + .level(1) + .exp(0) + .build(); + + HttpSession session = new MockHttpSession(); + session.setAttribute(SessionEnum.USER_LOGIN.getValue(), sessionUserData); + + ArticleDto mockArticleDto = new ArticleDto(); + + //mock + when(articleMapper.upsertArticle(mockArticleDto)).thenReturn(1); + + //when + boolean result = articleService.upsertArticle(mockArticleDto, session, false); + + //then + assertEquals(result, true); + } + + @Test + @DisplayName("일반 사용자이며 게시물을 수정하는 경우에 기존 글과 동일한 사용자이면, Return값으로 True를 받는다") + void upsertArticle_WhenUserIsAuthUserAndUpdateIsSuccessThenReturnTrue() { + //given + UserDto sessionUserData = new UserDto().builder() + .userId("test") + .userEmail("test@test.com") + .userName("test") + .userRole(UserRoleStatus.AUTH_USER.getRoleValue()) + .level(1) + .exp(0) + .build(); + + HttpSession session = new MockHttpSession(); + session.setAttribute(SessionEnum.USER_LOGIN.getValue(), sessionUserData); + + ArticleDto mockArticleDto = new ArticleDto().builder().articleId(1).build(); + + //mock + when(articleMapper.upsertArticle(mockArticleDto)).thenReturn(1); + when(articleMapper.selectCreatedUserId(mockArticleDto.getArticleId())) + .thenReturn(sessionUserData.getUserId()); + + //when + boolean result = articleService.upsertArticle(mockArticleDto, session, false); + + //then + assertEquals(result, true); + } + + @Test + @DisplayName("일반 사용자이며 게시물을 수정하는 경우에 기존 글과 다른 사용자이면, Forbidden Exception이 발생한다") + void upsertArticle_WhenUserIsAuthUserAndNotEqualCreatedUserThenForbiddenException() { + //given + UserDto sessionUserData = new UserDto().builder() + .userId("test") + .userEmail("test@test.com") + .userName("test") + .userRole(UserRoleStatus.AUTH_USER.getRoleValue()) + .level(1) + .exp(0) + .build(); + + HttpSession session = new MockHttpSession(); + session.setAttribute(SessionEnum.USER_LOGIN.getValue(), sessionUserData); + + ArticleDto mockArticleDto = new ArticleDto().builder().articleId(1).build(); + + //mock + when(articleMapper.selectCreatedUserId(mockArticleDto.getArticleId())) + .thenReturn("different User"); + + //when + Exception e = assertThrows(IllegalArgumentException.class, () -> { + articleService.upsertArticle(mockArticleDto, session, false); + }); + + //then + assertEquals(e.getMessage(), ExceptionMessage.FORBIDDEN); + } + + @Test + @DisplayName("게시글을 삭제할 때 DB에 데이터가 없는 경우 NoData Exception이 발생한다.") + void deleteArticle_WhenIsNotHaveArticleInDbThenNoDataException() { + //given + int testArticleId = 1; + + //mock + when(articleMapper.selectArticleByArticleId(testArticleId)).thenReturn(null); + + //when + Exception e = assertThrows(IllegalArgumentException.class, () -> { + articleService.deleteArticle(testArticleId, null); + }); + + //then + assertEquals(e.getMessage(), ExceptionMessage.NO_DATA); + } + + @Test + @DisplayName("게시글을 삭제할 때 요청자가 관리자인 경우, 정상삭제 되었으면 Return으로 True값을 받는다.") + void deleteArticle_WhenUserIsAdminAndDeleteSuccessThenReturnTrue() { + //given + int testArticleId = 1; + + UserDto sessionUserData = new UserDto().builder() + .userId("test") + .userEmail("test@test.com") + .userName("test") + .userRole(UserRoleStatus.ADMIN.getRoleValue()) + .level(1) + .exp(0) + .build(); + HttpSession session = new MockHttpSession(); + session.setAttribute(SessionEnum.USER_LOGIN.getValue(), sessionUserData); + + ArticleDto mockReturnDto = new ArticleDto().builder() + .createdUserId(sessionUserData.getUserId()) + .build(); + + //mock + when(articleMapper.deleteArticleByArticleId(testArticleId)).thenReturn(1); + when(articleMapper.selectArticleByArticleId(testArticleId)).thenReturn(mockReturnDto); + when(articleStatusMapper.deleteArticleStatus(any())).thenReturn(1); + + //when + boolean result = articleService.deleteArticle(testArticleId, session); + + //then + assertEquals(result, true); + } + + @Test + @DisplayName("게시글을 삭제할 때 요청자가 작성자인 경우, 정상삭제 되었으면 Return으로 True값을 받는다.") + void deleteArticle_WhenUserIsAuthUserAndDeleteSuccessThenReturnTrue() { + //given + int testArticleId = 1; + + UserDto sessionUserData = new UserDto().builder() + .userId("test") + .userEmail("test@test.com") + .userName("test") + .userRole(UserRoleStatus.AUTH_USER.getRoleValue()) + .level(1) + .exp(0) + .build(); + HttpSession session = new MockHttpSession(); + session.setAttribute(SessionEnum.USER_LOGIN.getValue(), sessionUserData); + + ArticleDto mockReturnDto = new ArticleDto().builder() + .createdUserId(sessionUserData.getUserId()) + .build(); + + //mock + when(articleMapper.deleteArticleByArticleId(testArticleId)).thenReturn(1); + when(articleMapper.selectArticleByArticleId(testArticleId)).thenReturn(mockReturnDto); + when(articleStatusMapper.deleteArticleStatus(any())).thenReturn(1); + + //when + boolean result = articleService.deleteArticle(testArticleId, session); + + //then + assertEquals(result, true); + } + + @Test + @DisplayName("게시글을 삭제할 때 일반사용자가 요청시 작성자가 아닌 경우, ForBidden Exception이 발생한다.") + void deleteArticle_WhenUserIsNotCreatedUserThenForbiddenException() { + //given + int testArticleId = 1; + + UserDto sessionUserData = new UserDto().builder() + .userId("test") + .userEmail("test@test.com") + .userName("test") + .userRole(UserRoleStatus.AUTH_USER.getRoleValue()) + .level(1) + .exp(0) + .build(); + HttpSession session = new MockHttpSession(); + session.setAttribute(SessionEnum.USER_LOGIN.getValue(), sessionUserData); + + ArticleDto mockReturnDto = new ArticleDto().builder() + .createdUserId("different UserId") + .build(); + + //mock + when(articleMapper.selectArticleByArticleId(testArticleId)).thenReturn(mockReturnDto); + + //when + Exception e = assertThrows(IllegalArgumentException.class, () -> { + articleService.deleteArticle(testArticleId, session); + }); + + //then + assertEquals(e.getMessage(), ExceptionMessage.FORBIDDEN); + } +} \ No newline at end of file diff --git a/src/test/java/com/nooblol/board/service/impl/ArticleStatusServiceImplTest.java b/src/test/java/com/nooblol/board/service/impl/ArticleStatusServiceImplTest.java new file mode 100644 index 00000000..ed781df1 --- /dev/null +++ b/src/test/java/com/nooblol/board/service/impl/ArticleStatusServiceImplTest.java @@ -0,0 +1,159 @@ +package com.nooblol.board.service.impl; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import com.nooblol.board.dto.ArticleDto; +import com.nooblol.board.dto.ArticleStatusDto; +import com.nooblol.board.mapper.ArticleMapper; +import com.nooblol.board.mapper.ArticleStatusMapper; +import com.nooblol.board.utils.ArticleLikeStatusEnum; +import com.nooblol.global.exception.ExceptionMessage; +import com.nooblol.global.utils.SessionEnum; +import com.nooblol.user.dto.UserDto; +import com.nooblol.user.utils.UserRoleStatus; +import javax.servlet.http.HttpSession; +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 org.springframework.mock.web.MockHttpSession; + +@ExtendWith(MockitoExtension.class) +class ArticleStatusServiceImplTest { + + @Mock + private ArticleMapper articleMapper; + + @Mock + private ArticleStatusMapper articleStatusMapper; + + @InjectMocks + private ArticleStatusServiceImpl articleStatusService; + + + @Test + @DisplayName("게시글을 추천 또는 비추천 할 때, DB에 게시물이 존재하지 않으면 BadRequest Exception이 발생한다") + void likeArticle_WhenHaveNotInDbThenBadRequestException() { + //given + int testArticleId = 3; + + //mock + when(articleMapper.selectArticleByArticleId(testArticleId)).thenReturn(null); + + //when + Exception e = assertThrows(IllegalArgumentException.class, () -> { + articleStatusService.likeArticle(testArticleId, null); + }); + + //then + assertEquals(e.getMessage(), ExceptionMessage.BAD_REQUEST); + } + + @Test + @DisplayName("게시글을 추천 또는 비추천 하려고 할 때, 추천했던 적이 없으면 결과값으로 true를 획득한다") + void likeArticle_WhenArticleFirstLikeThenReturnTrue() { + //given + int testArticleId = 1; + + UserDto mockUserDto = new UserDto().builder() + .userId("test") + .userEmail("test@test.com") + .userName("test") + .userRole(UserRoleStatus.AUTH_USER.getRoleValue()) + .level(1) + .exp(0) + .build(); + + HttpSession session = new MockHttpSession(); + session.setAttribute(SessionEnum.USER_LOGIN.getValue(), mockUserDto); + + //mock + when(articleMapper.selectArticleByArticleId(testArticleId)).thenReturn(new ArticleDto()); + when(articleStatusMapper.selectArticleStatusByArticleIdAndUserId(any())).thenReturn(null); + when(articleStatusMapper.insertArticleStatus(any())).thenReturn(1); + + //then + boolean result = articleStatusService.likeArticle(testArticleId, session); + + assertEquals(result, true); + } + + @Test + @DisplayName("게시글을 추천할떄, 이미 비추천을 한상황이면 BadRequest Exception이 발생한다.") + void likeArticle_WhenArticleLikeAndHistoryIsNotLikeThenBadRequestException() { + //given + int testArticleId = 1; + + UserDto mockUserDto = new UserDto().builder() + .userId("test") + .userEmail("test@test.com") + .userName("test") + .userRole(UserRoleStatus.AUTH_USER.getRoleValue()) + .level(1) + .exp(0) + .build(); + + HttpSession session = new MockHttpSession(); + session.setAttribute(SessionEnum.USER_LOGIN.getValue(), mockUserDto); + + ArticleStatusDto mockArticleStatusDto = new ArticleStatusDto().builder() + .articleId(testArticleId) + .userId(mockUserDto.getUserId()) + .likeType(ArticleLikeStatusEnum.NOT_LIKE) + .build(); + + //mock + when(articleMapper.selectArticleByArticleId(testArticleId)).thenReturn(new ArticleDto()); + when(articleStatusMapper.selectArticleStatusByArticleIdAndUserId(any())).thenReturn( + mockArticleStatusDto); + + //when + Exception result = assertThrows(IllegalArgumentException.class, () -> { + articleStatusService.likeArticle(testArticleId, session); + }); + + //then + assertEquals(result.getMessage(), ExceptionMessage.BAD_REQUEST); + } + + @Test + @DisplayName("게시글을 추천할떄, 이미 추천을 한상황이면 추천했던 이력을 삭제후 Return값으로 True를 획득한다") + void likeArticle_WhenArticleLikeAndHistoryIsLikeThenReturnTrue() { + //given + int testArticleId = 1; + + UserDto mockUserDto = new UserDto().builder() + .userId("test") + .userEmail("test@test.com") + .userName("test") + .userRole(UserRoleStatus.AUTH_USER.getRoleValue()) + .level(1) + .exp(0) + .build(); + + HttpSession session = new MockHttpSession(); + session.setAttribute(SessionEnum.USER_LOGIN.getValue(), mockUserDto); + + ArticleStatusDto mockArticleStatusDto = new ArticleStatusDto().builder() + .articleId(testArticleId) + .userId(mockUserDto.getUserId()) + .likeType(ArticleLikeStatusEnum.LIKE) + .build(); + + //mock + when(articleMapper.selectArticleByArticleId(testArticleId)).thenReturn(new ArticleDto()); + when(articleStatusMapper.selectArticleStatusByArticleIdAndUserId(any())).thenReturn( + mockArticleStatusDto); + when(articleStatusMapper.deleteArticleStatus(any())).thenReturn(1); + + //when + boolean result = articleStatusService.likeArticle(testArticleId, session); + + //then + assertEquals(result, true); + } +} \ No newline at end of file