Skip to content

Commit

Permalink
Merge pull request #192 from PawWithU/feat/189-notification-send-and-…
Browse files Browse the repository at this point in the history
…save

[Feature] 알림 전송 구현 및 알림 목록 조회를 위한 Entity 생성
  • Loading branch information
kyeong-hyeok authored May 13, 2024
2 parents c84343e + 5202ae0 commit 57a8d4d
Show file tree
Hide file tree
Showing 11 changed files with 225 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,28 @@
import com.pawwithu.connectdog.domain.application.entity.ApplicationStatus;
import com.pawwithu.connectdog.domain.application.repository.ApplicationRepository;
import com.pawwithu.connectdog.domain.application.repository.CustomApplicationRepository;
import com.pawwithu.connectdog.domain.dogStatus.repository.DogStatusRepository;
import com.pawwithu.connectdog.domain.fcm.entity.IntermediaryFcm;
import com.pawwithu.connectdog.domain.fcm.entity.VolunteerFcm;
import com.pawwithu.connectdog.domain.fcm.repository.IntermediaryFcmRepository;
import com.pawwithu.connectdog.domain.fcm.repository.VolunteerFcmRepository;
import com.pawwithu.connectdog.domain.fcm.service.FcmService;
import com.pawwithu.connectdog.domain.intermediary.entity.Intermediary;
import com.pawwithu.connectdog.domain.intermediary.repository.IntermediaryRepository;
import com.pawwithu.connectdog.domain.post.entity.Post;
import com.pawwithu.connectdog.domain.post.entity.PostStatus;
import com.pawwithu.connectdog.domain.post.repository.CustomPostRepository;
import com.pawwithu.connectdog.domain.post.repository.PostRepository;
import com.pawwithu.connectdog.domain.review.repository.ReviewRepository;
import com.pawwithu.connectdog.domain.volunteer.entity.Volunteer;
import com.pawwithu.connectdog.domain.volunteer.repository.VolunteerRepository;
import com.pawwithu.connectdog.error.exception.custom.BadRequestException;
import jakarta.persistence.LockModeType;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

import static com.pawwithu.connectdog.domain.fcm.dto.NotificationMessage.*;
import static com.pawwithu.connectdog.error.ErrorCode.*;

