diff --git a/src/main/java/com/tomato/market/config/JpaAuditingConfig.java b/src/main/java/com/tomato/market/config/JpaAuditingConfig.java new file mode 100644 index 0000000..6eb86bc --- /dev/null +++ b/src/main/java/com/tomato/market/config/JpaAuditingConfig.java @@ -0,0 +1,9 @@ +package com.tomato.market.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaAuditingConfig { +} diff --git a/src/main/java/com/tomato/market/config/SwaggerConfig.java b/src/main/java/com/tomato/market/config/SwaggerConfig.java index cea189e..f057975 100644 --- a/src/main/java/com/tomato/market/config/SwaggerConfig.java +++ b/src/main/java/com/tomato/market/config/SwaggerConfig.java @@ -1,7 +1,14 @@ package com.tomato.market.config; +import java.lang.reflect.Type; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; @@ -17,4 +24,31 @@ public OpenAPI openApi() { return new OpenAPI().info(info); } + // swagger에 multipart/form-data 형식을 인지시키기 위한 설정 + @Component + public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter { + + /** + * "Content-Type: multipart/form-data" 헤더를 지원하는 HTTP 요청 변환기 + */ + public MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) { + super(objectMapper, MediaType.APPLICATION_OCTET_STREAM); + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return false; + } + + @Override + public boolean canWrite(Type type, Class clazz, MediaType mediaType) { + return false; + } + + @Override + protected boolean canWrite(MediaType mediaType) { + return false; + } + } + } diff --git a/src/main/java/com/tomato/market/config/WebConfig.java b/src/main/java/com/tomato/market/config/WebConfig.java index 8564879..b7998a4 100644 --- a/src/main/java/com/tomato/market/config/WebConfig.java +++ b/src/main/java/com/tomato/market/config/WebConfig.java @@ -8,7 +8,6 @@ public class WebConfig implements WebMvcConfigurer { // CORS 처리 - @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") // CORS를 적용할 URL 패턴 diff --git a/src/main/java/com/tomato/market/controller/BoardController.java b/src/main/java/com/tomato/market/controller/BoardController.java index d99de5d..f2d5f49 100644 --- a/src/main/java/com/tomato/market/controller/BoardController.java +++ b/src/main/java/com/tomato/market/controller/BoardController.java @@ -1,20 +1,41 @@ package com.tomato.market.controller; +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.List; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import com.tomato.market.data.dto.ImageDto; +import com.tomato.market.data.dto.PageDto; +import com.tomato.market.data.dto.PostDto; +import com.tomato.market.data.dto.PostListResponseDto; +import com.tomato.market.data.dto.PostResponseDto; import com.tomato.market.service.BoardService; -import jakarta.servlet.http.HttpSession; +import jakarta.validation.Valid; @RestController @RequestMapping("/api") public class BoardController { private final Logger logger = LoggerFactory.getLogger(BoardController.class); + private final BoardService boardService; @Autowired @@ -22,32 +43,118 @@ public BoardController(BoardService boardService) { this.boardService = boardService; } - @GetMapping("/board/getList") - public void getList(HttpSession session) { // 리턴 타입 변경해야할 가능성 있음 -// logger.info("BoardController.getList() is called"); -// // 세션을 확인 -// logger.info("BoardController.getList() : 세션 아이디-" + session.getId()); -// logger.info("BoardController.getList() : 세션 값-" + session.getAttribute("userId")); -// -// if (session.getAttribute("userId") != null) { // 세션이 있는 경우 -// logger.info("BoardController.getList() : 세션에 userId가 있음"); -// // 있으면 List?? 반환 -// -// // (임시) 세션이 잘 들어왔는지 확인 -// return UserResponseDto.builder() -// .status(HttpStatus.OK) -// .message(session.getAttribute("userId")) -// .build(); -// } else { // 세션이 없는 경우 -// // 없으면 유효하지 않은 세션입니다. -// logger.warn("BoardController.getList() : 세션에 userId가 없음"); -// // throw new // 예외를 던지기 -// // React에서 로그인 페이지로 Redirect -// } -// // 수정 예정 -// return UserResponseDto.builder() -// .status(HttpStatus.BAD_REQUEST) -// .message("세션이 존재하지 않음") -// .build(); + + // 계획을 수정 + // 기본 등록 메소드 부터 만든 후 + // 조회 메소드를 만들기로 함 + @PostMapping(value = "/board/writePost", consumes = {MediaType.APPLICATION_JSON_VALUE, + MediaType.MULTIPART_FORM_DATA_VALUE}) + public PostResponseDto writePost( + @RequestPart(value = "postDto") @Valid PostDto postDto, + @RequestPart(value = "images", required = false) List files) + throws IOException { // 추후 @Valid // 이미지도 받아와야 함 + logger.info("BoardController.writePost() is called"); + logger.info("BoardController.writerPost() : Validation 검증 성공"); + + // 게시글 등록 + PostDto savedPost = boardService.writePost(postDto); + logger.info("BoardController.writePost() : 게시글 저장 성공"); + + Integer postNum = savedPost.getPostNum(); // + + if (files != null) { + logger.info("BoardController.writePost() : 이미지 개수-" + files.size()); + // postID와 연관하여 이미지 등록 // Image API를 따로 분리? : DB간 관계가 성립되지 않는 문제 있음 + boardService.uploadImages(postNum, files); + logger.info("BoardController.writePost() : 이미지 저장 성공"); + } + + // 결과 리턴 + return PostResponseDto.builder() + .status(HttpStatus.OK) + .message("게시글 등록 성공") + .build(); + } + + // 조회 삭제 수정 : 각각 별도 이슈로 분리 +// PutMapping? +// updatePost() {} + + + @GetMapping(value = "/board/getPostList") + public PostListResponseDto getPostList( + @PageableDefault(page = 0, size = 10, sort = "postNum", direction = Sort.Direction.DESC) Pageable pageable, + @RequestParam(required = false) String keyword) throws MalformedURLException { + logger.info("BoardController.getPostList() is called"); +// logger.info("BoardController.getPostList() page : " + pageable.getPageNumber()); + + // 게시글 리스트를 받음 + Page postList = null; + if (keyword == null) { + logger.info("BoardController.getPostList() : 검색 키워드 없음"); + postList = boardService.getPostList(pageable); + } else { + logger.info("BoardController.getPostList() : 검색 키워드 있음"); + postList = boardService.getPostSearchList(keyword, pageable); + } + + + logger.info("BoardController.getPostList() : 게시글 리스트를 찾음"); + + int nowPage = postList.getPageable().getPageNumber() + 1; + int startPage = Math.max(nowPage - 2, 1); + int endPage = Math.min(nowPage + 2, postList.getTotalPages()); + int totalPage = postList.getTotalPages(); + + PageDto pageDto = PageDto.builder() + .nowPage(nowPage) + .startPage(startPage) + .endPage(endPage) + .totalPage(totalPage) + .build(); + + + // 찾은 postList에서 각 Post의 ID로 Image를 찾음 + List imageList = new ArrayList<>(); + for (PostDto postDto : postList) { + // 썸네일로 사용할 Image 1개만 필요 + imageList.add(boardService.getPostImage(postDto.getPostNum())); + } + logger.info("BoardController.getPostList() : 게시글의 이미지 정보를 찾음"); + + // Return + // 게시글 List 첨부 + // 이미지 List 첨부 + return PostListResponseDto.builder() + .status(HttpStatus.OK) + .message("게시글 리스트 불러오기 성공") + .postList(postList) + .imageList(imageList) + .page(pageDto) + .build(); + } + + @GetMapping("/board/getPost") // {id} 형태로 받는게 나았을 수도? + public PostResponseDto getPost(Integer postNum) { // 게시글 조회 + logger.info("BoardController.getPost() is called"); + + // 특정 값(postNum)을 받아 그 내용을 조회 + PostDto postDto = boardService.getPost(postNum); + logger.info("BoardController.getPost() : 게시글 불러오기 성공"); + + + // postNum으로 Image 데이터(다수) 조회 + List imageList = boardService.getPostImageList(postNum); + // 애초에 Post에 image 포함 여부 항목이 있었어야 했다.. // 전부 수정하는 것은 너무 복잡 + logger.info("BoardController.getPost() : 이미지 리스트 불러오기 성공"); + + + // return값으로 postDto, imageList 전달 + return PostResponseDto.builder() + .status(HttpStatus.OK) + .message("게시글 불러오기 성공") + .postDto(postDto) + .imageList(imageList) + .build(); } } diff --git a/src/main/java/com/tomato/market/dao/BoardDao.java b/src/main/java/com/tomato/market/dao/BoardDao.java new file mode 100644 index 0000000..9b86ff6 --- /dev/null +++ b/src/main/java/com/tomato/market/dao/BoardDao.java @@ -0,0 +1,25 @@ +package com.tomato.market.dao; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import com.tomato.market.data.entity.ImageEntity; +import com.tomato.market.data.entity.PostEntity; + +public interface BoardDao { + PostEntity save(PostEntity postEntity); + + ImageEntity saveImage(ImageEntity imageEntity); + + Page findPostList(Pageable pageable); + + Page findPostSearchList(String keyword, Pageable pageable); + + ImageEntity findImageByPostNum(Integer postNum); + + PostEntity findPostByPostNum(Integer postNum); + + List findImageListByPostNum(Integer postNum); +} diff --git a/src/main/java/com/tomato/market/dao/impl/BoardDaoImpl.java b/src/main/java/com/tomato/market/dao/impl/BoardDaoImpl.java new file mode 100644 index 0000000..ea18d81 --- /dev/null +++ b/src/main/java/com/tomato/market/dao/impl/BoardDaoImpl.java @@ -0,0 +1,133 @@ +package com.tomato.market.dao.impl; + +import java.util.List; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.tomato.market.dao.BoardDao; +import com.tomato.market.data.entity.ImageEntity; +import com.tomato.market.data.entity.PostEntity; +import com.tomato.market.data.repository.ImageRepository; +import com.tomato.market.data.repository.PostRepository; +import com.tomato.market.handler.exception.BoardException; + +@Service +@Transactional +public class BoardDaoImpl implements BoardDao { + private final Logger logger = LoggerFactory.getLogger(BoardDaoImpl.class); + + private final PostRepository postRepository; + private final ImageRepository imageRepository; + + @Autowired + public BoardDaoImpl(PostRepository postRepository, ImageRepository imageRepository) { + this.postRepository = postRepository; + this.imageRepository = imageRepository; + } + + + @Override + public PostEntity save(PostEntity postEntity) { + logger.info("BoardDaoImpl.save() is called"); + // Entity로 Post 등록 + PostEntity savedResult = postRepository.save(postEntity); + if (savedResult.getPostNum() != null) { + logger.info("BoardDaoImpl.save() : 데이터 저장 성공"); + return savedResult; + } else { + logger.warn("BoardDaoImpl.save() : 데이터 저장 실패"); + return null; + } + } + + @Override + public ImageEntity saveImage(ImageEntity imageEntity) { + logger.info("BoardDaoImpl.saveImage() is called"); + + ImageEntity savedResult = imageRepository.save(imageEntity); + if (savedResult.getImageNum() != null) { + logger.info("BoardDaoImpl.saveImage() : 이미지 정보 저장 성공"); + return savedResult; + } else { + logger.warn("BoardDaoImpl.saveImage() : 이미지 정보 저장 실패"); + return null; + } + } + + @Override + public Page findPostList(Pageable pageable) { + logger.info("BoardDaoImpl.findPostList() is called"); + + Page postEntities = postRepository.findAll(pageable); // 페이징 여부에 따라 수정될 여지 있음 // findPostAll + if (postEntities != null) { + logger.info("BoardDaoImpl.findPostList() : 데이터 목록 조회 성공"); + return postEntities; + } else { + logger.warn("BoardDaoImpl.findPostList() : 데이터 목록 조회 실패"); + throw new BoardException("데이터 목록을 불러오지 못했습니다."); + } + } + + @Override + public Page findPostSearchList(String keyword, Pageable pageable) { + logger.info("BoardDaoImpl.findPostSearchList() is called"); + + Page postEntities = postRepository.findByTitleContaining(keyword, pageable); + if (postEntities != null) { + logger.info("BoardDaoImpl.findPostSearchList() : 검색 목록 조회 성공"); + return postEntities; + } else { + logger.warn("BoardDaoImpl.findPostSearchList() : 검색 목록 조회 실패"); + throw new BoardException("검색 결과 목록을 불러오지 못했습니다."); + } + } + + @Override + public ImageEntity findImageByPostNum(Integer postNum) { + logger.info("BoardDaoImpl.findImageByPostNum() is called"); +// Optional imageEntity = imageRepository.findImageByPostNum(postNum); + Optional imageEntity = imageRepository.findTopByPostNumOrderByImageNum(postNum); + if (imageEntity.isPresent()) { // PostId로 이미지를 찾음 + logger.info("BoardDaoImpl.findImageByPostNum() : 이미지 조회 성공"); + return imageEntity.get(); + } else { // 이미지를 찾지 못함 or 애초에 없음 + logger.warn("BoardDaoImpl.findImageByPostNum() : 이미지 조회 실패"); + return null; + } + } + + @Override + public PostEntity findPostByPostNum(Integer postNum) { + logger.info("BoardDaoImpl.findPostByPostNum() is called"); + + Optional postEntity = postRepository.findByPostNum(postNum); + if (postEntity.isPresent()) { + logger.info("BoardDaoImpl.findPostByPostNum() : 게시글 조회 성공"); + return postEntity.get(); + } else { + logger.warn("BoardDaoImpl.findPostByPostNum() : 게시글 조회 실패"); + return null; + } + } + + @Override + public List findImageListByPostNum(Integer postNum) { + logger.info("BoarDaoImpl.findImageListByPostNum() is called"); + + List imageEntities = imageRepository.findByPostNum(postNum); + if (imageEntities == null) { + logger.warn("BoarDaoImpl.findImageListByPostNum() : 이미지 리스트 조회 실패"); + return null; + } else { + logger.info("BoardDaoImpl.findImageListByPostNum() : 이미지 리스트 조회 성공"); + return imageEntities; + } + } +} diff --git a/src/main/java/com/tomato/market/data/dto/ImageDto.java b/src/main/java/com/tomato/market/data/dto/ImageDto.java new file mode 100644 index 0000000..c58b3d7 --- /dev/null +++ b/src/main/java/com/tomato/market/data/dto/ImageDto.java @@ -0,0 +1,29 @@ +package com.tomato.market.data.dto; + +import com.tomato.market.data.entity.ImageEntity; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Builder +@Getter +@Setter +@ToString +public class ImageDto { + Integer imageNum; + Integer postNum; + String imageName; + String uuid; + + public static ImageDto toImageDto(ImageEntity imageEntity) { + + return ImageDto.builder() + .imageNum(imageEntity.getImageNum()) + .postNum(imageEntity.getPostNum()) + .imageName(imageEntity.getImageName()) + .uuid(imageEntity.getUuid()) + .build(); + } +} diff --git a/src/main/java/com/tomato/market/data/dto/PageDto.java b/src/main/java/com/tomato/market/data/dto/PageDto.java new file mode 100644 index 0000000..3d8de78 --- /dev/null +++ b/src/main/java/com/tomato/market/data/dto/PageDto.java @@ -0,0 +1,21 @@ +package com.tomato.market.data.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Builder +@Getter +@Setter +@ToString +@AllArgsConstructor +@NoArgsConstructor +public class PageDto { + int nowPage; + int startPage; + int endPage; + int totalPage; +} diff --git a/src/main/java/com/tomato/market/data/dto/PostDto.java b/src/main/java/com/tomato/market/data/dto/PostDto.java new file mode 100644 index 0000000..0c6a7d8 --- /dev/null +++ b/src/main/java/com/tomato/market/data/dto/PostDto.java @@ -0,0 +1,75 @@ +package com.tomato.market.data.dto; + +import java.util.Date; + +import com.tomato.market.data.entity.PostEntity; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Positive; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Builder +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class PostDto { + + Integer postNum; + @Pattern(message = "잘못된 아이디 형식입니다.", regexp = "^[a-z0-9_-]{6,20}") + String userId; // userId는 세션에서 들고옴 + @NotBlank(message = "지역을 입력하세요.") + String location; + @NotBlank(message = "제목을 입력하세요.") + String title; + String category; // Enum 클래스로 제한? + @NotBlank(message = "내용을 입력하세요.") + String content; + @Positive(message = "숫자만 입력 가능합니다.") + Integer price; + @NotBlank(message = "거래희망 장소를 입력하세요. ") + String detailLocation; + Integer status; // 판매 상태 : 판매중(0), 예약중(1), 판매완료(2), 취소(3) + Date createAt; // 등록 시간 + String boughtUserId; + + + public static PostEntity toPostEntity(PostDto postDto) { + + return PostEntity.builder() + .userId(postDto.getUserId()) + .location(postDto.getLocation()) + .title(postDto.getTitle()) + .category(postDto.getCategory()) + .content(postDto.getContent()) + .price(postDto.getPrice()) + .detailLocation(postDto.getDetailLocation()) + .status(postDto.getStatus()) + .boughtUserId(postDto.getBoughtUserId()) + .build(); + } + + public static PostDto toPostDto(PostEntity postEntity) { + + return PostDto.builder() + .postNum(postEntity.getPostNum()) + .userId(postEntity.getUserId()) + .location(postEntity.getLocation()) + .title(postEntity.getTitle()) + .category(postEntity.getCategory()) + .content(postEntity.getContent()) + .price(postEntity.getPrice()) + .detailLocation(postEntity.getDetailLocation()) + .status(postEntity.getStatus()) + .createAt(postEntity.getCreatedAt()) + .boughtUserId(postEntity.getBoughtUserId()) + .build(); + } +} diff --git a/src/main/java/com/tomato/market/data/dto/PostListResponseDto.java b/src/main/java/com/tomato/market/data/dto/PostListResponseDto.java new file mode 100644 index 0000000..a4a0eb1 --- /dev/null +++ b/src/main/java/com/tomato/market/data/dto/PostListResponseDto.java @@ -0,0 +1,22 @@ +package com.tomato.market.data.dto; + +import org.springframework.http.HttpStatus; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +public class PostListResponseDto { + HttpStatus status; + Object message; + Object postList; + Object imageList; + Object page; +} diff --git a/src/main/java/com/tomato/market/data/dto/PostResponseDto.java b/src/main/java/com/tomato/market/data/dto/PostResponseDto.java new file mode 100644 index 0000000..6de812d --- /dev/null +++ b/src/main/java/com/tomato/market/data/dto/PostResponseDto.java @@ -0,0 +1,21 @@ +package com.tomato.market.data.dto; + +import org.springframework.http.HttpStatus; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class PostResponseDto { + HttpStatus status; + Object message; + Object postDto; + Object imageList; +} diff --git a/src/main/java/com/tomato/market/data/entity/ImageEntity.java b/src/main/java/com/tomato/market/data/entity/ImageEntity.java new file mode 100644 index 0000000..c042f77 --- /dev/null +++ b/src/main/java/com/tomato/market/data/entity/ImageEntity.java @@ -0,0 +1,27 @@ +package com.tomato.market.data.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Builder +@Getter +@Table(name = "image") +@NoArgsConstructor +@AllArgsConstructor +public class ImageEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Integer imageNum; + Integer postNum; + String imageName; + String uuid; + +} diff --git a/src/main/java/com/tomato/market/data/entity/PostEntity.java b/src/main/java/com/tomato/market/data/entity/PostEntity.java new file mode 100644 index 0000000..d31d0b3 --- /dev/null +++ b/src/main/java/com/tomato/market/data/entity/PostEntity.java @@ -0,0 +1,45 @@ +package com.tomato.market.data.entity; + +import java.util.Date; + +import org.hibernate.annotations.ColumnDefault; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Entity +@Builder +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +@EntityListeners(AuditingEntityListener.class) +@Table(name = "post") +public class PostEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + Integer postNum; + String userId; // 관계를 가지는 키 값을 어떻게 구축? + String location; + String title; + String category; + String content; + Integer price; + String detailLocation; + @ColumnDefault("0") + Integer status; + @CreatedDate + Date createdAt; // 애노테이션? + String boughtUserId; +} diff --git a/src/main/java/com/tomato/market/data/entity/UserEntity.java b/src/main/java/com/tomato/market/data/entity/UserEntity.java index 58b39c9..090fc09 100644 --- a/src/main/java/com/tomato/market/data/entity/UserEntity.java +++ b/src/main/java/com/tomato/market/data/entity/UserEntity.java @@ -38,5 +38,4 @@ public class UserEntity { String lastLogin; Integer status; String resignReason; - } diff --git a/src/main/java/com/tomato/market/data/repository/ImageRepository.java b/src/main/java/com/tomato/market/data/repository/ImageRepository.java new file mode 100644 index 0000000..dde0f55 --- /dev/null +++ b/src/main/java/com/tomato/market/data/repository/ImageRepository.java @@ -0,0 +1,14 @@ +package com.tomato.market.data.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.tomato.market.data.entity.ImageEntity; + +public interface ImageRepository extends JpaRepository { + Optional findTopByPostNumOrderByImageNum(Integer postNum); + + List findByPostNum(Integer postNum); +} diff --git a/src/main/java/com/tomato/market/data/repository/PostRepository.java b/src/main/java/com/tomato/market/data/repository/PostRepository.java new file mode 100644 index 0000000..ccfd74e --- /dev/null +++ b/src/main/java/com/tomato/market/data/repository/PostRepository.java @@ -0,0 +1,17 @@ +package com.tomato.market.data.repository; + +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import com.tomato.market.data.entity.PostEntity; + +public interface PostRepository extends JpaRepository { + Page findAll(Pageable pageable); + + Page findByTitleContaining(String keyword, Pageable pageable); + + Optional findByPostNum(Integer postNum); +} diff --git a/src/main/java/com/tomato/market/handler/BoardExceptionHandler.java b/src/main/java/com/tomato/market/handler/BoardExceptionHandler.java new file mode 100644 index 0000000..9ab5df9 --- /dev/null +++ b/src/main/java/com/tomato/market/handler/BoardExceptionHandler.java @@ -0,0 +1,50 @@ +package com.tomato.market.handler; + +import java.util.HashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import com.tomato.market.controller.BoardController; +import com.tomato.market.data.dto.PostResponseDto; +import com.tomato.market.handler.exception.BoardException; + +@RestControllerAdvice(basePackageClasses = BoardController.class) +public class BoardExceptionHandler { + private final Logger logger = LoggerFactory.getLogger(BoardExceptionHandler.class); + + @ExceptionHandler(BoardException.class) + public PostResponseDto handlePostException(BoardException exception) { + logger.warn("BoardExceptionHandler.handlerPostException() is called"); + + return PostResponseDto.builder() + .status(HttpStatus.OK) + .message(exception.getMessage()) + .build(); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public PostResponseDto handlePostValidation(MethodArgumentNotValidException exception) { + logger.info("BoardExceptionHandler.handlePostValidation() is called"); + logger.warn("BoardExceptionHandler.handlePostValidation() : Validation 오류, 데이터 형식이 올바르지 않음"); + + // Validation 에러 맵 + HashMap validationErrorMap = new HashMap<>(); + exception.getBindingResult().getFieldErrors().forEach(error -> { + validationErrorMap.put(error.getField(), error.getDefaultMessage()); + }); + + PostResponseDto postResponseDto = PostResponseDto.builder() + .status(HttpStatus.OK) + .message(validationErrorMap) + .build(); + + return postResponseDto; + } + + +} diff --git a/src/main/java/com/tomato/market/handler/UserExceptionHandler.java b/src/main/java/com/tomato/market/handler/UserExceptionHandler.java index 0ba54ed..ddea6c4 100644 --- a/src/main/java/com/tomato/market/handler/UserExceptionHandler.java +++ b/src/main/java/com/tomato/market/handler/UserExceptionHandler.java @@ -9,10 +9,11 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import com.tomato.market.controller.UserController; import com.tomato.market.data.dto.UserResponseDto; import com.tomato.market.handler.exception.UserException; -@RestControllerAdvice +@RestControllerAdvice(basePackageClasses = UserController.class) public class UserExceptionHandler { private final Logger logger = LoggerFactory.getLogger(UserExceptionHandler.class); @@ -35,6 +36,7 @@ public UserResponseDto handleUserException(UserException exception) { @ExceptionHandler(MethodArgumentNotValidException.class) public UserResponseDto handleUserValidation(MethodArgumentNotValidException exception) { + logger.info("UserExceptionHandler.handleUserValidation() is called"); logger.warn("UserExceptionHandler.handleUserValidation() : Validation 오류, 데이터 형식이 올바르지 않음"); // Validation 에러 맵 diff --git a/src/main/java/com/tomato/market/handler/exception/BoardException.java b/src/main/java/com/tomato/market/handler/exception/BoardException.java new file mode 100644 index 0000000..6386523 --- /dev/null +++ b/src/main/java/com/tomato/market/handler/exception/BoardException.java @@ -0,0 +1,7 @@ +package com.tomato.market.handler.exception; + +public class BoardException extends RuntimeException { + public BoardException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tomato/market/service/BoardService.java b/src/main/java/com/tomato/market/service/BoardService.java index 896927c..67ee661 100644 --- a/src/main/java/com/tomato/market/service/BoardService.java +++ b/src/main/java/com/tomato/market/service/BoardService.java @@ -1,4 +1,27 @@ package com.tomato.market.service; +import java.io.IOException; +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.web.multipart.MultipartFile; + +import com.tomato.market.data.dto.ImageDto; +import com.tomato.market.data.dto.PostDto; + public interface BoardService { + PostDto writePost(PostDto postDto); + + void uploadImages(Integer postNum, List files) throws IOException; + + Page getPostList(Pageable pageable); + + Page getPostSearchList(String keyword, Pageable pageable); + + ImageDto getPostImage(Integer postNum); + + PostDto getPost(Integer postNum); + + List getPostImageList(Integer postNum); } diff --git a/src/main/java/com/tomato/market/service/impl/BoardServiceImpl.java b/src/main/java/com/tomato/market/service/impl/BoardServiceImpl.java index 52b5f30..45d1f1c 100644 --- a/src/main/java/com/tomato/market/service/impl/BoardServiceImpl.java +++ b/src/main/java/com/tomato/market/service/impl/BoardServiceImpl.java @@ -1,9 +1,187 @@ package com.tomato.market.service.impl; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import com.tomato.market.dao.BoardDao; +import com.tomato.market.data.dto.ImageDto; +import com.tomato.market.data.dto.PostDto; +import com.tomato.market.data.entity.ImageEntity; +import com.tomato.market.data.entity.PostEntity; +import com.tomato.market.handler.exception.BoardException; import com.tomato.market.service.BoardService; @Service +@Transactional public class BoardServiceImpl implements BoardService { + private final Logger logger = LoggerFactory.getLogger(BoardServiceImpl.class); + + private final BoardDao boardDao; + + @Value("${image.path}") + private String projectPath; // 이미지 경로 얻기 : profile + + @Autowired + public BoardServiceImpl(BoardDao boardDao) { + this.boardDao = boardDao; + } + + @Override + public PostDto writePost(PostDto postDto) { + logger.info("BoardServiceImpl.writePost() is called"); + + // DTO -> Entity 전환 + PostEntity postEntity = PostDto.toPostEntity(postDto); + + logger.info("BoardServiceImpl.writePost() : 게시글 등록 시도"); + PostEntity saveResult = boardDao.save(postEntity); + if (saveResult == null) { // 등록 실패 + logger.warn("BoardServiceImpl.writePost() : 게시글 등록 실패"); + throw new BoardException("게시글 등록에 실패했습니다."); + } + + // 반환 + logger.info("BoardServiceImpl.writePost() : 게시글 등록 성공"); + return PostDto.toPostDto(saveResult); + } + + @Override + public void uploadImages(Integer postNum, List files) { // 이미지 업로드 + logger.info("BoardServiceImpl.uploadImages() is called"); + + int count = 1; + for (MultipartFile file : files) { + logger.info("BoardServiceImpl.uploadImages() : 이미지" + (count++) + " 저장 시도"); + UUID uuid = UUID.randomUUID(); // UUID 생성 + String fileName = uuid + "_" + file.getOriginalFilename(); // 저장될 unique한 이름 생성 : originalName 형식 어떻게 되는지? + logger.info("BoardServiceImpl.uploadImages() : fileName-" + fileName); // 파일 이름 형식 체크 + File savedFile = new File(projectPath, fileName); // + + try { + file.transferTo(savedFile); + } catch (Exception e) { + logger.warn("BoardServiceImpl.uploadImages() : 파일 저장 실패"); + e.printStackTrace(); + throw new BoardException("이미지 파일 저장에 실패했습니다."); + } + logger.info("BoardServiceImpl.uploadImages() : 이미지 파일 저장 성공"); + + // DB에 파일 정보 저장 + ImageEntity imageEntity = + ImageEntity.builder().postNum(postNum).imageName(file.getOriginalFilename()).uuid(fileName).build(); + ImageEntity saveResult = boardDao.saveImage(imageEntity); // 어떤 식으로 저장되는지 repository 분리? + if (saveResult == null) { + logger.warn("BoardServiceImpl.uploadImages() : DB에 정보 저장 실패"); + throw new BoardException("이미지 정보 저장에 실패했습니다."); + } // 예외 처리 + logger.info("BoardServiceImpl.uploadImages() : DB에 정보 저장 성공"); + } + + logger.info("BoardServiceImpl.uploadImages() : 모든 이미지 저장 성공"); + } + + @Override + public Page getPostList(Pageable pageable) { // 페이징 예정 + logger.info("BoardServiceImpl.getPostList() is called"); + + Page postEntities = boardDao.findPostList(pageable); + if (postEntities == null) { + // 예외처리 + logger.warn("BoardServiceImpl.getPostList() : 포스트 목록 조회 실패"); + throw new BoardException("목록을 불러오지 못했습니다."); + } + logger.info("BoardServiceImpl.getPostList() : 포스트 목록 조회 성공"); + + // Entity -> DTO 전환 후 List에 추가 + Page postList = postEntities.map(PostDto::toPostDto); + + return postList; + } + + @Override + public Page getPostSearchList(String keyword, Pageable pageable) { + logger.info("BoardServiceImpl.getPostSearchList() is called"); + + Page postEntities = boardDao.findPostSearchList(keyword, pageable); + if (postEntities == null) { + logger.warn("BoardServiceImpl.getPostSearchList() : 검색 결과 목록 조회 실패"); + throw new BoardException("검색 결과 목록을 불러오지 못했습니다."); + } + logger.info("BoardServiceImpl.getPostSearchList() : 검색 결과 목록 조회 성공"); + + Page postList = postEntities.map(PostDto::toPostDto); + return postList; + } + + @Override + public ImageDto getPostImage(Integer postNum) { + logger.info("BoardServiceImpl.getPostImage() is called"); + // 게시글의 id로 image를 찾아 반환 + ImageEntity imageEntity = boardDao.findImageByPostNum(postNum); // 1개만 받는 메소드 + + // 이미지가 없는 경우, Default 이미지 전송 + if (imageEntity == null) { // 이미지 없는 게시물 + logger.info("BoardServiceImpl.getPostImage() : 이미지가 없는 포스트"); + // Default Image 반환 + imageEntity = ImageEntity.builder() + .postNum(postNum) + .imageName("default.png") + .uuid("default.png") + .build(); + } else { + logger.info("BoardServiceImpl.getPostImage() : 이미지가 있는 포스트"); + } + + // Entity -> DTO 전환하여 반환 + return ImageDto.toImageDto(imageEntity); + } + + @Override + public PostDto getPost(Integer postNum) { + logger.info("BoardServiceImpl.getPost() is called"); + + PostEntity postEntity = boardDao.findPostByPostNum(postNum); + if (postEntity == null) { + logger.warn("BoardServiceImpl.getPost() : 게시글 조회 실패"); + throw new BoardException("게시글을 찾을 수 없습니다."); + } + logger.info("BoardServiceImpl.getPost() : 게시글 조회 성공"); + + // Entity -> DTO 전환 + return PostDto.toPostDto(postEntity); + } + + @Override + public List getPostImageList(Integer postNum) { + logger.info("BoardServiceImpl.getPostImageList() is called"); + + List imageEntities = boardDao.findImageListByPostNum(postNum); + if ((imageEntities == null)) { // 이미지가 없는 데이터라면? + logger.warn("BoardServiceImpl.getPostImageList() : 이미지 조회 실패"); + throw new BoardException("이미지를 불러오지 못했습니다."); + } + + if (imageEntities.size() == 0) { + logger.warn("BoardServiceImpl.getPostImageList() : 이미지가 0개인 게시글"); + } + + // Entity -> DTO 변환 + List imageList = new ArrayList<>(); + for (ImageEntity imageEntity : imageEntities) { + imageList.add(ImageDto.toImageDto(imageEntity)); + } + return imageList; + } } diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 1c2b557..567625b 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -12,3 +12,5 @@ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect spring.redis.host=localhost spring.redis.port=6379 spring.redis.password=1111 +# image +image.path=/home/ec2-user/app/tomato/images diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties index ada444f..5ff4739 100644 --- a/src/main/resources/application-local.properties +++ b/src/main/resources/application-local.properties @@ -12,3 +12,5 @@ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect spring.redis.host=localhost spring.redis.port=6379 spring.redis.password="" +# image +image.path=C:/Users/dksgu/Desktop/Capstone/images diff --git a/src/test/java/com/tomato/market/controller/BoardControllerTest.java b/src/test/java/com/tomato/market/controller/BoardControllerTest.java new file mode 100644 index 0000000..469e29e --- /dev/null +++ b/src/test/java/com/tomato/market/controller/BoardControllerTest.java @@ -0,0 +1,380 @@ +package com.tomato.market.controller; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.List; + +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.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; +import org.springframework.web.multipart.MultipartFile; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.tomato.market.data.dto.ImageDto; +import com.tomato.market.data.dto.PostDto; +import com.tomato.market.handler.exception.BoardException; +import com.tomato.market.service.impl.BoardServiceImpl; + +@WebMvcTest(BoardController.class) +public class BoardControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private BoardServiceImpl boardService; + + // Post 입력값 + private Integer postNum = 1; + private String userId = "spring"; + private String location = "노원구"; + private String title = "제목입니다."; + private String category = "1"; + private String content = "본문입니다."; + private Integer price = 10000; + private String detailLocation = "상계동"; + private Integer status = 0; + private String boughtUserId = ""; + + private PostDto postDto; + private MockMultipartFile postFile; + + // Image 입력값 + private String fileName = "images"; + private List files = new ArrayList<>(); // + private MockMultipartFile file1; + private MockMultipartFile file2; + + private String postDtoJson = ""; // API 요청 body + + // PostList 반환값 + private List postList; + private List imageList; + + private ImageDto imageDto; + private String imageName = "original.png"; + private String uuid = "uuidoriginal.png"; + + // Page + private Pageable pageable = PageRequest.of(0, 10); + private Page postPageList; + + // Search + private String keyword = "keyword"; + + @Autowired + private WebApplicationContext ctx; + + @BeforeEach + void setUp() { + mockMvc = + MockMvcBuilders.webAppContextSetup(ctx) + .addFilters(new CharacterEncodingFilter("UTF-8", true)).build(); + + // 게시글 객체 + postDto = PostDto.builder() + .postNum(postNum) + .userId(userId) + .location(location) + .title(title) + .category(category) + .content(content) + .price(price) + .detailLocation(detailLocation) + .status(status) + .boughtUserId(boughtUserId) + .build(); + + // 이미지 객체 + file1 = new MockMultipartFile(fileName, "test1.png", MediaType.IMAGE_PNG_VALUE, "test1".getBytes()); + file2 = new MockMultipartFile(fileName, "test2.png", MediaType.IMAGE_PNG_VALUE, "test2".getBytes()); + + files.add(file1); + files.add(file2); + + // 게시글 리스트 + postList = new ArrayList<>(); + postList.add(postDto); + postList.add(postDto); + + + // 이미지 DB 정보 + imageDto = ImageDto.builder() + .postNum(postNum) + .imageName(imageName) + .uuid(uuid) + .build(); + + // 이미지 리스트 + imageList = new ArrayList<>(); + imageList.add(imageDto); + imageList.add(imageDto); + + // Page + postPageList = new PageImpl<>(postList, pageable, 2); + + } + + @Test + @DisplayName("게시글_등록_성공") + void writePostSuccess() throws Exception { + given(boardService.writePost(any(PostDto.class))).willReturn(postDto); + doNothing().when(boardService).uploadImages(postDto.getPostNum(), files); // void 반환 메소드에 대한 코드 + + // Body 생성 + // PostDto와 MultiPartFile[]이 전달됨 + postDtoJson = new ObjectMapper().writeValueAsString(postDto); // postDto와, Image가 어떻게 전송되는지? + postFile = new MockMultipartFile( + "postDto", "", "application/json", postDtoJson.getBytes()); + try { + mockMvc.perform(multipart("/api/board/writePost") + .file(file1) + .file(file2) + .file(postFile) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message", is("게시글 등록 성공"))) + .andDo(print()); + + } catch (Exception exception) { + exception.printStackTrace(); + } + + // + verify(boardService).writePost(any(PostDto.class)); + verify(boardService).uploadImages(postDto.getPostNum(), files); + } + + @Test + @DisplayName("게시글_형식_불일치") + void validationTest() throws Exception { + // 제목이 입력되지 않은 상태 가정 + title = ""; + postDto.setTitle(title); + + postDtoJson = new ObjectMapper().writeValueAsString(postDto); // postDto와, Image가 어떻게 전송되는지? + postFile = new MockMultipartFile( + "postDto", "", "application/json", postDtoJson.getBytes()); + mockMvc.perform(multipart("/api/board/writePost") + .file(file1) + .file(file2) + .file(postFile) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message.title").exists()) // message: title 존재 + .andDo(print()); + } + + @Test + @DisplayName("게시글_DTO_저장_실패") + void writePostFailure() throws Exception { + given(boardService.writePost(any(PostDto.class))).willThrow(new BoardException("게시글 등록에 실패했습니다.")); + + postDtoJson = new ObjectMapper().writeValueAsString(postDto); // postDto와, Image가 어떻게 전송되는지? + postFile = new MockMultipartFile( + "postDto", "", "application/json", postDtoJson.getBytes()); + mockMvc.perform(multipart("/api/board/writePost") + .file(file1) + .file(file2) + .file(postFile) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message", is("게시글 등록에 실패했습니다."))) + .andDo(print()); + + // + verify(boardService).writePost(any(PostDto.class)); + } + + @Test + @DisplayName("게시글_이미지_저장_실패") + void uploadImageFailure() throws Exception { + given(boardService.writePost(any(PostDto.class))).willReturn(postDto); + doThrow(new BoardException("1번째 이미지 저장에 실패했습니다.")) + .when(boardService).uploadImages(postDto.getPostNum(), files); + + postDtoJson = new ObjectMapper().writeValueAsString(postDto); // postDto와, Image가 어떻게 전송되는지? + postFile = new MockMultipartFile( + "postDto", "", "application/json", postDtoJson.getBytes()); + mockMvc.perform(multipart("/api/board/writePost") + .file(file1) + .file(file2) + .file(postFile) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message", is("1번째 이미지 저장에 실패했습니다."))) + .andDo(print()); + + // + verify(boardService).writePost(any(PostDto.class)); + verify(boardService).uploadImages(postDto.getPostNum(), files); + } + + @Test + @DisplayName("게시글_이미지_정보_저장_실패") + void saveImageInfoFailure() throws Exception { + given(boardService.writePost(any(PostDto.class))).willReturn(postDto); + doThrow(new BoardException("1번째 이미지 정보 저장에 실패했습니다.")) + .when(boardService).uploadImages(postDto.getPostNum(), files); + + postDtoJson = new ObjectMapper().writeValueAsString(postDto); // postDto와, Image가 어떻게 전송되는지? + postFile = new MockMultipartFile( + "postDto", "", "application/json", postDtoJson.getBytes()); + mockMvc.perform(multipart("/api/board/writePost") + .file(file1) + .file(file2) + .file(postFile) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message", is("1번째 이미지 정보 저장에 실패했습니다."))) + .andDo(print()); + + // + verify(boardService).writePost(any(PostDto.class)); + verify(boardService).uploadImages(postDto.getPostNum(), files); + } + + @Test + @DisplayName("게시글_리스트_불러오기_성공") + void getPostListSuccess() throws Exception { + given(boardService.getPostList(any(Pageable.class))).willReturn(postPageList); + given(boardService.getPostImage(postNum)).willReturn(imageDto); + + + mockMvc.perform(get("/api/board/getPostList")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message", is("게시글 리스트 불러오기 성공"))) + .andExpect(jsonPath("$.postList").exists()) + .andExpect(jsonPath("$.imageList").exists()) + .andDo(print()); + + verify(boardService).getPostList(any(Pageable.class)); + verify(boardService, times(2)).getPostImage(postNum); + } + + @Test + @DisplayName("게시글_리스트_게시글_불러오기_실패") + void getPostListFailure() throws Exception { + given(boardService.getPostList(any(Pageable.class))).willThrow(new BoardException("목록을 불러오지 못했습니다.")); + + mockMvc.perform(get("/api/board/getPostList")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message", is("목록을 불러오지 못했습니다."))) + .andDo(print()); + + verify(boardService).getPostList(any(Pageable.class)); + } + + @Test + @DisplayName("게시글_리스트_검색_성공") + void getPostSearchListSuccess() throws Exception { + given(boardService.getPostSearchList(any(String.class), any(Pageable.class))).willReturn(postPageList); + given(boardService.getPostImage(postNum)).willReturn(imageDto); + + mockMvc.perform(get("/api/board/getPostList").param("keyword", "keyword")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message", is("게시글 리스트 불러오기 성공"))) + .andExpect(jsonPath("$.postList").exists()) + .andExpect(jsonPath("$.imageList").exists()) + .andDo(print()); + + + verify(boardService).getPostSearchList(any(String.class), any(Pageable.class)); + verify(boardService, times(2)).getPostImage(postNum); + } + + @Test + @DisplayName("게시글_리스트_검색_실패") + void getPostSearchListFailure() throws Exception { + given(boardService.getPostSearchList(any(String.class), any(Pageable.class))).willThrow( + new BoardException("검색 결과 목록을 불러오지 못했습니다.")); + + mockMvc.perform(get("/api/board/getPostList").param("keyword", "keyword")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message", is("검색 결과 목록을 불러오지 못했습니다."))) + .andDo(print()); + + verify(boardService).getPostSearchList(any(String.class), any(Pageable.class)); + } + + @Test + @DisplayName("게시글_조회_성공") + void getPostSuccess() throws Exception { + given(boardService.getPost(postNum)).willReturn(postDto); + given(boardService.getPostImageList(postNum)).willReturn(imageList); + + mockMvc.perform(get("/api/board/getPost").param("postNum", String.valueOf(postNum))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message", is("게시글 불러오기 성공"))) + .andExpect(jsonPath("$.postDto").exists()) + .andExpect(jsonPath("$.imageList").exists()) + .andDo(print()); + + verify(boardService).getPost(postNum); + verify(boardService).getPostImageList(postNum); + } + + @Test + @DisplayName("게시글_조회_실패") + void getPostFailure() throws Exception { + given(boardService.getPost(postNum)).willThrow(new BoardException("게시글을 찾을 수 없습니다.")); + + mockMvc.perform(get("/api/board/getPost").param("postNum", String.valueOf(postNum))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message", is("게시글을 찾을 수 없습니다."))) + .andDo(print()); + + verify(boardService).getPost(postNum); + + } + + @Test + @DisplayName("게시글_이미지_리스트_조회_실패") + void getPostImageFailure() throws Exception { + given(boardService.getPost(postNum)).willReturn(postDto); + given(boardService.getPostImageList(postNum)).willThrow(new BoardException("이미지를 불러오지 못했습니다.")); + + mockMvc.perform(get("/api/board/getPost").param("postNum", String.valueOf(postNum))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message", is("이미지를 불러오지 못했습니다."))) + .andDo(print()); + + verify(boardService).getPost(postNum); + verify(boardService).getPostImageList(postNum); + } +} diff --git a/src/test/java/com/tomato/market/service/BoardServiceTest.java b/src/test/java/com/tomato/market/service/BoardServiceTest.java new file mode 100644 index 0000000..df5cc66 --- /dev/null +++ b/src/test/java/com/tomato/market/service/BoardServiceTest.java @@ -0,0 +1,321 @@ +package com.tomato.market.service; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Assertions; +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.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.web.multipart.MultipartFile; + +import com.tomato.market.dao.impl.BoardDaoImpl; +import com.tomato.market.data.dto.ImageDto; +import com.tomato.market.data.dto.PostDto; +import com.tomato.market.data.entity.ImageEntity; +import com.tomato.market.data.entity.PostEntity; +import com.tomato.market.handler.exception.BoardException; +import com.tomato.market.service.impl.BoardServiceImpl; + +@ExtendWith(MockitoExtension.class) +public class BoardServiceTest { + + @Mock + private BoardDaoImpl boardDao; + + // Post 입력값 + private Integer postNum = 1; + private String userId = "spring"; + private String location = "노원구"; + private String title = "제목입니다."; + private String category = "물건"; + private String content = "본문입니다."; + private Integer price = 10000; + private String detailLocation = "상계동"; + private Integer status = 0; + private String boughtUserId = ""; + + private PostDto postDto; + private PostEntity postEntity; + + private ImageEntity imageEntity; + private String imageName = "original.png"; + private String uuid = "uuidoriginal.png"; + + + private List files = new ArrayList<>(); + + private List postEntities; + private List imageEntities; + + private Pageable pageable = PageRequest.of(0, 10); + private Page postEntityList; + private String keyword = "keyword"; + private List postDtoList; + + private Page postDtoPage; + + + @BeforeEach + void setUp() { + postDto = PostDto.builder().userId(userId).location(location).title(title).category(category).content(content) + .price(price).detailLocation(detailLocation).status(status).boughtUserId(boughtUserId).build(); + + postEntity = PostDto.toPostEntity(postDto); + + imageEntity = ImageEntity.builder() + .postNum(postNum) + .imageName(imageName) + .uuid(uuid) + .build(); + + postEntities = new ArrayList<>(); + postEntities.add(postEntity); + postEntities.add(postEntity); + + imageEntities = new ArrayList<>(); + imageEntities.add(imageEntity); + imageEntities.add(imageEntity); + + postEntityList = new PageImpl<>(postEntities, pageable, 2); + + postDtoList = new ArrayList<>(); + postDtoList.add(PostDto.toPostDto(postEntity)); + postDtoList.add(PostDto.toPostDto(postEntity)); + + postDtoPage = new PageImpl<>(postDtoList, pageable, 2); + } + + @Test + @DisplayName("게시글_등록_성공") + void writePostSuccess() { + given(boardDao.save(any(PostEntity.class))).willReturn(postEntity); + + BoardServiceImpl boardService = new BoardServiceImpl(boardDao); + PostDto test1 = PostDto.toPostDto(postEntity); + PostDto test2 = boardService.writePost(postDto); + Assertions.assertEquals(test1.toString(), test2.toString()); + + verify(boardDao).save(any(PostEntity.class)); + } + + @Test + @DisplayName("게시글_데이터_저장_실패") + void writePostFail() { + given(boardDao.save(any(PostEntity.class))).willReturn(null); + + BoardServiceImpl boardService = new BoardServiceImpl(boardDao); + assertThrows(BoardException.class, () -> { + boardService.writePost(postDto); + }); + + verify(boardDao).save(any(PostEntity.class)); + } + + @Test + @DisplayName("이미지_등록_성공") + void uploadImageSuccess() throws IOException { + MultipartFile mockFile1 = Mockito.mock(MultipartFile.class); + MultipartFile mockFile2 = Mockito.mock(MultipartFile.class); + doNothing().when(mockFile1).transferTo(any(File.class)); + doNothing().when(mockFile2).transferTo(any(File.class)); + given(boardDao.saveImage(any(ImageEntity.class))).willReturn(imageEntity); + + BoardServiceImpl boardService = new BoardServiceImpl(boardDao); + boardService.uploadImages(postNum, List.of(mockFile1, mockFile2)); + + verify(mockFile1).transferTo(any(File.class)); + verify(mockFile2).transferTo(any(File.class)); + verify(boardDao, times(2)).saveImage(any(ImageEntity.class)); + } + + @Test + @DisplayName("이미지_파일_저장_실패") + void saveImageFileFailure() throws IOException { + MultipartFile mockFile = Mockito.mock(MultipartFile.class); + // MultipartFile의 transferTo 메소드가 IOException을 던질 때를 시뮬레이트 + doThrow(BoardException.class).when(mockFile).transferTo(any(File.class)); + + BoardServiceImpl boardService = new BoardServiceImpl(boardDao); + BoardException exception = assertThrows(BoardException.class, () -> { + boardService.uploadImages(postNum, List.of(mockFile)); + }); + Assertions.assertEquals(exception.getMessage(), "이미지 파일 저장에 실패했습니다."); + + // transferTo 메소드가 호출되었는지 확인 + verify(mockFile).transferTo(any(File.class)); + } + + @Test + @DisplayName("이미지_정보_저장_실패") + void saveImageInfoFailure() throws IOException { + MultipartFile mockFile = Mockito.mock(MultipartFile.class); + doNothing().when(mockFile).transferTo(any(File.class)); + given(boardDao.saveImage(any(ImageEntity.class))).willReturn(null); + + BoardServiceImpl boardService = new BoardServiceImpl(boardDao); + BoardException exception = assertThrows(BoardException.class, () -> { + boardService.uploadImages(postNum, List.of(mockFile)); + }); + + Assertions.assertEquals(exception.getMessage(), "이미지 정보 저장에 실패했습니다."); + + verify(mockFile).transferTo(any(File.class)); + verify(boardDao).saveImage(any(ImageEntity.class)); + } + + @Test + @DisplayName("게시글_리스트_조회_성공") + void getPostListSuccess() { + given(boardDao.findPostList(pageable)).willReturn(postEntityList); + + BoardServiceImpl boardService = new BoardServiceImpl(boardDao); + + Assertions.assertEquals(boardService.getPostList(pageable).toString(), postDtoPage.toString()); + + verify(boardDao).findPostList(pageable); + } + + @Test + @DisplayName("게시글_리스트_조회_실패") + void getPostListFailure() { + given(boardDao.findPostList(pageable)).willReturn(null); + + BoardServiceImpl boardService = new BoardServiceImpl(boardDao); + BoardException exception = Assertions.assertThrows(BoardException.class, () -> { + boardService.getPostList(pageable); + }); + Assertions.assertEquals(exception.getMessage(), "목록을 불러오지 못했습니다."); + + verify(boardDao).findPostList(pageable); + } + + @Test + @DisplayName("게시글_리스트_이미지_조회_성공") + void getPostImageSuccess() { + given(boardDao.findImageByPostNum(postNum)).willReturn(imageEntity); + + BoardServiceImpl boardService = new BoardServiceImpl(boardDao); + Assertions.assertEquals( + boardService.getPostImage(postNum).toString(), ImageDto.toImageDto(imageEntity).toString()); + + verify(boardDao).findImageByPostNum(postNum); + } + + @Test + @DisplayName("게시글_리스트_이미지_조회_실패") + void getPostImageFailure() { + given(boardDao.findImageByPostNum(postNum)).willReturn(null); + + ImageEntity defaultImage = ImageEntity.builder() + .postNum(postNum) + .imageName("default.png") + .uuid("default.png") + .build(); + + BoardServiceImpl boardService = new BoardServiceImpl(boardDao); + Assertions.assertEquals( + boardService.getPostImage(postNum).toString(), ImageDto.toImageDto(defaultImage).toString()); + + verify(boardDao).findImageByPostNum(postNum); + } + + @Test + @DisplayName("게시글_리스트_검색_성공") + void getPostSearchSuccess() { + given(boardDao.findPostSearchList(any(String.class), any(Pageable.class))).willReturn(postEntityList); + + BoardServiceImpl boardService = new BoardServiceImpl(boardDao); + Assertions.assertEquals(boardService.getPostSearchList(keyword, pageable).toString(), postDtoPage.toString()); + + verify(boardDao).findPostSearchList(any(String.class), any(Pageable.class)); + } + + @Test + @DisplayName("게시글_리스트_검색_실패") + void getPostSearchFailure() { + given(boardDao.findPostSearchList(any(String.class), any(Pageable.class))).willThrow( + new BoardException("검색 결과 목록을 불러오지 못했습니다.")); + + BoardServiceImpl boardService = new BoardServiceImpl(boardDao); + BoardException exception = Assertions.assertThrows(BoardException.class, () -> { + boardService.getPostSearchList(keyword, pageable); + }); + Assertions.assertEquals(exception.getMessage(), "검색 결과 목록을 불러오지 못했습니다."); + + verify(boardDao).findPostSearchList(any(String.class), any(Pageable.class)); + } + + @Test + @DisplayName("게시글_조회_성공") + void getPostSuccess() { + given(boardDao.findPostByPostNum(postNum)).willReturn(postEntity); + + BoardServiceImpl boardService = new BoardServiceImpl(boardDao); + Assertions.assertEquals(PostDto.toPostDto(postEntity).toString(), boardService.getPost(postNum).toString()); + + verify(boardDao).findPostByPostNum(postNum); + } + + @Test + @DisplayName("게시글_조회_실패") + void getPostFailure() { + given(boardDao.findPostByPostNum(postNum)).willReturn(null); + + BoardServiceImpl boardService = new BoardServiceImpl(boardDao); + BoardException exception = Assertions.assertThrows(BoardException.class, () -> { + boardService.getPost(postNum); + }); + Assertions.assertEquals(exception.getMessage(), "게시글을 찾을 수 없습니다."); + + verify(boardDao).findPostByPostNum(postNum); + } + + @Test + @DisplayName("게시글_이미지_리스트_조회_성공") + void getPostImageListSuccess() { + given(boardDao.findImageListByPostNum(postNum)).willReturn(imageEntities); + + List imageList = new ArrayList<>(); + for (ImageEntity image : imageEntities) { + imageList.add(ImageDto.toImageDto(image)); + } + + BoardServiceImpl boardService = new BoardServiceImpl(boardDao); + Assertions.assertEquals(imageList.toString(), boardService.getPostImageList(postNum).toString()); + + verify(boardDao).findImageListByPostNum(postNum); + } + + @Test + @DisplayName("게시글_이미지_리스트_조회_실패") + void getPostImageListFailure() { + given(boardDao.findImageListByPostNum(postNum)).willReturn(null); + + BoardServiceImpl boardService = new BoardServiceImpl(boardDao); + BoardException exception = Assertions.assertThrows(BoardException.class, () -> { + boardService.getPostImageList(postNum); + }); + Assertions.assertEquals(exception.getMessage(), "이미지를 불러오지 못했습니다."); + + verify(boardDao).findImageListByPostNum(postNum); + } +}