diff --git a/OnionHotSayYo/build.gradle b/OnionHotSayYo/build.gradle index aa93a5b..bc002b2 100644 --- a/OnionHotSayYo/build.gradle +++ b/OnionHotSayYo/build.gradle @@ -21,11 +21,14 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.mockito:mockito-junit-jupiter' testImplementation 'org.springframework.security:spring-security-test' // https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api @@ -37,8 +40,10 @@ dependencies { // https://mvnrepository.com/artifact/org.modelmapper/modelmapper implementation 'org.modelmapper:modelmapper:3.1.1' -} + + +} tasks.named('test') { useJUnitPlatform() } diff --git a/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/exceptions/BusinessRuleViolationException.java b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/exceptions/BusinessRuleViolationException.java new file mode 100644 index 0000000..8d46fb1 --- /dev/null +++ b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/exceptions/BusinessRuleViolationException.java @@ -0,0 +1,8 @@ +package org.omoknoone.onionhotsayyo.exceptions; + +public class BusinessRuleViolationException extends RuntimeException { + + public BusinessRuleViolationException(String message) { + super(message); + } +} diff --git a/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/exceptions/GlobalExceptionHandler.java b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/exceptions/GlobalExceptionHandler.java new file mode 100644 index 0000000..6abb6a6 --- /dev/null +++ b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/exceptions/GlobalExceptionHandler.java @@ -0,0 +1,28 @@ +package org.omoknoone.onionhotsayyo.exceptions; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class GlobalExceptionHandler { + + private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + @ExceptionHandler(PostNotFoundException.class) + public ResponseEntity handlePostNotFoundException(PostNotFoundException ex) { + logger.error("게시물을 찾을 수 없음: {}", ex.getMessage()); + return ResponseEntity + .status(HttpStatus.NOT_FOUND).body("요청하신 게시물을 찾을 수 없습니다(유감)."); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGeneralException(Exception ex) { + logger.error("에러 발생: {}", ex.getMessage()); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR).body("서버 내부에서 예상치 못한 에러가 발생했습니다(ㅠ.ㅠ)."); + } +} diff --git a/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/exceptions/PostNotFoundException.java b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/exceptions/PostNotFoundException.java new file mode 100644 index 0000000..b3db387 --- /dev/null +++ b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/exceptions/PostNotFoundException.java @@ -0,0 +1,7 @@ +package org.omoknoone.onionhotsayyo.exceptions; + +public class PostNotFoundException extends RuntimeException { + public PostNotFoundException(String message) { + super(message); + } +} diff --git a/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/aggregate/Category.java b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/aggregate/Category.java new file mode 100644 index 0000000..0a7db05 --- /dev/null +++ b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/aggregate/Category.java @@ -0,0 +1,21 @@ +package org.omoknoone.onionhotsayyo.post.command.aggregate; + +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@ToString +@Entity +@Table(name = "category") +public class Category { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "category_id") + private Integer categoryId; + + @Column(name = "category_name") + private String categoryName; +} diff --git a/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/aggregate/Language.java b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/aggregate/Language.java new file mode 100644 index 0000000..822d4c0 --- /dev/null +++ b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/aggregate/Language.java @@ -0,0 +1,17 @@ +package org.omoknoone.onionhotsayyo.post.command.aggregate; + +import jakarta.persistence.*; +import lombok.*; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@ToString +@Entity +@Table(name = "language") +public class Language { + + @Id + @Column(name = "language") + private String language; +} diff --git a/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/aggregate/Location.java b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/aggregate/Location.java new file mode 100644 index 0000000..114201f --- /dev/null +++ b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/aggregate/Location.java @@ -0,0 +1,24 @@ +package org.omoknoone.onionhotsayyo.post.command.aggregate; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.*; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@ToString +@Entity +@Table(name = "location") +public class Location { + + @Id + @Column(name = "location_id") + private String locationId; // 지역번호 + + @Column(name = "location") + private String location; // 시도명 + +} diff --git a/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/aggregate/Post.java b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/aggregate/Post.java new file mode 100644 index 0000000..e6679f1 --- /dev/null +++ b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/aggregate/Post.java @@ -0,0 +1,77 @@ +package org.omoknoone.onionhotsayyo.post.command.aggregate; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +@Entity +@AllArgsConstructor +@Getter +@Setter +@ToString +@Table(name = "post") +public class Post { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "post_id") + private Integer postId; + + @NotNull(message = "제목은 필수입니다.") + @Size(min = 1, max = 30, message = "제목은 1자 이상, 30자 이하이어야 합니다.") + @Column(name = "title") + private String title; + + @NotNull(message = "내용은 필수입니다.") + @Size(min = 1, message = "내용은 최소 1자 이상이어야 합니다.") + @Column(name = "content") + private String content; + + @CreationTimestamp + @Column(name = "posted_date", updatable = false) + private LocalDateTime postedDate; + + @Column(name = "hits") + private int hits = 0; // 초기 조회수는 0으로 설정됨 + + @UpdateTimestamp + @Column(name = "last_modified_date") + private LocalDateTime lastModifiedDate; + + @Column(name = "is_deleted") + private boolean isDeleted = false; // 초기 삭제 상태는 false 삭제 되지 않음으로 설정 + + @JoinColumn(name = "category_id") + private String categoryId; + + @JoinColumn(name = "member_id") + private String memberId; + + @Column(name = "image") + private String image; + + @JoinColumn(name = "location_id") + private String location; + + // 조회수 증가 + public void increaseHits() { + this.hits += 1; + } + + // 소프트 삭제 + public void markAsDeleted() { + this.isDeleted = true; + } + + public Post() { + } + +} diff --git a/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/controller/PostController.java b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/controller/PostController.java new file mode 100644 index 0000000..805602f --- /dev/null +++ b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/controller/PostController.java @@ -0,0 +1,97 @@ +package org.omoknoone.onionhotsayyo.post.command.controller; + +import org.omoknoone.onionhotsayyo.post.command.dto.MyPostListDTO; +import org.omoknoone.onionhotsayyo.post.command.dto.PostFormDTO; +import org.omoknoone.onionhotsayyo.post.command.service.PostService; +import org.omoknoone.onionhotsayyo.post.command.vo.PostDetailVO; +import org.omoknoone.onionhotsayyo.post.command.vo.PostSummaryVO; +import org.omoknoone.onionhotsayyo.post.command.vo.ResponseMyPostList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/posts") +public class PostController { + + private final PostService postService; + + Logger logger = LoggerFactory.getLogger(getClass()); + + @Autowired + public PostController(PostService postService) { + this.postService = postService; + } + + // 카테고리별 게시글 목록 조회 + @GetMapping("/list/{categoryId}") + public ResponseEntity> viewPostListByCategory(@PathVariable String categoryId) { + logger.info("카테고리별 게시글 목록 조회 요청: 카테고리 ID {}", categoryId); + List posts = postService.viewPostsByCategory(categoryId); + logger.info("카테고리 ID {}에 대한 게시글 {}개 발견", categoryId, posts.size()); + + return ResponseEntity.ok(posts); + } + + // 게시글 상세 조회 + @GetMapping("/view/{postId}") + public ResponseEntity viewPostById(@PathVariable Integer postId) { + logger.info("게시글 상세 조회 요청: 게시글 ID {}", postId); + PostDetailVO postDetail = postService.viewPostById(postId); + if (postDetail == null) { + logger.error("게시글 ID {}에 해당하는 게시글을 찾을 수 없음", postId); + return ResponseEntity.notFound().build(); + } + + logger.info("게시글 ID {}에 해당하는 게시글 찾음", postId); + return ResponseEntity.ok(postDetail); + } + + // 게시글 작성 + @PostMapping("/create") + public ResponseEntity createPost(@RequestBody PostFormDTO postFormDTO) { + logger.info("새 게시글 작성 요청: 제목 {}", postFormDTO.getTitle()); + PostFormDTO createdPost = postService.createPost(postFormDTO); + logger.info("게시글 생성 완료: 게시글 ID {}", createdPost.getTitle()); + + return new ResponseEntity<>(createdPost, HttpStatus.CREATED); + } + + // 게시글 수정 + @PutMapping("/modify/{postId}") + public ResponseEntity modifyPost(@PathVariable Integer postId, @RequestBody PostFormDTO postFormDTO) { + logger.info("게시글 수정 요청: 게시글 ID {}", postId); + PostFormDTO updatedPost = postService.modifyPost(postId, postFormDTO); + if (updatedPost == null) { + logger.error("게시글 ID {}를 찾을 수 없어 수정 불가", postId); + return ResponseEntity.notFound().build(); + } + logger.info("게시글 ID {} 수정 완료", postId); + return ResponseEntity.ok(updatedPost); + } + + // 게시글 삭제 + @DeleteMapping("/remove/{postId}") + public ResponseEntity removePost(@PathVariable Integer postId) { + logger.info("게시글 삭제 요청: 게시글 ID {}", postId); + postService.removePost(postId); + logger.info("게시글 ID {} 삭제 완료", postId); + return ResponseEntity.noContent().build(); + } + + // 내가 작성한 게시글 목록 조회 + @GetMapping("/list/mypost/{memberId}") + public ResponseEntity viewMyPosts(@PathVariable String memberId) { + logger.info("나의 게시글 리스트 요청: 맴버 ID {}", memberId); + List myPosts = postService.viewMyPosts(memberId); + logger.info("나의 게시물 리스트 조회 완료 {}", memberId); + ResponseMyPostList myPostList = new ResponseMyPostList(myPosts); + + return ResponseEntity.ok(myPostList); + } +} diff --git a/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/dto/MyPostListDTO.java b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/dto/MyPostListDTO.java new file mode 100644 index 0000000..ccd670a --- /dev/null +++ b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/dto/MyPostListDTO.java @@ -0,0 +1,26 @@ +package org.omoknoone.onionhotsayyo.post.command.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; +import java.time.LocalDateTime; + +@AllArgsConstructor +@Getter +@Setter +@ToString +public class MyPostListDTO implements Serializable { + + private Integer postId; + private String title; + private LocalDateTime postedDate; + private int hits; + private String categoryId; + private String location; + + public MyPostListDTO() { + } +} \ No newline at end of file diff --git a/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/dto/PostFormDTO.java b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/dto/PostFormDTO.java new file mode 100644 index 0000000..df3b7f8 --- /dev/null +++ b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/dto/PostFormDTO.java @@ -0,0 +1,65 @@ +package org.omoknoone.onionhotsayyo.post.command.dto; + +import lombok.*; + + +// 게시글 작성시에 작성자가 직접 입력해야 하는 데이터 +@ToString +public class PostFormDTO { + private String title; + private String content; + private String categoryId; + private String image; + private String location; // Location 엔티티로부터 location을 사용하기 위한 설정 + + public PostFormDTO() { + } + + public PostFormDTO(String title, String content, String categoryId, String image, String location) { + this.title = title; + this.content = content; + this.categoryId = categoryId; + this.image = image; + this.location = location; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getCategoryId() { + return categoryId; + } + + public void setCategoryId(String categoryId) { + this.categoryId = categoryId; + } + + public String getImage() { + return image; + } + + public void setImage(String image) { + this.image = image; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } +} diff --git a/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/repository/PostRepository.java b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/repository/PostRepository.java new file mode 100644 index 0000000..e8d4858 --- /dev/null +++ b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/repository/PostRepository.java @@ -0,0 +1,20 @@ +package org.omoknoone.onionhotsayyo.post.command.repository; + +import org.omoknoone.onionhotsayyo.post.command.aggregate.Post; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface PostRepository extends JpaRepository { + + // 카테고리 ID에 해당하는 게시물 목록 조회 메소드 + List findByCategoryId(String categoryId); + + // JPQL +// @Query("SELECT p FROM Post p WHERE p.categoryId = :categoryId AND p.isDeleted = false") +// List findActivePostsByCategoryId(@Param("categoryId") String categoryId); + + List findByMemberId(String memberId); +} diff --git a/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/service/PostService.java b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/service/PostService.java new file mode 100644 index 0000000..b1014b4 --- /dev/null +++ b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/service/PostService.java @@ -0,0 +1,44 @@ +package org.omoknoone.onionhotsayyo.post.command.service; + +import org.omoknoone.onionhotsayyo.post.command.dto.MyPostListDTO; +import org.omoknoone.onionhotsayyo.post.command.dto.PostFormDTO; +import org.omoknoone.onionhotsayyo.post.command.vo.PostDetailVO; +import org.omoknoone.onionhotsayyo.post.command.vo.PostSummaryVO; + +import java.util.List; + +public interface PostService { + + // 카테고리별 게시글 목록 조회 + List viewPostsByCategory(String categoryId); + + // 게시글 상세 조회 + PostDetailVO viewPostById(Integer postId); + + // 게시글 작성 + PostFormDTO createPost(PostFormDTO postFormDTO); + + // 게시글 수정 + PostFormDTO modifyPost(Integer postId, PostFormDTO postFormDTO); + + // 게시글 삭제 + void removePost(Integer postId); + + // 내가 작성한 게시글 목록 조회 + List viewMyPosts(String memberId); + +// // 내가 북마크한 게시글 목록 조회 +// List viewBookmarkedPosts(Integer userId); +// +// // 내가 좋아요한 게시글 목록 조회 +// List viewLikedPosts(Integer userId); +// +// // 언어별 게시글 (제목+내용) 검색 (상단 검색) +// List searchPostsByLanguageAndText(String language, String text); +// +// // 번역검색 허용 게시글 (제목+내용) 검색 (상단 검색) +// List searchTranslatablePostsByText(String text); +// +// // 카테고리 내 조건 검색 +// List searchPostsInCategoryWithCriteria(String categoryId, String criteriaType, String keyword, String location, String language); +} diff --git a/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/service/PostServiceImpl.java b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/service/PostServiceImpl.java new file mode 100644 index 0000000..e5d83f9 --- /dev/null +++ b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/service/PostServiceImpl.java @@ -0,0 +1,108 @@ +package org.omoknoone.onionhotsayyo.post.command.service; + +import org.modelmapper.ModelMapper; +import org.omoknoone.onionhotsayyo.exceptions.PostNotFoundException; +import org.omoknoone.onionhotsayyo.member.dto.MemberDTO; +import org.omoknoone.onionhotsayyo.member.service.MemberService; +import org.omoknoone.onionhotsayyo.post.command.aggregate.Post; +import org.omoknoone.onionhotsayyo.post.command.dto.MyPostListDTO; +import org.omoknoone.onionhotsayyo.post.command.dto.PostFormDTO; +import org.omoknoone.onionhotsayyo.post.command.repository.PostRepository; +import org.omoknoone.onionhotsayyo.post.command.vo.PostDetailVO; +import org.omoknoone.onionhotsayyo.post.command.vo.PostSummaryVO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class PostServiceImpl implements PostService { + + private static final Logger log = LoggerFactory.getLogger(PostServiceImpl.class); + private final PostRepository postRepository; + private final ModelMapper modelMapper; + private final MemberService memberService; + + @Autowired + public PostServiceImpl(PostRepository postRepository, ModelMapper modelMapper, MemberService memberService) { + this.postRepository = postRepository; + this.modelMapper = modelMapper; + this.memberService = memberService; + } + + @Transactional(readOnly = true) + @Override + public List viewPostsByCategory(String categoryId) { + log.info("카테고리 ID {}에 해당하는 게시물 목록 조회를 시작합니다.", categoryId); + List posts = postRepository.findByCategoryId(categoryId); + List postSummaryVOList = posts.stream() + .map(post -> modelMapper.map(post, PostSummaryVO.class)) + .collect(Collectors.toList()); + log.info("카테고리 ID {}에 해당하는 게시물 목록 조회를 완료했습니다. 조회된 게시물 수: {}", categoryId, postSummaryVOList.size()); + return postSummaryVOList; + } + + @Transactional(readOnly = true) + @Override + public PostDetailVO viewPostById(Integer postId) { + log.info("게시물 ID {}에 해당하는 게시물 상세 조회를 시작합니다.", postId); + Post post = postRepository.findById(postId).orElseThrow(() -> new PostNotFoundException("게시물 ID를 찾을 수 없습니다: " + postId)); + PostDetailVO postDetailVO = modelMapper.map(post, PostDetailVO.class); + log.info("게시물 ID {}에 해당하는 게시물 상세 조회를 완료했습니다.", postId); + return postDetailVO; + } + + @Transactional + @Override + public PostFormDTO createPost(PostFormDTO postFormDTO) { + log.info("새 게시물 생성을 시작합니다. 제목: {}", postFormDTO.getTitle()); + Post post = modelMapper.map(postFormDTO, Post.class); + Post savedPost = postRepository.save(post); + log.info("새 게시물이 성공적으로 생성되었습니다. 게시물 ID: {}", savedPost.getPostId()); + return modelMapper.map(savedPost, PostFormDTO.class); + } + + @Transactional + @Override + public PostFormDTO modifyPost(Integer postId, PostFormDTO postFormDTO) { + log.info("게시물 ID {} 수정을 시도합니다.", postId); + Post post = postRepository.findById(postId) + .orElseThrow(() -> new PostNotFoundException("게시물 ID를 찾을 수 없습니다: " + postId)); + + log.info("게시물 ID {}에 해당하는 게시물을 찾았습니다. 수정을 진행합니다.", postId); + modelMapper.map(postFormDTO, post); + Post updatedPost = postRepository.save(post); + log.info("게시물 ID {}이(가) 성공적으로 수정되었습니다.", postId); + + return modelMapper.map(updatedPost, PostFormDTO.class); + } + + @Transactional + @Override + public void removePost(Integer postId) { + log.info("게시물 ID {} (소프트)삭제를 시작합니다.", postId); + postRepository.deleteById(postId); + log.info("게시물 ID {}이(가) 성공적으로 삭제되었습니다.", postId); + } + + @Transactional + @Override + public List viewMyPosts(String memberId) { + MemberDTO member = memberService.getMemberDetailsByMemberId(memberId); + if (member == null) { + log.error("맴버 ID {} 를 찾을 수 없습니다.", memberId); + throw new UsernameNotFoundException("맴버 ID " + memberId + " 를 찾을 수 없습니다."); + } + + List posts = postRepository.findByMemberId(memberId); + log.info("맴버 ID {} 확인 되었습니다.", memberId); + return posts.stream() + .map(post -> modelMapper.map(post, MyPostListDTO.class)) + .collect(Collectors.toList()); + } +} diff --git a/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/service/PostServiceImpl_old.java b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/service/PostServiceImpl_old.java new file mode 100644 index 0000000..1d43467 --- /dev/null +++ b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/service/PostServiceImpl_old.java @@ -0,0 +1,109 @@ +//package org.omoknoone.onionhotsayyo.post.command.service; +// +//import org.modelmapper.ModelMapper; +//import org.omoknoone.onionhotsayyo.exceptions.BusinessRuleViolationException; +//import org.omoknoone.onionhotsayyo.post.command.aggregate.Location; +//import org.omoknoone.onionhotsayyo.post.command.aggregate.Post; +//import org.omoknoone.onionhotsayyo.post.command.dto.PostFormDTO; +//import org.omoknoone.onionhotsayyo.post.command.repository.LocationRepository; +//import org.omoknoone.onionhotsayyo.post.command.vo.PostDetailVO; +//import org.omoknoone.onionhotsayyo.post.command.vo.PostSummaryVO; +//import org.omoknoone.onionhotsayyo.post.command.repository.PostRepository; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.stereotype.Service; +//import org.springframework.transaction.annotation.Transactional; +// +//import java.util.List; +//import java.util.stream.Collectors; +// +//@Service +//public class PostServiceImpl implements PostService { +// +// private final PostRepository postRepository; +// private final LocationRepository locationRepository; +// private final ModelMapper modelMapper; +// +// @Autowired +// public PostServiceImpl(PostRepository postRepository, LocationRepository locationRepository, ModelMapper modelMapper) { +// this.postRepository = postRepository; +// this.locationRepository = locationRepository; +// this.modelMapper = modelMapper; +// } +// +// @Transactional(readOnly = true) +// @Override +// public List viewPostsByCategory(String categoryId) { +// +// // PostRepository 를 사용하여 특정 카테고리에 속하는 게시물 조회. +// List posts = postRepository.findByCategoryId(categoryId); +// +// // 조회된 Post 엔티티 목록을 PostSummaryVO(게시물 목록)로 반환 +// return posts.stream() +// .map(post -> modelMapper.map(post, PostSummaryVO.class)) +// .collect(Collectors.toList()); +// } +// +// @Transactional(readOnly = true) +// @Override +// public PostDetailVO viewPostById(Integer postId) { +// Post post = postRepository.findById(postId) +// .orElseThrow(() -> +// new BusinessRuleViolationException("게시글 ID " + postId + " 는 존재하지 않습니다!(ㅠ.ㅠ).")); +// +// return modelMapper.map(post, PostDetailVO.class); +// } +// +// @Transactional +// @Override +// public PostFormDTO createPost(PostFormDTO postFormDTO) { +// +// // PostFormDTO에서 Post 엔티티로 변환. Location은 별도로 처리. +// Post post = modelMapper.map(postFormDTO, Post.class); +// +// // 지역 이름으로 Location 엔티티 조회 +// Location location = locationRepository.findByLocation(postFormDTO.getLocation()); +// post.setLocation(location); // Post 엔티티에 Location 설정 +// +// // 작성된 게시글 저장 +// Post savedPost = postRepository.save(post); +// +// // 저장된 Post 엔티티를 PostFormDTO로 변환하여 반환 +// return modelMapper.map(savedPost, PostFormDTO.class); +// } +// +// @Transactional +// @Override +// public PostFormDTO modifyPost(Integer postId, PostFormDTO postFormDTO) { +// Post existingPost = postRepository.findById(postId) +// .orElseThrow(() -> +// new BusinessRuleViolationException("게시글 ID " + postId + "에 해당하는 게시글이 아니군요!.")); +// +// // PostFormDTO에서 제공하는 정보를 기반으로 기존 Post 엔티티 수정 +// existingPost.setTitle(postFormDTO.getTitle()); +// existingPost.setContent(postFormDTO.getContent()); +// existingPost.setCategoryId(postFormDTO.getCategoryId()); +// existingPost.setImage(postFormDTO.getImage()); +// +// // 지역명으로 Location 정보 업데이트 처리 +// Location location = locationRepository.findByLocation(postFormDTO.getLocation()); +// existingPost.setLocation(location); +// +// // 수정된 게시물 저장 +// Post modifiedPost = postRepository.save(existingPost); +// +// // 저장된 Post 엔티티를 PostFormDTO로 변환하여 반환 +// return modelMapper.map(modifiedPost, PostFormDTO.class); +// } +// +// @Transactional +// @Override +// public void removePost(Integer postId) { +// Post post = postRepository.findById(postId) +// .orElseThrow(() -> +// new BusinessRuleViolationException("게시글 ID " + postId + "은 이미 삭제된 게시글입니다.")); +// +// // 게시물을 소프트 삭제 처리 +// post.setDeleted(true); +// postRepository.save(post); +// } +//} diff --git a/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/vo/PostDetailVO.java b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/vo/PostDetailVO.java new file mode 100644 index 0000000..2381bc8 --- /dev/null +++ b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/vo/PostDetailVO.java @@ -0,0 +1,27 @@ +package org.omoknoone.onionhotsayyo.post.command.vo; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@ToString +public class PostDetailVO { + + private Integer postingId; + private String title; + private String content; + private LocalDateTime postedDate; + private int hits = 0; // 초기 조회수는 0으로 설정됨 + private LocalDateTime lastModifiedDate; + private boolean isDeleted = false; // 초기 삭제 상태는 false 삭제 되지 않음으로 설정 + private String categoryId; + private String memberId; + private String image; + private String location; +} diff --git a/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/vo/PostSummaryVO.java b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/vo/PostSummaryVO.java new file mode 100644 index 0000000..aa70a13 --- /dev/null +++ b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/vo/PostSummaryVO.java @@ -0,0 +1,26 @@ +package org.omoknoone.onionhotsayyo.post.command.vo; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.omoknoone.onionhotsayyo.post.command.aggregate.Location; + +import java.time.LocalDateTime; + + +// 게시물의 목록에 포함되어야 하는 데이터 +@NoArgsConstructor +@AllArgsConstructor +@Getter +@ToString +public class PostSummaryVO { + + private int postingId; + private String title; + private LocalDateTime postedDate; + private int hits; + private String categoryId; + private String location; + +} diff --git a/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/vo/RequestMyPostList.java b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/vo/RequestMyPostList.java new file mode 100644 index 0000000..70470e5 --- /dev/null +++ b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/vo/RequestMyPostList.java @@ -0,0 +1,21 @@ +package org.omoknoone.onionhotsayyo.post.command.vo; + +public class RequestMyPostList { + + private final String memberId; + + public RequestMyPostList(String memberId) { + this.memberId = memberId; + } + + public String getMemberId() { + return memberId; + } + + @Override + public String toString() { + return "RequestMyPosts{" + + "memberId='" + memberId + '\'' + + '}'; + } +} diff --git a/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/vo/ResponseMyPostList.java b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/vo/ResponseMyPostList.java new file mode 100644 index 0000000..cdbc573 --- /dev/null +++ b/OnionHotSayYo/src/main/java/org/omoknoone/onionhotsayyo/post/command/vo/ResponseMyPostList.java @@ -0,0 +1,25 @@ +package org.omoknoone.onionhotsayyo.post.command.vo; + +import org.omoknoone.onionhotsayyo.post.command.dto.MyPostListDTO; + +import java.util.List; + +public class ResponseMyPostList { + private final List myPosts; + + public ResponseMyPostList(List myPosts) { + this.myPosts = myPosts; + } + + public List getMyPosts() { + return myPosts; + } + + @Override + public String toString() { + return "ResponseMyPosts{" + + "myPosts=" + myPosts + + '}'; + } +} + diff --git a/OnionHotSayYo/src/main/resources/application.yml b/OnionHotSayYo/src/main/resources/application.yml index e69de29..9676701 100644 --- a/OnionHotSayYo/src/main/resources/application.yml +++ b/OnionHotSayYo/src/main/resources/application.yml @@ -0,0 +1,31 @@ +spring: + application: + name: onion-hot-say-yo + datasource: + driver-class-name: org.mariadb.jdbc.Driver + url: jdbc:mariadb://192.168.0.138/onionhotsayyo + # username: master + # password: master + username: root + password: mariadb + + jpa: + generate-ddl: false + show-sql: true + database: mysql + properties: + hibernate: + '[format_sql]': true + +token: + access-expiration-time: 43200000 # 12H + refresh-expiration-time: 604800000 # 7D + secret: iW50hkbOkPd92kEi4Z+hdBtNlGoaUwI0Aa9UY+Fb5jZyQK7Sm1dbEIaRauzhHyu6NVOT6bUZwXk3tOhiBuggnA== # ??? ? + +deepl: + api-key: 5de79c27-6426-4a36-8c08-f06e9f425d08:fx + + +#logging: +# level: +# root: debug \ No newline at end of file diff --git a/OnionHotSayYo/src/test/java/org/omoknoone/onionhotsayyo/post/command/service/PostServiceImplTest.java b/OnionHotSayYo/src/test/java/org/omoknoone/onionhotsayyo/post/command/service/PostServiceImplTest.java new file mode 100644 index 0000000..fd0741a --- /dev/null +++ b/OnionHotSayYo/src/test/java/org/omoknoone/onionhotsayyo/post/command/service/PostServiceImplTest.java @@ -0,0 +1,199 @@ +//package org.omoknoone.onionhotsayyo.post.command.service; +// +//import org.junit.jupiter.api.DisplayName; +//import org.junit.jupiter.api.Nested; +//import org.junit.jupiter.api.Test; +//import org.modelmapper.ModelMapper; +//import org.omoknoone.onionhotsayyo.exceptions.BusinessRuleViolationException; +//import org.omoknoone.onionhotsayyo.post.command.aggregate.Location; +//import org.omoknoone.onionhotsayyo.post.command.aggregate.Post; +//import org.omoknoone.onionhotsayyo.post.command.dto.PostFormDTO; +//import org.omoknoone.onionhotsayyo.post.command.repository.LocationRepository; +//import org.omoknoone.onionhotsayyo.post.command.vo.PostDetailVO; +//import org.omoknoone.onionhotsayyo.post.command.vo.PostSummaryVO; +//import org.omoknoone.onionhotsayyo.post.command.repository.PostRepository; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +//import org.springframework.boot.test.mock.mockito.MockBean; +// +//import java.time.LocalDateTime; +//import java.util.Arrays; +//import java.util.List; +//import java.util.Optional; +//import java.util.stream.Collectors; +// +//import static org.junit.jupiter.api.Assertions.*; +//import static org.mockito.ArgumentMatchers.anyString; +//import static org.mockito.Mockito.*; +// +//@SpringBootTest +//class PostServiceImplTests { +// +// private static final Logger logger = LoggerFactory.getLogger(PostServiceImplTests.class); +// +// @MockBean +// private PostRepository postRepository; +// +// @MockBean +// private LocationRepository locationRepository; +// +// @MockBean +// private ModelMapper modelMapper; +// +// @Autowired +// private PostService postService; +// +// // @Nested를 활용하여 테스트 메소드를 그룹화 +// @Nested +// @DisplayName("Category ID로 게시물 조회 테스트") +// class ViewPostsByCategoryTests { +// @Test +// @DisplayName("조회 성공 테스트") +// void viewPostsByCategorySuccessTest() { +// +// String categoryId = "여행"; +// +// // Given +// List mockPosts = Arrays.asList( +// new Post(1, "첫 번째 게시물", "내용1", LocalDateTime.now(), 100, +// LocalDateTime.now(), false, +// categoryId, "회원1", "image1.jpg", "서울"), +// +// new Post(2, "두 번째 게시물", "내용2", LocalDateTime.now(), 200, +// LocalDateTime.now(), false, +// categoryId, "회원2", "image2.jpg", "부산") +// ); +// +// List expectedVOs = Arrays.asList( +// new PostSummaryVO(1, "첫 번째 게시물", +// LocalDateTime.now(), 100, categoryId, "서울"), +// +// new PostSummaryVO(2, "두 번째 게시물", +// LocalDateTime.now(), 200, categoryId, "부산") +// ); +// +// when(postRepository.findByCategoryId(categoryId)).thenReturn(mockPosts); +// for (int i = 0; i < mockPosts.size(); i++) { +// when(modelMapper.map(mockPosts.get(i), PostSummaryVO.class)).thenReturn(expectedVOs.get(i)); +// } +// +// // When +// List actualVOs = postService.viewPostsByCategory(categoryId); +// +// // Then +// assertNotNull(actualVOs); +// assertEquals(expectedVOs.size(), actualVOs.size()); +// // 여기서는 VO 객체들의 실제 값을 비교하는 로직이 필요합니다. +// // 예: assertEquals(expectedVOs.get(i).getTitle(), actualVOs.get(i).getTitle()); +// +// verify(postRepository).findByCategoryId(categoryId); +// mockPosts.forEach(post -> verify(modelMapper).map(post, PostSummaryVO.class)); +// +// } +// +// @Nested +// @DisplayName("게시글 ID로 조회 테스트") +// class ViewPostByIdTests { +// @Test +// @DisplayName("상세 게시글 조회 성공") +// void viewPostById_Success() { +// int postId = 1; +// String location = "서울"; +// +// // Post 인스턴스 생성 +// Post mockPost = new Post(postId, "샘플 제목", "샘플 내용", LocalDateTime.now(), 100, +// LocalDateTime.now(), false, "trip", "회원1", "image1.jpg", location); +// +// // 예상되는 PostDetailVO 객체 생성 +// PostDetailVO expectedDetailVO = new PostDetailVO(postId, "샘플 제목", "샘플 내용", +// LocalDateTime.now(), 100, LocalDateTime.now(), false, +// "trip", "회원1", "image1.jpg", location); +// +// // PostRepository와 ModelMapper의 동작을 모킹 +// when(postRepository.findById(postId)).thenReturn(Optional.of(mockPost)); +// when(modelMapper.map(mockPost, PostDetailVO.class)).thenReturn(expectedDetailVO); +// +// // 실제 서비스 메소드를 호출하여 결과를 검증 +// PostDetailVO result = postService.viewPostById(postId); +// +// assertNotNull(result); +// assertEquals(expectedDetailVO.getTitle(), result.getTitle()); +// assertEquals(expectedDetailVO.getContent(), result.getContent()); +// assertEquals(expectedDetailVO.getHits(), result.getHits()); +// assertEquals(expectedDetailVO.getLocation(), result.getLocation()); +// logger.info("게시글 상세 조회 성공: {}", result); +// +// // 저장소 및 ModelMapper 사용 확인 +// verify(postRepository).findById(postId); +// verify(modelMapper).map(mockPost, PostDetailVO.class); +// } +// +// @Test +// @DisplayName("게시글 미존재 시 예외 발생") +// void viewPostById_NotFound() { +// int postId = 999; +// +// when(postRepository.findById(postId)).thenReturn(Optional.empty()); +// +// Exception exception = assertThrows(BusinessRuleViolationException.class, () -> { +// postService.viewPostById(postId); +// }); +// +// String expectedMessage = "게시글 ID " + postId + "에 해당하는 게시글을 찾을 수 없습니다!"; +// assertEquals(expectedMessage, exception.getMessage()); +// +// logger.info("게시글 ID {} 조회 실패: 게시글을 찾을 수 없습니다.", postId); +// +// verify(postRepository).findById(postId); +// } +// } +// +// +// private void verifyInteractionsAndAssertFields(List postSummaryVOs) { +// assertEquals("첫 번째 게시물", postSummaryVOs.get(0).getTitle()); +// assertEquals("두 번째 게시물", postSummaryVOs.get(1).getTitle()); +// assertNotNull(postSummaryVOs.get(0).getPostedDate()); +// assertNotNull(postSummaryVOs.get(1).getPostedDate()); +// assertEquals(100, postSummaryVOs.get(0).getHits()); +// assertEquals(150, postSummaryVOs.get(1).getHits()); +// +// String categoryId = "trip"; +// +// // 저장소와의 상호작용 확인 +// verify(postRepository, times(1)).findByCategoryId(categoryId); +// logger.info("postRepository의 findByCategoryId 메소드가 " + categoryId + " 인자와 함께 정확히 한 번 호출됨"); +// } +// +// private Post createMockPostSummary(String categoryId, String title, +// LocalDateTime postedDate, int hits, String image) { +// Post post = new Post(); +// post.setCategoryId(categoryId); +// post.setTitle(title); +// post.setPostedDate(postedDate); +// post.setHits(hits); +// post.setImage(image); +// return post; +// } +// +// private Post createMockPostDetail(int postingId, String title, String content, LocalDateTime postedDate, +// int hits, String image, String categoryId, String memberId, +// boolean isDeleted, LocalDateTime lastModifiedDate, Location location) { +// Post post = new Post(); +// post.setPostingId(postingId); +// post.setTitle(title); +// post.setContent(content); +// post.setPostedDate(postedDate); +// post.setHits(hits); +// post.setImage(image); +// post.setCategoryId(categoryId); +// post.setMemberId(memberId); +// post.setDeleted(isDeleted); +// post.setLastModifiedDate(lastModifiedDate); +// post.setLocation(location); // 지역 직접 설정 +// return post; +// } +// +//} + diff --git a/OnionHotSayYo/src/test/java/org/omoknoone/onionhotsayyo/post/command/service/PostServiceImplTests.java b/OnionHotSayYo/src/test/java/org/omoknoone/onionhotsayyo/post/command/service/PostServiceImplTests.java new file mode 100644 index 0000000..822d3ab --- /dev/null +++ b/OnionHotSayYo/src/test/java/org/omoknoone/onionhotsayyo/post/command/service/PostServiceImplTests.java @@ -0,0 +1,150 @@ +package org.omoknoone.onionhotsayyo.post.command.service; + +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.modelmapper.ModelMapper; +import org.omoknoone.onionhotsayyo.post.command.aggregate.Post; +import org.omoknoone.onionhotsayyo.post.command.dto.PostFormDTO; +import org.omoknoone.onionhotsayyo.post.command.repository.PostRepository; +import org.omoknoone.onionhotsayyo.post.command.vo.PostDetailVO; +import org.omoknoone.onionhotsayyo.post.command.vo.PostSummaryVO; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class PostServiceImplTests { + @Mock + private PostRepository postRepository; + + @Mock + private ModelMapper modelMapper; + + @InjectMocks + private PostServiceImpl postService; + + @Test + void whenViewPostsByCategory_thenReturnsPostSummaryList() { + // Arrange + String categoryId = "testCategory"; + List posts = new ArrayList<>(); + posts.add(new Post()); + when(postRepository.findByCategoryId(categoryId)).thenReturn(posts); + when(modelMapper.map(any(Post.class), eq(PostSummaryVO.class))).thenReturn(new PostSummaryVO()); + + // Act + List result = postService.viewPostsByCategory(categoryId); + + // Assert + assertFalse(result.isEmpty()); + } + + @Test + void whenViewPostById_thenReturnsPostDetail() { + // Arrange + Integer postId = 1; + Post post = new Post(); + when(postRepository.findById(postId)).thenReturn(Optional.of(post)); + when(modelMapper.map(any(Post.class), eq(PostDetailVO.class))).thenReturn(new PostDetailVO()); + + // Act + PostDetailVO result = postService.viewPostById(postId); + + // Assert + assertNotNull(result); + } + + @Test + void whenCreatePost_thenReturnsPostFormDTO() { + // Arrange + PostFormDTO postFormDTO = new PostFormDTO(); + Post post = new Post(); + when(modelMapper.map(any(PostFormDTO.class), eq(Post.class))).thenReturn(post); + when(postRepository.save(any(Post.class))).thenReturn(post); + when(modelMapper.map(any(Post.class), eq(PostFormDTO.class))).thenReturn(postFormDTO); + + // Act + PostFormDTO result = postService.createPost(postFormDTO); + + // Assert + assertNotNull(result); + } + +// @Test +// void whenModifyPostWithExistingPost_thenReturnsUpdatedPostFormDTO() throws Exception { +// // Arrange +// Integer postId = 1; +// PostFormDTO postFormDTO = new PostFormDTO(); +// postFormDTO.setTitle("My Updated Title"); +// Post existingPost = new Post(); // 기존 Post 엔티티 준비 +// Post updatedPost = new Post(); // 업데이트된 Post 엔티티 준비 +// +// // 리플렉션을 사용하여 updatedPost 객체의 title 필드 값을 설정합니다. +// setTitleUsingReflection(existingPost, "Original Title"); +// setTitleUsingReflection(updatedPost, "My Updated Title"); +// +// when(postRepository.findById(postId)).thenReturn(Optional.of(existingPost)); +// +//// ModelMapper 스터빙을 수정하여, 정확한 객체 매핑을 보장합니다. +// when(modelMapper.map(any(PostFormDTO.class), eq(Post.class))).thenAnswer(invocation -> { +// PostFormDTO dto = invocation.getArgument(0); +// Post post = existingPost; // 기존 Post 엔티티를 사용합니다. +// // 리플렉션을 사용하여 Post 객체의 title 필드 값을 설정합니다. +// Field field = Post.class.getDeclaredField("title"); +// field.setAccessible(true); +// field.set(post, dto.getTitle()); // DTO에서 제공된 제목으로 Post 엔티티의 title 필드를 업데이트합니다. +// return post; // 업데이트된 Post 엔티티를 반환합니다. +// }); +// +// when(postRepository.save(any(Post.class))).thenReturn(updatedPost); // 업데이트된 Post 엔티티를 저장하고 반환합니다. +// +// when(modelMapper.map(any(Post.class), eq(PostFormDTO.class))).thenReturn(postFormDTO); // Post 엔티티를 PostFormDTO로 매핑합니다. +// +// when(postRepository.save(any(Post.class))).thenReturn(updatedPost); +// +// // Act +// PostFormDTO result = postService.modifyPost(postId, postFormDTO); +// +// // Assert +// assertNotNull(result.getTitle()); // 수정된 PostFormDTO의 제목이 null이 아닌지 확인 +// assertEquals("My Updated Title", result.getTitle()); +// +// // Verify +// verify(postRepository).findById(postId); +//// verify(modelMapper).map(eq(postFormDTO), eq(Post.class)); // 변경 +// verify(postRepository).save(any(Post.class)); +// +// } +// +// // 헬퍼 메소드: Post 객체의 title 필드에 값을 설정합니다. +// private void setTitleUsingReflection(Post testPost, String title) +// throws NoSuchFieldException, IllegalAccessException { +// Field field = Post.class.getDeclaredField("title"); +// field.setAccessible(true); +// field.set(testPost, title); +// +// } + + @Test + void whenRemovePost_thenPostIsDeleted() { + // Arrange + Integer postId = 1; + doNothing().when(postRepository).deleteById(postId); + + // Act + postService.removePost(postId); + + // Assert + verify(postRepository).deleteById(postId); + } +}