Skip to content

Commit

Permalink
[BE] 알림 읽기 기능 구현(#738) (#740)
Browse files Browse the repository at this point in the history
* refactor: 액션으로 이름 변경

* feat: 아이디로 조회 위한 조회 기능 구현

* feat: 사용자의 알림 조회 기능 구현

* feat: 기능 경로 매핑 및 명세

* fix: createAt 검사에서 제외

* refactor: 피드백 반영

* [BE] 검색 기능 구현(#728) (#732)

* feat: Room Specification 작성

* feat: 검색 로직 구현

* feat: 컨트롤러 및 명세 작성

* docs: 명세 수정

---------

Co-authored-by: ashsty <[email protected]>

* fix: 변수명 수정 & 빈 값으로 검색 시 default 값 추가 (#737)

Co-authored-by: ashsty <[email protected]>

* feat: 알람 확인 기능 구현

* feat: 기능 경로 매핑 및 명세

* fix: 충돌 해결

---------

Co-authored-by: youngsu5582 <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: ashsty <[email protected]>
  • Loading branch information
3 people authored Nov 8, 2024
1 parent 2379a2f commit fb8e540
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 9 deletions.
10 changes: 10 additions & 0 deletions backend/src/main/java/corea/alarm/controller/AlarmController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package corea.alarm.controller;

import corea.alarm.dto.AlarmCheckRequest;
import corea.alarm.dto.AlarmCountResponse;
import corea.alarm.dto.AlarmResponses;
import corea.alarm.service.AlarmService;
Expand All @@ -9,6 +10,8 @@
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@Controller
@RequiredArgsConstructor
Expand All @@ -27,4 +30,11 @@ public ResponseEntity<AlarmResponses> getAlarms(@LoginMember AuthInfo authInfo)
AlarmResponses responses = alarmService.getAlarm(authInfo.getId());
return ResponseEntity.ok(responses);
}

@PostMapping("/alarm/check")
public ResponseEntity<Void> checkAlarm(@LoginMember AuthInfo authInfo, @RequestBody AlarmCheckRequest request) {
alarmService.checkAlarm(authInfo.getId(), request);
return ResponseEntity.ok()
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package corea.alarm.controller;

import corea.alarm.dto.AlarmCheckRequest;
import corea.alarm.dto.AlarmCountResponse;
import corea.alarm.dto.AlarmResponses;
import corea.auth.domain.AuthInfo;
Expand Down Expand Up @@ -31,4 +32,14 @@ public interface AlarmControllerSpecification {
"토큰이 없거나 유효하지 않은 경우 인증 오류가 발생합니다.")
@ApiErrorResponses(value = ExceptionType.MEMBER_NOT_FOUND)
ResponseEntity<AlarmResponses> getAlarms(AuthInfo authInfo);

@Operation(summary = "알림을 체크합니다.",
description = "자신의 마이페이지에 디스플레이 되는 프로필 정보를 작성합니다. <br>" +
"요청 시 `Authorization Header`에 `Bearer JWT token`을 포함시켜야 합니다. " +
"이 토큰을 기반으로 `AuthInfo` 객체가 생성되며 사용자의 정보가 자동으로 주입됩니다. <br>" +
"JWT 토큰에서 추출된 사용자 정보는 피드백 작성에 필요한 인증된 사용자 정보를 제공합니다. " +
"<br><br>**참고:** 이 API를 사용하기 위해서는 유효한 JWT 토큰이 필요하며, " +
"토큰이 없거나 유효하지 않은 경우 인증 오류가 발생합니다.")
@ApiErrorResponses(value = {ExceptionType.MEMBER_NOT_FOUND, ExceptionType.NOT_RECEIVED_ALARM})
ResponseEntity<Void> checkAlarm(AuthInfo authInfo, AlarmCheckRequest request);
}
6 changes: 5 additions & 1 deletion backend/src/main/java/corea/alarm/domain/AlarmType.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@

public enum AlarmType {
// 유저간 상호작용으로 인한 알람
USER
USER;

public static AlarmType from(String value) {
return AlarmType.valueOf(value);
}
}
9 changes: 9 additions & 0 deletions backend/src/main/java/corea/alarm/domain/UserToUserAlarm.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package corea.alarm.domain;

import corea.global.BaseTimeEntity;
import corea.member.domain.Member;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
Expand Down Expand Up @@ -46,4 +47,12 @@ public boolean isStatus(boolean status) {
public String getActionType() {
return alarmActionType.name();
}

public boolean isNotReceiver(Member member) {
return !receiverId.equals(member.getId());
}

public void read() {
isRead = true;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package corea.alarm.domain;

import corea.exception.CoreaException;
import corea.exception.ExceptionType;
import corea.global.annotation.Reader;
import corea.member.domain.Member;
import lombok.RequiredArgsConstructor;
Expand All @@ -20,6 +22,11 @@ public long countReceivedAlarm(Member member, boolean isRead) {
.count();
}

public UserToUserAlarm find(long actionId) {
return userToUserAlarmRepository.findById(actionId)
.orElseThrow(() -> new CoreaException(ExceptionType.NOT_RECEIVED_ALARM));
}

public UserAlarmsByActionType findAllByReceiver(Member member) {
return new UserAlarmsByActionType(userToUserAlarmRepository.findAllByReceiverId(member.getId())
.stream()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package corea.alarm.domain;

import corea.exception.CoreaException;
import corea.exception.ExceptionType;
import corea.global.annotation.Writer;
import corea.member.domain.Member;
import lombok.RequiredArgsConstructor;

@Writer
Expand All @@ -12,4 +15,12 @@ public class UserToUserAlarmWriter {
public UserToUserAlarm create(UserToUserAlarm userToUserAlarm) {
return userToUserAlarmRepository.save(userToUserAlarm);
}

public UserToUserAlarm check(Member member, UserToUserAlarm userToUserAlarm) {
if (userToUserAlarm.isNotReceiver(member)) {
throw new CoreaException(ExceptionType.NOT_RECEIVED_ALARM);
}
userToUserAlarm.read();
return userToUserAlarm;
}
}
12 changes: 12 additions & 0 deletions backend/src/main/java/corea/alarm/dto/AlarmCheckRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package corea.alarm.dto;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "알림 체크 요청")
public record AlarmCheckRequest(
@Schema(description = "액션 아이디", example = "4")
long actionId,

@Schema(description = "알림 타입", example = "USER")
String alarmType) {
}
18 changes: 15 additions & 3 deletions backend/src/main/java/corea/alarm/service/AlarmService.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package corea.alarm.service;

import corea.alarm.domain.*;
import corea.alarm.dto.AlarmCheckRequest;
import corea.alarm.dto.AlarmCountResponse;
import corea.alarm.dto.AlarmResponses;
import corea.alarm.domain.AlarmActionType;
import corea.alarm.domain.UserAlarmsByActionType;
import corea.alarm.domain.UserToUserAlarmReader;
import corea.alarm.domain.UserToUserAlarmWriter;
import corea.alarm.dto.AlarmCountResponse;
import corea.alarm.dto.AlarmResponses;
import corea.alarm.dto.CreateUserToUserAlarmInput;
import corea.member.domain.Member;
import corea.member.domain.MemberReader;
Expand All @@ -27,9 +29,9 @@ public class AlarmService {
private static final boolean UNREAD = false;

private final MemberReader memberReader;
private final RoomReader roomReader;
private final UserToUserAlarmWriter userToUserAlarmWriter;
private final UserToUserAlarmReader userToUserAlarmReader;
private final RoomReader roomReader;

public AlarmCountResponse getUnReadAlarmCount(long userId) {
Member member = memberReader.findOne(userId);
Expand All @@ -55,4 +57,14 @@ public AlarmResponses getAlarm(long userId) {
Map<Long, Room> rooms = roomReader.findRoomsMappedById(userToUserAlarms.getRoomIds());
return AlarmResponses.from(userToUserAlarms.getList(), actors, rooms);
}

@Transactional
public void checkAlarm(long userId, AlarmCheckRequest request) {
Member member = memberReader.findOne(userId);
AlarmType alarmType = AlarmType.from(request.alarmType());
if (alarmType == AlarmType.USER) {
UserToUserAlarm userToUserAlarm = userToUserAlarmReader.find(request.actionId());
userToUserAlarmWriter.check(member, userToUserAlarm);
}
}
}
3 changes: 3 additions & 0 deletions backend/src/main/java/corea/exception/ExceptionType.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public enum ExceptionType {
PARTICIPANT_SIZE_LACK_DUE_TO_PULL_REQUEST(HttpStatus.BAD_REQUEST, "pull request 미제출로 인해 인원이 부족하여 매칭을 진행할 수 없습니다."),
NOT_MATCHED_MEMBER(HttpStatus.BAD_REQUEST, "매칭된 인원들이 아닙니다."),
ALREADY_COMPLETED_REVIEW(HttpStatus.BAD_REQUEST, "이미 리뷰를 완료했습니다."),

NOT_RECEIVED_ALARM(HttpStatus.BAD_REQUEST,"본인이 받은 알람이 아닙니다."),

ALREADY_COMPLETED_FEEDBACK(HttpStatus.BAD_REQUEST, "이미 작성한 피드백이 존재합니다."),
INVALID_CALCULATION_FORMULA(HttpStatus.BAD_REQUEST, "잘못된 계산식입니다."),
INVALID_VALUE(HttpStatus.BAD_REQUEST, "올바르지 않은 값입니다."),
Expand Down
8 changes: 4 additions & 4 deletions backend/src/main/java/corea/room/domain/RoomReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ public Room find(long roomId) {
.orElseThrow(() -> new CoreaException(ExceptionType.ROOM_NOT_FOUND, String.format("해당 Id의 방이 없습니다. 입력된 Id=%d", roomId)));
}

public List<Room> findAll(Specification<Room> spec) {
return roomRepository.findAll(spec, Sort.by("recruitmentDeadline").descending());
}

public Map<Long, Room> findRoomsMappedById(Iterable<Long> roomIds) {
return roomRepository.findAllById(roomIds)
.stream()
.collect(Collectors.toMap(Room::getId, Function.identity()));
}

public List<Room> findAll(Specification<Room> spec) {
return roomRepository.findAll(spec, Sort.by("recruitmentDeadline").descending());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package corea.alarm.controller;

import config.ControllerTest;
import corea.alarm.domain.UserToUserAlarm;
import corea.alarm.domain.UserToUserAlarmRepository;
import corea.alarm.dto.AlarmCheckRequest;
import corea.auth.service.TokenService;
import corea.fixture.AlarmFixture;
import corea.fixture.MemberFixture;
import corea.fixture.RoomFixture;
import corea.member.domain.Member;
import corea.member.repository.MemberRepository;
import corea.room.domain.Room;
import corea.room.repository.RoomRepository;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

import static org.assertj.core.api.Assertions.assertThat;

@ControllerTest
class AlarmControllerTest {
@Autowired
private UserToUserAlarmRepository userToUserAlarmRepository;

@Autowired
private MemberRepository memberRepository;

@Autowired
private TokenService tokenService;

private Member actor;
private Member receiver;
private Room interaction;
private long interactionId;

@Autowired
private RoomRepository roomRepository;

@BeforeEach
void setUp() {
this.actor = memberRepository.save(MemberFixture.MEMBER_MOVIN());
this.receiver = memberRepository.save(MemberFixture.MEMBER_PORORO());
this.interaction = roomRepository.save(RoomFixture.ROOM_DOMAIN(memberRepository.save(MemberFixture.MEMBER_ROOM_MANAGER_JOYSON())));
interactionId = interaction.getId();
}

@Test
@DisplayName("알림을 체크하면 읽은 상태가 된다.")
void alarm_check() {
UserToUserAlarm alarm = userToUserAlarmRepository.save(AlarmFixture.REVIEW_COMPLETE(actor.getId(), receiver.getId(), interactionId));
//@formatter:off
RestAssured.given().auth().oauth2(tokenService.createAccessToken(receiver))
.body(new AlarmCheckRequest(alarm.getId(),"USER")).contentType(ContentType.JSON)
.when().post("/alarm/check")
.then().assertThat().statusCode(200);
//@formatter:on

alarm = userToUserAlarmRepository.findById(alarm.getId())
.get();
assertThat(alarm.isRead()).isTrue();
}
}
34 changes: 33 additions & 1 deletion backend/src/test/java/corea/alarm/service/AlarmServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@
import config.ServiceTest;
import corea.alarm.domain.UserToUserAlarm;
import corea.alarm.domain.UserToUserAlarmRepository;
import corea.alarm.dto.AlarmCheckRequest;
import corea.alarm.dto.AlarmCountResponse;
import corea.alarm.dto.AlarmResponse;
import corea.alarm.dto.AlarmResponses;
import corea.exception.CoreaException;
import corea.fixture.AlarmFixture;
import corea.fixture.MemberFixture;
import corea.fixture.RoomFixture;
import corea.member.domain.Member;
import corea.member.repository.MemberRepository;
import corea.room.domain.Room;
import corea.room.repository.RoomRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;

import static org.assertj.core.api.Assertions.assertThat;

Expand Down Expand Up @@ -78,7 +82,35 @@ void get_alarm() {
.usingElementComparatorIgnoringFields("createAt")
.containsExactly(
AlarmResponse.from(alarm2, actor, interaction),
AlarmResponse.from(alarm1, actor,interaction)
AlarmResponse.from(alarm1, actor, interaction)
);
}

@Test
@DisplayName("자신에게 해당된 알람이 아니면 예외를 발생한다.")
void throw_exception_when_not_receive_alarm() {
UserToUserAlarm alarm = userToUserAlarmRepository.save(AlarmFixture.REVIEW_COMPLETE(actor.getId(), receiver.getId(), interactionId));

Assertions.assertThatThrownBy(() -> alarmService.checkAlarm(actor.getId(), new AlarmCheckRequest(alarm.getId(), "USER")))
.isInstanceOf(CoreaException.class);
}

@Test
@DisplayName("자신에게 해당된 알람이 아니면 예외를 발생한다.")
void throw_exception_when_not_exist_alarm() {
userToUserAlarmRepository.save(AlarmFixture.REVIEW_COMPLETE(actor.getId(), receiver.getId(), interactionId));

Assertions.assertThatThrownBy(() -> alarmService.checkAlarm(receiver.getId(), new AlarmCheckRequest(-1, "USER")))
.isInstanceOf(CoreaException.class);
}

@Test
@Transactional
@DisplayName("알림 체크를 한다.")
void some() {
UserToUserAlarm alarm = userToUserAlarmRepository.save(AlarmFixture.REVIEW_COMPLETE(actor.getId(), receiver.getId(), interactionId));

alarmService.checkAlarm(receiver.getId(), new AlarmCheckRequest(alarm.getId(), "USER"));
assertThat(alarm.isRead()).isTrue();
}
}

0 comments on commit fb8e540

Please sign in to comment.