diff --git a/noti-service/build.gradle b/noti-service/build.gradle index ae41b641..ef63bff8 100644 --- a/noti-service/build.gradle +++ b/noti-service/build.gradle @@ -60,6 +60,9 @@ dependencies { //JUnit + AssertJ testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2' testImplementation 'org.assertj:assertj-core:3.23.1' + + //firebase + implementation 'com.google.firebase:firebase-admin:9.2.0' } //openApi { diff --git a/noti-service/src/main/java/com/waither/notiservice/api/NotificationController.java b/noti-service/src/main/java/com/waither/notiservice/api/NotificationController.java index 80a13357..cb343d52 100644 --- a/noti-service/src/main/java/com/waither/notiservice/api/NotificationController.java +++ b/noti-service/src/main/java/com/waither/notiservice/api/NotificationController.java @@ -1,9 +1,9 @@ package com.waither.notiservice.api; import com.waither.notiservice.api.request.LocationDto; +import com.waither.notiservice.global.annotation.AuthUser; import com.waither.notiservice.global.response.ApiResponse; import com.waither.notiservice.service.NotificationService; -import com.waither.notiservice.utils.RedisUtils; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -17,31 +17,32 @@ public class NotificationController { private final NotificationService notificationService; - private final RedisUtils redisUtils; @Operation(summary = "Get notification", description = "알림 목록 조회하기") @GetMapping("") - public ApiResponse getNotifications(Long userId) { - return ApiResponse.onSuccess(notificationService.getNotifications(userId)); + public ApiResponse getNotifications(@AuthUser String email) { + return ApiResponse.onSuccess(notificationService.getNotifications(email)); } @Operation(summary = "Delete notification", description = "알림 삭제하기") @DeleteMapping("") - public ApiResponse deleteNotification(@RequestParam("id") String notificationId) { - notificationService.deleteNotification(notificationId); + public ApiResponse deleteNotification(@AuthUser String email, @RequestParam("id") String notificationId) { + notificationService.deleteNotification(email, notificationId); return ApiResponse.onSuccess(HttpStatus.OK); } @Operation(summary = "Send Go Out Alarm", description = "외출 알림 전송하기") @PostMapping("/goOut") - public void sendGoOutAlarm(Long userId) { - notificationService.sendGoOutAlarm(userId); + public ApiResponse sendGoOutAlarm(@AuthUser String email) { + notificationService.sendGoOutAlarm(email); + return ApiResponse.onSuccess(HttpStatus.OK); } @Operation(summary = "Current Location", description = "현재 위치 전송") @PostMapping("/location") - public void checkCurrentAlarm(@RequestBody @Valid LocationDto locationDto) { - notificationService.checkCurrentAlarm(locationDto); + public ApiResponse updateLocation(@AuthUser String email, @RequestBody @Valid LocationDto locationDto) { + notificationService.updateLocation(email, locationDto); + return ApiResponse.onSuccess(HttpStatus.OK); } } diff --git a/noti-service/src/main/java/com/waither/notiservice/api/TokenContoller.java b/noti-service/src/main/java/com/waither/notiservice/api/TokenContoller.java new file mode 100644 index 00000000..15d9dfab --- /dev/null +++ b/noti-service/src/main/java/com/waither/notiservice/api/TokenContoller.java @@ -0,0 +1,28 @@ +package com.waither.notiservice.api; + +import com.waither.notiservice.api.request.TokenDto; +import com.waither.notiservice.global.annotation.AuthUser; +import com.waither.notiservice.global.response.ApiResponse; +import com.waither.notiservice.service.AlarmService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RequestMapping("/noti") +@RestController +public class TokenContoller { + + private final AlarmService alarmService; + + @Operation(summary = "Firebase Token 업데이트", description = "Request Body에 발급한 FCM토큰 값을 넣어서 주시면 됩니다.") + @PostMapping("/token") + public ApiResponse updateToken(@AuthUser String email, @RequestBody TokenDto tokenDto) { + alarmService.updateToken(email, tokenDto); + return ApiResponse.onSuccess("토큰 업로드가 완료되었습니다."); + } +} diff --git a/noti-service/src/main/java/com/waither/notiservice/api/request/LocationDto.java b/noti-service/src/main/java/com/waither/notiservice/api/request/LocationDto.java index 4c1deb25..16d8f080 100644 --- a/noti-service/src/main/java/com/waither/notiservice/api/request/LocationDto.java +++ b/noti-service/src/main/java/com/waither/notiservice/api/request/LocationDto.java @@ -1,27 +1,23 @@ package com.waither.notiservice.api.request; -import jakarta.validation.Valid; import jakarta.validation.constraints.DecimalMax; import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -@Getter -@AllArgsConstructor -@NoArgsConstructor -public class LocationDto { +public record LocationDto ( + @NotBlank(message = " 위도(lat) 값은 필수입니다.") + @DecimalMax(value = "132.0", inclusive = true, message = "위도(lat)는 대한민국 내에서만 가능합니다.") + @DecimalMin(value = "124.0", inclusive = true, message = "위도(lat)는 대한민국 내에서만 가능합니다.") + double lat, + + @NotBlank(message = " 경도(y) 값은 필수입니다.") + @DecimalMax(value = "43.0", inclusive = true, message = "경도(lon)는 대한민국 내에서만 가능합니다.") + @DecimalMin(value = "33.0", inclusive = true, message = "경도(lon)는 대한민국 내에서만 가능합니다.") + double lon +) { - @NotBlank(message = " 위도(x) 값은 필수입니다.") - @DecimalMax(value = "132.0", inclusive = true, message = "위도(x)는 대한민국 내에서만 가능합니다.") - @DecimalMin(value = "124.0", inclusive = true, message = "위도(x)는 대한민국 내에서만 가능합니다.") - public double x; - @NotBlank(message = " 경도(y) 값은 필수입니다.") - @DecimalMax(value = "43.0", inclusive = true, message = "경도(y)는 대한민국 내에서만 가능합니다.") - @DecimalMin(value = "33.0", inclusive = true, message = "경도(y)는 대한민국 내에서만 가능합니다.") - public double y; } diff --git a/noti-service/src/main/java/com/waither/notiservice/api/request/TokenDto.java b/noti-service/src/main/java/com/waither/notiservice/api/request/TokenDto.java new file mode 100644 index 00000000..5d5e098f --- /dev/null +++ b/noti-service/src/main/java/com/waither/notiservice/api/request/TokenDto.java @@ -0,0 +1,7 @@ +package com.waither.notiservice.api.request; + +public record TokenDto( + String token + +) { +} diff --git a/noti-service/src/main/java/com/waither/notiservice/config/FirebaseConfig.java b/noti-service/src/main/java/com/waither/notiservice/config/FirebaseConfig.java new file mode 100644 index 00000000..3bf9e337 --- /dev/null +++ b/noti-service/src/main/java/com/waither/notiservice/config/FirebaseConfig.java @@ -0,0 +1,32 @@ +package com.waither.notiservice.config; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +import java.io.FileInputStream; +import java.io.IOException; + +@Configuration +@RequiredArgsConstructor +public class FirebaseConfig { + + @Value("${firebase.key.path}") + private final String keyPath; + + @PostConstruct + public void initializeApp() throws IOException { + FileInputStream serviceAccount = + new FileInputStream(keyPath); + + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(serviceAccount)) + .build(); + + FirebaseApp.initializeApp(options); + } +} diff --git a/noti-service/src/main/java/com/waither/notiservice/config/KafkaConsumerConfig.java b/noti-service/src/main/java/com/waither/notiservice/config/KafkaConsumerConfig.java index 0accca1e..35a685c1 100644 --- a/noti-service/src/main/java/com/waither/notiservice/config/KafkaConsumerConfig.java +++ b/noti-service/src/main/java/com/waither/notiservice/config/KafkaConsumerConfig.java @@ -1,8 +1,6 @@ package com.waither.notiservice.config; -import com.waither.notiservice.dto.kafka.TokenDto; -import com.waither.notiservice.dto.kafka.UserMedianDto; -import com.waither.notiservice.dto.kafka.UserSettingsDto; +import com.waither.notiservice.dto.kafka.KafkaDto; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.common.serialization.StringDeserializer; import org.springframework.beans.factory.annotation.Value; @@ -81,8 +79,8 @@ public ConsumerFactory stringConsumerFactory() { @Bean("userMedianKafkaListenerContainerFactory") - public ConcurrentKafkaListenerContainerFactory userMedianDtoConcurrentKafkaListenerContainerFactory(){ - ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + public ConcurrentKafkaListenerContainerFactory userMedianDtoConcurrentKafkaListenerContainerFactory(){ + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); factory.setConsumerFactory(userMedianConsumerFactory()); factory.setConcurrency(3); factory.setBatchListener(true); @@ -90,16 +88,16 @@ public ConcurrentKafkaListenerContainerFactory userMedian return factory; } - private ConsumerFactory userMedianConsumerFactory() { + private ConsumerFactory userMedianConsumerFactory() { Map props = dtoSettings(); - return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(), new JsonDeserializer<>(UserMedianDto.class)); + return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(), new JsonDeserializer<>(KafkaDto.UserMedianDto.class)); } @Bean("firebaseTokenKafkaListenerContainerFactory") - public ConcurrentKafkaListenerContainerFactory firebaseTokenConcurrentKafkaListenerContainerFactory(){ - ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + public ConcurrentKafkaListenerContainerFactory firebaseTokenConcurrentKafkaListenerContainerFactory(){ + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); factory.setConsumerFactory(firebaseTokenConsumerFactory()); factory.setConcurrency(3); factory.setBatchListener(true); @@ -107,16 +105,16 @@ public ConcurrentKafkaListenerContainerFactory firebaseTokenCo return factory; } - private ConsumerFactory firebaseTokenConsumerFactory() { + private ConsumerFactory firebaseTokenConsumerFactory() { Map props = dtoSettings(); - return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(), new JsonDeserializer<>(TokenDto.class)); + return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(), new JsonDeserializer<>(KafkaDto.TokenDto.class)); } @Bean("userSettingsKafkaListenerContainerFactory") - public ConcurrentKafkaListenerContainerFactory userSettingsConcurrentKafkaListenerContainerFactory(){ - ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + public ConcurrentKafkaListenerContainerFactory userSettingsConcurrentKafkaListenerContainerFactory(){ + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); factory.setConsumerFactory(userSettingsConsumerFactory()); factory.setConcurrency(3); factory.setBatchListener(true); @@ -124,9 +122,40 @@ public ConcurrentKafkaListenerContainerFactory userSett return factory; } - private ConsumerFactory userSettingsConsumerFactory() { + private ConsumerFactory userSettingsConsumerFactory() { Map props = dtoSettings(); - return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(), new JsonDeserializer<>(UserSettingsDto.class)); + return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(), new JsonDeserializer<>(KafkaDto.UserSettingsDto.class)); + } + + + @Bean("initialDataKafkaListenerContainerFactory") + public ConcurrentKafkaListenerContainerFactory initialDataConcurrentKafkaListenerContainerFactory(){ + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(initialDataConsumerFactory()); + factory.setConcurrency(3); + factory.setBatchListener(true); + factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.BATCH); + return factory; + } + + private ConsumerFactory initialDataConsumerFactory() { + Map props = dtoSettings(); + return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(), new JsonDeserializer<>(KafkaDto.InitialDataDto.class)); + } + + @Bean("weatherKafkaListenerContainerFactory") + public ConcurrentKafkaListenerContainerFactory weatherConcurrentKafkaListenerContainerFactory(){ + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(weatherConsumerFactory()); + factory.setConcurrency(3); + factory.setBatchListener(true); + factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.BATCH); + return factory; + } + + private ConsumerFactory weatherConsumerFactory() { + Map props = dtoSettings(); + return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(), new JsonDeserializer<>(KafkaDto.WeatherDto.class)); } diff --git a/noti-service/src/main/java/com/waither/notiservice/config/KafkaTopicConfig.java b/noti-service/src/main/java/com/waither/notiservice/config/KafkaTopicConfig.java index 2b803cec..d2b40ad5 100644 --- a/noti-service/src/main/java/com/waither/notiservice/config/KafkaTopicConfig.java +++ b/noti-service/src/main/java/com/waither/notiservice/config/KafkaTopicConfig.java @@ -29,7 +29,7 @@ public KafkaAdmin kafkaAdmin() { *

userMedian 동기화 토픽

* @Producer : user-service * @Consumer : noti-service - * @MessageObject : {@link com.waither.notiservice.dto.kafka.UserMedianDto} + * @MessageObject : {@link com.waither.notiservice.dto.kafka.KafkaDto.UserMedianDto} * @Description : noti-service의 userMedian 테이블의 데이터를 동기화 하기 위해 사용합니다. * 계절은 자동으로 계산합니다. *
@@ -48,7 +48,7 @@ public NewTopic userMedianTopic(){ *

Firebase Token 동기화 토픽

* @Producer : user-service * @Consumer : noti-service - * @MessageObject : {@link com.waither.notiservice.dto.kafka.TokenDto} + * @MessageObject : {@link com.waither.notiservice.dto.kafka.KafkaDto.TokenDto} * @Description : noti-service의 firebase 토큰을 저장을 위해 사용됩니다. * */ @@ -64,7 +64,7 @@ public NewTopic fireBaseTokenTopic(){ *

User Settings 동기화 토픽

* @Producer : user-service * @Consumer : noti-service - * @MessageObject : {@link com.waither.notiservice.dto.kafka.UserSettingsDto} + * @MessageObject : {@link com.waither.notiservice.dto.kafka.KafkaDto.UserSettingsDto} * @Description : noti-service의 User Data 데이터 동기화를 위해 사용됩니다. * */ diff --git a/noti-service/src/main/java/com/waither/notiservice/domain/FireBaseToken.java b/noti-service/src/main/java/com/waither/notiservice/domain/FireBaseToken.java index 29f459da..22f77668 100644 --- a/noti-service/src/main/java/com/waither/notiservice/domain/FireBaseToken.java +++ b/noti-service/src/main/java/com/waither/notiservice/domain/FireBaseToken.java @@ -11,7 +11,7 @@ public class FireBaseToken { @Id - private Long userId; + private String email; private String token; } diff --git a/noti-service/src/main/java/com/waither/notiservice/domain/Notification.java b/noti-service/src/main/java/com/waither/notiservice/domain/Notification.java index fb17100b..810b8adf 100644 --- a/noti-service/src/main/java/com/waither/notiservice/domain/Notification.java +++ b/noti-service/src/main/java/com/waither/notiservice/domain/Notification.java @@ -20,6 +20,6 @@ public class Notification extends BaseEntity { private String content; - private Long userId; + private String email; } diff --git a/noti-service/src/main/java/com/waither/notiservice/domain/UserData.java b/noti-service/src/main/java/com/waither/notiservice/domain/UserData.java index 246c2df8..6debf4d8 100644 --- a/noti-service/src/main/java/com/waither/notiservice/domain/UserData.java +++ b/noti-service/src/main/java/com/waither/notiservice/domain/UserData.java @@ -1,9 +1,7 @@ package com.waither.notiservice.domain; -import com.waither.notiservice.domain.type.Season; import jakarta.persistence.*; import lombok.*; -import org.hibernate.annotations.ColumnDefault; import org.hibernate.annotations.DynamicInsert; @Builder @@ -16,7 +14,7 @@ public class UserData { @Id - private Long userId; + private String email; private String nickName; @@ -38,6 +36,9 @@ public class UserData { // 직장 지역 레포트 알림 받기 private boolean regionReport; + //가중치 + private Double weight; + public void updateValue(String key, String value) { switch (key) { case "nickName" -> nickName = value; @@ -47,6 +48,7 @@ public void updateValue(String key, String value) { case "windAlert" -> windAlert = Boolean.parseBoolean(value); case "regionReport" -> regionReport = Boolean.parseBoolean(value); case "windDegree" -> windDegree = Integer.valueOf(value); + case "weight" -> weight = Double.valueOf(value); } } diff --git a/noti-service/src/main/java/com/waither/notiservice/domain/UserMedian.java b/noti-service/src/main/java/com/waither/notiservice/domain/UserMedian.java index cdb757b7..0b40d182 100644 --- a/noti-service/src/main/java/com/waither/notiservice/domain/UserMedian.java +++ b/noti-service/src/main/java/com/waither/notiservice/domain/UserMedian.java @@ -1,6 +1,7 @@ package com.waither.notiservice.domain; -import com.waither.notiservice.domain.type.Season; +import com.waither.notiservice.enums.Season; +import com.waither.notiservice.dto.kafka.KafkaDto; import jakarta.persistence.*; import lombok.*; import org.hibernate.annotations.DynamicInsert; @@ -15,7 +16,7 @@ public class UserMedian { @Id - private Long userId; + private String email; private Double medianOf1And2; private Double medianOf2And3; private Double medianOf3And4; @@ -24,7 +25,7 @@ public class UserMedian { @Enumerated(value = EnumType.STRING) public Season season; - public void setLevel(int level, double value) { + public void setLevel(int level, Double value) { switch (level) { case 1 -> medianOf1And2 = value; case 2 -> medianOf2And3 = value; @@ -32,4 +33,13 @@ public void setLevel(int level, double value) { case 4 -> medianOf4And5 = value; } } + + public void setLevel(KafkaDto.UserMedianDto userMedianDto) { + KafkaDto.SeasonData seasonData = userMedianDto.seasonData(); + medianOf1And2 = seasonData.medianOf1And2(); + medianOf2And3 = seasonData.medianOf2And3(); + medianOf3And4 = seasonData.medianOf3And4(); + medianOf4And5 = seasonData.medianOf4And5(); + + } } diff --git a/noti-service/src/main/java/com/waither/notiservice/domain/redis/NotificationRecord.java b/noti-service/src/main/java/com/waither/notiservice/domain/redis/NotificationRecord.java new file mode 100644 index 00000000..18d8b8d3 --- /dev/null +++ b/noti-service/src/main/java/com/waither/notiservice/domain/redis/NotificationRecord.java @@ -0,0 +1,50 @@ +package com.waither.notiservice.domain.redis; + +import lombok.*; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +import java.time.LocalDateTime; +import java.util.Date; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@RedisHash +public class NotificationRecord { + + //유저 식별자 + @Id + private String email; + + //마지막 강수 알림 받은 시간 + private LocalDateTime lastRainAlarmReceived; + + //마지막 바람세기 알림 받은 시간 + private LocalDateTime lastWindAlarmReceived; + + //사용자 마지막 위치 (지역) + private String region; + + public void initializeWindTime() { + lastWindAlarmReceived = LocalDateTime.now(); + } + + public void initializeRainTime() { + lastRainAlarmReceived = LocalDateTime.now(); + } + + public void setLastRainAlarmReceived(LocalDateTime lastRainAlarmReceived) { + this.lastRainAlarmReceived = lastRainAlarmReceived; + } + + public void setLastWindAlarmReceived(LocalDateTime lastWindAlarmReceived) { + this.lastWindAlarmReceived = lastWindAlarmReceived; + } + + public void setRegion(String region) { + this.region = region; + } + +} diff --git a/noti-service/src/main/java/com/waither/notiservice/domain/type/Season.java b/noti-service/src/main/java/com/waither/notiservice/domain/type/Season.java deleted file mode 100644 index c1c6a2b3..00000000 --- a/noti-service/src/main/java/com/waither/notiservice/domain/type/Season.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.waither.notiservice.domain.type; - -public enum Season { - SPRING, - SUMMER, - AUTUMN, - WINTER -} \ No newline at end of file diff --git a/noti-service/src/main/java/com/waither/notiservice/dto/kafka/KafkaDto.java b/noti-service/src/main/java/com/waither/notiservice/dto/kafka/KafkaDto.java new file mode 100644 index 00000000..e752b2a9 --- /dev/null +++ b/noti-service/src/main/java/com/waither/notiservice/dto/kafka/KafkaDto.java @@ -0,0 +1,85 @@ +package com.waither.notiservice.dto.kafka; + +import com.waither.notiservice.domain.UserData; +import com.waither.notiservice.domain.UserMedian; +import com.waither.notiservice.enums.Season; +import lombok.Builder; + +import java.util.List; +import java.util.Map; + +public class KafkaDto { + + @Builder + public record SeasonData( + Double medianOf1And2, + Double medianOf2And3, + Double medianOf3And4, + Double medianOf4And5 + ) {} + + @Builder + public record InitialDataDto( + String email, + String nickName, + boolean climateAlert, + boolean userAlert, + boolean snowAlert, + boolean windAlert, + int windDegree, + boolean regionReport, + double weight, + Map seasonData + ) { + public UserData toUserDataEntity() { + return UserData.builder() + .email(email) + .climateAlert(climateAlert) + .userAlert(userAlert) + .snowAlert(snowAlert) + .windAlert(windAlert) + .windDegree(windDegree) + .regionReport(regionReport) + .build(); + } + + public List toUserMedianList() { + return seasonData.entrySet().stream() + .map(entry -> UserMedian.builder() + .email(email) + .season(Season.valueOf(entry.getKey())) + .medianOf1And2(entry.getValue().medianOf1And2()) + .medianOf2And3(entry.getValue().medianOf2And3()) + .medianOf3And4(entry.getValue().medianOf3And4()) + .medianOf4And5(entry.getValue().medianOf4And5()) + .build()) + .toList(); + } + } + + @Builder + public record UserMedianDto( + String email, + SeasonData seasonData + ) {} + + @Builder + public record UserSettingsDto( + String email, + String key, + String value + ) {} + + @Builder + public record TokenDto( + String email, + String token + ){} + + @Builder + public record WeatherDto( + String region, + String message + ) {} + +} diff --git a/noti-service/src/main/java/com/waither/notiservice/dto/kafka/TokenDto.java b/noti-service/src/main/java/com/waither/notiservice/dto/kafka/TokenDto.java deleted file mode 100644 index 4fdc256e..00000000 --- a/noti-service/src/main/java/com/waither/notiservice/dto/kafka/TokenDto.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.waither.notiservice.dto.kafka; - -import com.waither.notiservice.domain.FireBaseToken; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class TokenDto { - - public Long userId; - public String token; - - public FireBaseToken toEntity() { - return FireBaseToken.builder() - .userId(userId) - .token(token) - .build(); - } -} diff --git a/noti-service/src/main/java/com/waither/notiservice/dto/kafka/UserMedianDto.java b/noti-service/src/main/java/com/waither/notiservice/dto/kafka/UserMedianDto.java deleted file mode 100644 index f3cfd341..00000000 --- a/noti-service/src/main/java/com/waither/notiservice/dto/kafka/UserMedianDto.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.waither.notiservice.dto.kafka; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@AllArgsConstructor -@NoArgsConstructor -@Builder -@Getter -public class UserMedianDto { - - public Long userId; - - public int level; - - public double temperature; - -} diff --git a/noti-service/src/main/java/com/waither/notiservice/dto/kafka/UserSettingsDto.java b/noti-service/src/main/java/com/waither/notiservice/dto/kafka/UserSettingsDto.java deleted file mode 100644 index 4b32fa4f..00000000 --- a/noti-service/src/main/java/com/waither/notiservice/dto/kafka/UserSettingsDto.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.waither.notiservice.dto.kafka; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class UserSettingsDto { - - public Long userId; - - public String key; - - public String value; -} diff --git a/noti-service/src/main/java/com/waither/notiservice/enums/Expressions.java b/noti-service/src/main/java/com/waither/notiservice/enums/Expressions.java index 2708e6aa..c49678be 100644 --- a/noti-service/src/main/java/com/waither/notiservice/enums/Expressions.java +++ b/noti-service/src/main/java/com/waither/notiservice/enums/Expressions.java @@ -1,6 +1,5 @@ package com.waither.notiservice.enums; -import com.waither.notiservice.domain.type.Season; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/noti-service/src/main/java/com/waither/notiservice/enums/Season.java b/noti-service/src/main/java/com/waither/notiservice/enums/Season.java new file mode 100644 index 00000000..775e2033 --- /dev/null +++ b/noti-service/src/main/java/com/waither/notiservice/enums/Season.java @@ -0,0 +1,9 @@ +package com.waither.notiservice.enums; + +public enum Season { + SPRING, + SUMMER, + AUTUMN, + WINTER, + SPRING_AUTUMN +} \ No newline at end of file diff --git a/noti-service/src/main/java/com/waither/notiservice/global/annotation/AuthUser.java b/noti-service/src/main/java/com/waither/notiservice/global/annotation/AuthUser.java new file mode 100644 index 00000000..c6ff4143 --- /dev/null +++ b/noti-service/src/main/java/com/waither/notiservice/global/annotation/AuthUser.java @@ -0,0 +1,14 @@ +package com.waither.notiservice.global.annotation; + +import io.swagger.v3.oas.annotations.Parameter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Parameter(hidden = true) //Swagger에서 파라미터 숨기기 +public @interface AuthUser { +} diff --git a/noti-service/src/main/java/com/waither/notiservice/global/annotation/AuthUserArgumentResolver.java b/noti-service/src/main/java/com/waither/notiservice/global/annotation/AuthUserArgumentResolver.java new file mode 100644 index 00000000..e49fa5e7 --- /dev/null +++ b/noti-service/src/main/java/com/waither/notiservice/global/annotation/AuthUserArgumentResolver.java @@ -0,0 +1,33 @@ +package com.waither.notiservice.global.annotation; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +@RequiredArgsConstructor +@Transactional +public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + boolean hasParameterAnnotation = parameter.hasParameterAnnotation(AuthUser.class); + boolean isUserParameterType = parameter.getParameterType().isAssignableFrom(String.class); + return hasParameterAnnotation && isUserParameterType; + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + HttpServletRequest httpServletRequest = (HttpServletRequest) webRequest.getNativeRequest(); + + return httpServletRequest.getHeader("email"); + } +} diff --git a/noti-service/src/main/java/com/waither/notiservice/global/response/ErrorCode.java b/noti-service/src/main/java/com/waither/notiservice/global/response/ErrorCode.java index 4b5b3c4e..c0805b83 100644 --- a/noti-service/src/main/java/com/waither/notiservice/global/response/ErrorCode.java +++ b/noti-service/src/main/java/com/waither/notiservice/global/response/ErrorCode.java @@ -34,11 +34,13 @@ public enum ErrorCode implements BaseErrorCode { // 데이터 관련 에러 NO_USER_MEDIAN_REGISTERED(HttpStatus.NOT_FOUND, "USER404_0", "사용자 설정값이 존재하지 않습니다."), NO_USER_DATA_REGISTERED(HttpStatus.NOT_FOUND, "USER404_1", "사용자 데이터 값이 존재하지 않습니다."), - FIREBASE_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "TOKEN404", "푸시알림 토큰이 존재하지 않습니다."), //통신 과정 에러 - COMMUNICATION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500_1", "통신 과정에서 문제가 발생했습니다.") + COMMUNICATION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500_1", "통신 과정에서 문제가 발생했습니다."), + //FirebaseError + FIREBASE_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "FB404", "푸시알림 토큰이 존재하지 않습니다."), + FIREBASE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "FB500", "Firebase 메세지 전송 오류가 발생했습니다.") ; diff --git a/noti-service/src/main/java/com/waither/notiservice/repository/UserDataRepository.java b/noti-service/src/main/java/com/waither/notiservice/repository/UserDataRepository.java deleted file mode 100644 index 5b021e68..00000000 --- a/noti-service/src/main/java/com/waither/notiservice/repository/UserDataRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.waither.notiservice.repository; - -import com.waither.notiservice.domain.UserData; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface UserDataRepository extends JpaRepository { -} diff --git a/noti-service/src/main/java/com/waither/notiservice/repository/NotificationRepository.java b/noti-service/src/main/java/com/waither/notiservice/repository/jpa/NotificationRepository.java similarity index 69% rename from noti-service/src/main/java/com/waither/notiservice/repository/NotificationRepository.java rename to noti-service/src/main/java/com/waither/notiservice/repository/jpa/NotificationRepository.java index 231055ed..add74278 100644 --- a/noti-service/src/main/java/com/waither/notiservice/repository/NotificationRepository.java +++ b/noti-service/src/main/java/com/waither/notiservice/repository/jpa/NotificationRepository.java @@ -1,13 +1,14 @@ -package com.waither.notiservice.repository; +package com.waither.notiservice.repository.jpa; import com.waither.notiservice.domain.Notification; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; @Repository public interface NotificationRepository extends JpaRepository { - List findAllByUserId(Long userId); + List findAllByEmail(String email); } diff --git a/noti-service/src/main/java/com/waither/notiservice/repository/jpa/UserDataRepository.java b/noti-service/src/main/java/com/waither/notiservice/repository/jpa/UserDataRepository.java new file mode 100644 index 00000000..f555c14a --- /dev/null +++ b/noti-service/src/main/java/com/waither/notiservice/repository/jpa/UserDataRepository.java @@ -0,0 +1,20 @@ +package com.waither.notiservice.repository.jpa; + +import com.waither.notiservice.domain.UserData; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface UserDataRepository extends JpaRepository { + + Optional findByEmail(String email); + + List findAllByClimateAlertIsTrue(); + + List findAllByWindAlertIsTrue(); + + List findAllBySnowAlertIsTrue(); +} diff --git a/noti-service/src/main/java/com/waither/notiservice/repository/UserMedianRepository.java b/noti-service/src/main/java/com/waither/notiservice/repository/jpa/UserMedianRepository.java similarity index 52% rename from noti-service/src/main/java/com/waither/notiservice/repository/UserMedianRepository.java rename to noti-service/src/main/java/com/waither/notiservice/repository/jpa/UserMedianRepository.java index 469ea553..b39cfb5a 100644 --- a/noti-service/src/main/java/com/waither/notiservice/repository/UserMedianRepository.java +++ b/noti-service/src/main/java/com/waither/notiservice/repository/jpa/UserMedianRepository.java @@ -1,9 +1,14 @@ -package com.waither.notiservice.repository; +package com.waither.notiservice.repository.jpa; import com.waither.notiservice.domain.UserMedian; +import com.waither.notiservice.enums.Season; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository -public interface UserMedianRepository extends JpaRepository { +public interface UserMedianRepository extends JpaRepository { + + Optional findByEmailAndSeason(String email, Season season); } diff --git a/noti-service/src/main/java/com/waither/notiservice/repository/redis/NotificationRecordRepository.java b/noti-service/src/main/java/com/waither/notiservice/repository/redis/NotificationRecordRepository.java new file mode 100644 index 00000000..da8b52d2 --- /dev/null +++ b/noti-service/src/main/java/com/waither/notiservice/repository/redis/NotificationRecordRepository.java @@ -0,0 +1,16 @@ +package com.waither.notiservice.repository.redis; + +import com.waither.notiservice.domain.redis.NotificationRecord; +import org.springframework.data.repository.CrudRepository; + +import javax.swing.text.html.Option; +import java.util.List; +import java.util.Optional; + +public interface NotificationRecordRepository extends CrudRepository { + + Optional findByEmail(String email); + + + +} diff --git a/noti-service/src/main/java/com/waither/notiservice/service/AlarmService.java b/noti-service/src/main/java/com/waither/notiservice/service/AlarmService.java new file mode 100644 index 00000000..8797476a --- /dev/null +++ b/noti-service/src/main/java/com/waither/notiservice/service/AlarmService.java @@ -0,0 +1,59 @@ +package com.waither.notiservice.service; + +import com.waither.notiservice.api.request.TokenDto; +import com.waither.notiservice.domain.Notification; +import com.waither.notiservice.global.exception.CustomException; +import com.waither.notiservice.global.response.ErrorCode; +import com.waither.notiservice.repository.jpa.NotificationRepository; +import com.waither.notiservice.utils.FireBaseUtils; +import com.waither.notiservice.utils.RedisUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@RequiredArgsConstructor +@Service +public class AlarmService { + + private final RedisUtils redisUtils; + private final FireBaseUtils fireBaseUtils; + private final NotificationRepository notificationRepository; + + public void updateToken(String email, TokenDto tokenDto) { + redisUtils.save(email, tokenDto.token()); + } + + public void sendSingleAlarm(String email, String title, String message) { + String token = String.valueOf(redisUtils.get(email)); + fireBaseUtils.sendSingleMessage(token, title, message); + notificationRepository.save(Notification.builder() + .email(email) + .title(title) + .content(message) + .build()); + } + + public void sendAlarms(List userEmails, String title, String message) { + List tokens = userEmails.stream() + .map(email -> String.valueOf(redisUtils.get(email))) + .toList(); + + log.info("[ 푸시알림 ] Email ---> {}", userEmails); + log.info("[ 푸시알림 ] message ---> {}", message); + + fireBaseUtils.sendAllMessages(tokens,title, message); + + List notifications = userEmails.stream() + .map(email -> Notification.builder() + .email(email) + .title(title) + .content(message) + .build()) + .toList(); + + notificationRepository.saveAll(notifications); + } +} diff --git a/noti-service/src/main/java/com/waither/notiservice/service/KafkaConsumer.java b/noti-service/src/main/java/com/waither/notiservice/service/KafkaConsumer.java index 2f6fb3ac..a51b8ae5 100644 --- a/noti-service/src/main/java/com/waither/notiservice/service/KafkaConsumer.java +++ b/noti-service/src/main/java/com/waither/notiservice/service/KafkaConsumer.java @@ -2,187 +2,304 @@ import com.waither.notiservice.domain.UserData; import com.waither.notiservice.domain.UserMedian; -import com.waither.notiservice.domain.type.Season; -import com.waither.notiservice.dto.kafka.TokenDto; -import com.waither.notiservice.dto.kafka.UserMedianDto; -import com.waither.notiservice.dto.kafka.UserSettingsDto; -import com.waither.notiservice.global.exception.CustomException; -import com.waither.notiservice.global.response.ErrorCode; -import com.waither.notiservice.repository.UserDataRepository; -import com.waither.notiservice.repository.UserMedianRepository; +import com.waither.notiservice.domain.redis.NotificationRecord; +import com.waither.notiservice.enums.Season; +import com.waither.notiservice.dto.kafka.KafkaDto; +import com.waither.notiservice.repository.jpa.UserDataRepository; +import com.waither.notiservice.repository.jpa.UserMedianRepository; +import com.waither.notiservice.repository.redis.NotificationRecordRepository; import com.waither.notiservice.utils.RedisUtils; import com.waither.notiservice.utils.TemperatureUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.kafka.annotation.KafkaListener; -import org.springframework.messaging.handler.annotation.Payload; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @Slf4j @RequiredArgsConstructor @Component -@Transactional public class KafkaConsumer { + private final AlarmService alarmService; private final UserDataRepository userDataRepository; private final UserMedianRepository userMedianRepository; - + private final NotificationRecordRepository notificationRecordRepository; private final RedisUtils redisUtils; /** * 중앙값 동기화 Listener * */ - @KafkaListener(topics = "user-median", containerFactory = "userMedianKafkaListenerContainerFactory") - public void consumeUserMedian(UserMedianDto userMedianDto) { + @Transactional + @KafkaListener(topics = "${spring.kafka.template.user-median-topic}", containerFactory = "userMedianKafkaListenerContainerFactory") + public void consumeUserMedian(KafkaDto.UserMedianDto userMedianDto) { - Season season = TemperatureUtils.getCurrentSeason(); + Season currentSeason = TemperatureUtils.getCurrentSeason(); log.info("[ Kafka Listener ] 사용자 중앙값 데이터 동기화"); - log.info("[ Kafka Listener ] Season : -- {} ", season.name()); - log.info("[ Kafka Listener ] User Id : --> {}", userMedianDto.getUserId()); - log.info("[ Kafka Listener ] Level : --> {}", userMedianDto.getLevel()); - log.info("[ Kafka Listener ] Temperature : --> {}", userMedianDto.getTemperature()); + log.info("[ Kafka Listener ] Season : -- {} ", currentSeason.name()); + log.info("[ Kafka Listener ] Email : --> {}", userMedianDto.email()); + - Optional userMedian = userMedianRepository.findById(userMedianDto.getUserId()); - if (userMedian.isPresent()) { + Optional optionalUserMedian = userMedianRepository.findByEmailAndSeason(userMedianDto.email(), currentSeason); + if (optionalUserMedian.isPresent()) { //User Median 이미 있을 경우 - userMedian.get() - .setLevel(userMedianDto.getLevel(), userMedianDto.getTemperature()); + UserMedian userMedian = optionalUserMedian.get(); + userMedian.setLevel(userMedianDto); + userMedianRepository.save(userMedian); + } else { - //User Median 없을 경우 (생성) + //User Median 없을 경우 생성 + //TODO : 계절당 초기값 받아야 함 + log.warn("[ Kafka Listener ] User Median 초기값이 없었습니다."); UserMedian newUserMedian = UserMedian.builder() - .userId(userMedianDto.getUserId()) + .email(userMedianDto.email()) + .season(currentSeason) .build(); - newUserMedian.setLevel(userMedianDto.getLevel(), - userMedianDto.getTemperature()); + newUserMedian.setLevel(userMedianDto); userMedianRepository.save(newUserMedian); } - - } /** * Firebase Token Listener * */ + @Transactional @KafkaListener(topics = "firebase-token", containerFactory = "firebaseTokenKafkaListenerContainerFactory") - public void consumeFirebaseToken(TokenDto tokenDto) { + public void consumeFirebaseToken(KafkaDto.TokenDto tokenDto) { log.info("[ Kafka Listener ] Firebase Token 동기화"); - log.info("[ Kafka Listener ] User Id : --> {}", tokenDto.getUserId()); - log.info("[ Kafka Listener ] Token : --> {}", tokenDto.getToken()); + log.info("[ Kafka Listener ] Email : --> {}", tokenDto.email()); + log.info("[ Kafka Listener ] Token : --> {}", tokenDto.token()); //토큰 Redis 저장 - redisUtils.save(String.valueOf(tokenDto.getUserId()), tokenDto.getToken()); + redisUtils.save(tokenDto.email(), tokenDto.token()); } /** * User Settings Listener * */ - @KafkaListener(topics = "user-settings", containerFactory = "userSettingsKafkaListenerContainerFactory") - public void consumeUserSettings(UserSettingsDto userSettingsDto) { + @Transactional + @KafkaListener(topics = "${spring.kafka.template.user-settings-topic}", containerFactory = "userSettingsKafkaListenerContainerFactory") + public void consumeUserSettings(KafkaDto.UserSettingsDto userSettingsDto) { log.info("[ Kafka Listener ] 사용자 설정값 데이터 동기화"); - log.info("[ Kafka Listener ] User Id : --> {}", userSettingsDto.getUserId()); - log.info("[ Kafka Listener ] Key : --> {}", userSettingsDto.getKey()); - log.info("[ Kafka Listener ] Value : --> {}", userSettingsDto.getValue()); + log.info("[ Kafka Listener ] Email : --> {}", userSettingsDto.email()); + log.info("[ Kafka Listener ] Key : --> {}", userSettingsDto.key()); + log.info("[ Kafka Listener ] Value : --> {}", userSettingsDto.value()); - Optional userData = userDataRepository.findById(userSettingsDto.getUserId()); + Optional userData = userDataRepository.findByEmail(userSettingsDto.email()); if (userData.isPresent()) { - userData.get().updateValue(userSettingsDto.getKey(), userSettingsDto.getValue()); + userData.get().updateValue(userSettingsDto.key(), userSettingsDto.value()); userDataRepository.save(userData.get()); } else { + log.warn("[ Kafka Listener ] User Data 초기값이 없었습니다."); UserData newUserData = UserData.builder() - .userId(userSettingsDto.getUserId()) + .email(userSettingsDto.email()) .build(); - newUserData.updateValue(userSettingsDto.getKey(), userSettingsDto.getValue()); + newUserData.updateValue(userSettingsDto.key(), userSettingsDto.value()); userDataRepository.save(newUserData); } } + @Transactional + @KafkaListener(topics = "${spring.kafka.template.initial-data-topic}", containerFactory = "initialDataKafkaListenerContainerFactory") + public void consumeUserInit(KafkaDto.InitialDataDto initialDataDto) { + + log.info("[ Kafka Listener ] 초기 설정값 세팅"); + log.info("[ Kafka Listener ] email --> {}", initialDataDto.email()); + userDataRepository.save(initialDataDto.toUserDataEntity()); + userMedianRepository.saveAll(initialDataDto.toUserMedianList()); + } /** * 바람 세기 알림 Listener - * */ - @KafkaListener(topics = "alarm-wind") - public void consumeWindAlarm(@Payload String message) { - String resultMessage = ""; - Long windStrength = Long.valueOf(message); //바람세기 + * @Query : 0200, 0500, 0800, 1100, 1400, 1700, 2000, 2300 */ + @Transactional + @KafkaListener(topics = "alarm-wind", containerFactory = "weatherKafkaListenerContainerFactory") + public void consumeWindAlarm(KafkaDto.WeatherDto weatherDto) { + + int currentHour = LocalDateTime.now().getHour(); + // 22:00 ~ 07:00 는 알림을 전송하지 않음 + if (currentHour >= 22 || currentHour <= 7) { + return; + } + + String title = "Waither 바람 세기 알림"; + StringBuilder sb = new StringBuilder(); + + String region = weatherDto.region(); + Double windStrength = Double.valueOf(weatherDto.message()); //바람세기 log.info("[ Kafka Listener ] 바람 세기"); log.info("[ Kafka Listener ] Wind Strength : --> {}", windStrength); - //TODO : 알림 보낼 사용자 정보 가져오기 (Redis) - List userIds = new ArrayList<>(); + // Wind Alert를 True로 설정한 User Query + List userData = userDataRepository.findAllByWindAlertIsTrue(); + + //알림 보낼 사용자 이메일 + List userEmails = filterRegionAndWindAlarm(region, userData, currentHour); - //TODO : 바람 세기 알림 멘트 정리 - resultMessage += "현재 바람 세기가 " + windStrength + "m/s 이상입니다. 강풍에 주의하세요."; + sb.append("현재 바람 세기가 ").append(windStrength).append("m/s 이상입니다."); System.out.println("[ 푸시알림 ] 바람 세기 알림"); - sendAlarms(userIds, resultMessage); + + alarmService.sendAlarms(userEmails,title, sb.toString()); + + //Record 알림 시간 초기화 + userEmails + .forEach(email -> { + Optional notificationRecord = notificationRecordRepository.findByEmail(email); + notificationRecord.ifPresent(NotificationRecord::initializeWindTime); + }); } /** - * 강설 정보 알림 Listener - * */ - @KafkaListener(topics = "alarm-snow") - public void consumeSnow(@Payload String message) { - String resultMessage = ""; - Double snow = Double.valueOf(message); //강수량 + * 강설 정보 알림 Listener
+ * 기상청 기준
+ * 약한 비 1~3mm
+ * 보통 비 3~15mm
+ * 강한 비 15~30mm
+ * 매우 강한 비 30mm 이상
+ * 참고 + */ + @Transactional + @KafkaListener(topics = "alarm-snow", containerFactory = "weatherKafkaListenerContainerFactory") + public void consumeSnow(KafkaDto.WeatherDto weatherDto) { + + int currentHour = LocalDateTime.now().getHour(); + // 22:00 ~ 07:00 는 알림을 전송하지 않음 + if (currentHour >= 22 || currentHour <= 7) { + return; + } + + String title = "Waither 강수 정보 알림"; + StringBuilder sb = new StringBuilder(); + + String region = weatherDto.region(); + Double prediction = Double.valueOf(weatherDto.message()); //강수량 + + log.info("[ Kafka Listener ] 강수량 지역 --> {}", region); + log.info("[ Kafka Listener ] 걍수량 --> {}", prediction); + + List userData = userDataRepository.findAllBySnowAlertIsTrue(); - log.info("[ Kafka Listener ] 강수량"); - log.info("[ Kafka Listener ] Snow : --> {}", snow); + //예시 : 현재 서울특별시 지역에 2mm의 약한 비가 내릴 예정입니다. + //TODO: 언제 내리는지? 확인 필요 + sb.append("현재 ").append(region).append(" 지역에 ").append(prediction).append("mm의 ") + .append(getExpression(prediction)).append("가 내릴 예정입니다."); - //TODO : 알림 보낼 사용자 정보 가져오기 (Redis) - List userIds = new ArrayList<>(); + //알림 보낼 사용자 이메일 + List userEmails = filterRegionAndRainAlarm(region, userData, currentHour); - //TODO : 알림 멘트 정리 - resultMessage += "현재 강수량 " + snow + "m/s 이상입니다."; System.out.println("[ 푸시알림 ] 강수량 알림"); - sendAlarms(userIds, resultMessage); + alarmService.sendAlarms(userEmails, title, sb.toString()); + + //Record 알림 시간 초기화 + userEmails + .forEach(email -> { + Optional notificationRecord = notificationRecordRepository.findByEmail(email); + notificationRecord.ifPresent(NotificationRecord::initializeRainTime); + }); + } /** * 기상 특보 알림 Listener * */ - @KafkaListener(topics = "alarm-climate") - public void consumeClimateAlarm(@Payload String message) { - String resultMessage = ""; + @Transactional + @KafkaListener(topics = "alarm-climate", containerFactory = "weatherKafkaListenerContainerFactory") + public void consumeClimateAlarm(KafkaDto.WeatherDto weatherDto) { + int currentHour = LocalDateTime.now().getHour(); + // 22:00 ~ 07:00 는 알림을 전송하지 않음 + if (currentHour >= 22 || currentHour <= 7) { + return; + } + + String title = "Waither 기상 특보 알림"; + StringBuilder sb = new StringBuilder(); log.info("[ Kafka Listener ] 기상 특보"); - //TODO : 알림 보낼 사용자 정보 가져오기 (Redis) - List userIds = new ArrayList<>(); + String region = weatherDto.region(); + String message = weatherDto.message(); + + // Wind Climate를 True로 설정한 User Query + List userData = userDataRepository.findAllByClimateAlertIsTrue(); - resultMessage += "[기상청 기상 특보] " + message; + // 알림 보낼 사용자 이메일 + List userEmails = filterRegion(region, userData); + + sb.append("[기상청 기상 특보] ").append(message); - //TODO : 푸시알림 전송 System.out.println("[ 푸시알림 ] 기상 특보 알림"); - sendAlarms(userIds, resultMessage); + alarmService.sendAlarms(userEmails, title, sb.toString()); } - private void sendAlarms(List userIds, String message) { - userIds.forEach(id ->{ - String token = String.valueOf(redisUtils.get(String.valueOf(id))); - if (token == null) { //token을 찾지 못했을 경우 - throw new CustomException(ErrorCode.FIREBASE_TOKEN_NOT_FOUND); - } + //지역 필터링 & 알림 규칙 검사 + private List filterRegionAndWindAlarm(String region, List userData, int currentHour) { + return userData.stream() + .filter(data -> { + Optional notiRecord = notificationRecordRepository.findByEmail(data.getEmail()); + return notiRecord.map(notificationRecord -> + (Math.abs(notiRecord.get().getLastWindAlarmReceived().getHour() - currentHour) >=3 + && notificationRecord.getRegion().equals(region) ) + ).orElse(false); + }) + .map(UserData::getEmail) + .toList(); + } + //지역 필터링 & 알림 규칙 검사 + private List filterRegionAndRainAlarm(String region, List userData, int currentHour) { + return userData.stream() + .filter(data -> { + Optional notiRecord = notificationRecordRepository.findByEmail(data.getEmail()); + return notiRecord.map(notificationRecord -> + (Math.abs(notiRecord.get().getLastRainAlarmReceived().getHour() - currentHour) >=3 + && notificationRecord.getRegion().equals(region) ) + ).orElse(false); + }) + .map(UserData::getEmail) + .toList(); + } + + private List filterRegion(String region, List userData) { + return userData.stream() + .filter(data -> { + Optional notiRecord = notificationRecordRepository.findByEmail(data.getEmail()); + return notiRecord.map(notificationRecord -> notificationRecord.getRegion().equals(region)).orElse(false); + }) + .map(UserData::getEmail) + .toList(); + } - System.out.printf("[ 푸시알림 ] userId ---> {%d}", id); - System.out.printf("[ 푸시알림 ] message ---> {%s}", message); - }); + //강수 표현 + private String getExpression(double prediction) { + //1~3mm : 약한 비 + if (prediction > 1 && prediction < 3) { + return "약한 비"; + //3~15mm : 비 + } else if (prediction >=3 && prediction < 15) { + return "비"; + //15~30mm 강한 비 + } else if (prediction >= 15 &&prediction <= 30) { + return "강한 비"; + //30mm~ 매우 강한 비 + } else if (prediction >= 30) { + return "매우 강한 비"; + } else return "비"; } } diff --git a/noti-service/src/main/java/com/waither/notiservice/service/NotificaitonRecordService.java b/noti-service/src/main/java/com/waither/notiservice/service/NotificaitonRecordService.java new file mode 100644 index 00000000..d5599ca6 --- /dev/null +++ b/noti-service/src/main/java/com/waither/notiservice/service/NotificaitonRecordService.java @@ -0,0 +1,30 @@ +package com.waither.notiservice.service; + +import com.waither.notiservice.domain.redis.NotificationRecord; +import com.waither.notiservice.global.exception.CustomException; +import com.waither.notiservice.repository.redis.NotificationRecordRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.Optional; + +@RequiredArgsConstructor +@Service +public class NotificaitonRecordService { + + private final NotificationRecordRepository notificationRecordRepository; + + public void updateWindAlarm(String email) { + Optional notificationRecord = notificationRecordRepository.findByEmail(email); + + notificationRecord.ifPresentOrElse(record -> record.setLastWindAlarmReceived(LocalDateTime.now()), null); + } + + public void updateRainAlarm(String email) { + Optional notificationRecord = notificationRecordRepository.findByEmail(email); + notificationRecord.ifPresent(record -> record.setLastRainAlarmReceived(LocalDateTime.now())); + } + + +} diff --git a/noti-service/src/main/java/com/waither/notiservice/service/NotificationService.java b/noti-service/src/main/java/com/waither/notiservice/service/NotificationService.java index ec416767..2b4bf4e0 100644 --- a/noti-service/src/main/java/com/waither/notiservice/service/NotificationService.java +++ b/noti-service/src/main/java/com/waither/notiservice/service/NotificationService.java @@ -1,20 +1,27 @@ package com.waither.notiservice.service; import com.waither.notiservice.api.response.NotificationResponse; +import com.waither.notiservice.domain.Notification; import com.waither.notiservice.domain.UserData; import com.waither.notiservice.domain.UserMedian; import com.waither.notiservice.api.request.LocationDto; +import com.waither.notiservice.domain.redis.NotificationRecord; +import com.waither.notiservice.enums.Season; import com.waither.notiservice.global.exception.CustomException; import com.waither.notiservice.global.response.ErrorCode; -import com.waither.notiservice.repository.NotificationRepository; -import com.waither.notiservice.repository.UserDataRepository; -import com.waither.notiservice.repository.UserMedianRepository; +import com.waither.notiservice.repository.jpa.NotificationRepository; +import com.waither.notiservice.repository.jpa.UserDataRepository; +import com.waither.notiservice.repository.jpa.UserMedianRepository; +import com.waither.notiservice.repository.redis.NotificationRecordRepository; import com.waither.notiservice.utils.TemperatureUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; @Slf4j @RequiredArgsConstructor @@ -22,36 +29,50 @@ public class NotificationService { private final NotificationRepository notificationRepository; - private final UserMedianRepository userMedianRepository; - private final UserDataRepository userDataRepository; + private final NotificationRecordRepository notificationRecordRepository; + private final AlarmService alarmService; - public List getNotifications(Long userId) { - return notificationRepository.findAllByUserId(userId) + @Transactional(readOnly = true) + public List getNotifications(String email) { + + return notificationRepository.findAllByEmail(email) .stream().map(NotificationResponse::of).toList(); } - public void deleteNotification(String notificationId) { - notificationRepository.deleteById(notificationId); + @Transactional + public void deleteNotification(String email, String notificationId) { + Notification notification = notificationRepository.findById(notificationId).orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_404)); + + if (!notification.getEmail().equals(email)) { + throw new CustomException(ErrorCode.UNAUTHORIZED_401); + } + + notificationRepository.delete(notification); } - public String sendGoOutAlarm(Long userId) { + @Transactional + public String sendGoOutAlarm(String email) { - UserData userData = userDataRepository.findById(userId).orElseThrow( + UserData userData = userDataRepository.findByEmail(email).orElseThrow( () -> new CustomException(ErrorCode.NO_USER_DATA_REGISTERED)); - String finalMessage = ""; + Season currentSeason = TemperatureUtils.getCurrentSeason(); + + LocalDateTime now = LocalDateTime.now(); + String title = now.getMonth() + "월 " + now.getDayOfMonth() + "일 날씨 정보입니다."; + StringBuilder sb = new StringBuilder(); /** * 1. 기본 메세지 시작 형식 */ //TODO : 날씨 정보 가져오기 & 날씨별 멘트 정리 String nickName = userData.getNickName(); - finalMessage += nickName + "님, 오늘은 "; + sb.append(nickName).append("님, 오늘은 "); /** @@ -59,14 +80,16 @@ public String sendGoOutAlarm(Long userId) { * {@link com.waither.notiservice.enums.Expressions} 참고 */ double temperature = 10.8; + if (userData.isUserAlert()) { //사용자 맞춤 알림이 on이라면 -> 계산 후 전용 정보 제공 - UserMedian userMedian = userMedianRepository.findById(userId).orElseThrow( + UserMedian userMedian = userMedianRepository.findByEmailAndSeason(email, currentSeason).orElseThrow( () -> new CustomException(ErrorCode.NO_USER_MEDIAN_REGISTERED)); - finalMessage += TemperatureUtils.createUserDataMessage(userMedian, temperature); + + sb.append(TemperatureUtils.createUserDataMessage(userMedian, temperature)); } else { //사용자 맞춤 알림이 off라면 -> 하루 평균 온도 정보 제공 - finalMessage += "평균 온도가 "+temperature+"도예요."; + sb.append("평균 온도가 ").append(temperature).append("도입니다."); } @@ -80,33 +103,59 @@ public String sendGoOutAlarm(Long userId) { * 3. 강수 정보 가져오기 - Weather Service */ //TODO : 강수량 확인, 멘트 - finalMessage += " 오후 6시부터 8시까지 120mm의 비가 올 예정입니다."; + sb.append(" 오후 6시부터 8시까지 120mm의 비가 올 예정입니다."); /** * 4. 꽃가루 정보 가져오기 - Weather Service */ //TODO : 꽃가루 확인 - finalMessage += " 꽃가루는 없습니다. "; + sb.append(" 꽃가루는 없습니다. ") ; /** * 알림 전송 */ //TODO : FireBase 알림 보내기 - log.info("[ Notification Service ] Final Message ---> {}", finalMessage); + log.info("[ Notification Service ] Final Message ---> {}", sb.toString()); - return finalMessage; + //알림 보내기 + alarmService.sendSingleAlarm(email, title, sb.toString()); + return sb.toString(); } - //현재 위치 공유 -> 상시 알림 검사 - public void checkCurrentAlarm(LocationDto locationDto) { + //현재 위치 업데이트 + @Transactional + public void updateLocation(String email, LocationDto locationDto) { + + log.info("[ Notification Service ] email ---> {}", email); + log.info("[ Notification Service ] 현재 위치 위도 (lat) ---> {}", locationDto.lat()); + log.info("[ Notification Service ] 현재 위치 경도 (lon) ---> {}", locationDto.lon()); + + Optional notiRecord = notificationRecordRepository.findByEmail(email); + + //TODO : 위도 경도 -> 지역 변환 + String region = "서울특별시"; + + if (notiRecord.isPresent()) { + NotificationRecord notificationRecord = notiRecord.get(); + + if (!notiRecord.get().getRegion().equals(region)) { + //만약 위치가 이동됐다면 알림 시간 초기화 + notificationRecord.setLastWindAlarmReceived(LocalDateTime.now().minusHours(4)); + notificationRecord.setLastRainAlarmReceived(LocalDateTime.now().minusHours(4)); + } + notificationRecord.setRegion(region); + + } else notificationRecordRepository.save( + NotificationRecord.builder() + .email(email) + .region(region) + .lastRainAlarmReceived(LocalDateTime.now().minusHours(4)) + .lastWindAlarmReceived(LocalDateTime.now().minusHours(4)) + .build() + ); - log.info("[ Notification Service ] 현재 위치 공유 위도 (x) ---> {}", locationDto.getX()); - log.info("[ Notification Service ] 현재 위치 공유 위도 (y) ---> {}", locationDto.getY()); - //TODO : 현재 지역에 강수량 정보가 있는지? - //TODO : 현재 지역에 바람 세기 정보는 있는지? - //TODO : 만약 알림 내용이 있다면 전송하기 } } diff --git a/noti-service/src/main/java/com/waither/notiservice/utils/FireBaseUtils.java b/noti-service/src/main/java/com/waither/notiservice/utils/FireBaseUtils.java new file mode 100644 index 00000000..fc8d83c9 --- /dev/null +++ b/noti-service/src/main/java/com/waither/notiservice/utils/FireBaseUtils.java @@ -0,0 +1,77 @@ +package com.waither.notiservice.utils; + +import com.google.firebase.messaging.*; +import com.waither.notiservice.global.exception.CustomException; +import com.waither.notiservice.global.response.ErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Component +@RequiredArgsConstructor +public class FireBaseUtils { + + //특정 기기에 메시지 전송 +// @Retryable(maxAttempts = 3) + public void sendSingleMessage(String token, String title, String body) { + try { + Message message = Message.builder() + .setNotification(Notification.builder() + .setTitle(title) + .setBody(body) + .build()) + .setToken(token) + .build(); + + String response = FirebaseMessaging.getInstance().send(message); + + log.info("[ FireBaseUtils ] Successfully sent message --> {}", response); + + } catch (FirebaseMessagingException ex) { + ex.printStackTrace(); + log.error("[ FireBaseUtils ] Failed to send message : {}",ex.getMessage()); + } + } + + // 일괄 메세지 전송 + // 호출당 최대 500기기 등록 가능 + public void sendAllMessages(List tokens, String title, String body) { + try { + + log.info("[ FireBaseUtils ] Try Sending Messages, Count ---> {}", tokens.size()); + + List messages = tokens.stream().map( + token -> Message.builder() + .setNotification(Notification.builder() + .setTitle(title) + .setBody(body) + .build()) + .setToken(token) + .build() + ).toList(); + + //send All 은 deprecated. 대신 sendEach 사용 + BatchResponse response = FirebaseMessaging.getInstance().sendEach(messages); + + log.info("[ FireBaseUtils ] Successfully Sent Messages, Count ---> {}", response.getSuccessCount()); + + if (response.getFailureCount() > 0) { + response.getResponses() + .forEach( singleResponse -> { + if (!singleResponse.isSuccessful()) { + log.warn("[ FireBaseUtils ] Failed to send message, id ---> {}", singleResponse.getMessageId()); + } + }); + } + + } catch (FirebaseMessagingException ex) { + ex.printStackTrace(); + log.error("[ FireBaseUtils ] Failed to send message : {}",ex.getMessage()); + } + } +} diff --git a/noti-service/src/main/java/com/waither/notiservice/utils/TemperatureUtils.java b/noti-service/src/main/java/com/waither/notiservice/utils/TemperatureUtils.java index 9bb2ea32..efafe93e 100644 --- a/noti-service/src/main/java/com/waither/notiservice/utils/TemperatureUtils.java +++ b/noti-service/src/main/java/com/waither/notiservice/utils/TemperatureUtils.java @@ -1,7 +1,7 @@ package com.waither.notiservice.utils; import com.waither.notiservice.domain.UserMedian; -import com.waither.notiservice.domain.type.Season; +import com.waither.notiservice.enums.Season; import com.waither.notiservice.enums.*; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; diff --git a/noti-service/src/main/resources/bootstrap.yml b/noti-service/src/main/resources/bootstrap.yml index 92e343bd..7e27fa7f 100644 --- a/noti-service/src/main/resources/bootstrap.yml +++ b/noti-service/src/main/resources/bootstrap.yml @@ -9,7 +9,7 @@ spring: cloud: config: uri: http://localhost:8888 - name: database-noti,redis + name: database-noti, redis, kafka, firebase kafka: bootstrap-servers: "localhost:9092" diff --git a/noti-service/src/test/java/com/waither/notiservice/service/KafkaConsumerTest.java b/noti-service/src/test/java/com/waither/notiservice/service/KafkaConsumerTest.java index 43eaa0f1..f6bd0023 100644 --- a/noti-service/src/test/java/com/waither/notiservice/service/KafkaConsumerTest.java +++ b/noti-service/src/test/java/com/waither/notiservice/service/KafkaConsumerTest.java @@ -1,18 +1,16 @@ package com.waither.notiservice.service; import com.waither.notiservice.domain.UserData; -import com.waither.notiservice.dto.kafka.TokenDto; -import com.waither.notiservice.dto.kafka.UserMedianDto; -import com.waither.notiservice.dto.kafka.UserSettingsDto; -import com.waither.notiservice.repository.UserDataRepository; -import com.waither.notiservice.repository.UserMedianRepository; +import com.waither.notiservice.domain.UserMedian; +import com.waither.notiservice.enums.Season; +import com.waither.notiservice.dto.kafka.KafkaDto; +import com.waither.notiservice.repository.jpa.UserDataRepository; +import com.waither.notiservice.repository.jpa.UserMedianRepository; import com.waither.notiservice.utils.RedisUtils; +import com.waither.notiservice.utils.TemperatureUtils; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.common.serialization.StringSerializer; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -26,7 +24,9 @@ import org.springframework.transaction.annotation.Transactional; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import static org.assertj.core.api.Assertions.assertThat; @@ -40,7 +40,7 @@ public class KafkaConsumerTest { Map stringProps; @BeforeEach - void setUp() { + void setUp() throws InterruptedException { jsonProps = new HashMap<>(); jsonProps.put(ProducerConfig.ACKS_CONFIG, "all"); jsonProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); @@ -63,10 +63,8 @@ static void beforeAll() { @Autowired private UserMedianRepository userMedianRepository; - @Autowired private RedisUtils redisUtils; - @Autowired private UserDataRepository userDataRepository; @@ -74,17 +72,24 @@ static void beforeAll() { @DisplayName("User Median Consumer Test") @Transactional //Transaction 후 Rollback (작동하지 않음. Listener 로 작동해서?) void userMedianTest() throws InterruptedException { + //Given - ProducerFactory pf = new DefaultKafkaProducerFactory<>(jsonProps); - KafkaTemplate template = new KafkaTemplate<>(pf); + ProducerFactory pf = new DefaultKafkaProducerFactory<>(jsonProps); + KafkaTemplate template = new KafkaTemplate<>(pf); + Season currentSeason = TemperatureUtils.getCurrentSeason(); + String tempEmail = "kafkaTest@gmail.com"; //when - UserMedianDto userMedianDto = UserMedianDto.builder() - .userId(0L) - .level(1) - .temperature(10.5) + KafkaDto.UserMedianDto userMedianDto = KafkaDto.UserMedianDto.builder() + .email(tempEmail) + .seasonData(KafkaDto.SeasonData.builder() + .medianOf1And2(10.5) + .medianOf2And3(12.5) + .build() + ) .build(); - CompletableFuture> future = template.send("user-median", userMedianDto); + System.out.println("[ Kafka Test ] data --> "+ userMedianDto); + CompletableFuture> future = template.send("user-median", userMedianDto); //then future.whenComplete(((result, throwable) -> { @@ -93,16 +98,20 @@ void userMedianTest() throws InterruptedException { System.out.println("offset : "+ result.getRecordMetadata().offset()); } )); - Thread.sleep(2000); //2초 대기 + + System.out.println("5초 대기"); + Thread.sleep(5000); userMedianRepository.findAll().forEach(userMedian -> { - System.out.println(" userId : " + userMedian.getUserId()); + System.out.println(" email : " + userMedian.getEmail()); }); - assertThat(userMedianRepository.findById(0L).get().getMedianOf1And2()).isEqualTo(10.5); + Optional byEmailAndSeason = userMedianRepository.findByEmailAndSeason(tempEmail, currentSeason); + assertThat(byEmailAndSeason.isPresent()).isTrue(); + assertThat(byEmailAndSeason.get().getMedianOf1And2()).isEqualTo(10.5); //끝나고 삭제 -> Rollback 일어나지 않아서 - userMedianRepository.deleteById(0L); + userMedianRepository.deleteById(tempEmail); } @@ -112,15 +121,16 @@ void userMedianTest() throws InterruptedException { @Transactional //Transaction 후 Rollback (작동하지 않음. Listener 로 작동해서?) void firebaseTokenTest() throws InterruptedException { //Given - ProducerFactory pf = new DefaultKafkaProducerFactory<>(jsonProps); - KafkaTemplate template = new KafkaTemplate<>(pf); + ProducerFactory pf = new DefaultKafkaProducerFactory<>(jsonProps); + KafkaTemplate template = new KafkaTemplate<>(pf); + String tempEmail = "kafkaTest@gmail.com"; //when - TokenDto tokenDto = TokenDto.builder() - .userId(0L) + KafkaDto.TokenDto tokenDto = KafkaDto.TokenDto.builder() + .email(tempEmail) .token("test token") .build(); - CompletableFuture> future = template.send("firebase-token", tokenDto); + CompletableFuture> future = template.send("firebase-token", tokenDto); //then future.whenComplete(((result, throwable) -> { @@ -132,10 +142,10 @@ void firebaseTokenTest() throws InterruptedException { Thread.sleep(2000); //2초 대기 - assertThat(String.valueOf(redisUtils.get("0"))).isEqualTo("test token"); + assertThat(String.valueOf(redisUtils.get(tempEmail))).isEqualTo("test token"); //끝나고 삭제 -> Rollback 일어나지 않아서 - redisUtils.delete("0"); + redisUtils.delete(tempEmail); } @Test @@ -143,21 +153,23 @@ void firebaseTokenTest() throws InterruptedException { @Transactional //Transaction 후 Rollback (작동하지 않음. Listener 로 작동해서?) void userSettingsWindDegreeTest() throws InterruptedException { //Given - ProducerFactory pf = new DefaultKafkaProducerFactory<>(jsonProps); - KafkaTemplate template = new KafkaTemplate<>(pf); + ProducerFactory pf = new DefaultKafkaProducerFactory<>(jsonProps); + KafkaTemplate template = new KafkaTemplate<>(pf); + String tempEmail = "kafkaTest@gmail.com"; //when userDataRepository.save(UserData.builder() .windDegree(11) - .userId(0L) + .email(tempEmail) .build()); - UserSettingsDto userSettingsDto = UserSettingsDto.builder() - .userId(0L) + + KafkaDto.UserSettingsDto userSettingsDto = KafkaDto.UserSettingsDto.builder() + .email(tempEmail) .key("windDegree") .value("11") .build(); - CompletableFuture> future = template.send("user-settings", userSettingsDto); + CompletableFuture> future = template.send("user-settings", userSettingsDto); //then future.whenComplete(((result, throwable) -> { @@ -169,10 +181,10 @@ void userSettingsWindDegreeTest() throws InterruptedException { Thread.sleep(2000); //2초 대기 - assertThat(userDataRepository.findById(0L).get().getWindDegree()).isEqualTo(11); + assertThat(userDataRepository.findByEmail(tempEmail).get().getWindDegree()).isEqualTo(11); //끝나고 삭제 -> Rollback 일어나지 않아서 - userDataRepository.deleteById(0L); + userDataRepository.deleteById(tempEmail); } @Test diff --git a/noti-service/src/test/java/com/waither/notiservice/service/NotificationServiceTest.java b/noti-service/src/test/java/com/waither/notiservice/service/NotificationServiceTest.java index b1685532..c1e5a509 100644 --- a/noti-service/src/test/java/com/waither/notiservice/service/NotificationServiceTest.java +++ b/noti-service/src/test/java/com/waither/notiservice/service/NotificationServiceTest.java @@ -4,20 +4,18 @@ import com.waither.notiservice.domain.Notification; import com.waither.notiservice.domain.UserData; import com.waither.notiservice.domain.UserMedian; -import com.waither.notiservice.repository.NotificationRepository; -import com.waither.notiservice.repository.UserDataRepository; -import com.waither.notiservice.repository.UserMedianRepository; +import com.waither.notiservice.enums.Season; +import com.waither.notiservice.repository.jpa.NotificationRepository; +import com.waither.notiservice.repository.jpa.UserDataRepository; +import com.waither.notiservice.repository.jpa.UserMedianRepository; +import com.waither.notiservice.repository.redis.NotificationRecordRepository; import com.waither.notiservice.utils.TemperatureUtils; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; @@ -41,10 +39,13 @@ public class NotificationServiceTest { @MockBean NotificationRepository notificationRepository; + @MockBean + NotificationRecordRepository notificationRecordRepository; + @BeforeEach void setUp() { //가짜 객체 주입 - notificationService = new NotificationService(notificationRepository, userMedianRepository, userDataRepository); + notificationService = new NotificationService(notificationRepository, userMedianRepository, userDataRepository, notificationRecordRepository); } @Test @@ -53,8 +54,12 @@ void goOutAlarm() { //Given System.out.println("DB Mock 데이터 생성.. userid : 0"); + + String tempEmail = "serviceTest@gmail.com"; + Season currentSeason = TemperatureUtils.getCurrentSeason(); + UserData newUser = UserData.builder() - .userId(0L) + .email(tempEmail) .nickName("추워하는 곰탱이") .climateAlert(true) .regionReport(true) @@ -63,25 +68,25 @@ void goOutAlarm() { .windAlert(true) .windDegree(0) .build(); - Mockito.when(userDataRepository.findById(0L)).thenReturn(Optional.of(newUser)); // (Mock) find시 Return + Mockito.when(userDataRepository.findByEmail(tempEmail)).thenReturn(Optional.of(newUser)); // (Mock) find시 Return UserMedian newUserMedian = UserMedian.builder() - .userId(0L) + .email(tempEmail) .medianOf1And2(12.0) .medianOf2And3(15.0) .medianOf3And4(20.0) .medianOf4And5(25.0) .season(TemperatureUtils.getCurrentSeason()) //현재 계절로 저장 .build(); - Mockito.when(userMedianRepository.findById(0L)).thenReturn(Optional.of(newUserMedian)); // (Mock) find시 Return + Mockito.when(userMedianRepository.findByEmailAndSeason(tempEmail, currentSeason)).thenReturn(Optional.of(newUserMedian)); // (Mock) find시 Return //when - String resultMessage = notificationService.sendGoOutAlarm(0L); + String resultMessage = notificationService.sendGoOutAlarm(tempEmail); //then System.out.println("[ Notification Service Test ] result Message --> "+resultMessage); - assertThat(userDataRepository.findById(0L)).isNotNull(); - assertThat(userMedianRepository.findById(0L)).isNotNull(); + assertThat(userDataRepository.findByEmail(tempEmail)).isNotNull(); + assertThat(userMedianRepository.findByEmailAndSeason(tempEmail, currentSeason)).isNotNull(); assertThat(resultMessage).isNotBlank(); } @@ -89,16 +94,17 @@ void goOutAlarm() { @DisplayName("알림 조회 테스트") void getAlarm() { //given + String tempEmail = "serviceTest@gmail.com"; Notification newNotification = Notification.builder() - .userId(0L) + .email(tempEmail) .title("test title") .content("test content") .build(); notificationRepository.save(newNotification); - Mockito.when(notificationRepository.findAllByUserId(0L)).thenReturn(List.of(newNotification)); // (Mock) find시 Return + Mockito.when(notificationRepository.findAllByEmail(tempEmail)).thenReturn(List.of(newNotification)); // (Mock) find시 Return //when - List notifications = notificationService.getNotifications(0L); + List notifications = notificationService.getNotifications(tempEmail); //then assertEquals(1, notifications.size()); // 예상되는 알림 개수가 맞는지 확인 diff --git a/user-service/src/main/java/com/waither/userservice/controller/OAuthController.java b/user-service/src/main/java/com/waither/userservice/controller/OAuthController.java index 84a72f4e..970a40bd 100644 --- a/user-service/src/main/java/com/waither/userservice/controller/OAuthController.java +++ b/user-service/src/main/java/com/waither/userservice/controller/OAuthController.java @@ -4,6 +4,7 @@ import com.waither.userservice.global.response.ApiResponse; import com.waither.userservice.service.KakaoService; import com.waither.userservice.service.commandService.UserService; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -19,6 +20,7 @@ public class OAuthController { private final UserService userService; + @Operation(hidden = true) @GetMapping("/kakao/callback") public ApiResponse callback(@RequestParam("code") String code) {