Skip to content
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
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