Skip to content

Commit

Permalink
Merge pull request #1544 from woowacourse/feature/#1526-아티클_북마크_기능_추가
Browse files Browse the repository at this point in the history
Feature/#1526 아티클 북마크 기능 추가
  • Loading branch information
donghae-kim authored Sep 16, 2023
2 parents 203cc2a + e93e72e commit 68aa0ff
Show file tree
Hide file tree
Showing 16 changed files with 446 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package wooteco.prolog.steps;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
import static wooteco.prolog.fixtures.ArticleFixture.ARTICLE_REQUEST1;
import static wooteco.prolog.fixtures.ArticleFixture.ARTICLE_REQUEST2;

Expand All @@ -10,6 +11,7 @@
import java.util.List;
import org.springframework.http.HttpStatus;
import wooteco.prolog.AcceptanceSteps;
import wooteco.prolog.article.ui.ArticleBookmarkRequest;
import wooteco.prolog.article.ui.ArticleResponse;
import wooteco.prolog.article.ui.ArticleUrlResponse;

Expand All @@ -22,7 +24,7 @@ public class ArticleStepDefinitions extends AcceptanceSteps {
context.invokeHttpPostWithToken("/articles", ARTICLE_REQUEST2);
}

@Given("아티클을 작성하고")
@Given("아티클이 작성되어 있고")
@When("아티클을 작성하면")
public void 아티클을_작성하면() {
context.invokeHttpPostWithToken("/articles", ARTICLE_REQUEST1);
Expand Down Expand Up @@ -96,4 +98,23 @@ public class ArticleStepDefinitions extends AcceptanceSteps {
.usingRecursiveComparison()
.isEqualTo(expected);
}

@When("{long}번 아티클에 북마크 요청을 보내면")
public void 아티클에_북마크_요청을_보내면(final Long articleId) {
//final String articleUrl = context.response.header("Location");
final ArticleBookmarkRequest request = new ArticleBookmarkRequest(true);
context.invokeHttpPutWithToken(
String.format("/articles/%d/bookmark", articleId),
request
);
}

@Then("아티클에 북마크가 등록된다")
public void 아티클에_북마크가_등록된다() {
final int statusCode = context.response.statusCode();
assertAll(
() -> assertThat(statusCode).isEqualTo(HttpStatus.OK.value())
);
//아티클 단건 조회 변경 시, 단건 조회로 bookmark 여부까지 검증해보기
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,8 @@ Feature: 아티클 관련 기능
Scenario: Url og태그 파싱하기
When Url을 입력하면
Then og태그를 파싱해서 반환한다.

Scenario: 아티클을 북마크로 등록하기
Given 아티클이 작성되어 있고
When 1번 아티클에 북마크 요청을 보내면
Then 아티클에 북마크가 등록된다
12 changes: 12 additions & 0 deletions backend/src/documentation/adoc/article.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[[article]]
== 아티클

=== 아티클 북마크 추가

==== Request

include::{snippets}/article/bookmark/http-request.adoc[]

==== Response

include::{snippets}/article/bookmark/http-response.adoc[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package wooteco.prolog.docu;

import static io.netty.handler.codec.http.HttpHeaders.Values.APPLICATION_JSON;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import io.restassured.module.mockmvc.response.ValidatableMockMvcResponse;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import wooteco.prolog.NewDocumentation;
import wooteco.prolog.article.application.ArticleService;
import wooteco.prolog.article.ui.ArticleBookmarkRequest;
import wooteco.prolog.article.ui.ArticleController;

@WebMvcTest(controllers = ArticleController.class)
public class ArticleDocumentation extends NewDocumentation {

@MockBean
private ArticleService articleService;

@Test
void 아티클에_북마크를_변경한다() {
//given, when
final ArticleBookmarkRequest bookmarkRequest = new ArticleBookmarkRequest(true);

final ValidatableMockMvcResponse response = given
.header("Authorization", "Bearer " + accessToken)
.contentType(APPLICATION_JSON)
.body(bookmarkRequest)
.when().put("/articles/{bookmark-id}/bookmark", 1L)
.then().log().all();

//then
response.expect(status().isOk());

//docs
response.apply(document("article/bookmark"));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package wooteco.prolog.article.application;

import static java.lang.Boolean.TRUE;
import static java.util.stream.Collectors.toList;
import static wooteco.prolog.common.exception.BadRequestCode.ARTICLE_NOT_FOUND_EXCEPTION;

import java.util.List;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -45,7 +47,7 @@ public List<ArticleResponse> getAll() {
public void update(final Long id, final ArticleRequest articleRequest,
final LoginMember loginMember) {
final Article article = articleRepository.findById(id)
.orElseThrow(() -> new BadRequestException(BadRequestCode.ARTICLE_NOT_FOUND_EXCEPTION));
.orElseThrow(() -> new BadRequestException(ARTICLE_NOT_FOUND_EXCEPTION));

final Member member = memberService.findById(loginMember.getId());
article.validateOwner(member);
Expand All @@ -56,11 +58,25 @@ public void update(final Long id, final ArticleRequest articleRequest,
@Transactional
public void delete(final Long id, final LoginMember loginMember) {
final Article article = articleRepository.findById(id)
.orElseThrow(() -> new BadRequestException(BadRequestCode.ARTICLE_NOT_FOUND_EXCEPTION));
.orElseThrow(() -> new BadRequestException(ARTICLE_NOT_FOUND_EXCEPTION));

final Member member = memberService.findById(loginMember.getId());
article.validateOwner(member);

articleRepository.delete(article);
}

@Transactional
public void bookmarkArticle(final Long id, final LoginMember loginMember,
final Boolean checked) {
final Article article = articleRepository.findFetchById(id)
.orElseThrow(() -> new BadRequestException(ARTICLE_NOT_FOUND_EXCEPTION));
final Member member = memberService.findById(loginMember.getId());

if (TRUE.equals(checked)) {
article.addBookmark(member);
} else {
article.removeBookmark(member);
}
}
}
26 changes: 23 additions & 3 deletions backend/src/main/java/wooteco/prolog/article/domain/Article.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
package wooteco.prolog.article.domain;

import java.time.LocalDateTime;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -9,9 +19,6 @@
import wooteco.prolog.common.exception.BadRequestException;
import wooteco.prolog.member.domain.Member;

import javax.persistence.*;
import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
Expand All @@ -35,6 +42,9 @@ public class Article {
@Embedded
private ImageUrl imageUrl;

@Embedded
private ArticleBookmarks articleBookmarks;

@CreatedDate
private LocalDateTime createdAt;

Expand All @@ -43,6 +53,7 @@ public Article(final Member member, final Title title, final Url url, final Imag
this.title = title;
this.url = url;
this.imageUrl = imageUrl;
this.articleBookmarks = new ArticleBookmarks();
}

public void validateOwner(final Member member) {
Expand All @@ -55,4 +66,13 @@ public void update(final String title, final String url) {
this.title = new Title(title);
this.url = new Url(url);
}

public void addBookmark(final Member member) {
final ArticleBookmark articleBookmark = new ArticleBookmark(this, member.getId());
articleBookmarks.addBookmark(articleBookmark);
}

public void removeBookmark(final Member member) {
articleBookmarks.removeBookmark(member.getId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package wooteco.prolog.article.domain;

import java.util.Objects;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "article_bookmark")
public class ArticleBookmark {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "article_id")
private Article article;

private Long memberId;

private ArticleBookmark(final Long id, final Article article, final Long memberId) {
this.id = id;
this.article = article;
this.memberId = memberId;
}

public ArticleBookmark(final Article article, final Long memberId) {
this(null, article, memberId);
}

public boolean isOwner(final Long memberId) {
return Objects.equals(this.memberId, memberId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package wooteco.prolog.article.domain;

import java.util.ArrayList;
import java.util.List;
import javax.persistence.Embeddable;
import javax.persistence.OneToMany;
import org.hibernate.annotations.Cascade;
import org.hibernate.annotations.CascadeType;

@Embeddable
public class ArticleBookmarks {

@OneToMany(mappedBy = "article")
@Cascade(value = {CascadeType.PERSIST, CascadeType.DELETE})
private List<ArticleBookmark> articleBookmarks;

public ArticleBookmarks() {
articleBookmarks = new ArrayList<>();
}

public void addBookmark(final ArticleBookmark bookmark) {
articleBookmarks.add(bookmark);
}

public void removeBookmark(final Long memberId) {
articleBookmarks.stream()
.filter(bookmark -> bookmark.isOwner(memberId))
.findAny()
.ifPresent(bookmark -> articleBookmarks.remove(bookmark));
}

public boolean containBookmark(final Long memberId) {
return articleBookmarks.stream()
.anyMatch(bookmark -> bookmark.isOwner(memberId));
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package wooteco.prolog.article.domain.repository;

import java.util.List;
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 wooteco.prolog.article.domain.Article;

import java.util.List;

public interface ArticleRepository extends JpaRepository<Article, Long> {

List<Article> findAllByOrderByCreatedAtDesc();

@Query("select a from Article a join fetch a.articleBookmarks where a.id = :id")
Optional<Article> findFetchById(@Param("id") final Long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package wooteco.prolog.article.ui;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class ArticleBookmarkRequest {

private final Boolean bookmark;

public ArticleBookmarkRequest() {
this(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import wooteco.prolog.article.application.ArticleService;
import wooteco.prolog.article.domain.ArticleBookmark;
import wooteco.prolog.login.domain.AuthMemberPrincipal;
import wooteco.prolog.login.ui.LoginMember;

Expand Down Expand Up @@ -50,4 +51,12 @@ public ResponseEntity<Void> deleteArticle(@PathVariable final Long id,
articleService.delete(id, member);
return ResponseEntity.noContent().build();
}

@PutMapping("/{id}/bookmark")
public ResponseEntity<Void> bookmarkArticle(@PathVariable final Long id,
@AuthMemberPrincipal final LoginMember member,
@RequestBody final ArticleBookmarkRequest request) {
articleService.bookmarkArticle(id, member, request.getBookmark());
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
create table if not exists prolog.article_bookmark
(
id bigint auto_increment primary key,
article_id bigint not null,
member_id bigint not null
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_0900_ai_ci;
Loading

0 comments on commit 68aa0ff

Please sign in to comment.