-
Notifications
You must be signed in to change notification settings - Fork 8
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
Changes from 26 commits
ffa3bbf
16f9fcd
5282a50
73c69f6
7b9e2d9
3629eb3
3f8f14a
a7ea199
5079562
881c906
f4fff00
e93a71e
1c3d3cd
5dbd257
3a25bdc
0d309fb
f2932d0
e4efd02
8af1ce8
62966bb
7875820
0b071b7
7776ca0
7dffc0f
9b5d90f
e1ffd78
554b433
b856e70
03647ed
1c4763a
994fe05
35b8799
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
|
||
//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("북마크는 저장 갯수를 초과하였습니다."); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
) { | ||
|
||
@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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #769 |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
default School getOrThrow(Long schoolId) { | ||
return findById(schoolId) | ||
|
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SchoolBookmarkCommand 서비스의 로직을 보니, 통합 테스트로 수행할만한 특정 구현체의 의존이 필요 하지는 않는 것 같네요! |
||
|
||
@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(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
각 북마크마다 별도로 Validate 해주기로 했기 때문에 이름을 Bookmark에서 SchoolBookmark로 변경했어요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Service 내부 메서드가 아닌 validator를 유지하시는 건가요??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
양방향 의존을 피하기 위해 Validator를 사용하는게 아니라면 Service 내부에 위치 시켜도 좋을 것 같네요!