diff --git a/src/main/java/com/prgrms/prolog/domain/comment/model/Comment.java b/src/main/java/com/prgrms/prolog/domain/comment/model/Comment.java index 4d7d0b7..59e867d 100644 --- a/src/main/java/com/prgrms/prolog/domain/comment/model/Comment.java +++ b/src/main/java/com/prgrms/prolog/domain/comment/model/Comment.java @@ -7,7 +7,6 @@ import javax.persistence.Entity; import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; diff --git a/src/main/java/com/prgrms/prolog/domain/post/api/PostController.java b/src/main/java/com/prgrms/prolog/domain/post/api/PostController.java index e82627a..8b23f65 100644 --- a/src/main/java/com/prgrms/prolog/domain/post/api/PostController.java +++ b/src/main/java/com/prgrms/prolog/domain/post/api/PostController.java @@ -22,16 +22,16 @@ import com.prgrms.prolog.domain.post.dto.PostRequest.CreateRequest; import com.prgrms.prolog.domain.post.dto.PostRequest.UpdateRequest; import com.prgrms.prolog.domain.post.dto.PostResponse; -import com.prgrms.prolog.domain.post.service.PostService; +import com.prgrms.prolog.domain.post.service.PostServiceImpl; import com.prgrms.prolog.global.jwt.JwtAuthentication; @RestController @RequestMapping("/api/v1/posts") public class PostController { - private final PostService postService; + private final PostServiceImpl postService; - public PostController(PostService postService) { + public PostController(PostServiceImpl postService) { this.postService = postService; } @@ -58,9 +58,11 @@ public ResponseEntity> findAll(Pageable pageable) { } @PatchMapping("/{id}") - public ResponseEntity update(@PathVariable Long id, + public ResponseEntity update( + @PathVariable Long id, + @AuthenticationPrincipal JwtAuthentication user, @Valid @RequestBody UpdateRequest postRequest) { - PostResponse update = postService.update(id, postRequest); + PostResponse update = postService.update(postRequest, user.id(), id); return ResponseEntity.ok(update); } diff --git a/src/main/java/com/prgrms/prolog/domain/post/dto/PostRequest.java b/src/main/java/com/prgrms/prolog/domain/post/dto/PostRequest.java index 78595db..5e3380e 100644 --- a/src/main/java/com/prgrms/prolog/domain/post/dto/PostRequest.java +++ b/src/main/java/com/prgrms/prolog/domain/post/dto/PostRequest.java @@ -3,12 +3,15 @@ import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size; +import org.springframework.lang.Nullable; + import com.prgrms.prolog.domain.post.model.Post; import com.prgrms.prolog.domain.user.model.User; public class PostRequest { public record CreateRequest(@NotBlank @Size(max = 200) String title, @NotBlank String content, + @Nullable String tagText, boolean openStatus) { public static Post toEntity(CreateRequest create, User user) { return Post.builder() @@ -22,7 +25,7 @@ public static Post toEntity(CreateRequest create, User user) { public record UpdateRequest(@NotBlank @Size(max = 200) String title, @NotBlank String content, + @Nullable String tagText, boolean openStatus) { - } } diff --git a/src/main/java/com/prgrms/prolog/domain/post/dto/PostResponse.java b/src/main/java/com/prgrms/prolog/domain/post/dto/PostResponse.java index 96d5f93..2285f91 100644 --- a/src/main/java/com/prgrms/prolog/domain/post/dto/PostResponse.java +++ b/src/main/java/com/prgrms/prolog/domain/post/dto/PostResponse.java @@ -1,8 +1,10 @@ package com.prgrms.prolog.domain.post.dto; +import static com.prgrms.prolog.domain.posttag.dto.PostTagDto.*; import static com.prgrms.prolog.domain.user.dto.UserDto.UserProfile.*; import java.util.List; +import java.util.Set; import com.prgrms.prolog.domain.comment.model.Comment; import com.prgrms.prolog.domain.post.model.Post; @@ -12,11 +14,17 @@ public record PostResponse(String title, String content, boolean openStatus, UserProfile user, + Set tags, List comment, int commentCount) { public static PostResponse toPostResponse(Post post) { - return new PostResponse(post.getTitle(), post.getContent(), post.isOpenStatus(), - toUserProfile(post.getUser()), post.getComments(), post.getComments().size()); + return new PostResponse(post.getTitle(), + post.getContent(), + post.isOpenStatus(), + toUserProfile(post.getUser()), + PostTagsResponse.from(post.getPostTags()).tagNames(), + post.getComments(), + post.getComments().size()); } } diff --git a/src/main/java/com/prgrms/prolog/domain/post/model/Post.java b/src/main/java/com/prgrms/prolog/domain/post/model/Post.java index 647b685..0146c26 100644 --- a/src/main/java/com/prgrms/prolog/domain/post/model/Post.java +++ b/src/main/java/com/prgrms/prolog/domain/post/model/Post.java @@ -1,15 +1,17 @@ package com.prgrms.prolog.domain.post.model; +import static javax.persistence.CascadeType.*; import static javax.persistence.FetchType.*; import static javax.persistence.GenerationType.*; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import javax.persistence.Entity; import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.Lob; @@ -20,6 +22,7 @@ import com.prgrms.prolog.domain.comment.model.Comment; import com.prgrms.prolog.domain.post.dto.PostRequest.UpdateRequest; +import com.prgrms.prolog.domain.posttag.model.PostTag; import com.prgrms.prolog.domain.user.model.User; import com.prgrms.prolog.global.common.BaseEntity; @@ -58,6 +61,9 @@ public class Post extends BaseEntity { @OneToMany(mappedBy = "post") private final List comments = new ArrayList<>(); + @OneToMany(mappedBy = "post", cascade = ALL) + private final Set postTags = new HashSet<>(); + @Builder public Post(String title, String content, boolean openStatus, User user) { this.title = validateTitle(title); @@ -95,6 +101,10 @@ public void changePost(UpdateRequest updateRequest) { this.openStatus = updateRequest.openStatus(); } + public void addPostTagsFrom(List postTags) { + this.postTags.addAll(postTags); + } + private String validateTitle(String title) { checkText(title); checkOverLength(title, TITLE_MAX_SIZE); diff --git a/src/main/java/com/prgrms/prolog/domain/post/repository/PostRepository.java b/src/main/java/com/prgrms/prolog/domain/post/repository/PostRepository.java index 5d6b3f8..9cacba6 100644 --- a/src/main/java/com/prgrms/prolog/domain/post/repository/PostRepository.java +++ b/src/main/java/com/prgrms/prolog/domain/post/repository/PostRepository.java @@ -1,8 +1,28 @@ package com.prgrms.prolog.domain.post.repository; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import com.prgrms.prolog.domain.post.model.Post; public interface PostRepository extends JpaRepository { + + @Query(""" + SELECT p + FROM Post p + LEFT JOIN FETCH p.comments c + where p.id = :postId + """) + Optional joinCommentFindById(@Param(value = "postId") Long postId); + + @Query(""" + SELECT p + FROM Post p + LEFT JOIN FETCH p.user + WHERE p.id = :postId + """) + Optional joinUserFindById(@Param(value = "postId") Long postId); } diff --git a/src/main/java/com/prgrms/prolog/domain/post/service/PostService.java b/src/main/java/com/prgrms/prolog/domain/post/service/PostService.java index 7d2d774..175c166 100644 --- a/src/main/java/com/prgrms/prolog/domain/post/service/PostService.java +++ b/src/main/java/com/prgrms/prolog/domain/post/service/PostService.java @@ -2,62 +2,14 @@ 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.prgrms.prolog.domain.post.dto.PostRequest.CreateRequest; -import com.prgrms.prolog.domain.post.dto.PostRequest.UpdateRequest; +import com.prgrms.prolog.domain.post.dto.PostRequest; import com.prgrms.prolog.domain.post.dto.PostResponse; -import com.prgrms.prolog.domain.post.model.Post; -import com.prgrms.prolog.domain.post.repository.PostRepository; -import com.prgrms.prolog.domain.user.model.User; -import com.prgrms.prolog.domain.user.repository.UserRepository; -@Service -@Transactional -public class PostService { - - private static final String POST_NOT_EXIST_MESSAGE = "존재하지 않는 게시물입니다."; - private static final String USER_NOT_EXIST_MESSAGE = "존재하지 않는 사용자입니다."; - - private final PostRepository postRepository; - private final UserRepository userRepository; - - public PostService(PostRepository postRepository, UserRepository userRepository) { - this.postRepository = postRepository; - this.userRepository = userRepository; - } - - public Long save(CreateRequest create, Long userId) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new IllegalArgumentException(USER_NOT_EXIST_MESSAGE)); - Post post = postRepository.save(CreateRequest.toEntity(create, user)); - return post.getId(); - } - - @Transactional(readOnly = true) - public PostResponse findById(Long id) { - return postRepository.findById(id) - .map(PostResponse::toPostResponse) - .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE)); - } - - @Transactional(readOnly = true) - public Page findAll(Pageable pageable) { - return postRepository.findAll(pageable) - .map(PostResponse::toPostResponse); - } - - public PostResponse update(Long id, UpdateRequest update) { - Post post = postRepository.findById(id) - .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE)); - post.changePost(update); - return PostResponse.toPostResponse(post); - } - - public void delete(Long id) { - Post findPost = postRepository.findById(id) - .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE)); - postRepository.delete(findPost); - } -} \ No newline at end of file +public interface PostService { + public Long save(PostRequest.CreateRequest request, Long userId); + public PostResponse findById(Long postId); + public Page findAll(Pageable pageable); + public PostResponse update(PostRequest.UpdateRequest update, Long userId, Long postId); + public void delete(Long id); +} diff --git a/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java b/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java new file mode 100644 index 0000000..cf379cd --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/post/service/PostServiceImpl.java @@ -0,0 +1,202 @@ +package com.prgrms.prolog.domain.post.service; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +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.prgrms.prolog.domain.post.dto.PostRequest.CreateRequest; +import com.prgrms.prolog.domain.post.dto.PostRequest.UpdateRequest; +import com.prgrms.prolog.domain.post.dto.PostResponse; +import com.prgrms.prolog.domain.post.model.Post; +import com.prgrms.prolog.domain.post.repository.PostRepository; +import com.prgrms.prolog.domain.posttag.model.PostTag; +import com.prgrms.prolog.domain.posttag.repository.PostTagRepository; +import com.prgrms.prolog.domain.roottag.model.RootTag; +import com.prgrms.prolog.domain.roottag.repository.RootTagRepository; +import com.prgrms.prolog.domain.roottag.util.TagConverter; +import com.prgrms.prolog.domain.user.model.User; +import com.prgrms.prolog.domain.user.repository.UserRepository; +import com.prgrms.prolog.domain.usertag.model.UserTag; +import com.prgrms.prolog.domain.usertag.repository.UserTagRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class PostServiceImpl implements PostService { + + private static final String POST_NOT_EXIST_MESSAGE = "존재하지 않는 게시물입니다."; + private static final String USER_NOT_EXIST_MESSAGE = "존재하지 않는 사용자입니다."; + + private final PostRepository postRepository; + private final UserRepository userRepository; + private final RootTagRepository rootTagRepository; + private final PostTagRepository postTagRepository; + private final UserTagRepository userTagRepository; + + @Override + @Transactional + public Long save(CreateRequest request, Long userId) { + User findUser = userRepository.joinUserTagFindByUserId(userId); + Post createdPost = CreateRequest.toEntity(request, findUser); + Post savedPost = postRepository.save(createdPost); + updateNewPostAndUserIfTagExists(request.tagText(), savedPost, findUser); + return savedPost.getId(); + } + + @Override + public PostResponse findById(Long postId) { + Post post = postRepository.joinCommentFindById(postId) + .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE)); + List findPostTags = postTagRepository.joinPostTagFindByPostId(postId); + post.addPostTagsFrom(findPostTags); + return PostResponse.toPostResponse(post); + } + + @Override + public Page findAll(Pageable pageable) { + return postRepository.findAll(pageable) + .map(PostResponse::toPostResponse); + } + + @Override + @Transactional + public PostResponse update(UpdateRequest update, Long userId, Long postId) { + Post findPost = postRepository.joinUserFindById(postId) + .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE)); + + if (!findPost.getUser().checkSameUserId(userId)) { + throw new IllegalArgumentException("exception.post.not.owner"); + } + + findPost.changePost(update); + updatePostAndUserIfTagChanged(update.tagText(), findPost); + return PostResponse.toPostResponse(findPost); + } + + @Override + @Transactional + public void delete(Long id) { + Post findPost = postRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException(POST_NOT_EXIST_MESSAGE)); + postRepository.delete(findPost); + } + + private void updatePostAndUserIfTagChanged(String tagText, Post findPost) { + Set tagNames = TagConverter.convertFrom(tagText); + Set currentRootTags = rootTagRepository.findByTagNamesIn(tagNames); + Set newTagNames = distinguishNewTagNames(tagNames, currentRootTags); + Set oldRootTags = distinguishOldRootTags(tagNames, findPost.getPostTags()); + Set savedNewRootTags = saveNewRootTags(newTagNames); + currentRootTags.addAll(savedNewRootTags); + + removeOldPostTags(findPost, oldRootTags); + saveNewPostTags(findPost, savedNewRootTags); + removeOrDecreaseUserTags(findPost.getUser(), oldRootTags); + } + + private void removeOldPostTags(Post post, Set oldRootTags) { + if (oldRootTags.isEmpty()) { + return; + } + List rootTagIds = oldRootTags.stream() + .map(RootTag::getId) + .toList(); + postTagRepository.deleteByPostIdAndRootTagIds(post.getId(), rootTagIds); + } + + private void removeOrDecreaseUserTags(User user, Set oldRootTags) { + Map userTagMap = getFindUserTagMap(user, oldRootTags); + for (RootTag rootTag : oldRootTags) { + if (userTagMap.containsKey(rootTag.getId())) { + userTagMap.get(rootTag.getId()).decreaseCount(1); + } + } + } + + private Set distinguishOldRootTags(Set tagNames, Set postTags) { + Set oldRootTags = new HashSet<>(); + for (PostTag postTag : postTags) { + String postTagName = postTag.getRootTag().getName(); + boolean isPostTagRemoved = !tagNames.contains(postTagName); + if (isPostTagRemoved) { + oldRootTags.add(postTag.getRootTag()); + } + } + return oldRootTags; + } + + private void updateNewPostAndUserIfTagExists(String tagText, Post savedPost, User findUser) { + Set tagNames = TagConverter.convertFrom(tagText); + if (tagNames.isEmpty()) { + return; + } + + Set currentRootTags = rootTagRepository.findByTagNamesIn(tagNames); + Set newTagNames = distinguishNewTagNames(tagNames, currentRootTags); + Set savedNewRootTags = saveNewRootTags(newTagNames); + currentRootTags.addAll(savedNewRootTags); + saveNewPostTags(savedPost, currentRootTags); + saveOrIncreaseUserTags(findUser, currentRootTags); + } + + private void saveOrIncreaseUserTags(User user, Set rootTags) { + Map userTagMap = getFindUserTagMap(user, rootTags); + for (RootTag rootTag : rootTags) { + boolean isUserTagExists = userTagMap.containsKey(rootTag.getId()); + if (isUserTagExists) { + userTagMap.get(rootTag.getId()).increaseCount(1); + } + if (!isUserTagExists) { + userTagRepository.save(UserTag.builder() + .user(user) + .rootTag(rootTag) + .count(1) + .build()); + } + } + } + + private Map getFindUserTagMap(User user, Set rootTags) { + List rootTagIds = rootTags.stream() + .map(RootTag::getId) + .toList(); + return userTagRepository.findByUserIdAndInRootTagIds(user.getId(), rootTagIds) + .stream() + .collect(Collectors.toMap(userTag -> userTag.getRootTag().getId(), userTag -> userTag)); + } + + private void saveNewPostTags(Post post, Set rootTags) { + rootTags.forEach(rootTag -> postTagRepository.save( + PostTag.builder() + .rootTag(rootTag) + .post(post) + .build())); + } + + private Set distinguishNewTagNames(Set newTagNames, Set rootTags) { + for (RootTag rootTag : rootTags) { + newTagNames.remove(rootTag.getName()); + } + return newTagNames; + } + + private Set saveNewRootTags(Set newTagNames) { + if (newTagNames.isEmpty()) { + return Collections.emptySet(); + } + return newTagNames.stream() + .map(RootTag::new) + .map(rootTagRepository::save) + .collect(Collectors.toSet()); + } +} \ No newline at end of file diff --git a/src/main/java/com/prgrms/prolog/domain/posttag/dto/PostTagDto.java b/src/main/java/com/prgrms/prolog/domain/posttag/dto/PostTagDto.java new file mode 100644 index 0000000..f302461 --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/posttag/dto/PostTagDto.java @@ -0,0 +1,18 @@ +package com.prgrms.prolog.domain.posttag.dto; + +import java.util.Set; +import java.util.stream.Collectors; + +import com.prgrms.prolog.domain.posttag.model.PostTag; + +public class PostTagDto { + + public record PostTagsResponse(Set tagNames) { + public static PostTagsResponse from(Set postTags) { + Set postTagNames = postTags.stream() + .map(postTag -> postTag.getRootTag().getName()) + .collect(Collectors.toSet()); + return new PostTagsResponse(postTagNames); + } + } +} diff --git a/src/main/java/com/prgrms/prolog/domain/posttag/model/PostTag.java b/src/main/java/com/prgrms/prolog/domain/posttag/model/PostTag.java new file mode 100644 index 0000000..a4e8abc --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/posttag/model/PostTag.java @@ -0,0 +1,56 @@ +package com.prgrms.prolog.domain.posttag.model; + +import static javax.persistence.FetchType.*; +import static javax.persistence.GenerationType.*; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; + +import org.springframework.util.Assert; + +import com.prgrms.prolog.domain.post.model.Post; +import com.prgrms.prolog.domain.roottag.model.RootTag; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@EqualsAndHashCode +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class PostTag { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "post_id") + private Post post; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "root_tag_id") + private RootTag rootTag; + + @Builder + public PostTag(Post post, RootTag rootTag) { + this.post = validatePost(post); + this.rootTag = validateRootTag(rootTag); + } + + private RootTag validateRootTag(RootTag rootTag) { + Assert.notNull(rootTag, "exception.postTag.rootTag.null"); + return rootTag; + } + + private Post validatePost(Post post) { + Assert.notNull(post, "exception.postTag.post.null"); + return post; + } +} diff --git a/src/main/java/com/prgrms/prolog/domain/posttag/repository/PostTagRepository.java b/src/main/java/com/prgrms/prolog/domain/posttag/repository/PostTagRepository.java new file mode 100644 index 0000000..ca27fd3 --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/posttag/repository/PostTagRepository.java @@ -0,0 +1,31 @@ +package com.prgrms.prolog.domain.posttag.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.prgrms.prolog.domain.posttag.model.PostTag; + +public interface PostTagRepository extends JpaRepository { + + @Query(""" + DELETE + FROM PostTag pt + WHERE pt.post.id = :postId + AND pt.rootTag.id IN :rootTagIds + """) + void deleteByPostIdAndRootTagIds( + @Param(value = "postId") Long postId, + @Param(value = "rootTagIds") List rootTagIds + ); + + @Query(""" + SELECT pt + FROM PostTag pt + LEFT JOIN FETCH pt.rootTag + WHERE pt.post.id = :postId + """) + List joinPostTagFindByPostId(@Param(value = "postId") Long postId); +} diff --git a/src/main/java/com/prgrms/prolog/domain/roottag/model/RootTag.java b/src/main/java/com/prgrms/prolog/domain/roottag/model/RootTag.java new file mode 100644 index 0000000..4243f54 --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/roottag/model/RootTag.java @@ -0,0 +1,56 @@ +package com.prgrms.prolog.domain.roottag.model; + +import static javax.persistence.GenerationType.*; + +import java.util.HashSet; +import java.util.Set; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.validation.constraints.NotNull; + +import org.springframework.util.Assert; + +import com.prgrms.prolog.domain.posttag.model.PostTag; +import com.prgrms.prolog.domain.usertag.model.UserTag; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@EqualsAndHashCode +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class RootTag { + + private static final int NAME_MAX_LENGTH = 100; + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @NotNull + private String name; + + @OneToMany(mappedBy = "rootTag") + private final Set postTags = new HashSet<>(); + + @OneToMany(mappedBy = "rootTag") + private final Set userTag = new HashSet<>(); + + @Builder + public RootTag(String name) { + this.name = validateRootTagName(name); + } + + private String validateRootTagName(String name) { + Assert.hasText(name, "exception.rootTag.name.text"); + Assert.isTrue(name.length() <= NAME_MAX_LENGTH, "exception.rootTag.name.length"); + return name; + } +} diff --git a/src/main/java/com/prgrms/prolog/domain/roottag/repository/RootTagRepository.java b/src/main/java/com/prgrms/prolog/domain/roottag/repository/RootTagRepository.java new file mode 100644 index 0000000..a8202a5 --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/roottag/repository/RootTagRepository.java @@ -0,0 +1,19 @@ +package com.prgrms.prolog.domain.roottag.repository; + +import java.util.Set; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.prgrms.prolog.domain.roottag.model.RootTag; + +public interface RootTagRepository extends JpaRepository { + + @Query(""" + SELECT rt + FROM RootTag rt + WHERE rt.name IN :tagNames + """) + Set findByTagNamesIn(@Param(value = "tagNames") Set tagNames); +} diff --git a/src/main/java/com/prgrms/prolog/domain/roottag/util/TagConverter.java b/src/main/java/com/prgrms/prolog/domain/roottag/util/TagConverter.java new file mode 100644 index 0000000..e668390 --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/roottag/util/TagConverter.java @@ -0,0 +1,24 @@ +package com.prgrms.prolog.domain.roottag.util; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; + +public class TagConverter { + + private static final String TAG_EXPRESSION = "#"; + + private TagConverter() { + } + + public static Set convertFrom(String tagNames) { + if (tagNames == null || tagNames.isBlank()) { + return Collections.emptySet(); + } + + return Arrays.stream(tagNames.split(TAG_EXPRESSION)) + .filter(tagName -> !tagName.isBlank()) + .collect(Collectors.toSet()); + } +} diff --git a/src/main/java/com/prgrms/prolog/domain/user/model/User.java b/src/main/java/com/prgrms/prolog/domain/user/model/User.java index 6f4b132..0d1945d 100644 --- a/src/main/java/com/prgrms/prolog/domain/user/model/User.java +++ b/src/main/java/com/prgrms/prolog/domain/user/model/User.java @@ -1,8 +1,12 @@ package com.prgrms.prolog.domain.user.model; +import static javax.persistence.CascadeType.*; + import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -18,6 +22,7 @@ import com.prgrms.prolog.domain.comment.model.Comment; import com.prgrms.prolog.domain.post.model.Post; +import com.prgrms.prolog.domain.usertag.model.UserTag; import com.prgrms.prolog.global.common.BaseEntity; import lombok.AccessLevel; @@ -49,6 +54,8 @@ public class User extends BaseEntity { private final List posts = new ArrayList<>(); @OneToMany(mappedBy = "user") private final List comments = new ArrayList<>(); + @OneToMany(mappedBy = "user", cascade = ALL) + private final Set userTags = new HashSet<>(); @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -138,6 +145,14 @@ public boolean checkSameEmail(String email) { return this.email.equals(email); } + public void removeUserTag(UserTag userTag) { + this.userTags.remove(userTag); + } + + public boolean checkSameUserId(Long userId) { + return Objects.equals(this.id, userId); + } + @Override public String toString() { return "User{" diff --git a/src/main/java/com/prgrms/prolog/domain/user/repository/UserRepository.java b/src/main/java/com/prgrms/prolog/domain/user/repository/UserRepository.java index 0136ef5..04bd078 100644 --- a/src/main/java/com/prgrms/prolog/domain/user/repository/UserRepository.java +++ b/src/main/java/com/prgrms/prolog/domain/user/repository/UserRepository.java @@ -4,6 +4,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import com.prgrms.prolog.domain.user.model.User; @@ -18,4 +19,12 @@ public interface UserRepository extends JpaRepository { and u.oauthId = :oauthId """) Optional findByProviderAndOauthId(String provider, String oauthId); + + @Query(""" + SELECT u + FROM User u + LEFT JOIN FETCH u.userTags + WHERE u.id = :userId + """) + User joinUserTagFindByUserId(@Param(value = "userId") Long userId); } diff --git a/src/main/java/com/prgrms/prolog/domain/usertag/model/UserTag.java b/src/main/java/com/prgrms/prolog/domain/usertag/model/UserTag.java new file mode 100644 index 0000000..a14bf22 --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/usertag/model/UserTag.java @@ -0,0 +1,84 @@ +package com.prgrms.prolog.domain.usertag.model; + +import static javax.persistence.FetchType.*; +import static javax.persistence.GenerationType.*; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.validation.constraints.PositiveOrZero; + +import org.springframework.util.Assert; + +import com.prgrms.prolog.domain.roottag.model.RootTag; +import com.prgrms.prolog.domain.user.model.User; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@EqualsAndHashCode +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class UserTag { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @PositiveOrZero + private Integer count; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "root_tag_id") + private RootTag rootTag; + + public UserTag(User user, RootTag rootTag) { + this.user = validateUser(user); + this.rootTag = validateRootTag(rootTag); + } + + @Builder + public UserTag(Integer count, User user, RootTag rootTag) { + this.user = validateUser(user); + this.rootTag = validateRootTag(rootTag); + this.count = validateCount(count); + } + + public void increaseCount(int count) { + Assert.isTrue(count >= 0, "exception.userTag.count.positive"); + this.count += count; + } + + public void decreaseCount(int count) { + Assert.isTrue(count >= 0, "exception.userTag.count.positive"); + this.count -= count; + if (count == 0) { + this.user.removeUserTag(this); + } + } + + private RootTag validateRootTag(RootTag rootTag) { + Assert.notNull(rootTag, "exception.userTag.rootTag.null"); + return rootTag; + } + + private User validateUser(User user) { + Assert.notNull(user, "exception.userTag.user.null"); + return user; + } + + private Integer validateCount(Integer count) { + Assert.isTrue(count >= 0, "exception.userTag.count.positiveOrZero"); + return count; + } +} diff --git a/src/main/java/com/prgrms/prolog/domain/usertag/repository/UserTagRepository.java b/src/main/java/com/prgrms/prolog/domain/usertag/repository/UserTagRepository.java new file mode 100644 index 0000000..2b2fd49 --- /dev/null +++ b/src/main/java/com/prgrms/prolog/domain/usertag/repository/UserTagRepository.java @@ -0,0 +1,24 @@ +package com.prgrms.prolog.domain.usertag.repository; + +import java.util.List; +import java.util.Set; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.prgrms.prolog.domain.usertag.model.UserTag; + +public interface UserTagRepository extends JpaRepository { + + @Query(""" + SELECT ut + FROM UserTag ut + WHERE ut.user.id = :userId + AND ut.rootTag.id IN :rootTagIds + """) + Set findByUserIdAndInRootTagIds( + @Param(value = "userId") Long userId, + @Param(value = "rootTagIds") List rootTagIds + ); +} diff --git a/src/main/java/com/prgrms/prolog/global/config/SecurityConfig.java b/src/main/java/com/prgrms/prolog/global/config/SecurityConfig.java index da7c02c..c830202 100644 --- a/src/main/java/com/prgrms/prolog/global/config/SecurityConfig.java +++ b/src/main/java/com/prgrms/prolog/global/config/SecurityConfig.java @@ -1,14 +1,12 @@ package com.prgrms.prolog.global.config; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; -import org.springframework.security.web.SecurityFilterChain; import com.prgrms.prolog.global.jwt.JwtAuthenticationEntryPoint; import com.prgrms.prolog.global.jwt.JwtAuthenticationFilter; diff --git a/src/main/resources/db/migration/V2.1__add_tag_table.sql b/src/main/resources/db/migration/V2.1__add_tag_table.sql new file mode 100644 index 0000000..b46e414 --- /dev/null +++ b/src/main/resources/db/migration/V2.1__add_tag_table.sql @@ -0,0 +1,28 @@ +DROP TABLE IF EXISTS user_tag; +DROP TABLE IF EXISTS post_tag; +DROP TABLE IF EXISTS root_tag; + +CREATE TABLE root_tag +( + id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT, + name varchar(100) NOT NULL UNIQUE +); + +CREATE TABLE post_tag +( + id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT, + post_id bigint NOT NULL, + root_tag_id bigint NOT NULL, + FOREIGN KEY fk_post_tag_post_id (post_id) REFERENCES post (id), + FOREIGN KEY fk_post_tag_root_tag_id (root_tag_id) REFERENCES root_tag (id) +); + +CREATE TABLE user_tag +( + id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT, + count int NOT NULL default 0, + user_id bigint NOT NULL, + root_tag_id bigint NOT NULL, + FOREIGN KEY fk_user_tag_user_id (user_id) REFERENCES users (id), + FOREIGN KEY fk_user_tag_root_tag_id (root_tag_id) REFERENCES root_tag (id) +) \ No newline at end of file diff --git a/src/test/java/com/prgrms/prolog/domain/comment/service/CommentServiceImplTest.java b/src/test/java/com/prgrms/prolog/domain/comment/service/CommentServiceImplTest.java index bc53c75..d72ca03 100644 --- a/src/test/java/com/prgrms/prolog/domain/comment/service/CommentServiceImplTest.java +++ b/src/test/java/com/prgrms/prolog/domain/comment/service/CommentServiceImplTest.java @@ -15,7 +15,7 @@ class CommentServiceImplTest { @Mock - CommentService commentService; + CommentServiceImpl commentService; final CreateCommentRequest CREATE_COMMENT_REQUEST = new CreateCommentRequest(COMMENT.getContent()); final UpdateCommentRequest UPDATE_COMMENT_REQUEST = new UpdateCommentRequest(COMMENT.getContent() + "updated"); diff --git a/src/test/java/com/prgrms/prolog/domain/post/api/PostControllerTest.java b/src/test/java/com/prgrms/prolog/domain/post/api/PostControllerTest.java index 873ad7e..38b3b5f 100644 --- a/src/test/java/com/prgrms/prolog/domain/post/api/PostControllerTest.java +++ b/src/test/java/com/prgrms/prolog/domain/post/api/PostControllerTest.java @@ -31,7 +31,7 @@ import com.prgrms.prolog.config.TestContainerConfig; import com.prgrms.prolog.domain.post.dto.PostRequest.CreateRequest; import com.prgrms.prolog.domain.post.dto.PostRequest.UpdateRequest; -import com.prgrms.prolog.domain.post.service.PostService; +import com.prgrms.prolog.domain.post.service.PostServiceImpl; import com.prgrms.prolog.domain.user.repository.UserRepository; import com.prgrms.prolog.global.jwt.JwtTokenProvider; import com.prgrms.prolog.global.jwt.JwtTokenProvider.Claims; @@ -52,7 +52,7 @@ class PostControllerTest { @Autowired private ObjectMapper objectMapper; @Autowired - private PostService postService; + private PostServiceImpl postService; @Autowired private UserRepository userRepository; Long postId; @@ -66,9 +66,8 @@ void setUp(WebApplicationContext webApplicationContext, userId = userRepository.save(USER).getId(); claims = Claims.from(userId, "ROLE_USER"); - CreateRequest createRequest = new CreateRequest("테스트 제목", "테스트 내용", false); + CreateRequest createRequest = new CreateRequest("테스트 제목", "테스트 내용", "#tag", false); postId = postService.save(createRequest, userId); - this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .apply(documentationConfiguration(restDocumentation)) .alwaysDo(restDocs) @@ -79,7 +78,7 @@ void setUp(WebApplicationContext webApplicationContext, @Test @DisplayName("게시물을 등록할 수 있다.") void save() throws Exception { - CreateRequest request = new CreateRequest("생성된 테스트 제목", "생성된 테스트 내용", true); + CreateRequest request = new CreateRequest("생성된 테스트 제목", "생성된 테스트 내용", "tag", true); mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/posts") .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims)) @@ -90,6 +89,7 @@ void save() throws Exception { requestFields( fieldWithPath("title").type(JsonFieldType.STRING).description("title"), fieldWithPath("content").type(JsonFieldType.STRING).description("content"), + fieldWithPath("tagText").type(JsonFieldType.STRING).description("tagText"), fieldWithPath("openStatus").type(JsonFieldType.BOOLEAN).description("openStatus") ), responseBody() @@ -117,6 +117,7 @@ void findAll() throws Exception { fieldWithPath("[].user.introduce").type(JsonFieldType.STRING).description("introduce"), fieldWithPath("[].user.prologName").type(JsonFieldType.STRING).description("prologName"), fieldWithPath("[].user.profileImgUrl").type(JsonFieldType.STRING).description("profileImgUrl"), + fieldWithPath("[].tags").type(JsonFieldType.ARRAY).description("tags"), fieldWithPath("[].comment").type(JsonFieldType.ARRAY).description("comment"), fieldWithPath("[].commentCount").type(JsonFieldType.NUMBER).description("commentCount") ))); @@ -140,6 +141,7 @@ void findById() throws Exception { fieldWithPath("user.introduce").type(JsonFieldType.STRING).description("introduce"), fieldWithPath("user.prologName").type(JsonFieldType.STRING).description("prologName"), fieldWithPath("user.profileImgUrl").type(JsonFieldType.STRING).description("profileImgUrl"), + fieldWithPath("tags").type(JsonFieldType.ARRAY).description("tags"), fieldWithPath("comment").type(JsonFieldType.ARRAY).description("comment"), fieldWithPath("commentCount").type(JsonFieldType.NUMBER).description("commentCount") ))); @@ -148,7 +150,7 @@ void findById() throws Exception { @Test @DisplayName("게시물 아이디로 게시물을 수정할 수 있다.") void update() throws Exception { - UpdateRequest request = new UpdateRequest("수정된 테스트 제목", "수정된 테스트 내용", true); + UpdateRequest request = new UpdateRequest("수정된 테스트 제목", "수정된 테스트 내용", "", true); mockMvc.perform(RestDocumentationRequestBuilders.patch("/api/v1/posts/{id}", postId) .header(HttpHeaders.AUTHORIZATION, BEARER_TYPE + jwtTokenProvider.createAccessToken(claims)) @@ -159,6 +161,7 @@ void update() throws Exception { requestFields( fieldWithPath("title").type(JsonFieldType.STRING).description("title"), fieldWithPath("content").type(JsonFieldType.STRING).description("content"), + fieldWithPath("tagText").type(JsonFieldType.STRING).description("tagText"), fieldWithPath("openStatus").type(JsonFieldType.BOOLEAN).description("openStatus") ), responseFields( @@ -171,6 +174,7 @@ void update() throws Exception { fieldWithPath("user.introduce").type(JsonFieldType.STRING).description("introduce"), fieldWithPath("user.prologName").type(JsonFieldType.STRING).description("prologName"), fieldWithPath("user.profileImgUrl").type(JsonFieldType.STRING).description("profileImgUrl"), + fieldWithPath("tags").type(JsonFieldType.ARRAY).description("tags"), fieldWithPath("comment").type(JsonFieldType.ARRAY).description("comment"), fieldWithPath("commentCount").type(JsonFieldType.NUMBER).description("commentCount") ) @@ -190,7 +194,7 @@ void remove() throws Exception { @Test @DisplayName("게시물 작성 중 제목이 공백인 경우 에러가 발생해야한다.") void isValidateTitleNull() throws Exception { - CreateRequest createRequest = new CreateRequest("", "테스트 게시물 내용", true); + CreateRequest createRequest = new CreateRequest("", "테스트 게시물 내용", "#tag", true); String requestJsonString = objectMapper.writeValueAsString(createRequest); @@ -204,7 +208,7 @@ void isValidateTitleNull() throws Exception { @Test @DisplayName("게시물 작성 중 내용이 빈칸인 경우 에러가 발생해야한다.") void isValidateContentEmpty() throws Exception { - CreateRequest createRequest = new CreateRequest("테스트 게시물 제목", " ", true); + CreateRequest createRequest = new CreateRequest("테스트 게시물 제목", " ", "#tag", true); String requestJsonString = objectMapper.writeValueAsString(createRequest); @@ -220,7 +224,7 @@ void isValidateContentEmpty() throws Exception { void isValidateTitleSizeOver() throws Exception { CreateRequest createRequest = new CreateRequest( "안녕하세요. 여기는 프로그래머스 기술 블로그 prolog입니다. 이곳에 글을 작성하기 위해서는 제목은 50글자 미만이어야합니다.", - "null 게시물 내용", + "null 게시물 내용", "#tag", true); String requestJsonString = objectMapper.writeValueAsString(createRequest); diff --git a/src/test/java/com/prgrms/prolog/domain/post/service/PostServiceTest.java b/src/test/java/com/prgrms/prolog/domain/post/service/PostServiceTest.java index 56eeec0..2eaea18 100644 --- a/src/test/java/com/prgrms/prolog/domain/post/service/PostServiceTest.java +++ b/src/test/java/com/prgrms/prolog/domain/post/service/PostServiceTest.java @@ -3,7 +3,9 @@ import static com.prgrms.prolog.utils.TestUtils.*; import static org.assertj.core.api.Assertions.*; +import java.util.List; import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -11,12 +13,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Import; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.transaction.annotation.Transactional; -import com.prgrms.prolog.config.TestContainerConfig; import com.prgrms.prolog.domain.post.dto.PostRequest.CreateRequest; import com.prgrms.prolog.domain.post.dto.PostRequest.UpdateRequest; import com.prgrms.prolog.domain.post.dto.PostResponse; @@ -28,11 +28,10 @@ @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @SpringBootTest @Transactional -@Import(TestContainerConfig.class) class PostServiceTest { @Autowired - private PostService postService; + PostServiceImpl postService; @Autowired UserRepository userRepository; @@ -58,20 +57,83 @@ void setData() { @Test @DisplayName("게시물을 등록할 수 있다.") void save_success() { - CreateRequest postRequest = new CreateRequest("테스트", "테스트 내용", true); + final CreateRequest postRequest = new CreateRequest("테스트", "테스트 내용", "#테스트", true); Long savePostId = postService.save(postRequest, user.getId()); assertThat(savePostId).isNotNull(); } + @Test + @DisplayName("게시글에 태그 없이 등록할 수 있다.") + void savePostAndWithOutAnyTagTest() { + // given + final CreateRequest request = new CreateRequest("테스트 제목", "테스트 내용", null, true); + + // when + Long savedPostId = postService.save(request, user.getId()); + PostResponse findPostResponse = postService.findById(savedPostId); + Set findTags = findPostResponse.tags(); + + // then + assertThat(findTags).isEmpty(); + } + + @Test + @DisplayName("게시글에 태그가 공백이거나 빈 칸이라면 태그는 무시된다.") + void savePostWithBlankTagTest() { + // given + final CreateRequest request = new CreateRequest("테스트 제목", "테스트 내용", "# #", true); + + // when + Long savedPostId = postService.save(request, user.getId()); + PostResponse findPostResponse = postService.findById(savedPostId); + Set findTags = findPostResponse.tags(); + + // then + assertThat(findTags).isEmpty(); + } + + @Test + @DisplayName("게시글에 복수의 태그를 등록할 수 있다.") + void savePostAndTagsTest() { + // given + final CreateRequest request = new CreateRequest("테스트 제목", "테스트 내용", "#테스트#test#test1#테 스트", true); + final List expectedTags = List.of("테스트", "test", "test1", "테 스트"); + + // when + Long savedPostId = postService.save(request, user.getId()); + PostResponse findPostResponse = postService.findById(savedPostId); + Set findTags = findPostResponse.tags(); + + // then + assertThat(findTags) + .containsExactlyInAnyOrderElementsOf(expectedTags); + } + + @Test + @DisplayName("게시물과 태그를 조회할 수 있다.") + void findPostAndTagsTest() { + // given + final CreateRequest request = new CreateRequest("테스트 제목", "테스트 내용", "#테스트", true); + + // when + Long savedPostId = postService.save(request, user.getId()); + PostResponse findPost = postService.findById(savedPostId); + + // then + assertThat(findPost) + .hasFieldOrPropertyWithValue("title", request.title()) + .hasFieldOrPropertyWithValue("content", request.content()) + .hasFieldOrPropertyWithValue("openStatus", request.openStatus()) + .hasFieldOrPropertyWithValue("tags", Set.of("테스트")); + } + @Test @DisplayName("존재하지 않는 사용자(비회원)의 이메일로 게시물을 등록할 수 없다.") void save_fail() { - String notExistEmail = "no_email@test.com"; - - CreateRequest postRequest = new CreateRequest("테스트", "테스트 내용", true); + CreateRequest postRequest = new CreateRequest("테스트", "테스트 내용", "#테스트", true); - assertThatThrownBy(() -> postService.save(postRequest, USER_ID)) - .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> postService.save(postRequest, UNSAVED_USER_ID)) + .isInstanceOf(NullPointerException.class); } @Test @@ -93,22 +155,23 @@ void findById_fail() { } @Test - @DisplayName("존재하는 게시물의 아이디로 게시물을 수정할 수 있다.") + @DisplayName("존재하는 게시물의 아이디로 게시물의 제목, 내용, 태그, 공개범위를 수정할 수 있다.") void update_success() { - UpdateRequest updateRequest = new UpdateRequest("수정된 테스트", "수정된 테스트 내용", true); + UpdateRequest updateRequest = new UpdateRequest("수정된 테스트", "수정된 테스트 내용", "#수정된 태그", true); - PostResponse update = postService.update(post.getId(), updateRequest); + PostResponse update = postService.update(updateRequest, user.getId(), post.getId()); - assertThat(update.title()).isEqualTo("수정된 테스트"); - assertThat(update.content()).isEqualTo("수정된 테스트 내용"); + assertThat(update) + .hasFieldOrPropertyWithValue("title", updateRequest.title()) + .hasFieldOrPropertyWithValue("content", updateRequest.content()); } @Test @DisplayName("존재하지 않는 게시물의 아이디로 게시물을 수정할 수 없다.") void update_fail() { - UpdateRequest updateRequest = new UpdateRequest("수정된 테스트", "수정된 테스트 내용", true); + UpdateRequest updateRequest = new UpdateRequest("수정된 테스트", "수정된 테스트 내용", "", true); - assertThatThrownBy(() -> postService.update(0L, updateRequest)) + assertThatThrownBy(() -> postService.update(updateRequest, user.getId(), 0L)) .isInstanceOf(IllegalArgumentException.class); } diff --git a/src/test/java/com/prgrms/prolog/domain/posttag/model/PostTagTest.java b/src/test/java/com/prgrms/prolog/domain/posttag/model/PostTagTest.java new file mode 100644 index 0000000..0ec8d73 --- /dev/null +++ b/src/test/java/com/prgrms/prolog/domain/posttag/model/PostTagTest.java @@ -0,0 +1,39 @@ +package com.prgrms.prolog.domain.posttag.model; + +import static com.prgrms.prolog.utils.TestUtils.*; +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class PostTagTest { + + @Test + @DisplayName("게시글 태그 생성") + void createPostTagTest() { + // given + PostTag postTag = PostTag.builder() + .rootTag(ROOT_TAG) + .post(POST) + .build(); + // when & then + assertThat(postTag) + .hasFieldOrPropertyWithValue("rootTag", ROOT_TAG) + .hasFieldOrPropertyWithValue("post", POST); + } + + @Test + @DisplayName("게시글 태그에는 게시글과 루트 태그가 null일 수 없다.") + void validatePostTagNullTest() { + assertAll( + () -> assertThatThrownBy(() -> new PostTag(null, ROOT_TAG)) + .isInstanceOf(IllegalArgumentException.class), + () -> assertThatThrownBy(() -> new PostTag(POST, null)) + .isInstanceOf(IllegalArgumentException.class), + () -> assertThatThrownBy(() -> new PostTag(null, null)) + .isInstanceOf(IllegalArgumentException.class) + ); + } + +} \ No newline at end of file diff --git a/src/test/java/com/prgrms/prolog/domain/roottag/model/RootTagTest.java b/src/test/java/com/prgrms/prolog/domain/roottag/model/RootTagTest.java new file mode 100644 index 0000000..e67f6db --- /dev/null +++ b/src/test/java/com/prgrms/prolog/domain/roottag/model/RootTagTest.java @@ -0,0 +1,45 @@ +package com.prgrms.prolog.domain.roottag.model; + +import static com.prgrms.prolog.utils.TestUtils.*; +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +class RootTagTest { + + @Test + @DisplayName("태그 생성") + void createRootTagTest() { + // given + RootTag rootTag = RootTag.builder() + .name(ROOT_TAG_NAME) + .build(); + // when & then + assertThat(rootTag).hasFieldOrPropertyWithValue("name", ROOT_TAG_NAME); + } + + @ParameterizedTest + @NullAndEmptySource + @DisplayName("태그 이름은 null, 빈 값일 수 없다.") + void validateRootTagNameTextTest(String name) { + // given & when & then + assertAll( + () -> assertThatThrownBy(() -> RootTag.builder().name(name).build()) + .isInstanceOf(IllegalArgumentException.class), + () -> assertThatThrownBy(() -> new RootTag(name)) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + @DisplayName("태그 이름은 100글자 이내여야 한다.") + void validateRootTagNameLengthTest() { + // given & when & then + assertThatThrownBy(() -> new RootTag(OVER_SIZE_100)) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file diff --git a/src/test/java/com/prgrms/prolog/domain/roottag/repository/RootTagRepositoryTest.java b/src/test/java/com/prgrms/prolog/domain/roottag/repository/RootTagRepositoryTest.java new file mode 100644 index 0000000..6fee4a5 --- /dev/null +++ b/src/test/java/com/prgrms/prolog/domain/roottag/repository/RootTagRepositoryTest.java @@ -0,0 +1,42 @@ +package com.prgrms.prolog.domain.roottag.repository; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace.*; + +import java.util.Set; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import com.prgrms.prolog.domain.roottag.model.RootTag; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = NONE) +class RootTagRepositoryTest { + + @Autowired + RootTagRepository rootTagRepository; + + @Test + @DisplayName("태그 이름들로 루트 태그들을 검색한다.") + void findByTagNamesInTest() { + // given + final Set tagNames = Set.of("태그1", "태그2", "태그3", "태그4", "태그5"); + final Set tags = Set.of( + new RootTag("태그1"), + new RootTag("태그2"), + new RootTag("태그3"), + new RootTag("태그4"), + new RootTag("태그5")); + rootTagRepository.saveAll(tags); + + // when + Set findTags = rootTagRepository.findByTagNamesIn(tagNames); + + // then + assertThat(findTags).hasSize(5); + } +} \ No newline at end of file diff --git a/src/test/java/com/prgrms/prolog/domain/user/Repository/UserRepositoryTest.java b/src/test/java/com/prgrms/prolog/domain/user/Repository/UserRepositoryTest.java index a456bae..e91ec8e 100644 --- a/src/test/java/com/prgrms/prolog/domain/user/Repository/UserRepositoryTest.java +++ b/src/test/java/com/prgrms/prolog/domain/user/Repository/UserRepositoryTest.java @@ -57,4 +57,13 @@ void findFailTest() { // then assertThat(foundUser).isNotPresent(); } + + @Test + @DisplayName("유저와 유저 태그를 조인하여 조회할 수 있다.") + void joinUserTagFindByEmailTest() { + // given & when + User findUser = userRepository.joinUserTagFindByUserId(savedUser.getId()); + // then + assertThat(findUser).isNotNull(); + } } diff --git a/src/test/java/com/prgrms/prolog/domain/usertag/model/UserTagTest.java b/src/test/java/com/prgrms/prolog/domain/usertag/model/UserTagTest.java new file mode 100644 index 0000000..ea06aa7 --- /dev/null +++ b/src/test/java/com/prgrms/prolog/domain/usertag/model/UserTagTest.java @@ -0,0 +1,39 @@ +package com.prgrms.prolog.domain.usertag.model; + +import static com.prgrms.prolog.utils.TestUtils.*; +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class UserTagTest { + + @Test + @DisplayName("유저 태그 생성 성공") + void createUserTagTest() { + // given + UserTag userTag = UserTag.builder() + .user(USER) + .rootTag(ROOT_TAG) + .count(1) + .build(); + // when & then + assertThat(userTag) + .hasFieldOrPropertyWithValue("user", USER) + .hasFieldOrPropertyWithValue("rootTag", ROOT_TAG); + } + + @Test + @DisplayName("유저 태그에는 유저와 루트 태그가 null일 수 없다.") + void validateUserTagNulLTest() { + assertAll( + () -> assertThatThrownBy(() -> new UserTag(USER, null)) + .isInstanceOf(IllegalArgumentException.class), + () -> assertThatThrownBy(() -> new UserTag(null, ROOT_TAG)) + .isInstanceOf(IllegalArgumentException.class), + () -> assertThatThrownBy(() -> new UserTag(null, null)) + .isInstanceOf(IllegalArgumentException.class) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/prgrms/prolog/utils/TestUtils.java b/src/test/java/com/prgrms/prolog/utils/TestUtils.java index e37465a..0a76da4 100644 --- a/src/test/java/com/prgrms/prolog/utils/TestUtils.java +++ b/src/test/java/com/prgrms/prolog/utils/TestUtils.java @@ -2,8 +2,10 @@ import com.prgrms.prolog.domain.comment.model.Comment; import com.prgrms.prolog.domain.post.model.Post; -import com.prgrms.prolog.domain.user.dto.UserDto.UserProfile; +import com.prgrms.prolog.domain.posttag.model.PostTag; +import com.prgrms.prolog.domain.roottag.model.RootTag; import com.prgrms.prolog.domain.user.dto.UserDto.UserInfo; +import com.prgrms.prolog.domain.user.dto.UserDto.UserProfile; import com.prgrms.prolog.domain.user.model.User; public class TestUtils { @@ -27,6 +29,11 @@ public class TestUtils { public static final String TITLE = "제목을 입력해주세요"; public static final String CONTENT = "내용을 입력해주세요"; public static final String USER_ROLE = "ROLE_USER"; + // RootTag & PostTag Data + public static final String ROOT_TAG_NAME = "머쓱 태그"; + public static final Integer POST_TAG_COUNT = 0; + public static final RootTag ROOT_TAG = getRootTag(); + public static final PostTag POST_TAG = getPostTag(); // Over Size String Dummy public static final String OVER_SIZE_50 = "0" + "1234567890".repeat(5); public static final String OVER_SIZE_100 = "0" + "1234567890".repeat(10); @@ -89,4 +96,17 @@ public static UserProfile getUserProfile() { .build(); } + public static RootTag getRootTag() { + return RootTag.builder() + .name(ROOT_TAG_NAME) + .build(); + } + + public static PostTag getPostTag() { + return PostTag.builder() + .rootTag(ROOT_TAG) + .post(POST) + .build(); + } + } diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql index 5bbba69..62f6f7e 100644 --- a/src/test/resources/schema.sql +++ b/src/test/resources/schema.sql @@ -1,11 +1,14 @@ -- V1__init.sql --- create database if not exists test; --- use test; +# create database if not exists prolog; +# use prolog; DROP TABLE IF EXISTS social_account; DROP TABLE IF EXISTS comment; DROP TABLE IF EXISTS post; DROP TABLE IF EXISTS series; DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS user_tag; +DROP TABLE IF EXISTS post_tag; +DROP TABLE IF EXISTS root_tag; CREATE TABLE users ( @@ -78,4 +81,29 @@ CREATE TABLE comment user_id bigint NOT NULL, FOREIGN KEY fk_comment_post_id (post_id) REFERENCES post (id), FOREIGN KEY fk_comment_user_id (user_id) REFERENCES users (id) -); \ No newline at end of file +); + +CREATE TABLE root_tag +( + id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT, + name varchar(100) NOT NULL UNIQUE +); + +CREATE TABLE post_tag +( + id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT, + post_id bigint NOT NULL, + root_tag_id bigint NOT NULL, + FOREIGN KEY fk_post_tag_post_id (post_id) REFERENCES post (id), + FOREIGN KEY fk_post_tag_root_tag_id (root_tag_id) REFERENCES root_tag (id) +); + +CREATE TABLE user_tag +( + id bigint NOT NULL PRIMARY KEY AUTO_INCREMENT, + count int NOT NULL default 0, + user_id bigint NOT NULL, + root_tag_id bigint NOT NULL, + FOREIGN KEY fk_user_tag_user_id (user_id) REFERENCES users (id), + FOREIGN KEY fk_user_tag_root_tag_id (root_tag_id) REFERENCES root_tag (id) +)