Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] feat: 학교 북마크 기능 추가 (#787) #809

Merged
merged 32 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ffa3bbf
feat: 학교 북마크 저장 로직 추가
carsago Mar 20, 2024
16f9fcd
refactor: 북마크 검증 Validator 클래스로 분리
carsago Mar 20, 2024
5282a50
refactor: 북마크 최대 갯수 매직넘버 제거
carsago Mar 20, 2024
73c69f6
feat: 회원의 북마크 학교 목록을 조회하는 기능 추가
carsago Mar 23, 2024
7b9e2d9
refactor: 테스트 메소드명 변경
carsago Mar 23, 2024
3629eb3
refactor: 테스트 클래스명이 달랐던 문제 변경
carsago Mar 23, 2024
3f8f14a
refactor: DTO 포장하여 전달
carsago Mar 23, 2024
a7ea199
feat: 학교 북마크 조회 API 추가
carsago Mar 23, 2024
5079562
feat: swagger 설정 추가
carsago Mar 23, 2024
881c906
refactor: 북마크 테스트시 회원 저장하도록 변경
carsago Mar 23, 2024
f4fff00
refactor: 학교 북마크 삭제 기능 추가
carsago Mar 23, 2024
e93a71e
fix: 학교 조회시 CustomException throw
carsago Mar 23, 2024
1c3d3cd
feat: 학교 검색 DTO 가져오기
seokjin8678 Mar 3, 2024
5dbd257
refactor: 학교 검색 DTO 공용적으로 사용하도록 수정
carsago Mar 25, 2024
3a25bdc
refactor: 북마크 조회 API 테스트 구체적으로 변경
carsago Mar 25, 2024
0d309fb
refactor: 통합 테스트 클래스명 컨벤션에 맞게 변경
carsago Mar 25, 2024
f2932d0
refactor: 조회 Service에 readonly 옵션 추가
carsago Mar 25, 2024
e4efd02
refactor: 사용하지 않는 클래스 제거
carsago Mar 25, 2024
8af1ce8
refactor: SchoolBookmarkAppendValidator 로 클래스명 변경
carsago Mar 28, 2024
62966bb
refactor: 테스트에서 repository 타입을 interface로 변경
carsago Mar 28, 2024
7875820
refactor: 컨벤션에 맞게 개행 및 클래스명 변경
carsago Mar 28, 2024
0b071b7
refactor: 회원 학교 북마크 조회 시 join절에 조건을 명시하도록 변경
carsago Mar 28, 2024
7776ca0
refactor: 북마크 예외 갯수 초과 에러 메시지 변경
carsago Mar 28, 2024
7dffc0f
refactor: SchoolRepository를 Repository를 상속하도록 변경
seokjin8678 Mar 24, 2024
9b5d90f
refactor: 학교 존재 검증 Validator에서 수행하도록 변경
carsago Mar 28, 2024
e1ffd78
test: MemorySchoolRepositoryTest 추가
carsago Mar 28, 2024
554b433
refactor: merge 충돌 방지를 위해 사용하지 않는 클래스 제거
carsago Mar 28, 2024
b856e70
refactor: 사용하지 않는 메소드 제거
carsago Mar 28, 2024
03647ed
refactor: SchoolBookmarkAppendValidator 삭제
carsago Mar 28, 2024
1c4763a
refactor: MemorySchoolRepository 구현 변경
carsago Mar 28, 2024
994fe05
fix: 테스트 깨지던 것 수정
carsago Mar 28, 2024
35b8799
style: 개행 수정
carsago Mar 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.festago.bookmark.application;

import com.festago.bookmark.domain.BookmarkType;
import com.festago.bookmark.repository.BookmarkRepository;
import com.festago.common.exception.ErrorCode;
import com.festago.common.exception.NotFoundException;
import com.festago.school.repository.SchoolRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class SchoolBookmarkAppendValidator {

private static final int BOOKMARK_MAXIMUM_COUNT = 12;
private final BookmarkRepository bookmarkRepository;
private final SchoolRepository schoolRepository;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

각 북마크마다 별도로 Validate 해주기로 했기 때문에 이름을 Bookmark에서 SchoolBookmark로 변경했어요

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Service 내부 메서드가 아닌 validator를 유지하시는 건가요??

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

양방향 의존을 피하기 위해 Validator를 사용하는게 아니라면 Service 내부에 위치 시켜도 좋을 것 같네요!


//TODO: 커스텀 예외 적용
public void validate(Long schoolId, Long memberId) {
if (!schoolRepository.existsById(schoolId)) {
throw new NotFoundException(ErrorCode.SCHOOL_NOT_FOUND);
}

if (bookmarkRepository.existsByBookmarkTypeAndMemberIdAndResourceId(BookmarkType.SCHOOL, memberId, schoolId)) {
throw new IllegalArgumentException("이미 저장된 북마크입니다.");
}

long bookmarkCount = bookmarkRepository.countByMemberIdAndBookmarkType(memberId, BookmarkType.SCHOOL);
if (bookmarkCount >= BOOKMARK_MAXIMUM_COUNT) {
throw new IllegalArgumentException("북마크는 저장 갯수를 초과하였습니다.");
}
}
Copy link
Collaborator Author

@carsago carsago Mar 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이걸 일단 IllegalArgumentException을 처리했는데, 커스텀 에러로 한다면 각자가 다 다르게 에러를 만들어 놔서 중복될 것 같고, 500을 던지는 것도 어떤 에러로 던지냐를 하나의 브랜치에서 처리하는게 나을 것 같아서 일단 그대로 나뒀습니다.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.festago.bookmark.application;

import com.festago.bookmark.domain.Bookmark;
import com.festago.bookmark.domain.BookmarkType;
import com.festago.bookmark.repository.BookmarkRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class SchoolBookmarkCommandService {

private final BookmarkRepository bookmarkRepository;
private final SchoolBookmarkAppendValidator schoolBookmarkAppendValidator;

public Long save(Long schoolId, Long memberId) {
schoolBookmarkAppendValidator.validate(schoolId, memberId);
return bookmarkRepository.save(new Bookmark(BookmarkType.SCHOOL, schoolId, memberId))
.getId();
}

public void delete(Long schoolId, Long memberId) {
bookmarkRepository.deleteByBookmarkTypeAndMemberIdAndResourceId(BookmarkType.SCHOOL, memberId, schoolId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.festago.bookmark.application;

import com.festago.bookmark.dto.v1.SchoolBookmarkV1Response;
import com.festago.bookmark.repository.SchoolBookmarkV1QuerydslRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class SchoolBookmarkV1QueryService {

private final SchoolBookmarkV1QuerydslRepository schoolBookmarkV1QuerydslRepository;

public List<SchoolBookmarkV1Response> findAllByMemberId(Long memberId) {
return schoolBookmarkV1QuerydslRepository.findAllByMemberId(memberId);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.festago.bookmark.dto.v1;

import com.festago.school.dto.v1.SchoolSearchV1Response;
import com.querydsl.core.annotations.QueryProjection;
import java.time.LocalDateTime;

public record SchoolBookmarkV1Response(
SchoolSearchV1Response school,
LocalDateTime bookmarkCreatedAt
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createdAt, bookmarkCreatedAt 둘 중 어느게 좋을지 클라이언트 분께 투표 받아보죠!

) {

@QueryProjection
public SchoolBookmarkV1Response {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.festago.bookmark.presentation;

import com.festago.auth.annotation.Member;
import com.festago.bookmark.application.SchoolBookmarkV1QueryService;
import com.festago.bookmark.dto.v1.SchoolBookmarkV1Response;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/bookmarks/schools")
@Tag(name = "학교 북마크 API V1")
public class SchoolBookmarkV1Controller {

private final SchoolBookmarkV1QueryService schoolBookmarkV1QueryService;

@GetMapping
@Operation(description = "특정한 회원의 학교 북마크 목록을 반환한다", summary = "회원 학교 북마크 목록 조회")
public ResponseEntity<List<SchoolBookmarkV1Response>> findAllByMemberId(@Member Long memberId) {
return ResponseEntity.ok(schoolBookmarkV1QueryService.findAllByMemberId(memberId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.festago.bookmark.repository;

import static com.festago.bookmark.domain.QBookmark.bookmark;
import static com.festago.school.domain.QSchool.school;

import com.festago.bookmark.domain.Bookmark;
import com.festago.bookmark.domain.BookmarkType;
import com.festago.bookmark.dto.v1.QSchoolBookmarkV1Response;
import com.festago.bookmark.dto.v1.SchoolBookmarkV1Response;
import com.festago.common.querydsl.QueryDslRepositorySupport;
import com.festago.school.dto.v1.QSchoolSearchV1Response;
import java.util.List;
import org.springframework.stereotype.Repository;

@Repository
public class SchoolBookmarkV1QuerydslRepository extends QueryDslRepositorySupport {

protected SchoolBookmarkV1QuerydslRepository() {
super(Bookmark.class);
}

public List<SchoolBookmarkV1Response> findAllByMemberId(Long memberId) {
return select(new QSchoolBookmarkV1Response(
new QSchoolSearchV1Response(school.id, school.name, school.logoUrl), bookmark.createdAt))
.from(bookmark)
.innerJoin(school).on(school.id.eq(bookmark.resourceId)
.and(bookmark.memberId.eq(memberId))
.and(bookmark.bookmarkType.eq(BookmarkType.SCHOOL)))
.fetch();
Comment on lines +26 to +29
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

join절에 조건을 주었습니다.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.festago.school.dto.v1;

import com.querydsl.core.annotations.QueryProjection;

public record SchoolSearchV1Response(
Long id,
String name,
String logoUrl
) {

@QueryProjection
public SchoolSearchV1Response {
}
Comment on lines +5 to +13
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#769
여기서 들고왔는데 아마 충돌 안날겁니다 ㅎㅎ;

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,21 @@
import com.festago.school.domain.SchoolRegion;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.Repository;

public interface SchoolRepository extends JpaRepository<School, Long> {
public interface SchoolRepository extends Repository<School, Long> {

School save(School school);

List<School> findAll();

Optional<School> findById(Long schoolId);

void deleteById(Long id);

void flush();

boolean existsById(Long id);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findAll(), flush() 해당 메서드는 사용되는 곳이 없고, findAllByRegion() 메서드는 사용되는 곳이 테스트 밖에 없네요.
해당 메서드는 삭제해도 될 것 같습니다!


default School getOrThrow(Long schoolId) {
return findById(schoolId)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.festago.bookmark.application;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.festago.bookmark.domain.Bookmark;
import com.festago.bookmark.domain.BookmarkType;
import com.festago.bookmark.repository.BookmarkRepository;
import com.festago.bookmark.repository.MemoryBookmarkRepository;
import com.festago.common.exception.ErrorCode;
import com.festago.common.exception.NotFoundException;
import com.festago.school.repository.MemorySchoolRepository;
import com.festago.school.repository.SchoolRepository;
import com.festago.support.SchoolFixture;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores;
import org.junit.jupiter.api.Test;

@DisplayNameGeneration(ReplaceUnderscores.class)
@SuppressWarnings("NonAsciiCharacters")
class SchoolBookmarkAppendValidatorTest {

BookmarkRepository bookmarkRepository;
SchoolRepository schoolRepository;
SchoolBookmarkAppendValidator schoolBookmarkAppendValidator;

@BeforeEach
void setUp() {
bookmarkRepository = new MemoryBookmarkRepository();
schoolRepository = new MemorySchoolRepository();
schoolBookmarkAppendValidator = new SchoolBookmarkAppendValidator(bookmarkRepository, schoolRepository);
}

@Test
void 해당하는_학교가_없으면_예외() {
// when && then
assertThatThrownBy(() -> schoolBookmarkAppendValidator.validate(-1L, 1L))
.isInstanceOf(NotFoundException.class)
.hasMessage(ErrorCode.SCHOOL_NOT_FOUND.getMessage());
}

@Test
void 학교_북마크_갯수가_이미_12개_이상이면_예외() {
// given
Long memberId = 1L;
for (long i = 0; i < 12; i++) {
Long schoolId = schoolRepository.save(SchoolFixture.school().build()).getId();
bookmarkRepository.save(new Bookmark(BookmarkType.SCHOOL, schoolId, memberId));
}

Long schoolId = schoolRepository.save(SchoolFixture.school().build()).getId();

// when && then
assertThatThrownBy(() -> schoolBookmarkAppendValidator.validate(schoolId, memberId))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("북마크는 저장 갯수를 초과하였습니다.");
}

@Test
void 이미_해당하는_북마크가_저장됐다면_예외() {
// given
Long schoolId = schoolRepository.save(SchoolFixture.school().build()).getId();
bookmarkRepository.save(new Bookmark(BookmarkType.SCHOOL, schoolId, 1L));

// when && then
assertThatThrownBy(() -> schoolBookmarkAppendValidator.validate(schoolId, 1L))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("이미 저장된 북마크입니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.festago.bookmark.application;

import static com.festago.bookmark.domain.BookmarkType.SCHOOL;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.only;
import static org.mockito.Mockito.verify;

import com.festago.bookmark.domain.Bookmark;
import com.festago.bookmark.repository.BookmarkRepository;
import com.festago.member.repository.MemberRepository;
import com.festago.school.repository.SchoolRepository;
import com.festago.support.ApplicationIntegrationTest;
import com.festago.support.MemberFixture;
import com.festago.support.SchoolFixture;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;

@DisplayNameGeneration(ReplaceUnderscores.class)
@SuppressWarnings("NonAsciiCharacters")
class SchoolBookmarkCommandIntegrationServiceTest extends ApplicationIntegrationTest {

@Autowired
SchoolRepository schoolRepository;

@Autowired
BookmarkRepository bookmarkRepository;

@Autowired
MemberRepository memberRepository;

@Autowired
SchoolBookmarkCommandService schoolBookmarkCommandService;

@MockBean
SchoolBookmarkAppendValidator schoolBookmarkAppendValidator;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SchoolBookmarkCommand 서비스의 로직을 보니, 통합 테스트로 수행할만한 특정 구현체의 의존이 필요 하지는 않는 것 같네요!
그리고 SchoolBookmarkAppendValidator@MockBean으로 사용할 필요가 있을까요?


@Test
void 북마크_저장_성공() {
// given
Long memberId = memberRepository.save(MemberFixture.member().build()).getId();
Long schoolId = schoolRepository.save(SchoolFixture.school().build()).getId();

// when
Long actual = schoolBookmarkCommandService.save(schoolId, memberId);

// then
assertThat(actual).isPositive();
verify(schoolBookmarkAppendValidator, only()).validate(schoolId, memberId);
}

@Test
void 북마크를_삭제한다() {
// given
Long memberId = memberRepository.save(MemberFixture.member().build()).getId();
Long schoolId = schoolRepository.save(SchoolFixture.school().build()).getId();
bookmarkRepository.save(new Bookmark(SCHOOL, schoolId, memberId));

// when
schoolBookmarkCommandService.delete(schoolId, memberId);

// then
var actual = bookmarkRepository.existsByBookmarkTypeAndMemberIdAndResourceId(SCHOOL, memberId,
schoolId);
assertThat(actual).isFalse();
}
}
Loading
Loading