diff --git a/src/main/java/com/api/trip/domain/article/controller/ArticleController.java b/src/main/java/com/api/trip/domain/article/controller/ArticleController.java index b18b4c9..0bcb45d 100644 --- a/src/main/java/com/api/trip/domain/article/controller/ArticleController.java +++ b/src/main/java/com/api/trip/domain/article/controller/ArticleController.java @@ -38,7 +38,8 @@ public ResponseEntity deleteArticle(@PathVariable Long articleId) { @GetMapping("/{articleId}") public ResponseEntity readArticle(@PathVariable Long articleId) { - return ResponseEntity.ok(articleService.readArticle(articleId)); + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + return ResponseEntity.ok(articleService.readArticle(articleId, email)); } @GetMapping diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java b/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java index f1262af..d4c86f6 100644 --- a/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java +++ b/src/main/java/com/api/trip/domain/article/controller/dto/GetArticlesResponse.java @@ -45,6 +45,8 @@ private static class ArticleDto { private Long writerId; private String writerNickname; private String writerRole; + private long viewCount; + private long likeCount; private LocalDateTime createdAt; private static ArticleDto of(Article article) { @@ -55,6 +57,8 @@ private static ArticleDto of(Article article) { .writerId(writer.getId()) .writerNickname(writer.getNickname()) .writerRole(writer.getRole().name()) + .viewCount(article.getViewCount()) + .likeCount(article.getLikeCount()) .createdAt(article.getCreatedAt()) .build(); } diff --git a/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java b/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java index 343183a..f853c14 100644 --- a/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java +++ b/src/main/java/com/api/trip/domain/article/controller/dto/ReadArticleResponse.java @@ -1,6 +1,7 @@ package com.api.trip.domain.article.controller.dto; import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.interestarticle.model.InterestArticle; import com.api.trip.domain.member.model.Member; import lombok.Builder; import lombok.Getter; @@ -16,21 +17,27 @@ public class ReadArticleResponse { private Long writerId; private String writerNickname; private String writerRole; + private String writerProfileImg; private String content; private long viewCount; + private long likeCount; private LocalDateTime createdAt; + private Long interestArticleId; - public static ReadArticleResponse of(Article article) { + public static ReadArticleResponse of(Article article, InterestArticle interestArticle) { Member writer = article.getWriter(); return builder() .articleId(article.getId()) .title(article.getTitle()) .writerId(writer.getId()) .writerNickname(writer.getNickname()) + .writerProfileImg(writer.getProfileImg()) .writerRole(writer.getRole().name()) .content(article.getContent()) - .viewCount(article.getViewCount()) + .viewCount(article.getViewCount() + 1) + .likeCount(article.getLikeCount()) .createdAt(article.getCreatedAt()) + .interestArticleId(interestArticle != null ? interestArticle.getId() : null) .build(); } } diff --git a/src/main/java/com/api/trip/domain/article/model/Article.java b/src/main/java/com/api/trip/domain/article/model/Article.java index d34e154..c017dd9 100644 --- a/src/main/java/com/api/trip/domain/article/model/Article.java +++ b/src/main/java/com/api/trip/domain/article/model/Article.java @@ -28,20 +28,19 @@ public class Article extends BaseTimeEntity { private long viewCount; + private long likeCount; + @Builder - private Article(Member writer, String title, String content, long viewCount) { + private Article(Member writer, String title, String content, long viewCount, long likeCount) { this.writer = writer; this.title = title; this.content = content; this.viewCount = viewCount; + this.likeCount = likeCount; } public void modify(String title, String content) { this.title = title; this.content = content; } - - public void increaseViewCount() { - this.viewCount++; - } } diff --git a/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java b/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java index 68c2f4d..5dbb977 100644 --- a/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java +++ b/src/main/java/com/api/trip/domain/article/repository/ArticleRepository.java @@ -3,10 +3,25 @@ import com.api.trip.domain.article.model.Article; import com.api.trip.domain.member.model.Member; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; public interface ArticleRepository extends JpaRepository, ArticleRepositoryCustom { List
findAllByWriterOrderByIdDesc(Member writer); + + @Modifying + @Query("UPDATE Article a SET a.viewCount = a.viewCount + 1 WHERE a = :article") + void increaseViewCount(@Param("article") Article article); + + @Modifying + @Query("UPDATE Article a SET a.likeCount = a.likeCount + 1 WHERE a = :article") + void increaseLikeCount(@Param("article") Article article); + + @Modifying + @Query("UPDATE Article a SET a.likeCount = a.likeCount - 1 WHERE a = :article") + void decreaseLikeCount(@Param("article") Article article); } diff --git a/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustomImpl.java b/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustomImpl.java index c6b8fc5..aff3711 100644 --- a/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustomImpl.java +++ b/src/main/java/com/api/trip/domain/article/repository/ArticleRepositoryCustomImpl.java @@ -12,6 +12,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -27,12 +28,12 @@ public ArticleRepositoryCustomImpl(EntityManager em) { } @Override - public Optional
findArticle(Long id) { + public Optional
findArticle(Long articleId) { return Optional.ofNullable( jpaQueryFactory .selectFrom(article) .innerJoin(article.writer, member).fetchJoin() - .where(article.id.eq(id)) + .where(article.id.eq(articleId)) .fetchOne() ); } @@ -43,7 +44,7 @@ public Page
findArticles(Pageable pageable, String filter) { .selectFrom(article) .innerJoin(article.writer, member).fetchJoin() .where(eqFilter(filter)) - .orderBy(getOrderSpecifier(pageable)) + .orderBy(getOrderSpecifiers(pageable)) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); @@ -67,12 +68,14 @@ private BooleanExpression eqFilter(String filter) { return null; } - private OrderSpecifier getOrderSpecifier(Pageable pageable) { + private OrderSpecifier[] getOrderSpecifiers(Pageable pageable) { + List> orderSpecifierList = new ArrayList<>(); for (Sort.Order order : pageable.getSort()) { - if ("POPULAR".equals(order.getProperty())) { - return new OrderSpecifier<>(Order.DESC, article.viewCount); + if ("popular".equals(order.getProperty())) { + orderSpecifierList.add(new OrderSpecifier<>(Order.DESC, article.likeCount)); } } - return new OrderSpecifier<>(Order.DESC, article.id); + orderSpecifierList.add(new OrderSpecifier<>(Order.DESC, article.id)); + return orderSpecifierList.toArray(OrderSpecifier[]::new); } } diff --git a/src/main/java/com/api/trip/domain/article/service/ArticleService.java b/src/main/java/com/api/trip/domain/article/service/ArticleService.java index cbd32e7..24509d8 100644 --- a/src/main/java/com/api/trip/domain/article/service/ArticleService.java +++ b/src/main/java/com/api/trip/domain/article/service/ArticleService.java @@ -3,6 +3,8 @@ import com.api.trip.domain.article.controller.dto.*; import com.api.trip.domain.article.model.Article; import com.api.trip.domain.article.repository.ArticleRepository; +import com.api.trip.domain.interestarticle.model.InterestArticle; +import com.api.trip.domain.interestarticle.repository.InterestArticleRepository; import com.api.trip.domain.member.model.Member; import com.api.trip.domain.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; @@ -12,6 +14,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Objects; @Service @Transactional @@ -20,6 +23,7 @@ public class ArticleService { private final ArticleRepository articleRepository; private final MemberRepository memberRepository; + private final InterestArticleRepository interestArticleRepository; public Long createArticle(CreateArticleRequest request, String email) { Member member = memberRepository.findByEmail(email).orElseThrow(); @@ -51,12 +55,18 @@ public void deleteArticle(Long articleId, String email) { articleRepository.delete(article); } - public ReadArticleResponse readArticle(Long articleId) { + public ReadArticleResponse readArticle(Long articleId, String email) { Article article = articleRepository.findArticle(articleId).orElseThrow(); - article.increaseViewCount(); + articleRepository.increaseViewCount(article); - return ReadArticleResponse.of(article); + InterestArticle interestArticle = null; + if (!Objects.equals(email, "anonymousUser")) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + interestArticle = interestArticleRepository.findByMemberAndArticle(member, article); + } + + return ReadArticleResponse.of(article, interestArticle); } @Transactional(readOnly = true) diff --git a/src/main/java/com/api/trip/domain/comment/controller/dto/GetCommentsResponse.java b/src/main/java/com/api/trip/domain/comment/controller/dto/GetCommentsResponse.java index 95f86d4..d615b8d 100644 --- a/src/main/java/com/api/trip/domain/comment/controller/dto/GetCommentsResponse.java +++ b/src/main/java/com/api/trip/domain/comment/controller/dto/GetCommentsResponse.java @@ -45,6 +45,7 @@ private static class CommentDto { private Long commentId; private Long writerId; private String writerNickname; + private String writerProfileImg; private Long articleId; private String content; private Long parentId; @@ -58,6 +59,7 @@ private static CommentDto of(Comment comment) { .commentId(comment.getId()) .writerId(comment.getWriter().getId()) .writerNickname(comment.getWriter().getNickname()) + .writerProfileImg(comment.getWriter().getProfileImg()) .articleId(comment.getArticle().getId()) .content(comment.getContent()) .parentId(comment.getParent() != null ? comment.getParent().getId() : null) diff --git a/src/main/java/com/api/trip/domain/interestarticle/controller/InterestArticleController.java b/src/main/java/com/api/trip/domain/interestarticle/controller/InterestArticleController.java new file mode 100644 index 0000000..a46bbb9 --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestarticle/controller/InterestArticleController.java @@ -0,0 +1,29 @@ +package com.api.trip.domain.interestarticle.controller; + +import com.api.trip.domain.interestarticle.controller.dto.CreateInterestArticleRequest; +import com.api.trip.domain.interestarticle.service.InterestArticleService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/interest-articles") +@RequiredArgsConstructor +public class InterestArticleController { + + private final InterestArticleService interestArticleService; + + @PostMapping + public ResponseEntity createInterestArticle(@RequestBody CreateInterestArticleRequest request) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + return ResponseEntity.ok(interestArticleService.createInterestArticle(request, email)); + } + + @DeleteMapping("/{interestArticleId}") + public ResponseEntity deleteInterestArticle(@PathVariable Long interestArticleId) { + String email = SecurityContextHolder.getContext().getAuthentication().getName(); + interestArticleService.deleteInterestArticle(interestArticleId, email); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/api/trip/domain/interestarticle/controller/dto/CreateInterestArticleRequest.java b/src/main/java/com/api/trip/domain/interestarticle/controller/dto/CreateInterestArticleRequest.java new file mode 100644 index 0000000..ed21e3d --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestarticle/controller/dto/CreateInterestArticleRequest.java @@ -0,0 +1,19 @@ +package com.api.trip.domain.interestarticle.controller.dto; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.interestarticle.model.InterestArticle; +import com.api.trip.domain.member.model.Member; +import lombok.Getter; + +@Getter +public class CreateInterestArticleRequest { + + private Long articleId; + + public InterestArticle toEntity(Member member, Article article) { + return InterestArticle.builder() + .member(member) + .article(article) + .build(); + } +} diff --git a/src/main/java/com/api/trip/domain/interestarticle/model/InterestArticle.java b/src/main/java/com/api/trip/domain/interestarticle/model/InterestArticle.java new file mode 100644 index 0000000..5cd6066 --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestarticle/model/InterestArticle.java @@ -0,0 +1,33 @@ +package com.api.trip.domain.interestarticle.model; + +import com.api.trip.common.auditing.entity.BaseTimeEntity; +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.member.model.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"member_id", "article_id"})}) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class InterestArticle extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + private Article article; + + @Builder + private InterestArticle(Member member, Article article) { + this.member = member; + this.article = article; + } +} diff --git a/src/main/java/com/api/trip/domain/interestarticle/repository/InterestArticleRepository.java b/src/main/java/com/api/trip/domain/interestarticle/repository/InterestArticleRepository.java new file mode 100644 index 0000000..04bb2ed --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestarticle/repository/InterestArticleRepository.java @@ -0,0 +1,11 @@ +package com.api.trip.domain.interestarticle.repository; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.interestarticle.model.InterestArticle; +import com.api.trip.domain.member.model.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface InterestArticleRepository extends JpaRepository { + + InterestArticle findByMemberAndArticle(Member member, Article article); +} diff --git a/src/main/java/com/api/trip/domain/interestarticle/service/InterestArticleService.java b/src/main/java/com/api/trip/domain/interestarticle/service/InterestArticleService.java new file mode 100644 index 0000000..93265c1 --- /dev/null +++ b/src/main/java/com/api/trip/domain/interestarticle/service/InterestArticleService.java @@ -0,0 +1,52 @@ +package com.api.trip.domain.interestarticle.service; + +import com.api.trip.domain.article.model.Article; +import com.api.trip.domain.article.repository.ArticleRepository; +import com.api.trip.domain.interestarticle.controller.dto.CreateInterestArticleRequest; +import com.api.trip.domain.interestarticle.model.InterestArticle; +import com.api.trip.domain.interestarticle.repository.InterestArticleRepository; +import com.api.trip.domain.member.model.Member; +import com.api.trip.domain.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class InterestArticleService { + + private final InterestArticleRepository interestArticleRepository; + private final ArticleRepository articleRepository; + private final MemberRepository memberRepository; + + public Long createInterestArticle(CreateInterestArticleRequest request, String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + Article article = articleRepository.findById(request.getArticleId()).orElseThrow(); + + InterestArticle interestArticle = interestArticleRepository.findByMemberAndArticle(member, article); + if (interestArticle != null) { + throw new RuntimeException("잘못된 요청입니다."); + } + + interestArticle = request.toEntity(member, article); + + articleRepository.increaseLikeCount(interestArticle.getArticle()); + + return interestArticleRepository.save(interestArticle).getId(); + } + + public void deleteInterestArticle(Long interestArticleId, String email) { + Member member = memberRepository.findByEmail(email).orElseThrow(); + + InterestArticle interestArticle = interestArticleRepository.findById(interestArticleId).orElseThrow(); + if (interestArticle.getMember() != member) { + throw new RuntimeException("삭제 권한이 없습니다."); + } + + articleRepository.decreaseLikeCount(interestArticle.getArticle()); + + interestArticleRepository.delete(interestArticle); + } +}