diff --git a/backend/src/main/java/com/votogether/domain/alarm/entity/Alarm.java b/backend/src/main/java/com/votogether/domain/alarm/entity/Alarm.java index cb833a54e..eb41b1550 100644 --- a/backend/src/main/java/com/votogether/domain/alarm/entity/Alarm.java +++ b/backend/src/main/java/com/votogether/domain/alarm/entity/Alarm.java @@ -15,6 +15,7 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import java.time.LocalDateTime; import java.util.Objects; import lombok.AccessLevel; import lombok.Builder; @@ -74,4 +75,11 @@ public void checkOwner(final Member member) { } } + public LocalDateTime getLatestAlarmCreatedAt(final LocalDateTime reportActionAlarmCreatedAt) { + if (this.getCreatedAt().isAfter(reportActionAlarmCreatedAt)) { + return this.getCreatedAt(); + } + return reportActionAlarmCreatedAt; + } + } diff --git a/backend/src/main/java/com/votogether/domain/alarm/exception/AlarmExceptionType.java b/backend/src/main/java/com/votogether/domain/alarm/exception/AlarmExceptionType.java index e60ab801d..e28b62f49 100644 --- a/backend/src/main/java/com/votogether/domain/alarm/exception/AlarmExceptionType.java +++ b/backend/src/main/java/com/votogether/domain/alarm/exception/AlarmExceptionType.java @@ -6,7 +6,7 @@ @Getter public enum AlarmExceptionType implements ExceptionType { - NOT_FOUND_ACTION(1300, "신고조치알림이 존재하지 않습니다."), + NOT_FOUND_ACTION(1300, "신고 조치 알림이 존재하지 않습니다."), NOT_FOUND(1301, "알림이 존재하지 않습니다."), NOT_OWNER(1302, "알림을 읽을 대상이 아닙니다."), NOT_FOUND_ACTION_TYPE(1303, "등록되지 않은 알림 동작입니다."), diff --git a/backend/src/main/java/com/votogether/domain/alarm/repository/AlarmRepository.java b/backend/src/main/java/com/votogether/domain/alarm/repository/AlarmRepository.java index 3050ce13f..c4ab2738f 100644 --- a/backend/src/main/java/com/votogether/domain/alarm/repository/AlarmRepository.java +++ b/backend/src/main/java/com/votogether/domain/alarm/repository/AlarmRepository.java @@ -3,6 +3,7 @@ import com.votogether.domain.alarm.entity.Alarm; import com.votogether.domain.member.entity.Member; import java.util.List; +import java.util.Optional; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; @@ -13,4 +14,6 @@ public interface AlarmRepository extends JpaRepository { List findAllByMember(final Member member); + Optional findByMemberOrderByIdDesc(final Member member); + } diff --git a/backend/src/main/java/com/votogether/domain/alarm/repository/ReportActionAlarmRepository.java b/backend/src/main/java/com/votogether/domain/alarm/repository/ReportActionAlarmRepository.java index 4b43c3948..3ea00917d 100644 --- a/backend/src/main/java/com/votogether/domain/alarm/repository/ReportActionAlarmRepository.java +++ b/backend/src/main/java/com/votogether/domain/alarm/repository/ReportActionAlarmRepository.java @@ -13,6 +13,8 @@ public interface ReportActionAlarmRepository extends JpaRepository findByIdAndMember(final Long Id, final Member member); + Optional findByMemberOrderByIdDesc(final Member member); + List findAllByMember(final Member member); } diff --git a/backend/src/main/java/com/votogether/domain/member/controller/MemberController.java b/backend/src/main/java/com/votogether/domain/member/controller/MemberController.java index 86c84b39f..3cd32d3d6 100644 --- a/backend/src/main/java/com/votogether/domain/member/controller/MemberController.java +++ b/backend/src/main/java/com/votogether/domain/member/controller/MemberController.java @@ -46,6 +46,12 @@ public ResponseEntity updateDetails( return ResponseEntity.ok().build(); } + @PatchMapping("/me/check-alarm") + public ResponseEntity checkLatestAlarm(@Auth final Member member) { + memberService.checkLatestAlarm(member); + return ResponseEntity.ok().build(); + } + @DeleteMapping("/me/delete") public ResponseEntity deleteMember(@Auth final Member member) { memberService.deleteMember(member); diff --git a/backend/src/main/java/com/votogether/domain/member/controller/MemberControllerDocs.java b/backend/src/main/java/com/votogether/domain/member/controller/MemberControllerDocs.java index 082b424d0..58f09b1d9 100644 --- a/backend/src/main/java/com/votogether/domain/member/controller/MemberControllerDocs.java +++ b/backend/src/main/java/com/votogether/domain/member/controller/MemberControllerDocs.java @@ -5,6 +5,7 @@ import com.votogether.domain.member.dto.response.MemberInfoResponse; import com.votogether.domain.member.entity.Member; import com.votogether.global.exception.ExceptionResponse; +import com.votogether.global.jwt.Auth; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -17,7 +18,14 @@ public interface MemberControllerDocs { @Operation(summary = "회원 정보 조회", description = "회원 정보를 조회한다.") - @ApiResponse(responseCode = "200", description = "회원 정보 조회 성공") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "회원 정보 조회 성공"), + @ApiResponse( + responseCode = "400", + description = "회원에 해당하는 통계 정보가 없는 경우", + content = @Content(schema = @Schema(implementation = ExceptionResponse.class)) + ) + }) ResponseEntity findMemberInfo(final Member member); @Operation(summary = "회원 닉네임 변경", description = "회원 닉네임을 변경한다.") @@ -48,6 +56,10 @@ ResponseEntity updateDetails( final Member member ); + @Operation(summary = "회원 최신 알림 확인", description = "회원의 최신 알림 읽은 시간을 수정한다.") + @ApiResponse(responseCode = "200", description = "최신 알림 읽기 성공") + ResponseEntity checkLatestAlarm(@Auth final Member member); + @Operation(summary = "회원 탈퇴", description = "회원 탈퇴한다.") @ApiResponse(responseCode = "200", description = "회원 탈퇴 성공") ResponseEntity deleteMember(final Member member); diff --git a/backend/src/main/java/com/votogether/domain/member/dto/response/MemberInfoResponse.java b/backend/src/main/java/com/votogether/domain/member/dto/response/MemberInfoResponse.java index cfa073055..5ff5fe81b 100644 --- a/backend/src/main/java/com/votogether/domain/member/dto/response/MemberInfoResponse.java +++ b/backend/src/main/java/com/votogether/domain/member/dto/response/MemberInfoResponse.java @@ -15,13 +15,16 @@ public record MemberInfoResponse( @Schema(description = "출생년도", example = "2002") Integer birthYear, - @Schema(description = "권한", example = "MEMBER") - Roles roles, - @Schema(description = "작성한 게시글 수", example = "5") long postCount, @Schema(description = "투표한 수", example = "10") - long voteCount + long voteCount, + + @Schema(description = "권한", example = "MEMBER") + Roles roles, + + @Schema(description = "최신 알림 존재 여부", example = "false") + boolean hasLatestAlarm ) { } diff --git a/backend/src/main/java/com/votogether/domain/member/entity/Member.java b/backend/src/main/java/com/votogether/domain/member/entity/Member.java index ce854e871..a335ada2f 100644 --- a/backend/src/main/java/com/votogether/domain/member/entity/Member.java +++ b/backend/src/main/java/com/votogether/domain/member/entity/Member.java @@ -56,9 +56,12 @@ public class Member extends BaseEntity { private String socialId; @Enumerated(EnumType.STRING) - @Column(nullable = false, length = 20) + @Column(length = 20, nullable = false) private Roles roles; + @Column(columnDefinition = "datetime(6)", nullable = false) + private LocalDateTime alarmCheckedAt; + @Builder private Member( final String nickname, @@ -66,7 +69,8 @@ private Member( final Integer birthYear, final SocialType socialType, final String socialId, - final Roles roles + final Roles roles, + final LocalDateTime alarmCheckedAt ) { this.nickname = new Nickname(nickname); this.gender = gender; @@ -74,6 +78,7 @@ private Member( this.socialType = socialType; this.socialId = socialId; this.roles = roles; + this.alarmCheckedAt = alarmCheckedAt; } public static Member from(final KakaoMemberResponse response) { @@ -82,6 +87,7 @@ public static Member from(final KakaoMemberResponse response) { .socialType(SocialType.KAKAO) .socialId(String.valueOf(response.id())) .roles(Roles.MEMBER) + .alarmCheckedAt(LocalDateTime.now()) .build(); } @@ -117,6 +123,14 @@ public boolean hasEssentialInfo() { return (this.gender != null && this.birthYear != null); } + public boolean hasLatestAlarmCompareTo(final LocalDateTime latestAlarmCreatedAt) { + return alarmCheckedAt.isBefore(latestAlarmCreatedAt); + } + + public void checkAlarm() { + alarmCheckedAt = LocalDateTime.now(); + } + public String getNickname() { return this.nickname.getValue(); } diff --git a/backend/src/main/java/com/votogether/domain/member/service/MemberService.java b/backend/src/main/java/com/votogether/domain/member/service/MemberService.java index f244dbf6d..1023c53a4 100644 --- a/backend/src/main/java/com/votogether/domain/member/service/MemberService.java +++ b/backend/src/main/java/com/votogether/domain/member/service/MemberService.java @@ -27,6 +27,7 @@ import com.votogether.domain.vote.repository.VoteRepository; import com.votogether.global.exception.BadRequestException; import com.votogether.global.exception.NotFoundException; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -79,15 +80,39 @@ public Member findById(final Long memberId) { public MemberInfoResponse findMemberInfo(final Member member) { final MemberMetric memberMetric = memberMetricRepository.findByMember(member) .orElseThrow(() -> new NotFoundException(MemberExceptionType.NOT_FOUND_METRIC)); + final boolean hasLatestAlarm = hasLatestAlarm(member); return new MemberInfoResponse( member.getNickname(), member.getGender(), member.getBirthYear(), - member.getRoles(), memberMetric.getPostCount(), - memberMetric.getVoteCount() + memberMetric.getVoteCount(), + member.getRoles(), + hasLatestAlarm + ); + } + + private boolean hasLatestAlarm(final Member member) { + final Optional maybeAlarm = alarmRepository.findByMemberOrderByIdDesc(member); + final Optional maybeReportActionAlarm = + reportActionAlarmRepository.findByMemberOrderByIdDesc(member); + final List> maybeCreatedAts = List.of( + maybeAlarm.map(Alarm::getCreatedAt), + maybeReportActionAlarm.map(ReportActionAlarm::getCreatedAt) ); + + return getLatestAlarmCreatedAt(maybeCreatedAts, member); + } + + private boolean getLatestAlarmCreatedAt( + final List> maybeCreatedAts, + final Member member + ) { + return maybeCreatedAts.stream() + .filter(Optional::isPresent) + .map(Optional::get) + .anyMatch(member::hasLatestAlarmCompareTo); } @Transactional @@ -118,6 +143,11 @@ private void validateExistentDetails(final Member member) { } } + @Transactional + public void checkLatestAlarm(final Member member) { + member.checkAlarm(); + } + @Transactional public void deleteMember(final Member member) { final List posts = deletePosts(member); diff --git a/backend/src/test/java/com/votogether/domain/alarm/entity/AlarmTest.java b/backend/src/test/java/com/votogether/domain/alarm/entity/AlarmTest.java index a0cc671f2..6a6668d7c 100644 --- a/backend/src/test/java/com/votogether/domain/alarm/entity/AlarmTest.java +++ b/backend/src/test/java/com/votogether/domain/alarm/entity/AlarmTest.java @@ -7,6 +7,7 @@ import com.votogether.domain.member.entity.Member; import com.votogether.global.exception.BadRequestException; import com.votogether.test.fixtures.MemberFixtures; +import java.time.LocalDateTime; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.test.util.ReflectionTestUtils; @@ -54,4 +55,26 @@ void checkOwner() { .hasMessage("알림을 읽을 대상이 아닙니다."); } + @Test + @DisplayName("알림이 생성된 시각과 인자로 받은 시각을 비교하여 최신 시각을 반환한다.") + void getLatestAlarmCreatedAt() { + // given + Member member = MemberFixtures.MALE_30.get(); + Alarm alarm = Alarm.builder() + .member(member) + .alarmType(AlarmType.COMMENT) + .targetId(1L) + .detail("detail") + .isChecked(false) + .build(); + ReflectionTestUtils.setField(alarm, "createdAt", LocalDateTime.of(2010, 10, 18, 12, 0)); + LocalDateTime now = LocalDateTime.now(); + + // when + LocalDateTime latestAlarmCreatedAt = alarm.getLatestAlarmCreatedAt(now); + + // then + assertThat(latestAlarmCreatedAt).isEqualTo(now); + } + } diff --git a/backend/src/test/java/com/votogether/domain/member/controller/MemberControllerTest.java b/backend/src/test/java/com/votogether/domain/member/controller/MemberControllerTest.java index d0758af95..1372b8a18 100644 --- a/backend/src/test/java/com/votogether/domain/member/controller/MemberControllerTest.java +++ b/backend/src/test/java/com/votogether/domain/member/controller/MemberControllerTest.java @@ -55,9 +55,10 @@ void findMemberInfo() throws Exception { "저문", Gender.MALE, 1988, - Roles.MEMBER, 0, - 0 + 0, + Roles.MEMBER, + false ); given(tokenProcessor.resolveToken(anyString())).willReturn("token"); @@ -219,6 +220,26 @@ void invalidNullOfBirthYear(Integer birthYear) throws Exception { } + @Test + @DisplayName("최신 알림 읽기에 성공하면 200을 반환한다.") + void checkLatestAlarm() throws Exception { + // given + TokenPayload tokenPayload = new TokenPayload(1L, 1L, 1L); + given(tokenProcessor.resolveToken(anyString())).willReturn("token"); + given(tokenProcessor.parseToken(anyString())).willReturn(tokenPayload); + given(memberService.findById(anyLong())).willReturn(MemberFixtures.FEMALE_20.get()); + + willDoNothing().given(memberService).checkLatestAlarm(any(Member.class)); + + // when, then + RestAssuredMockMvc + .given().log().all() + .headers(HttpHeaders.AUTHORIZATION, "Bearer token") + .when().patch("/members/me/check-alarm") + .then().log().all() + .statusCode(HttpStatus.OK.value()); + } + @Test @DisplayName("회원 탈퇴에 성공하면 204를 반환한다.") void deleteMember() throws Exception { diff --git a/backend/src/test/java/com/votogether/domain/member/entity/MemberTest.java b/backend/src/test/java/com/votogether/domain/member/entity/MemberTest.java index b3eb1f33a..0cdba4e65 100644 --- a/backend/src/test/java/com/votogether/domain/member/entity/MemberTest.java +++ b/backend/src/test/java/com/votogether/domain/member/entity/MemberTest.java @@ -6,6 +6,7 @@ import com.votogether.domain.member.entity.vo.Gender; import com.votogether.domain.member.entity.vo.SocialType; import com.votogether.global.exception.BadRequestException; +import com.votogether.test.fixtures.MemberFixtures; import java.time.LocalDateTime; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -15,7 +16,7 @@ class MemberTest { @Nested - @DisplayName("닉네임을 주기에 따라 변경하는 경우") + @DisplayName("닉네임을 주기에 따라 변경하는 경우 ") class ChangeNicknameByCycle { @Test @@ -112,4 +113,53 @@ void notAllowedChangeToInitialNicknamePrefix() { } + @Test + @DisplayName("회원은 알림을 확인하면 이전에 저장되어있던 시간 이후의 시간으로 변경된다.") + void checkAlarm() { + // given + Member member = MemberFixtures.MALE_20.get(); + ReflectionTestUtils.setField(member, "createdAt", LocalDateTime.of(2001, 10, 10, 12, 0)); + LocalDateTime beforeAlarmCheckedAt = member.getAlarmCheckedAt(); + + // when + member.checkAlarm(); + + // then + LocalDateTime afterAlarmCheckedAt = member.getAlarmCheckedAt(); + assertThat(afterAlarmCheckedAt).isAfter(beforeAlarmCheckedAt); + } + + @Nested + @DisplayName("회원의 최신 알림 읽은 시각이 인자로 받은 시각보다") + class HasLatestAlarmCompareTo { + + @Test + @DisplayName("이전이면 true를 반환한다.") + void returnsTrue() { + // given + Member member = MemberFixtures.MALE_20.get(); + + // when + boolean hasLatestAlarm = member.hasLatestAlarmCompareTo(LocalDateTime.now()); + + // then + assertThat(hasLatestAlarm).isTrue(); + } + + @Test + @DisplayName("이후이면 false를 반환한다.") + void returnsFalse() { + // given + Member member = MemberFixtures.MALE_20.get(); + LocalDateTime beforeTime = LocalDateTime.of(2010, 10, 18, 12, 0); + + // when + boolean hasLatestAlarm = member.hasLatestAlarmCompareTo(beforeTime); + + // then + assertThat(hasLatestAlarm).isFalse(); + } + + } + } diff --git a/backend/src/test/java/com/votogether/domain/member/service/MemberServiceTest.java b/backend/src/test/java/com/votogether/domain/member/service/MemberServiceTest.java index 64d0ac694..3634b3666 100644 --- a/backend/src/test/java/com/votogether/domain/member/service/MemberServiceTest.java +++ b/backend/src/test/java/com/votogether/domain/member/service/MemberServiceTest.java @@ -5,11 +5,13 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.junit.jupiter.api.Assertions.assertAll; +import com.votogether.domain.alarm.entity.Alarm; import com.votogether.domain.alarm.entity.ReportActionAlarm; import com.votogether.domain.alarm.repository.ReportActionAlarmRepository; import com.votogether.domain.category.entity.Category; import com.votogether.domain.category.repository.CategoryRepository; import com.votogether.domain.member.dto.request.MemberDetailRequest; +import com.votogether.domain.member.dto.response.MemberInfoResponse; import com.votogether.domain.member.entity.Member; import com.votogether.domain.member.entity.MemberCategory; import com.votogether.domain.member.entity.vo.Gender; @@ -28,9 +30,12 @@ import com.votogether.domain.report.entity.vo.ReportType; import com.votogether.domain.report.repository.ReportRepository; import com.votogether.global.exception.BadRequestException; +import com.votogether.global.exception.NotFoundException; import com.votogether.test.ServiceTest; import com.votogether.test.fixtures.MemberFixtures; +import com.votogether.test.persister.AlarmTestPersister; import com.votogether.test.persister.PostTestPersister; +import com.votogether.test.persister.ReportActionAlarmTestPersister; import jakarta.persistence.EntityManager; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; @@ -41,6 +46,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.util.ReflectionTestUtils; class MemberServiceTest extends ServiceTest { @@ -71,6 +77,12 @@ class MemberServiceTest extends ServiceTest { @Autowired PostTestPersister postTestPersister; + @Autowired + AlarmTestPersister alarmTestPersister; + + @Autowired + ReportActionAlarmTestPersister reportActionAlarmTestPersister; + @Autowired ReportActionAlarmRepository reportActionAlarmRepository; @@ -111,6 +123,7 @@ void changeNickname() { .socialId("abc123") .socialType(SocialType.KAKAO) .roles(Roles.MEMBER) + .alarmCheckedAt(LocalDateTime.now()) .build(); String newNickname = "jeomxon"; Member savedMember = memberRepository.save(member); @@ -172,6 +185,7 @@ void changeNicknameThrowsExceptionNotPassedChangingCycle() { .socialId("abc123") .socialType(SocialType.KAKAO) .roles(Roles.MEMBER) + .alarmCheckedAt(LocalDateTime.now()) .build(); Member savedMember = memberRepository.save(member); @@ -198,6 +212,7 @@ void updateDetailsSuccess() { .socialType(SocialType.KAKAO) .socialId("123123123") .roles(Roles.MEMBER) + .alarmCheckedAt(LocalDateTime.now()) .build(); Member member = memberRepository.save(unsavedMember); MemberDetailRequest request = new MemberDetailRequest(Gender.FEMALE, 2000); @@ -235,6 +250,7 @@ void updateDetailsSameBirthYear() { .socialType(SocialType.KAKAO) .socialId("123123123") .roles(Roles.MEMBER) + .alarmCheckedAt(LocalDateTime.now()) .build(); Member member = memberRepository.save(unsavedMember); @@ -248,6 +264,177 @@ void updateDetailsSameBirthYear() { } + @Nested + @DisplayName("내 정보 조회를 할 때") + class FindMemberInfo { + + @Test + @DisplayName("정상적으로 조회가 된다.") + void success() { + // given + Member member = memberTestPersister.builder().save(); + memberMetricTestPersister.builder().member(member).save(); + + // when + MemberInfoResponse memberInfoResponse = memberService.findMemberInfo(member); + + // then + assertSoftly(softly -> { + softly.assertThat(memberInfoResponse.gender()).isEqualTo(Gender.MALE); + softly.assertThat(memberInfoResponse.birthYear()).isEqualTo(1995); + softly.assertThat(memberInfoResponse.postCount()).isZero(); + softly.assertThat(memberInfoResponse.voteCount()).isZero(); + softly.assertThat(memberInfoResponse.hasLatestAlarm()).isFalse(); + }); + } + + @Test + @DisplayName("메트릭 정보가 없는 경우 예외가 발생한다.") + void throwsExceptionWhenNoMetrics() { + // given + Member member = memberTestPersister.builder().save(); + + // when, then + assertThatThrownBy(() -> memberService.findMemberInfo(member)) + .isInstanceOf(NotFoundException.class) + .hasMessage("메트릭 정보가 존재하지 않습니다."); + } + + @Test + @DisplayName("모든 알림이 존재하지 않는 경우 false가 반환된다.") + void returnsFalseWhenNoAlarmExists() { + // given + Member member = memberTestPersister.builder().save(); + memberMetricTestPersister.builder().member(member).save(); + + // when + MemberInfoResponse memberInfoResponse = memberService.findMemberInfo(member); + + // then + assertThat(memberInfoResponse.hasLatestAlarm()).isFalse(); + } + + @Test + @DisplayName("게시글 내역 알림이 존재한다면 회원의 최신 알림 확인 시각과 비교하여 게시글 알림이 더 최신인 경우 true를 반환한다.") + void returnsTrueWhenPostAlarmIsLatest() { + // given + Member member = memberTestPersister.builder().save(); + memberMetricTestPersister.builder().member(member).save(); + alarmTestPersister.builder().member(member).save(); + + // when + MemberInfoResponse memberInfoResponse = memberService.findMemberInfo(member); + + // then + assertThat(memberInfoResponse.hasLatestAlarm()).isTrue(); + } + + @Test + @DisplayName("신고 조치 내역 알림이 존재한다면 회원의 최신 알림 확인 시각과 비교하여 신고 조치 내역 알림이 더 최신인 경우 true를 반환한다.") + void returnsTrueWhenReportActionAlarmIsLatest() { + // given + Member member = memberTestPersister.builder().save(); + memberMetricTestPersister.builder().member(member).save(); + reportActionAlarmTestPersister.builder().member(member).save(); + + // when + MemberInfoResponse memberInfoResponse = memberService.findMemberInfo(member); + + // then + assertThat(memberInfoResponse.hasLatestAlarm()).isTrue(); + } + + @Test + @DisplayName("신고 조치 내역 알림과 게시글 내역 알림이 모두 존재한다면 회원의 최신 알림 확인 시각과 비교하여 알림들이 더 최신인 경우 true를 반환한다.") + void returnsTrueWhenReportActionAlarmOrPostAlarmIsLatest() { + // given + Member member = memberTestPersister.builder().save(); + memberMetricTestPersister.builder().member(member).save(); + alarmTestPersister.builder().member(member).save(); + reportActionAlarmTestPersister.builder().member(member).save(); + + // when + MemberInfoResponse memberInfoResponse = memberService.findMemberInfo(member); + + // then + assertThat(memberInfoResponse.hasLatestAlarm()).isTrue(); + } + + @Test + @DisplayName("게시글 내역 알림과 신고 조치 내역 알림 모두 존재하고, 게시글 내역 알림이 가장 최신인 경우 true를 반환한다.") + void returnsTrueWhenPostAlarmIsLatestAndReportActionAlarmIsNotLatest() { + // given + Member member = memberTestPersister.builder().save(); + memberMetricTestPersister.builder().member(member).save(); + Alarm alarm = alarmTestPersister.builder().member(member).save(); + ReportActionAlarm reportActionAlarm = reportActionAlarmTestPersister.builder().member(member).save(); + + ReflectionTestUtils.setField(member, "alarmCheckedAt", LocalDateTime.now().minusDays(1)); + ReflectionTestUtils.setField(alarm, "createdAt", LocalDateTime.now()); + ReflectionTestUtils.setField(reportActionAlarm, "createdAt", LocalDateTime.now().minusDays(1)); + + // when + MemberInfoResponse memberInfoResponse = memberService.findMemberInfo(member); + + // then + assertThat(memberInfoResponse.hasLatestAlarm()).isTrue(); + } + + @Test + @DisplayName("게시글 내역 알림과 신고 조치 내역 알림 모두 존재하고, 신고 조치 내역 알림이 가장 최신인 경우 true를 반환한다.") + void returnsTrueWhenPostAlarmIsNotLatestAndReportActionAlarmIsLatest() { + // given + Member member = memberTestPersister.builder().save(); + memberMetricTestPersister.builder().member(member).save(); + Alarm alarm = alarmTestPersister.builder().member(member).save(); + ReportActionAlarm reportActionAlarm = reportActionAlarmTestPersister.builder().member(member).save(); + + ReflectionTestUtils.setField(member, "alarmCheckedAt", LocalDateTime.now().minusDays(1)); + ReflectionTestUtils.setField(alarm, "createdAt", LocalDateTime.now().minusDays(1)); + ReflectionTestUtils.setField(reportActionAlarm, "createdAt", LocalDateTime.now()); + + // when + MemberInfoResponse memberInfoResponse = memberService.findMemberInfo(member); + + // then + assertThat(memberInfoResponse.hasLatestAlarm()).isTrue(); + } + + @Test + @DisplayName("신고 조치 내역 알림이 존재하지만, 회원의 최신 알림 확인 시각이 더 최신인 경우 false를 반환한다.") + void returnsFalseWhenReportActionAlarmOrPostAlarmIsNotLatest() { + // given + Member member = memberTestPersister.builder().save(); + memberMetricTestPersister.builder().member(member).save(); + Alarm alarm = alarmTestPersister.builder().member(member).save(); + ReportActionAlarm reportActionAlarm = reportActionAlarmTestPersister.builder().member(member).save(); + + ReflectionTestUtils.setField(alarm, "createdAt", LocalDateTime.now().minusDays(1)); + ReflectionTestUtils.setField(reportActionAlarm, "createdAt", LocalDateTime.now().minusDays(1)); + + // when + MemberInfoResponse memberInfoResponse = memberService.findMemberInfo(member); + + // then + assertThat(memberInfoResponse.hasLatestAlarm()).isFalse(); + } + + } + + @Test + @DisplayName("최신 알림을 확인한다.") + void checkLatestAlarm() { + // given + Member member = memberTestPersister.builder().save(); + LocalDateTime beforeTime = member.getCreatedAt(); + + // when + memberService.checkLatestAlarm(member); + + // then + assertThat(member.getAlarmCheckedAt()).isNotEqualTo(beforeTime); + } + @Nested @DisplayName("회원 탈퇴를 할 때") class DeleteMember { diff --git a/backend/src/test/java/com/votogether/test/fixtures/MemberFixtures.java b/backend/src/test/java/com/votogether/test/fixtures/MemberFixtures.java index ed1ab2631..0d14b2e7f 100644 --- a/backend/src/test/java/com/votogether/test/fixtures/MemberFixtures.java +++ b/backend/src/test/java/com/votogether/test/fixtures/MemberFixtures.java @@ -4,29 +4,30 @@ import com.votogether.domain.member.entity.vo.Gender; import com.votogether.domain.member.entity.vo.Roles; import com.votogether.domain.member.entity.vo.SocialType; +import java.time.LocalDateTime; public enum MemberFixtures { - MALE_UNDER_10("user1", Gender.MALE, 2015, "user1", Roles.MEMBER), - FEMALE_UNDER_10("user2", Gender.FEMALE, 2015, "user2", Roles.MEMBER), - MALE_10("user3", Gender.MALE, 2005, "user3", Roles.MEMBER), - FEMALE_10("user4", Gender.FEMALE, 2005, "user4", Roles.MEMBER), - MALE_20("user7", Gender.MALE, 1995, "user7", Roles.MEMBER), - FEMALE_20("user8", Gender.FEMALE, 1995, "user8", Roles.MEMBER), - MALE_30("user9", Gender.MALE, 1985, "user9", Roles.MEMBER), - FEMALE_30("user10", Gender.FEMALE, 1985, "user10", Roles.MEMBER), - MALE_40("user11", Gender.MALE, 1975, "user11", Roles.MEMBER), - FEMALE_40("user12", Gender.FEMALE, 1975, "user12", Roles.MEMBER), - MALE_50("user13", Gender.MALE, 1965, "user13", Roles.MEMBER), - FEMALE_50("user14", Gender.FEMALE, 1965, "user14", Roles.MEMBER), - MALE_60("user15", Gender.MALE, 1955, "user15", Roles.MEMBER), - FEMALE_60("user16", Gender.FEMALE, 1955, "user16", Roles.MEMBER), - MALE_70("user17", Gender.MALE, 1945, "user17", Roles.MEMBER), - FEMALE_70("user18", Gender.FEMALE, 1945, "user18", Roles.MEMBER), - MALE_80("user19", Gender.MALE, 1935, "user19", Roles.MEMBER), - FEMALE_80("user20", Gender.FEMALE, 1935, "user20", Roles.MEMBER), - MALE_OVER_90("user21", Gender.MALE, 1925, "user21", Roles.MEMBER), - FEMALE_OVER_90("user22", Gender.FEMALE, 1925, "user22", Roles.MEMBER), + MALE_UNDER_10("user1", Gender.MALE, 2015, "user1", Roles.MEMBER, LocalDateTime.now()), + FEMALE_UNDER_10("user2", Gender.FEMALE, 2015, "user2", Roles.MEMBER, LocalDateTime.now()), + MALE_10("user3", Gender.MALE, 2005, "user3", Roles.MEMBER, LocalDateTime.now()), + FEMALE_10("user4", Gender.FEMALE, 2005, "user4", Roles.MEMBER, LocalDateTime.now()), + MALE_20("user7", Gender.MALE, 1995, "user7", Roles.MEMBER, LocalDateTime.now()), + FEMALE_20("user8", Gender.FEMALE, 1995, "user8", Roles.MEMBER, LocalDateTime.now()), + MALE_30("user9", Gender.MALE, 1985, "user9", Roles.MEMBER, LocalDateTime.now()), + FEMALE_30("user10", Gender.FEMALE, 1985, "user10", Roles.MEMBER, LocalDateTime.now()), + MALE_40("user11", Gender.MALE, 1975, "user11", Roles.MEMBER, LocalDateTime.now()), + FEMALE_40("user12", Gender.FEMALE, 1975, "user12", Roles.MEMBER, LocalDateTime.now()), + MALE_50("user13", Gender.MALE, 1965, "user13", Roles.MEMBER, LocalDateTime.now()), + FEMALE_50("user14", Gender.FEMALE, 1965, "user14", Roles.MEMBER, LocalDateTime.now()), + MALE_60("user15", Gender.MALE, 1955, "user15", Roles.MEMBER, LocalDateTime.now()), + FEMALE_60("user16", Gender.FEMALE, 1955, "user16", Roles.MEMBER, LocalDateTime.now()), + MALE_70("user17", Gender.MALE, 1945, "user17", Roles.MEMBER, LocalDateTime.now()), + FEMALE_70("user18", Gender.FEMALE, 1945, "user18", Roles.MEMBER, LocalDateTime.now()), + MALE_80("user19", Gender.MALE, 1935, "user19", Roles.MEMBER, LocalDateTime.now()), + FEMALE_80("user20", Gender.FEMALE, 1935, "user20", Roles.MEMBER, LocalDateTime.now()), + MALE_OVER_90("user21", Gender.MALE, 1925, "user21", Roles.MEMBER, LocalDateTime.now()), + FEMALE_OVER_90("user22", Gender.FEMALE, 1925, "user22", Roles.MEMBER, LocalDateTime.now()), ; private final String nickname; @@ -34,19 +35,22 @@ public enum MemberFixtures { private final Integer birthYear; private final String socialId; private final Roles roles; + private final LocalDateTime alarmCheckedAt; MemberFixtures( final String nickname, final Gender gender, final Integer birthYear, final String socialId, - final Roles roles + final Roles roles, + final LocalDateTime alarmCheckedAt ) { this.nickname = nickname; this.gender = gender; this.birthYear = birthYear; this.socialId = socialId; this.roles = roles; + this.alarmCheckedAt = alarmCheckedAt; } public Member get() { @@ -57,6 +61,7 @@ public Member get() { .socialType(SocialType.KAKAO) .socialId(socialId) .roles(roles) + .alarmCheckedAt(alarmCheckedAt) .build(); } diff --git a/backend/src/test/java/com/votogether/test/persister/AlarmTestPersister.java b/backend/src/test/java/com/votogether/test/persister/AlarmTestPersister.java new file mode 100644 index 000000000..08a7b6a4a --- /dev/null +++ b/backend/src/test/java/com/votogether/test/persister/AlarmTestPersister.java @@ -0,0 +1,66 @@ +package com.votogether.test.persister; + +import com.votogether.domain.alarm.entity.Alarm; +import com.votogether.domain.alarm.entity.vo.AlarmType; +import com.votogether.domain.alarm.repository.AlarmRepository; +import com.votogether.domain.member.entity.Member; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Persister +public class AlarmTestPersister { + + private final AlarmRepository alarmRepository; + private final MemberTestPersister memberTestPersister; + + public AlarmBuilder builder() { + return new AlarmBuilder(); + } + + public final class AlarmBuilder { + + private Member member; + private AlarmType alarmType; + private Long targetId; + private String detail; + private boolean isChecked; + + public AlarmBuilder member(Member member) { + this.member = member; + return this; + } + + public AlarmBuilder alarmType(AlarmType alarmType) { + this.alarmType = alarmType; + return this; + } + + public AlarmBuilder targetId(Long targetId) { + this.targetId = targetId; + return this; + } + + public AlarmBuilder detail(String detail) { + this.detail = detail; + return this; + } + + public AlarmBuilder isChecked(boolean isChecked) { + this.isChecked = isChecked; + return this; + } + + public Alarm save() { + Alarm alarm = Alarm.builder() + .member(member == null ? memberTestPersister.builder().save() : member) + .alarmType(alarmType == null ? AlarmType.COMMENT : alarmType) + .targetId(targetId == null ? 1L : targetId) + .detail(detail == null ? "detail" : detail) + .isChecked(false) + .build(); + return alarmRepository.save(alarm); + } + + } + +} diff --git a/backend/src/test/java/com/votogether/test/persister/MemberTestPersister.java b/backend/src/test/java/com/votogether/test/persister/MemberTestPersister.java index e62e0a0b0..ec549908e 100644 --- a/backend/src/test/java/com/votogether/test/persister/MemberTestPersister.java +++ b/backend/src/test/java/com/votogether/test/persister/MemberTestPersister.java @@ -5,6 +5,7 @@ import com.votogether.domain.member.entity.vo.Roles; import com.votogether.domain.member.entity.vo.SocialType; import com.votogether.domain.member.repository.MemberRepository; +import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.RandomStringUtils; @@ -26,6 +27,7 @@ public final class MemberBuilder { private SocialType socialType; private String socialId; private Roles roles; + private LocalDateTime alarmCheckedAt; public MemberBuilder nickname(String nickname) { this.nickname = nickname; @@ -57,6 +59,11 @@ public MemberBuilder roles(Roles roles) { return this; } + public MemberBuilder alarmCheckedAt(LocalDateTime alarmCheckedAt) { + this.alarmCheckedAt = alarmCheckedAt; + return this; + } + public Member save() { Member member = Member.builder() .nickname(nickname == null ? RandomStringUtils.random(10, true, true) : nickname) @@ -65,6 +72,7 @@ public Member save() { .socialType(socialType == null ? SocialType.KAKAO : socialType) .socialId(socialId == null ? RandomStringUtils.random(10, true, true) : socialId) .roles(roles == null ? Roles.MEMBER : roles) + .alarmCheckedAt(alarmCheckedAt == null ? LocalDateTime.now() : alarmCheckedAt) .build(); return memberRepository.save(member); } diff --git a/backend/src/test/java/com/votogether/test/persister/ReportActionAlarmTestPersister.java b/backend/src/test/java/com/votogether/test/persister/ReportActionAlarmTestPersister.java new file mode 100644 index 000000000..62d56421e --- /dev/null +++ b/backend/src/test/java/com/votogether/test/persister/ReportActionAlarmTestPersister.java @@ -0,0 +1,66 @@ +package com.votogether.test.persister; + +import com.votogether.domain.alarm.entity.ReportActionAlarm; +import com.votogether.domain.alarm.repository.ReportActionAlarmRepository; +import com.votogether.domain.member.entity.Member; +import com.votogether.domain.report.entity.vo.ReportType; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Persister +public class ReportActionAlarmTestPersister { + + private final ReportActionAlarmRepository reportActionAlarmRepository; + private final MemberTestPersister memberTestPersister; + + public ReportActionAlarmBuilder builder() { + return new ReportActionAlarmBuilder(); + } + + public final class ReportActionAlarmBuilder { + + private Member member; + private ReportType reportType; + private String target; + private String reasons; + private boolean isChecked; + + public ReportActionAlarmBuilder member(Member member) { + this.member = member; + return this; + } + + public ReportActionAlarmBuilder reportType(ReportType reportType) { + this.reportType = reportType; + return this; + } + + public ReportActionAlarmBuilder target(String target) { + this.target = target; + return this; + } + + public ReportActionAlarmBuilder reasons(String reasons) { + this.reasons = reasons; + return this; + } + + public ReportActionAlarmBuilder isChecked(boolean isChecked) { + this.isChecked = isChecked; + return this; + } + + public ReportActionAlarm save() { + ReportActionAlarm reportActionAlarm = ReportActionAlarm.builder() + .member(member == null ? memberTestPersister.builder().save() : member) + .reportType(reportType == null ? ReportType.NICKNAME : reportType) + .target(target == null ? "target" : target) + .reasons(reasons == null ? "reasons" : reasons) + .isChecked(false) + .build(); + return reportActionAlarmRepository.save(reportActionAlarm); + } + + } + +}