From 29740e96f0bd6406fddf9ccbda609ab527aff5ab Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sat, 6 Jul 2024 18:18:51 +0900 Subject: [PATCH 01/62] =?UTF-8?q?chore:=20Firebase=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index df9723ad..75707650 100644 --- a/build.gradle +++ b/build.gradle @@ -77,6 +77,9 @@ dependencies { // actuator, prometheus runtimeOnly 'io.micrometer:micrometer-registry-prometheus' implementation 'org.springframework.boot:spring-boot-starter-actuator' + + // Firebase + implementation 'com.google.firebase:firebase-admin:8.0.0' } def generated = 'src/main/generated' From 128e3a61fee519c164fb8674ab83160c0e81c9d8 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sat, 6 Jul 2024 18:20:54 +0900 Subject: [PATCH 02/62] =?UTF-8?q?chore:=20Firebase=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Firebase 설정 파일 환경 등록 --- .../atwoz/alert/config/FirebaseConfig.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/main/java/com/atwoz/alert/config/FirebaseConfig.java diff --git a/src/main/java/com/atwoz/alert/config/FirebaseConfig.java b/src/main/java/com/atwoz/alert/config/FirebaseConfig.java new file mode 100644 index 00000000..24b48702 --- /dev/null +++ b/src/main/java/com/atwoz/alert/config/FirebaseConfig.java @@ -0,0 +1,31 @@ +package com.atwoz.alert.config; + +import com.atwoz.alert.exception.exceptions.FirebaseFileNotFoundException; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.FileInputStream; +import java.io.IOException; + +@Configuration +public class FirebaseConfig { + + private static final String FIREBASE_URL = "src/main/resources/firebase/firebase_key.json"; + + @Bean + public FirebaseApp firebaseApp() { + try { + FileInputStream firebaseFile = new FileInputStream(FIREBASE_URL); + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(GoogleCredentials.fromStream(firebaseFile)) + .build(); + + return FirebaseApp.initializeApp(options); + } catch (IOException e) { + throw new FirebaseFileNotFoundException(); + } + } +} From 734f7f97a481e1ce7303eb43576ef7b4cab80b98 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sat, 6 Jul 2024 18:23:23 +0900 Subject: [PATCH 03/62] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EA=B8=B0=EC=B4=88=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알림 도메인 기초 설계 작성 - 관련 예외 핸들러 등록 --- .../java/com/atwoz/alert/domain/Alert.java | 67 +++++++++++++++++++ .../com/atwoz/alert/domain/vo/AlertGroup.java | 32 +++++++++ .../atwoz/alert/domain/vo/AlertMessage.java | 24 +++++++ .../exception/AlertExceptionHandler.java | 27 ++++++++ .../AlertGroupNotFoundException.java | 8 +++ .../exceptions/AlertSendException.java | 8 +++ .../FirebaseFileNotFoundException.java | 8 +++ 7 files changed, 174 insertions(+) create mode 100644 src/main/java/com/atwoz/alert/domain/Alert.java create mode 100644 src/main/java/com/atwoz/alert/domain/vo/AlertGroup.java create mode 100644 src/main/java/com/atwoz/alert/domain/vo/AlertMessage.java create mode 100644 src/main/java/com/atwoz/alert/exception/AlertExceptionHandler.java create mode 100644 src/main/java/com/atwoz/alert/exception/exceptions/AlertGroupNotFoundException.java create mode 100644 src/main/java/com/atwoz/alert/exception/exceptions/AlertSendException.java create mode 100644 src/main/java/com/atwoz/alert/exception/exceptions/FirebaseFileNotFoundException.java diff --git a/src/main/java/com/atwoz/alert/domain/Alert.java b/src/main/java/com/atwoz/alert/domain/Alert.java new file mode 100644 index 00000000..87d6968c --- /dev/null +++ b/src/main/java/com/atwoz/alert/domain/Alert.java @@ -0,0 +1,67 @@ +package com.atwoz.alert.domain; + +import com.atwoz.alert.domain.vo.AlertGroup; +import com.atwoz.alert.domain.vo.AlertMessage; +import com.atwoz.global.domain.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Alert extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Boolean isRead; + + @Column(nullable = false) + @Enumerated + private AlertGroup alertGroup; + + @Embedded + @Column(nullable = false) + private AlertMessage alertMessage; + + @Column(nullable = false) + private String token; + + private Alert(final AlertGroup alertGroup, final AlertMessage alertMessage, final String token) { + this.alertGroup = alertGroup; + this.alertMessage = alertMessage; + this.token = token; + } + + public static Alert createWith(final String group, final String title, final String body, final String token) { + AlertGroup alertGroup = AlertGroup.findByName(group); + AlertMessage message = AlertMessage.createWith(title, body); + return new Alert(alertGroup, message, token); + } + + public void read() { + this.isRead = true; + } + + public String getTitle() { + return alertMessage.getTitle(); + } + + public String getBody() { + return alertMessage.getBody(); + } + + public String getGroup() { + return alertGroup.getName(); + } +} diff --git a/src/main/java/com/atwoz/alert/domain/vo/AlertGroup.java b/src/main/java/com/atwoz/alert/domain/vo/AlertGroup.java new file mode 100644 index 00000000..23fbf40f --- /dev/null +++ b/src/main/java/com/atwoz/alert/domain/vo/AlertGroup.java @@ -0,0 +1,32 @@ +package com.atwoz.alert.domain.vo; + +import com.atwoz.alert.exception.exceptions.AlertGroupNotFoundException; +import lombok.Getter; + +import java.util.Arrays; + +@Getter +public enum AlertGroup { + + MEMBER_LIKE("좋아요"), + SELF_INTRODUCE("셀프소개"), + INTERVIEW("인터뷰"), + ALERT("알림"); + + private String name; + + AlertGroup(final String name) { + this.name = name; + } + + public static AlertGroup findByName(final String name) { + return Arrays.stream(values()) + .filter(group -> group.isSame(name)) + .findAny() + .orElseThrow(AlertGroupNotFoundException::new); + } + + private boolean isSame(final String name) { + return name.equals(this.name); + } +} diff --git a/src/main/java/com/atwoz/alert/domain/vo/AlertMessage.java b/src/main/java/com/atwoz/alert/domain/vo/AlertMessage.java new file mode 100644 index 00000000..660d5d63 --- /dev/null +++ b/src/main/java/com/atwoz/alert/domain/vo/AlertMessage.java @@ -0,0 +1,24 @@ +package com.atwoz.alert.domain.vo; + +import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Embeddable +public class AlertMessage { + + private String title; + private String body; + + private AlertMessage(final String title, final String body) { + this.title = title; + this.body = body; + } + + public static AlertMessage createWith(final String title, final String body) { + return new AlertMessage(title, body); + } +} diff --git a/src/main/java/com/atwoz/alert/exception/AlertExceptionHandler.java b/src/main/java/com/atwoz/alert/exception/AlertExceptionHandler.java new file mode 100644 index 00000000..6f93f817 --- /dev/null +++ b/src/main/java/com/atwoz/alert/exception/AlertExceptionHandler.java @@ -0,0 +1,27 @@ +package com.atwoz.alert.exception; + +import com.atwoz.alert.exception.exceptions.AlertSendException; +import com.atwoz.alert.exception.exceptions.FirebaseFileNotFoundException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class AlertExceptionHandler { + + @ExceptionHandler(FirebaseFileNotFoundException.class) + public ResponseEntity handleFirebaseFileNotFoundException(final FirebaseFileNotFoundException e) { + return getExceptionWithStatus(e, HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(AlertSendException.class) + public ResponseEntity handleAlertSendException(final AlertSendException e) { + return getExceptionWithStatus(e, HttpStatus.INTERNAL_SERVER_ERROR); + } + + private ResponseEntity getExceptionWithStatus(final Exception exception, final HttpStatus status) { + return ResponseEntity.status(status) + .body(exception.getMessage()); + } +} diff --git a/src/main/java/com/atwoz/alert/exception/exceptions/AlertGroupNotFoundException.java b/src/main/java/com/atwoz/alert/exception/exceptions/AlertGroupNotFoundException.java new file mode 100644 index 00000000..25ef9abe --- /dev/null +++ b/src/main/java/com/atwoz/alert/exception/exceptions/AlertGroupNotFoundException.java @@ -0,0 +1,8 @@ +package com.atwoz.alert.exception.exceptions; + +public class AlertGroupNotFoundException extends RuntimeException { + + public AlertGroupNotFoundException() { + super("알림 전송 그룹을 찾을 수 없습니다."); + } +} diff --git a/src/main/java/com/atwoz/alert/exception/exceptions/AlertSendException.java b/src/main/java/com/atwoz/alert/exception/exceptions/AlertSendException.java new file mode 100644 index 00000000..b063acdc --- /dev/null +++ b/src/main/java/com/atwoz/alert/exception/exceptions/AlertSendException.java @@ -0,0 +1,8 @@ +package com.atwoz.alert.exception.exceptions; + +public class AlertSendException extends RuntimeException { + + public AlertSendException() { + super("알림 전송 과정에서 예외가 발생했습니다."); + } +} diff --git a/src/main/java/com/atwoz/alert/exception/exceptions/FirebaseFileNotFoundException.java b/src/main/java/com/atwoz/alert/exception/exceptions/FirebaseFileNotFoundException.java new file mode 100644 index 00000000..0a53195c --- /dev/null +++ b/src/main/java/com/atwoz/alert/exception/exceptions/FirebaseFileNotFoundException.java @@ -0,0 +1,8 @@ +package com.atwoz.alert.exception.exceptions; + +public class FirebaseFileNotFoundException extends RuntimeException { + + public FirebaseFileNotFoundException() { + super("Firebase json 파일을 찾을 수 없습니다."); + } +} From 40b31de846d35bdfc4aef3837d6b8925e0ca7d1b Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sat, 6 Jul 2024 18:24:42 +0900 Subject: [PATCH 04/62] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알림 전송 기능에 대한 서비스, 구현체 작성 --- .../atwoz/alert/application/AlertService.java | 21 ++++++++++++++++ .../application/dto/CreateAlertRequest.java | 15 +++++++++++ .../alert/infrastructure/AlertManager.java | 8 ++++++ .../infrastructure/FirebaseAlertManager.java | 25 +++++++++++++++++++ 4 files changed, 69 insertions(+) create mode 100644 src/main/java/com/atwoz/alert/application/AlertService.java create mode 100644 src/main/java/com/atwoz/alert/application/dto/CreateAlertRequest.java create mode 100644 src/main/java/com/atwoz/alert/infrastructure/AlertManager.java create mode 100644 src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java diff --git a/src/main/java/com/atwoz/alert/application/AlertService.java b/src/main/java/com/atwoz/alert/application/AlertService.java new file mode 100644 index 00000000..2b51fe9a --- /dev/null +++ b/src/main/java/com/atwoz/alert/application/AlertService.java @@ -0,0 +1,21 @@ +package com.atwoz.alert.application; + +import com.atwoz.alert.application.dto.CreateAlertRequest; +import com.atwoz.alert.domain.Alert; +import com.atwoz.alert.infrastructure.AlertManager; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional +@Service +public class AlertService { + + private final AlertManager alertManager; + + public void sendAlert(final CreateAlertRequest request) { + Alert alert = Alert.createWith(request.group(), request.title(), request.body(), request.token()); + alertManager.send(alert); + } +} diff --git a/src/main/java/com/atwoz/alert/application/dto/CreateAlertRequest.java b/src/main/java/com/atwoz/alert/application/dto/CreateAlertRequest.java new file mode 100644 index 00000000..5adeaa6c --- /dev/null +++ b/src/main/java/com/atwoz/alert/application/dto/CreateAlertRequest.java @@ -0,0 +1,15 @@ +package com.atwoz.alert.application.dto; + +import jakarta.validation.constraints.NotEmpty; + +public record CreateAlertRequest( + @NotEmpty(message = "알림 발송 그룹이 있어야 합니다.") + String group, + @NotEmpty(message = "대상 토큰이 있어야 합니다.") + String token, + @NotEmpty(message = "알림 제목이 있어야 합니다.") + String title, + @NotEmpty(message = "알림 본문이 있어야 합니다.") + String body +) { +} diff --git a/src/main/java/com/atwoz/alert/infrastructure/AlertManager.java b/src/main/java/com/atwoz/alert/infrastructure/AlertManager.java new file mode 100644 index 00000000..c26b8962 --- /dev/null +++ b/src/main/java/com/atwoz/alert/infrastructure/AlertManager.java @@ -0,0 +1,8 @@ +package com.atwoz.alert.infrastructure; + +import com.atwoz.alert.domain.Alert; + +public interface AlertManager { + + void send(Alert alert); +} diff --git a/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java b/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java new file mode 100644 index 00000000..ec6a53e3 --- /dev/null +++ b/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java @@ -0,0 +1,25 @@ +package com.atwoz.alert.infrastructure; + +import com.atwoz.alert.domain.Alert; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.Notification; +import org.springframework.stereotype.Component; + +@Component +public class FirebaseAlertManager implements AlertManager { + + @Override + public void send(final Alert alert) { + Notification firebaseNotification = Notification.builder() + .setTitle(alert.getTitle()) + .setBody(alert.getBody()) + .build(); + Message firebaseMessage = Message.builder() + .setToken(alert.getToken()) + .setNotification(firebaseNotification) + .build(); + FirebaseMessaging firebase = FirebaseMessaging.getInstance(); + firebase.sendAsync(firebaseMessage); + } +} From cec714f12d0c54e1e2a4ef7e285a161c935b156b Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sat, 6 Jul 2024 18:26:52 +0900 Subject: [PATCH 05/62] =?UTF-8?q?chore:=20Firebase=20key=20gitignore=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 보안을 위해 Firebase key gitignore에 등록 --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 7185b66d..40ce9cab 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ out/ ### REST Docs, QueryDSL ### /src/main/resources/static/docs/ /src/main/generated + +### Firebase ### +/src/main/resources/firebase/firebase_key.json From 47f863aae9e20f189c8eda488967959e989f0855 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Thu, 11 Jul 2024 13:48:35 +0900 Subject: [PATCH 06/62] =?UTF-8?q?refactor:=20body=20NotEmpty=20=EC=A0=9C?= =?UTF-8?q?=EC=95=BD=EC=82=AC=ED=95=AD=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/atwoz/alert/application/dto/CreateAlertRequest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/atwoz/alert/application/dto/CreateAlertRequest.java b/src/main/java/com/atwoz/alert/application/dto/CreateAlertRequest.java index 5adeaa6c..ad44222a 100644 --- a/src/main/java/com/atwoz/alert/application/dto/CreateAlertRequest.java +++ b/src/main/java/com/atwoz/alert/application/dto/CreateAlertRequest.java @@ -9,7 +9,6 @@ public record CreateAlertRequest( String token, @NotEmpty(message = "알림 제목이 있어야 합니다.") String title, - @NotEmpty(message = "알림 본문이 있어야 합니다.") String body ) { } From 830072b3531fd092630879a99beb0b1938985941 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Thu, 11 Jul 2024 13:59:39 +0900 Subject: [PATCH 07/62] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=EC=97=90=20Ale?= =?UTF-8?q?rtGroup=20=EC=A0=95=EB=B3=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알림에 AlertGroup 정보 추가 - AlertManager 패키지 위치 수정 --- .../java/com/atwoz/alert/application/AlertService.java | 2 +- src/main/java/com/atwoz/alert/domain/AlertManager.java | 6 ++++++ .../java/com/atwoz/alert/infrastructure/AlertManager.java | 8 -------- .../atwoz/alert/infrastructure/FirebaseAlertManager.java | 4 ++++ 4 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/atwoz/alert/domain/AlertManager.java delete mode 100644 src/main/java/com/atwoz/alert/infrastructure/AlertManager.java diff --git a/src/main/java/com/atwoz/alert/application/AlertService.java b/src/main/java/com/atwoz/alert/application/AlertService.java index 2b51fe9a..13fa1461 100644 --- a/src/main/java/com/atwoz/alert/application/AlertService.java +++ b/src/main/java/com/atwoz/alert/application/AlertService.java @@ -2,7 +2,7 @@ import com.atwoz.alert.application.dto.CreateAlertRequest; import com.atwoz.alert.domain.Alert; -import com.atwoz.alert.infrastructure.AlertManager; +import com.atwoz.alert.domain.AlertManager; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/com/atwoz/alert/domain/AlertManager.java b/src/main/java/com/atwoz/alert/domain/AlertManager.java new file mode 100644 index 00000000..472a5a0a --- /dev/null +++ b/src/main/java/com/atwoz/alert/domain/AlertManager.java @@ -0,0 +1,6 @@ +package com.atwoz.alert.domain; + +public interface AlertManager { + + void send(Alert alert); +} diff --git a/src/main/java/com/atwoz/alert/infrastructure/AlertManager.java b/src/main/java/com/atwoz/alert/infrastructure/AlertManager.java deleted file mode 100644 index c26b8962..00000000 --- a/src/main/java/com/atwoz/alert/infrastructure/AlertManager.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.atwoz.alert.infrastructure; - -import com.atwoz.alert.domain.Alert; - -public interface AlertManager { - - void send(Alert alert); -} diff --git a/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java b/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java index ec6a53e3..60cd5621 100644 --- a/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java +++ b/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java @@ -1,6 +1,7 @@ package com.atwoz.alert.infrastructure; import com.atwoz.alert.domain.Alert; +import com.atwoz.alert.domain.AlertManager; import com.google.firebase.messaging.FirebaseMessaging; import com.google.firebase.messaging.Message; import com.google.firebase.messaging.Notification; @@ -9,6 +10,8 @@ @Component public class FirebaseAlertManager implements AlertManager { + private static final String GROUP = "group"; + @Override public void send(final Alert alert) { Notification firebaseNotification = Notification.builder() @@ -17,6 +20,7 @@ public void send(final Alert alert) { .build(); Message firebaseMessage = Message.builder() .setToken(alert.getToken()) + .putData(GROUP, alert.getGroup()) .setNotification(firebaseNotification) .build(); FirebaseMessaging firebase = FirebaseMessaging.getInstance(); From de9e5e5aa1edc60786b08e4462e709b825dafd9b Mon Sep 17 00:00:00 2001 From: devholic22 Date: Thu, 11 Jul 2024 14:37:15 +0900 Subject: [PATCH 08/62] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=EC=97=90=20?= =?UTF-8?q?=EB=B0=9C=EC=8B=A0=EC=9E=90=20=EC=A0=95=EB=B3=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알림에 발신자 정보 추가 (닉네임) - 보낸 발신자가 없을 경우 시스템이 보낸 것으로 간주 --- .../atwoz/alert/application/AlertService.java | 2 +- .../application/dto/CreateAlertRequest.java | 1 + .../java/com/atwoz/alert/domain/Alert.java | 19 ++++++++++++++++--- .../infrastructure/FirebaseAlertManager.java | 2 ++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/atwoz/alert/application/AlertService.java b/src/main/java/com/atwoz/alert/application/AlertService.java index 13fa1461..7177ea2b 100644 --- a/src/main/java/com/atwoz/alert/application/AlertService.java +++ b/src/main/java/com/atwoz/alert/application/AlertService.java @@ -15,7 +15,7 @@ public class AlertService { private final AlertManager alertManager; public void sendAlert(final CreateAlertRequest request) { - Alert alert = Alert.createWith(request.group(), request.title(), request.body(), request.token()); + Alert alert = Alert.createWith(request.group(), request.title(), request.body(), request.sender(), request.token()); alertManager.send(alert); } } diff --git a/src/main/java/com/atwoz/alert/application/dto/CreateAlertRequest.java b/src/main/java/com/atwoz/alert/application/dto/CreateAlertRequest.java index ad44222a..d690dfd6 100644 --- a/src/main/java/com/atwoz/alert/application/dto/CreateAlertRequest.java +++ b/src/main/java/com/atwoz/alert/application/dto/CreateAlertRequest.java @@ -5,6 +5,7 @@ public record CreateAlertRequest( @NotEmpty(message = "알림 발송 그룹이 있어야 합니다.") String group, + String sender, @NotEmpty(message = "대상 토큰이 있어야 합니다.") String token, @NotEmpty(message = "알림 제목이 있어야 합니다.") diff --git a/src/main/java/com/atwoz/alert/domain/Alert.java b/src/main/java/com/atwoz/alert/domain/Alert.java index 87d6968c..2355d2a9 100644 --- a/src/main/java/com/atwoz/alert/domain/Alert.java +++ b/src/main/java/com/atwoz/alert/domain/Alert.java @@ -19,6 +19,8 @@ @Entity public class Alert extends BaseEntity { + private static final String SYSTEM_SEND = "SYSTEM"; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -34,19 +36,30 @@ public class Alert extends BaseEntity { @Column(nullable = false) private AlertMessage alertMessage; + @Column(nullable = false) + private String sender; + @Column(nullable = false) private String token; - private Alert(final AlertGroup alertGroup, final AlertMessage alertMessage, final String token) { + private Alert(final AlertGroup alertGroup, final AlertMessage alertMessage, final String sender, final String token) { this.alertGroup = alertGroup; this.alertMessage = alertMessage; + this.sender = sender; this.token = token; } - public static Alert createWith(final String group, final String title, final String body, final String token) { + public static Alert createWith(final String group, final String title, final String body, final String sender, final String token) { AlertGroup alertGroup = AlertGroup.findByName(group); AlertMessage message = AlertMessage.createWith(title, body); - return new Alert(alertGroup, message, token); + return new Alert(alertGroup, message, convertSenderValue(sender), token); + } + + private static String convertSenderValue(final String sender) { + if (sender == null) { + return SYSTEM_SEND; + } + return sender; } public void read() { diff --git a/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java b/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java index 60cd5621..2799231f 100644 --- a/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java +++ b/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java @@ -11,6 +11,7 @@ public class FirebaseAlertManager implements AlertManager { private static final String GROUP = "group"; + private static final String SENDER = "sender"; @Override public void send(final Alert alert) { @@ -22,6 +23,7 @@ public void send(final Alert alert) { .setToken(alert.getToken()) .putData(GROUP, alert.getGroup()) .setNotification(firebaseNotification) + .putData(SENDER, alert.getSender()) .build(); FirebaseMessaging firebase = FirebaseMessaging.getInstance(); firebase.sendAsync(firebaseMessage); From d469b7b3e286fccdbb70e90b15f590b6462c2b4d Mon Sep 17 00:00:00 2001 From: devholic22 Date: Thu, 11 Jul 2024 14:54:46 +0900 Subject: [PATCH 09/62] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알림 이벤트 핸들러 추가 - CreateAlertRequest 삭제 대신 이벤트 인자로 변경 --- .../alert/application/AlertEventHandler.java | 18 ++++++++++++++++++ .../atwoz/alert/application/AlertService.java | 6 +++--- .../application/dto/CreateAlertRequest.java | 15 --------------- .../application/event/AlertCreatedEvent.java | 12 ++++++++++++ .../java/com/atwoz/alert/domain/Alert.java | 5 ++--- 5 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/atwoz/alert/application/AlertEventHandler.java delete mode 100644 src/main/java/com/atwoz/alert/application/dto/CreateAlertRequest.java create mode 100644 src/main/java/com/atwoz/alert/application/event/AlertCreatedEvent.java diff --git a/src/main/java/com/atwoz/alert/application/AlertEventHandler.java b/src/main/java/com/atwoz/alert/application/AlertEventHandler.java new file mode 100644 index 00000000..8b2212f4 --- /dev/null +++ b/src/main/java/com/atwoz/alert/application/AlertEventHandler.java @@ -0,0 +1,18 @@ +package com.atwoz.alert.application; + +import com.atwoz.alert.application.event.AlertCreatedEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class AlertEventHandler { + + private final AlertService alertService; + + @EventListener + public void sendAlertCreatedEvent(final AlertCreatedEvent event) { + alertService.sendAlert(event.group(), event.title(), event.body(), event.sender(), event.token()); + } +} diff --git a/src/main/java/com/atwoz/alert/application/AlertService.java b/src/main/java/com/atwoz/alert/application/AlertService.java index 7177ea2b..a4992f40 100644 --- a/src/main/java/com/atwoz/alert/application/AlertService.java +++ b/src/main/java/com/atwoz/alert/application/AlertService.java @@ -1,8 +1,8 @@ package com.atwoz.alert.application; -import com.atwoz.alert.application.dto.CreateAlertRequest; import com.atwoz.alert.domain.Alert; import com.atwoz.alert.domain.AlertManager; +import com.atwoz.alert.domain.vo.AlertGroup; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,8 +14,8 @@ public class AlertService { private final AlertManager alertManager; - public void sendAlert(final CreateAlertRequest request) { - Alert alert = Alert.createWith(request.group(), request.title(), request.body(), request.sender(), request.token()); + public void sendAlert(final AlertGroup group, final String title, final String body, final String sender, final String token) { + Alert alert = Alert.createWith(group, title, body, sender, token); alertManager.send(alert); } } diff --git a/src/main/java/com/atwoz/alert/application/dto/CreateAlertRequest.java b/src/main/java/com/atwoz/alert/application/dto/CreateAlertRequest.java deleted file mode 100644 index d690dfd6..00000000 --- a/src/main/java/com/atwoz/alert/application/dto/CreateAlertRequest.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.atwoz.alert.application.dto; - -import jakarta.validation.constraints.NotEmpty; - -public record CreateAlertRequest( - @NotEmpty(message = "알림 발송 그룹이 있어야 합니다.") - String group, - String sender, - @NotEmpty(message = "대상 토큰이 있어야 합니다.") - String token, - @NotEmpty(message = "알림 제목이 있어야 합니다.") - String title, - String body -) { -} diff --git a/src/main/java/com/atwoz/alert/application/event/AlertCreatedEvent.java b/src/main/java/com/atwoz/alert/application/event/AlertCreatedEvent.java new file mode 100644 index 00000000..2177fd57 --- /dev/null +++ b/src/main/java/com/atwoz/alert/application/event/AlertCreatedEvent.java @@ -0,0 +1,12 @@ +package com.atwoz.alert.application.event; + +import com.atwoz.alert.domain.vo.AlertGroup; + +public record AlertCreatedEvent( + AlertGroup group, + String title, + String body, + String sender, + String token +) { +} diff --git a/src/main/java/com/atwoz/alert/domain/Alert.java b/src/main/java/com/atwoz/alert/domain/Alert.java index 2355d2a9..8e07c359 100644 --- a/src/main/java/com/atwoz/alert/domain/Alert.java +++ b/src/main/java/com/atwoz/alert/domain/Alert.java @@ -49,10 +49,9 @@ private Alert(final AlertGroup alertGroup, final AlertMessage alertMessage, fina this.token = token; } - public static Alert createWith(final String group, final String title, final String body, final String sender, final String token) { - AlertGroup alertGroup = AlertGroup.findByName(group); + public static Alert createWith(final AlertGroup group, final String title, final String body, final String sender, final String token) { AlertMessage message = AlertMessage.createWith(title, body); - return new Alert(alertGroup, message, convertSenderValue(sender), token); + return new Alert(group, message, convertSenderValue(sender), token); } private static String convertSenderValue(final String sender) { From 4e64cc93bd05f61f89a332c764bb9bc67b3ede86 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Thu, 11 Jul 2024 14:57:26 +0900 Subject: [PATCH 10/62] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 보낸 알림과 데이터베이스 연결 작성 --- .../atwoz/alert/application/AlertService.java | 3 +++ .../atwoz/alert/domain/AlertRepository.java | 6 ++++++ .../infrastructure/AlertJpaRepository.java | 9 +++++++++ .../infrastructure/AlertRepositoryImpl.java | 18 ++++++++++++++++++ 4 files changed, 36 insertions(+) create mode 100644 src/main/java/com/atwoz/alert/domain/AlertRepository.java create mode 100644 src/main/java/com/atwoz/alert/infrastructure/AlertJpaRepository.java create mode 100644 src/main/java/com/atwoz/alert/infrastructure/AlertRepositoryImpl.java diff --git a/src/main/java/com/atwoz/alert/application/AlertService.java b/src/main/java/com/atwoz/alert/application/AlertService.java index a4992f40..5f13ab82 100644 --- a/src/main/java/com/atwoz/alert/application/AlertService.java +++ b/src/main/java/com/atwoz/alert/application/AlertService.java @@ -2,6 +2,7 @@ import com.atwoz.alert.domain.Alert; import com.atwoz.alert.domain.AlertManager; +import com.atwoz.alert.domain.AlertRepository; import com.atwoz.alert.domain.vo.AlertGroup; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -12,10 +13,12 @@ @Service public class AlertService { + private final AlertRepository alertRepository; private final AlertManager alertManager; public void sendAlert(final AlertGroup group, final String title, final String body, final String sender, final String token) { Alert alert = Alert.createWith(group, title, body, sender, token); alertManager.send(alert); + alertRepository.save(alert); } } diff --git a/src/main/java/com/atwoz/alert/domain/AlertRepository.java b/src/main/java/com/atwoz/alert/domain/AlertRepository.java new file mode 100644 index 00000000..3bd6ce8c --- /dev/null +++ b/src/main/java/com/atwoz/alert/domain/AlertRepository.java @@ -0,0 +1,6 @@ +package com.atwoz.alert.domain; + +public interface AlertRepository { + + Alert save(Alert alert); +} diff --git a/src/main/java/com/atwoz/alert/infrastructure/AlertJpaRepository.java b/src/main/java/com/atwoz/alert/infrastructure/AlertJpaRepository.java new file mode 100644 index 00000000..ba4c848c --- /dev/null +++ b/src/main/java/com/atwoz/alert/infrastructure/AlertJpaRepository.java @@ -0,0 +1,9 @@ +package com.atwoz.alert.infrastructure; + +import com.atwoz.alert.domain.Alert; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AlertJpaRepository extends JpaRepository { + + Alert save(Alert alert); +} diff --git a/src/main/java/com/atwoz/alert/infrastructure/AlertRepositoryImpl.java b/src/main/java/com/atwoz/alert/infrastructure/AlertRepositoryImpl.java new file mode 100644 index 00000000..ade15e39 --- /dev/null +++ b/src/main/java/com/atwoz/alert/infrastructure/AlertRepositoryImpl.java @@ -0,0 +1,18 @@ +package com.atwoz.alert.infrastructure; + +import com.atwoz.alert.domain.Alert; +import com.atwoz.alert.domain.AlertRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class AlertRepositoryImpl implements AlertRepository { + + private final AlertJpaRepository alertJpaRepository; + + @Override + public Alert save(final Alert alert) { + return alertJpaRepository.save(alert); + } +} From 6b507f19303781be2fdde85d9049e507ad658d9c Mon Sep 17 00:00:00 2001 From: devholic22 Date: Thu, 11 Jul 2024 15:45:39 +0900 Subject: [PATCH 11/62] =?UTF-8?q?refactor:=20Soft=20delete=EB=A5=BC=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=B4=20?= =?UTF-8?q?=EC=83=81=EC=86=8D=EC=9E=90=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Soft delete를 적용하기 위해 SoftDeleteBaseEntity를 상속받도록 수정 --- src/main/java/com/atwoz/alert/domain/Alert.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/atwoz/alert/domain/Alert.java b/src/main/java/com/atwoz/alert/domain/Alert.java index 8e07c359..4b858464 100644 --- a/src/main/java/com/atwoz/alert/domain/Alert.java +++ b/src/main/java/com/atwoz/alert/domain/Alert.java @@ -2,7 +2,7 @@ import com.atwoz.alert.domain.vo.AlertGroup; import com.atwoz.alert.domain.vo.AlertMessage; -import com.atwoz.global.domain.BaseEntity; +import com.atwoz.global.domain.SoftDeleteBaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; @@ -17,7 +17,7 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity -public class Alert extends BaseEntity { +public class Alert extends SoftDeleteBaseEntity { private static final String SYSTEM_SEND = "SYSTEM"; From 52911c42fa61f1ba7aecbc3b9f64daa4d5669bc9 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Thu, 11 Jul 2024 16:01:42 +0900 Subject: [PATCH 12/62] =?UTF-8?q?feat:=2060=EC=9D=BC=20=EC=B4=88=EA=B3=BC?= =?UTF-8?q?=EB=90=9C=20=EC=95=8C=EB=A6=BC=EC=9D=80=20soft=20delete?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../atwoz/alert/application/AlertService.java | 8 ++++++++ .../atwoz/alert/domain/AlertRepository.java | 1 + .../infrastructure/AlertJdbcRepository.java | 19 +++++++++++++++++++ .../infrastructure/AlertRepositoryImpl.java | 6 ++++++ 4 files changed, 34 insertions(+) create mode 100644 src/main/java/com/atwoz/alert/infrastructure/AlertJdbcRepository.java diff --git a/src/main/java/com/atwoz/alert/application/AlertService.java b/src/main/java/com/atwoz/alert/application/AlertService.java index 5f13ab82..1d9b9146 100644 --- a/src/main/java/com/atwoz/alert/application/AlertService.java +++ b/src/main/java/com/atwoz/alert/application/AlertService.java @@ -5,6 +5,7 @@ import com.atwoz.alert.domain.AlertRepository; import com.atwoz.alert.domain.vo.AlertGroup; import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -13,6 +14,8 @@ @Service public class AlertService { + private static final String MIDNIGHT = "0 0 0 * * ?"; + private final AlertRepository alertRepository; private final AlertManager alertManager; @@ -21,4 +24,9 @@ public void sendAlert(final AlertGroup group, final String title, final String b alertManager.send(alert); alertRepository.save(alert); } + + @Scheduled(cron = MIDNIGHT) + public void deleteExpiredAlerts() { + alertRepository.deleteExpiredAlerts(); + } } diff --git a/src/main/java/com/atwoz/alert/domain/AlertRepository.java b/src/main/java/com/atwoz/alert/domain/AlertRepository.java index 3bd6ce8c..697cbb77 100644 --- a/src/main/java/com/atwoz/alert/domain/AlertRepository.java +++ b/src/main/java/com/atwoz/alert/domain/AlertRepository.java @@ -3,4 +3,5 @@ public interface AlertRepository { Alert save(Alert alert); + void deleteExpiredAlerts(); } diff --git a/src/main/java/com/atwoz/alert/infrastructure/AlertJdbcRepository.java b/src/main/java/com/atwoz/alert/infrastructure/AlertJdbcRepository.java new file mode 100644 index 00000000..b7b7d509 --- /dev/null +++ b/src/main/java/com/atwoz/alert/infrastructure/AlertJdbcRepository.java @@ -0,0 +1,19 @@ +package com.atwoz.alert.infrastructure; + +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class AlertJdbcRepository { + + private final JdbcTemplate jdbcTemplate; + + public void deleteExpiredAlerts() { + String sql = "UPDATE alert" + + " SET deleted_at = CURRENT_TIMESTAMP" + + " WHERE created_at < TIMESTAMPADD(DAY, -61, CURRENT_TIMESTAMP)"; + jdbcTemplate.update(sql); + } +} diff --git a/src/main/java/com/atwoz/alert/infrastructure/AlertRepositoryImpl.java b/src/main/java/com/atwoz/alert/infrastructure/AlertRepositoryImpl.java index ade15e39..e8310c39 100644 --- a/src/main/java/com/atwoz/alert/infrastructure/AlertRepositoryImpl.java +++ b/src/main/java/com/atwoz/alert/infrastructure/AlertRepositoryImpl.java @@ -10,9 +10,15 @@ public class AlertRepositoryImpl implements AlertRepository { private final AlertJpaRepository alertJpaRepository; + private final AlertJdbcRepository alertJdbcRepository; @Override public Alert save(final Alert alert) { return alertJpaRepository.save(alert); } + + @Override + public void deleteExpiredAlerts() { + alertJdbcRepository.deleteExpiredAlerts(); + } } From ad4ac4ca8e655bee02f8257fc014065e6b436a83 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Thu, 11 Jul 2024 18:00:44 +0900 Subject: [PATCH 13/62] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=EA=B0=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알림 생성 시각 추가 --- .../atwoz/alert/infrastructure/FirebaseAlertManager.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java b/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java index 2799231f..748181df 100644 --- a/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java +++ b/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java @@ -7,14 +7,22 @@ import com.google.firebase.messaging.Notification; import org.springframework.stereotype.Component; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + @Component public class FirebaseAlertManager implements AlertManager { private static final String GROUP = "group"; private static final String SENDER = "sender"; + private static final String CREATED_TIME = "created_at"; + private static final String TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; @Override public void send(final Alert alert) { + LocalDateTime createdAt = alert.getCreatedAt(); + String time = createdAt.format(DateTimeFormatter.ofPattern(TIME_FORMAT)); + Notification firebaseNotification = Notification.builder() .setTitle(alert.getTitle()) .setBody(alert.getBody()) @@ -24,6 +32,7 @@ public void send(final Alert alert) { .putData(GROUP, alert.getGroup()) .setNotification(firebaseNotification) .putData(SENDER, alert.getSender()) + .putData(CREATED_TIME, time) .build(); FirebaseMessaging firebase = FirebaseMessaging.getInstance(); firebase.sendAsync(firebaseMessage); From 7c37d58308348dd4a91ec0e8f91417c1c8c0d699 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Thu, 11 Jul 2024 19:57:45 +0900 Subject: [PATCH 14/62] =?UTF-8?q?refactor:=20Alert=20=EC=86=8D=EC=84=B1=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알림에 쓰인 토큰을 관리하지 않도록 제거 - 알림의 대상이 되는 회원의 ID가 연결되도록 수정 --- .../alert/application/AlertEventHandler.java | 2 +- .../atwoz/alert/application/AlertService.java | 6 ++--- .../application/event/AlertCreatedEvent.java | 1 + .../java/com/atwoz/alert/domain/Alert.java | 26 +++++-------------- .../com/atwoz/alert/domain/AlertManager.java | 2 +- .../infrastructure/FirebaseAlertManager.java | 6 ++--- 6 files changed, 15 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/atwoz/alert/application/AlertEventHandler.java b/src/main/java/com/atwoz/alert/application/AlertEventHandler.java index 8b2212f4..4b1c1a38 100644 --- a/src/main/java/com/atwoz/alert/application/AlertEventHandler.java +++ b/src/main/java/com/atwoz/alert/application/AlertEventHandler.java @@ -13,6 +13,6 @@ public class AlertEventHandler { @EventListener public void sendAlertCreatedEvent(final AlertCreatedEvent event) { - alertService.sendAlert(event.group(), event.title(), event.body(), event.sender(), event.token()); + alertService.sendAlert(event.group(), event.title(), event.body(), event.sender(), event.senderId(), event.token()); } } diff --git a/src/main/java/com/atwoz/alert/application/AlertService.java b/src/main/java/com/atwoz/alert/application/AlertService.java index 1d9b9146..91c7eb42 100644 --- a/src/main/java/com/atwoz/alert/application/AlertService.java +++ b/src/main/java/com/atwoz/alert/application/AlertService.java @@ -19,9 +19,9 @@ public class AlertService { private final AlertRepository alertRepository; private final AlertManager alertManager; - public void sendAlert(final AlertGroup group, final String title, final String body, final String sender, final String token) { - Alert alert = Alert.createWith(group, title, body, sender, token); - alertManager.send(alert); + public void sendAlert(final AlertGroup group, final String title, final String body, final String sender, final Long senderId, final String token) { + Alert alert = Alert.createWith(group, title, body, senderId); + alertManager.send(alert, sender, token); alertRepository.save(alert); } diff --git a/src/main/java/com/atwoz/alert/application/event/AlertCreatedEvent.java b/src/main/java/com/atwoz/alert/application/event/AlertCreatedEvent.java index 2177fd57..4a9cc486 100644 --- a/src/main/java/com/atwoz/alert/application/event/AlertCreatedEvent.java +++ b/src/main/java/com/atwoz/alert/application/event/AlertCreatedEvent.java @@ -7,6 +7,7 @@ public record AlertCreatedEvent( String title, String body, String sender, + Long senderId, String token ) { } diff --git a/src/main/java/com/atwoz/alert/domain/Alert.java b/src/main/java/com/atwoz/alert/domain/Alert.java index 4b858464..f5d191b7 100644 --- a/src/main/java/com/atwoz/alert/domain/Alert.java +++ b/src/main/java/com/atwoz/alert/domain/Alert.java @@ -11,16 +11,12 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import lombok.AccessLevel; -import lombok.Getter; import lombok.NoArgsConstructor; -@Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity public class Alert extends SoftDeleteBaseEntity { - private static final String SYSTEM_SEND = "SYSTEM"; - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -37,28 +33,18 @@ public class Alert extends SoftDeleteBaseEntity { private AlertMessage alertMessage; @Column(nullable = false) - private String sender; - - @Column(nullable = false) - private String token; + private Long senderId; - private Alert(final AlertGroup alertGroup, final AlertMessage alertMessage, final String sender, final String token) { + private Alert(final AlertGroup alertGroup, final AlertMessage alertMessage, final Long senderId) { this.alertGroup = alertGroup; this.alertMessage = alertMessage; - this.sender = sender; - this.token = token; + this.senderId = senderId; + this.isRead = false; } - public static Alert createWith(final AlertGroup group, final String title, final String body, final String sender, final String token) { + public static Alert createWith(final AlertGroup group, final String title, final String body, final Long senderId) { AlertMessage message = AlertMessage.createWith(title, body); - return new Alert(group, message, convertSenderValue(sender), token); - } - - private static String convertSenderValue(final String sender) { - if (sender == null) { - return SYSTEM_SEND; - } - return sender; + return new Alert(group, message, senderId); } public void read() { diff --git a/src/main/java/com/atwoz/alert/domain/AlertManager.java b/src/main/java/com/atwoz/alert/domain/AlertManager.java index 472a5a0a..f90615dc 100644 --- a/src/main/java/com/atwoz/alert/domain/AlertManager.java +++ b/src/main/java/com/atwoz/alert/domain/AlertManager.java @@ -2,5 +2,5 @@ public interface AlertManager { - void send(Alert alert); + void send(Alert alert, String sender, String token); } diff --git a/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java b/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java index 748181df..b9d31cf8 100644 --- a/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java +++ b/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java @@ -19,7 +19,7 @@ public class FirebaseAlertManager implements AlertManager { private static final String TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; @Override - public void send(final Alert alert) { + public void send(final Alert alert, final String sender, final String token) { LocalDateTime createdAt = alert.getCreatedAt(); String time = createdAt.format(DateTimeFormatter.ofPattern(TIME_FORMAT)); @@ -28,10 +28,10 @@ public void send(final Alert alert) { .setBody(alert.getBody()) .build(); Message firebaseMessage = Message.builder() - .setToken(alert.getToken()) + .setToken(token) .putData(GROUP, alert.getGroup()) .setNotification(firebaseNotification) - .putData(SENDER, alert.getSender()) + .putData(SENDER, sender) .putData(CREATED_TIME, time) .build(); FirebaseMessaging firebase = FirebaseMessaging.getInstance(); From 1aa3be9920158ffa7c5f59efe9d67a390092dcbb Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 08:36:58 +0900 Subject: [PATCH 15/62] =?UTF-8?q?feat:=20=EB=A0=88=EB=94=94=EC=8A=A4?= =?UTF-8?q?=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20FCM=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로그인 시 토큰을 저장하도록 설정 - 토큰 관리 리포지터리 정의 --- .../alert/application/AlertEventHandler.java | 6 +++ .../atwoz/alert/application/AlertService.java | 6 +++ .../event/AlertTokenCreatedEvent.java | 7 ++++ .../alert/domain/AlertTokenRepository.java | 9 ++++ .../FirebaseTokenRepository.java | 41 +++++++++++++++++++ .../application/auth/MemberAuthService.java | 4 ++ .../application/auth/dto/LoginRequest.java | 5 ++- 7 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/atwoz/alert/application/event/AlertTokenCreatedEvent.java create mode 100644 src/main/java/com/atwoz/alert/domain/AlertTokenRepository.java create mode 100644 src/main/java/com/atwoz/alert/infrastructure/FirebaseTokenRepository.java diff --git a/src/main/java/com/atwoz/alert/application/AlertEventHandler.java b/src/main/java/com/atwoz/alert/application/AlertEventHandler.java index 4b1c1a38..f393937f 100644 --- a/src/main/java/com/atwoz/alert/application/AlertEventHandler.java +++ b/src/main/java/com/atwoz/alert/application/AlertEventHandler.java @@ -1,6 +1,7 @@ package com.atwoz.alert.application; import com.atwoz.alert.application.event.AlertCreatedEvent; +import com.atwoz.alert.application.event.AlertTokenCreatedEvent; import lombok.RequiredArgsConstructor; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @@ -15,4 +16,9 @@ public class AlertEventHandler { public void sendAlertCreatedEvent(final AlertCreatedEvent event) { alertService.sendAlert(event.group(), event.title(), event.body(), event.sender(), event.senderId(), event.token()); } + + @EventListener + public void sendAlertTokenCreatedEvent(final AlertTokenCreatedEvent event) { + alertService.saveToken(event.id(), event.token()); + } } diff --git a/src/main/java/com/atwoz/alert/application/AlertService.java b/src/main/java/com/atwoz/alert/application/AlertService.java index 91c7eb42..69847f33 100644 --- a/src/main/java/com/atwoz/alert/application/AlertService.java +++ b/src/main/java/com/atwoz/alert/application/AlertService.java @@ -3,6 +3,7 @@ import com.atwoz.alert.domain.Alert; import com.atwoz.alert.domain.AlertManager; import com.atwoz.alert.domain.AlertRepository; +import com.atwoz.alert.domain.AlertTokenRepository; import com.atwoz.alert.domain.vo.AlertGroup; import lombok.RequiredArgsConstructor; import org.springframework.scheduling.annotation.Scheduled; @@ -17,8 +18,13 @@ public class AlertService { private static final String MIDNIGHT = "0 0 0 * * ?"; private final AlertRepository alertRepository; + private final AlertTokenRepository tokenRepository; private final AlertManager alertManager; + public void saveToken(final Long id, final String token) { + tokenRepository.saveToken(id, token); + } + public void sendAlert(final AlertGroup group, final String title, final String body, final String sender, final Long senderId, final String token) { Alert alert = Alert.createWith(group, title, body, senderId); alertManager.send(alert, sender, token); diff --git a/src/main/java/com/atwoz/alert/application/event/AlertTokenCreatedEvent.java b/src/main/java/com/atwoz/alert/application/event/AlertTokenCreatedEvent.java new file mode 100644 index 00000000..c40af061 --- /dev/null +++ b/src/main/java/com/atwoz/alert/application/event/AlertTokenCreatedEvent.java @@ -0,0 +1,7 @@ +package com.atwoz.alert.application.event; + +public record AlertTokenCreatedEvent( + Long id, + String token +) { +} diff --git a/src/main/java/com/atwoz/alert/domain/AlertTokenRepository.java b/src/main/java/com/atwoz/alert/domain/AlertTokenRepository.java new file mode 100644 index 00000000..1810fc50 --- /dev/null +++ b/src/main/java/com/atwoz/alert/domain/AlertTokenRepository.java @@ -0,0 +1,9 @@ +package com.atwoz.alert.domain; + +public interface AlertTokenRepository { + + void saveToken(Long id, String token); + String getToken(Long id); + void deleteToken(Long id); + boolean hasKey(Long id); +} diff --git a/src/main/java/com/atwoz/alert/infrastructure/FirebaseTokenRepository.java b/src/main/java/com/atwoz/alert/infrastructure/FirebaseTokenRepository.java new file mode 100644 index 00000000..5e00df80 --- /dev/null +++ b/src/main/java/com/atwoz/alert/infrastructure/FirebaseTokenRepository.java @@ -0,0 +1,41 @@ +package com.atwoz.alert.infrastructure; + +import com.atwoz.alert.domain.AlertTokenRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import static java.lang.Boolean.TRUE; + +@RequiredArgsConstructor +@Repository +public class FirebaseTokenRepository implements AlertTokenRepository { + + private final StringRedisTemplate tokenRedisTemplate; + + @Override + public void saveToken(final Long id, final String token) { + tokenRedisTemplate.opsForValue() + .set(convertId(id), token); + } + + @Override + public String getToken(final Long id) { + return tokenRedisTemplate.opsForValue() + .get(convertId(id)); + } + + @Override + public void deleteToken(final Long id) { + tokenRedisTemplate.delete(convertId(id)); + } + + @Override + public boolean hasKey(final Long id) { + return TRUE.equals(tokenRedisTemplate.hasKey(convertId(id))); + } + + private String convertId(final Long id) { + return String.valueOf(id); + } +} diff --git a/src/main/java/com/atwoz/member/application/auth/MemberAuthService.java b/src/main/java/com/atwoz/member/application/auth/MemberAuthService.java index f6383453..cabd61c8 100644 --- a/src/main/java/com/atwoz/member/application/auth/MemberAuthService.java +++ b/src/main/java/com/atwoz/member/application/auth/MemberAuthService.java @@ -1,5 +1,7 @@ package com.atwoz.member.application.auth; +import com.atwoz.alert.application.event.AlertTokenCreatedEvent; +import com.atwoz.global.event.Events; import com.atwoz.member.application.auth.dto.LoginRequest; import com.atwoz.member.domain.auth.MemberTokenProvider; import com.atwoz.member.domain.member.Member; @@ -29,6 +31,8 @@ public String login(final LoginRequest request, final OAuthProviderRequest provi MemberInfoResponse memberInfoResponse = oAuthRequester.getMemberInfo(accessToken, provider); Member createdMember = Member.createWithOAuth(DEFAULT_PHONE_NUMBER); memberRepository.save(createdMember); + Events.raise(new AlertTokenCreatedEvent(createdMember.getId(), request.token())); + return memberTokenProvider.createAccessToken(createdMember.getId()); } } diff --git a/src/main/java/com/atwoz/member/application/auth/dto/LoginRequest.java b/src/main/java/com/atwoz/member/application/auth/dto/LoginRequest.java index 5b038045..f2d18cf0 100644 --- a/src/main/java/com/atwoz/member/application/auth/dto/LoginRequest.java +++ b/src/main/java/com/atwoz/member/application/auth/dto/LoginRequest.java @@ -7,6 +7,9 @@ public record LoginRequest( String provider, @NotBlank(message = "인증 코드가 비었습니다.") - String code + String code, + + @NotBlank(message = "FCM 토큰이 비었습니다.") + String token ) { } From b6a4bf2d680476cb78d1100f2b43e39b6536df74 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 08:45:21 +0900 Subject: [PATCH 16/62] =?UTF-8?q?refactor:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - senderId 대신 receiverId를 관리하도록 수정 - 토큰을 직접 받기보다 서비스 내부에서 가져오도록 수정 --- .../com/atwoz/alert/application/AlertEventHandler.java | 2 +- .../java/com/atwoz/alert/application/AlertService.java | 5 +++-- .../alert/application/event/AlertCreatedEvent.java | 2 +- src/main/java/com/atwoz/alert/domain/Alert.java | 10 +++++----- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/atwoz/alert/application/AlertEventHandler.java b/src/main/java/com/atwoz/alert/application/AlertEventHandler.java index f393937f..49f9fa04 100644 --- a/src/main/java/com/atwoz/alert/application/AlertEventHandler.java +++ b/src/main/java/com/atwoz/alert/application/AlertEventHandler.java @@ -14,7 +14,7 @@ public class AlertEventHandler { @EventListener public void sendAlertCreatedEvent(final AlertCreatedEvent event) { - alertService.sendAlert(event.group(), event.title(), event.body(), event.sender(), event.senderId(), event.token()); + alertService.sendAlert(event.group(), event.title(), event.body(), event.sender(), event.receiverId()); } @EventListener diff --git a/src/main/java/com/atwoz/alert/application/AlertService.java b/src/main/java/com/atwoz/alert/application/AlertService.java index 69847f33..2d7b4403 100644 --- a/src/main/java/com/atwoz/alert/application/AlertService.java +++ b/src/main/java/com/atwoz/alert/application/AlertService.java @@ -25,8 +25,9 @@ public void saveToken(final Long id, final String token) { tokenRepository.saveToken(id, token); } - public void sendAlert(final AlertGroup group, final String title, final String body, final String sender, final Long senderId, final String token) { - Alert alert = Alert.createWith(group, title, body, senderId); + public void sendAlert(final AlertGroup group, final String title, final String body, final String sender, final Long receiverId) { + String token = tokenRepository.getToken(receiverId); + Alert alert = Alert.createWith(group, title, body, receiverId); alertManager.send(alert, sender, token); alertRepository.save(alert); } diff --git a/src/main/java/com/atwoz/alert/application/event/AlertCreatedEvent.java b/src/main/java/com/atwoz/alert/application/event/AlertCreatedEvent.java index 4a9cc486..e1ed8382 100644 --- a/src/main/java/com/atwoz/alert/application/event/AlertCreatedEvent.java +++ b/src/main/java/com/atwoz/alert/application/event/AlertCreatedEvent.java @@ -7,7 +7,7 @@ public record AlertCreatedEvent( String title, String body, String sender, - Long senderId, + Long receiverId, String token ) { } diff --git a/src/main/java/com/atwoz/alert/domain/Alert.java b/src/main/java/com/atwoz/alert/domain/Alert.java index f5d191b7..14fe6d63 100644 --- a/src/main/java/com/atwoz/alert/domain/Alert.java +++ b/src/main/java/com/atwoz/alert/domain/Alert.java @@ -33,18 +33,18 @@ public class Alert extends SoftDeleteBaseEntity { private AlertMessage alertMessage; @Column(nullable = false) - private Long senderId; + private Long receiverId; - private Alert(final AlertGroup alertGroup, final AlertMessage alertMessage, final Long senderId) { + private Alert(final AlertGroup alertGroup, final AlertMessage alertMessage, final Long receiverId) { this.alertGroup = alertGroup; this.alertMessage = alertMessage; - this.senderId = senderId; + this.receiverId = receiverId; this.isRead = false; } - public static Alert createWith(final AlertGroup group, final String title, final String body, final Long senderId) { + public static Alert createWith(final AlertGroup group, final String title, final String body, final Long receiverId) { AlertMessage message = AlertMessage.createWith(title, body); - return new Alert(group, message, senderId); + return new Alert(group, message, receiverId); } public void read() { From 0dce06ce23ab7fc17ebebb09140a0033b2750590 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 08:48:21 +0900 Subject: [PATCH 17/62] =?UTF-8?q?refactor:=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=20=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 토큰 조회 시 유효성 검사 진행 --- .../com/atwoz/alert/exception/AlertExceptionHandler.java | 6 ++++++ .../exceptions/ReceiverTokenNotFoundException.java | 8 ++++++++ .../alert/infrastructure/FirebaseTokenRepository.java | 8 ++++++++ 3 files changed, 22 insertions(+) create mode 100644 src/main/java/com/atwoz/alert/exception/exceptions/ReceiverTokenNotFoundException.java diff --git a/src/main/java/com/atwoz/alert/exception/AlertExceptionHandler.java b/src/main/java/com/atwoz/alert/exception/AlertExceptionHandler.java index 6f93f817..ff6ad80c 100644 --- a/src/main/java/com/atwoz/alert/exception/AlertExceptionHandler.java +++ b/src/main/java/com/atwoz/alert/exception/AlertExceptionHandler.java @@ -2,6 +2,7 @@ import com.atwoz.alert.exception.exceptions.AlertSendException; import com.atwoz.alert.exception.exceptions.FirebaseFileNotFoundException; +import com.atwoz.alert.exception.exceptions.ReceiverTokenNotFoundException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -20,6 +21,11 @@ public ResponseEntity handleAlertSendException(final AlertSendException return getExceptionWithStatus(e, HttpStatus.INTERNAL_SERVER_ERROR); } + @ExceptionHandler(ReceiverTokenNotFoundException.class) + public ResponseEntity handleReceiverTokenNotFoundException(final ReceiverTokenNotFoundException e) { + return getExceptionWithStatus(e, HttpStatus.NOT_FOUND); + } + private ResponseEntity getExceptionWithStatus(final Exception exception, final HttpStatus status) { return ResponseEntity.status(status) .body(exception.getMessage()); diff --git a/src/main/java/com/atwoz/alert/exception/exceptions/ReceiverTokenNotFoundException.java b/src/main/java/com/atwoz/alert/exception/exceptions/ReceiverTokenNotFoundException.java new file mode 100644 index 00000000..3a4552d9 --- /dev/null +++ b/src/main/java/com/atwoz/alert/exception/exceptions/ReceiverTokenNotFoundException.java @@ -0,0 +1,8 @@ +package com.atwoz.alert.exception.exceptions; + +public class ReceiverTokenNotFoundException extends RuntimeException { + + public ReceiverTokenNotFoundException() { + super("대상 회원의 FCM 토큰이 존재하지 않습니다."); + } +} diff --git a/src/main/java/com/atwoz/alert/infrastructure/FirebaseTokenRepository.java b/src/main/java/com/atwoz/alert/infrastructure/FirebaseTokenRepository.java index 5e00df80..3bc84121 100644 --- a/src/main/java/com/atwoz/alert/infrastructure/FirebaseTokenRepository.java +++ b/src/main/java/com/atwoz/alert/infrastructure/FirebaseTokenRepository.java @@ -1,6 +1,7 @@ package com.atwoz.alert.infrastructure; import com.atwoz.alert.domain.AlertTokenRepository; +import com.atwoz.alert.exception.exceptions.ReceiverTokenNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Repository; @@ -21,10 +22,17 @@ public void saveToken(final Long id, final String token) { @Override public String getToken(final Long id) { + validateTokenExistence(id); return tokenRedisTemplate.opsForValue() .get(convertId(id)); } + private void validateTokenExistence(final Long id) { + if (!hasKey(id)) { + throw new ReceiverTokenNotFoundException(); + } + } + @Override public void deleteToken(final Long id) { tokenRedisTemplate.delete(convertId(id)); From d8c13293f7f0ea0111cddc62e05fe8e8b25ddf99 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 13:21:09 +0900 Subject: [PATCH 18/62] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알림 조회 기능 개발 (페이징, 단일) - 알림 단일 조회 시 read 작업 수행되도록 설정 --- .../alert/application/AlertQueryService.java | 34 +++++++++++++ .../atwoz/alert/application/AlertService.java | 12 +++++ .../atwoz/alert/domain/AlertRepository.java | 8 +++ .../exception/AlertExceptionHandler.java | 6 +++ .../exceptions/AlertNotFoundException.java | 8 +++ .../infrastructure/AlertQueryRepository.java | 51 +++++++++++++++++++ .../infrastructure/AlertRepositoryImpl.java | 16 ++++++ .../dto/AlertContentSearchResponse.java | 7 +++ .../dto/AlertPagingResponse.java | 14 +++++ .../dto/AlertSearchResponse.java | 12 +++++ .../com/atwoz/alert/ui/AlertController.java | 40 +++++++++++++++ 11 files changed, 208 insertions(+) create mode 100644 src/main/java/com/atwoz/alert/application/AlertQueryService.java create mode 100644 src/main/java/com/atwoz/alert/exception/exceptions/AlertNotFoundException.java create mode 100644 src/main/java/com/atwoz/alert/infrastructure/AlertQueryRepository.java create mode 100644 src/main/java/com/atwoz/alert/infrastructure/dto/AlertContentSearchResponse.java create mode 100644 src/main/java/com/atwoz/alert/infrastructure/dto/AlertPagingResponse.java create mode 100644 src/main/java/com/atwoz/alert/infrastructure/dto/AlertSearchResponse.java create mode 100644 src/main/java/com/atwoz/alert/ui/AlertController.java diff --git a/src/main/java/com/atwoz/alert/application/AlertQueryService.java b/src/main/java/com/atwoz/alert/application/AlertQueryService.java new file mode 100644 index 00000000..1b1db1bb --- /dev/null +++ b/src/main/java/com/atwoz/alert/application/AlertQueryService.java @@ -0,0 +1,34 @@ +package com.atwoz.alert.application; + +import com.atwoz.alert.domain.AlertRepository; +import com.atwoz.alert.infrastructure.dto.AlertPagingResponse; +import com.atwoz.alert.infrastructure.dto.AlertSearchResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class AlertQueryService { + + private static final int NEXT_PAGE_INDEX = 1; + private static final int NO_MORE_PAGE = -1; + + private final AlertRepository alertRepository; + + public AlertPagingResponse findMemberAlertsWithPaging(final Long memberId, final Pageable pageable) { + Page response = alertRepository.findMemberAlertsWithPaging(memberId, pageable); + int nextPage = getNextPage(pageable.getPageNumber(), response); + return AlertPagingResponse.of(response, nextPage); + } + + private int getNextPage(final int pageNumber, final Page alerts) { + if (alerts.hasNext()) { + return pageNumber + NEXT_PAGE_INDEX; + } + return NO_MORE_PAGE; + } +} diff --git a/src/main/java/com/atwoz/alert/application/AlertService.java b/src/main/java/com/atwoz/alert/application/AlertService.java index 2d7b4403..f6a48ca4 100644 --- a/src/main/java/com/atwoz/alert/application/AlertService.java +++ b/src/main/java/com/atwoz/alert/application/AlertService.java @@ -5,11 +5,14 @@ import com.atwoz.alert.domain.AlertRepository; import com.atwoz.alert.domain.AlertTokenRepository; import com.atwoz.alert.domain.vo.AlertGroup; +import com.atwoz.alert.exception.exceptions.AlertNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + @RequiredArgsConstructor @Transactional @Service @@ -32,6 +35,15 @@ public void sendAlert(final AlertGroup group, final String title, final String b alertRepository.save(alert); } + public void readAlert(final Long memberId, final Long id) { + Optional alert = alertRepository.findByMemberIdAndId(memberId, id); + if (alert.isEmpty()) { + throw new AlertNotFoundException(); + } + Alert targetAlert = alert.get(); + targetAlert.read(); + } + @Scheduled(cron = MIDNIGHT) public void deleteExpiredAlerts() { alertRepository.deleteExpiredAlerts(); diff --git a/src/main/java/com/atwoz/alert/domain/AlertRepository.java b/src/main/java/com/atwoz/alert/domain/AlertRepository.java index 697cbb77..82b2cce8 100644 --- a/src/main/java/com/atwoz/alert/domain/AlertRepository.java +++ b/src/main/java/com/atwoz/alert/domain/AlertRepository.java @@ -1,7 +1,15 @@ package com.atwoz.alert.domain; +import com.atwoz.alert.infrastructure.dto.AlertSearchResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.Optional; + public interface AlertRepository { Alert save(Alert alert); + Page findMemberAlertsWithPaging(Long memberId, Pageable pageable); + Optional findByMemberIdAndId(Long memberId, Long id); void deleteExpiredAlerts(); } diff --git a/src/main/java/com/atwoz/alert/exception/AlertExceptionHandler.java b/src/main/java/com/atwoz/alert/exception/AlertExceptionHandler.java index ff6ad80c..a72d7ee6 100644 --- a/src/main/java/com/atwoz/alert/exception/AlertExceptionHandler.java +++ b/src/main/java/com/atwoz/alert/exception/AlertExceptionHandler.java @@ -1,5 +1,6 @@ package com.atwoz.alert.exception; +import com.atwoz.alert.exception.exceptions.AlertNotFoundException; import com.atwoz.alert.exception.exceptions.AlertSendException; import com.atwoz.alert.exception.exceptions.FirebaseFileNotFoundException; import com.atwoz.alert.exception.exceptions.ReceiverTokenNotFoundException; @@ -26,6 +27,11 @@ public ResponseEntity handleReceiverTokenNotFoundException(final Receive return getExceptionWithStatus(e, HttpStatus.NOT_FOUND); } + @ExceptionHandler(AlertNotFoundException.class) + public ResponseEntity handleAlertNotFoundException(final AlertNotFoundException e) { + return getExceptionWithStatus(e, HttpStatus.NOT_FOUND); + } + private ResponseEntity getExceptionWithStatus(final Exception exception, final HttpStatus status) { return ResponseEntity.status(status) .body(exception.getMessage()); diff --git a/src/main/java/com/atwoz/alert/exception/exceptions/AlertNotFoundException.java b/src/main/java/com/atwoz/alert/exception/exceptions/AlertNotFoundException.java new file mode 100644 index 00000000..3ba0a125 --- /dev/null +++ b/src/main/java/com/atwoz/alert/exception/exceptions/AlertNotFoundException.java @@ -0,0 +1,8 @@ +package com.atwoz.alert.exception.exceptions; + +public class AlertNotFoundException extends RuntimeException { + + public AlertNotFoundException() { + super("회원과 ID에 해당되는 알림 내역이 없습니다."); + } +} diff --git a/src/main/java/com/atwoz/alert/infrastructure/AlertQueryRepository.java b/src/main/java/com/atwoz/alert/infrastructure/AlertQueryRepository.java new file mode 100644 index 00000000..f9428815 --- /dev/null +++ b/src/main/java/com/atwoz/alert/infrastructure/AlertQueryRepository.java @@ -0,0 +1,51 @@ +package com.atwoz.alert.infrastructure; + +import com.atwoz.alert.domain.Alert; +import com.atwoz.alert.infrastructure.dto.AlertContentSearchResponse; +import com.atwoz.alert.infrastructure.dto.AlertSearchResponse; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; +import static com.atwoz.alert.domain.QAlert.alert; +import static com.querydsl.core.types.Projections.constructor; + +@RequiredArgsConstructor +@Repository +public class AlertQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + public Page findMemberAlertsWithPaging(final Long memberId, final Pageable pageable) { + List results = jpaQueryFactory.select( + constructor(AlertSearchResponse.class, + alert.id, + alert.alertGroup, + constructor(AlertContentSearchResponse.class, + alert.alertMessage.title, + alert.alertMessage.body), + alert.isRead, + alert.createdAt)) + .from(alert) + .where(alert.receiverId.eq(memberId), alert.deletedAt.isNull()) + .orderBy(alert.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + return new PageImpl<>(results, pageable, results.size()); + } + + public Optional findByMemberIdAndId(final Long memberId, final Long id) { + Alert findAlert = jpaQueryFactory.select(alert) + .from(alert) + .where(alert.id.eq(id), alert.receiverId.eq(memberId)) + .fetchOne(); + return Optional.ofNullable(findAlert); + } +} diff --git a/src/main/java/com/atwoz/alert/infrastructure/AlertRepositoryImpl.java b/src/main/java/com/atwoz/alert/infrastructure/AlertRepositoryImpl.java index e8310c39..3fc6a9d3 100644 --- a/src/main/java/com/atwoz/alert/infrastructure/AlertRepositoryImpl.java +++ b/src/main/java/com/atwoz/alert/infrastructure/AlertRepositoryImpl.java @@ -2,21 +2,37 @@ import com.atwoz.alert.domain.Alert; import com.atwoz.alert.domain.AlertRepository; +import com.atwoz.alert.infrastructure.dto.AlertSearchResponse; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; +import java.util.Optional; + @RequiredArgsConstructor @Repository public class AlertRepositoryImpl implements AlertRepository { private final AlertJpaRepository alertJpaRepository; private final AlertJdbcRepository alertJdbcRepository; + private final AlertQueryRepository alertQueryRepository; @Override public Alert save(final Alert alert) { return alertJpaRepository.save(alert); } + @Override + public Page findMemberAlertsWithPaging(final Long memberId, final Pageable pageable) { + return alertQueryRepository.findMemberAlertsWithPaging(memberId, pageable); + } + + @Override + public Optional findByMemberIdAndId(final Long memberId, final Long id) { + return alertQueryRepository.findByMemberIdAndId(memberId, id); + } + @Override public void deleteExpiredAlerts() { alertJdbcRepository.deleteExpiredAlerts(); diff --git a/src/main/java/com/atwoz/alert/infrastructure/dto/AlertContentSearchResponse.java b/src/main/java/com/atwoz/alert/infrastructure/dto/AlertContentSearchResponse.java new file mode 100644 index 00000000..28cdd839 --- /dev/null +++ b/src/main/java/com/atwoz/alert/infrastructure/dto/AlertContentSearchResponse.java @@ -0,0 +1,7 @@ +package com.atwoz.alert.infrastructure.dto; + +public record AlertContentSearchResponse( + String title, + String body +) { +} diff --git a/src/main/java/com/atwoz/alert/infrastructure/dto/AlertPagingResponse.java b/src/main/java/com/atwoz/alert/infrastructure/dto/AlertPagingResponse.java new file mode 100644 index 00000000..7cb6ed6d --- /dev/null +++ b/src/main/java/com/atwoz/alert/infrastructure/dto/AlertPagingResponse.java @@ -0,0 +1,14 @@ +package com.atwoz.alert.infrastructure.dto; + +import org.springframework.data.domain.Page; + +import java.util.List; + +public record AlertPagingResponse( + List alerts, + int nextPage +) { + public static AlertPagingResponse of(final Page alerts, final int nextPage) { + return new AlertPagingResponse(alerts.getContent(), nextPage); + } +} diff --git a/src/main/java/com/atwoz/alert/infrastructure/dto/AlertSearchResponse.java b/src/main/java/com/atwoz/alert/infrastructure/dto/AlertSearchResponse.java new file mode 100644 index 00000000..4e70d9a0 --- /dev/null +++ b/src/main/java/com/atwoz/alert/infrastructure/dto/AlertSearchResponse.java @@ -0,0 +1,12 @@ +package com.atwoz.alert.infrastructure.dto; + +import java.time.LocalDateTime; + +public record AlertSearchResponse( + Long id, + String group, + AlertContentSearchResponse alert, + Boolean isRead, + LocalDateTime createdAt +) { +} diff --git a/src/main/java/com/atwoz/alert/ui/AlertController.java b/src/main/java/com/atwoz/alert/ui/AlertController.java new file mode 100644 index 00000000..91f01cd5 --- /dev/null +++ b/src/main/java/com/atwoz/alert/ui/AlertController.java @@ -0,0 +1,40 @@ +package com.atwoz.alert.ui; + +import com.atwoz.alert.application.AlertQueryService; +import com.atwoz.alert.application.AlertService; +import com.atwoz.alert.infrastructure.dto.AlertPagingResponse; +import com.atwoz.member.ui.auth.support.AuthMember; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.springframework.data.domain.Sort.Direction.DESC; + +@RequiredArgsConstructor +@RequestMapping("/api/members/me/alerts") +@RestController +public class AlertController { + + private final AlertQueryService alertQueryService; + private final AlertService alertService; + + @GetMapping + public ResponseEntity findMemberAlertsWithPaging( + @AuthMember final Long memberId, + @PageableDefault(sort = "created_at", direction = DESC) final Pageable pageable + ) { + return ResponseEntity.ok(alertQueryService.findMemberAlertsWithPaging(memberId, pageable)); + } + + @GetMapping("/{id}") + public ResponseEntity readAlert(@AuthMember final Long memberId, @PathVariable final Long id) { + alertService.readAlert(memberId, id); + return ResponseEntity.ok() + .build(); + } +} From 53f104ce69c32ed75212544a98f9799800da4bf2 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 13:47:44 +0900 Subject: [PATCH 19/62] =?UTF-8?q?test:=20=EC=95=8C=EB=A6=BC=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알림 도메인 단위테스트 작성 --- .../java/com/atwoz/alert/domain/Alert.java | 8 ++- .../atwoz/alert/domain/AlertGroupTest.java | 42 ++++++++++++++ .../atwoz/alert/domain/AlertMessageTest.java | 29 ++++++++++ .../com/atwoz/alert/domain/AlertTest.java | 55 +++++++++++++++++++ 4 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/atwoz/alert/domain/AlertGroupTest.java create mode 100644 src/test/java/com/atwoz/alert/domain/AlertMessageTest.java create mode 100644 src/test/java/com/atwoz/alert/domain/AlertTest.java diff --git a/src/main/java/com/atwoz/alert/domain/Alert.java b/src/main/java/com/atwoz/alert/domain/Alert.java index 14fe6d63..b899dd5b 100644 --- a/src/main/java/com/atwoz/alert/domain/Alert.java +++ b/src/main/java/com/atwoz/alert/domain/Alert.java @@ -48,7 +48,9 @@ public static Alert createWith(final AlertGroup group, final String title, final } public void read() { - this.isRead = true; + if (!isRead) { + this.isRead = true; + } } public String getTitle() { @@ -62,4 +64,8 @@ public String getBody() { public String getGroup() { return alertGroup.getName(); } + + public boolean isRead() { + return isRead; + } } diff --git a/src/test/java/com/atwoz/alert/domain/AlertGroupTest.java b/src/test/java/com/atwoz/alert/domain/AlertGroupTest.java new file mode 100644 index 00000000..a3ab21d5 --- /dev/null +++ b/src/test/java/com/atwoz/alert/domain/AlertGroupTest.java @@ -0,0 +1,42 @@ +package com.atwoz.alert.domain; + +import com.atwoz.alert.domain.vo.AlertGroup; +import com.atwoz.alert.exception.exceptions.AlertGroupNotFoundException; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AlertGroupTest { + + @Nested + class 그룹_조회 { + + @Test + void 그룹이_유효하면_정상_반환한다() { + // given + String groupName = "좋아요"; + + // when + AlertGroup targetGroup = AlertGroup.findByName(groupName); + + // then + assertThat(targetGroup.getName()).isEqualTo(groupName); + } + + @Test + void 그룹이_유효하지_않으면_예외가_발생한다() { + // given + String groupName = "-123"; + + // when & then + assertThatThrownBy(() -> AlertGroup.findByName(groupName)) + .isInstanceOf(AlertGroupNotFoundException.class); + } + } +} diff --git a/src/test/java/com/atwoz/alert/domain/AlertMessageTest.java b/src/test/java/com/atwoz/alert/domain/AlertMessageTest.java new file mode 100644 index 00000000..cfcbd17b --- /dev/null +++ b/src/test/java/com/atwoz/alert/domain/AlertMessageTest.java @@ -0,0 +1,29 @@ +package com.atwoz.alert.domain; + +import com.atwoz.alert.domain.vo.AlertMessage; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AlertMessageTest { + + @Test + void 메시지_정상_생성() { + // given + String title = "메시지 제목"; + String body = "메시지 상세 내용"; + + // when + AlertMessage message = AlertMessage.createWith(title, body); + + // then + assertSoftly(softly -> { + softly.assertThat(message.getTitle()).isEqualTo(title); + softly.assertThat(message.getBody()).isEqualTo(body); + }); + } +} diff --git a/src/test/java/com/atwoz/alert/domain/AlertTest.java b/src/test/java/com/atwoz/alert/domain/AlertTest.java new file mode 100644 index 00000000..51a2a81d --- /dev/null +++ b/src/test/java/com/atwoz/alert/domain/AlertTest.java @@ -0,0 +1,55 @@ +package com.atwoz.alert.domain; + +import com.atwoz.alert.domain.vo.AlertGroup; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AlertTest { + + @Nested + class 알림_정상 { + + @Test + void 알림_정상_생성() { + // given + AlertGroup alertGroup = AlertGroup.MEMBER_LIKE; + String title = "알림 제목"; + String body = "알림 상세 내용"; + Long receiverId = 1L; + + // when + Alert alert = Alert.createWith(alertGroup, title, body, receiverId); + + // then + assertSoftly(softly -> { + softly.assertThat(alert.isRead()).isEqualTo(false); + softly.assertThat(alert.getGroup()).isEqualTo(alertGroup.getName()); + softly.assertThat(alert.getTitle()).isEqualTo(title); + softly.assertThat(alert.getBody()).isEqualTo(body); + }); + } + + @Test + void 알림_읽음() { + // given + AlertGroup alertGroup = AlertGroup.MEMBER_LIKE; + String title = "알림 제목"; + String body = "알림 상세 내용"; + Long receiverId = 1L; + Alert alert = Alert.createWith(alertGroup, title, body, receiverId); + + // when + alert.read(); + + // then + assertThat(alert.isRead()).isTrue(); + } + } +} From 432ab79dee09e271a19ea4df38d743902d4f693d Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 14:24:48 +0900 Subject: [PATCH 20/62] =?UTF-8?q?test:=20AlertJdbcRepository=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AlertJdbcRepository 테스트 작성 - 관련 Fixture 생성, 값을 작성하기 위해 빌더 패턴 추가 --- .../java/com/atwoz/alert/domain/Alert.java | 6 +++ .../global/domain/SoftDeleteBaseEntity.java | 5 +- .../com/atwoz/alert/fixture/AlertFixture.java | 22 ++++++++ .../AlertJdbcRepositoryTest.java | 54 +++++++++++++++++++ 4 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/atwoz/alert/fixture/AlertFixture.java create mode 100644 src/test/java/com/atwoz/alert/infrastructure/AlertJdbcRepositoryTest.java diff --git a/src/main/java/com/atwoz/alert/domain/Alert.java b/src/main/java/com/atwoz/alert/domain/Alert.java index b899dd5b..2221eed9 100644 --- a/src/main/java/com/atwoz/alert/domain/Alert.java +++ b/src/main/java/com/atwoz/alert/domain/Alert.java @@ -11,8 +11,14 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +@Getter +@SuperBuilder +@EqualsAndHashCode(of = "id", callSuper = false) @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity public class Alert extends SoftDeleteBaseEntity { diff --git a/src/main/java/com/atwoz/global/domain/SoftDeleteBaseEntity.java b/src/main/java/com/atwoz/global/domain/SoftDeleteBaseEntity.java index ff60bc14..736b1d45 100644 --- a/src/main/java/com/atwoz/global/domain/SoftDeleteBaseEntity.java +++ b/src/main/java/com/atwoz/global/domain/SoftDeleteBaseEntity.java @@ -2,14 +2,17 @@ import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; -import java.time.LocalDateTime; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import java.time.LocalDateTime; + @Getter @MappedSuperclass +@SuperBuilder @NoArgsConstructor @AllArgsConstructor @EntityListeners(AuditingEntityListener.class) diff --git a/src/test/java/com/atwoz/alert/fixture/AlertFixture.java b/src/test/java/com/atwoz/alert/fixture/AlertFixture.java new file mode 100644 index 00000000..f36e91fc --- /dev/null +++ b/src/test/java/com/atwoz/alert/fixture/AlertFixture.java @@ -0,0 +1,22 @@ +package com.atwoz.alert.fixture; + +import com.atwoz.alert.domain.Alert; +import com.atwoz.alert.domain.vo.AlertGroup; +import com.atwoz.alert.domain.vo.AlertMessage; + +import java.time.LocalDateTime; + +public class AlertFixture { + + public static Alert 옛날_알림_생성() { + return Alert.builder() + .isRead(false) + .alertGroup(AlertGroup.ALERT) + .alertMessage(AlertMessage.createWith("알림 제목", "알림 상세 내용")) + .receiverId(1L) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .deletedAt(null) + .build(); + } +} diff --git a/src/test/java/com/atwoz/alert/infrastructure/AlertJdbcRepositoryTest.java b/src/test/java/com/atwoz/alert/infrastructure/AlertJdbcRepositoryTest.java new file mode 100644 index 00000000..149aa484 --- /dev/null +++ b/src/test/java/com/atwoz/alert/infrastructure/AlertJdbcRepositoryTest.java @@ -0,0 +1,54 @@ +package com.atwoz.alert.infrastructure; + +import com.atwoz.alert.domain.Alert; +import com.atwoz.alert.domain.AlertRepository; +import com.atwoz.helper.IntegrationHelper; +import jakarta.persistence.EntityManager; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.auditing.AuditingHandler; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Optional; +import static com.atwoz.alert.fixture.AlertFixture.옛날_알림_생성; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AlertJdbcRepositoryTest extends IntegrationHelper { + + private static final int MINUS_DAY_FOR_DELETE_ALERT = 61; + + @Autowired + private AlertRepository alertRepository; + + @Autowired + private AuditingHandler auditingHandler; + + @Autowired + private EntityManager entityManager; + + @Test + @Transactional + void 생성된_지_60일이_초과된_알림은_삭제_상태로_변경된다() { + // given + LocalDateTime pastTime = LocalDateTime.now() + .minusDays(MINUS_DAY_FOR_DELETE_ALERT); + auditingHandler.setDateTimeProvider(() -> Optional.of(pastTime)); + + Alert alert = 옛날_알림_생성(); + entityManager.persist(alert); + entityManager.flush(); + entityManager.clear(); + + // when + alertRepository.deleteExpiredAlerts(); + + // then + Alert findAlert = entityManager.find(Alert.class, alert.getId()); + assertThat(findAlert.getDeletedAt()).isNotNull(); + } +} From 52196317d4e893ef6718a0f986783183e475f0cc Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 14:28:49 +0900 Subject: [PATCH 21/62] =?UTF-8?q?test:=20AlertJpaRepository=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AlertJpaRepository 테스트 작성 - 관련 Fixture 생성 --- .../com/atwoz/alert/fixture/AlertFixture.java | 12 +++++++ .../AlertJpaRepositoryTest.java | 34 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/test/java/com/atwoz/alert/infrastructure/AlertJpaRepositoryTest.java diff --git a/src/test/java/com/atwoz/alert/fixture/AlertFixture.java b/src/test/java/com/atwoz/alert/fixture/AlertFixture.java index f36e91fc..200607c2 100644 --- a/src/test/java/com/atwoz/alert/fixture/AlertFixture.java +++ b/src/test/java/com/atwoz/alert/fixture/AlertFixture.java @@ -19,4 +19,16 @@ public class AlertFixture { .deletedAt(null) .build(); } + + public static Alert 알림_생성_id_없음() { + return Alert.builder() + .isRead(false) + .alertGroup(AlertGroup.ALERT) + .alertMessage(AlertMessage.createWith("알림 제목", "알림 상세 내용")) + .receiverId(1L) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .deletedAt(null) + .build(); + } } diff --git a/src/test/java/com/atwoz/alert/infrastructure/AlertJpaRepositoryTest.java b/src/test/java/com/atwoz/alert/infrastructure/AlertJpaRepositoryTest.java new file mode 100644 index 00000000..8eebfc3b --- /dev/null +++ b/src/test/java/com/atwoz/alert/infrastructure/AlertJpaRepositoryTest.java @@ -0,0 +1,34 @@ +package com.atwoz.alert.infrastructure; + +import com.atwoz.alert.domain.Alert; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import static com.atwoz.alert.fixture.AlertFixture.알림_생성_id_없음; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +@DataJpaTest +class AlertJpaRepositoryTest { + + @Autowired + private AlertJpaRepository alertJpaRepository; + + @Test + void 알림_생성() { + // given + Alert alert = 알림_생성_id_없음(); + + // when + Alert savedAlert = alertJpaRepository.save(alert); + + // then + assertThat(alert).usingRecursiveComparison() + .ignoringFields("id") + .isEqualTo(savedAlert); + } +} From 2de88134aa9ea2902b21ee371eceeb6afd091bfd Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 15:21:25 +0900 Subject: [PATCH 22/62] =?UTF-8?q?test:=20AlertQueryRepository=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AlertQueryRepository 테스트 작성 - 페이징 조회 타입을 QueryResults로 변경 - AlertSearchResponse 반환 타입 변경 - 관련 Fixture 등록 --- .../infrastructure/AlertQueryRepository.java | 8 +- .../dto/AlertSearchResponse.java | 4 +- .../com/atwoz/alert/fixture/AlertFixture.java | 14 +++ .../AlertQueryRepositoryTest.java | 115 ++++++++++++++++++ 4 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java diff --git a/src/main/java/com/atwoz/alert/infrastructure/AlertQueryRepository.java b/src/main/java/com/atwoz/alert/infrastructure/AlertQueryRepository.java index f9428815..66724fd5 100644 --- a/src/main/java/com/atwoz/alert/infrastructure/AlertQueryRepository.java +++ b/src/main/java/com/atwoz/alert/infrastructure/AlertQueryRepository.java @@ -3,6 +3,7 @@ import com.atwoz.alert.domain.Alert; import com.atwoz.alert.infrastructure.dto.AlertContentSearchResponse; import com.atwoz.alert.infrastructure.dto.AlertSearchResponse; +import com.querydsl.core.QueryResults; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -10,7 +11,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; -import java.util.List; import java.util.Optional; import static com.atwoz.alert.domain.QAlert.alert; import static com.querydsl.core.types.Projections.constructor; @@ -22,7 +22,7 @@ public class AlertQueryRepository { private final JPAQueryFactory jpaQueryFactory; public Page findMemberAlertsWithPaging(final Long memberId, final Pageable pageable) { - List results = jpaQueryFactory.select( + QueryResults results = jpaQueryFactory.select( constructor(AlertSearchResponse.class, alert.id, alert.alertGroup, @@ -36,9 +36,9 @@ public Page findMemberAlertsWithPaging(final Long memberId, .orderBy(alert.createdAt.desc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) - .fetch(); + .fetchResults(); - return new PageImpl<>(results, pageable, results.size()); + return new PageImpl<>(results.getResults(), pageable, results.getTotal()); } public Optional findByMemberIdAndId(final Long memberId, final Long id) { diff --git a/src/main/java/com/atwoz/alert/infrastructure/dto/AlertSearchResponse.java b/src/main/java/com/atwoz/alert/infrastructure/dto/AlertSearchResponse.java index 4e70d9a0..15e5d9ea 100644 --- a/src/main/java/com/atwoz/alert/infrastructure/dto/AlertSearchResponse.java +++ b/src/main/java/com/atwoz/alert/infrastructure/dto/AlertSearchResponse.java @@ -1,10 +1,12 @@ package com.atwoz.alert.infrastructure.dto; +import com.atwoz.alert.domain.vo.AlertGroup; + import java.time.LocalDateTime; public record AlertSearchResponse( Long id, - String group, + AlertGroup group, AlertContentSearchResponse alert, Boolean isRead, LocalDateTime createdAt diff --git a/src/test/java/com/atwoz/alert/fixture/AlertFixture.java b/src/test/java/com/atwoz/alert/fixture/AlertFixture.java index 200607c2..bf13ac2d 100644 --- a/src/test/java/com/atwoz/alert/fixture/AlertFixture.java +++ b/src/test/java/com/atwoz/alert/fixture/AlertFixture.java @@ -31,4 +31,18 @@ public class AlertFixture { .deletedAt(null) .build(); } + + public static Alert 알림_생성_제목_날짜_주입(final String title, final int day) { + return Alert.builder() + .isRead(false) + .alertGroup(AlertGroup.ALERT) + .alertMessage(AlertMessage.createWith(title, "알림 상세 내용")) + .receiverId(1L) + .createdAt(LocalDateTime.now() + .plusDays(day)) + .updatedAt(LocalDateTime.now() + .plusDays(day)) + .deletedAt(null) + .build(); + } } diff --git a/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java b/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java new file mode 100644 index 00000000..4b2c5f1c --- /dev/null +++ b/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java @@ -0,0 +1,115 @@ +package com.atwoz.alert.infrastructure; + +import com.atwoz.alert.domain.Alert; +import com.atwoz.alert.domain.AlertRepository; +import com.atwoz.alert.fixture.AlertFixture; +import com.atwoz.alert.infrastructure.dto.AlertContentSearchResponse; +import com.atwoz.alert.infrastructure.dto.AlertSearchResponse; +import com.atwoz.helper.IntegrationHelper; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import static com.atwoz.alert.fixture.AlertFixture.알림_생성_id_없음; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AlertQueryRepositoryTest extends IntegrationHelper { + + @Autowired + private AlertQueryRepository alertQueryRepository; + + @Autowired + private AlertRepository alertRepository; + + @Nested + class 알림_조회_정상 { + + @Test + void 받은_알림_페이징_조회() { + // given + Long senderId = 1L; + List alerts = new ArrayList<>(); + + for (int day = 1; day <= 10; day++) { + Alert alert = AlertFixture.알림_생성_제목_날짜_주입("알림 제목 " + day, day); + alertRepository.save(alert); + alerts.add(alert); + } + + PageRequest pageRequest = PageRequest.of(0, 9); + + // when + Page found = alertQueryRepository.findMemberAlertsWithPaging(senderId, pageRequest); + + // then + List expected = extractAlertResponsesWithLimit(alerts, 9); + assertSoftly(softly -> { + softly.assertThat(found).hasSize(9); + softly.assertThat(found.hasNext()).isTrue(); + softly.assertThat(found.getContent()).isEqualTo(expected); + }); + } + + private List extractAlertResponsesWithLimit(final List alerts, final int limit) { + return alerts.stream() + .sorted(Comparator.comparing(Alert::getCreatedAt) + .reversed()) + .map(alert -> new AlertSearchResponse( + alert.getId(), + alert.getAlertGroup(), + new AlertContentSearchResponse(alert.getTitle(), alert.getBody()), + alert.getIsRead(), + alert.getCreatedAt() + )) + .limit(limit) + .toList(); + } + + @Test + void 생성_후_조회() { + // given + Long senderId = 1L; + Long alertId = 1L; + Alert alert = 알림_생성_id_없음(); + alertRepository.save(alert); + + // when + Optional found = alertQueryRepository.findByMemberIdAndId(senderId, alertId); + + // then + assertSoftly(softly -> { + softly.assertThat(found).isNotEmpty(); + softly.assertThat(found.get()).usingRecursiveComparison() + .ignoringFields("id") + .isEqualTo(alert); + }); + } + } + + @Test + void 저장되지_않은_알림_id는_조회되지_않는다() { + // given + Long senderId = 1L; + + Alert alert = 알림_생성_id_없음(); + Alert savedAlert = alertRepository.save(alert); + Long alertId = savedAlert.getId() + 1L; + + // when + Optional found = alertQueryRepository.findByMemberIdAndId(senderId, alertId); + + // then + assertThat(found).isEmpty(); + } +} From 9a8cc59de67270b05ce538facc4255c4b8533d5b Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 15:33:45 +0900 Subject: [PATCH 23/62] =?UTF-8?q?refactor:=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 레디스 의존성을 드러내기 위해 파일 이름 변경 --- ...seTokenRepository.java => FirebaseRedisTokenRepository.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/java/com/atwoz/alert/infrastructure/{FirebaseTokenRepository.java => FirebaseRedisTokenRepository.java} (94%) diff --git a/src/main/java/com/atwoz/alert/infrastructure/FirebaseTokenRepository.java b/src/main/java/com/atwoz/alert/infrastructure/FirebaseRedisTokenRepository.java similarity index 94% rename from src/main/java/com/atwoz/alert/infrastructure/FirebaseTokenRepository.java rename to src/main/java/com/atwoz/alert/infrastructure/FirebaseRedisTokenRepository.java index 3bc84121..99afddec 100644 --- a/src/main/java/com/atwoz/alert/infrastructure/FirebaseTokenRepository.java +++ b/src/main/java/com/atwoz/alert/infrastructure/FirebaseRedisTokenRepository.java @@ -10,7 +10,7 @@ @RequiredArgsConstructor @Repository -public class FirebaseTokenRepository implements AlertTokenRepository { +public class FirebaseRedisTokenRepository implements AlertTokenRepository { private final StringRedisTemplate tokenRedisTemplate; From dd8245603c881a5647d87ecee197071fc10ae7f9 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 15:34:30 +0900 Subject: [PATCH 24/62] =?UTF-8?q?test:=20FakeAlertTokenRepository=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FakeAlertTokenRepository 작성 --- .../FakeAlertTokenRepository.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/test/java/com/atwoz/alert/infrastructure/FakeAlertTokenRepository.java diff --git a/src/test/java/com/atwoz/alert/infrastructure/FakeAlertTokenRepository.java b/src/test/java/com/atwoz/alert/infrastructure/FakeAlertTokenRepository.java new file mode 100644 index 00000000..4528b8bd --- /dev/null +++ b/src/test/java/com/atwoz/alert/infrastructure/FakeAlertTokenRepository.java @@ -0,0 +1,39 @@ +package com.atwoz.alert.infrastructure; + +import com.atwoz.alert.domain.AlertTokenRepository; +import com.atwoz.alert.exception.exceptions.ReceiverTokenNotFoundException; + +import java.util.HashMap; +import java.util.Map; + +public class FakeAlertTokenRepository implements AlertTokenRepository { + + private final Map map = new HashMap<>(); + + @Override + public void saveToken(final Long id, final String token) { + map.put(id, token); + } + + @Override + public String getToken(final Long id) { + validateTokenExistence(id); + return map.get(id); + } + + private void validateTokenExistence(final Long id) { + if (!map.containsKey(id)) { + throw new ReceiverTokenNotFoundException(); + } + } + + @Override + public void deleteToken(final Long id) { + map.remove(id); + } + + @Override + public boolean hasKey(final Long id) { + return map.containsKey(id); + } +} From 56b3ad5b0b2e2faa356df763938c7c09e2bec1c3 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 15:48:07 +0900 Subject: [PATCH 25/62] =?UTF-8?q?test:=20FakeAlertRepository=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FakeAlertRepository 작성 --- .../infrastructure/FakeAlertRepository.java | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/test/java/com/atwoz/alert/infrastructure/FakeAlertRepository.java diff --git a/src/test/java/com/atwoz/alert/infrastructure/FakeAlertRepository.java b/src/test/java/com/atwoz/alert/infrastructure/FakeAlertRepository.java new file mode 100644 index 00000000..4c92acac --- /dev/null +++ b/src/test/java/com/atwoz/alert/infrastructure/FakeAlertRepository.java @@ -0,0 +1,102 @@ +package com.atwoz.alert.infrastructure; + +import com.atwoz.alert.domain.Alert; +import com.atwoz.alert.domain.AlertRepository; +import com.atwoz.alert.infrastructure.dto.AlertContentSearchResponse; +import com.atwoz.alert.infrastructure.dto.AlertSearchResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class FakeAlertRepository implements AlertRepository { + + private static final int DELETION_THRESHOLD_DATE = 61; + + private final Map map = new HashMap<>(); + private Long id = 1L; + + @Override + public Alert save(final Alert alert) { + Alert savedAlert = Alert.builder() + .id(id) + .isRead(alert.getIsRead()) + .alertMessage(alert.getAlertMessage()) + .alertGroup(alert.getAlertGroup()) + .createdAt(alert.getCreatedAt()) + .updatedAt(alert.getUpdatedAt()) + .deletedAt(alert.getDeletedAt()) + .build(); + map.put(id, savedAlert); + + return map.get(id++); + } + + @Override + public Page findMemberAlertsWithPaging(Long memberId, Pageable pageable) { + List alertResponses = map.values() + .stream() + .filter(alert -> memberId.equals(alert.getReceiverId())) + .sorted(Comparator.comparing(Alert::getCreatedAt) + .reversed()) + .skip(pageable.getOffset()) + .limit(pageable.getPageSize()) + .map(alert -> new AlertSearchResponse( + alert.getId(), + alert.getAlertGroup(), + new AlertContentSearchResponse(alert.getTitle(), alert.getBody()), + alert.getIsRead(), + alert.getCreatedAt() + )) + .toList(); + + return new PageImpl<>(alertResponses, pageable, map.size()); + } + + @Override + public Optional findByMemberIdAndId(final Long memberId, final Long id) { + return map.values() + .stream() + .filter(alert -> isSameTargetAlert(alert, memberId, id)) + .findAny(); + } + + private boolean isSameTargetAlert(final Alert alert, final Long memberId, final Long id) { + return memberId.equals(alert.getReceiverId()) && id.equals(alert.getId()); + } + + @Override + public void deleteExpiredAlerts() { + map.values() + .stream() + .filter(this::isExpiredAlert) + .forEach(this::convertDeleted); + } + + private boolean isExpiredAlert(final Alert alert) { + LocalDateTime sixtyDaysAgo = LocalDateTime.now() + .minusDays(DELETION_THRESHOLD_DATE); + + return alert.getCreatedAt() + .isBefore(sixtyDaysAgo); + } + + private void convertDeleted(final Alert alert) { + Alert deletedAlert = Alert.builder() + .id(alert.getId()) + .isRead(alert.getIsRead()) + .alertMessage(alert.getAlertMessage()) + .alertGroup(alert.getAlertGroup()) + .createdAt(alert.getCreatedAt()) + .updatedAt(alert.getUpdatedAt()) + .deletedAt(LocalDateTime.now()) + .build(); + map.put(alert.getId(), deletedAlert); + } +} From 3a21613e65dd2cb04cfed469254b7d172a939db0 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 16:26:46 +0900 Subject: [PATCH 26/62] =?UTF-8?q?test:=20AlertService=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AlertService 테스트 작성 - 관련 Fixture 등록 - FakeAlertRepository 문제 수정 --- .../alert/application/AlertServiceTest.java | 201 ++++++++++++++++++ .../com/atwoz/alert/fixture/AlertFixture.java | 22 +- .../AlertJdbcRepositoryTest.java | 4 +- .../infrastructure/FakeAlertManager.java | 12 ++ .../infrastructure/FakeAlertRepository.java | 1 + 5 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 src/test/java/com/atwoz/alert/application/AlertServiceTest.java create mode 100644 src/test/java/com/atwoz/alert/infrastructure/FakeAlertManager.java diff --git a/src/test/java/com/atwoz/alert/application/AlertServiceTest.java b/src/test/java/com/atwoz/alert/application/AlertServiceTest.java new file mode 100644 index 00000000..4109c174 --- /dev/null +++ b/src/test/java/com/atwoz/alert/application/AlertServiceTest.java @@ -0,0 +1,201 @@ +package com.atwoz.alert.application; + +import com.atwoz.alert.domain.Alert; +import com.atwoz.alert.domain.AlertManager; +import com.atwoz.alert.domain.AlertRepository; +import com.atwoz.alert.domain.AlertTokenRepository; +import com.atwoz.alert.domain.vo.AlertGroup; +import com.atwoz.alert.exception.exceptions.AlertNotFoundException; +import com.atwoz.alert.exception.exceptions.ReceiverTokenNotFoundException; +import com.atwoz.alert.infrastructure.FakeAlertManager; +import com.atwoz.alert.infrastructure.FakeAlertRepository; +import com.atwoz.alert.infrastructure.FakeAlertTokenRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.Optional; +import static com.atwoz.alert.fixture.AlertFixture.알림_생성_id_없음; +import static com.atwoz.alert.fixture.AlertFixture.알림_생성_id_있음; +import static com.atwoz.alert.fixture.AlertFixture.옛날_알림_생성; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AlertServiceTest { + + private AlertService alertService; + private AlertRepository alertRepository; + private AlertTokenRepository tokenRepository; + private AlertManager alertManager; + + @BeforeEach + void init() { + alertRepository = new FakeAlertRepository(); + tokenRepository = new FakeAlertTokenRepository(); + alertManager = new FakeAlertManager(); + alertService = new AlertService(alertRepository, tokenRepository, alertManager); + } + + @Nested + class 토큰_관리 { + + @Test + void 토큰을_저장한다() { + // given + Long id = 1L; + String token = "token"; + + // when + alertService.saveToken(id, token); + + // then + assertSoftly(softly -> { + softly.assertThat(tokenRepository.hasKey(id)).isTrue(); + softly.assertThat(tokenRepository.getToken(id)).isEqualTo(token); + }); + } + + @Test + void 토큰을_저장하면_조회_시_가져올_수_있다() { + // given + Long id = 1L; + String token = "token"; + + // when + alertService.saveToken(id, token); + + // then + assertDoesNotThrow(() -> tokenRepository.getToken(id)); + } + + @Test + void 저장되지_않은_토큰을_조회하면_예외가_발생한다() { + // given + Long id = 1L; + Long otherId = 2L; + String token = "token"; + + // when + alertService.saveToken(id, token); + + // then + assertThatThrownBy(() -> tokenRepository.getToken(otherId)) + .isInstanceOf(ReceiverTokenNotFoundException.class); + } + } + + @Nested + class 알림_전송_관리 { + + @Test + void 알림_전송_정상() { + // given + AlertGroup group = AlertGroup.ALERT; + String title = "알림 제목"; + String body = "알림 상세 내용"; + String sender = "보낸 사람 닉네임"; + Long id = 1L; + Long alertId = 1L; + String token = "token"; + alertService.saveToken(id, token); + + // when + alertService.sendAlert(group, title, body, sender, id); + + // then + assertSoftly(softly -> { + Optional found = alertRepository.findByMemberIdAndId(id, alertId); + softly.assertThat(found).isNotEmpty(); + softly.assertThat(found.get()).usingRecursiveComparison() + .ignoringActualNullFields() + .isEqualTo(알림_생성_id_있음()); + }); + } + + @Test + void 알림_전송_시_저장되지_않은_토큰을_쓰면_예외가_발생한다() { + // given + AlertGroup group = AlertGroup.ALERT; + String title = "알림 제목"; + String body = "알림 상세 내용"; + String sender = "보낸 사람 닉네임"; + Long id = 1L; + + // when & then + assertThatThrownBy(() -> alertService.sendAlert(group, title, body, sender, id)) + .isInstanceOf(ReceiverTokenNotFoundException.class); + } + } + + @Nested + class 알림_단일_조회_관리 { + + @Test + void 알림_단일_조회_정상() { + // given + AlertGroup group = AlertGroup.ALERT; + String title = "알림 제목"; + String body = "알림 상세 내용"; + String sender = "보낸 사람 닉네임"; + Long id = 1L; + Long alertId = 1L; + String token = "token"; + alertService.saveToken(id, token); + alertService.sendAlert(group, title, body, sender, id); + + // when + alertService.readAlert(id, alertId); + + // then + Optional savedAlert = alertRepository.findByMemberIdAndId(id, alertId); + assertSoftly(softly -> { + softly.assertThat(savedAlert).isPresent(); + Alert alert = savedAlert.get(); + softly.assertThat(alert.isRead()).isTrue(); + }); + } + + @Test + void 존재하지_않는_알림을_조회할_경우_예외가_발생한다() { + // given + AlertGroup group = AlertGroup.ALERT; + String title = "알림 제목"; + String body = "알림 상세 내용"; + String sender = "보낸 사람 닉네임"; + Long id = 1L; + Long otherId = 2L; + String token = "token"; + alertService.saveToken(id, token); + alertService.sendAlert(group, title, body, sender, id); + + // when & then + assertThatThrownBy(() -> alertService.readAlert(id, otherId)) + .isInstanceOf(AlertNotFoundException.class); + } + } + + @Test + void 생성된_지_60일을_초과한_알림은_삭제_상태로_된다() { + // given + Long memberId = 1L; + Alert savedAlert = alertRepository.save(알림_생성_id_없음()); + Alert savedOldAlert = alertRepository.save(옛날_알림_생성()); + + // when + alertService.deleteExpiredAlerts(); + + // then + Optional foundSavedAlert = alertRepository.findByMemberIdAndId(memberId, savedAlert.getId()); + Optional foundSavedOldAlert = alertRepository.findByMemberIdAndId(memberId, savedOldAlert.getId()); + + assertSoftly(softly -> { + softly.assertThat(foundSavedAlert).isPresent(); + softly.assertThat(foundSavedOldAlert).isEmpty(); + }); + } +} diff --git a/src/test/java/com/atwoz/alert/fixture/AlertFixture.java b/src/test/java/com/atwoz/alert/fixture/AlertFixture.java index bf13ac2d..0362dfe2 100644 --- a/src/test/java/com/atwoz/alert/fixture/AlertFixture.java +++ b/src/test/java/com/atwoz/alert/fixture/AlertFixture.java @@ -6,16 +6,21 @@ import java.time.LocalDateTime; +@SuppressWarnings("NonAsciiCharacters") public class AlertFixture { + private static final int DELETION_THRESHOLD = 61; + public static Alert 옛날_알림_생성() { return Alert.builder() .isRead(false) .alertGroup(AlertGroup.ALERT) .alertMessage(AlertMessage.createWith("알림 제목", "알림 상세 내용")) .receiverId(1L) - .createdAt(LocalDateTime.now()) - .updatedAt(LocalDateTime.now()) + .createdAt(LocalDateTime.now() + .minusDays(DELETION_THRESHOLD)) + .updatedAt(LocalDateTime.now() + .minusDays(DELETION_THRESHOLD)) .deletedAt(null) .build(); } @@ -45,4 +50,17 @@ public class AlertFixture { .deletedAt(null) .build(); } + + public static Alert 알림_생성_id_있음() { + return Alert.builder() + .id(1L) + .isRead(false) + .alertGroup(AlertGroup.ALERT) + .alertMessage(AlertMessage.createWith("알림 제목", "알림 상세 내용")) + .receiverId(1L) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .deletedAt(null) + .build(); + } } diff --git a/src/test/java/com/atwoz/alert/infrastructure/AlertJdbcRepositoryTest.java b/src/test/java/com/atwoz/alert/infrastructure/AlertJdbcRepositoryTest.java index 149aa484..0342dc9e 100644 --- a/src/test/java/com/atwoz/alert/infrastructure/AlertJdbcRepositoryTest.java +++ b/src/test/java/com/atwoz/alert/infrastructure/AlertJdbcRepositoryTest.java @@ -13,7 +13,7 @@ import java.time.LocalDateTime; import java.util.Optional; -import static com.atwoz.alert.fixture.AlertFixture.옛날_알림_생성; +import static com.atwoz.alert.fixture.AlertFixture.알림_생성_id_없음; import static org.assertj.core.api.Assertions.assertThat; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @@ -39,7 +39,7 @@ class AlertJdbcRepositoryTest extends IntegrationHelper { .minusDays(MINUS_DAY_FOR_DELETE_ALERT); auditingHandler.setDateTimeProvider(() -> Optional.of(pastTime)); - Alert alert = 옛날_알림_생성(); + Alert alert = 알림_생성_id_없음(); entityManager.persist(alert); entityManager.flush(); entityManager.clear(); diff --git a/src/test/java/com/atwoz/alert/infrastructure/FakeAlertManager.java b/src/test/java/com/atwoz/alert/infrastructure/FakeAlertManager.java new file mode 100644 index 00000000..3ba90e0b --- /dev/null +++ b/src/test/java/com/atwoz/alert/infrastructure/FakeAlertManager.java @@ -0,0 +1,12 @@ +package com.atwoz.alert.infrastructure; + +import com.atwoz.alert.domain.Alert; +import com.atwoz.alert.domain.AlertManager; + +public class FakeAlertManager implements AlertManager { + + @Override + public void send(final Alert alert, final String sender, final String token) { + return; + } +} diff --git a/src/test/java/com/atwoz/alert/infrastructure/FakeAlertRepository.java b/src/test/java/com/atwoz/alert/infrastructure/FakeAlertRepository.java index 4c92acac..cfa32014 100644 --- a/src/test/java/com/atwoz/alert/infrastructure/FakeAlertRepository.java +++ b/src/test/java/com/atwoz/alert/infrastructure/FakeAlertRepository.java @@ -29,6 +29,7 @@ public Alert save(final Alert alert) { .isRead(alert.getIsRead()) .alertMessage(alert.getAlertMessage()) .alertGroup(alert.getAlertGroup()) + .receiverId(alert.getReceiverId()) .createdAt(alert.getCreatedAt()) .updatedAt(alert.getUpdatedAt()) .deletedAt(alert.getDeletedAt()) From 5fa61fd0a91dc98d144c4073e5c47fb7c567cc49 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 16:37:53 +0900 Subject: [PATCH 27/62] =?UTF-8?q?fix:=20=EC=95=8C=EB=A6=BC=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=20=EA=B8=B0=EC=A4=80=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알림 정렬 조건에 id 추가되도록 수정 --- .../atwoz/alert/infrastructure/AlertQueryRepository.java | 2 +- .../alert/infrastructure/AlertQueryRepositoryTest.java | 8 ++++---- .../atwoz/alert/infrastructure/FakeAlertRepository.java | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/atwoz/alert/infrastructure/AlertQueryRepository.java b/src/main/java/com/atwoz/alert/infrastructure/AlertQueryRepository.java index 66724fd5..14ad1770 100644 --- a/src/main/java/com/atwoz/alert/infrastructure/AlertQueryRepository.java +++ b/src/main/java/com/atwoz/alert/infrastructure/AlertQueryRepository.java @@ -33,7 +33,7 @@ public Page findMemberAlertsWithPaging(final Long memberId, alert.createdAt)) .from(alert) .where(alert.receiverId.eq(memberId), alert.deletedAt.isNull()) - .orderBy(alert.createdAt.desc()) + .orderBy(alert.createdAt.desc(), alert.id.desc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetchResults(); diff --git a/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java b/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java index 4b2c5f1c..040cd31e 100644 --- a/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java +++ b/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java @@ -2,7 +2,6 @@ import com.atwoz.alert.domain.Alert; import com.atwoz.alert.domain.AlertRepository; -import com.atwoz.alert.fixture.AlertFixture; import com.atwoz.alert.infrastructure.dto.AlertContentSearchResponse; import com.atwoz.alert.infrastructure.dto.AlertSearchResponse; import com.atwoz.helper.IntegrationHelper; @@ -19,6 +18,7 @@ import java.util.List; import java.util.Optional; import static com.atwoz.alert.fixture.AlertFixture.알림_생성_id_없음; +import static com.atwoz.alert.fixture.AlertFixture.알림_생성_제목_날짜_주입; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; @@ -42,7 +42,7 @@ class 알림_조회_정상 { List alerts = new ArrayList<>(); for (int day = 1; day <= 10; day++) { - Alert alert = AlertFixture.알림_생성_제목_날짜_주입("알림 제목 " + day, day); + Alert alert = 알림_생성_제목_날짜_주입("알림 제목 " + day, day); alertRepository.save(alert); alerts.add(alert); } @@ -63,8 +63,8 @@ class 알림_조회_정상 { private List extractAlertResponsesWithLimit(final List alerts, final int limit) { return alerts.stream() - .sorted(Comparator.comparing(Alert::getCreatedAt) - .reversed()) + .sorted(Comparator.comparing(Alert::getCreatedAt).reversed() + .thenComparing(Alert::getId).reversed()) .map(alert -> new AlertSearchResponse( alert.getId(), alert.getAlertGroup(), diff --git a/src/test/java/com/atwoz/alert/infrastructure/FakeAlertRepository.java b/src/test/java/com/atwoz/alert/infrastructure/FakeAlertRepository.java index cfa32014..53d5d26d 100644 --- a/src/test/java/com/atwoz/alert/infrastructure/FakeAlertRepository.java +++ b/src/test/java/com/atwoz/alert/infrastructure/FakeAlertRepository.java @@ -44,8 +44,8 @@ public Page findMemberAlertsWithPaging(Long memberId, Pagea List alertResponses = map.values() .stream() .filter(alert -> memberId.equals(alert.getReceiverId())) - .sorted(Comparator.comparing(Alert::getCreatedAt) - .reversed()) + .sorted(Comparator.comparing(Alert::getCreatedAt).reversed() + .thenComparing(Alert::getId).reversed()) .skip(pageable.getOffset()) .limit(pageable.getPageSize()) .map(alert -> new AlertSearchResponse( From 02d6b836c610c140b19a9d5c01fdb7371f722fab Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 16:50:44 +0900 Subject: [PATCH 28/62] =?UTF-8?q?test:=20AlertQueryService=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AlertQueryService 테스트 작성 - 관련 Fixture 등록 --- .../application/AlertQueryServiceTest.java | 73 +++++++++++++++++++ .../com/atwoz/alert/fixture/AlertFixture.java | 15 ++++ 2 files changed, 88 insertions(+) create mode 100644 src/test/java/com/atwoz/alert/application/AlertQueryServiceTest.java diff --git a/src/test/java/com/atwoz/alert/application/AlertQueryServiceTest.java b/src/test/java/com/atwoz/alert/application/AlertQueryServiceTest.java new file mode 100644 index 00000000..1d037e10 --- /dev/null +++ b/src/test/java/com/atwoz/alert/application/AlertQueryServiceTest.java @@ -0,0 +1,73 @@ +package com.atwoz.alert.application; + +import com.atwoz.alert.domain.Alert; +import com.atwoz.alert.domain.AlertRepository; +import com.atwoz.alert.infrastructure.FakeAlertRepository; +import com.atwoz.alert.infrastructure.dto.AlertContentSearchResponse; +import com.atwoz.alert.infrastructure.dto.AlertPagingResponse; +import com.atwoz.alert.infrastructure.dto.AlertSearchResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.PageRequest; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import static com.atwoz.alert.fixture.AlertFixture.알림_생성_제목_날짜_id_주입; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AlertQueryServiceTest { + + private AlertQueryService alertQueryService; + private AlertRepository alertRepository; + + @BeforeEach + void init() { + alertRepository = new FakeAlertRepository(); + alertQueryService = new AlertQueryService(alertRepository); + } + + @Test + void 받은_알림들을_페이징_조회한다() { + // given + Long memberId = 1L; + List alerts = new ArrayList<>(); + PageRequest request = PageRequest.of(0, 9); + + for (int day = 1; day <= 10; day++) { + Alert alert = 알림_생성_제목_날짜_id_주입("알림 제목 " + day, day, day); + alerts.add(alert); + alertRepository.save(alert); + } + + // when + AlertPagingResponse response = alertQueryService.findMemberAlertsWithPaging(memberId, request); + List expected = extractAlertResponsesWithLimit(alerts, 9); + + // then + assertSoftly(softly -> { + softly.assertThat(response.alerts()).hasSize(9); + softly.assertThat(response.nextPage()).isEqualTo(1); + softly.assertThat(response.alerts()).isEqualTo(expected); + }); + } + + private List extractAlertResponsesWithLimit(final List alerts, final int limit) { + return alerts.stream() + .sorted(Comparator.comparing(Alert::getCreatedAt).reversed() + .thenComparing(Alert::getId).reversed()) + .map(alert -> new AlertSearchResponse( + alert.getId(), + alert.getAlertGroup(), + new AlertContentSearchResponse(alert.getTitle(), alert.getBody()), + alert.getIsRead(), + alert.getCreatedAt() + )) + .limit(limit) + .toList(); + } +} diff --git a/src/test/java/com/atwoz/alert/fixture/AlertFixture.java b/src/test/java/com/atwoz/alert/fixture/AlertFixture.java index 0362dfe2..bfcc073a 100644 --- a/src/test/java/com/atwoz/alert/fixture/AlertFixture.java +++ b/src/test/java/com/atwoz/alert/fixture/AlertFixture.java @@ -51,6 +51,21 @@ public class AlertFixture { .build(); } + public static Alert 알림_생성_제목_날짜_id_주입(final String title, final int day, final long id) { + return Alert.builder() + .id(id) + .isRead(false) + .alertGroup(AlertGroup.ALERT) + .alertMessage(AlertMessage.createWith(title, "알림 상세 내용")) + .receiverId(1L) + .createdAt(LocalDateTime.now() + .plusDays(day)) + .updatedAt(LocalDateTime.now() + .plusDays(day)) + .deletedAt(null) + .build(); + } + public static Alert 알림_생성_id_있음() { return Alert.builder() .id(1L) From 7aa4f70b8bb0131592c7fa54ae4ea4ae86da4fe9 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 17:40:26 +0900 Subject: [PATCH 29/62] =?UTF-8?q?fix:=20FirebaseApp=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FirebaseApp 중복 문제 해결 --- src/main/java/com/atwoz/alert/config/FirebaseConfig.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/atwoz/alert/config/FirebaseConfig.java b/src/main/java/com/atwoz/alert/config/FirebaseConfig.java index 24b48702..fdfb554c 100644 --- a/src/main/java/com/atwoz/alert/config/FirebaseConfig.java +++ b/src/main/java/com/atwoz/alert/config/FirebaseConfig.java @@ -17,6 +17,9 @@ public class FirebaseConfig { @Bean public FirebaseApp firebaseApp() { + if (!FirebaseApp.getApps().isEmpty()) { + return FirebaseApp.getInstance(); + } try { FileInputStream firebaseFile = new FileInputStream(FIREBASE_URL); FirebaseOptions options = new FirebaseOptions.Builder() From 7afc2711601ffa7abbe5d8f704e4b1f6969637f4 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 17:41:16 +0900 Subject: [PATCH 30/62] =?UTF-8?q?test:=20=EB=8B=A8=EC=9D=BC=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 단일 알림 조회 테스트 수정 - API 문서 작성 --- src/docs/asciidoc/alert.adoc | 28 ++++ src/docs/asciidoc/index.adoc | 1 + .../atwoz/alert/application/AlertService.java | 3 +- .../dto/AlertSearchResponse.java | 10 ++ .../com/atwoz/alert/ui/AlertController.java | 9 +- .../alert/ui/AlertControllerWebMvcTest.java | 136 ++++++++++++++++++ .../com/atwoz/helper/MockBeanInjection.java | 10 ++ 7 files changed, 192 insertions(+), 5 deletions(-) create mode 100644 src/docs/asciidoc/alert.adoc create mode 100644 src/test/java/com/atwoz/alert/ui/AlertControllerWebMvcTest.java diff --git a/src/docs/asciidoc/alert.adoc b/src/docs/asciidoc/alert.adoc new file mode 100644 index 00000000..b2e2149c --- /dev/null +++ b/src/docs/asciidoc/alert.adoc @@ -0,0 +1,28 @@ +:toc: left +:source-highlighter: highlightjs +:sectlinks: +:toclevels: 2 +:sectlinks: +:sectnums: + +== Alert + +=== 알림 페이징 조회 (GET api/members/me/alerts) +==== 요청 +include::{snippets}/alert-controller-web-mvc-test/받은_알림_페이징조회/request-headers.adoc[] +include::{snippets}/alert-controller-web-mvc-test/받은_알림_페이징조회/request-parts.adoc[] +include::{snippets}/alert-controller-web-mvc-test/받은_알림_페이징조회/http-request.adoc[] +==== 응답 +include::{snippets}/alert-controller-web-mvc-test/받은_알림_페이징조회/response-fields.adoc[] +include::{snippets}/alert-controller-web-mvc-test/받은_알림_페이징조회/http-response.adoc[] + +=== 알림 단일 조회 (GET api/members/me/alerts/{alertId}) +읽지 않았던 알림일 경우 읽음 처리됨 + +==== 요청 +include::{snippets}/alert-controller-web-mvc-test/단일_알림_조회/request-headers.adoc[] +include::{snippets}/alert-controller-web-mvc-test/단일_알림_조회/path-parameters.adoc[] +include::{snippets}/alert-controller-web-mvc-test/단일_알림_조회/http-request.adoc[] +==== 응답 +include::{snippets}/alert-controller-web-mvc-test/단일_알림_조회/response-fields.adoc[] +include::{snippets}/alert-controller-web-mvc-test/단일_알림_조회/http-response.adoc[] diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 4f2c51d8..854f9d47 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -13,3 +13,4 @@ * link:membersurveys.html[회원 연애고사 API] * link:report.html[회원 신고 API] * link:likes.html[호감 API] +* link:alert.html[알림 API] diff --git a/src/main/java/com/atwoz/alert/application/AlertService.java b/src/main/java/com/atwoz/alert/application/AlertService.java index f6a48ca4..07e2100f 100644 --- a/src/main/java/com/atwoz/alert/application/AlertService.java +++ b/src/main/java/com/atwoz/alert/application/AlertService.java @@ -35,13 +35,14 @@ public void sendAlert(final AlertGroup group, final String title, final String b alertRepository.save(alert); } - public void readAlert(final Long memberId, final Long id) { + public Alert readAlert(final Long memberId, final Long id) { Optional alert = alertRepository.findByMemberIdAndId(memberId, id); if (alert.isEmpty()) { throw new AlertNotFoundException(); } Alert targetAlert = alert.get(); targetAlert.read(); + return targetAlert; } @Scheduled(cron = MIDNIGHT) diff --git a/src/main/java/com/atwoz/alert/infrastructure/dto/AlertSearchResponse.java b/src/main/java/com/atwoz/alert/infrastructure/dto/AlertSearchResponse.java index 15e5d9ea..6012a210 100644 --- a/src/main/java/com/atwoz/alert/infrastructure/dto/AlertSearchResponse.java +++ b/src/main/java/com/atwoz/alert/infrastructure/dto/AlertSearchResponse.java @@ -1,5 +1,6 @@ package com.atwoz.alert.infrastructure.dto; +import com.atwoz.alert.domain.Alert; import com.atwoz.alert.domain.vo.AlertGroup; import java.time.LocalDateTime; @@ -11,4 +12,13 @@ public record AlertSearchResponse( Boolean isRead, LocalDateTime createdAt ) { + + public static AlertSearchResponse from(final Alert alert) { + return new AlertSearchResponse(alert.getId(), + alert.getAlertGroup(), + new AlertContentSearchResponse(alert.getTitle(), alert.getBody()), + alert.getIsRead(), + alert.getCreatedAt() + ); + } } diff --git a/src/main/java/com/atwoz/alert/ui/AlertController.java b/src/main/java/com/atwoz/alert/ui/AlertController.java index 91f01cd5..c02c9d99 100644 --- a/src/main/java/com/atwoz/alert/ui/AlertController.java +++ b/src/main/java/com/atwoz/alert/ui/AlertController.java @@ -2,7 +2,9 @@ import com.atwoz.alert.application.AlertQueryService; import com.atwoz.alert.application.AlertService; +import com.atwoz.alert.domain.Alert; import com.atwoz.alert.infrastructure.dto.AlertPagingResponse; +import com.atwoz.alert.infrastructure.dto.AlertSearchResponse; import com.atwoz.member.ui.auth.support.AuthMember; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; @@ -32,9 +34,8 @@ public ResponseEntity findMemberAlertsWithPaging( } @GetMapping("/{id}") - public ResponseEntity readAlert(@AuthMember final Long memberId, @PathVariable final Long id) { - alertService.readAlert(memberId, id); - return ResponseEntity.ok() - .build(); + public ResponseEntity readAlert(@AuthMember final Long memberId, @PathVariable final Long id) { + Alert alert = alertService.readAlert(memberId, id); + return ResponseEntity.ok(AlertSearchResponse.from(alert)); } } diff --git a/src/test/java/com/atwoz/alert/ui/AlertControllerWebMvcTest.java b/src/test/java/com/atwoz/alert/ui/AlertControllerWebMvcTest.java new file mode 100644 index 00000000..c30c0e4d --- /dev/null +++ b/src/test/java/com/atwoz/alert/ui/AlertControllerWebMvcTest.java @@ -0,0 +1,136 @@ +package com.atwoz.alert.ui; + +import com.atwoz.alert.application.AlertQueryService; +import com.atwoz.alert.application.AlertService; +import com.atwoz.alert.domain.Alert; +import com.atwoz.alert.domain.vo.AlertGroup; +import com.atwoz.alert.infrastructure.dto.AlertContentSearchResponse; +import com.atwoz.alert.infrastructure.dto.AlertPagingResponse; +import com.atwoz.alert.infrastructure.dto.AlertSearchResponse; +import com.atwoz.helper.MockBeanInjection; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.data.domain.Pageable; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.ArrayList; +import java.util.List; +import static com.atwoz.alert.fixture.AlertFixture.알림_생성_제목_날짜_id_주입; +import static com.atwoz.helper.RestDocsHelper.customDocument; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +@AutoConfigureRestDocs +@WebMvcTest(AlertController.class) +class AlertControllerWebMvcTest extends MockBeanInjection { + + private static final String BEARER_TOKEN = "Bearer token"; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private AlertService alertService; + + @Autowired + private AlertQueryService alertQueryService; + + @Test + void 알림을_페이징_조회한다() throws Exception { + // given + List details = new ArrayList<>(); + for (long id = 1; id <= 10; id++) { + Alert alert = 알림_생성_제목_날짜_id_주입("알림 제목 " + id, (int) id, id); + AlertSearchResponse detail = new AlertSearchResponse( + id, + AlertGroup.ALERT, + new AlertContentSearchResponse(alert.getTitle(), alert.getBody()), + alert.getIsRead(), + alert.getCreatedAt() + ); + details.add(detail); + } + + when(alertQueryService.findMemberAlertsWithPaging(any(), any(Pageable.class))) + .thenReturn(new AlertPagingResponse(details, 1)); + + // when & then + mockMvc.perform(get("/api/members/me/alerts") + .param("page", "0") + .param("size", "3") + .header(AUTHORIZATION, BEARER_TOKEN)) + .andExpect(status().isOk()) + .andDo(print()) + .andDo(customDocument("받은_알림_페이징조회", + requestHeaders( + headerWithName(AUTHORIZATION).description("유저 토큰 정보") + ), + requestParts( + partWithName("page").description("페이지 번호").optional(), + partWithName("size").description("몇 개씩 조회를 할 것인지").optional() + ), + responseFields( + fieldWithPath("alerts").description("받은 알림 목록"), + fieldWithPath("alerts[].id").description("알림 id"), + fieldWithPath("alerts[].group").description("알림의 그룹"), + fieldWithPath("alerts[].alert").description("알림의 실제 내용"), + fieldWithPath("alerts[].alert.title").description("알림의 제목"), + fieldWithPath("alerts[].alert.body").description("알림의 본문 (선택)"), + fieldWithPath("alerts[].isRead").description("알림 읽음 여부"), + fieldWithPath("alerts[].createdAt").description("알림 생성 시각"), + fieldWithPath("nextPage").description("다음 페이지가 존재하면 1, 없다면 -1") + )) + ); + } + + @Test + void 알림을_단일_조회한다() throws Exception { + // given + long id = 1L; + Alert alert = 알림_생성_제목_날짜_id_주입("알림 제목", (int) id, id); + + when(alertService.readAlert(any(), any())).thenReturn(alert); + + // when & then + mockMvc.perform(get("/api/members/me/alerts/{alertId}", id) + .header(AUTHORIZATION, BEARER_TOKEN)) + .andExpect(status().isOk()) + .andDo(print()) + .andDo(customDocument("단일_알림_조회", + requestHeaders( + headerWithName(AUTHORIZATION).description("유저 토큰 정보") + ), + pathParameters( + parameterWithName("alertId").description("읽고자 하는 알림 id") + ), + responseFields( + fieldWithPath("id").description("알림 id"), + fieldWithPath("group").description("알림의 그룹"), + fieldWithPath("alert").description("알림의 실제 내용"), + fieldWithPath("alert.title").description("알림의 제목"), + fieldWithPath("alert.body").description("알림의 본문 (선택)"), + fieldWithPath("isRead").description("알림 읽음 여부"), + fieldWithPath("createdAt").description("알림 생성 시각") + )) + ); + + } +} diff --git a/src/test/java/com/atwoz/helper/MockBeanInjection.java b/src/test/java/com/atwoz/helper/MockBeanInjection.java index bfdb248c..2ecde5db 100644 --- a/src/test/java/com/atwoz/helper/MockBeanInjection.java +++ b/src/test/java/com/atwoz/helper/MockBeanInjection.java @@ -6,6 +6,8 @@ import com.atwoz.admin.ui.auth.support.AdminAuthenticationExtractor; import com.atwoz.admin.ui.auth.support.resolver.AdminAuthArgumentResolver; import com.atwoz.admin.ui.auth.support.resolver.AdminRefreshTokenExtractionArgumentResolver; +import com.atwoz.alert.application.AlertQueryService; +import com.atwoz.alert.application.AlertService; import com.atwoz.member.application.auth.MemberAuthService; import com.atwoz.member.application.member.MemberQueryService; import com.atwoz.member.application.member.MemberService; @@ -117,9 +119,17 @@ public class MockBeanInjection { @MockBean protected ReportService reportService; + // MemberLike @MockBean protected MemberLikeQueryService memberLikeQueryService; @MockBean protected MemberLikeService memberLikeService; + + // Alert + @MockBean + protected AlertQueryService alertQueryService; + + @MockBean + protected AlertService alertService; } From 88e5e5263e86f88dec768f1943d09b8af1e6195a Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 19:15:11 +0900 Subject: [PATCH 31/62] =?UTF-8?q?test:=20AlertControllerAcceptance=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AlertControllerAcceptance 테스트 작성 - 관련 Fixture 등록 --- .../com/atwoz/alert/fixture/AlertFixture.java | 14 +++ .../ui/AlertControllerAcceptanceFixture.java | 94 +++++++++++++++++++ .../ui/AlertControllerAcceptanceTest.java | 48 ++++++++++ 3 files changed, 156 insertions(+) create mode 100644 src/test/java/com/atwoz/alert/ui/AlertControllerAcceptanceFixture.java create mode 100644 src/test/java/com/atwoz/alert/ui/AlertControllerAcceptanceTest.java diff --git a/src/test/java/com/atwoz/alert/fixture/AlertFixture.java b/src/test/java/com/atwoz/alert/fixture/AlertFixture.java index bfcc073a..672ff323 100644 --- a/src/test/java/com/atwoz/alert/fixture/AlertFixture.java +++ b/src/test/java/com/atwoz/alert/fixture/AlertFixture.java @@ -66,6 +66,20 @@ public class AlertFixture { .build(); } + public static Alert 알림_생성_제목_날짜_회원id_주입(final String title, final int day, final long receiverId) { + return Alert.builder() + .isRead(false) + .alertGroup(AlertGroup.ALERT) + .alertMessage(AlertMessage.createWith(title, "알림 상세 내용")) + .receiverId(receiverId) + .createdAt(LocalDateTime.now() + .plusDays(day)) + .updatedAt(LocalDateTime.now() + .plusDays(day)) + .deletedAt(null) + .build(); + } + public static Alert 알림_생성_id_있음() { return Alert.builder() .id(1L) diff --git a/src/test/java/com/atwoz/alert/ui/AlertControllerAcceptanceFixture.java b/src/test/java/com/atwoz/alert/ui/AlertControllerAcceptanceFixture.java new file mode 100644 index 00000000..e7f2b1d8 --- /dev/null +++ b/src/test/java/com/atwoz/alert/ui/AlertControllerAcceptanceFixture.java @@ -0,0 +1,94 @@ +package com.atwoz.alert.ui; + +import com.atwoz.alert.domain.Alert; +import com.atwoz.alert.domain.AlertRepository; +import com.atwoz.alert.infrastructure.dto.AlertPagingResponse; +import com.atwoz.alert.infrastructure.dto.AlertSearchResponse; +import com.atwoz.helper.IntegrationHelper; +import com.atwoz.member.domain.member.Member; +import com.atwoz.member.domain.member.MemberRepository; +import com.atwoz.member.infrastructure.auth.MemberJwtTokenProvider; +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import org.apache.http.HttpHeaders; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; + +import static com.atwoz.alert.fixture.AlertFixture.알림_생성_제목_날짜_회원id_주입; +import static com.atwoz.member.fixture.MemberFixture.일반_유저_생성; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +public class AlertControllerAcceptanceFixture extends IntegrationHelper { + + @Autowired + private MemberJwtTokenProvider jwtTokenProvider; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private AlertRepository alertRepository; + + private Long id = 0L; + + protected Member 회원_생성() { + id++; + return memberRepository.save(일반_유저_생성("nickname" + id, "000-0000-000" + id)); + } + + protected String 토큰_생성(final Member member) { + return jwtTokenProvider.createAccessToken(member.getId()); + } + + protected void 알림_목록_생성(final Long memberId) { + for (int day = 1; day <= 10; day++) { + Alert alert = 알림_생성_제목_날짜_회원id_주입("알림 제목 " + day, day, memberId); + alertRepository.save(alert); + } + } + + protected ExtractableResponse 알림_목록을_조회한다(final String token, final String url) { + return RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) + .when() + .get(url) + .then().log().all() + .extract(); + } + + protected void 알림_목록_조회_결과_검증(final ExtractableResponse response) { + AlertPagingResponse result = response.as(AlertPagingResponse.class); + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + softly.assertThat(result.alerts().size()).isEqualTo(3); + softly.assertThat(result.nextPage()).isEqualTo(1); + }); + } + + protected Alert 알림_생성(final Long memberId) { + Alert alert = 알림_생성_제목_날짜_회원id_주입("알림 제목", 0, memberId); + return alertRepository.save(alert); + } + + protected ExtractableResponse 알림을_조회한다(final String token, final String url) { + return RestAssured.given().log().all() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) + .when() + .get(url) + .then().log().all() + .extract(); + } + + protected void 알림_조회_검증(final ExtractableResponse response) { + AlertSearchResponse result = response.as(AlertSearchResponse.class); + assertSoftly(softly -> { + softly.assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + softly.assertThat(result.alert()).isNotNull(); + softly.assertThat(result.isRead()).isTrue(); + }); + } +} diff --git a/src/test/java/com/atwoz/alert/ui/AlertControllerAcceptanceTest.java b/src/test/java/com/atwoz/alert/ui/AlertControllerAcceptanceTest.java new file mode 100644 index 00000000..0100beb5 --- /dev/null +++ b/src/test/java/com/atwoz/alert/ui/AlertControllerAcceptanceTest.java @@ -0,0 +1,48 @@ +package com.atwoz.alert.ui; + +import com.atwoz.member.domain.member.Member; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class AlertControllerAcceptanceTest extends AlertControllerAcceptanceFixture { + + private static final String 알림_페이징_URI = "/api/members/me/alerts?page=0&size=3"; + private static final String 알림_단일_URI = "/api/members/me/alerts/"; + + private String 토큰; + private Member 회원; + + @BeforeEach + void setup() { + 회원 = 회원_생성(); + 토큰 = 토큰_생성(회원); + } + + @Test + void 알림을_페이징_조회한다() { + // given + 알림_목록_생성(회원.getId()); + + // when + var 알림_목록_조회_결과 = 알림_목록을_조회한다(토큰, 알림_페이징_URI); + + // then + 알림_목록_조회_결과_검증(알림_목록_조회_결과); + } + + @Test + void 알림을_단일_조회한다() { + // given + var 알림 = 알림_생성(회원.getId()); + + // when + var 알림_조회_결과 = 알림을_조회한다(토큰, 알림_단일_URI + 알림.getId()); + + // then + 알림_조회_검증(알림_조회_결과); + } +} From 550e70b8aa85001ba056c636f4bba67167954b5c Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 19:27:06 +0900 Subject: [PATCH 32/62] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=9C=EC=83=9D=20=EA=B2=80=EC=A6=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 회원 로그인 시 FCM 토큰 저장 이벤트 발생 검증 추가 - 회원 로그인 문서에 FCM 토큰 정보 추가 --- .../auth/MemberAuthServiceTest.java | 24 ++++++++++++++++--- .../auth/MemberAuthControllerWebMvcTest.java | 5 ++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/atwoz/member/application/auth/MemberAuthServiceTest.java b/src/test/java/com/atwoz/member/application/auth/MemberAuthServiceTest.java index 2c7bf9ec..6691b971 100644 --- a/src/test/java/com/atwoz/member/application/auth/MemberAuthServiceTest.java +++ b/src/test/java/com/atwoz/member/application/auth/MemberAuthServiceTest.java @@ -1,5 +1,7 @@ package com.atwoz.member.application.auth; +import com.atwoz.alert.application.event.AlertTokenCreatedEvent; +import com.atwoz.global.event.Events; import com.atwoz.member.application.auth.dto.LoginRequest; import com.atwoz.member.domain.auth.MemberTokenProvider; import com.atwoz.member.infrastructure.auth.OAuthFakeRequester; @@ -11,15 +13,22 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.test.context.event.RecordApplicationEvents; import static com.atwoz.member.fixture.OAuthProviderFixture.인증_기관_생성; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @DisplayNameGeneration(ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") +@RecordApplicationEvents @ExtendWith(MockitoExtension.class) class MemberAuthServiceTest { @@ -27,15 +36,20 @@ class MemberAuthServiceTest { private MemberTokenProvider memberTokenProvider; private MemberAuthService memberAuthService; + @MockBean + private ApplicationEventPublisher eventPublisher; + @BeforeEach void setup() { memberAuthService = new MemberAuthService(memberTokenProvider, new OAuthFakeRequester(), new MemberFakeRepository()); + eventPublisher = mock(ApplicationEventPublisher.class); + Events.setPublisher(eventPublisher); } @Test void 로그인을_진행하면_토큰을_반환한다() { // given - LoginRequest loginRequest = new LoginRequest("kakao", "code"); + LoginRequest loginRequest = new LoginRequest("kakao", "code", "token"); OAuthProviderRequest oAuthProviderRequest = 인증_기관_생성(); String expectedToken = "token"; when(memberTokenProvider.createAccessToken(any())).thenReturn(expectedToken); @@ -44,6 +58,10 @@ void setup() { String token = memberAuthService.login(loginRequest, oAuthProviderRequest); // then - assertThat(token).isEqualTo(expectedToken); + assertSoftly(softly -> { + softly.assertThat(token).isEqualTo(expectedToken); + softly.assertThatCode(() -> verify(eventPublisher, times(1)).publishEvent(any(AlertTokenCreatedEvent.class))) + .doesNotThrowAnyException(); + }); } } diff --git a/src/test/java/com/atwoz/member/ui/auth/MemberAuthControllerWebMvcTest.java b/src/test/java/com/atwoz/member/ui/auth/MemberAuthControllerWebMvcTest.java index 84ee1e17..db910de7 100644 --- a/src/test/java/com/atwoz/member/ui/auth/MemberAuthControllerWebMvcTest.java +++ b/src/test/java/com/atwoz/member/ui/auth/MemberAuthControllerWebMvcTest.java @@ -39,7 +39,7 @@ class MemberAuthControllerWebMvcTest extends MockBeanInjection { void 로그인을_진행한다() throws Exception { // given OAuthProviderRequest oAuthProviderRequest = 인증_기관_생성(); - LoginRequest loginRequest = new LoginRequest("kakao", "code"); + LoginRequest loginRequest = new LoginRequest("kakao", "code", "token"); String expectedToken = "token"; when(memberAuthService.login(loginRequest, oAuthProviderRequest)).thenReturn(expectedToken); @@ -52,7 +52,8 @@ class MemberAuthControllerWebMvcTest extends MockBeanInjection { .andDo(customDocument("유저_로그인", requestFields( fieldWithPath("provider").description("인증기관"), - fieldWithPath("code").description("인증코드") + fieldWithPath("code").description("인증코드"), + fieldWithPath("token").description("FCM 토큰") ), responseFields( fieldWithPath("token").description("발급되는 토큰") From 196049bf63ec0ff4ff3bb24b9cf63ca5c9812999 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 19:29:31 +0900 Subject: [PATCH 33/62] =?UTF-8?q?refactor:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EB=8B=A8=EC=9D=BC=20=EC=A1=B0=ED=9A=8C=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=EA=B0=92=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알림 단일 조회 반환값 (읽음 여부) 변경 --- src/test/java/com/atwoz/alert/ui/AlertControllerWebMvcTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/com/atwoz/alert/ui/AlertControllerWebMvcTest.java b/src/test/java/com/atwoz/alert/ui/AlertControllerWebMvcTest.java index c30c0e4d..55966e0b 100644 --- a/src/test/java/com/atwoz/alert/ui/AlertControllerWebMvcTest.java +++ b/src/test/java/com/atwoz/alert/ui/AlertControllerWebMvcTest.java @@ -106,6 +106,7 @@ class AlertControllerWebMvcTest extends MockBeanInjection { // given long id = 1L; Alert alert = 알림_생성_제목_날짜_id_주입("알림 제목", (int) id, id); + alert.read(); when(alertService.readAlert(any(), any())).thenReturn(alert); From ac16c6d7b4efebd7932b4de6a1d96748247d80c0 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 19:49:02 +0900 Subject: [PATCH 34/62] =?UTF-8?q?refactor:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=20=EA=B8=B0=EC=A4=80=20=EA=B5=90=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알림 조회 테스트 정렬 기준 교정 --- .../atwoz/alert/application/AlertQueryServiceTest.java | 5 +++-- .../alert/infrastructure/AlertQueryRepositoryTest.java | 5 +++-- .../atwoz/alert/infrastructure/FakeAlertRepository.java | 5 +++-- .../com/atwoz/alert/ui/AlertControllerWebMvcTest.java | 9 +++++++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/test/java/com/atwoz/alert/application/AlertQueryServiceTest.java b/src/test/java/com/atwoz/alert/application/AlertQueryServiceTest.java index 1d037e10..03af21ed 100644 --- a/src/test/java/com/atwoz/alert/application/AlertQueryServiceTest.java +++ b/src/test/java/com/atwoz/alert/application/AlertQueryServiceTest.java @@ -58,8 +58,9 @@ void init() { private List extractAlertResponsesWithLimit(final List alerts, final int limit) { return alerts.stream() - .sorted(Comparator.comparing(Alert::getCreatedAt).reversed() - .thenComparing(Alert::getId).reversed()) + .sorted(Comparator.comparing(Alert::getCreatedAt) + .reversed() + .thenComparing(Comparator.comparing(Alert::getId).reversed())) .map(alert -> new AlertSearchResponse( alert.getId(), alert.getAlertGroup(), diff --git a/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java b/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java index 040cd31e..8bf2ce5a 100644 --- a/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java +++ b/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java @@ -63,8 +63,9 @@ class 알림_조회_정상 { private List extractAlertResponsesWithLimit(final List alerts, final int limit) { return alerts.stream() - .sorted(Comparator.comparing(Alert::getCreatedAt).reversed() - .thenComparing(Alert::getId).reversed()) + .sorted(Comparator.comparing(Alert::getCreatedAt) + .reversed() + .thenComparing(Comparator.comparing(Alert::getId).reversed())) .map(alert -> new AlertSearchResponse( alert.getId(), alert.getAlertGroup(), diff --git a/src/test/java/com/atwoz/alert/infrastructure/FakeAlertRepository.java b/src/test/java/com/atwoz/alert/infrastructure/FakeAlertRepository.java index 53d5d26d..70827e13 100644 --- a/src/test/java/com/atwoz/alert/infrastructure/FakeAlertRepository.java +++ b/src/test/java/com/atwoz/alert/infrastructure/FakeAlertRepository.java @@ -44,8 +44,9 @@ public Page findMemberAlertsWithPaging(Long memberId, Pagea List alertResponses = map.values() .stream() .filter(alert -> memberId.equals(alert.getReceiverId())) - .sorted(Comparator.comparing(Alert::getCreatedAt).reversed() - .thenComparing(Alert::getId).reversed()) + .sorted(Comparator.comparing(Alert::getCreatedAt) + .reversed() + .thenComparing(Comparator.comparing(Alert::getId).reversed())) .skip(pageable.getOffset()) .limit(pageable.getPageSize()) .map(alert -> new AlertSearchResponse( diff --git a/src/test/java/com/atwoz/alert/ui/AlertControllerWebMvcTest.java b/src/test/java/com/atwoz/alert/ui/AlertControllerWebMvcTest.java index 55966e0b..546b73a6 100644 --- a/src/test/java/com/atwoz/alert/ui/AlertControllerWebMvcTest.java +++ b/src/test/java/com/atwoz/alert/ui/AlertControllerWebMvcTest.java @@ -18,6 +18,7 @@ import org.springframework.test.web.servlet.MockMvc; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import static com.atwoz.alert.fixture.AlertFixture.알림_생성_제목_날짜_id_주입; import static com.atwoz.helper.RestDocsHelper.customDocument; @@ -68,9 +69,13 @@ class AlertControllerWebMvcTest extends MockBeanInjection { ); details.add(detail); } - + List sorted = details.stream() + .sorted(Comparator.comparing(AlertSearchResponse::createdAt) + .reversed() + .thenComparing(Comparator.comparing(AlertSearchResponse::id).reversed())) + .toList(); when(alertQueryService.findMemberAlertsWithPaging(any(), any(Pageable.class))) - .thenReturn(new AlertPagingResponse(details, 1)); + .thenReturn(new AlertPagingResponse(sorted, 1)); // when & then mockMvc.perform(get("/api/members/me/alerts") From 5090251fe393558fbc4c3cb49ae194de5c43aeb0 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 19:49:15 +0900 Subject: [PATCH 35/62] =?UTF-8?q?refactor:=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=9D=B8=EC=9E=90=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 필요없는 인자 제거 --- .../com/atwoz/alert/application/event/AlertCreatedEvent.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/atwoz/alert/application/event/AlertCreatedEvent.java b/src/main/java/com/atwoz/alert/application/event/AlertCreatedEvent.java index e1ed8382..67568428 100644 --- a/src/main/java/com/atwoz/alert/application/event/AlertCreatedEvent.java +++ b/src/main/java/com/atwoz/alert/application/event/AlertCreatedEvent.java @@ -7,7 +7,6 @@ public record AlertCreatedEvent( String title, String body, String sender, - Long receiverId, - String token + Long receiverId ) { } From 796239eb51b5a370d36c8fdd14e2b9d45d09d491 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 22:34:18 +0900 Subject: [PATCH 36/62] =?UTF-8?q?refactor:=20=ED=8C=8C=EC=9D=B4=EC=96=B4?= =?UTF-8?q?=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=ED=82=A4=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 직접적인 파일을 미리 만들지 않고 암호화된 값을 통해 파일을 만드는 방식으로 조회하도록 수정 --- .../atwoz/alert/config/FirebaseConfig.java | 119 ++++++++++++++++-- src/main/resources/application.yml | 13 ++ 2 files changed, 120 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/atwoz/alert/config/FirebaseConfig.java b/src/main/java/com/atwoz/alert/config/FirebaseConfig.java index fdfb554c..9be220fe 100644 --- a/src/main/java/com/atwoz/alert/config/FirebaseConfig.java +++ b/src/main/java/com/atwoz/alert/config/FirebaseConfig.java @@ -1,34 +1,129 @@ package com.atwoz.alert.config; -import com.atwoz.alert.exception.exceptions.FirebaseFileNotFoundException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; import com.google.auth.oauth2.GoogleCredentials; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; @Configuration public class FirebaseConfig { - private static final String FIREBASE_URL = "src/main/resources/firebase/firebase_key.json"; + private static final String FILE_URL = "src/main/resources/firebase/firebase_key.json"; + private static final String TYPE = "type"; + private static final String PROJECT_ID = "project_id"; + private static final String PRIVATE_KEY_ID = "private_key_id"; + private static final String PRIVATE_KEY = "private_key"; + private static final String CLIENT_EMAIL = "client_email"; + private static final String CLIENT_ID = "client_id"; + private static final String AUTH_URI = "auth_uri"; + private static final String TOKEN_URI = "token_uri"; + private static final String AUTH_CERT = "auth_provider_x509_cert_url"; + private static final String CLIENT_CERT = "client_x509_cert_url"; + private static final String UNIVERSE_DOMAIN = "universe_domain"; + private static final String REPLACE_TARGET_MARK = "\\\\n"; + private static final String REPLACE_VALUE_MARK = "\n"; + + @Value("${firebase.type}") + private String type; + + @Value("${firebase.project_id}") + private String projectId; + + @Value("${firebase.private_key_id}") + private String privateKeyId; + + @Value("${firebase.private_key}") + private String privateKey; + + @Value("${firebase.client_email}") + private String clientEmail; + + @Value("${firebase.client_id}") + private String clientId; + + @Value("${firebase.auth_uri}") + private String authUri; + + @Value("${firebase.token_uri}") + private String tokenUri; + + @Value("${firebase.auth_provider_x509_cert_url}") + private String authCert; + + @Value("${firebase.client_x509_cert_url}") + private String clientCert; + + @Value("${firebase.universe_domain}") + private String universeDomain; @Bean - public FirebaseApp firebaseApp() { + public FirebaseApp firebaseApp() throws IOException { if (!FirebaseApp.getApps().isEmpty()) { return FirebaseApp.getInstance(); } - try { - FileInputStream firebaseFile = new FileInputStream(FIREBASE_URL); - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(GoogleCredentials.fromStream(firebaseFile)) - .build(); - - return FirebaseApp.initializeApp(options); - } catch (IOException e) { - throw new FirebaseFileNotFoundException(); + + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(getCredentials()) + .build(); + + return FirebaseApp.initializeApp(options); + } + + private GoogleCredentials getCredentials() throws IOException { + saveConfigToFile(); + + File file = new File(FILE_URL); + return GoogleCredentials.fromStream(new FileInputStream(file)); + } + + private void saveConfigToFile() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectWriter writer = mapper.writerWithDefaultPrettyPrinter(); // Use pretty printer to format output + + Map firebaseConfig = saveFirebaseConfigByValues(); + + File file = new File(FILE_URL); + if (!file.exists()) { + writer.writeValue(file, firebaseConfig); + file.createNewFile(); } + updatePrivateKeyInJson(file, mapper, privateKey); + } + + private Map saveFirebaseConfigByValues() { + Map firebaseConfig = new HashMap<>(); + firebaseConfig.put(TYPE, type); + firebaseConfig.put(PROJECT_ID, projectId); + firebaseConfig.put(PRIVATE_KEY_ID, privateKeyId); + firebaseConfig.put(PRIVATE_KEY, privateKey); + firebaseConfig.put(CLIENT_EMAIL, clientEmail); + firebaseConfig.put(CLIENT_ID, clientId); + firebaseConfig.put(AUTH_URI, authUri); + firebaseConfig.put(TOKEN_URI, tokenUri); + firebaseConfig.put(AUTH_CERT, authCert); + firebaseConfig.put(CLIENT_CERT, clientCert); + firebaseConfig.put(UNIVERSE_DOMAIN, universeDomain); + + return firebaseConfig; + } + + private void updatePrivateKeyInJson(File jsonFile, ObjectMapper mapper, String newPrivateKey) throws IOException { + Map jsonMap = mapper.readValue(jsonFile, Map.class); + jsonMap.put(PRIVATE_KEY, replaceNewLines(newPrivateKey)); + mapper.writeValue(jsonFile, jsonMap); + } + + private String replaceNewLines(String value) { + return value.replaceAll(REPLACE_TARGET_MARK, REPLACE_VALUE_MARK); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d46d1a2a..8a93cf66 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -35,6 +35,19 @@ jwt: redis: expiration-period: ENC(wFlCe4YyS4e4YGsFGvYAkA==) +firebase: + type: ENC(F4zcX8BfVuQNp22k7JJgKV1rtn6SD+6B) + project_id: ENC(UFaUsDkslrYYTsghWmsZ7Yaxxeavv6UA) + private_key_id: ENC(CB4BAR9Nn7qymqGMHuDeDAGoUDXgu7d9w7CyDSzdyaiIHVdKs8avawFW3X8xjs8m+fSEz4oS+es=) + private_key: ENC(QaXAP0NLQ2MQrtmCwTgnv4nOuZn8mrTC7iRCop15+I/ghNnN5N5VOqNO2MZC2OirCixNypfrF62NGmBcU1A66+XN8kgK7ih6BQSchbTCYaWkY7CuFsIN8vYZsXqcoAM2B20x3JqvSiUogCkioeicYEzwfO8XGBOmjzrlG49Xzc/3qd29w3ML+fTazBKyeFL2kTfUwL1rfS/ZjpbdUbYAn6+JW3gU7M9Rm6pTLLq5IUrTcSWJT4Iy1yyrbPC3rKs+ECeiQSKGvUV7zPM4Rd3NqelYmtD70zcgUnUqi8ZpACRUPHd58+AXWdLxXvDlnfPsxkOcDYlVvDdby+0GoEEpmOmbWcOdTA2nBEKY6b7lyv+qLa2aw3gIGEVEwtOE8nBnv46Qfl4/dSH7mqLRharNnInwjEGzKUKpvn/WDEmaMLi9I2crgRsdRNwqEVVKTo59hVSYZCgiHerC2nzgvh6gr0qZBvz1v6+g74efcmH4likOV410yFKSVy+SMaDTilmIwnrJkWg2QunlwUFUARwkAlkvwuE3ouz2RcBop8L/GC/zfvU3zq5ropQa7vbfgzlIss1Z9WMdrXiVqMs36GC2DE9y+Zjkbo1yzCa0G2VIYFN25xPwVAnhBOMrRi9bABIf/gH8thjFC4Pod8VqzVRJH5owM+4x7snXIgmfVibnCQDl3GzIGHMbTvjCsjoAmKnPrAXt1Sj0B5VCwWF1vAGgaPmNUrL60k6jjHBV0cre2fmjtYh3PRRldeLvj5FDfe3poZkIDsdiao/4qT4viRrFMosHCiawphPW/5nlgVLZEv9e4DkJEqiHHPQ9+h6bD715MqBfrc2PgXilbPP1CP6+kustG/xRkctTvusZ4r5Dy4wluhfpWnDbF5Rhs+nePAvcCMAIutm61itXhTp92SX+XROhx0rD1PqXI5YDi97GaoiX+RvXJwt+MuHu+d/otmIAlLjMNjeq4X143EwVt/ivL2KS0qyMpENDPZWmlxJidKDg9TMDG4R0LIs4cd5BxRtX6BQPjsO722XRNAQMfmX+qB3DMQ5GQ/keNzyQiCwZPDYYFs18Eg1mPa47s0YwimXY3barmI/gDNkhbhPrwpAtq5DVoNAYec3IBhuoUWup4U7j53TtuOzvjDW1uu7ZsfoFM88AaZXaZ2hTzRI51AUWaZzO32YS87HkK2HB1gOXQNaKcTx8u0NQtHsQ43bdRnL4FIaVZJPoHv5/BrLJUWt9tXSbynjQW+rXGlAwX2xUoP117e7ma9UWxSCd7UQbmP2kyaaChOK4kDRkoWCfwETX+LuZ3mw8aojgyGH39sktODP8NtmvCVldLubhOBPNCJneZSwMhaxld95ocMPfenXgSMkmPZ8Bjr2eAjYtWERd2sh0GAzsFNy6Js99lLox0cSawMiqBJNuQ+xMGZSVa7GKqRCWp/3fzQG2J9Mr62p4fj0PjO7PLW3bTedtD5YNnEdlt9dA5XoDLx7qu1/bTXR1DPTDxVfbyL+hK6ujJ0IKdBRw81NKaCDAfn6gkrk/SMkLvGJQolcQUJmjs15cIAjXWJfBwDu26sLYWE0NHCaJRQarSiwUkCXhC+HWix4wbU4imX28KY25+wi4lKtKFZ/IS0QJVj4d6IL5nwGcEDxZOCo02lTvKcoXWiOhlS6ZhU5iLQRhqE8BrEI5Z5Zz4ycf1sIsvowYWUt+ZPZFkyIloGjeSqU/iSEiQ7KIhR9OjK/vZ07kr7CrMXJuCHhTWca8VYtFKrQMaJelWdB3/HG+PGTVFshij0S66B6MAeTAEtgfzDWCIzkpCBp59R1XAbK4bVR4G9n8lmlKLFmEmj60HBTHkBoaR15ThlTp1cqABXjnQGGI3gj+EB+RmDD+TnYgEaqTXWbLR1evZlalxlVH4MhB3iCVnL+jFyaPdeKAT0od2Vk3dYAbxRDWFxC8dEjEk3vuuL6vPDyTNoDRDrZDwyWFdn90eQdg/y5ighiHY9dQhjziQbwnpzCj6fXVMRo+5XoTr6vwof6Ftv+9Fis0jct/IW9QDN8DeljqaQGeb+B10IbUkcJOs/BIzoeszcY7CL3aXUIRDi+IXn6Pmo7jF+x4XKeEMmKU9I9awmGiZdVGVSrSfv+z4OhhD7ZoicVAWPoxSkhvCi3UIbrcWo4+nR1T8qf37G4azIHzmkJwGvi4LB3/V54Pw8nEYBrYsKGQ/nh8AAxOZKVQLPVgMsBjU8z2IVKElMbZwlvAj5SbGjtBdRK3+I/gAVzXuBcp8xXqgg45woBywvFc1jjnmm72UXc27VkN6PoKr54XqCnsL/bP3JSHRUbWK6f+kErsQZePww==) + client_email: ENC(+j6daXlFgOM38ayzSuJy0Q2vHVq/eO94eEyGOr+e0ggyap3J5LOVLmb+riSegU0SFYQNRDF24mX82JHUrigb2vkvXc30vEnm) + client_id: ENC(RTaiQPN2N0P7j+SD3V50kVOKW7rYQLddlZHgVWYykmQ=) + auth_uri: ENC(pMLc1oq5YZyyA9U0QZFOIALtiLg835Y4rBVdTPGy9U8cH/b/h336fQyzVXTyUpQlFIzka7svAbM=) + token_uri: ENC(MfLebQuWZGWcUdT+zNf7gaAspKQgvdWtcFSaTaGD7D1xRQHI736TeoQNgg43ykIM) + auth_provider_x509_cert_url: ENC(0Rjf2AbMiB9aCYNMSxlUsp91hfJRcPg+A0BVKLfPsVuhjKCwoMowgUGwsz2nsZphzF43BCA3+VY=) + client_x509_cert_url: ENC(qmWXGlIex60ylnfTgJmw+jUQqh3yRWdX7comN5XWwfxByHXLhQAZP/kPi7+Sb6A/buxb9DGH3daqvZPDM8lKiz5yGFHKvtgwIEnStc7LzUSTOeXAJv/kwdG7sckrUokeYpzVtJKL1CQBB9YtiZs0QBc9Z1bVJeBO) + universe_domain: ENC(Q4ZkcZWMOXTfgk7OODq/M7utCcr1tIsN) + mail: host: ENC(2PviuayFL6dKe91WydIBx81bpSBoI3FU) username: ENC(Hj0Qwzuifugz4sfvDCq9H0u7+WZC4nL+) From 00402345840b1b250a01cfa4cebf77ff79dd811b Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 23:43:47 +0900 Subject: [PATCH 37/62] =?UTF-8?q?fix:=20=ED=8C=8C=EC=9D=B4=EC=96=B4?= =?UTF-8?q?=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=ED=82=A4=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테스트 환경에서의 문제 해결하기 위해 테스트 application.yml에 파이어베이스 관련 정보 등록 - 파일 생성 중단, 데이터만 이용하도록 변경 - 빈 생성 중 예외 발생 (테스트 환경)일 시, 기본 GoogleCredentials 반환하도록 수정 --- .../atwoz/alert/config/FirebaseConfig.java | 47 +++++++------------ src/test/resources/application.yml | 13 +++++ 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/atwoz/alert/config/FirebaseConfig.java b/src/main/java/com/atwoz/alert/config/FirebaseConfig.java index 9be220fe..f03aee43 100644 --- a/src/main/java/com/atwoz/alert/config/FirebaseConfig.java +++ b/src/main/java/com/atwoz/alert/config/FirebaseConfig.java @@ -1,7 +1,7 @@ package com.atwoz.alert.config; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectWriter; import com.google.auth.oauth2.GoogleCredentials; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; @@ -9,16 +9,15 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import java.io.File; -import java.io.FileInputStream; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.util.HashMap; import java.util.Map; @Configuration public class FirebaseConfig { - private static final String FILE_URL = "src/main/resources/firebase/firebase_key.json"; private static final String TYPE = "type"; private static final String PROJECT_ID = "project_id"; private static final String PRIVATE_KEY_ID = "private_key_id"; @@ -67,7 +66,7 @@ public class FirebaseConfig { private String universeDomain; @Bean - public FirebaseApp firebaseApp() throws IOException { + public FirebaseApp firebaseApp() { if (!FirebaseApp.getApps().isEmpty()) { return FirebaseApp.getInstance(); } @@ -75,37 +74,33 @@ public FirebaseApp firebaseApp() throws IOException { FirebaseOptions options = new FirebaseOptions.Builder() .setCredentials(getCredentials()) .build(); - return FirebaseApp.initializeApp(options); } - private GoogleCredentials getCredentials() throws IOException { - saveConfigToFile(); - - File file = new File(FILE_URL); - return GoogleCredentials.fromStream(new FileInputStream(file)); + private GoogleCredentials getCredentials() { + try { + String jsonContent = generateJsonContent(); + InputStream inputStream = new ByteArrayInputStream(jsonContent.getBytes()); + return GoogleCredentials.fromStream(inputStream); + } catch (IOException e) { + return GoogleCredentials.newBuilder() + .build(); + } } - private void saveConfigToFile() throws IOException { + private String generateJsonContent() throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); - ObjectWriter writer = mapper.writerWithDefaultPrettyPrinter(); // Use pretty printer to format output - - Map firebaseConfig = saveFirebaseConfigByValues(); + Map firebaseConfig = generateFirebaseConfig(); + firebaseConfig.put(PRIVATE_KEY, replaceNewLines(privateKey)); - File file = new File(FILE_URL); - if (!file.exists()) { - writer.writeValue(file, firebaseConfig); - file.createNewFile(); - } - updatePrivateKeyInJson(file, mapper, privateKey); + return mapper.writeValueAsString(firebaseConfig); } - private Map saveFirebaseConfigByValues() { + private Map generateFirebaseConfig() { Map firebaseConfig = new HashMap<>(); firebaseConfig.put(TYPE, type); firebaseConfig.put(PROJECT_ID, projectId); firebaseConfig.put(PRIVATE_KEY_ID, privateKeyId); - firebaseConfig.put(PRIVATE_KEY, privateKey); firebaseConfig.put(CLIENT_EMAIL, clientEmail); firebaseConfig.put(CLIENT_ID, clientId); firebaseConfig.put(AUTH_URI, authUri); @@ -117,12 +112,6 @@ private Map saveFirebaseConfigByValues() { return firebaseConfig; } - private void updatePrivateKeyInJson(File jsonFile, ObjectMapper mapper, String newPrivateKey) throws IOException { - Map jsonMap = mapper.readValue(jsonFile, Map.class); - jsonMap.put(PRIVATE_KEY, replaceNewLines(newPrivateKey)); - mapper.writeValue(jsonFile, jsonMap); - } - private String replaceNewLines(String value) { return value.replaceAll(REPLACE_TARGET_MARK, REPLACE_VALUE_MARK); } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 3fcd4e75..cdb2fcdf 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -37,6 +37,19 @@ cookie: redis: expiration-period: 1000 +firebase: + type: service_account + project_id: project_id + private_key_id: private_key_id + private_key: private_key + client_email: client_email + client_id: client_id + auth_uri: auth_uri + token_uri: token_uri + auth_provider_x509_cert_url: auth_provider_x509_cert_url + client_x509_cert_url: client_x509_cert_url + universe_domain: universe_domain + jwt: secret: fortestfortestfortestfortestfortestfortestfortestfortestfortestfortestfortestfortestfortestfortestfortest access-token-expiration-period: 10000 From f183748b19bca00d351dd90434576bd7bc8af345 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 12 Jul 2024 23:58:56 +0900 Subject: [PATCH 38/62] =?UTF-8?q?refactor:=20Fake=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fake 객체 이름 규칙에 맞게 변경 --- .../alert/application/AlertQueryServiceTest.java | 4 ++-- .../atwoz/alert/application/AlertServiceTest.java | 12 ++++++------ .../{FakeAlertManager.java => AlertFakeManager.java} | 2 +- ...AlertRepository.java => AlertFakeRepository.java} | 2 +- ...Repository.java => AlertFakeTokenRepository.java} | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) rename src/test/java/com/atwoz/alert/infrastructure/{FakeAlertManager.java => AlertFakeManager.java} (81%) rename src/test/java/com/atwoz/alert/infrastructure/{FakeAlertRepository.java => AlertFakeRepository.java} (98%) rename src/test/java/com/atwoz/alert/infrastructure/{FakeAlertTokenRepository.java => AlertFakeTokenRepository.java} (93%) diff --git a/src/test/java/com/atwoz/alert/application/AlertQueryServiceTest.java b/src/test/java/com/atwoz/alert/application/AlertQueryServiceTest.java index 03af21ed..cbccd7b6 100644 --- a/src/test/java/com/atwoz/alert/application/AlertQueryServiceTest.java +++ b/src/test/java/com/atwoz/alert/application/AlertQueryServiceTest.java @@ -2,7 +2,7 @@ import com.atwoz.alert.domain.Alert; import com.atwoz.alert.domain.AlertRepository; -import com.atwoz.alert.infrastructure.FakeAlertRepository; +import com.atwoz.alert.infrastructure.AlertFakeRepository; import com.atwoz.alert.infrastructure.dto.AlertContentSearchResponse; import com.atwoz.alert.infrastructure.dto.AlertPagingResponse; import com.atwoz.alert.infrastructure.dto.AlertSearchResponse; @@ -27,7 +27,7 @@ class AlertQueryServiceTest { @BeforeEach void init() { - alertRepository = new FakeAlertRepository(); + alertRepository = new AlertFakeRepository(); alertQueryService = new AlertQueryService(alertRepository); } diff --git a/src/test/java/com/atwoz/alert/application/AlertServiceTest.java b/src/test/java/com/atwoz/alert/application/AlertServiceTest.java index 4109c174..4c69baf9 100644 --- a/src/test/java/com/atwoz/alert/application/AlertServiceTest.java +++ b/src/test/java/com/atwoz/alert/application/AlertServiceTest.java @@ -7,9 +7,9 @@ import com.atwoz.alert.domain.vo.AlertGroup; import com.atwoz.alert.exception.exceptions.AlertNotFoundException; import com.atwoz.alert.exception.exceptions.ReceiverTokenNotFoundException; -import com.atwoz.alert.infrastructure.FakeAlertManager; -import com.atwoz.alert.infrastructure.FakeAlertRepository; -import com.atwoz.alert.infrastructure.FakeAlertTokenRepository; +import com.atwoz.alert.infrastructure.AlertFakeManager; +import com.atwoz.alert.infrastructure.AlertFakeRepository; +import com.atwoz.alert.infrastructure.AlertFakeTokenRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; @@ -35,9 +35,9 @@ class AlertServiceTest { @BeforeEach void init() { - alertRepository = new FakeAlertRepository(); - tokenRepository = new FakeAlertTokenRepository(); - alertManager = new FakeAlertManager(); + alertRepository = new AlertFakeRepository(); + tokenRepository = new AlertFakeTokenRepository(); + alertManager = new AlertFakeManager(); alertService = new AlertService(alertRepository, tokenRepository, alertManager); } diff --git a/src/test/java/com/atwoz/alert/infrastructure/FakeAlertManager.java b/src/test/java/com/atwoz/alert/infrastructure/AlertFakeManager.java similarity index 81% rename from src/test/java/com/atwoz/alert/infrastructure/FakeAlertManager.java rename to src/test/java/com/atwoz/alert/infrastructure/AlertFakeManager.java index 3ba90e0b..0b3cfa69 100644 --- a/src/test/java/com/atwoz/alert/infrastructure/FakeAlertManager.java +++ b/src/test/java/com/atwoz/alert/infrastructure/AlertFakeManager.java @@ -3,7 +3,7 @@ import com.atwoz.alert.domain.Alert; import com.atwoz.alert.domain.AlertManager; -public class FakeAlertManager implements AlertManager { +public class AlertFakeManager implements AlertManager { @Override public void send(final Alert alert, final String sender, final String token) { diff --git a/src/test/java/com/atwoz/alert/infrastructure/FakeAlertRepository.java b/src/test/java/com/atwoz/alert/infrastructure/AlertFakeRepository.java similarity index 98% rename from src/test/java/com/atwoz/alert/infrastructure/FakeAlertRepository.java rename to src/test/java/com/atwoz/alert/infrastructure/AlertFakeRepository.java index 70827e13..646cc556 100644 --- a/src/test/java/com/atwoz/alert/infrastructure/FakeAlertRepository.java +++ b/src/test/java/com/atwoz/alert/infrastructure/AlertFakeRepository.java @@ -15,7 +15,7 @@ import java.util.Map; import java.util.Optional; -public class FakeAlertRepository implements AlertRepository { +public class AlertFakeRepository implements AlertRepository { private static final int DELETION_THRESHOLD_DATE = 61; diff --git a/src/test/java/com/atwoz/alert/infrastructure/FakeAlertTokenRepository.java b/src/test/java/com/atwoz/alert/infrastructure/AlertFakeTokenRepository.java similarity index 93% rename from src/test/java/com/atwoz/alert/infrastructure/FakeAlertTokenRepository.java rename to src/test/java/com/atwoz/alert/infrastructure/AlertFakeTokenRepository.java index 4528b8bd..ac9cea0e 100644 --- a/src/test/java/com/atwoz/alert/infrastructure/FakeAlertTokenRepository.java +++ b/src/test/java/com/atwoz/alert/infrastructure/AlertFakeTokenRepository.java @@ -6,7 +6,7 @@ import java.util.HashMap; import java.util.Map; -public class FakeAlertTokenRepository implements AlertTokenRepository { +public class AlertFakeTokenRepository implements AlertTokenRepository { private final Map map = new HashMap<>(); From f92ecbc97f6139062ae1e0d51aca6a5eaf022dd4 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sat, 13 Jul 2024 00:00:33 +0900 Subject: [PATCH 39/62] =?UTF-8?q?docs:=20=EC=9D=98=EB=AF=B8=EC=97=86?= =?UTF-8?q?=EB=8A=94=20gitignore=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 의미없는 gitignore 코드 삭제 --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 40ce9cab..7185b66d 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,3 @@ out/ ### REST Docs, QueryDSL ### /src/main/resources/static/docs/ /src/main/generated - -### Firebase ### -/src/main/resources/firebase/firebase_key.json From f103ac6fdb30b3569789a5f8642e2f22c1c5db14 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sat, 13 Jul 2024 00:02:37 +0900 Subject: [PATCH 40/62] =?UTF-8?q?refactor:=20FirebaseFileNotFoundException?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FirebaseFileNotFoundException 파일 삭제 --- .../com/atwoz/alert/exception/AlertExceptionHandler.java | 6 ------ .../exceptions/FirebaseFileNotFoundException.java | 8 -------- 2 files changed, 14 deletions(-) delete mode 100644 src/main/java/com/atwoz/alert/exception/exceptions/FirebaseFileNotFoundException.java diff --git a/src/main/java/com/atwoz/alert/exception/AlertExceptionHandler.java b/src/main/java/com/atwoz/alert/exception/AlertExceptionHandler.java index a72d7ee6..6195652d 100644 --- a/src/main/java/com/atwoz/alert/exception/AlertExceptionHandler.java +++ b/src/main/java/com/atwoz/alert/exception/AlertExceptionHandler.java @@ -2,7 +2,6 @@ import com.atwoz.alert.exception.exceptions.AlertNotFoundException; import com.atwoz.alert.exception.exceptions.AlertSendException; -import com.atwoz.alert.exception.exceptions.FirebaseFileNotFoundException; import com.atwoz.alert.exception.exceptions.ReceiverTokenNotFoundException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -12,11 +11,6 @@ @RestControllerAdvice public class AlertExceptionHandler { - @ExceptionHandler(FirebaseFileNotFoundException.class) - public ResponseEntity handleFirebaseFileNotFoundException(final FirebaseFileNotFoundException e) { - return getExceptionWithStatus(e, HttpStatus.INTERNAL_SERVER_ERROR); - } - @ExceptionHandler(AlertSendException.class) public ResponseEntity handleAlertSendException(final AlertSendException e) { return getExceptionWithStatus(e, HttpStatus.INTERNAL_SERVER_ERROR); diff --git a/src/main/java/com/atwoz/alert/exception/exceptions/FirebaseFileNotFoundException.java b/src/main/java/com/atwoz/alert/exception/exceptions/FirebaseFileNotFoundException.java deleted file mode 100644 index 0a53195c..00000000 --- a/src/main/java/com/atwoz/alert/exception/exceptions/FirebaseFileNotFoundException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.atwoz.alert.exception.exceptions; - -public class FirebaseFileNotFoundException extends RuntimeException { - - public FirebaseFileNotFoundException() { - super("Firebase json 파일을 찾을 수 없습니다."); - } -} From 833df6b5bcf9ac9905f403cffc258bbca91a1b0d Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sat, 13 Jul 2024 11:51:28 +0900 Subject: [PATCH 41/62] =?UTF-8?q?refactor:=20final=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AlertGroup final 추가 --- src/main/java/com/atwoz/alert/domain/vo/AlertGroup.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/atwoz/alert/domain/vo/AlertGroup.java b/src/main/java/com/atwoz/alert/domain/vo/AlertGroup.java index 23fbf40f..6344328a 100644 --- a/src/main/java/com/atwoz/alert/domain/vo/AlertGroup.java +++ b/src/main/java/com/atwoz/alert/domain/vo/AlertGroup.java @@ -13,7 +13,7 @@ public enum AlertGroup { INTERVIEW("인터뷰"), ALERT("알림"); - private String name; + private final String name; AlertGroup(final String name) { this.name = name; From 3ed25628da8024180b617c2075d28cc21df179da Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sat, 13 Jul 2024 11:52:14 +0900 Subject: [PATCH 42/62] =?UTF-8?q?refactor:=20AllArgsConstructor=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AlertMessage AllArgsConstructor 추가 --- src/main/java/com/atwoz/alert/domain/vo/AlertMessage.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/atwoz/alert/domain/vo/AlertMessage.java b/src/main/java/com/atwoz/alert/domain/vo/AlertMessage.java index 660d5d63..edcb327f 100644 --- a/src/main/java/com/atwoz/alert/domain/vo/AlertMessage.java +++ b/src/main/java/com/atwoz/alert/domain/vo/AlertMessage.java @@ -2,10 +2,12 @@ import jakarta.persistence.Embeddable; import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PROTECTED) @Embeddable public class AlertMessage { @@ -13,11 +15,6 @@ public class AlertMessage { private String title; private String body; - private AlertMessage(final String title, final String body) { - this.title = title; - this.body = body; - } - public static AlertMessage createWith(final String title, final String body) { return new AlertMessage(title, body); } From 5d1910fdbfcc8d47313da850421ad88d124b6c04 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sat, 13 Jul 2024 11:53:42 +0900 Subject: [PATCH 43/62] =?UTF-8?q?refactor:=20=EC=96=B4=EB=85=B8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=88=9C=EC=84=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 어노테이션 순서 통일되도록 수정 --- src/main/java/com/atwoz/alert/domain/Alert.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/atwoz/alert/domain/Alert.java b/src/main/java/com/atwoz/alert/domain/Alert.java index 2221eed9..5d07548e 100644 --- a/src/main/java/com/atwoz/alert/domain/Alert.java +++ b/src/main/java/com/atwoz/alert/domain/Alert.java @@ -30,8 +30,8 @@ public class Alert extends SoftDeleteBaseEntity { @Column(nullable = false) private Boolean isRead; - @Column(nullable = false) @Enumerated + @Column(nullable = false) private AlertGroup alertGroup; @Embedded From fc577e80d8cec96a97346a6864abd054e4ee5c99 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sat, 13 Jul 2024 12:04:14 +0900 Subject: [PATCH 44/62] =?UTF-8?q?refactor:=20AlertManager=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AlertManager 인터페이스의 위치를 도메인에서 애플리케이션으로 변경 --- .../com/atwoz/alert/{domain => application}/AlertManager.java | 4 +++- src/main/java/com/atwoz/alert/application/AlertService.java | 1 - .../com/atwoz/alert/infrastructure/FirebaseAlertManager.java | 2 +- .../java/com/atwoz/alert/application/AlertServiceTest.java | 1 - .../java/com/atwoz/alert/infrastructure/AlertFakeManager.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename src/main/java/com/atwoz/alert/{domain => application}/AlertManager.java (55%) diff --git a/src/main/java/com/atwoz/alert/domain/AlertManager.java b/src/main/java/com/atwoz/alert/application/AlertManager.java similarity index 55% rename from src/main/java/com/atwoz/alert/domain/AlertManager.java rename to src/main/java/com/atwoz/alert/application/AlertManager.java index f90615dc..363854c2 100644 --- a/src/main/java/com/atwoz/alert/domain/AlertManager.java +++ b/src/main/java/com/atwoz/alert/application/AlertManager.java @@ -1,4 +1,6 @@ -package com.atwoz.alert.domain; +package com.atwoz.alert.application; + +import com.atwoz.alert.domain.Alert; public interface AlertManager { diff --git a/src/main/java/com/atwoz/alert/application/AlertService.java b/src/main/java/com/atwoz/alert/application/AlertService.java index 07e2100f..bc77c8a0 100644 --- a/src/main/java/com/atwoz/alert/application/AlertService.java +++ b/src/main/java/com/atwoz/alert/application/AlertService.java @@ -1,7 +1,6 @@ package com.atwoz.alert.application; import com.atwoz.alert.domain.Alert; -import com.atwoz.alert.domain.AlertManager; import com.atwoz.alert.domain.AlertRepository; import com.atwoz.alert.domain.AlertTokenRepository; import com.atwoz.alert.domain.vo.AlertGroup; diff --git a/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java b/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java index b9d31cf8..02b1ed3d 100644 --- a/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java +++ b/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java @@ -1,7 +1,7 @@ package com.atwoz.alert.infrastructure; import com.atwoz.alert.domain.Alert; -import com.atwoz.alert.domain.AlertManager; +import com.atwoz.alert.application.AlertManager; import com.google.firebase.messaging.FirebaseMessaging; import com.google.firebase.messaging.Message; import com.google.firebase.messaging.Notification; diff --git a/src/test/java/com/atwoz/alert/application/AlertServiceTest.java b/src/test/java/com/atwoz/alert/application/AlertServiceTest.java index 4c69baf9..31c9a6fd 100644 --- a/src/test/java/com/atwoz/alert/application/AlertServiceTest.java +++ b/src/test/java/com/atwoz/alert/application/AlertServiceTest.java @@ -1,7 +1,6 @@ package com.atwoz.alert.application; import com.atwoz.alert.domain.Alert; -import com.atwoz.alert.domain.AlertManager; import com.atwoz.alert.domain.AlertRepository; import com.atwoz.alert.domain.AlertTokenRepository; import com.atwoz.alert.domain.vo.AlertGroup; diff --git a/src/test/java/com/atwoz/alert/infrastructure/AlertFakeManager.java b/src/test/java/com/atwoz/alert/infrastructure/AlertFakeManager.java index 0b3cfa69..36eddbb7 100644 --- a/src/test/java/com/atwoz/alert/infrastructure/AlertFakeManager.java +++ b/src/test/java/com/atwoz/alert/infrastructure/AlertFakeManager.java @@ -1,7 +1,7 @@ package com.atwoz.alert.infrastructure; import com.atwoz.alert.domain.Alert; -import com.atwoz.alert.domain.AlertManager; +import com.atwoz.alert.application.AlertManager; public class AlertFakeManager implements AlertManager { From 4c8cca266733fefc96faa3ed9f53685344d2126e Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sat, 13 Jul 2024 12:06:29 +0900 Subject: [PATCH 45/62] =?UTF-8?q?refactor:=20Alert=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=8B=9C=20=EB=B9=8C=EB=8D=94=20=ED=8C=A8=ED=84=B4=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Alert 생성 시 빌더 패턴 적용되도록 수정 --- src/main/java/com/atwoz/alert/domain/Alert.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/atwoz/alert/domain/Alert.java b/src/main/java/com/atwoz/alert/domain/Alert.java index 5d07548e..cb830abe 100644 --- a/src/main/java/com/atwoz/alert/domain/Alert.java +++ b/src/main/java/com/atwoz/alert/domain/Alert.java @@ -41,16 +41,14 @@ public class Alert extends SoftDeleteBaseEntity { @Column(nullable = false) private Long receiverId; - private Alert(final AlertGroup alertGroup, final AlertMessage alertMessage, final Long receiverId) { - this.alertGroup = alertGroup; - this.alertMessage = alertMessage; - this.receiverId = receiverId; - this.isRead = false; - } - public static Alert createWith(final AlertGroup group, final String title, final String body, final Long receiverId) { AlertMessage message = AlertMessage.createWith(title, body); - return new Alert(group, message, receiverId); + return Alert.builder() + .alertGroup(group) + .alertMessage(message) + .receiverId(receiverId) + .isRead(false) + .build(); } public void read() { From 64040f35bb9f246309d7d56256d65ba3ace290ef Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sat, 13 Jul 2024 12:08:03 +0900 Subject: [PATCH 46/62] =?UTF-8?q?refactor:=20AlertService=20orElseThrow=20?= =?UTF-8?q?=EC=B6=95=EC=95=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AlertService orElseThrow 축약 적용 --- .../com/atwoz/alert/application/AlertService.java | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/atwoz/alert/application/AlertService.java b/src/main/java/com/atwoz/alert/application/AlertService.java index bc77c8a0..04d37b88 100644 --- a/src/main/java/com/atwoz/alert/application/AlertService.java +++ b/src/main/java/com/atwoz/alert/application/AlertService.java @@ -10,8 +10,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; - @RequiredArgsConstructor @Transactional @Service @@ -35,13 +33,10 @@ public void sendAlert(final AlertGroup group, final String title, final String b } public Alert readAlert(final Long memberId, final Long id) { - Optional alert = alertRepository.findByMemberIdAndId(memberId, id); - if (alert.isEmpty()) { - throw new AlertNotFoundException(); - } - Alert targetAlert = alert.get(); - targetAlert.read(); - return targetAlert; + Alert alert = alertRepository.findByMemberIdAndId(memberId, id) + .orElseThrow(AlertNotFoundException::new); + alert.read(); + return alert; } @Scheduled(cron = MIDNIGHT) From 22f27bf4461256e4c492fd8d0b6e44cdd03d8e5e Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sat, 13 Jul 2024 12:26:54 +0900 Subject: [PATCH 47/62] =?UTF-8?q?chore:=20Firebase=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EB=B2=84=EC=A0=84=20=EC=97=85?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 파이어베이스 이후 버전의 특징 (sendEach, 트래픽 처리 개선 등)을 적용하기 위해 버전 업그레이드 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 71235723..0161c2e0 100644 --- a/build.gradle +++ b/build.gradle @@ -80,7 +80,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' // Firebase - implementation 'com.google.firebase:firebase-admin:8.0.0' + implementation 'com.google.firebase:firebase-admin:9.3.0' } def generated = 'src/main/generated' From 976ea50bb684ad49dfeb53cc635cda02b93909e6 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sat, 13 Jul 2024 13:18:11 +0900 Subject: [PATCH 48/62] =?UTF-8?q?refactor:=20=EC=95=8C=EB=A6=BC=EC=9D=98?= =?UTF-8?q?=20=ED=8A=B9=EC=84=B1=EC=9D=84=20=EA=B3=A0=EB=A0=A4=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 외부 트랜잭션이 커밋된 이후에만 이벤트를 발송할 수 있도록 & 추가적인 데이터베이스 작업이 필요하므로 TransactionalEventListener로 변경 - 커밋 후 트랜잭션 작업을 하기 위해 REQUIRES_NEW 적용 --- .../java/com/atwoz/alert/application/AlertEventHandler.java | 6 +++--- src/main/java/com/atwoz/alert/application/AlertService.java | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/atwoz/alert/application/AlertEventHandler.java b/src/main/java/com/atwoz/alert/application/AlertEventHandler.java index 49f9fa04..53772f73 100644 --- a/src/main/java/com/atwoz/alert/application/AlertEventHandler.java +++ b/src/main/java/com/atwoz/alert/application/AlertEventHandler.java @@ -3,8 +3,8 @@ import com.atwoz.alert.application.event.AlertCreatedEvent; import com.atwoz.alert.application.event.AlertTokenCreatedEvent; import lombok.RequiredArgsConstructor; -import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionalEventListener; @RequiredArgsConstructor @Component @@ -12,12 +12,12 @@ public class AlertEventHandler { private final AlertService alertService; - @EventListener + @TransactionalEventListener public void sendAlertCreatedEvent(final AlertCreatedEvent event) { alertService.sendAlert(event.group(), event.title(), event.body(), event.sender(), event.receiverId()); } - @EventListener + @TransactionalEventListener public void sendAlertTokenCreatedEvent(final AlertTokenCreatedEvent event) { alertService.saveToken(event.id(), event.token()); } diff --git a/src/main/java/com/atwoz/alert/application/AlertService.java b/src/main/java/com/atwoz/alert/application/AlertService.java index 04d37b88..e35235fc 100644 --- a/src/main/java/com/atwoz/alert/application/AlertService.java +++ b/src/main/java/com/atwoz/alert/application/AlertService.java @@ -8,10 +8,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor -@Transactional +@Transactional(propagation = Propagation.REQUIRES_NEW) @Service public class AlertService { From 734c23cf0cfcf3c30bc5c7ffa50ad7150873e964 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sat, 13 Jul 2024 15:12:11 +0900 Subject: [PATCH 49/62] =?UTF-8?q?fix:=20AlertMessage=20Equals&HashCode=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테스트 정상화를 위해 AlertMessage Equals&HashCode 추가 --- src/main/java/com/atwoz/alert/domain/vo/AlertMessage.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/atwoz/alert/domain/vo/AlertMessage.java b/src/main/java/com/atwoz/alert/domain/vo/AlertMessage.java index edcb327f..b5e1ea60 100644 --- a/src/main/java/com/atwoz/alert/domain/vo/AlertMessage.java +++ b/src/main/java/com/atwoz/alert/domain/vo/AlertMessage.java @@ -3,10 +3,12 @@ import jakarta.persistence.Embeddable; import lombok.AccessLevel; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @Getter +@EqualsAndHashCode @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PROTECTED) @Embeddable From 4ea704682d683a8f946bdf69b8e1fa58405ab422 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sat, 13 Jul 2024 17:54:12 +0900 Subject: [PATCH 50/62] =?UTF-8?q?fix:=20AuditingHandler=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=A9=EC=8B=9D=20=EC=9D=B4=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AlertQueryRepositoryTest에 AuditingHandler 추가 --- .../alert/infrastructure/AlertQueryRepositoryTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java b/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java index 8bf2ce5a..40722417 100644 --- a/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java +++ b/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java @@ -10,9 +10,11 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.auditing.AuditingHandler; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -29,6 +31,9 @@ class AlertQueryRepositoryTest extends IntegrationHelper { @Autowired private AlertQueryRepository alertQueryRepository; + @Autowired + private AuditingHandler auditingHandler; + @Autowired private AlertRepository alertRepository; @@ -43,6 +48,8 @@ class 알림_조회_정상 { for (int day = 1; day <= 10; day++) { Alert alert = 알림_생성_제목_날짜_주입("알림 제목 " + day, day); + LocalDateTime futureTime = LocalDateTime.now().plusDays(day); + auditingHandler.setDateTimeProvider(() -> Optional.of(futureTime)); alertRepository.save(alert); alerts.add(alert); } From 24d5187ac58d719451d9c886baad431e44cdccbb Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sat, 13 Jul 2024 18:09:44 +0900 Subject: [PATCH 51/62] =?UTF-8?q?fix:=20LocalDateTime=20=EB=B9=84=EA=B5=90?= =?UTF-8?q?=20=EC=A0=9C=EC=99=B8=20=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 값 비교 시 LocalDateTime 제외되도록 수정 --- .../alert/infrastructure/AlertQueryRepositoryTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java b/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java index 40722417..1b9fadf5 100644 --- a/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java +++ b/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java @@ -64,7 +64,9 @@ class 알림_조회_정상 { assertSoftly(softly -> { softly.assertThat(found).hasSize(9); softly.assertThat(found.hasNext()).isTrue(); - softly.assertThat(found.getContent()).isEqualTo(expected); + softly.assertThat(found.getContent()) + .usingRecursiveFieldByFieldElementComparatorIgnoringFields("createdAt", "updatedAt", "deletedAt") + .isEqualTo(expected); }); } @@ -99,7 +101,7 @@ private List extractAlertResponsesWithLimit(final List { softly.assertThat(found).isNotEmpty(); softly.assertThat(found.get()).usingRecursiveComparison() - .ignoringFields("id") + .ignoringFields("id", "createdAt", "updatedAt", "deletedAt") .isEqualTo(alert); }); } From 808f8c3f288d6b3f0a458b11ea06d2dbab30b585 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sat, 13 Jul 2024 18:19:35 +0900 Subject: [PATCH 52/62] =?UTF-8?q?refactor:=20AuditingHandler=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AuditingHandler 제거 --- .../alert/infrastructure/AlertQueryRepositoryTest.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java b/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java index 1b9fadf5..a70a880a 100644 --- a/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java +++ b/src/test/java/com/atwoz/alert/infrastructure/AlertQueryRepositoryTest.java @@ -10,11 +10,9 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.auditing.AuditingHandler; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -31,9 +29,6 @@ class AlertQueryRepositoryTest extends IntegrationHelper { @Autowired private AlertQueryRepository alertQueryRepository; - @Autowired - private AuditingHandler auditingHandler; - @Autowired private AlertRepository alertRepository; @@ -48,8 +43,6 @@ class 알림_조회_정상 { for (int day = 1; day <= 10; day++) { Alert alert = 알림_생성_제목_날짜_주입("알림 제목 " + day, day); - LocalDateTime futureTime = LocalDateTime.now().plusDays(day); - auditingHandler.setDateTimeProvider(() -> Optional.of(futureTime)); alertRepository.save(alert); alerts.add(alert); } From 0f59e986e29b5e07160dffd18c51e0481849c1a9 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sun, 14 Jul 2024 20:57:18 +0900 Subject: [PATCH 53/62] =?UTF-8?q?refactor:=20AlertGroup=20Enumerated=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AlertGroup Enumerated value 수정 --- src/main/java/com/atwoz/alert/domain/Alert.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/atwoz/alert/domain/Alert.java b/src/main/java/com/atwoz/alert/domain/Alert.java index cb830abe..0aca8b41 100644 --- a/src/main/java/com/atwoz/alert/domain/Alert.java +++ b/src/main/java/com/atwoz/alert/domain/Alert.java @@ -6,6 +6,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -30,7 +31,7 @@ public class Alert extends SoftDeleteBaseEntity { @Column(nullable = false) private Boolean isRead; - @Enumerated + @Enumerated(value = EnumType.STRING) @Column(nullable = false) private AlertGroup alertGroup; From dab7f33902deabe3a7fc0cf7e9c20c14c37f1bb0 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Thu, 18 Jul 2024 15:03:55 +0900 Subject: [PATCH 54/62] =?UTF-8?q?fix:=20import=20=EA=B5=90=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 바뀐 경로 import 교체 - assertSoftly 문제 수정 --- .../com/atwoz/alert/ui/AlertControllerAcceptanceFixture.java | 2 +- .../atwoz/member/application/auth/MemberAuthServiceTest.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/atwoz/alert/ui/AlertControllerAcceptanceFixture.java b/src/test/java/com/atwoz/alert/ui/AlertControllerAcceptanceFixture.java index e7f2b1d8..6bf3f869 100644 --- a/src/test/java/com/atwoz/alert/ui/AlertControllerAcceptanceFixture.java +++ b/src/test/java/com/atwoz/alert/ui/AlertControllerAcceptanceFixture.java @@ -17,7 +17,7 @@ import org.springframework.http.HttpStatus; import static com.atwoz.alert.fixture.AlertFixture.알림_생성_제목_날짜_회원id_주입; -import static com.atwoz.member.fixture.MemberFixture.일반_유저_생성; +import static com.atwoz.member.fixture.member.MemberFixture.일반_유저_생성; import static org.assertj.core.api.SoftAssertions.assertSoftly; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) diff --git a/src/test/java/com/atwoz/member/application/auth/MemberAuthServiceTest.java b/src/test/java/com/atwoz/member/application/auth/MemberAuthServiceTest.java index ce75b9ad..25373f0b 100644 --- a/src/test/java/com/atwoz/member/application/auth/MemberAuthServiceTest.java +++ b/src/test/java/com/atwoz/member/application/auth/MemberAuthServiceTest.java @@ -18,6 +18,7 @@ import org.springframework.test.context.event.RecordApplicationEvents; import static com.atwoz.member.fixture.auth.OAuthProviderFixture.인증_기관_생성; +import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; From 72806c71fa0c3f054e97e2f1ee24627b8a6d3b2e Mon Sep 17 00:00:00 2001 From: devholic22 Date: Fri, 19 Jul 2024 01:24:48 +0900 Subject: [PATCH 55/62] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EC=8A=A4=EC=BC=80=EC=A4=84=EB=9F=AC=20=EB=B6=84?= =?UTF-8?q?=EC=82=B0=20=EB=9D=BD=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 레디스 분산 락을 적용하기 위해 redisson 등록 - AlertScheduler 인터페이스 분리 - 기존 AlertService 스케줄러 메서드 삭제 및 이동 - RedissonAlertSchedulerTest 테스트 작성 --- build.gradle | 1 + .../alert/application/AlertScheduler.java | 6 ++ .../atwoz/alert/application/AlertService.java | 8 -- .../exception/AlertExceptionHandler.java | 6 ++ .../exceptions/AlertLockException.java | 8 ++ .../RedissonAlertScheduler.java | 46 +++++++++ .../com/atwoz/global/config/RedisConfig.java | 14 +++ .../alert/application/AlertServiceTest.java | 22 ----- .../RedissonAlertSchedulerTest.java | 93 +++++++++++++++++++ 9 files changed, 174 insertions(+), 30 deletions(-) create mode 100644 src/main/java/com/atwoz/alert/application/AlertScheduler.java create mode 100644 src/main/java/com/atwoz/alert/exception/exceptions/AlertLockException.java create mode 100644 src/main/java/com/atwoz/alert/infrastructure/RedissonAlertScheduler.java create mode 100644 src/test/java/com/atwoz/alert/infrastructure/RedissonAlertSchedulerTest.java diff --git a/build.gradle b/build.gradle index 0161c2e0..00152265 100644 --- a/build.gradle +++ b/build.gradle @@ -43,6 +43,7 @@ dependencies { runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.redisson:redisson-spring-boot-starter:3.33.0' // flyway implementation 'org.flywaydb:flyway-core' diff --git a/src/main/java/com/atwoz/alert/application/AlertScheduler.java b/src/main/java/com/atwoz/alert/application/AlertScheduler.java new file mode 100644 index 00000000..8b930381 --- /dev/null +++ b/src/main/java/com/atwoz/alert/application/AlertScheduler.java @@ -0,0 +1,6 @@ +package com.atwoz.alert.application; + +public interface AlertScheduler { + + void deleteExpiredAlerts(); +} diff --git a/src/main/java/com/atwoz/alert/application/AlertService.java b/src/main/java/com/atwoz/alert/application/AlertService.java index e35235fc..6a4d7ba7 100644 --- a/src/main/java/com/atwoz/alert/application/AlertService.java +++ b/src/main/java/com/atwoz/alert/application/AlertService.java @@ -6,7 +6,6 @@ import com.atwoz.alert.domain.vo.AlertGroup; import com.atwoz.alert.exception.exceptions.AlertNotFoundException; import lombok.RequiredArgsConstructor; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -16,8 +15,6 @@ @Service public class AlertService { - private static final String MIDNIGHT = "0 0 0 * * ?"; - private final AlertRepository alertRepository; private final AlertTokenRepository tokenRepository; private final AlertManager alertManager; @@ -39,9 +36,4 @@ public Alert readAlert(final Long memberId, final Long id) { alert.read(); return alert; } - - @Scheduled(cron = MIDNIGHT) - public void deleteExpiredAlerts() { - alertRepository.deleteExpiredAlerts(); - } } diff --git a/src/main/java/com/atwoz/alert/exception/AlertExceptionHandler.java b/src/main/java/com/atwoz/alert/exception/AlertExceptionHandler.java index 6195652d..abc05f8d 100644 --- a/src/main/java/com/atwoz/alert/exception/AlertExceptionHandler.java +++ b/src/main/java/com/atwoz/alert/exception/AlertExceptionHandler.java @@ -1,5 +1,6 @@ package com.atwoz.alert.exception; +import com.atwoz.alert.exception.exceptions.AlertLockException; import com.atwoz.alert.exception.exceptions.AlertNotFoundException; import com.atwoz.alert.exception.exceptions.AlertSendException; import com.atwoz.alert.exception.exceptions.ReceiverTokenNotFoundException; @@ -26,6 +27,11 @@ public ResponseEntity handleAlertNotFoundException(final AlertNotFoundEx return getExceptionWithStatus(e, HttpStatus.NOT_FOUND); } + @ExceptionHandler(AlertLockException.class) + public ResponseEntity handleAlertLockException(final AlertLockException e) { + return getExceptionWithStatus(e, HttpStatus.INTERNAL_SERVER_ERROR); + } + private ResponseEntity getExceptionWithStatus(final Exception exception, final HttpStatus status) { return ResponseEntity.status(status) .body(exception.getMessage()); diff --git a/src/main/java/com/atwoz/alert/exception/exceptions/AlertLockException.java b/src/main/java/com/atwoz/alert/exception/exceptions/AlertLockException.java new file mode 100644 index 00000000..84e889bf --- /dev/null +++ b/src/main/java/com/atwoz/alert/exception/exceptions/AlertLockException.java @@ -0,0 +1,8 @@ +package com.atwoz.alert.exception.exceptions; + +public class AlertLockException extends RuntimeException { + + public AlertLockException() { + super("알림 락 획득 과정에서 예외가 발생하였습니다."); + } +} diff --git a/src/main/java/com/atwoz/alert/infrastructure/RedissonAlertScheduler.java b/src/main/java/com/atwoz/alert/infrastructure/RedissonAlertScheduler.java new file mode 100644 index 00000000..b9ab8a0f --- /dev/null +++ b/src/main/java/com/atwoz/alert/infrastructure/RedissonAlertScheduler.java @@ -0,0 +1,46 @@ +package com.atwoz.alert.infrastructure; + +import com.atwoz.alert.application.AlertScheduler; +import com.atwoz.alert.domain.AlertRepository; +import com.atwoz.alert.exception.exceptions.AlertLockException; +import lombok.RequiredArgsConstructor; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@RequiredArgsConstructor +@Component +public class RedissonAlertScheduler implements AlertScheduler { + + private static final String MIDNIGHT = "0 0 0 * * ?"; + private static final String DELETE_ALERT_LOCK = "delete_alert_lock"; + private static final long WAIT_TIME = 0L; + private static final long HOLD_TIME = 40L; + + private final AlertRepository alertRepository; + private final RedissonClient redissonClient; + + @Scheduled(cron = MIDNIGHT) + @Override + public void deleteExpiredAlerts() { + RLock lock = redissonClient.getLock(DELETE_ALERT_LOCK); + boolean isLocked = false; + try { + isLocked = lock.tryLock(WAIT_TIME, HOLD_TIME, TimeUnit.SECONDS); + if (isLocked) { + alertRepository.deleteExpiredAlerts(); + return; + } + throw new AlertLockException(); + } catch (InterruptedException e) { + throw new AlertLockException(); + } finally { + if (isLocked) { + lock.unlock(); + } + } + } +} diff --git a/src/main/java/com/atwoz/global/config/RedisConfig.java b/src/main/java/com/atwoz/global/config/RedisConfig.java index 01e70cee..afe4ebe4 100644 --- a/src/main/java/com/atwoz/global/config/RedisConfig.java +++ b/src/main/java/com/atwoz/global/config/RedisConfig.java @@ -1,6 +1,10 @@ package com.atwoz.global.config; import lombok.RequiredArgsConstructor; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.SingleServerConfig; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -14,6 +18,8 @@ @Configuration public class RedisConfig { + private static final String REDISSON_HOST_PREFIX = "redis://"; + private final RedisProperties redisProperties; @Bean @@ -21,6 +27,14 @@ public RedisConnectionFactory redisConnectionFactory() { return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort()); } + @Bean + public RedissonClient redissonClient() { + Config redissonConfig = new Config(); + SingleServerConfig singleServer = redissonConfig.useSingleServer(); + singleServer.setAddress(REDISSON_HOST_PREFIX + redisProperties.getHost() + ":" + redisProperties.getPort()); + return Redisson.create(redissonConfig); + } + @Bean public RedisTemplate redisTemplate() { RedisTemplate redisTemplate = new RedisTemplate<>(); diff --git a/src/test/java/com/atwoz/alert/application/AlertServiceTest.java b/src/test/java/com/atwoz/alert/application/AlertServiceTest.java index 31c9a6fd..c67e0495 100644 --- a/src/test/java/com/atwoz/alert/application/AlertServiceTest.java +++ b/src/test/java/com/atwoz/alert/application/AlertServiceTest.java @@ -16,9 +16,7 @@ import org.junit.jupiter.api.Test; import java.util.Optional; -import static com.atwoz.alert.fixture.AlertFixture.알림_생성_id_없음; import static com.atwoz.alert.fixture.AlertFixture.알림_생성_id_있음; -import static com.atwoz.alert.fixture.AlertFixture.옛날_알림_생성; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @@ -177,24 +175,4 @@ class 알림_단일_조회_관리 { .isInstanceOf(AlertNotFoundException.class); } } - - @Test - void 생성된_지_60일을_초과한_알림은_삭제_상태로_된다() { - // given - Long memberId = 1L; - Alert savedAlert = alertRepository.save(알림_생성_id_없음()); - Alert savedOldAlert = alertRepository.save(옛날_알림_생성()); - - // when - alertService.deleteExpiredAlerts(); - - // then - Optional foundSavedAlert = alertRepository.findByMemberIdAndId(memberId, savedAlert.getId()); - Optional foundSavedOldAlert = alertRepository.findByMemberIdAndId(memberId, savedOldAlert.getId()); - - assertSoftly(softly -> { - softly.assertThat(foundSavedAlert).isPresent(); - softly.assertThat(foundSavedOldAlert).isEmpty(); - }); - } } diff --git a/src/test/java/com/atwoz/alert/infrastructure/RedissonAlertSchedulerTest.java b/src/test/java/com/atwoz/alert/infrastructure/RedissonAlertSchedulerTest.java new file mode 100644 index 00000000..f47e5ab6 --- /dev/null +++ b/src/test/java/com/atwoz/alert/infrastructure/RedissonAlertSchedulerTest.java @@ -0,0 +1,93 @@ +package com.atwoz.alert.infrastructure; + +import com.atwoz.alert.domain.Alert; +import com.atwoz.alert.domain.AlertRepository; +import com.atwoz.helper.IntegrationHelper; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.auditing.AuditingHandler; + +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import static com.atwoz.alert.fixture.AlertFixture.알림_생성_id_없음; +import static com.atwoz.alert.fixture.AlertFixture.옛날_알림_생성; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class RedissonAlertSchedulerTest extends IntegrationHelper { + + @Autowired + private RedissonAlertScheduler redissonAlertScheduler; + + @Autowired + private AuditingHandler auditingHandler; + + @Autowired + private AlertRepository alertRepository; + + @Test + void 생성된_지_60일을_초과한_알림은_삭제_상태로_된다() { + // given + Long memberId = 1L; + + LocalDateTime pastTime = LocalDateTime.now() + .minusDays(61); + auditingHandler.setDateTimeProvider(() -> Optional.of(pastTime)); + Alert savedOldAlert = alertRepository.save(옛날_알림_생성()); + + auditingHandler.setDateTimeProvider(() -> Optional.of(LocalDateTime.now())); + Alert savedAlert = alertRepository.save(알림_생성_id_없음()); + + // when + redissonAlertScheduler.deleteExpiredAlerts(); + + // then + Optional foundSavedAlert = alertRepository.findByMemberIdAndId(memberId, savedAlert.getId()); + Optional foundSavedOldAlert = alertRepository.findByMemberIdAndId(memberId, savedOldAlert.getId()); + + assertSoftly(softly -> { + softly.assertThat(foundSavedAlert).isPresent(); + softly.assertThat(foundSavedOldAlert).isPresent(); + Alert recentAlert = foundSavedAlert.get(); + Alert oldAlert = foundSavedOldAlert.get(); + softly.assertThat(recentAlert.getDeletedAt()).isNull(); + softly.assertThat(oldAlert.getDeletedAt()).isNotNull(); + }); + } + + @Test + void 분산_락으로_중복호출을_막는다() throws InterruptedException { + // given + int numberOfThreads = 5; + ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads); + CountDownLatch latch = new CountDownLatch(numberOfThreads); + AtomicLong atomicLong = new AtomicLong(); + + // when + for (int i = 0; i < numberOfThreads; i++) { + executorService.submit(() -> { + try { + redissonAlertScheduler.deleteExpiredAlerts(); + atomicLong.incrementAndGet(); + } finally { + latch.countDown(); + } + }); + } + + latch.await(40, TimeUnit.SECONDS); + executorService.shutdown(); + + // then + assertThat(atomicLong.get()).isEqualTo(1); + } +} From 41335c046cc870da4f32eb5a57711989ee6dc1fe Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sat, 20 Jul 2024 19:17:36 +0900 Subject: [PATCH 56/62] =?UTF-8?q?refactor:=20Alert=20=EC=9D=B8=EB=8D=B1?= =?UTF-8?q?=EC=8B=B1=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/atwoz/alert/domain/Alert.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/atwoz/alert/domain/Alert.java b/src/main/java/com/atwoz/alert/domain/Alert.java index 0aca8b41..889e9f58 100644 --- a/src/main/java/com/atwoz/alert/domain/Alert.java +++ b/src/main/java/com/atwoz/alert/domain/Alert.java @@ -11,6 +11,8 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -21,6 +23,7 @@ @SuperBuilder @EqualsAndHashCode(of = "id", callSuper = false) @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(indexes = {@Index(name = "alert_index", columnList = "receiver_id, deleted_at, created_at DESC, id DESC")}) @Entity public class Alert extends SoftDeleteBaseEntity { From 69697dd7eeed5422f5491969f595dc5ee3a29cfb Mon Sep 17 00:00:00 2001 From: devholic22 Date: Mon, 22 Jul 2024 12:17:27 +0900 Subject: [PATCH 57/62] =?UTF-8?q?refactor:=20=EB=B9=84=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9=20=EB=B0=A9=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Async 어노테이션으로 알림 전송 스레드 분리 - Async 전용 설정 파일 생성 - Firebase 전용 스레드 매니저 생성 - 트랜잭션 전파 수준 수정 --- src/main/java/com/atwoz/AtwozApplication.java | 2 - .../alert/application/AlertEventHandler.java | 4 + .../atwoz/alert/application/AlertService.java | 3 +- .../atwoz/alert/config/FirebaseConfig.java | 4 +- .../alert/config/FirebaseThreadManager.java | 30 ++++++ .../infrastructure/FirebaseAlertManager.java | 98 ++++++++++++++++++- .../com/atwoz/global/config/AsyncConfig.java | 48 +++++++++ 7 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/atwoz/alert/config/FirebaseThreadManager.java create mode 100644 src/main/java/com/atwoz/global/config/AsyncConfig.java diff --git a/src/main/java/com/atwoz/AtwozApplication.java b/src/main/java/com/atwoz/AtwozApplication.java index 4e15462e..926482f3 100644 --- a/src/main/java/com/atwoz/AtwozApplication.java +++ b/src/main/java/com/atwoz/AtwozApplication.java @@ -4,10 +4,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; -import org.springframework.scheduling.annotation.EnableAsync; @EnableJpaAuditing -@EnableAsync @SpringBootApplication @ConfigurationPropertiesScan public class AtwozApplication { diff --git a/src/main/java/com/atwoz/alert/application/AlertEventHandler.java b/src/main/java/com/atwoz/alert/application/AlertEventHandler.java index 53772f73..0239214c 100644 --- a/src/main/java/com/atwoz/alert/application/AlertEventHandler.java +++ b/src/main/java/com/atwoz/alert/application/AlertEventHandler.java @@ -3,6 +3,7 @@ import com.atwoz.alert.application.event.AlertCreatedEvent; import com.atwoz.alert.application.event.AlertTokenCreatedEvent; import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import org.springframework.transaction.event.TransactionalEventListener; @@ -10,8 +11,11 @@ @Component public class AlertEventHandler { + private static final String ASYNC_EXECUTOR = "asyncExecutor"; + private final AlertService alertService; + @Async(value = ASYNC_EXECUTOR) @TransactionalEventListener public void sendAlertCreatedEvent(final AlertCreatedEvent event) { alertService.sendAlert(event.group(), event.title(), event.body(), event.sender(), event.receiverId()); diff --git a/src/main/java/com/atwoz/alert/application/AlertService.java b/src/main/java/com/atwoz/alert/application/AlertService.java index 6a4d7ba7..9271538a 100644 --- a/src/main/java/com/atwoz/alert/application/AlertService.java +++ b/src/main/java/com/atwoz/alert/application/AlertService.java @@ -7,11 +7,10 @@ import com.atwoz.alert.exception.exceptions.AlertNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor -@Transactional(propagation = Propagation.REQUIRES_NEW) +@Transactional @Service public class AlertService { diff --git a/src/main/java/com/atwoz/alert/config/FirebaseConfig.java b/src/main/java/com/atwoz/alert/config/FirebaseConfig.java index f03aee43..4d9114e9 100644 --- a/src/main/java/com/atwoz/alert/config/FirebaseConfig.java +++ b/src/main/java/com/atwoz/alert/config/FirebaseConfig.java @@ -5,6 +5,7 @@ import com.google.auth.oauth2.GoogleCredentials; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; +import com.google.firebase.ThreadManager; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -70,8 +71,9 @@ public FirebaseApp firebaseApp() { if (!FirebaseApp.getApps().isEmpty()) { return FirebaseApp.getInstance(); } - + ThreadManager threadManager = new FirebaseThreadManager(); FirebaseOptions options = new FirebaseOptions.Builder() + .setThreadManager(threadManager) .setCredentials(getCredentials()) .build(); return FirebaseApp.initializeApp(options); diff --git a/src/main/java/com/atwoz/alert/config/FirebaseThreadManager.java b/src/main/java/com/atwoz/alert/config/FirebaseThreadManager.java new file mode 100644 index 00000000..edbee481 --- /dev/null +++ b/src/main/java/com/atwoz/alert/config/FirebaseThreadManager.java @@ -0,0 +1,30 @@ +package com.atwoz.alert.config; + +import com.google.firebase.FirebaseApp; +import com.google.firebase.ThreadManager; +import org.springframework.stereotype.Component; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +@Component +public class FirebaseThreadManager extends ThreadManager { + + private static final int FIREBASE_THREADS_SIZE = 40; + + @Override + protected ExecutorService getExecutor(final FirebaseApp firebaseApp) { + return Executors.newFixedThreadPool(FIREBASE_THREADS_SIZE); + } + + @Override + protected void releaseExecutor(final FirebaseApp firebaseApp, final ExecutorService executorService) { + executorService.shutdownNow(); + } + + @Override + protected ThreadFactory getThreadFactory() { + return Executors.defaultThreadFactory(); + } +} diff --git a/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java b/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java index 02b1ed3d..43a23c34 100644 --- a/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java +++ b/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java @@ -1,25 +1,55 @@ package com.atwoz.alert.infrastructure; -import com.atwoz.alert.domain.Alert; import com.atwoz.alert.application.AlertManager; +import com.atwoz.alert.domain.Alert; +import com.google.api.core.ApiFuture; import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.MessagingErrorCode; import com.google.firebase.messaging.Notification; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +@Slf4j @Component public class FirebaseAlertManager implements AlertManager { + private static final String EXECUTOR = "alertCallbackExecutor"; private static final String GROUP = "group"; private static final String SENDER = "sender"; private static final String CREATED_TIME = "created_at"; private static final String TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + private static final String THREAD_PROBLEM = "FCM 스레드에서 문제가 발생했습니다."; + private static final String ALERT_FAIL = "알림 전송 실패"; + private static final String ALERT_RETRY_FAIL = "알림 재전송 실패"; + private static final String ALERT_THREAD_WAIT_FAIL = "알림 재전송 스레드 대기 예외"; + private static final int RETRY_MAX = 3; + private static final int[] RETRY_TIMES = {1000, 2000, 4000}; + + private final Executor executor; + + public FirebaseAlertManager(@Qualifier(value = EXECUTOR) final Executor executor) { + this.executor = executor; + } @Override public void send(final Alert alert, final String sender, final String token) { + Message firebaseMessage = createAlertMessage(alert, sender, token); + FirebaseMessaging firebase = FirebaseMessaging.getInstance(); + ApiFuture process = firebase.sendAsync(firebaseMessage); + + Runnable task = () -> logAlertResult(process, firebaseMessage); + process.addListener(task, executor); + } + + private Message createAlertMessage(final Alert alert, final String sender, final String token) { LocalDateTime createdAt = alert.getCreatedAt(); String time = createdAt.format(DateTimeFormatter.ofPattern(TIME_FORMAT)); @@ -27,14 +57,76 @@ public void send(final Alert alert, final String sender, final String token) { .setTitle(alert.getTitle()) .setBody(alert.getBody()) .build(); - Message firebaseMessage = Message.builder() + + return Message.builder() .setToken(token) .putData(GROUP, alert.getGroup()) .setNotification(firebaseNotification) .putData(SENDER, sender) .putData(CREATED_TIME, time) .build(); + } + + private void logAlertResult(final ApiFuture process, final Message message) { + try { + process.get(); + } catch (InterruptedException exception) { + log.error(THREAD_PROBLEM); + } catch (ExecutionException exception) { + log.error(ALERT_FAIL); + retryAlert(message, exception); + } + } + + private void retryAlert(final Message message, final ExecutionException exception) { + if (exception.getCause() instanceof FirebaseMessagingException e) { + MessagingErrorCode errorCode = e.getMessagingErrorCode(); + if (!isRetryErrorCode(errorCode)) { + return; + } + retryInThreeTimes(message); + } + } + + private boolean isRetryErrorCode(final MessagingErrorCode errorCode) { + return errorCode.equals(MessagingErrorCode.INTERNAL) || errorCode.equals(MessagingErrorCode.UNAVAILABLE); + } + + private void retryInThreeTimes(final Message message) { + int retryCount = 0; + while (retryCount < RETRY_MAX) { + if (!shouldRetry(message, retryCount)) { + break; + } + retryCount++; + } + + if (retryCount == RETRY_MAX) { + log.error(ALERT_RETRY_FAIL); + } + } + + private boolean shouldRetry(final Message message, final int retryCount) { + wait(retryCount); FirebaseMessaging firebase = FirebaseMessaging.getInstance(); - firebase.sendAsync(firebaseMessage); + try { + firebase.sendAsync(message).get(); + } catch (Exception exception) { + if (exception.getCause() instanceof FirebaseMessagingException e) { + MessagingErrorCode errorCode = e.getMessagingErrorCode(); + if (isRetryErrorCode(errorCode)) { + return true; + } + } + } + return false; + } + + private void wait(final int retryCount) { + try { + Thread.sleep(RETRY_TIMES[retryCount]); + } catch (InterruptedException exception) { + log.error(ALERT_THREAD_WAIT_FAIL); + } } } diff --git a/src/main/java/com/atwoz/global/config/AsyncConfig.java b/src/main/java/com/atwoz/global/config/AsyncConfig.java new file mode 100644 index 00000000..9a77a713 --- /dev/null +++ b/src/main/java/com/atwoz/global/config/AsyncConfig.java @@ -0,0 +1,48 @@ +package com.atwoz.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +@EnableAsync +@Configuration +public class AsyncConfig { + + private static final String ASYNC_EXECUTOR = "asyncExecutor"; + private static final String ALERT_CALLBACK_EXECUTOR = "alertCallbackExecutor"; + private static final String THREAD_PREFIX_NAME = "ATWOZ_ASYNC_THREAD: "; + private static final String CALLBACK_THREAD_PREFIX_NAME = "ATWOZ_ALERT_THREAD: "; + private static final int DEFAULT_THREAD_SIZE = 30; + private static final int MAX_THREAD_SIZE = 40; + private static final int QUEUE_SIZE = 100; + private static final boolean WAIT_TASK_COMPLETE = true; + private static final int DEFAULT_CALLBACK_THREAD_SIZE = 10; + + @Bean(name = ASYNC_EXECUTOR) + public Executor getAsyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(DEFAULT_THREAD_SIZE); + executor.setMaxPoolSize(MAX_THREAD_SIZE); + executor.setQueueCapacity(QUEUE_SIZE); + executor.setThreadNamePrefix(THREAD_PREFIX_NAME); + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + executor.setWaitForTasksToCompleteOnShutdown(WAIT_TASK_COMPLETE); + executor.initialize(); + return executor; + } + + @Bean(name = ALERT_CALLBACK_EXECUTOR) + public Executor getAsyncCallbackExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(DEFAULT_CALLBACK_THREAD_SIZE); + executor.setThreadNamePrefix(CALLBACK_THREAD_PREFIX_NAME); + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + executor.setWaitForTasksToCompleteOnShutdown(WAIT_TASK_COMPLETE); + executor.initialize(); + return executor; + } +} From a6bcbcc9d7855827989b28062f02a15b1550b303 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Wed, 24 Jul 2024 21:21:21 +0900 Subject: [PATCH 58/62] =?UTF-8?q?refactor:=20QueryService=20abstract=20cla?= =?UTF-8?q?ss=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 페이지 조회 공통 메서드를 축약하기 위해 BaseQueryService 생성 --- .../alert/application/AlertQueryService.java | 13 ++----------- .../global/application/BaseQueryService.java | 16 ++++++++++++++++ .../application/MemberLikeQueryService.java | 14 ++------------ .../MemberMissionsQueryService.java | 14 ++------------ 4 files changed, 22 insertions(+), 35 deletions(-) create mode 100644 src/main/java/com/atwoz/global/application/BaseQueryService.java diff --git a/src/main/java/com/atwoz/alert/application/AlertQueryService.java b/src/main/java/com/atwoz/alert/application/AlertQueryService.java index 1b1db1bb..204a439f 100644 --- a/src/main/java/com/atwoz/alert/application/AlertQueryService.java +++ b/src/main/java/com/atwoz/alert/application/AlertQueryService.java @@ -3,6 +3,7 @@ import com.atwoz.alert.domain.AlertRepository; import com.atwoz.alert.infrastructure.dto.AlertPagingResponse; import com.atwoz.alert.infrastructure.dto.AlertSearchResponse; +import com.atwoz.global.application.BaseQueryService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -12,10 +13,7 @@ @RequiredArgsConstructor @Transactional(readOnly = true) @Service -public class AlertQueryService { - - private static final int NEXT_PAGE_INDEX = 1; - private static final int NO_MORE_PAGE = -1; +public class AlertQueryService extends BaseQueryService { private final AlertRepository alertRepository; @@ -24,11 +22,4 @@ public AlertPagingResponse findMemberAlertsWithPaging(final Long memberId, final int nextPage = getNextPage(pageable.getPageNumber(), response); return AlertPagingResponse.of(response, nextPage); } - - private int getNextPage(final int pageNumber, final Page alerts) { - if (alerts.hasNext()) { - return pageNumber + NEXT_PAGE_INDEX; - } - return NO_MORE_PAGE; - } } diff --git a/src/main/java/com/atwoz/global/application/BaseQueryService.java b/src/main/java/com/atwoz/global/application/BaseQueryService.java new file mode 100644 index 00000000..13e53dbd --- /dev/null +++ b/src/main/java/com/atwoz/global/application/BaseQueryService.java @@ -0,0 +1,16 @@ +package com.atwoz.global.application; + +import org.springframework.data.domain.Page; + +public abstract class BaseQueryService { + + private static final int NEXT_PAGE_INDEX = 1; + private static final int NO_MORE_PAGE = -1; + + protected int getNextPage(final int pageNumber, final Page page) { + if (page.hasNext()) { + return pageNumber + NEXT_PAGE_INDEX; + } + return NO_MORE_PAGE; + } +} diff --git a/src/main/java/com/atwoz/memberlike/application/MemberLikeQueryService.java b/src/main/java/com/atwoz/memberlike/application/MemberLikeQueryService.java index 3d448a7f..5097e210 100644 --- a/src/main/java/com/atwoz/memberlike/application/MemberLikeQueryService.java +++ b/src/main/java/com/atwoz/memberlike/application/MemberLikeQueryService.java @@ -1,5 +1,6 @@ package com.atwoz.memberlike.application; +import com.atwoz.global.application.BaseQueryService; import com.atwoz.memberlike.domain.MemberLikeRepository; import com.atwoz.memberlike.infrastructure.dto.MemberLikePagingResponse; import com.atwoz.memberlike.infrastructure.dto.MemberLikeSimpleResponse; @@ -12,10 +13,7 @@ @RequiredArgsConstructor @Transactional(readOnly = true) @Service -public class MemberLikeQueryService { - - private static final int NEXT_PAGE_INDEX = 1; - private static final int NO_MORE_PAGE = -1; +public class MemberLikeQueryService extends BaseQueryService { private final MemberLikeRepository memberLikeRepository; @@ -25,14 +23,6 @@ public MemberLikePagingResponse findSendLikesWithPaging(final Long memberId, fin return MemberLikePagingResponse.of(response, nextPage); } - private int getNextPage(final int pageNumber, final Page memberLikes) { - if (memberLikes.hasNext()) { - return pageNumber + NEXT_PAGE_INDEX; - } - - return NO_MORE_PAGE; - } - public MemberLikePagingResponse findReceivedLikesWithPaging(final Long memberId, final Pageable pageable) { Page response = memberLikeRepository.findReceivedLikesWithPaging(memberId, pageable); int nextPage = getNextPage(pageable.getPageNumber(), response); diff --git a/src/main/java/com/atwoz/mission/application/membermission/MemberMissionsQueryService.java b/src/main/java/com/atwoz/mission/application/membermission/MemberMissionsQueryService.java index b00d715b..429c9c8d 100644 --- a/src/main/java/com/atwoz/mission/application/membermission/MemberMissionsQueryService.java +++ b/src/main/java/com/atwoz/mission/application/membermission/MemberMissionsQueryService.java @@ -1,5 +1,6 @@ package com.atwoz.mission.application.membermission; +import com.atwoz.global.application.BaseQueryService; import com.atwoz.mission.domain.membermission.MemberMissionsRepository; import com.atwoz.mission.intrastructure.membermission.dto.MemberMissionPagingResponse; import com.atwoz.mission.intrastructure.membermission.dto.MemberMissionSimpleResponse; @@ -12,10 +13,7 @@ @RequiredArgsConstructor @Transactional(readOnly = true) @Service -public class MemberMissionsQueryService { - - private static final int NEXT_PAGE_INDEX = 1; - private static final int NO_MORE_PAGE = -1; +public class MemberMissionsQueryService extends BaseQueryService { private final MemberMissionsRepository memberMissionsRepository; @@ -24,12 +22,4 @@ public MemberMissionPagingResponse findMemberMissionsWithPaging(final Long membe int nextPage = getNextPage(pageable.getPageNumber(), response); return MemberMissionPagingResponse.of(response, nextPage); } - - private int getNextPage(final int pageNumber, final Page memberMissions) { - if (memberMissions.hasNext()) { - return pageNumber + NEXT_PAGE_INDEX; - } - - return NO_MORE_PAGE; - } } From fd105a08f79b9f30a99ee249271946673b62ecc5 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Wed, 24 Jul 2024 22:31:48 +0900 Subject: [PATCH 59/62] =?UTF-8?q?refactor:=20RedissonLock=20AOP=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Redisson Lock 재사용을 위해 AOP 생성 및 적용 --- .../RedissonAlertScheduler.java | 25 ++-------- .../RedissonDistributedLock.java | 17 +++++++ .../RedissonDistributedLockAspect.java | 48 +++++++++++++++++++ 3 files changed, 69 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/atwoz/global/aspect/distributedlock/RedissonDistributedLock.java create mode 100644 src/main/java/com/atwoz/global/aspect/distributedlock/RedissonDistributedLockAspect.java diff --git a/src/main/java/com/atwoz/alert/infrastructure/RedissonAlertScheduler.java b/src/main/java/com/atwoz/alert/infrastructure/RedissonAlertScheduler.java index b9ab8a0f..eea87540 100644 --- a/src/main/java/com/atwoz/alert/infrastructure/RedissonAlertScheduler.java +++ b/src/main/java/com/atwoz/alert/infrastructure/RedissonAlertScheduler.java @@ -3,14 +3,11 @@ import com.atwoz.alert.application.AlertScheduler; import com.atwoz.alert.domain.AlertRepository; import com.atwoz.alert.exception.exceptions.AlertLockException; +import com.atwoz.global.aspect.distributedlock.RedissonDistributedLock; import lombok.RequiredArgsConstructor; -import org.redisson.api.RLock; -import org.redisson.api.RedissonClient; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import java.util.concurrent.TimeUnit; - @RequiredArgsConstructor @Component public class RedissonAlertScheduler implements AlertScheduler { @@ -21,26 +18,12 @@ public class RedissonAlertScheduler implements AlertScheduler { private static final long HOLD_TIME = 40L; private final AlertRepository alertRepository; - private final RedissonClient redissonClient; @Scheduled(cron = MIDNIGHT) + @RedissonDistributedLock(lockName = DELETE_ALERT_LOCK, waitTime = WAIT_TIME, + holdTime = HOLD_TIME, exceptionClass = AlertLockException.class) @Override public void deleteExpiredAlerts() { - RLock lock = redissonClient.getLock(DELETE_ALERT_LOCK); - boolean isLocked = false; - try { - isLocked = lock.tryLock(WAIT_TIME, HOLD_TIME, TimeUnit.SECONDS); - if (isLocked) { - alertRepository.deleteExpiredAlerts(); - return; - } - throw new AlertLockException(); - } catch (InterruptedException e) { - throw new AlertLockException(); - } finally { - if (isLocked) { - lock.unlock(); - } - } + alertRepository.deleteExpiredAlerts(); } } diff --git a/src/main/java/com/atwoz/global/aspect/distributedlock/RedissonDistributedLock.java b/src/main/java/com/atwoz/global/aspect/distributedlock/RedissonDistributedLock.java new file mode 100644 index 00000000..311d5152 --- /dev/null +++ b/src/main/java/com/atwoz/global/aspect/distributedlock/RedissonDistributedLock.java @@ -0,0 +1,17 @@ +package com.atwoz.global.aspect.distributedlock; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface RedissonDistributedLock { + String lockName(); + long waitTime(); + long holdTime(); + TimeUnit timeUnit() default TimeUnit.SECONDS; + Class exceptionClass(); +} diff --git a/src/main/java/com/atwoz/global/aspect/distributedlock/RedissonDistributedLockAspect.java b/src/main/java/com/atwoz/global/aspect/distributedlock/RedissonDistributedLockAspect.java new file mode 100644 index 00000000..7a8d6386 --- /dev/null +++ b/src/main/java/com/atwoz/global/aspect/distributedlock/RedissonDistributedLockAspect.java @@ -0,0 +1,48 @@ +package com.atwoz.global.aspect.distributedlock; + +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Constructor; +import java.util.concurrent.TimeUnit; + +@RequiredArgsConstructor +@Component +@Aspect +public class RedissonDistributedLockAspect { + + private static final String AROUND_VALUE = "@annotation(redissonDistributedLock)"; + private final RedissonClient redissonClient; + + @Around(value = AROUND_VALUE) + public Object handleRedissonDistributedLock(final ProceedingJoinPoint joinPoint, + final RedissonDistributedLock redissonDistributedLock) throws Throwable { + String lockName = redissonDistributedLock.lockName(); + long waitTime = redissonDistributedLock.waitTime(); + long holdTime = redissonDistributedLock.holdTime(); + TimeUnit timeUnit = redissonDistributedLock.timeUnit(); + Class exceptionClass = redissonDistributedLock.exceptionClass(); + Constructor declaredConstructor = exceptionClass.getDeclaredConstructor(); + + RLock lock = redissonClient.getLock(lockName); + boolean isLocked = false; + try { + isLocked = lock.tryLock(waitTime, holdTime, timeUnit); + if (isLocked) { + return joinPoint.proceed(); + } + throw declaredConstructor.newInstance(); + } catch (InterruptedException e) { + throw declaredConstructor.newInstance(); + } finally { + if (isLocked) { + lock.unlock(); + } + } + } +} From cdd5116ddd09cbe6cabd5eb5223bbf886577d028 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Thu, 25 Jul 2024 15:40:52 +0900 Subject: [PATCH 60/62] =?UTF-8?q?refactor:=20=EB=B9=84=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AsyncUncaughtExceptionHandler 등록 --- .../config/{ => async}/AsyncConfig.java | 11 +++++++++-- .../config/async/AsyncExceptionHandler.java | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) rename src/main/java/com/atwoz/global/config/{ => async}/AsyncConfig.java (84%) create mode 100644 src/main/java/com/atwoz/global/config/async/AsyncExceptionHandler.java diff --git a/src/main/java/com/atwoz/global/config/AsyncConfig.java b/src/main/java/com/atwoz/global/config/async/AsyncConfig.java similarity index 84% rename from src/main/java/com/atwoz/global/config/AsyncConfig.java rename to src/main/java/com/atwoz/global/config/async/AsyncConfig.java index 9a77a713..aa036335 100644 --- a/src/main/java/com/atwoz/global/config/AsyncConfig.java +++ b/src/main/java/com/atwoz/global/config/async/AsyncConfig.java @@ -1,7 +1,9 @@ -package com.atwoz.global.config; +package com.atwoz.global.config.async; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.AsyncConfigurer; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @@ -10,7 +12,7 @@ @EnableAsync @Configuration -public class AsyncConfig { +public class AsyncConfig implements AsyncConfigurer { private static final String ASYNC_EXECUTOR = "asyncExecutor"; private static final String ALERT_CALLBACK_EXECUTOR = "alertCallbackExecutor"; @@ -45,4 +47,9 @@ public Executor getAsyncCallbackExecutor() { executor.initialize(); return executor; } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return new AsyncExceptionHandler(); + } } diff --git a/src/main/java/com/atwoz/global/config/async/AsyncExceptionHandler.java b/src/main/java/com/atwoz/global/config/async/AsyncExceptionHandler.java new file mode 100644 index 00000000..bd17345b --- /dev/null +++ b/src/main/java/com/atwoz/global/config/async/AsyncExceptionHandler.java @@ -0,0 +1,19 @@ +package com.atwoz.global.config.async; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; + +@Slf4j +@Component +public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler { + + private static final String ASYNC_MESSAGE_PREFIX = "Async Error Message = "; + + @Override + public void handleUncaughtException(final Throwable throwable, final Method method, final Object... params) { + log.warn(ASYNC_MESSAGE_PREFIX + "{}", throwable.getMessage()); + } +} From 042b521d5fbc1978c8718ad1b73474d0bb37fc26 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Sun, 28 Jul 2024 16:48:14 +0900 Subject: [PATCH 61/62] =?UTF-8?q?refactor:=20Alert=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EA=B3=BC=EC=A0=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DB에 저장된 createdAt 속성 들어가도록 수정 - 메시지 바디 null 문제로 인해 data에 들어가도록 수정 --- src/main/java/com/atwoz/alert/application/AlertService.java | 4 ++-- .../com/atwoz/alert/infrastructure/FirebaseAlertManager.java | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/atwoz/alert/application/AlertService.java b/src/main/java/com/atwoz/alert/application/AlertService.java index 9271538a..accd1af4 100644 --- a/src/main/java/com/atwoz/alert/application/AlertService.java +++ b/src/main/java/com/atwoz/alert/application/AlertService.java @@ -25,8 +25,8 @@ public void saveToken(final Long id, final String token) { public void sendAlert(final AlertGroup group, final String title, final String body, final String sender, final Long receiverId) { String token = tokenRepository.getToken(receiverId); Alert alert = Alert.createWith(group, title, body, receiverId); - alertManager.send(alert, sender, token); - alertRepository.save(alert); + Alert savedAlert = alertRepository.save(alert); + alertManager.send(savedAlert, sender, token); } public Alert readAlert(final Long memberId, final Long id) { diff --git a/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java b/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java index 43a23c34..0dc96792 100644 --- a/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java +++ b/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java @@ -25,6 +25,7 @@ public class FirebaseAlertManager implements AlertManager { private static final String GROUP = "group"; private static final String SENDER = "sender"; private static final String CREATED_TIME = "created_at"; + private static final String BODY = "body"; private static final String TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; private static final String THREAD_PROBLEM = "FCM 스레드에서 문제가 발생했습니다."; private static final String ALERT_FAIL = "알림 전송 실패"; @@ -55,7 +56,6 @@ private Message createAlertMessage(final Alert alert, final String sender, final Notification firebaseNotification = Notification.builder() .setTitle(alert.getTitle()) - .setBody(alert.getBody()) .build(); return Message.builder() @@ -64,6 +64,7 @@ private Message createAlertMessage(final Alert alert, final String sender, final .setNotification(firebaseNotification) .putData(SENDER, sender) .putData(CREATED_TIME, time) + .putData(BODY, alert.getBody()) .build(); } From 4f7a9ac63ec338582f907c009b177f75476b1dc7 Mon Sep 17 00:00:00 2001 From: devholic22 Date: Mon, 29 Jul 2024 16:13:07 +0900 Subject: [PATCH 62/62] =?UTF-8?q?refactor:=20instanceof=20=ED=91=9C?= =?UTF-8?q?=ED=98=84=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - instanceof 대신 ClassCastException 탐지되도록 수정 --- .../infrastructure/FirebaseAlertManager.java | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java b/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java index 0dc96792..c33f14f2 100644 --- a/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java +++ b/src/main/java/com/atwoz/alert/infrastructure/FirebaseAlertManager.java @@ -80,12 +80,16 @@ private void logAlertResult(final ApiFuture process, final Message messa } private void retryAlert(final Message message, final ExecutionException exception) { - if (exception.getCause() instanceof FirebaseMessagingException e) { - MessagingErrorCode errorCode = e.getMessagingErrorCode(); + try { + Throwable cause = exception.getCause(); + FirebaseMessagingException firebaseException = (FirebaseMessagingException) cause; + MessagingErrorCode errorCode = firebaseException.getMessagingErrorCode(); if (!isRetryErrorCode(errorCode)) { return; } retryInThreeTimes(message); + } catch (ClassCastException e) { + return; } } @@ -113,16 +117,22 @@ private boolean shouldRetry(final Message message, final int retryCount) { try { firebase.sendAsync(message).get(); } catch (Exception exception) { - if (exception.getCause() instanceof FirebaseMessagingException e) { - MessagingErrorCode errorCode = e.getMessagingErrorCode(); - if (isRetryErrorCode(errorCode)) { - return true; - } - } + return shouldRetryFirebaseException(exception); } return false; } + private boolean shouldRetryFirebaseException(final Exception exception) { + try { + Throwable cause = exception.getCause(); + FirebaseMessagingException firebaseException = (FirebaseMessagingException) cause; + MessagingErrorCode errorCode = firebaseException.getMessagingErrorCode(); + return isRetryErrorCode(errorCode); + } catch (ClassCastException e) { + return false; + } + } + private void wait(final int retryCount) { try { Thread.sleep(RETRY_TIMES[retryCount]);