diff --git a/.github/workflows/pr_weekly_ci.yml b/.github/workflows/pr_weekly_ci.yml index f54701f..372f0ca 100644 --- a/.github/workflows/pr_weekly_ci.yml +++ b/.github/workflows/pr_weekly_ci.yml @@ -53,7 +53,7 @@ jobs: - name: 빌드 테스트 수행 run: | chmod +x ./gradlew - ./gradlew clean build --build-cache --stacktrace + ./gradlew clean build jacocoTestReport --build-cache --stacktrace - name: 테스트 수행 결과 보고 uses: EnricoMi/publish-unit-test-result-action@v2 @@ -67,3 +67,19 @@ jobs: with: report_paths: '**/build/test-results/test/TEST-*.xml' token: ${{ github.token }} + + - name: JaCoCo 테스트 커버리지 리포트 업로드 + uses: actions/upload-artifact@v3 + if: always() + with: + name: jacoco-report + path: '**/build/reports/jacoco/' + + - name: JaCoCo 테스트 커버리지 결과를 PR에 코멘트로 등록 + uses: madrapps/jacoco-report@v1.6.1 + with: + paths: ${{ github.workspace }}/build/reports/jacoco/test/jacocoTestReport.xml + token: ${{ github.token }} + min-coverage-overall: 70 + min-coverage-changed-files: 70 + title: '📊 테스트 커버리지 리포트' diff --git a/README.md b/README.md index 19ed58e..9ef5a47 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,147 @@ -# Team21_BE -21조 백엔드 +# Every Moment +> 하루를 자동으로 기록하고 공유하는 위치 기반 소셜 다이어리 📝 + +![image](https://github.com/user-attachments/assets/d2be179d-e5e4-4b14-914f-8d88f5e6f7b5) + +
+ +## 📌 프로젝트 소개 +바쁜 일상 속에서 매 순간을 기록하기란 쉽지 않습니다. +Every Moment는 위치 데이터를 기반으로 사용자의 하루를 **자동**으로 기록하고 친구들과 공유할 수 있는 소셜 다이어리 서비스입니다. + +### 핵심 기능 +- 📍 **위치 기반 자동 기록**: 15분 이상 머문 장소 자동 감지 및 일기로 기록 + +- 🤝 **소셜 다이어리**: 친구들과 일기 공유 및 소통 +- 🔍 **스마트 검색**: 다양한 필터링 옵션으로 원하는 일기 빠른 검색 +- 🔔 **실시간 알림**: 친구와의 상호작용(좋아요, 댓글, 친구 요청 등)과 새로운 장소 감지를 실시간으로 알림 + +
+ +## ⭐️ 주요 기능 +| 자동 일기 기록 | 손쉬운 일기 편집 | 다양한 검색 필터링 | 친구와의 일기 공유 | +|:---:|:---:|:---:|:---:| +| | | | | +| 15분동안 머문 장소를
자동으로 기록 | 저장된 일기를
손쉽게 편집 | 다양한 검색 조건으로
손쉬운 일기 찾기 | 원하는 일기를 공유하여
친구와 소통 | + +
+ +## 🔍 프로젝트 정보 +### 개발 기간 +- 2024.09 ~ 2024.11 (3개월) + +### 서비스 링크 +- **API 서버**: http://13.125.156.74:8080 + +- **API 문서**: [Swagger UI](http://13.125.156.74:8080/swagger-ui/index.html) | [노션 문서](https://peeerr.notion.site/API-2e575ca8df07493dbc25f3d0e91ca211?pvs=4) + +### 프로젝트 관리 +- **[Backend Repository](https://github.com/kakao-tech-campus-2nd-step3/Team21_BE)** +- **[Android Repository](https://github.com/kakao-tech-campus-2nd-step3/Team21_Android)** + +- **[Git Flow 전략](https://github.com/kakao-tech-campus-2nd-step3/Team21_BE/wiki/Git-Flow-%EC%A0%84%EB%9E%B5)** +- **[코딩 컨벤션](https://github.com/kakao-tech-campus-2nd-step3/Team21_BE/wiki/%EC%BD%94%EB%94%A9-%EC%BB%A8%EB%B2%A4%EC%85%98)** +- **[커밋 컨벤션](https://github.com/kakao-tech-campus-2nd-step3/Team21_BE/wiki/%EC%BB%A4%EB%B0%8B-%EC%BB%A8%EB%B2%A4%EC%85%98)** + +
+ +## 👥 팀원 소개 +저희 팀은 **백엔드 2명, 안드로이드 3명**으로 구성되어 있습니다. + + + + + + + + + + + + + + + + +
최준형전혜지이아림윤채원권새일
+ + + +
+ Backend +
+ + + +
+ Backend +
+ + + +
+ Android +
+ + + +
+ Android +
+ + + +
+ Android +
+ +
+ +## 🛠 기술 스택 +### 💻 Language & Framework +- Java 21 +- Spring Boot 3.3 +- Spring Data JPA + +### 📊 Database & Storage +- MySQL +- AWS S3 (파일 저장소) + +### 📱 Communication +- Firebase Cloud Messaging (FCM) + +### 🏗 Infra +- AWS EC2 +- AWS RDS + +### 🔧 Development Tools +- GitHub Actions + +### 📚 Documentation & Testing +- Swagger +- JUnit5 + +### ⚙️ Others +- JWT (인증/인가) + +
+ +## 🗄️ ERD +![erd](https://github.com/user-attachments/assets/72e66248-f217-434a-9f20-d8150abafee4) + +
+ +## 🔍 개발 주안점 +- **실시간성 확보** + - FCM을 활용한 즉각적인 푸시 알림 구현 + + - 댓글, 좋아요, 친구 요청, 자동 일기 작성 등 다양한 이벤트에 대한 실시간 알림 + +- **CI/CD 파이프라인 구축 및 최적화** + - Github Actions를 활용한 배포 자동화 + + - PR 단계에서 자동화된 테스트 실행으로 개발 생산성 향상 + - 이전에는 배포 환경에서 테스트 실패 시 새로운 PR을 생성하고, 배포하는 과정을 반복해야 했음 + +- **테스트 작성** + - 70% 이상의 테스트 커버리지 유지 diff --git a/build.gradle b/build.gradle index 98243d9..ff625e8 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,6 @@ plugins { id 'java' + id 'jacoco' id 'org.springframework.boot' version '3.3.3' id 'io.spring.dependency-management' version '1.1.6' } @@ -51,3 +52,41 @@ dependencies { tasks.named('test') { useJUnitPlatform() } + +jacocoTestReport { + dependsOn test // 테스트 실행 후 리포트 생성을 보장 + + reports { + xml.required = true + html.required = true + } + + afterEvaluate { + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, exclude: [ + '**/constant/**', + '**/config/**', + '**/dto/**', + '**/security/**', + '**/exception/**', + '**/*Application.class' + ]) + })) + } +} + +test { + useJUnitPlatform() + finalizedBy jacocoTestReport +} + +// 테스트 커버리지 최소 기준 설정 +jacocoTestCoverageVerification { + violationRules { + rule { + limit { + minimum = 0.70 + } + } + } +} diff --git a/src/main/java/com/potatocake/everymoment/controller/DiaryController.java b/src/main/java/com/potatocake/everymoment/controller/DiaryController.java index 41579ac..afae616 100644 --- a/src/main/java/com/potatocake/everymoment/controller/DiaryController.java +++ b/src/main/java/com/potatocake/everymoment/controller/DiaryController.java @@ -100,6 +100,8 @@ public ResponseEntity> getMyDiaries( @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate until, @Parameter(description = "북마크 여부") @RequestParam(required = false) Boolean bookmark, + @Parameter(description = "공유 여부") + @RequestParam(required = false) Boolean isPublic, @Parameter(description = "페이지 키") @RequestParam(defaultValue = "0") int key, @Parameter(description = "페이지 크기") @@ -115,6 +117,7 @@ public ResponseEntity> getMyDiaries( .from(from) .until(until) .isBookmark(bookmark) + .isPublic(isPublic) .key(key) .size(size) .build(); diff --git a/src/main/java/com/potatocake/everymoment/dto/request/DiaryFilterRequest.java b/src/main/java/com/potatocake/everymoment/dto/request/DiaryFilterRequest.java index 29fbb5d..429f0c7 100644 --- a/src/main/java/com/potatocake/everymoment/dto/request/DiaryFilterRequest.java +++ b/src/main/java/com/potatocake/everymoment/dto/request/DiaryFilterRequest.java @@ -18,6 +18,7 @@ public class DiaryFilterRequest { private LocalDate from; private LocalDate until; private Boolean isBookmark; + private Boolean isPublic; private int key; private int size; diff --git a/src/main/java/com/potatocake/everymoment/dto/request/DiaryManualCreateRequest.java b/src/main/java/com/potatocake/everymoment/dto/request/DiaryManualCreateRequest.java index 7659c2b..5ed8174 100644 --- a/src/main/java/com/potatocake/everymoment/dto/request/DiaryManualCreateRequest.java +++ b/src/main/java/com/potatocake/everymoment/dto/request/DiaryManualCreateRequest.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.potatocake.everymoment.dto.LocationPoint; import jakarta.validation.constraints.Size; +import java.time.LocalDate; import java.util.List; import lombok.Builder; import lombok.Getter; @@ -11,6 +12,8 @@ @Getter public class DiaryManualCreateRequest { + private LocalDate diaryDate; + private List categories; private LocationPoint locationPoint; diff --git a/src/main/java/com/potatocake/everymoment/dto/response/MyDiaryResponse.java b/src/main/java/com/potatocake/everymoment/dto/response/MyDiaryResponse.java index c8c8110..346a1f1 100644 --- a/src/main/java/com/potatocake/everymoment/dto/response/MyDiaryResponse.java +++ b/src/main/java/com/potatocake/everymoment/dto/response/MyDiaryResponse.java @@ -1,5 +1,6 @@ package com.potatocake.everymoment.dto.response; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import lombok.Builder; @@ -17,4 +18,5 @@ public class MyDiaryResponse { private String content; private boolean isLiked; private LocalDateTime createAt; + private LocalDate diaryDate; } diff --git a/src/main/java/com/potatocake/everymoment/dto/response/MyDiarySimpleResponse.java b/src/main/java/com/potatocake/everymoment/dto/response/MyDiarySimpleResponse.java index 59e7c70..6e7fa5b 100644 --- a/src/main/java/com/potatocake/everymoment/dto/response/MyDiarySimpleResponse.java +++ b/src/main/java/com/potatocake/everymoment/dto/response/MyDiarySimpleResponse.java @@ -1,5 +1,6 @@ package com.potatocake.everymoment.dto.response; +import java.time.LocalDate; import java.time.LocalDateTime; import lombok.Builder; import lombok.Getter; @@ -16,4 +17,5 @@ public class MyDiarySimpleResponse { private ThumbnailResponse thumbnailResponse; private String content; private LocalDateTime createAt; + private LocalDate diaryDate; } diff --git a/src/main/java/com/potatocake/everymoment/entity/Diary.java b/src/main/java/com/potatocake/everymoment/entity/Diary.java index c139ff3..bbdeedd 100644 --- a/src/main/java/com/potatocake/everymoment/entity/Diary.java +++ b/src/main/java/com/potatocake/everymoment/entity/Diary.java @@ -11,6 +11,7 @@ import jakarta.persistence.Lob; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; +import java.time.LocalDate; import java.util.HashSet; import java.util.Set; import lombok.AccessLevel; @@ -37,6 +38,9 @@ public class Diary extends BaseTimeEntity { @Column(columnDefinition = "TEXT") private String content; + @Column + private LocalDate diaryDate; + @Column(nullable = false) private Point locationPoint; diff --git a/src/main/java/com/potatocake/everymoment/entity/Like.java b/src/main/java/com/potatocake/everymoment/entity/Like.java index ff8ecfb..2db2b54 100644 --- a/src/main/java/com/potatocake/everymoment/entity/Like.java +++ b/src/main/java/com/potatocake/everymoment/entity/Like.java @@ -7,12 +7,18 @@ import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -@Table(name = "likes") +@Table( + name = "likes", + uniqueConstraints = { + @UniqueConstraint(columnNames = {"member_id", "diary_id"}) + } +) @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Entity diff --git a/src/main/java/com/potatocake/everymoment/repository/LikeRepository.java b/src/main/java/com/potatocake/everymoment/repository/LikeRepository.java index 367cd00..fa45665 100644 --- a/src/main/java/com/potatocake/everymoment/repository/LikeRepository.java +++ b/src/main/java/com/potatocake/everymoment/repository/LikeRepository.java @@ -2,12 +2,18 @@ import com.potatocake.everymoment.entity.Diary; import com.potatocake.everymoment.entity.Like; +import jakarta.persistence.LockModeType; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface LikeRepository extends JpaRepository { - Optional findByMemberIdAndDiaryId(Long memberId, Long diaryId); + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT l FROM Like l WHERE l.member.id = :memberId AND l.diary.id = :diaryId") + Optional findByMemberIdAndDiaryIdWithLock(@Param("memberId") Long memberId, @Param("diaryId") Long diaryId); Long countByDiary(Diary diary); diff --git a/src/main/java/com/potatocake/everymoment/repository/MemberRepository.java b/src/main/java/com/potatocake/everymoment/repository/MemberRepository.java index 7f318ed..6e2e1e7 100644 --- a/src/main/java/com/potatocake/everymoment/repository/MemberRepository.java +++ b/src/main/java/com/potatocake/everymoment/repository/MemberRepository.java @@ -1,13 +1,11 @@ package com.potatocake.everymoment.repository; import com.potatocake.everymoment.entity.Member; -import jakarta.persistence.LockModeType; import java.util.Optional; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Window; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; public interface MemberRepository extends JpaRepository { @@ -18,7 +16,6 @@ public interface MemberRepository extends JpaRepository { Window findByNicknameContaining(String nickname, ScrollPosition position, Pageable pageable); - @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT CASE WHEN MIN(m.number) > 0 OR MIN(m.number) IS NULL THEN -1 ELSE MIN(m.number) - 1 END FROM Member m") Long findNextAnonymousNumber(); diff --git a/src/main/java/com/potatocake/everymoment/repository/NotificationRepository.java b/src/main/java/com/potatocake/everymoment/repository/NotificationRepository.java index 19297a5..e752355 100644 --- a/src/main/java/com/potatocake/everymoment/repository/NotificationRepository.java +++ b/src/main/java/com/potatocake/everymoment/repository/NotificationRepository.java @@ -2,8 +2,11 @@ import com.potatocake.everymoment.entity.Notification; import java.util.List; +import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.JpaRepository; public interface NotificationRepository extends JpaRepository { - List findAllByMemberId(Long memberId); + + List findAllByMemberId(Long memberId, Sort sort); + } diff --git a/src/main/java/com/potatocake/everymoment/service/DiaryService.java b/src/main/java/com/potatocake/everymoment/service/DiaryService.java index 3378c30..da0c65d 100644 --- a/src/main/java/com/potatocake/everymoment/service/DiaryService.java +++ b/src/main/java/com/potatocake/everymoment/service/DiaryService.java @@ -95,6 +95,7 @@ public void createDiaryManual(Long memberId, DiaryManualCreateRequest diaryManua Diary diary = Diary.builder() .member(currentMember) .content(diaryManualCreateRequest.getContent()) + .diaryDate(diaryManualCreateRequest.getDiaryDate()) .locationPoint(point) .locationName(diaryManualCreateRequest.getLocationName()) .address(diaryManualCreateRequest.getAddress()) @@ -107,22 +108,8 @@ public void createDiaryManual(Long memberId, DiaryManualCreateRequest diaryManua //카테고리 저장 List categoryRequestList = diaryManualCreateRequest.getCategories(); - for (CategoryRequest categoryRequest : categoryRequestList) { - Long categoryId = categoryRequest.getCategoryId(); - - DiaryCategory diaryCategory = DiaryCategory.builder() - .diary(savedDiary) - .category(categoryRepository.findById(categoryId) - .map(category -> { - // Category가 현재 사용자의 소유인지 확인 - category.checkOwner(currentMember.getId()); - return category; - }) - .orElseThrow(() -> new GlobalException(ErrorCode.CATEGORY_NOT_FOUND))) - .build(); - - diaryCategoryRepository.save(diaryCategory); - + if (categoryRequestList != null) { + addDiaryCategory(savedDiary, currentMember.getId(), categoryRequestList); } } @@ -150,7 +137,8 @@ public MyDiariesResponse getMyDiaries(Long memberId, DiaryFilterRequest diaryFil today, diaryFilterRequest.getFrom(), diaryFilterRequest.getUntil(), - diaryFilterRequest.getIsBookmark()) + diaryFilterRequest.getIsBookmark(), + diaryFilterRequest.getIsPublic()) .and((root, query, builder) -> builder.equal(root.get("member"), currentMember)); diaryPage = diaryRepository.findAll(spec, @@ -163,7 +151,8 @@ public MyDiariesResponse getMyDiaries(Long memberId, DiaryFilterRequest diaryFil diaryFilterRequest.getDate(), diaryFilterRequest.getFrom(), diaryFilterRequest.getUntil(), - diaryFilterRequest.getIsBookmark()) + diaryFilterRequest.getIsBookmark(), + diaryFilterRequest.getIsPublic()) .and((root, query, builder) -> builder.equal(root.get("member"), currentMember)); diaryPage = diaryRepository.findAll(spec, @@ -213,22 +202,7 @@ public void updateDiary(Long memberId, Long diaryId, DiaryPatchRequest diaryPatc if (categoryRequestList != null && !categoryRequestList.isEmpty()) { diaryCategoryRepository.deleteByDiary(existingDiary); - - for (CategoryRequest categoryRequest : categoryRequestList) { - Long categoryId = categoryRequest.getCategoryId(); - - DiaryCategory diaryCategory = DiaryCategory.builder() - .diary(existingDiary) - .category(categoryRepository.findById(categoryId) - .map(category -> { - category.checkOwner(memberId); - return category; - }) - .orElseThrow(() -> new GlobalException(ErrorCode.CATEGORY_NOT_FOUND))) - .build(); - - diaryCategoryRepository.save(diaryCategory); - } + addDiaryCategory(existingDiary, memberId, categoryRequestList); } } @@ -282,6 +256,25 @@ private Diary getExistDiary(Long memberId, Long diaryId) { return diary; } + //다이어리에 카테고리 추가 + private void addDiaryCategory(Diary savedDiary, Long memberId, List categoryRequestList) { + for (CategoryRequest categoryRequest : categoryRequestList) { + Long categoryId = categoryRequest.getCategoryId(); + + DiaryCategory diaryCategory = DiaryCategory.builder() + .diary(savedDiary) + .category(categoryRepository.findById(categoryId) + .map(category -> { + category.checkOwner(memberId); + return category; + }) + .orElseThrow(() -> new GlobalException(ErrorCode.CATEGORY_NOT_FOUND))) + .build(); + + diaryCategoryRepository.save(diaryCategory); + } + } + //상세 조회시 일기DTO 변환 private MyDiaryResponse convertToMyDiaryResponseDto(Diary savedDiary, Long memberId) { // 카테고리 찾음 @@ -305,6 +298,7 @@ private MyDiaryResponse convertToMyDiaryResponseDto(Diary savedDiary, Long membe .content(savedDiary.getContent()) .isLiked(isLiked) .createAt(savedDiary.getCreateAt()) + .diaryDate(savedDiary.getDiaryDate()) .build(); } @@ -329,6 +323,7 @@ private MyDiarySimpleResponse convertToMyDiarySimpleResponseDto(Diary savedDiary .thumbnailResponse(thumbnailResponse) .content(savedDiary.getContent()) .createAt(savedDiary.getCreateAt()) + .diaryDate(savedDiary.getDiaryDate()) .build(); } } diff --git a/src/main/java/com/potatocake/everymoment/service/DiarySpecification.java b/src/main/java/com/potatocake/everymoment/service/DiarySpecification.java index 96e8e9d..01184f3 100644 --- a/src/main/java/com/potatocake/everymoment/service/DiarySpecification.java +++ b/src/main/java/com/potatocake/everymoment/service/DiarySpecification.java @@ -18,7 +18,7 @@ public class DiarySpecification { public static Specification filterDiaries( String keyword, List emojis, List categories, - LocalDate date, LocalDate from, LocalDate until, Boolean isBookmark) { + LocalDate date, LocalDate from, LocalDate until, Boolean isBookmark, Boolean isPublic) { return (Root root, CriteriaQuery query, CriteriaBuilder builder) -> { Predicate predicate = builder.conjunction(); @@ -36,19 +36,42 @@ public static Specification filterDiaries( predicate = builder.and(predicate, categoryJoin.get("categoryName").in(categories)); } - if(date != null){ - predicate = builder.and(predicate, builder.equal(root.get("createAt").as(LocalDate.class), date)); + if (date != null) { + predicate = builder.and(predicate, + builder.or( + builder.and( + builder.isNotNull(root.get("diaryDate")), + builder.equal(root.get("diaryDate"), date) + ), + builder.and( + builder.isNull(root.get("diaryDate")), + builder.equal(root.get("createAt").as(LocalDate.class), date) + ) + )); } if (from != null && until != null) { predicate = builder.and(predicate, - builder.between(root.get("createAt"), from, until.plusDays(1))); + builder.or( + builder.and( + builder.isNotNull(root.get("diaryDate")), + builder.between(root.get("diaryDate"), from, until) + ), + builder.and( + builder.isNull(root.get("diaryDate")), + builder.between(root.get("createAt"), from, until.plusDays(1)) + ) + )); } if (isBookmark != null) { predicate = builder.and(predicate, builder.equal(root.get("isBookmark"), isBookmark)); } + if (isPublic != null) { + predicate = builder.and(predicate, builder.equal(root.get("isPublic"), isPublic)); + } + return predicate; }; } diff --git a/src/main/java/com/potatocake/everymoment/service/LikeService.java b/src/main/java/com/potatocake/everymoment/service/LikeService.java index d2c5586..95584ad 100644 --- a/src/main/java/com/potatocake/everymoment/service/LikeService.java +++ b/src/main/java/com/potatocake/everymoment/service/LikeService.java @@ -48,7 +48,7 @@ public void toggleLike(Long memberId, Long diaryId) { Member member = memberRepository.findById(memberId) .orElseThrow(() -> new GlobalException(ErrorCode.MEMBER_NOT_FOUND)); - Optional existingLike = likeRepository.findByMemberIdAndDiaryId(memberId, diaryId); + Optional existingLike = likeRepository.findByMemberIdAndDiaryIdWithLock(memberId, diaryId); if (existingLike.isPresent()) { // 이미 좋아요가 존재하면 삭제 (좋아요 취소) diff --git a/src/main/java/com/potatocake/everymoment/service/NotificationService.java b/src/main/java/com/potatocake/everymoment/service/NotificationService.java index 4f5fd36..bd352dd 100644 --- a/src/main/java/com/potatocake/everymoment/service/NotificationService.java +++ b/src/main/java/com/potatocake/everymoment/service/NotificationService.java @@ -14,6 +14,7 @@ import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -60,7 +61,8 @@ public List getNotifications(Long memberId) { Member currentMember = memberRepository.findById(memberId) .orElseThrow(() -> new GlobalException(ErrorCode.MEMBER_NOT_FOUND)); - List notifications = notificationRepository.findAllByMemberId(currentMember.getId()); + List notifications = notificationRepository.findAllByMemberId( + currentMember.getId(), Sort.by(Sort.Direction.DESC, "createAt")); return notifications.stream() .map(this::convertToNotificationResponseDTO) diff --git a/src/test/java/com/potatocake/everymoment/repository/DiaryRepositoryTest.java b/src/test/java/com/potatocake/everymoment/repository/DiaryRepositoryTest.java index 74f276c..37ec8df 100644 --- a/src/test/java/com/potatocake/everymoment/repository/DiaryRepositoryTest.java +++ b/src/test/java/com/potatocake/everymoment/repository/DiaryRepositoryTest.java @@ -93,7 +93,7 @@ void should_FindDiaries_When_FilteringWithSearchCriteria() { .locationName("Busan") .address("Busan Address") .emoji("😍") - .isPublic(true) + .isPublic(false) .isBookmark(false) .build(); @@ -107,7 +107,8 @@ void should_FindDiaries_When_FilteringWithSearchCriteria() { null, // date null, // from null, // until - false // isBookmark + false, // isBookmark + true // isPublic ); Page result = diaryRepository.findAll(spec, PageRequest.of(0, 10)); @@ -132,7 +133,7 @@ void should_FindDiaries_When_FilteringBookmarked() { .locationName("Seoul") .address("Seoul Address") .isBookmark(true) - .isPublic(false) + .isPublic(true) .build(); Diary diary2 = createDiary(member, "Content 2", "Busan", "Busan Address"); @@ -156,7 +157,8 @@ void should_FindDiaries_When_FilteringBookmarked() { null, // date null, // from null, // until - true // isBookmark + true, // isBookmark + true // isPublic ); Page result = diaryRepository.findAll(spec, PageRequest.of(0, 10)); diff --git a/src/test/java/com/potatocake/everymoment/repository/LikeRepositoryTest.java b/src/test/java/com/potatocake/everymoment/repository/LikeRepositoryTest.java index 87027c4..d3f3b8b 100644 --- a/src/test/java/com/potatocake/everymoment/repository/LikeRepositoryTest.java +++ b/src/test/java/com/potatocake/everymoment/repository/LikeRepositoryTest.java @@ -96,7 +96,7 @@ void should_FindLike_When_FilteringByMemberAndDiary() { likeRepository.save(like); // when - Optional foundLike = likeRepository.findByMemberIdAndDiaryId(member.getId(), diary.getId()); + Optional foundLike = likeRepository.findByMemberIdAndDiaryIdWithLock(member.getId(), diary.getId()); // then assertThat(foundLike).isPresent(); diff --git a/src/test/java/com/potatocake/everymoment/repository/NotificationRepositoryTest.java b/src/test/java/com/potatocake/everymoment/repository/NotificationRepositoryTest.java index b5924c8..76177f4 100644 --- a/src/test/java/com/potatocake/everymoment/repository/NotificationRepositoryTest.java +++ b/src/test/java/com/potatocake/everymoment/repository/NotificationRepositoryTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.data.domain.Sort; import org.springframework.test.context.TestPropertySource; @TestPropertySource(properties = "spring.jpa.hibernate.ddl-auto=create-drop") @@ -79,7 +80,7 @@ void should_FindNotifications_When_FilteringByMemberId() { // when List foundNotifications = notificationRepository - .findAllByMemberId(savedMember.getId()); + .findAllByMemberId(savedMember.getId(), Sort.by(Sort.Direction.DESC, "createAt")); // then assertThat(foundNotifications).hasSize(2); @@ -113,7 +114,7 @@ void should_DeleteNotification_When_ValidEntity() { // then List remainingNotifications = notificationRepository - .findAllByMemberId(savedMember.getId()); + .findAllByMemberId(savedMember.getId(), Sort.by(Sort.Direction.DESC, "createAt")); assertThat(remainingNotifications).isEmpty(); } diff --git a/src/test/java/com/potatocake/everymoment/service/LikeServiceTest.java b/src/test/java/com/potatocake/everymoment/service/LikeServiceTest.java index f645c71..0bd3eb7 100644 --- a/src/test/java/com/potatocake/everymoment/service/LikeServiceTest.java +++ b/src/test/java/com/potatocake/everymoment/service/LikeServiceTest.java @@ -85,7 +85,7 @@ void should_AddLike_When_NotLiked() { given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); - given(likeRepository.findByMemberIdAndDiaryId(memberId, diaryId)) + given(likeRepository.findByMemberIdAndDiaryIdWithLock(memberId, diaryId)) .willReturn(Optional.empty()); // when @@ -122,7 +122,7 @@ void should_RemoveLike_When_AlreadyLiked() { given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); given(diaryRepository.findById(diaryId)).willReturn(Optional.of(diary)); - given(likeRepository.findByMemberIdAndDiaryId(memberId, diaryId)) + given(likeRepository.findByMemberIdAndDiaryIdWithLock(memberId, diaryId)) .willReturn(Optional.of(like)); // when @@ -138,9 +138,7 @@ void should_ThrowException_When_DiaryNotPublic() { // given Long memberId = 1L; Long diaryId = 1L; - Member member = Member.builder() - .id(memberId) - .build(); + Diary diary = Diary.builder() .id(diaryId) .isPublic(false) diff --git a/src/test/java/com/potatocake/everymoment/service/NotificationServiceTest.java b/src/test/java/com/potatocake/everymoment/service/NotificationServiceTest.java index 4ad4aa0..7d842fa 100644 --- a/src/test/java/com/potatocake/everymoment/service/NotificationServiceTest.java +++ b/src/test/java/com/potatocake/everymoment/service/NotificationServiceTest.java @@ -24,6 +24,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Sort; @ExtendWith(MockitoExtension.class) class NotificationServiceTest { @@ -104,7 +105,7 @@ void should_GetNotifications_When_ValidMemberId() { ); given(memberRepository.findById(memberId)).willReturn(Optional.of(member)); - given(notificationRepository.findAllByMemberId(memberId)).willReturn(notifications); + given(notificationRepository.findAllByMemberId(memberId, Sort.by(Sort.Direction.DESC, "createAt"))).willReturn(notifications); // when List responses = notificationService.getNotifications(memberId);