Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

최신 알림 존재 여부 및 읽기 기능 구현 #783

Merged
merged 13 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
@Getter
public enum AlarmExceptionType implements ExceptionType {

NOT_FOUND_ACTION(1300, "신고조치알림이 존재하지 않습니다."),
NOT_FOUND_ACTION(1300, "신고 조치 알림이 존재하지 않습니다."),
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

NOT_FOUND(1301, "알림이 존재하지 않습니다."),
NOT_OWNER(1302, "알림을 읽을 대상이 아닙니다."),
NOT_FOUND_ACTION_TYPE(1303, "등록되지 않은 알림 동작입니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -13,4 +14,6 @@ public interface AlarmRepository extends JpaRepository<Alarm, Long> {

List<Alarm> findAllByMember(final Member member);

Optional<Alarm> findByMemberOrderByIdDesc(final Member member);

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public interface ReportActionAlarmRepository extends JpaRepository<ReportActionA

Optional<ReportActionAlarm> findByIdAndMember(final Long Id, final Member member);

Optional<ReportActionAlarm> findByMemberOrderByIdDesc(final Member member);

List<ReportActionAlarm> findAllByMember(final Member member);

}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ public ResponseEntity<Void> updateDetails(
return ResponseEntity.ok().build();
}

@PatchMapping("/me/check-alarm")
public ResponseEntity<Void> checkLatestAlarm(@Auth final Member member) {
memberService.checkLatestAlarm(member);
return ResponseEntity.ok().build();
}

@DeleteMapping("/me/delete")
public ResponseEntity<Void> deleteMember(@Auth final Member member) {
memberService.deleteMember(member);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<MemberInfoResponse> findMemberInfo(final Member member);

@Operation(summary = "회원 닉네임 변경", description = "회원 닉네임을 변경한다.")
Expand Down Expand Up @@ -48,6 +56,10 @@ ResponseEntity<Void> updateDetails(
final Member member
);

@Operation(summary = "회원 최신 알림 확인", description = "회원의 최신 알림 읽은 시간을 수정한다.")
@ApiResponse(responseCode = "200", description = "최신 알림 읽기 성공")
ResponseEntity<Void> checkLatestAlarm(@Auth final Member member);

@Operation(summary = "회원 탈퇴", description = "회원 탈퇴한다.")
@ApiResponse(responseCode = "200", description = "회원 탈퇴 성공")
ResponseEntity<Void> deleteMember(final Member member);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment on lines +24 to +25
Copy link
Collaborator

Choose a reason for hiding this comment

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

꼼꼼히 챙겨주셨네요 👍🏻


@Schema(description = "최신 알림 존재 여부", example = "false")
boolean hasLatestAlarm
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,29 @@ 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)
Copy link
Collaborator

Choose a reason for hiding this comment

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

다른 컬럼과 일관성있게 nullable이 앞으로 오면 좋을 것 같아요 😺

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

저희 컨벤션을 정하기로는 nullable = false가 가장 마지막으로 오는 것으로 정했는데,
Roles가 추가되면서 혼동이 있었던 것 같아요.
Rolesnullable = false를 뒤로 이동시킬게요!

Copy link
Collaborator

Choose a reason for hiding this comment

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

헉 그런가요 감사합니다 ㅎㅎ

private LocalDateTime alarmCheckedAt;

@Builder
private Member(
final String nickname,
final Gender gender,
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;
this.birthYear = birthYear;
this.socialType = socialType;
this.socialId = socialId;
this.roles = roles;
this.alarmCheckedAt = alarmCheckedAt;
}

public static Member from(final KakaoMemberResponse response) {
Expand All @@ -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();
}

Expand Down Expand Up @@ -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();
}
Comment on lines +130 to +132
Copy link
Collaborator

Choose a reason for hiding this comment

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

간결하고 깔끔한 구현 👍🏻


public String getNickname() {
return this.nickname.getValue();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Alarm> maybeAlarm = alarmRepository.findByMemberOrderByIdDesc(member);
final Optional<ReportActionAlarm> maybeReportActionAlarm =
reportActionAlarmRepository.findByMemberOrderByIdDesc(member);
final List<Optional<LocalDateTime>> maybeCreatedAts = List.of(
maybeAlarm.map(Alarm::getCreatedAt),
maybeReportActionAlarm.map(ReportActionAlarm::getCreatedAt)
);

return getLatestAlarmCreatedAt(maybeCreatedAts, member);
}

private boolean getLatestAlarmCreatedAt(
final List<Optional<LocalDateTime>> maybeCreatedAts,
final Member member
) {
return maybeCreatedAts.stream()
.filter(Optional::isPresent)
.map(Optional::get)
.anyMatch(member::hasLatestAlarmCompareTo);
}

@Transactional
Expand Down Expand Up @@ -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<Post> posts = deletePosts(member);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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());
Comment on lines +227 to +230
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
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());
mockingAuthArgumentResolver();

이렇게 바꾸면 어떻게 되나요??

지금 만드는 기능과 관련은 없지만 혹시나 하고 MemberControllerTest에서 전부 mockingAuthArgumentResolver(); 로 바꾸고 테스트를 실행해봤는데,

    @Test
    @DisplayName("회원 정보를 조회한다.")
    void findMemberInfo() throws Exception

이 테스트 제외하고는 전부 성공하더라구요. 그래서 MemberControllerTest 안에서 위 메서드 하나만 제외하고 mockingAuthArgumentResolver(); 로 다 바꿔주시는 것은 어떨까요

Copy link
Collaborator Author

@jeomxon jeomxon Oct 19, 2023

Choose a reason for hiding this comment

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

추후에 member 도메인을 리팩터링 하면서 통일성있게 변경해보는 것은 어떨까요?


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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,7 +16,7 @@
class MemberTest {

@Nested
@DisplayName("닉네임을 주기에 따라 변경하는 경우")
@DisplayName("닉네임을 주기에 따라 변경하는 경우 ")
class ChangeNicknameByCycle {

@Test
Expand Down Expand Up @@ -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();
Copy link
Collaborator

Choose a reason for hiding this comment

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

알림 읽기 전 시간을 정해두고 알림을 읽으면 해당 시간 이후임을 검증해보는건 어떨까요?!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

변경해보았습니다!


// then
LocalDateTime afterAlarmCheckedAt = member.getAlarmCheckedAt();
assertThat(afterAlarmCheckedAt).isAfter(beforeAlarmCheckedAt);
}

@Nested
@DisplayName("회원의 최신 알림 읽은 시각이 인자로 받은 시각보다")
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3
마지막에 띄어쓰기가 빠져있어요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

이게 결국 작성해보니깐 띄워쓰기가 있고 없고의 큰 차이가 없는 것 같더라구요...
앞으로 하나의 규칙만 정해서 밀고 가겠습니다..!

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();
}

}

}
Loading