From 312cbbbce34afb1a5a4f53a78e0d07d0b50e9f36 Mon Sep 17 00:00:00 2001 From: JiWoo Date: Tue, 23 Jan 2024 16:08:54 +0900 Subject: [PATCH] [Feat] create bookmark #119 (#124) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(ErrorCode): 사용하지 않는 에러코드 제거 * feat(Bookmarks): User의 Bookmarks 구현 * feat(UserQueryRepositoryImpl#findAllBookmarks): User가 구독한 공지를 조회하는 조회 메서드 구현 * feat(DepartmentNoticeRepository): 사용하지 않는 DepartmentNoticeRepository 제거 * feat(NoticeService#getNoticeV1): 사용하지 않는 메서드 제거 * feat(FeedbackService): 사용하지 않는 feedback 서비스 제거 * refactor(AcceptanceTest): 공통된 FirebaseMockBean 상위계층으로 올리기 * test(UserAcceptanceTest): 사용자가 북마크를 저장하는 인수테스트 작성 * test(UserAcceptanceTest): 북마크 인수테스트 릭팩토링 * feat(UserCommandApiV2#saveBookmark): 공지 북마크 기능 구현 완료 * refactor(UserRepositoryTest): 사용자의 id로 북마크한 공지를 조회하는 메서드를 NoticeRepository로 이동한다 * feat: 북마크한 공지 조회 기능 구현 * refactor: test 페키지 리팩토링 * refactor: test의 서식 지정자 제거 * docs(README.md): 내용 일부 업데이트 --- README.md | 36 +++++-- .../common/dto/ResponseCodeAndMessages.java | 9 +- .../common/exception/code/ErrorCode.java | 8 -- .../kuring/notice/business/NoticeService.java | 27 ++---- .../DepartmentNoticeQueryRepository.java | 19 ---- .../DepartmentNoticeQueryRepositoryImpl.java | 88 ----------------- .../domain/DepartmentNoticeRepository.java | 6 -- .../kustacks/kuring/notice/domain/Notice.java | 2 +- .../notice/domain/NoticeQueryRepository.java | 13 +++ .../domain/NoticeQueryRepositoryImpl.java | 96 +++++++++++++++++++ .../notice/domain/NoticeRepository.java | 4 - .../notice/facade/NoticeQueryFacade.java | 3 +- .../kuring/user/business/FeedbackService.java | 47 --------- .../kuring/user/business/UserService.java | 35 +++++-- .../kuring/user/common/dto/BookmarkDto.java | 30 ++++++ .../user/common/dto/SaveBookmarkRequest.java | 17 ++++ .../kuring/user/domain/Bookmarks.java | 41 ++++++++ .../com/kustacks/kuring/user/domain/User.java | 15 ++- .../kuring/user/facade/UserCommandFacade.java | 9 +- .../kuring/user/facade/UserQueryFacade.java | 12 ++- .../user/presentation/UserCommandApiV2.java | 12 ++- .../user/presentation/UserQueryApiV2.java | 10 +- .../notice/DepartmentNoticeUpdater.java | 14 +-- .../migration/V240122__Create_bookmarks.sql | 6 ++ .../acceptance/AdminAcceptanceTest.java | 8 +- .../kuring/acceptance/AuthAcceptanceTest.java | 3 +- .../acceptance/CategoryAcceptanceTest.java | 8 +- .../kuring/acceptance/CategoryStep.java | 2 +- .../acceptance/FeedbackAcceptanceTest.java | 8 +- .../acceptance/NoticeAcceptanceTest.java | 3 +- .../acceptance/StaffAcceptanceTest.java | 5 +- .../kuring/acceptance/UserAcceptanceTest.java | 42 ++++++-- .../kustacks/kuring/acceptance/UserStep.java | 44 +++++++++ .../repository/NoticeRepositoryTest.java | 66 +++++++++++++ .../DatabaseConfigurator.java | 25 ++--- .../IntegrationTestSupport.java} | 21 ++-- .../kuring/user/domain/BookmarksTest.java | 58 +++++++++++ .../user/repository/UserRepositoryTest.java | 47 +++++++++ .../update/DepartmentNoticeUpdaterTest.java | 13 ++- 39 files changed, 622 insertions(+), 290 deletions(-) delete mode 100644 src/main/java/com/kustacks/kuring/notice/domain/DepartmentNoticeQueryRepository.java delete mode 100644 src/main/java/com/kustacks/kuring/notice/domain/DepartmentNoticeQueryRepositoryImpl.java delete mode 100644 src/main/java/com/kustacks/kuring/notice/domain/DepartmentNoticeRepository.java delete mode 100644 src/main/java/com/kustacks/kuring/user/business/FeedbackService.java create mode 100644 src/main/java/com/kustacks/kuring/user/common/dto/BookmarkDto.java create mode 100644 src/main/java/com/kustacks/kuring/user/common/dto/SaveBookmarkRequest.java create mode 100644 src/main/java/com/kustacks/kuring/user/domain/Bookmarks.java create mode 100644 src/main/resources/db/migration/V240122__Create_bookmarks.sql create mode 100644 src/test/java/com/kustacks/kuring/notice/repository/NoticeRepositoryTest.java rename src/test/java/com/kustacks/kuring/{tool => support}/DatabaseConfigurator.java (84%) rename src/test/java/com/kustacks/kuring/{acceptance/AcceptanceTest.java => support/IntegrationTestSupport.java} (54%) create mode 100644 src/test/java/com/kustacks/kuring/user/domain/BookmarksTest.java create mode 100644 src/test/java/com/kustacks/kuring/user/repository/UserRepositoryTest.java diff --git a/README.md b/README.md index d026c76d..d140f507 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,23 @@ https://blogshine.tistory.com/345 **상세 내용 링크 : ([글 링크](https://blogshine.tistory.com/664))** --- -# 5. Bulk Query를 통한 성능 개선 +## 5. HeapDump를 통해 메모리 누수 원인 찾기 **검색 성능개선** + +**문제 상황** + +- 애플리케이션에 물리적으로 할당된 메모리를 넘어, swap 메모리까지 사용하고 있는 문제가 발생 + +**문제 해결** + +- 매번 URL 검증을 위한 객체를 생성후 검사 하는것이 아닌, 한번 Compiled된 Pattern 사용을 통한 **메모리 낭비 해결** + - 개선 **전** : 전체 512MB중 170MB가 eden space에 주기적으로 생성 → 32% + - 개선 **후** : 전체 512MB중 47MB만 생성되도록 개선 → 9% + - 미리 compiled된 Pattern 객체를 활용하여 메모리 누수를 해결하였습니다. + +**상세 내용 링크 : ([글 링크](https://blogshine.tistory.com/687))** + +--- +# 6. Bulk Query를 통한 성능 개선 **문제 상황** @@ -227,7 +243,7 @@ https://blogshine.tistory.com/345
-### 5-1) Insert 해결책 +### 6-1) Insert 해결책 해결책은 2가지가 존재했습니다. 1. Table Id strategy를 SEQUENCE로 변경하고 Batch 작업 @@ -242,14 +258,14 @@ MySQL과 MariaDB의 Table Id 전략은 대부분이 IDENTITY 전략을 사용하
-### 5-2) Delete 해결책 +### 6-2) Delete 해결책 이미 프로젝트에서 queryDsl를 사용하고 있어 이를 이용하는 것이 가장 간단했기 때문에 queryDsl의 delete in 쿼리를 사용하여 해결했습니다. **상세 내용 링크 : ([글 링크](https://blogshine.tistory.com/686))** --- -## 6. 인증, 인가를 비즈니스 로직으로부터 분리하기 +## 7. 인증, 인가를 비즈니스 로직으로부터 분리하기 **문제 상황** @@ -267,7 +283,7 @@ MySQL과 MariaDB의 Table Id 전략은 대부분이 IDENTITY 전략을 사용하 --- -## 7. 흔하디 흔한 N+1 쿼리 개선기 +## 8. 흔하디 흔한 N+1 쿼리 개선기 원래 로직에서는 사용자의 Category 이름 목록을 가져오기 위해서 다음과 같이 처리가 되고 잇었습니다! @@ -325,7 +341,7 @@ public List getCategoryNamesFromCategories(List categories) { 쿼리가 총 1 + 2N 만큼 발생중이다. -### 7 - 1) 변경 전 쿼리 +### 8 - 1) 변경 전 쿼리 ```bash Hibernate: @@ -382,7 +398,7 @@ Connection: keep-alive N+1 문제로 User한번 조회하는데 위와 같이 쿼리가 3번 나가게 됨 -### 7 - 2) 변경 후 +### 8 - 2) 변경 후 변경 후 한방 쿼리로 조회 끝 ```java @@ -399,7 +415,7 @@ public List getUserCategoryNamesByToken(String token) { ___ -## 8. Test Container를 통한 테스트의 멱등성 보장하기 +## 9. Test Container를 통한 테스트의 멱등성 보장하기 테스트와, 실제 운영 DB를 둘다 MariaDB 환경으로 사용하여 문제가 발생할 일이 없다 생각했었습니다. 하지만, utf8과 같은 인코딩 방식이 로컬과 프로덕션이 달라 문제가 발생하였으며, 이또한 테스트 환경에서 걸러내지 못한 것이 문제라 생각하였습니다. @@ -408,7 +424,7 @@ ___ --- -## 9. CI / 정적분석기(SonarCloud, jacoco)를 사용한 코드 컨벤션에 대한 코드리뷰 자동화 +## 10. CI / 정적분석기(SonarCloud, jacoco)를 사용한 코드 컨벤션에 대한 코드리뷰 자동화 **문제 상황** @@ -425,7 +441,7 @@ ___ --- -## 10. 서버 모니터링 +## 11. 서버 모니터링 **문제 상황** diff --git a/src/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.java b/src/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.java index b4f5c407..c3face4b 100644 --- a/src/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.java +++ b/src/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.java @@ -10,7 +10,6 @@ public enum ResponseCodeAndMessages { /* Category */ CATEGORY_SEARCH_SUCCESS(HttpStatus.OK.value(), "지원하는 학교 공지 카테고리 조회에 성공하였습니다"), CATEGORY_SUBSCRIBE_SUCCESS(HttpStatus.OK.value(), "사용자의 학교 공지 카테고리 구독에 성공하였습니다"), - CATEGORY_USER_SUBSCRIBES_LOOKUP_SUCCESS(HttpStatus.OK.value(), "사용자가 구독한 학교 공지 카테고리 조회에 성공하였습니다"), /* Department */ @@ -21,10 +20,6 @@ public enum ResponseCodeAndMessages { /* Staff */ STAFF_SEARCH_SUCCESS(HttpStatus.OK.value(), "교직원 조회에 성공하였습니다"), - /* Feedback */ - FEEDBACK_SAVE_SUCCESS(HttpStatus.OK.value(), "피드백 저장에 성공하였습니다"), - FEEDBACK_SEARCH_SUCCESS(HttpStatus.OK.value(), "피드백 조회에 성공하였습니다"), - /* Admin */ ADMIN_TEST_NOTICE_CREATE_SUCCESS(HttpStatus.OK.value(), "테스트 공지 생성에 성공하였습니다"), ADMIN_REAL_NOTICE_CREATE_SUCCESS(HttpStatus.OK.value(), "실제 공지 생성에 성공하였습니다"), @@ -32,6 +27,10 @@ public enum ResponseCodeAndMessages { /* User */ USER_REGISTER_SUCCESS(HttpStatus.OK.value(), "회원가입에 성공하였습니다"), USER_REGISTER_FAIL(HttpStatus.BAD_REQUEST.value(), "회원가입에 실패하였습니다"), + BOOKMAKR_SAVE_SUCCESS(HttpStatus.OK.value(), "북마크 저장에 성공하였습니다"), + BOOKMARK_LOOKUP_SUCCESS(HttpStatus.OK.value(), "북마크 조회에 성공하였습니다"), + FEEDBACK_SAVE_SUCCESS(HttpStatus.OK.value(), "피드백 저장에 성공하였습니다"), + FEEDBACK_SEARCH_SUCCESS(HttpStatus.OK.value(), "피드백 조회에 성공하였습니다"), /** * ErrorCodes about auth diff --git a/src/main/java/com/kustacks/kuring/common/exception/code/ErrorCode.java b/src/main/java/com/kustacks/kuring/common/exception/code/ErrorCode.java index d7004411..2f509eeb 100644 --- a/src/main/java/com/kustacks/kuring/common/exception/code/ErrorCode.java +++ b/src/main/java/com/kustacks/kuring/common/exception/code/ErrorCode.java @@ -21,7 +21,6 @@ public enum ErrorCode { API_NOTICE_NOT_EXIST_CATEGORY(HttpStatus.BAD_REQUEST, "해당 공지 카테고리를 지원하지 않습니다."), -// API_NOTICE_CANNOT_FIND_CATEGORY(HttpStatus.INTERNAL_SERVER_ERROR, "해당 공지 카테고리를 찾을 수 없습니다."), API_MISSING_PARAM(HttpStatus.BAD_REQUEST, "필수 파라미터가 없습니다."), API_INVALID_PARAM(HttpStatus.BAD_REQUEST, "파라미터 값 중 잘못된 값이 있습니다."), API_BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), @@ -37,13 +36,6 @@ public enum ErrorCode { UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "알 수 없는 오류"), - /** - * ErrorCodes about WebSocket - */ - WS_MISSING_PARAM(HttpStatus.BAD_REQUEST, "웹소켓 메세지의 파라미터가 누락되어있습니다."), - WS_INVALID_PARAM(HttpStatus.BAD_REQUEST, "웹소켓 메세지 파라미터 중 유효하지 않은 값이 있습니다."), - WS_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."), - /** * ErrorCodes about InternalLogicException */ diff --git a/src/main/java/com/kustacks/kuring/notice/business/NoticeService.java b/src/main/java/com/kustacks/kuring/notice/business/NoticeService.java index f5a35d67..59095dd7 100644 --- a/src/main/java/com/kustacks/kuring/notice/business/NoticeService.java +++ b/src/main/java/com/kustacks/kuring/notice/business/NoticeService.java @@ -1,15 +1,12 @@ package com.kustacks.kuring.notice.business; -import com.kustacks.kuring.notice.domain.CategoryName; +import com.kustacks.kuring.common.exception.InternalLogicException; import com.kustacks.kuring.common.exception.NotFoundException; import com.kustacks.kuring.common.exception.code.ErrorCode; -import com.kustacks.kuring.common.exception.InternalLogicException; -import com.kustacks.kuring.notice.common.OffsetBasedPageRequest; import com.kustacks.kuring.notice.common.dto.NoticeDto; -import com.kustacks.kuring.notice.common.dto.NoticeListResponse; import com.kustacks.kuring.notice.common.dto.NoticeSearchDto; +import com.kustacks.kuring.notice.domain.CategoryName; import com.kustacks.kuring.notice.domain.DepartmentName; -import com.kustacks.kuring.notice.domain.DepartmentNoticeRepository; import com.kustacks.kuring.notice.domain.NoticeRepository; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.PageRequest; @@ -18,14 +15,12 @@ import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; @Service @Transactional(readOnly = true) public class NoticeService { private final NoticeRepository noticeRepository; - private final DepartmentNoticeRepository departmentNoticeRepository; private final CategoryName[] supportedCategoryNameList; private final DepartmentName[] supportedDepartmentNameList; private final String SPACE_REGEX = "[\\s+]"; @@ -36,9 +31,8 @@ public class NoticeService { @Value("${notice.library-base-url}") private String libraryBaseUrl; - public NoticeService(NoticeRepository noticeRepository, DepartmentNoticeRepository departmentNoticeRepository) { + public NoticeService(NoticeRepository noticeRepository) { this.noticeRepository = noticeRepository; - this.departmentNoticeRepository = departmentNoticeRepository; this.supportedCategoryNameList = CategoryName.values(); this.supportedDepartmentNameList = DepartmentName.values(); } @@ -47,23 +41,14 @@ public List lookupSupportedDepartments() { return List.of(supportedDepartmentNameList); } - public NoticeListResponse getNotices(String type, int offset, int max) { - String categoryName = convertShortNameIntoLongName(type); - - List noticeDtoList = noticeRepository - .findNoticesByCategoryWithOffset(CategoryName.fromStringName(categoryName), new OffsetBasedPageRequest(offset, max)); - - return new NoticeListResponse(convertBaseUrl(categoryName), noticeDtoList); - } - - public List getNoticesV2(String type, String department, Boolean important, int page, int size) { + public List getNotices(String type, String department, Boolean important, int page, int size) { if (isDepartmentSearchRequest(type, department)) { DepartmentName departmentName = DepartmentName.fromHostPrefix(department); if (Boolean.TRUE.equals(important)) { - return departmentNoticeRepository.findImportantNoticesByDepartment(departmentName); + return noticeRepository.findImportantNoticesByDepartment(departmentName); } else { - return departmentNoticeRepository.findNormalNoticesByDepartmentWithOffset(departmentName, PageRequest.of(page, size)); + return noticeRepository.findNormalNoticesByDepartmentWithOffset(departmentName, PageRequest.of(page, size)); } } diff --git a/src/main/java/com/kustacks/kuring/notice/domain/DepartmentNoticeQueryRepository.java b/src/main/java/com/kustacks/kuring/notice/domain/DepartmentNoticeQueryRepository.java deleted file mode 100644 index ca082ad5..00000000 --- a/src/main/java/com/kustacks/kuring/notice/domain/DepartmentNoticeQueryRepository.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.kustacks.kuring.notice.domain; - -import com.kustacks.kuring.notice.common.dto.NoticeDto; -import org.springframework.data.domain.Pageable; - -import java.util.List; - -public interface DepartmentNoticeQueryRepository { - - List findImportantNoticesByDepartment(DepartmentName departmentName); - - List findNormalNoticesByDepartmentWithOffset(DepartmentName departmentName, Pageable pageable); - - List findImportantArticleIdsByDepartment(DepartmentName departmentNameEnum); - - List findNormalArticleIdsByDepartment(DepartmentName departmentNameEnum); - - void deleteAllByIdsAndDepartment(DepartmentName departmentName, List articleIds); -} diff --git a/src/main/java/com/kustacks/kuring/notice/domain/DepartmentNoticeQueryRepositoryImpl.java b/src/main/java/com/kustacks/kuring/notice/domain/DepartmentNoticeQueryRepositoryImpl.java deleted file mode 100644 index 82b79659..00000000 --- a/src/main/java/com/kustacks/kuring/notice/domain/DepartmentNoticeQueryRepositoryImpl.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.kustacks.kuring.notice.domain; - -import com.kustacks.kuring.notice.common.dto.NoticeDto; -import com.kustacks.kuring.notice.common.dto.QNoticeDto; -import com.querydsl.jpa.impl.JPAQueryFactory; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; - -import java.util.List; - -import static com.kustacks.kuring.notice.domain.QDepartmentNotice.departmentNotice; - -@RequiredArgsConstructor -public class DepartmentNoticeQueryRepositoryImpl implements DepartmentNoticeQueryRepository { - - private final JPAQueryFactory queryFactory; - - @Override - public List findImportantArticleIdsByDepartment(DepartmentName departmentName) { - return queryFactory - .select(departmentNotice.articleId.castToNum(Integer.class)) - .from(departmentNotice) - .where(departmentNotice.departmentName.eq(departmentName) - .and(departmentNotice.important.eq(true))) - .orderBy(departmentNotice.articleId.castToNum(Integer.class).asc()) - .fetch(); - } - - @Override - public List findNormalArticleIdsByDepartment(DepartmentName departmentName) { - return queryFactory - .select(departmentNotice.articleId.castToNum(Integer.class)) - .from(departmentNotice) - .where(departmentNotice.departmentName.eq(departmentName) - .and(departmentNotice.important.eq(false))) - .orderBy(departmentNotice.articleId.castToNum(Integer.class).asc()) - .fetch(); - } - - @Override - public List findImportantNoticesByDepartment(DepartmentName departmentName) { - return queryFactory - .select(new QNoticeDto( - departmentNotice.articleId, - departmentNotice.postedDate, - departmentNotice.url.value, - departmentNotice.subject, - departmentNotice.categoryName.stringValue().toLowerCase(), - departmentNotice.important)) - .from(departmentNotice) - .where(departmentNotice.departmentName.eq(departmentName) - .and(departmentNotice.important.isTrue())) - .orderBy(departmentNotice.postedDate.desc()) - .fetch(); - } - - @Override - public List findNormalNoticesByDepartmentWithOffset(DepartmentName departmentName, Pageable pageable) { - return queryFactory - .select(new QNoticeDto( - departmentNotice.articleId, - departmentNotice.postedDate, - departmentNotice.url.value, - departmentNotice.subject, - departmentNotice.categoryName.stringValue().toLowerCase(), - departmentNotice.important)) - .from(departmentNotice) - .where(departmentNotice.departmentName.eq(departmentName) - .and(departmentNotice.important.isFalse())) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .orderBy(departmentNotice.postedDate.desc()) - .fetch(); - } - - @Override - public void deleteAllByIdsAndDepartment(DepartmentName departmentName, List articleIds) { - if(articleIds.isEmpty()) { - return; - } - - queryFactory - .delete(departmentNotice) - .where(departmentNotice.departmentName.eq(departmentName) - .and(departmentNotice.articleId.in(articleIds))) - .execute(); - } -} diff --git a/src/main/java/com/kustacks/kuring/notice/domain/DepartmentNoticeRepository.java b/src/main/java/com/kustacks/kuring/notice/domain/DepartmentNoticeRepository.java deleted file mode 100644 index 2d709065..00000000 --- a/src/main/java/com/kustacks/kuring/notice/domain/DepartmentNoticeRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.kustacks.kuring.notice.domain; - -import org.springframework.data.jpa.repository.JpaRepository; - -public interface DepartmentNoticeRepository extends JpaRepository, DepartmentNoticeQueryRepository { -} diff --git a/src/main/java/com/kustacks/kuring/notice/domain/Notice.java b/src/main/java/com/kustacks/kuring/notice/domain/Notice.java index 00a58b2e..8e9545ed 100644 --- a/src/main/java/com/kustacks/kuring/notice/domain/Notice.java +++ b/src/main/java/com/kustacks/kuring/notice/domain/Notice.java @@ -18,7 +18,7 @@ public class Notice { private Long id; @Getter(AccessLevel.PUBLIC) - @Column(name = "article_id", length = 15, nullable = false) + @Column(name = "article_id", length = 32, nullable = false) private String articleId; @Getter(AccessLevel.PUBLIC) diff --git a/src/main/java/com/kustacks/kuring/notice/domain/NoticeQueryRepository.java b/src/main/java/com/kustacks/kuring/notice/domain/NoticeQueryRepository.java index bd054bcd..11b0dc31 100644 --- a/src/main/java/com/kustacks/kuring/notice/domain/NoticeQueryRepository.java +++ b/src/main/java/com/kustacks/kuring/notice/domain/NoticeQueryRepository.java @@ -2,6 +2,7 @@ import com.kustacks.kuring.notice.common.dto.NoticeDto; import com.kustacks.kuring.notice.common.dto.NoticeSearchDto; +import com.kustacks.kuring.user.common.dto.BookmarkDto; import org.springframework.data.domain.Pageable; import java.util.List; @@ -15,4 +16,16 @@ public interface NoticeQueryRepository { List findNormalArticleIdsByCategory(CategoryName categoryName); void deleteAllByIdsAndCategory(CategoryName categoryName, List articleIds); + + List findImportantNoticesByDepartment(DepartmentName departmentName); + + List findNormalNoticesByDepartmentWithOffset(DepartmentName departmentName, Pageable pageable); + + List findImportantArticleIdsByDepartment(DepartmentName departmentNameEnum); + + List findNormalArticleIdsByDepartment(DepartmentName departmentNameEnum); + + void deleteAllByIdsAndDepartment(DepartmentName departmentName, List articleIds); + + List findAllByBookmarkIds(List ids); } diff --git a/src/main/java/com/kustacks/kuring/notice/domain/NoticeQueryRepositoryImpl.java b/src/main/java/com/kustacks/kuring/notice/domain/NoticeQueryRepositoryImpl.java index 40713384..c3754d52 100644 --- a/src/main/java/com/kustacks/kuring/notice/domain/NoticeQueryRepositoryImpl.java +++ b/src/main/java/com/kustacks/kuring/notice/domain/NoticeQueryRepositoryImpl.java @@ -4,6 +4,8 @@ import com.kustacks.kuring.notice.common.dto.NoticeSearchDto; import com.kustacks.kuring.notice.common.dto.QNoticeDto; import com.kustacks.kuring.notice.common.dto.QNoticeSearchDto; +import com.kustacks.kuring.user.common.dto.BookmarkDto; +import com.kustacks.kuring.user.common.dto.QBookmarkDto; import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.NumberTemplate; @@ -14,6 +16,7 @@ import java.util.List; +import static com.kustacks.kuring.notice.domain.QDepartmentNotice.departmentNotice; import static com.kustacks.kuring.notice.domain.QNotice.notice; @RequiredArgsConstructor @@ -75,6 +78,99 @@ public void deleteAllByIdsAndCategory(CategoryName categoryName, List ar .execute(); } + @Transactional(readOnly = true) + @Override + public List findImportantArticleIdsByDepartment(DepartmentName departmentName) { + return queryFactory + .select(departmentNotice.articleId.castToNum(Integer.class)) + .from(departmentNotice) + .where(departmentNotice.departmentName.eq(departmentName) + .and(departmentNotice.important.eq(true))) + .orderBy(departmentNotice.articleId.castToNum(Integer.class).asc()) + .fetch(); + } + + @Transactional(readOnly = true) + @Override + public List findNormalArticleIdsByDepartment(DepartmentName departmentName) { + return queryFactory + .select(departmentNotice.articleId.castToNum(Integer.class)) + .from(departmentNotice) + .where(departmentNotice.departmentName.eq(departmentName) + .and(departmentNotice.important.eq(false))) + .orderBy(departmentNotice.articleId.castToNum(Integer.class).asc()) + .fetch(); + } + + @Transactional(readOnly = true) + @Override + public List findImportantNoticesByDepartment(DepartmentName departmentName) { + return queryFactory + .select(new QNoticeDto( + departmentNotice.articleId, + departmentNotice.postedDate, + departmentNotice.url.value, + departmentNotice.subject, + departmentNotice.categoryName.stringValue().toLowerCase(), + departmentNotice.important)) + .from(departmentNotice) + .where(departmentNotice.departmentName.eq(departmentName) + .and(departmentNotice.important.isTrue())) + .orderBy(departmentNotice.postedDate.desc()) + .fetch(); + } + + @Transactional(readOnly = true) + @Override + public List findNormalNoticesByDepartmentWithOffset(DepartmentName departmentName, Pageable pageable) { + return queryFactory + .select(new QNoticeDto( + departmentNotice.articleId, + departmentNotice.postedDate, + departmentNotice.url.value, + departmentNotice.subject, + departmentNotice.categoryName.stringValue().toLowerCase(), + departmentNotice.important)) + .from(departmentNotice) + .where(departmentNotice.departmentName.eq(departmentName) + .and(departmentNotice.important.isFalse())) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(departmentNotice.postedDate.desc()) + .fetch(); + } + + @Transactional + @Override + public void deleteAllByIdsAndDepartment(DepartmentName departmentName, List articleIds) { + if(articleIds.isEmpty()) { + return; + } + + queryFactory + .delete(departmentNotice) + .where(departmentNotice.departmentName.eq(departmentName) + .and(departmentNotice.articleId.in(articleIds))) + .execute(); + } + + @Transactional(readOnly = true) + @Override + public List findAllByBookmarkIds(List ids) { + return queryFactory.select( + new QBookmarkDto( + notice.articleId, + notice.postedDate, + notice.subject, + notice.categoryName.stringValue(), + notice.url.value + ) + ).from(notice) + .where(notice.articleId.in(ids)) + .orderBy(notice.postedDate.desc()) + .fetch(); + } + private static BooleanBuilder isContainSubject(List keywords) { BooleanBuilder booleanBuilder = new BooleanBuilder(); for (String containedName : keywords) { diff --git a/src/main/java/com/kustacks/kuring/notice/domain/NoticeRepository.java b/src/main/java/com/kustacks/kuring/notice/domain/NoticeRepository.java index d197b38e..a15d69ca 100644 --- a/src/main/java/com/kustacks/kuring/notice/domain/NoticeRepository.java +++ b/src/main/java/com/kustacks/kuring/notice/domain/NoticeRepository.java @@ -2,9 +2,5 @@ import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - public interface NoticeRepository extends JpaRepository, NoticeQueryRepository { - - List findByCategoryName(CategoryName categoryName); } diff --git a/src/main/java/com/kustacks/kuring/notice/facade/NoticeQueryFacade.java b/src/main/java/com/kustacks/kuring/notice/facade/NoticeQueryFacade.java index 48e33020..6f4617d2 100644 --- a/src/main/java/com/kustacks/kuring/notice/facade/NoticeQueryFacade.java +++ b/src/main/java/com/kustacks/kuring/notice/facade/NoticeQueryFacade.java @@ -9,7 +9,6 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.stream.Collectors; import java.util.stream.Stream; @Service @@ -20,7 +19,7 @@ public class NoticeQueryFacade { private final NoticeService noticeService; public List getNotices(String type, String department, Boolean important, int page, int size) { - return noticeService.getNoticesV2(type, department, important, page, size); + return noticeService.getNotices(type, department, important, page, size); } public NoticeLookupResponse searchNoticeByContent(String content) { diff --git a/src/main/java/com/kustacks/kuring/user/business/FeedbackService.java b/src/main/java/com/kustacks/kuring/user/business/FeedbackService.java deleted file mode 100644 index c59db0ba..00000000 --- a/src/main/java/com/kustacks/kuring/user/business/FeedbackService.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.kustacks.kuring.user.business; - -import com.kustacks.kuring.admin.common.dto.FeedbackDto; -import com.kustacks.kuring.common.exception.NotFoundException; -import com.kustacks.kuring.common.exception.code.ErrorCode; -import com.kustacks.kuring.message.firebase.FirebaseService; -import com.kustacks.kuring.message.firebase.ServerProperties; -import com.kustacks.kuring.user.domain.User; -import com.kustacks.kuring.user.domain.UserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.PageRequest; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.Optional; - -import static com.kustacks.kuring.message.firebase.FirebaseService.ALL_DEVICE_SUBSCRIBED_TOPIC; - -@Service -@Transactional -@RequiredArgsConstructor -public class FeedbackService { - - private final UserRepository userRepository; - private final FirebaseService firebaseService; - private final ServerProperties serverProperties; - - public void saveFeedback(String token, String content) { - Optional optionalUser = userRepository.findByToken(token); - if(optionalUser.isEmpty()) { - optionalUser = Optional.of(userRepository.save(new User(token))); - firebaseService.subscribe(token, serverProperties.ifDevThenAddSuffix(ALL_DEVICE_SUBSCRIBED_TOPIC)); - } - - User findUser = optionalUser - .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND)); - - findUser.addFeedback(content); - } - - @Transactional(readOnly = true) - public List lookupFeedbacks(int page, int size) { - PageRequest pageRequest = PageRequest.of(page, size); - return userRepository.findAllFeedbackByPageRequest(pageRequest); - } -} diff --git a/src/main/java/com/kustacks/kuring/user/business/UserService.java b/src/main/java/com/kustacks/kuring/user/business/UserService.java index 5bf14cb0..9d0e1601 100644 --- a/src/main/java/com/kustacks/kuring/user/business/UserService.java +++ b/src/main/java/com/kustacks/kuring/user/business/UserService.java @@ -1,22 +1,25 @@ package com.kustacks.kuring.user.business; +import com.kustacks.kuring.admin.common.dto.FeedbackDto; import com.kustacks.kuring.common.exception.NotFoundException; import com.kustacks.kuring.common.exception.code.ErrorCode; import com.kustacks.kuring.message.firebase.FirebaseService; import com.kustacks.kuring.message.firebase.ServerProperties; import com.kustacks.kuring.notice.domain.CategoryName; import com.kustacks.kuring.notice.domain.DepartmentName; +import com.kustacks.kuring.notice.domain.NoticeRepository; +import com.kustacks.kuring.user.common.dto.BookmarkDto; import com.kustacks.kuring.user.common.dto.SubscribeCompareResultDto; import com.kustacks.kuring.user.domain.User; import com.kustacks.kuring.user.domain.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; import static com.kustacks.kuring.message.firebase.FirebaseService.ALL_DEVICE_SUBSCRIBED_TOPIC; @@ -27,15 +30,10 @@ public class UserService { private final UserRepository userRepository; + private final NoticeRepository noticeRepository; private final FirebaseService firebaseService; private final ServerProperties serverProperties; - @Transactional(readOnly = true) - public User getUserByToken(String token) { - return userRepository.findByToken(token) - .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND)); - } - @Transactional(readOnly = true) public List lookupSubscribeDepartmentList(String id) { User findUser = findUserByToken(id); @@ -48,6 +46,24 @@ public List lookUpUserCategories(String token) { return findUser.getSubscribedCategoryList(); } + @Transactional(readOnly = true) + public List lookupFeedbacks(int page, int size) { + PageRequest pageRequest = PageRequest.of(page, size); + return userRepository.findAllFeedbackByPageRequest(pageRequest); + } + + @Transactional(readOnly = true) + public List lookupUserBookmarkedNotices(String userToken) { + User user = findUserByToken(userToken); + List bookmarkIds = user.lookupAllBookmarkIds(); + return noticeRepository.findAllByBookmarkIds(bookmarkIds); + } + + public void saveFeedback(String token, String content) { + User findUser = findUserByToken(token); + findUser.addFeedback(content); + } + public SubscribeCompareResultDto editSubscribeCategoryList( String userToken, List newCategoryStringNames) { User user = findUserByToken(userToken); @@ -90,6 +106,11 @@ public void unsubscribeDepartment(String userToken, DepartmentName removeDepartm user.unsubscribeDepartment(removeDepartmentName); } + public void saveBookmark(String userToken, String articleId) { + User user = findUserByToken(userToken); + user.addBookmark(articleId); + } + private User findUserByToken(String token) { Optional optionalUser = userRepository.findByToken(token); if (optionalUser.isEmpty()) { diff --git a/src/main/java/com/kustacks/kuring/user/common/dto/BookmarkDto.java b/src/main/java/com/kustacks/kuring/user/common/dto/BookmarkDto.java new file mode 100644 index 00000000..b8fdb95a --- /dev/null +++ b/src/main/java/com/kustacks/kuring/user/common/dto/BookmarkDto.java @@ -0,0 +1,30 @@ +package com.kustacks.kuring.user.common.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class BookmarkDto { + + private String articleId; + + private String postedDate; + + private String subject; + + private String category; + + private String baseUrl; + + @QueryProjection + public BookmarkDto(String articleId, String postedDate, String subject, String category, String baseUrl) { + this.articleId = articleId; + this.postedDate = postedDate; + this.subject = subject; + this.category = category; + this.baseUrl = baseUrl; + } +} diff --git a/src/main/java/com/kustacks/kuring/user/common/dto/SaveBookmarkRequest.java b/src/main/java/com/kustacks/kuring/user/common/dto/SaveBookmarkRequest.java new file mode 100644 index 00000000..e6cdfe50 --- /dev/null +++ b/src/main/java/com/kustacks/kuring/user/common/dto/SaveBookmarkRequest.java @@ -0,0 +1,17 @@ +package com.kustacks.kuring.user.common.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; + +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class SaveBookmarkRequest { + + @NotBlank + private String articleId; +} diff --git a/src/main/java/com/kustacks/kuring/user/domain/Bookmarks.java b/src/main/java/com/kustacks/kuring/user/domain/Bookmarks.java new file mode 100644 index 00000000..5a82f917 --- /dev/null +++ b/src/main/java/com/kustacks/kuring/user/domain/Bookmarks.java @@ -0,0 +1,41 @@ +package com.kustacks.kuring.user.domain; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.io.Serializable; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + + +@Embeddable +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Bookmarks implements Serializable { + + private static final int MAX_BOOKMARK_SIZE = 10; + + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable( + name = "user_bookmarks", + joinColumns = @JoinColumn(name = "id") + ) + @Column(name = "notice_id") + private Set bookmark = new HashSet<>(); + + public void add(String noticeId) { + if(isInvalidSize()) { + throw new IllegalArgumentException("북마크가 저장 가능한 사이즈를 초과하였습니다."); + } + this.bookmark.add(noticeId); + } + + public List lookupAllId() { + return List.copyOf(this.bookmark); + } + + private boolean isInvalidSize() { + return this.bookmark.size() == MAX_BOOKMARK_SIZE; + } +} diff --git a/src/main/java/com/kustacks/kuring/user/domain/User.java b/src/main/java/com/kustacks/kuring/user/domain/User.java index 82cd7152..e62b63cd 100644 --- a/src/main/java/com/kustacks/kuring/user/domain/User.java +++ b/src/main/java/com/kustacks/kuring/user/domain/User.java @@ -35,11 +35,24 @@ public class User implements Serializable { @Embedded private Categories categories = new Categories(); + @Embedded + private Bookmarks bookmarks = new Bookmarks(); + public User(String token) { this.token = token; } - public Long getId() {return id;} + public Long getId() { + return id; + } + + public void addBookmark(String noticeId) { + this.bookmarks.add(noticeId); + } + + public List lookupAllBookmarkIds() { + return this.bookmarks.lookupAllId(); + } public void addFeedback(String content) { this.feedbacks.add(new Feedback(content, this)); diff --git a/src/main/java/com/kustacks/kuring/user/facade/UserCommandFacade.java b/src/main/java/com/kustacks/kuring/user/facade/UserCommandFacade.java index 0f8528a0..72377368 100644 --- a/src/main/java/com/kustacks/kuring/user/facade/UserCommandFacade.java +++ b/src/main/java/com/kustacks/kuring/user/facade/UserCommandFacade.java @@ -1,6 +1,5 @@ package com.kustacks.kuring.user.facade; -import com.kustacks.kuring.user.business.FeedbackService; import com.kustacks.kuring.message.firebase.FirebaseService; import com.kustacks.kuring.message.firebase.exception.FirebaseSubscribeException; import com.kustacks.kuring.message.firebase.exception.FirebaseUnSubscribeException; @@ -25,7 +24,6 @@ public class UserCommandFacade { private final UserService userService; private final FirebaseService firebaseService; - private final FeedbackService feedbackService; public void editSubscribeCategories(String userToken, List newCategoryNames) { firebaseService.validationToken(userToken); @@ -41,7 +39,12 @@ public void editSubscribeDepartments(String userToken, List departments) public void saveFeedback(String userToken, String feedback) { firebaseService.validationToken(userToken); - feedbackService.saveFeedback(userToken, feedback); + userService.saveFeedback(userToken, feedback); + } + + public void saveBookmark(String userToken, String articleId) { + firebaseService.validationToken(userToken); + userService.saveBookmark(userToken, articleId); } private void editUserCategoryList( diff --git a/src/main/java/com/kustacks/kuring/user/facade/UserQueryFacade.java b/src/main/java/com/kustacks/kuring/user/facade/UserQueryFacade.java index 513ccbf2..60089a0d 100644 --- a/src/main/java/com/kustacks/kuring/user/facade/UserQueryFacade.java +++ b/src/main/java/com/kustacks/kuring/user/facade/UserQueryFacade.java @@ -1,26 +1,24 @@ package com.kustacks.kuring.user.facade; import com.kustacks.kuring.admin.common.dto.FeedbackDto; -import com.kustacks.kuring.user.business.FeedbackService; -import com.kustacks.kuring.notice.domain.CategoryName; import com.kustacks.kuring.message.firebase.FirebaseService; import com.kustacks.kuring.notice.common.dto.CategoryNameDto; import com.kustacks.kuring.notice.common.dto.DepartmentNameDto; +import com.kustacks.kuring.notice.domain.CategoryName; import com.kustacks.kuring.notice.domain.DepartmentName; import com.kustacks.kuring.user.business.UserService; +import com.kustacks.kuring.user.common.dto.BookmarkDto; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.stream.Collectors; @Service @Transactional(readOnly = true) @RequiredArgsConstructor public class UserQueryFacade { - private final FeedbackService feedbackService; private final UserService userService; private final FirebaseService firebaseService; @@ -35,7 +33,11 @@ public List lookupSubscribeDepartments(String userToken) { } public List lookupFeedbacks(int page, int size) { - return feedbackService.lookupFeedbacks(page, size); + return userService.lookupFeedbacks(page, size); + } + + public List lookupUserBookmarkedNotices(String userToken) { + return userService.lookupUserBookmarkedNotices(userToken); } private List convertCategoryNameDtoList(List categoryNamesList) { diff --git a/src/main/java/com/kustacks/kuring/user/presentation/UserCommandApiV2.java b/src/main/java/com/kustacks/kuring/user/presentation/UserCommandApiV2.java index 01144206..fb0cdc66 100644 --- a/src/main/java/com/kustacks/kuring/user/presentation/UserCommandApiV2.java +++ b/src/main/java/com/kustacks/kuring/user/presentation/UserCommandApiV2.java @@ -1,9 +1,10 @@ package com.kustacks.kuring.user.presentation; import com.kustacks.kuring.common.dto.BaseResponse; +import com.kustacks.kuring.user.common.dto.SaveBookmarkRequest; +import com.kustacks.kuring.user.common.dto.SaveFeedbackRequest; import com.kustacks.kuring.user.common.dto.SubscribeCategoriesRequest; import com.kustacks.kuring.user.common.dto.SubscribeDepartmentsRequest; -import com.kustacks.kuring.user.common.dto.SaveFeedbackRequest; import com.kustacks.kuring.user.facade.UserCommandFacade; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -53,4 +54,13 @@ public ResponseEntity> saveFeedback( userCommandFacade.saveFeedback(id, request.getContent()); return ResponseEntity.ok().body(new BaseResponse<>(FEEDBACK_SAVE_SUCCESS, null)); } + + @PostMapping("/bookmarks") + public ResponseEntity> saveBookmark( + @Valid @RequestBody SaveBookmarkRequest request, + @RequestHeader(USER_TOKEN_HEADER_KEY) String id + ) { + userCommandFacade.saveBookmark(id, request.getArticleId()); + return ResponseEntity.ok().body(new BaseResponse<>(BOOKMAKR_SAVE_SUCCESS, null)); + } } diff --git a/src/main/java/com/kustacks/kuring/user/presentation/UserQueryApiV2.java b/src/main/java/com/kustacks/kuring/user/presentation/UserQueryApiV2.java index 739f76f3..d451f5f4 100644 --- a/src/main/java/com/kustacks/kuring/user/presentation/UserQueryApiV2.java +++ b/src/main/java/com/kustacks/kuring/user/presentation/UserQueryApiV2.java @@ -3,6 +3,7 @@ import com.kustacks.kuring.common.dto.BaseResponse; import com.kustacks.kuring.notice.common.dto.CategoryNameDto; import com.kustacks.kuring.notice.common.dto.DepartmentNameDto; +import com.kustacks.kuring.user.common.dto.BookmarkDto; import com.kustacks.kuring.user.facade.UserQueryFacade; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -16,8 +17,7 @@ import java.util.List; -import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.CATEGORY_USER_SUBSCRIBES_LOOKUP_SUCCESS; -import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.DEPARTMENTS_USER_SUBSCRIBES_LOOKUP_SUCCESS; +import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.*; @Slf4j @Validated @@ -41,4 +41,10 @@ public ResponseEntity>> lookupUserSubscribe List departmentNameDtos = userQueryFacade.lookupSubscribeDepartments(id); return ResponseEntity.ok().body(new BaseResponse<>(DEPARTMENTS_USER_SUBSCRIBES_LOOKUP_SUCCESS, departmentNameDtos)); } + + @GetMapping("/bookmarks") + public ResponseEntity>> lookupUserBookmarks(@RequestHeader(USER_TOKEN_HEADER_KEY) String id) { + List bookmarkedDtos = userQueryFacade.lookupUserBookmarkedNotices(id); + return ResponseEntity.ok().body(new BaseResponse<>(BOOKMARK_LOOKUP_SUCCESS, bookmarkedDtos)); + } } diff --git a/src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdater.java b/src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdater.java index 42581217..8a8b421b 100644 --- a/src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdater.java +++ b/src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdater.java @@ -4,8 +4,8 @@ import com.kustacks.kuring.message.firebase.FirebaseService; import com.kustacks.kuring.notice.domain.DepartmentName; import com.kustacks.kuring.notice.domain.DepartmentNotice; -import com.kustacks.kuring.notice.domain.DepartmentNoticeRepository; import com.kustacks.kuring.notice.domain.NoticeJdbcRepository; +import com.kustacks.kuring.notice.domain.NoticeRepository; import com.kustacks.kuring.worker.scrap.DepartmentNoticeScraperTemplate; import com.kustacks.kuring.worker.scrap.deptinfo.DeptInfo; import com.kustacks.kuring.worker.scrap.dto.ComplexNoticeFormatDto; @@ -31,8 +31,8 @@ public class DepartmentNoticeUpdater { private final List deptInfoList; private final DepartmentNoticeScraperTemplate scrapperTemplate; + private final NoticeRepository noticeRepository; private final NoticeJdbcRepository noticeJdbcRepository; - private final DepartmentNoticeRepository departmentNoticeRepository; private final ThreadPoolTaskExecutor noticeUpdaterThreadTaskExecutor; private final FirebaseService firebaseService; private final NoticeUpdateSupport noticeUpdateSupport; @@ -84,14 +84,14 @@ private List compareLatestAndUpdateDB(List newNoticeList = new ArrayList<>(); for (ComplexNoticeFormatDto scrapResult : scrapResults) { // DB에서 모든 중요 공지를 가져와서 - List savedImportantArticleIds = departmentNoticeRepository.findImportantArticleIdsByDepartment(departmentNameEnum); + List savedImportantArticleIds = noticeRepository.findImportantArticleIdsByDepartment(departmentNameEnum); // db와 싱크를 맞춘다 List newImportantNotices = saveNewNotices(scrapResult.getImportantNoticeList(), savedImportantArticleIds, departmentNameEnum, true); newNoticeList.addAll(newImportantNotices); // DB에서 모든 일반 공지 id를 가져와서 - List savedNormalArticleIds = departmentNoticeRepository.findNormalArticleIdsByDepartment(departmentNameEnum); + List savedNormalArticleIds = noticeRepository.findNormalArticleIdsByDepartment(departmentNameEnum); // db와 싱크를 맞춘다 List newNormalNotices = saveNewNotices(scrapResult.getNormalNoticeList(), savedNormalArticleIds, departmentNameEnum, false); @@ -119,13 +119,13 @@ private void compareAllAndUpdateDB(List scrapResults, St for (ComplexNoticeFormatDto scrapResult : scrapResults) { // DB에서 최신 중요 공지를 가져와서 - List savedImportantArticleIds = departmentNoticeRepository.findImportantArticleIdsByDepartment(departmentNameEnum); + List savedImportantArticleIds = noticeRepository.findImportantArticleIdsByDepartment(departmentNameEnum); // db와 싱크를 맞춘다 synchronizationWithDb(scrapResult.getImportantNoticeList(), savedImportantArticleIds, departmentNameEnum, true); // DB에서 모든 일반 공지의 id를 가져와서 - List savedNormalArticleIds = departmentNoticeRepository.findNormalArticleIdsByDepartment(departmentNameEnum); + List savedNormalArticleIds = noticeRepository.findNormalArticleIdsByDepartment(departmentNameEnum); // db와 싱크를 맞춘다 synchronizationWithDb(scrapResult.getNormalNoticeList(), savedNormalArticleIds, departmentNameEnum, false); @@ -145,7 +145,7 @@ private void synchronizationWithDb(List scrapResults, Lis noticeJdbcRepository.saveAllDepartmentNotices(newNotices); if (!deletedNoticesArticleIds.isEmpty()) { - departmentNoticeRepository.deleteAllByIdsAndDepartment(departmentNameEnum, deletedNoticesArticleIds); + noticeRepository.deleteAllByIdsAndDepartment(departmentNameEnum, deletedNoticesArticleIds); } } } diff --git a/src/main/resources/db/migration/V240122__Create_bookmarks.sql b/src/main/resources/db/migration/V240122__Create_bookmarks.sql new file mode 100644 index 00000000..6a99994a --- /dev/null +++ b/src/main/resources/db/migration/V240122__Create_bookmarks.sql @@ -0,0 +1,6 @@ +create table user_bookmarks +( + id bigint not null, + notice_id varchar(255) not null, + constraint FK_userTBL_bookmarksTBL foreign key (id) references user (id) +); diff --git a/src/test/java/com/kustacks/kuring/acceptance/AdminAcceptanceTest.java b/src/test/java/com/kustacks/kuring/acceptance/AdminAcceptanceTest.java index c0b9bd9f..18dc0d97 100644 --- a/src/test/java/com/kustacks/kuring/acceptance/AdminAcceptanceTest.java +++ b/src/test/java/com/kustacks/kuring/acceptance/AdminAcceptanceTest.java @@ -1,13 +1,12 @@ package com.kustacks.kuring.acceptance; import com.kustacks.kuring.admin.common.dto.RealNotificationRequest; -import com.kustacks.kuring.message.firebase.FirebaseService; +import com.kustacks.kuring.support.IntegrationTestSupport; import io.restassured.RestAssured; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -20,10 +19,7 @@ import static org.mockito.Mockito.doNothing; @DisplayName("인수 : 관리자") -class AdminAcceptanceTest extends AcceptanceTest { - - @MockBean - FirebaseService firebaseService; +class AdminAcceptanceTest extends IntegrationTestSupport { /** * given : 사전에 등록된 어드민가 피드백들이 이다 diff --git a/src/test/java/com/kustacks/kuring/acceptance/AuthAcceptanceTest.java b/src/test/java/com/kustacks/kuring/acceptance/AuthAcceptanceTest.java index f98dfd67..6b01f1d2 100644 --- a/src/test/java/com/kustacks/kuring/acceptance/AuthAcceptanceTest.java +++ b/src/test/java/com/kustacks/kuring/acceptance/AuthAcceptanceTest.java @@ -1,5 +1,6 @@ package com.kustacks.kuring.acceptance; +import com.kustacks.kuring.support.IntegrationTestSupport; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; @@ -9,7 +10,7 @@ import static org.assertj.core.api.Assertions.assertThat; @DisplayName("인수 : 인증") -class AuthAcceptanceTest extends AcceptanceTest { +class AuthAcceptanceTest extends IntegrationTestSupport { @DisplayName("[v2] Bearer Auth login") @Test diff --git a/src/test/java/com/kustacks/kuring/acceptance/CategoryAcceptanceTest.java b/src/test/java/com/kustacks/kuring/acceptance/CategoryAcceptanceTest.java index c0581daa..29d6078d 100644 --- a/src/test/java/com/kustacks/kuring/acceptance/CategoryAcceptanceTest.java +++ b/src/test/java/com/kustacks/kuring/acceptance/CategoryAcceptanceTest.java @@ -1,12 +1,11 @@ package com.kustacks.kuring.acceptance; import com.google.firebase.messaging.FirebaseMessagingException; -import com.kustacks.kuring.message.firebase.FirebaseService; import com.kustacks.kuring.message.firebase.exception.FirebaseInvalidTokenException; +import com.kustacks.kuring.support.IntegrationTestSupport; import com.kustacks.kuring.user.common.dto.SubscribeCategoriesRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpStatus; import java.util.List; @@ -18,10 +17,7 @@ import static org.mockito.Mockito.doThrow; @DisplayName("인수 : 카테고리") -public class CategoryAcceptanceTest extends AcceptanceTest { - - @MockBean - FirebaseService firebaseService; +class CategoryAcceptanceTest extends IntegrationTestSupport { /** * Given : 쿠링앱을 실행한다 diff --git a/src/test/java/com/kustacks/kuring/acceptance/CategoryStep.java b/src/test/java/com/kustacks/kuring/acceptance/CategoryStep.java index 4847112f..54d0be8f 100644 --- a/src/test/java/com/kustacks/kuring/acceptance/CategoryStep.java +++ b/src/test/java/com/kustacks/kuring/acceptance/CategoryStep.java @@ -7,7 +7,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import static com.kustacks.kuring.acceptance.AcceptanceTest.USER_FCM_TOKEN; +import static com.kustacks.kuring.support.IntegrationTestSupport.USER_FCM_TOKEN; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; diff --git a/src/test/java/com/kustacks/kuring/acceptance/FeedbackAcceptanceTest.java b/src/test/java/com/kustacks/kuring/acceptance/FeedbackAcceptanceTest.java index 22f4ef9f..38035230 100644 --- a/src/test/java/com/kustacks/kuring/acceptance/FeedbackAcceptanceTest.java +++ b/src/test/java/com/kustacks/kuring/acceptance/FeedbackAcceptanceTest.java @@ -1,10 +1,9 @@ package com.kustacks.kuring.acceptance; import com.google.firebase.messaging.FirebaseMessagingException; -import com.kustacks.kuring.message.firebase.FirebaseService; +import com.kustacks.kuring.support.IntegrationTestSupport; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpStatus; import static com.kustacks.kuring.acceptance.CommonStep.실패_응답_확인; @@ -14,10 +13,7 @@ import static org.mockito.Mockito.doNothing; @DisplayName("인수: 피드백") -public class FeedbackAcceptanceTest extends AcceptanceTest { - - @MockBean - FirebaseService firebaseService; +class FeedbackAcceptanceTest extends IntegrationTestSupport { /** * Given : 사용자가 피드백 사항을 적는다 diff --git a/src/test/java/com/kustacks/kuring/acceptance/NoticeAcceptanceTest.java b/src/test/java/com/kustacks/kuring/acceptance/NoticeAcceptanceTest.java index 491f55bf..1279cb91 100644 --- a/src/test/java/com/kustacks/kuring/acceptance/NoticeAcceptanceTest.java +++ b/src/test/java/com/kustacks/kuring/acceptance/NoticeAcceptanceTest.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.acceptance; import com.kustacks.kuring.notice.domain.DepartmentName; +import com.kustacks.kuring.support.IntegrationTestSupport; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -9,7 +10,7 @@ import static com.kustacks.kuring.acceptance.NoticeStep.*; @DisplayName("인수 : 공지사항") -class NoticeAcceptanceTest extends AcceptanceTest { +class NoticeAcceptanceTest extends IntegrationTestSupport { /** * Given : 쿠링앱이 실행중이다 diff --git a/src/test/java/com/kustacks/kuring/acceptance/StaffAcceptanceTest.java b/src/test/java/com/kustacks/kuring/acceptance/StaffAcceptanceTest.java index 9de327eb..72c7ceef 100644 --- a/src/test/java/com/kustacks/kuring/acceptance/StaffAcceptanceTest.java +++ b/src/test/java/com/kustacks/kuring/acceptance/StaffAcceptanceTest.java @@ -1,5 +1,6 @@ package com.kustacks.kuring.acceptance; +import com.kustacks.kuring.support.IntegrationTestSupport; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -7,7 +8,7 @@ import static com.kustacks.kuring.acceptance.StaffStep.교직원_조회_응답_확인; @DisplayName("인수 : 교직원") -public class StaffAcceptanceTest extends AcceptanceTest { +class StaffAcceptanceTest extends IntegrationTestSupport { /** * Give : 사전에 저장된 교직원이 있다 @@ -15,7 +16,7 @@ public class StaffAcceptanceTest extends AcceptanceTest { * Then : 해당하는 교직원들이 조회된다 */ @Test - public void search_staff_by_keyword() { + void search_staff_by_keyword() { // when var 교직원_조회_응답 = 교직원_조회_요청("shine student"); diff --git a/src/test/java/com/kustacks/kuring/acceptance/UserAcceptanceTest.java b/src/test/java/com/kustacks/kuring/acceptance/UserAcceptanceTest.java index c4349043..17d66718 100644 --- a/src/test/java/com/kustacks/kuring/acceptance/UserAcceptanceTest.java +++ b/src/test/java/com/kustacks/kuring/acceptance/UserAcceptanceTest.java @@ -1,11 +1,10 @@ package com.kustacks.kuring.acceptance; import com.kustacks.kuring.auth.exception.RegisterException; -import com.kustacks.kuring.message.firebase.FirebaseService; +import com.kustacks.kuring.support.IntegrationTestSupport; import com.kustacks.kuring.user.common.dto.SubscribeCategoriesRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpStatus; import java.util.Collections; @@ -19,10 +18,7 @@ import static org.mockito.Mockito.doThrow; @DisplayName("인수 : 사용자") -class UserAcceptanceTest extends AcceptanceTest { - - @MockBean - FirebaseService firebaseService; +class UserAcceptanceTest extends IntegrationTestSupport { /** * Given: 가입되지 않은 사용자가 있다 @@ -165,4 +161,38 @@ void request_invalid_length_feedback() { // then 실패_응답_확인(피드백_요청_응답, HttpStatus.BAD_REQUEST); } + + @DisplayName("[v2] 사용자는 원하는 공지의 북마크를 추가할 수 있다") + @Test + void request_bookmark() { + // given + doNothing().when(firebaseService).validationToken(anyString()); + + // when + var 북마크_응답 = 북마크_생성_요청(USER_FCM_TOKEN, "article_1"); + + // then + 북마크_응답_확인(북마크_응답, HttpStatus.OK); + } + + /** + * Given : 사용자가 사전에 저장해둔 북마크가 있다 + * When : 북마크 목록을 요청한다 + * Then : 성공적으로 북마크 목록을 반환한다 + */ + @DisplayName("[v2] 사용자는 자신이 북마크한 공지를 조회할 수 있다") + @Test + void lookup_bookmark() { + // given + doNothing().when(firebaseService).validationToken(anyString()); + 북마크_생성_요청(USER_FCM_TOKEN, "article_1"); + 북마크_생성_요청(USER_FCM_TOKEN, "article_2"); + 북마크_생성_요청(USER_FCM_TOKEN, "depart_normal_article_1"); + + // when + var 북마크_조회_응답 = 북마크한_공지_조회_요청(USER_FCM_TOKEN); + + // then + 북마크_조회_응답_확인(북마크_조회_응답); + } } diff --git a/src/test/java/com/kustacks/kuring/acceptance/UserStep.java b/src/test/java/com/kustacks/kuring/acceptance/UserStep.java index eeb4326b..8ec5721c 100644 --- a/src/test/java/com/kustacks/kuring/acceptance/UserStep.java +++ b/src/test/java/com/kustacks/kuring/acceptance/UserStep.java @@ -1,6 +1,7 @@ package com.kustacks.kuring.acceptance; import com.kustacks.kuring.auth.dto.UserRegisterRequest; +import com.kustacks.kuring.user.common.dto.SaveBookmarkRequest; import com.kustacks.kuring.user.common.dto.SubscribeCategoriesRequest; import com.kustacks.kuring.user.common.dto.SubscribeDepartmentsRequest; import com.kustacks.kuring.user.common.dto.SaveFeedbackRequest; @@ -123,4 +124,47 @@ public class UserStep { () -> assertThat(response.jsonPath().getString("message")).isEqualTo("피드백 저장에 성공하였습니다") ); } + + public static void 북마크_응답_확인(ExtractableResponse response, HttpStatus status) { + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(status.value()), + () -> assertThat(response.jsonPath().getString("message")).isEqualTo("북마크 저장에 성공하였습니다"), + () -> assertThat(response.jsonPath().getList("data")).isNull() + ); + } + + public static ExtractableResponse 북마크_생성_요청(String token, String articleId) { + return RestAssured + .given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .header("User-Token", token) + .body(new SaveBookmarkRequest(articleId)) + .when().post("/api/v2/users/bookmarks") + .then().log().all() + .extract(); + } + + + public static void 북마크_조회_응답_확인(ExtractableResponse 북마크_조회_응답) { + assertAll( + () -> assertThat(북마크_조회_응답.statusCode()).isEqualTo(HttpStatus.OK.value()), + () -> assertThat(북마크_조회_응답.jsonPath().getInt("code")).isEqualTo(200), + () -> assertThat(북마크_조회_응답.jsonPath().getString("message")).isEqualTo("북마크 조회에 성공하였습니다"), + () -> assertThat(북마크_조회_응답.jsonPath().getList("data")).hasSize(3), + () -> assertThat(북마크_조회_응답.jsonPath().getString("data[].articleId")).isNotBlank(), + () -> assertThat(북마크_조회_응답.jsonPath().getString("data[].postedDate")).isNotBlank(), + () -> assertThat(북마크_조회_응답.jsonPath().getString("data[].subject")).isNotBlank(), + () -> assertThat(북마크_조회_응답.jsonPath().getString("data[].url")).isNotBlank(), + () -> assertThat(북마크_조회_응답.jsonPath().getString("data[].subject")).isNotBlank() + ); + } + + public static ExtractableResponse 북마크한_공지_조회_요청(String userToken) { + return RestAssured + .given().log().all() + .header("User-Token", userToken) + .when().get("/api/v2/users/bookmarks") + .then().log().all() + .extract(); + } } diff --git a/src/test/java/com/kustacks/kuring/notice/repository/NoticeRepositoryTest.java b/src/test/java/com/kustacks/kuring/notice/repository/NoticeRepositoryTest.java new file mode 100644 index 00000000..cd3cc008 --- /dev/null +++ b/src/test/java/com/kustacks/kuring/notice/repository/NoticeRepositoryTest.java @@ -0,0 +1,66 @@ +package com.kustacks.kuring.notice.repository; + +import com.kustacks.kuring.notice.domain.*; +import com.kustacks.kuring.support.IntegrationTestSupport; +import com.kustacks.kuring.user.common.dto.BookmarkDto; +import com.kustacks.kuring.user.domain.User; +import com.kustacks.kuring.user.domain.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; + +class NoticeRepositoryTest extends IntegrationTestSupport { + + @Autowired + private NoticeRepository noticeRepository; + + @Autowired + private NoticeJdbcRepository noticeJdbcRepository; + + @Autowired + private UserRepository userRepository; + + @DisplayName("사용자가 북마크해둔 공지의 ID로 해당 공지들을 찾아올 수 있다") + @Test + void lookupAllNoticeByIds() { + // given + Notice notice1 = new Notice("1", "2024-01-19", "updatedDate", + "notice1", CategoryName.BACHELOR, false, "https://www.example.com"); + Notice notice2 = new Notice("2", "2024-01-20", "updatedDate", + "notice2", CategoryName.BACHELOR, false, "https://www.example.com"); + noticeJdbcRepository.saveAllCategoryNotices(List.of(notice1, notice2)); + + DepartmentNotice departmentNotice1 = new DepartmentNotice("3", "2024-01-22", "updatedDate", + "departmentNotice1", CategoryName.DEPARTMENT, false, "https://www.example.com", DepartmentName.ADMINISTRATION); + DepartmentNotice departmentNotice2 = new DepartmentNotice("4", "2024-01-24", "updatedDate", + "departmentNotice2", CategoryName.DEPARTMENT, false, "https://www.example.com", DepartmentName.ADMINISTRATION); + noticeJdbcRepository.saveAllDepartmentNotices(List.of(departmentNotice1, departmentNotice2)); + + User user = new User("user_token"); + user.addBookmark(notice1.getArticleId()); + user.addBookmark(notice2.getArticleId()); + user.addBookmark(departmentNotice1.getArticleId()); + user.addBookmark(departmentNotice2.getArticleId()); + + User savedUser = userRepository.save(user); + List ids = savedUser.lookupAllBookmarkIds(); + + // when + List bookmarks = noticeRepository.findAllByBookmarkIds(ids); + + // then + assertThat(bookmarks).hasSize(4) + .extracting("articleId", "postedDate", "subject") + .containsExactly( + tuple("4", "2024-01-24", "departmentNotice2"), + tuple("3", "2024-01-22", "departmentNotice1"), + tuple("2", "2024-01-20", "notice2"), + tuple("1", "2024-01-19", "notice1") + ); + } +} diff --git a/src/test/java/com/kustacks/kuring/tool/DatabaseConfigurator.java b/src/test/java/com/kustacks/kuring/support/DatabaseConfigurator.java similarity index 84% rename from src/test/java/com/kustacks/kuring/tool/DatabaseConfigurator.java rename to src/test/java/com/kustacks/kuring/support/DatabaseConfigurator.java index d9d30840..fff021d5 100644 --- a/src/test/java/com/kustacks/kuring/tool/DatabaseConfigurator.java +++ b/src/test/java/com/kustacks/kuring/support/DatabaseConfigurator.java @@ -1,4 +1,4 @@ -package com.kustacks.kuring.tool; +package com.kustacks.kuring.support; import com.kustacks.kuring.admin.domain.Admin; import com.kustacks.kuring.admin.domain.AdminRepository; @@ -22,7 +22,6 @@ import java.sql.ResultSet; import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; import java.util.stream.Stream; @Component @@ -39,7 +38,6 @@ public class DatabaseConfigurator implements InitializingBean { private final UserRepository userRepository; private final StaffRepository staffRepository; private final AdminRepository adminRepository; - private final DepartmentNoticeRepository departmentNoticeRepository; private final DataSource dataSource; private final JdbcTemplate jdbcTemplate; private final PasswordEncoder passwordEncoder; @@ -48,13 +46,11 @@ public class DatabaseConfigurator implements InitializingBean { public DatabaseConfigurator(NoticeRepository noticeRepository, UserRepository userRepository, StaffRepository staffRepository, AdminRepository adminRepository, - DepartmentNoticeRepository departmentNoticeRepository, DataSource dataSource, JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) { this.noticeRepository = noticeRepository; this.userRepository = userRepository; this.staffRepository = staffRepository; this.adminRepository = adminRepository; - this.departmentNoticeRepository = departmentNoticeRepository; this.dataSource = dataSource; this.jdbcTemplate = jdbcTemplate; this.passwordEncoder = passwordEncoder; @@ -153,11 +149,11 @@ private void initNotice() { List noticeList = buildNotices(5, CategoryName.STUDENT); noticeRepository.saveAll(noticeList); - List importantDeptNotices = buildDepartmentNotice(7, DepartmentName.COMPUTER, CategoryName.DEPARTMENT, true); - departmentNoticeRepository.saveAll(importantDeptNotices); + List importantDeptNotices = buildImportantDepartmentNotice(7, DepartmentName.COMPUTER, CategoryName.DEPARTMENT, true); + noticeRepository.saveAll(importantDeptNotices); - List normalDeptNotices = buildDepartmentNotice(5, DepartmentName.COMPUTER, CategoryName.DEPARTMENT, false); - departmentNoticeRepository.saveAll(normalDeptNotices); + List normalDeptNotices = buildNormalDepartmentNotice(5, DepartmentName.COMPUTER, CategoryName.DEPARTMENT, false); + noticeRepository.saveAll(normalDeptNotices); } private void initStaff() { @@ -165,10 +161,17 @@ private void initStaff() { staffRepository.saveAll(staffList); } - private List buildDepartmentNotice(int cnt, DepartmentName departmentName, CategoryName categoryName, boolean important) { + private List buildImportantDepartmentNotice(int cnt, DepartmentName departmentName, CategoryName categoryName, boolean important) { return Stream.iterate(0, i -> i + 1) .limit(cnt) - .map(i -> new DepartmentNotice("article_" + i, "post_date_" + i, "update_date_" + i, "subject_" + i, categoryName, important, "https://www.example.com", departmentName)) + .map(i -> new DepartmentNotice("depart_import_article_" + i, "post_date_" + i, "update_date_" + i, "subject_" + i, categoryName, important, "https://www.example.com", departmentName)) + .toList(); + } + + private List buildNormalDepartmentNotice(int cnt, DepartmentName departmentName, CategoryName categoryName, boolean important) { + return Stream.iterate(0, i -> i + 1) + .limit(cnt) + .map(i -> new DepartmentNotice("depart_normal_article_" + i, "post_date_" + i, "update_date_" + i, "subject_" + i, categoryName, important, "https://www.example.com", departmentName)) .toList(); } diff --git a/src/test/java/com/kustacks/kuring/acceptance/AcceptanceTest.java b/src/test/java/com/kustacks/kuring/support/IntegrationTestSupport.java similarity index 54% rename from src/test/java/com/kustacks/kuring/acceptance/AcceptanceTest.java rename to src/test/java/com/kustacks/kuring/support/IntegrationTestSupport.java index 8d290b23..d81cdbc5 100644 --- a/src/test/java/com/kustacks/kuring/acceptance/AcceptanceTest.java +++ b/src/test/java/com/kustacks/kuring/support/IntegrationTestSupport.java @@ -1,23 +1,26 @@ -package com.kustacks.kuring.acceptance; +package com.kustacks.kuring.support; -import com.kustacks.kuring.tool.DatabaseConfigurator; +import com.kustacks.kuring.message.firebase.FirebaseService; import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.web.server.LocalServerPort; import org.springframework.test.context.TestPropertySource; @TestPropertySource(locations = "classpath:test-constants.properties") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class AcceptanceTest { - protected static final String ADMIN_LOGIN_ID = "admin@email.com"; - protected static final String ADMIN_PASSWORD = "admin_password"; - protected static final String USER_FCM_TOKEN = "test_fcm_token"; - protected static final String INVALID_USER_FCM_TOKEN = "invalid_fcm_token"; - protected static final String ADMIN_CLIENT_LOGIN_ID = "client@email.com"; - protected static final String ADMIN_CLIENT_PASSWORD = "client_password"; +public class IntegrationTestSupport { + public static final String ADMIN_LOGIN_ID = "admin@email.com"; + public static final String ADMIN_PASSWORD = "admin_password"; + public static final String USER_FCM_TOKEN = "test_fcm_token"; + public static final String ADMIN_CLIENT_LOGIN_ID = "client@email.com"; + public static final String ADMIN_CLIENT_PASSWORD = "client_password"; + + @MockBean + protected FirebaseService firebaseService; @LocalServerPort int port; diff --git a/src/test/java/com/kustacks/kuring/user/domain/BookmarksTest.java b/src/test/java/com/kustacks/kuring/user/domain/BookmarksTest.java new file mode 100644 index 00000000..f1aa3cdb --- /dev/null +++ b/src/test/java/com/kustacks/kuring/user/domain/BookmarksTest.java @@ -0,0 +1,58 @@ +package com.kustacks.kuring.user.domain; + +import org.assertj.core.api.ThrowableAssert; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +class BookmarksTest { + + @DisplayName("사용자는 원하는 공지를 북마크 할 수 있다") + @Test + void add() { + // given + User user = new User("token"); + String noticeId = "1234"; + + // when, then + assertThatCode(() -> user.addBookmark(noticeId)) + .doesNotThrowAnyException(); + } + + @DisplayName("사용자는 원하는 공지를 북마크 할 수 있다") + @Test + void lookup_all_bookmark_ids() { + // given + User user = new User("token"); + user.addBookmark("1"); + user.addBookmark("2"); + user.addBookmark("3"); + user.addBookmark("4"); + + // when + List ids = user.lookupAllBookmarkIds(); + + // then + assertThat(ids).hasSize(4) + .containsOnly("1", "2", "3", "4"); + } + + @DisplayName("사용자는 공지를 10개까지 북마크 할 수 있다") + @Test + void user_bookmark_limit() { + // given + User user = new User("token"); + for(int i = 1; i <= 10; i++) user.addBookmark(String.valueOf(i)); + + // when + ThrowableAssert.ThrowingCallable actual = () -> user.addBookmark(String.valueOf(11)); + + // then + assertThatThrownBy(actual) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("북마크가 저장 가능한 사이즈를 초과하였습니다."); + } +} diff --git a/src/test/java/com/kustacks/kuring/user/repository/UserRepositoryTest.java b/src/test/java/com/kustacks/kuring/user/repository/UserRepositoryTest.java new file mode 100644 index 00000000..dbf15885 --- /dev/null +++ b/src/test/java/com/kustacks/kuring/user/repository/UserRepositoryTest.java @@ -0,0 +1,47 @@ +package com.kustacks.kuring.user.repository; + +import com.kustacks.kuring.support.IntegrationTestSupport; +import com.kustacks.kuring.admin.common.dto.FeedbackDto; +import com.kustacks.kuring.user.domain.User; +import com.kustacks.kuring.user.domain.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; + + +class UserRepositoryTest extends IntegrationTestSupport { + + @Autowired + private UserRepository userRepository; + + @DisplayName("사용자가 작성한 피드백을 페이징 처리하여 가져올 수 있다") + @Test + void findAllFeedbackByPageRequest() { + // given + User user = new User("user_token"); + user.addFeedback("content1"); + user.addFeedback("content2"); + user.addFeedback("content3"); + + User savedUser = userRepository.save(user); + Long userId = savedUser.getId(); + + // when + List feedbackDtos = userRepository.findAllFeedbackByPageRequest(PageRequest.of(0, 3)); + + // then + assertThat(feedbackDtos).hasSize(3) + .extracting("contents", "userId") + .containsExactlyInAnyOrder( + tuple("content1", userId), + tuple("content2", userId), + tuple("content3", userId) + ); + } +} diff --git a/src/test/java/com/kustacks/kuring/worker/update/DepartmentNoticeUpdaterTest.java b/src/test/java/com/kustacks/kuring/worker/update/DepartmentNoticeUpdaterTest.java index 6bb719c7..9545d1c2 100644 --- a/src/test/java/com/kustacks/kuring/worker/update/DepartmentNoticeUpdaterTest.java +++ b/src/test/java/com/kustacks/kuring/worker/update/DepartmentNoticeUpdaterTest.java @@ -2,7 +2,8 @@ import com.kustacks.kuring.message.firebase.FirebaseService; import com.kustacks.kuring.notice.domain.DepartmentNotice; -import com.kustacks.kuring.notice.domain.DepartmentNoticeRepository; +import com.kustacks.kuring.notice.domain.Notice; +import com.kustacks.kuring.notice.domain.NoticeRepository; import com.kustacks.kuring.worker.scrap.DepartmentNoticeScraperTemplate; import com.kustacks.kuring.worker.scrap.dto.ComplexNoticeFormatDto; import com.kustacks.kuring.worker.update.notice.DepartmentNoticeUpdater; @@ -20,6 +21,7 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.doNothing; @@ -46,7 +48,7 @@ class DepartmentNoticeUpdaterTest { ThreadPoolTaskExecutor noticeUpdaterThreadTaskExecutor; @Autowired - DepartmentNoticeRepository departmentNoticeRepository; + NoticeRepository noticeRepository; @DisplayName("학과별 공지 업데이트 테스트") @Test @@ -60,8 +62,11 @@ void department_scrap_async_test() throws InterruptedException { noticeUpdaterThreadTaskExecutor.getThreadPoolExecutor().awaitTermination(2, TimeUnit.SECONDS); // then - List notices = departmentNoticeRepository.findAll(); - assertThat(notices).hasSize(3720); + List notices = noticeRepository.findAll(); + assertAll( + () -> assertThat(notices).hasSize(3720), + () -> assertThat(notices.get(0)).isExactlyInstanceOf(DepartmentNotice.class) + ); } private static List createDepartmentNoticesFixture() {