@Slf4j
Expand All @@ -43,6 +41,9 @@ public class ApplicationService {
private final ApplicationRepository applicationRepository;
private final CustomApplicationRepository customApplicationRepository;
private final IntermediaryRepository intermediaryRepository;
private final VolunteerFcmRepository volunteerFcmRepository;
private final IntermediaryFcmRepository intermediaryFcmRepository;
private final FcmService fcmService;

public void volunteerApply(String email, Long postId, VolunteerApplyRequest request) {
// 이동봉사자
Expand All @@ -51,18 +52,22 @@ public void volunteerApply(String email, Long postId, VolunteerApplyRequest requ
Post post = postRepository.findById(postId).orElseThrow(() -> new BadRequestException(POST_NOT_FOUND));
// 이동봉사 중개
Intermediary intermediary = post.getIntermediary();

// 해당 공고에 대한 신청이 이미 존재할 경우 - 신청 상태가 반려가 아닐 경우
if (customApplicationRepository.existsByPostIdAndPostStatus(postId)) {
throw new BadRequestException(ALREADY_EXIST_APPLICATION);
}

// 공고 신청 저장
Application application = request.toEntity(post, intermediary, volunteer);
applicationRepository.save(application);

// 공고 상태 승인 대기 중으로 변경
post.updateStatus(PostStatus.WAITING);
// 알림 전송
IntermediaryFcm intermediaryFcm = intermediaryFcmRepository.findByIntermediaryId(intermediary.getId()).orElse(null);
if (intermediaryFcm != null) {
fcmService.sendMessageToIntermediary(intermediaryFcm.getFcmToken(), intermediary, volunteer.getProfileImageNum() + "", APPLICATION.getTitle(), APPLICATION.getBodyWithName(volunteer.getNickname()));
} else {
log.info("----------이동봉사 신청 알림 전송 실패----------");
}
}

@Transactional(readOnly = true)
Expand Down Expand Up @@ -100,6 +105,13 @@ public ApplicationSuccessResponse deleteApplication(String email, Long applicati
// 상태 업데이트 (승인 대기중 -> 모집중)
Post post = application.getPost();
post.updateStatus(PostStatus.RECRUITING);
// 알림 전송
IntermediaryFcm intermediaryFcm = intermediaryFcmRepository.findByIntermediaryId(application.getIntermediary().getId()).orElse(null);
if (intermediaryFcm != null) {
fcmService.sendMessageToIntermediary(intermediaryFcm.getFcmToken(), application.getIntermediary(), volunteer.getProfileImageNum() + "", CANCELED.getTitle(), CANCELED.getBodyWithName(volunteer.getNickname()));
} else {
log.info("----------이동봉사 신청 취소 알림 전송 실패----------");
}
ApplicationSuccessResponse isSuccess = ApplicationSuccessResponse.of(true);
return isSuccess;
}
Expand All @@ -113,6 +125,13 @@ public ApplicationSuccessResponse confirmApplication(String email, Long applicat
// 상태 업데이트 (승인 대기중 -> 진행중)
application.updateStatus(ApplicationStatus.PROGRESSING);
post.updateStatus(PostStatus.PROGRESSING);
// 알림 전송
VolunteerFcm volunteerFcm = volunteerFcmRepository.findByVolunteerId(application.getVolunteer().getId()).orElse(null);
if (volunteerFcm != null) {
fcmService.sendMessageToVolunteer(volunteerFcm.getFcmToken(), application.getVolunteer(), post.getMainImage().getImage(), CONFIRM.getTitle(), CONFIRM.getBody());
} else {
log.info("----------이동봉사 승인 알림 전송 실패----------");
}
ApplicationSuccessResponse isSuccess = ApplicationSuccessResponse.of(true);
return isSuccess;
}
Expand All @@ -126,6 +145,13 @@ public ApplicationSuccessResponse cancelApplication(String email, Long applicati
// 상태 업데이트 (승인 대기중 -> 모집중)
post.updateStatus(PostStatus.RECRUITING);
application.updateStatus(ApplicationStatus.REJECTED);
// 알림 전송
VolunteerFcm volunteerFcm = volunteerFcmRepository.findByVolunteerId(application.getVolunteer().getId()).orElse(null);
if (volunteerFcm != null) {
fcmService.sendMessageToVolunteer(volunteerFcm.getFcmToken(), application.getVolunteer(), post.getMainImage().getImage(), REJECT.getTitle(), REJECT.getBody());
} else {
log.info("----------이동봉사 반려 알림 전송 실패----------");
}
ApplicationSuccessResponse isSuccess = ApplicationSuccessResponse.of(true);
return isSuccess;
}
Expand Down Expand Up @@ -189,6 +215,13 @@ public ApplicationSuccessResponse completeApplication(String email, Long applica
// 상태 업데이트 (진행중 -> 봉사 완료)
application.updateStatus(ApplicationStatus.COMPLETED);
post.updateStatus(PostStatus.COMPLETED);
// 알림 전송
VolunteerFcm volunteerFcm = volunteerFcmRepository.findByVolunteerId(application.getVolunteer().getId()).orElse(null);
if (volunteerFcm != null) {
fcmService.sendMessageToVolunteer(volunteerFcm.getFcmToken(), application.getVolunteer(), post.getMainImage().getImage(), COMPLETED.getTitle(), COMPLETED.getBody());
} else {
log.info("----------이동봉사 완료 알림 전송 실패----------");
}
ApplicationSuccessResponse isSuccess = ApplicationSuccessResponse.of(true);
return isSuccess;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public ResponseEntity<Void> saveIntermediaryFcm(@AuthenticationPrincipal UserDet
})
@PostMapping("/fcm-test")
public ResponseEntity<Void> testFcmToken(@Valid @RequestBody FcmTokenRequest request) {
fcmService.sendMessageTo(request.fcmToken(), APPLICATION.getTitleWithLoc("서울 강남구", "서울 도봉구"), APPLICATION.getBodyWithName("포윗유"));
fcmService.sendMessageToVolunteer(request.fcmToken(), null, null, APPLICATION.getTitleWithLoc("서울 강남구", "서울 도봉구"), APPLICATION.getBodyWithName("포윗유"));
return ResponseEntity.noContent().build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,18 @@
@Getter
@RequiredArgsConstructor
public enum NotificationMessage {
APPLICATION("", "님이 이동봉사를 신청하셨어요. 지금 확인해 보세요!");
// 봉사
GUIDE("이동봉사 시작하기", "이동봉사, 어떻게 하는 건가요?\n코넥독이 이동봉사 가이드를 준비했어요!"),
CONFIRM("이동봉사 승인", "이동봉사가 승인되었어요🎉\n모집자의 연락을 기다려 주세요!"),
REJECT("이동봉사 반려", "이동봉사가 반려되었어요😥\n다른 이동봉사를 찾아볼까요?"),
COMPLETED("이동봉사 완료", "이동봉사 진행이 완료되었어요🐾\n소중한 후기를 들려주세요!"),

// 모집자
APPLICATION("이동봉사 신청", "님이 이동봉사를 신청하셨어요.\n지금 확인해 보세요!"),
CANCELED("이동봉사 신청 취소", "님이 이동봉사를 취소하셨어요.\n해당 공고는 모집중 상태로 변경됩니다."),
REVIEW_REGISTERED("이동봉사 후기 등록", "봉사 후기가 등록되었습니다.\n지금 확인해 보세요!"),
EXPIRED("이동봉사 모집 기간 만료", "모집 기간 만료로 공고가 마감되었습니다.\n아직 봉사자를 구하지 못했다면 기간을 조정해 보세요!"),
COMPLETED_REQUEST("이동봉사 진행 완료", "이동봉사 진행이 완료되었나요?\n봉사 완료 버튼을 눌러주세요!");

private final String title;
private final String body;
Expand All @@ -15,8 +26,8 @@ public String getTitleWithLoc(String departureLoc, String arrivalLoc) {
return departureLoc + "→" + arrivalLoc;
}

public String getBodyWithName(String name) {
return name + body;
public String getBodyWithName(String nickname) {
return nickname + body;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import com.pawwithu.connectdog.domain.fcm.entity.IntermediaryFcm;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface IntermediaryFcmRepository extends JpaRepository<IntermediaryFcm, Long> {
void deleteByIntermediaryId(Long id);
Optional<IntermediaryFcm> findByIntermediaryId(Long id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import com.pawwithu.connectdog.domain.fcm.entity.VolunteerFcm;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface VolunteerFcmRepository extends JpaRepository<VolunteerFcm, Long> {
void deleteByVolunteerId(Long id);
Optional<VolunteerFcm> findByVolunteerId(Long id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
import com.pawwithu.connectdog.domain.fcm.repository.VolunteerFcmRepository;
import com.pawwithu.connectdog.domain.intermediary.entity.Intermediary;
import com.pawwithu.connectdog.domain.intermediary.repository.IntermediaryRepository;
import com.pawwithu.connectdog.domain.notification.entity.IntermediaryNotification;
import com.pawwithu.connectdog.domain.notification.entity.VolunteerNotification;
import com.pawwithu.connectdog.domain.notification.repository.IntermediaryNotificationRepository;
import com.pawwithu.connectdog.domain.notification.repository.VolunteerNotificationRepository;
import com.pawwithu.connectdog.domain.volunteer.entity.Volunteer;
import com.pawwithu.connectdog.domain.volunteer.repository.VolunteerRepository;
import com.pawwithu.connectdog.error.exception.custom.BadRequestException;
Expand All @@ -32,6 +36,7 @@
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class FcmService {

@Value("${fcm.config.path}")
Expand All @@ -45,8 +50,11 @@ public class FcmService {
private final VolunteerFcmRepository volunteerFcmRepository;
private final IntermediaryRepository intermediaryRepository;
private final IntermediaryFcmRepository intermediaryFcmRepository;
private final VolunteerNotificationRepository volunteerNotificationRepository;
private final IntermediaryNotificationRepository intermediaryNotificationRepository;

private String getAccessToken() throws IOException {
@Transactional(readOnly = true)
public String getAccessToken() throws IOException {

// firebase로 부터 access token을 가져온다.
GoogleCredentials googleCredentials = GoogleCredentials
Expand Down Expand Up @@ -89,7 +97,7 @@ public String makeMessage(String targetToken, String title, String body) throws
* 알림 푸쉬를 보내는 역할을 하는 메서드
* @param targetToken : 푸쉬 알림을 받을 클라이언트 앱의 식별 토큰
* */
public void sendMessageTo(String targetToken, String title, String body) {
public void sendMessageToVolunteer(String targetToken, Volunteer volunteer, String image, String title, String body) {

try {
String message = makeMessage(targetToken, title, body);
Expand All @@ -105,6 +113,56 @@ public void sendMessageTo(String targetToken, String title, String body) {
.build();

Response response = client.newCall(request).execute();

// 알림 저장
volunteerNotificationRepository.save(
VolunteerNotification.builder()
.image(image)
.title(title)
.body(body)
.volunteer(volunteer)
.isRead(false)
.build()
);

if (!response.isSuccessful()) {
log.error("FCM 푸시 알람 전송이 실패했습니다. 응답 코드: {}\n{}", response.code(), response.body().string());
}
}
catch (Exception e) {
log.error("Fcm 푸시 알람을 전송하는 도중에 에러가 발생했습니다. {}", e.getMessage());
throw new BadRequestException(NOTIFICATION_SEND_ERROR);
}
}

public void sendMessageToIntermediary(String targetToken, Intermediary intermediary, String image, String title, String body) {

try {
String message = makeMessage(targetToken, title, body);

OkHttpClient client = new OkHttpClient();
RequestBody requestBody = RequestBody.create(message, MediaType.get("application/json; charset=utf-8"));

Request request = new Request.Builder()
.url(FIREBASE_API_URL)
.post(requestBody)
.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + getAccessToken())
.addHeader(HttpHeaders.CONTENT_TYPE, "application/json; UTF-8")
.build();

Response response = client.newCall(request).execute();

// 알림 저장
intermediaryNotificationRepository.save(
IntermediaryNotification.builder()
.image(image)
.title(title)
.body(body)
.intermediary(intermediary)
.isRead(false)
.build()
);

if (!response.isSuccessful()) {
log.error("FCM 푸시 알람 전송이 실패했습니다. 응답 코드: {}\n{}", response.code(), response.body().string());
}
Expand All @@ -113,17 +171,14 @@ public void sendMessageTo(String targetToken, String title, String body) {
log.error("Fcm 푸시 알람을 전송하는 도중에 에러가 발생했습니다. {}", e.getMessage());
throw new BadRequestException(NOTIFICATION_SEND_ERROR);
}
return;
}

@Transactional
public void saveVolunteerFcm(String email, VolunteerFcmRequest request) {
Volunteer volunteer = volunteerRepository.findByEmail(email).orElseThrow(() -> new BadRequestException(VOLUNTEER_NOT_FOUND));
VolunteerFcm volunteerFcm = VolunteerFcmRequest.volunteerToEntity(volunteer, request);
volunteerFcmRepository.save(volunteerFcm);
}

@Transactional
public void saveIntermediaryFcm(String email, IntermediaryFcmRequest request) {
Intermediary intermediary = intermediaryRepository.findByEmail(email).orElseThrow(() -> new BadRequestException(INTERMEDIARY_NOT_FOUND));
IntermediaryFcm intermediaryFcm = IntermediaryFcmRequest.IntermediaryToEntity(intermediary, request);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.pawwithu.connectdog.domain.notification.entity;

import com.pawwithu.connectdog.common.entity.BaseTimeEntity;
import com.pawwithu.connectdog.domain.intermediary.entity.Intermediary;
import jakarta.persistence.*;
import lombok.*;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Entity
public class IntermediaryNotification extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String image;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String body;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "intermediary_id", nullable = false)
private Intermediary intermediary; // 이동봉사 중개 id
@Column(nullable = false)
private Boolean isRead;

@Builder
public IntermediaryNotification(String image, String title, String body, Intermediary intermediary, Boolean isRead) {
this.image = image;
this.title = title;
this.body = body;
this.intermediary = intermediary;
this.isRead = isRead;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.pawwithu.connectdog.domain.notification.entity;

import com.pawwithu.connectdog.common.entity.BaseTimeEntity;
import com.pawwithu.connectdog.domain.volunteer.entity.Volunteer;
import jakarta.persistence.*;
import lombok.*;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Entity
public class VolunteerNotification extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String image;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String body;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "volunteer_id", nullable = false)
private Volunteer volunteer; // 이동봉사자 id
@Column(nullable = false)
private Boolean isRead;

@Builder
public VolunteerNotification(String image, String title, String body, Volunteer volunteer, Boolean isRead) {
this.image = image;
this.title = title;
this.body = body;
this.volunteer = volunteer;
this.isRead = isRead;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.pawwithu.connectdog.domain.notification.repository;

import com.pawwithu.connectdog.domain.notification.entity.IntermediaryNotification;
import org.springframework.data.jpa.repository.JpaRepository;

public interface IntermediaryNotificationRepository extends JpaRepository<IntermediaryNotification, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.pawwithu.connectdog.domain.notification.repository;

import com.pawwithu.connectdog.domain.notification.entity.VolunteerNotification;
import org.springframework.data.jpa.repository.JpaRepository;

public interface VolunteerNotificationRepository extends JpaRepository<VolunteerNotification, Long> {
}
Loading

0 comments on commit 57a8d4d

Please sign in to comment.