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

[feat #91] 알림 목록/읽기 API #97

Merged
merged 47 commits into from
Sep 2, 2024
Merged
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
b2b8eab
[rename] : Alis 네이밍 변경
dudxo Sep 1, 2024
8493feb
[rename] : targetUrl -> targetId 네이밍 변경
dudxo Sep 1, 2024
f0f7336
[feat] : NotificationRepository 추가
dudxo Sep 1, 2024
ed806c8
[feat] : NotificationService 추가
dudxo Sep 1, 2024
79ad567
[feat] : Notification 정적 팩토리 메서드 추가
dudxo Sep 1, 2024
6d18987
[feat] : Notification 저장 로직 추가
dudxo Sep 1, 2024
801e01a
[feat] : Notification ErrorCode 추가
dudxo Sep 1, 2024
6443a04
[feat] : NotificationType 정적 팩토리 메서드 내부 throw 예외 변경
dudxo Sep 1, 2024
f13b1e1
[feat] : 트랜잭션, 예외처리 추가
dudxo Sep 1, 2024
d433404
[feat] : 댓글 저장 시 알림 생성 로직 추가
dudxo Sep 1, 2024
a8aecd1
[feat] : 알림 trigger 회원 필드 추가
dudxo Sep 1, 2024
de8f491
[feat] : 알림 저장 로직 trigger 회원 추가
dudxo Sep 1, 2024
301cfc6
'[feat] : 댓글 알림 저장 시 trigger(댓글 작성자) 회원 추가
dudxo Sep 1, 2024
7d53932
[rename] : ANSWER label 네이밍 변경
dudxo Sep 1, 2024
bae47e5
[feat] : 채택 시 알림 생성 로직 추가
dudxo Sep 1, 2024
dd8fbd5
[test] : 알림 저장 비즈니스 로직 단위테스트 작성
dudxo Sep 1, 2024
d48d0f4
[feat] : 알림 목록 API 추가
dudxo Sep 1, 2024
5a2bbd3
[feat] : 알림 목록 응답 DTO 추가
dudxo Sep 1, 2024
e225af4
[feat] : 알림 목록 비즈니스 로직 추가
dudxo Sep 1, 2024
5b1e37b
[feat] : 알림 목록 관련 에러코드 추가
dudxo Sep 1, 2024
a5cf36e
[feat] : 알림 QueryDSL 환경 추가
dudxo Sep 1, 2024
1db1570
[feat] : 알림 목록 QueryDSL 구현(필터 기능 추가)
dudxo Sep 1, 2024
a0c5371
[rename] : 정적 메서드 컨번션에 따른 네이밍 변경(of -> from)
dudxo Sep 1, 2024
621ebac
[style] : NotificationServiceTest 패키지 구조 변경
dudxo Sep 1, 2024
a45a44d
[test] : NotificationFixture 추가
dudxo Sep 1, 2024
4f1c2d7
[test] : NotificationRepository 단위테스트 작성
dudxo Sep 1, 2024
099741b
[tes] : 알림 목록조회 통합테스트 추가
dudxo Sep 1, 2024
90eeb1e
[feat] : 알림 읽기 API 추가
dudxo Sep 1, 2024
ec7c8d9
[feat] : 알림 읽기 요청/응답 DTO 추가
dudxo Sep 1, 2024
235779a
[feat] : 알림 DTO Mapper 추가
dudxo Sep 1, 2024
76ca070
[feat] : 알림 읽기 비즈니스 로직 추가
dudxo Sep 1, 2024
e7df8db
[feat] : 알림 읽기 관련 에러코드 추가
dudxo Sep 1, 2024
e0887ce
[feat] : 알림 읽기 상태변환 로직 추가
dudxo Sep 1, 2024
4a8a78e
[rename] : 알림 읽기 API PATH 변경
dudxo Sep 1, 2024
d54ab40
[fix] : 알림 읽기 비즈니스 로직 중 에러코드 오류 수정
dudxo Sep 1, 2024
85e0fda
[feat] : 알림 읽기 요청 DTO Validation 추가
dudxo Sep 1, 2024
150316c
[feat] : 해당 알림이 요청 회원 알림인지 검증하는 로직 추가
dudxo Sep 1, 2024
738d580
[test] : 알림 읽기 비즈니스 로직 단위 테스트 추가
dudxo Sep 1, 2024
a27a285
[test] : 알림 읽기 API 통합 테스트 추가
dudxo Sep 1, 2024
a9e2a8e
[feat] : 알림 소유주 관련 에러코드 추가
dudxo Sep 1, 2024
e73f1af
[test] : 단위테스트용 Fixture 변경
dudxo Sep 2, 2024
b9e4a4c
[refactor] : 알림 타입 String이 아닌 Enum으로 바로 받도록 수정
dudxo Sep 2, 2024
2d760b5
[test] : 본 로직 수정에 따른 테스트 코드 수정
dudxo Sep 2, 2024
dfdfccf
[rename] : 알림 읽음 여부 관련 DTO, Method 네이밍 변경
dudxo Sep 2, 2024
b7b28ee
[rename] : Method 네이밍 변경
dudxo Sep 2, 2024
5f39f54
[test] : 본 로직 method 네이밍 변경에 따른 수정
dudxo Sep 2, 2024
d3f666b
[test] : NotificationService mock 추가
dudxo Sep 2, 2024
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
14 changes: 13 additions & 1 deletion src/main/java/com/dnd/gongmuin/answer/service/AnswerService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.dnd.gongmuin.answer.service;

import static com.dnd.gongmuin.notification.domain.NotificationType.*;

import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -16,6 +18,7 @@
import com.dnd.gongmuin.common.exception.runtime.ValidationException;
import com.dnd.gongmuin.credit_history.service.CreditHistoryService;
import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.notification.service.NotificationService;
import com.dnd.gongmuin.question_post.domain.QuestionPost;
import com.dnd.gongmuin.question_post.exception.QuestionPostErrorCode;
import com.dnd.gongmuin.question_post.repository.QuestionPostRepository;
@@ -29,6 +32,7 @@ public class AnswerService {
private final QuestionPostRepository questionPostRepository;
private final AnswerRepository answerRepository;
private final CreditHistoryService creditHistoryService;
private final NotificationService notificationService;

private static void validateIfQuestioner(Member member, QuestionPost questionPost) {
if (!questionPost.isQuestioner(member.getId())) {
@@ -45,7 +49,12 @@ public AnswerDetailResponse registerAnswer(
QuestionPost questionPost = findQuestionPostById(questionPostId);
Answer answer = AnswerMapper.toAnswer(questionPostId, questionPost.isQuestioner(member.getId()), request,
member);
return AnswerMapper.toAnswerDetailResponse(answerRepository.save(answer));
Answer savedAnswer = answerRepository.save(answer);

notificationService.saveNotificationFromTarget(
ANSWER, questionPost.getId(), member.getId(), questionPost.getMember()
);
return AnswerMapper.toAnswerDetailResponse(savedAnswer);
}

@Transactional(readOnly = true)
@@ -66,6 +75,9 @@ public AnswerDetailResponse chooseAnswer(
QuestionPost questionPost = findQuestionPostById(answer.getQuestionPostId());
validateIfQuestioner(member, questionPost);
chooseAnswer(questionPost, answer);
notificationService.saveNotificationFromTarget(
CHOSEN, questionPost.getId(), member.getId(), answer.getMember()
);

return AnswerMapper.toAnswerDetailResponse(answer);
}
Original file line number Diff line number Diff line change
@@ -43,8 +43,8 @@ public Slice<QuestionPostsResponse> getQuestionPostsByMember(Member member, Page
List<QuestionPostsResponse> content = queryFactory
.select(new QQuestionPostsResponse(
qp,
saved.count.coalesce(0).as("savedTotalCount"),
recommend.count.coalesce(0).as("recommendTotalCount")
saved.count.coalesce(0).as("bookmarkCount"),
recommend.count.coalesce(0).as("recommendCount")
))
.from(qp)
.leftJoin(saved)
@@ -75,8 +75,8 @@ public Slice<AnsweredQuestionPostsResponse> getAnsweredQuestionPostsByMember(
queryFactory
.select(new QAnsweredQuestionPostsResponse(
qp,
saved.count.coalesce(0).as("savedTotalCount"),
recommend.count.coalesce(0).as("recommendTotalCount"),
saved.count.coalesce(0).as("bookmarkCount"),
recommend.count.coalesce(0).as("recommendCount"),
aw1
))
.from(qp)
@@ -119,8 +119,8 @@ public Slice<BookmarksResponse> getBookmarksByMember(Member member, Pageable pag
List<BookmarksResponse> content = queryFactory
.select(new QBookmarksResponse(
qp,
saved.count.coalesce(0).as("savedTotalCount"),
recommend.count.coalesce(0).as("recommendTotalCount")
saved.count.coalesce(0).as("bookmarkCount"),
recommend.count.coalesce(0).as("recommendCount")
))
.from(qp)
.join(ir)
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.dnd.gongmuin.notification.controller;

import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.dnd.gongmuin.common.dto.PageResponse;
import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.notification.dto.request.readNotificationRequest;
import com.dnd.gongmuin.notification.dto.response.NotificationsResponse;
import com.dnd.gongmuin.notification.dto.response.readNotificationResponse;
import com.dnd.gongmuin.notification.service.NotificationService;

import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@Tag(name = "알림 API", description = "알림 API")
@RestController
@RequiredArgsConstructor
public class NotificationController {

private final NotificationService notificationService;

@GetMapping("/api/notifications")
public ResponseEntity<PageResponse<NotificationsResponse>> getNotificationsByMember(
@RequestParam("type") String type,
@AuthenticationPrincipal Member member,
Pageable pageable) {

PageResponse<NotificationsResponse> response =
notificationService.getNotificationsByMember(type, member, pageable);

return ResponseEntity.ok(response);
}

@PatchMapping("/api/notification/read")
public ResponseEntity<readNotificationResponse> readNotification(
@RequestBody @Valid readNotificationRequest request,
@AuthenticationPrincipal Member member
) {
readNotificationResponse response = notificationService.readNotification(request, member);

return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@

import static jakarta.persistence.ConstraintMode.*;
import static jakarta.persistence.FetchType.*;
import static lombok.AccessLevel.*;

import com.dnd.gongmuin.common.entity.TimeBaseEntity;
import com.dnd.gongmuin.member.domain.Member;
@@ -38,20 +39,47 @@ public class Notification extends TimeBaseEntity {
@Column(name = "is_read", nullable = false)
private Boolean isRead;

@Column(name = "target_url")
private String targetUrl;
@Column(name = "target_id")
private Long targetId;

@Column(name = "trigger_member_id")
private Long triggerMemberId;

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "member_id",
nullable = false,
foreignKey = @ForeignKey(NO_CONSTRAINT))
private Member member;

@Builder
public Notification(NotificationType type, Boolean isRead, String targetUrl, Member member) {
@Builder(access = PRIVATE)
private Notification(
NotificationType type,
Boolean isRead,
Long targetId,
Long triggerMemberId,
Member member) {
this.type = type;
this.isRead = isRead;
this.targetUrl = targetUrl;
this.targetId = targetId;
this.triggerMemberId = triggerMemberId;
this.member = member;
}

public static Notification of(
NotificationType type,
Long targetId,
Long triggerMemberId,
Member member) {
return Notification.builder()
.type(type)
.isRead(false)
.targetId(targetId)
.triggerMemberId(triggerMemberId)
.member(member)
.build();
}

public void updateIsReadTrue() {
this.isRead = Boolean.TRUE;
}
}
Original file line number Diff line number Diff line change
@@ -2,24 +2,27 @@

import java.util.Arrays;

import com.dnd.gongmuin.common.exception.runtime.NotFoundException;
import com.dnd.gongmuin.notification.exception.NotificationErrorCode;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum NotificationType {

ANSWER("댓글"),
ANSWER("답변"),
CHOSEN("채택"),
CHAT("채팅");

private final String label;

public static NotificationType of(String input) {
public static NotificationType from(String input) {
return Arrays.stream(values())
.filter(type -> type.isEqual(input))
.findAny()
.orElseThrow(IllegalArgumentException::new);
.orElseThrow(() -> new NotFoundException(NotificationErrorCode.NOT_FOUND_NOTIFICATION_TYPE));
}

private boolean isEqual(String input) {
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.dnd.gongmuin.notification.dto;

import com.dnd.gongmuin.notification.domain.Notification;
import com.dnd.gongmuin.notification.dto.response.readNotificationResponse;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class NotificationMapper {

public static readNotificationResponse toIsReadNotificationResponse(Notification notification) {
return new readNotificationResponse(
notification.getId(),
notification.getIsRead()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.dnd.gongmuin.notification.dto.request;

import jakarta.validation.constraints.NotNull;

public record readNotificationRequest(

@NotNull(message = "알림 ID 값은 필수 값 입니다.")
Long notificationId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.dnd.gongmuin.notification.dto.response;

import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.notification.domain.Notification;
import com.querydsl.core.annotations.QueryProjection;

public record NotificationsResponse(

Long notificationId,

String type,

Boolean isRead,

Long targetId,

Long triggerMemberId,

String triggerMemberNickName,

Long targetMemberId,

String NotificationCreatedAt
) {

@QueryProjection
public NotificationsResponse(
Notification notification,
Member triggerMember
) {
this(
notification.getId(),
notification.getType().getLabel(),
notification.getIsRead(),
notification.getTargetId(),
triggerMember.getId(),
triggerMember.getNickname(),
notification.getMember().getId(),
notification.getCreatedAt().toString()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.dnd.gongmuin.notification.dto.response;

public record readNotificationResponse(
Long notificationId,
Boolean isRead
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.dnd.gongmuin.notification.exception;

import com.dnd.gongmuin.common.exception.ErrorCode;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum NotificationErrorCode implements ErrorCode {

NOT_FOUND_NOTIFICATION_TYPE("알맞은 알림 타입을 찾을 수 없습니다.", "NOTIFICATION_001"),
SAVE_NOTIFICATION_FAILED("알림 저장을 실패했습니다.", "NOTIFICATION_002"),
NOTIFICATIONS_BY_MEMBER_FAILED("알림 목록을 불러오는데 실패했습니다.", "NOTIFICATION_003"),
NOT_FOUND_NOTIFICATION("해당 알림을 찾을 수 없습니다", "NOTIFICATION_004"),
CHANGE_IS_READ_NOTIFICATION_FAILED("해당 알림의 읽음 여부 변경을 실패했습니다.", "NOTIFICATION_005"),
INVALID_NOTIFICATION_OWNER("해당 알림의 주인이 아닙니다.", "NOTIFICATION_006");

private final String message;
private final String code;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.dnd.gongmuin.notification.repository;

import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;

import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.notification.dto.response.NotificationsResponse;

public interface NotificationCustom {

Slice<NotificationsResponse> getNotificationsByMember(String type, Member member, Pageable pageable);

}
Loading