Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ dependencies {
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation 'com.h2database:h2'

// firebase
implementation 'com.google.firebase:firebase-admin:9.1.0'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.withtime.be.withtimebe.domain.member.alarm.factory;

import com.google.firebase.messaging.Message;
import jakarta.mail.internet.MimeMessage;
import jakarta.validation.constraints.NotNull;
import org.springframework.stereotype.Component;
import org.withtime.be.withtimebe.domain.member.alarm.service.AlarmSender;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
public class AlarmSenderFactory {

private static final Map<Class<?>, AlarmSender<?>> alarmSenderRepository = new ConcurrentHashMap<>();

public AlarmSenderFactory(@NotNull List<AlarmSender<?>> alarmSenders) {
alarmSenders.forEach(sender -> alarmSenderRepository.put(sender.supportedClass(), sender));
}

public <T> AlarmSender<T> getAlarmSender(Class<T> messageType) {
try {
@SuppressWarnings("unchecked")
AlarmSender<T> alarmSender = (AlarmSender<T>) alarmSenderRepository.get(messageType);
return alarmSender;
} catch (Exception e) {
return null;
}
}

public Class<?> getPushAlarmClass() {
return Message.class;
}

public Class<?> getEmailAlarmClass() {
return MimeMessage.class;
}

public Class<?> getSMSAlarmClass() {
return String.class;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.withtime.be.withtimebe.domain.member.alarm.firebase;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.withtime.be.withtimebe.global.data.FirebaseConfigData;

import java.io.ByteArrayInputStream;

@Slf4j
@Component
@RequiredArgsConstructor
public class FirebaseInitialization {

private final FirebaseConfigData firebaseConfigData;

@PostConstruct
public void initialize() {
try {
if (firebaseConfigData.isEnabled()) {
ByteArrayInputStream serviceAccountStream = new ByteArrayInputStream(firebaseConfigData.getConfig().getBytes());

FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(serviceAccountStream))
.build();

FirebaseApp.initializeApp(options);
}
} catch (Exception e) {
log.warn("Firebase Initialize", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.withtime.be.withtimebe.domain.member.alarm.generator;

import org.withtime.be.withtimebe.domain.member.dto.AlarmRequestDTO;
import org.withtime.be.withtimebe.domain.member.entity.Member;

public interface AlarmMessageGenerator<T> {
T generate(Member member, AlarmRequestDTO.SendAlarm request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.withtime.be.withtimebe.domain.member.alarm.generator;

import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import org.withtime.be.withtimebe.domain.member.dto.AlarmRequestDTO;
import org.withtime.be.withtimebe.domain.member.entity.Member;

@Component
@RequiredArgsConstructor
public class EmailSMTPAlarmMessageGenerator implements AlarmMessageGenerator<MimeMessage> {

private static final String TITLE_FORMAT = "[WithTime] %s: %s";
private final JavaMailSender javaMailSender;

@Override
public MimeMessage generate(Member member, AlarmRequestDTO.SendAlarm request) {
try {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, "UTF-8");
helper.setTo(member.getEmail());
helper.setSubject(String.format(TITLE_FORMAT, request.alarmType(), request.title()));
helper.setText(request.description());

return mimeMessage;
} catch (Exception e) {
return null;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.withtime.be.withtimebe.domain.member.alarm.generator;

import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.Notification;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.withtime.be.withtimebe.domain.member.dto.AlarmRequestDTO;
import org.withtime.be.withtimebe.domain.member.entity.Member;

@Component
@Transactional
public class FCMAlarmMessageGenerator implements AlarmMessageGenerator<Message> {

@Override
public Message generate(Member member, AlarmRequestDTO.SendAlarm request) {
return Message.builder()
.setNotification(toNotification(request))
.setToken(member.getDeviceToken())
.build();
}

private Notification toNotification(AlarmRequestDTO.SendAlarm request) {
return Notification.builder()
.setTitle(request.title())
.setBody(request.description())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.withtime.be.withtimebe.domain.member.alarm.sender;

import org.withtime.be.withtimebe.domain.member.entity.Member;

public interface AlarmSendUtil<T> {
void send(Member member, T message) throws Exception;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.withtime.be.withtimebe.domain.member.alarm.sender;

import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Component;
import org.withtime.be.withtimebe.domain.member.entity.Member;

@Component
@RequiredArgsConstructor
public class EmailSMTPAlarmSendUtil implements AlarmSendUtil<MimeMessage> {

private final JavaMailSender javaMailSender;

@Override
public void send(Member member, MimeMessage message) throws Exception {
javaMailSender.send(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.withtime.be.withtimebe.domain.member.alarm.sender;

import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.Message;
import org.springframework.stereotype.Component;
import org.withtime.be.withtimebe.domain.member.entity.Member;

@Component
public class FCMAlarmSendUtil implements AlarmSendUtil<Message> {

@Override
public void send(Member member, Message message) throws Exception {
FirebaseMessaging.getInstance().send(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.withtime.be.withtimebe.domain.member.alarm.service;

import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.withtime.be.withtimebe.domain.member.alarm.generator.AlarmMessageGenerator;
import org.withtime.be.withtimebe.domain.member.alarm.sender.AlarmSendUtil;
import org.withtime.be.withtimebe.domain.member.dto.AlarmRequestDTO;
import org.withtime.be.withtimebe.domain.member.entity.Member;

@Slf4j
@RequiredArgsConstructor
public abstract class AbstractAlarmSender<T> implements AlarmSender<T> {

private final AlarmMessageGenerator<T> alarmMessageGenerator;
private final AlarmSendUtil<T> alarmSendUtil;

@Override
public void send(Member member, AlarmRequestDTO.SendAlarm request) throws Exception {
try {
T message = alarmMessageGenerator.generate(member, request);

alarmSendUtil.send(member, message);
} catch (Exception e) {
handleException(e);
}
}

protected void handleException(Exception e) throws Exception{
log.warn("Alarm error", e);
throw e;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.withtime.be.withtimebe.domain.member.alarm.service;

import org.withtime.be.withtimebe.domain.member.dto.AlarmRequestDTO;
import org.withtime.be.withtimebe.domain.member.entity.Member;

public interface AlarmSender<T> {

void send(Member member, AlarmRequestDTO.SendAlarm request) throws Exception;
Class<T> supportedClass();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.withtime.be.withtimebe.domain.member.alarm.service;

import jakarta.mail.internet.MimeMessage;
import org.springframework.stereotype.Component;
import org.withtime.be.withtimebe.domain.member.alarm.generator.AlarmMessageGenerator;
import org.withtime.be.withtimebe.domain.member.alarm.sender.AlarmSendUtil;

@Component
public class EmailSMTPAlarmSender extends AbstractAlarmSender<MimeMessage> {

private static final Class<MimeMessage> SUPPORTED_CLASS = MimeMessage.class;

public EmailSMTPAlarmSender(AlarmMessageGenerator<MimeMessage> alarmMessageGenerator,
AlarmSendUtil<MimeMessage> alarmSendUtil) {
super(alarmMessageGenerator, alarmSendUtil);
}

@Override
public Class<MimeMessage> supportedClass() {
return SUPPORTED_CLASS;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.withtime.be.withtimebe.domain.member.alarm.service;

import com.google.firebase.messaging.Message;
import org.springframework.stereotype.Component;
import org.withtime.be.withtimebe.domain.member.alarm.generator.AlarmMessageGenerator;
import org.withtime.be.withtimebe.domain.member.alarm.sender.AlarmSendUtil;

@Component
public class FCMAlarmSender extends AbstractAlarmSender<Message> {

private static final Class<Message> SUPPORTED_CLASS= Message.class;

public FCMAlarmSender(AlarmMessageGenerator<Message> alarmMessageGenerator,
AlarmSendUtil<Message> alarmSendUtil) {
super(alarmMessageGenerator, alarmSendUtil);
}

@Override
public Class<Message> supportedClass() {
return SUPPORTED_CLASS;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.withtime.be.withtimebe.domain.member.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.namul.api.payload.response.DefaultResponse;
import org.springframework.web.bind.annotation.*;
import org.withtime.be.withtimebe.domain.member.converter.AlarmConverter;
import org.withtime.be.withtimebe.domain.member.dto.AlarmRequestDTO;
import org.withtime.be.withtimebe.domain.member.dto.AlarmResponseDTO;
import org.withtime.be.withtimebe.domain.member.entity.Member;
import org.withtime.be.withtimebe.domain.member.service.AlarmCommandService;
import org.withtime.be.withtimebe.domain.member.service.AlarmQueryService;
import org.withtime.be.withtimebe.global.security.annotation.AuthenticatedMember;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/alarms")
@Tag(name = "알림 API")
public class AlarmController {

private final AlarmCommandService alarmCommandService;
private final AlarmQueryService alarmQueryService;

@Operation(summary = "알림 테스트용 API by 요시", description = "알림 테스트하기 위해 생성한 API")
@ApiResponse(responseCode = "204", description = "알림 전송 성공, 해당 API는 일림 전송 실패로 따로 에러 메시지를 전송하지 않습니다.")
@PostMapping
public DefaultResponse<Void> alarm(@AuthenticatedMember Member member, @RequestBody AlarmRequestDTO.SendAlarm request) {
alarmCommandService.send(member, request);
return DefaultResponse.noContent();
}

@Operation(summary = "푸시알림 기기 업데이트 API by 요시", description = "푸시 알림 받을 기기에서 얻은 토큰을 적용하여 해당 기기로 받도록 하는 API")
@ApiResponse(responseCode = "204", description = "알림 받을 기기 변경에 성공했습니다.")
@PostMapping("/device-tokens")
public DefaultResponse<Void> updateDeviceToken(@AuthenticatedMember Member member, @RequestBody AlarmRequestDTO.UpdateDeviceToken request) {
alarmCommandService.updateDeviceToken(member, request);
return DefaultResponse.noContent();
}

@Operation(summary = "알림 설정 업데이트 API by 요시", description = "사용자의 알림 설정을 변경하는 API")
@ApiResponse(responseCode = "200", description = "알림 설정 변경에 성공하였습니다.")
@PatchMapping("/settings")
public DefaultResponse<AlarmResponseDTO.UpdateSetting> updateAlarmSetting(@AuthenticatedMember Member member, @RequestBody AlarmRequestDTO.UpdateSetting request) {
AlarmResponseDTO.UpdateSetting response = alarmCommandService.updateAlarmSetting(member, request);
return DefaultResponse.ok(response);
}

@Operation(summary = "알림 설정 조회 API by 요시", description = "사용자 알림 설정 상태를 조회합니다.")
@ApiResponse(responseCode = "200", description = "알림 설정 조회에 성공하였습니다.")
@GetMapping("/settings")
public DefaultResponse<AlarmResponseDTO.SettingInfo> findSettingInfo(@AuthenticatedMember Member member) {
return DefaultResponse.ok(AlarmConverter.toSettingInfo(member));
}

@Operation(summary = "알림 조회 API by 요시", description = "알림 조회 API")
@ApiResponse(responseCode = "200", description = "알림 조회에 성공했습니다.")
@GetMapping
public DefaultResponse<AlarmResponseDTO.FindAlarmList> findAlarms(@RequestParam(defaultValue = "10") Integer size,
@RequestParam(defaultValue = "0") Long cursor) {
return DefaultResponse.ok(alarmQueryService.findAlarms(cursor, size));
}
}
Loading