diff --git a/.gitignore b/.gitignore index 700f317b..b229989f 100644 --- a/.gitignore +++ b/.gitignore @@ -264,4 +264,5 @@ $RECYCLE.BIN/ *.html -custom-kibana/ \ No newline at end of file +custom-kibana/ +/src/main/resources/fcm_root_key/monitory-28aad-firebase-adminsdk-fbsvc-0d7886e5f5.json diff --git a/build.gradle b/build.gradle index 60f88500..44ecccba 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ dependencies { implementation 'org.bouncycastle:bcpkix-jdk15on:1.70' // swagger doc - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8' // flyway db implementation 'org.flywaydb:flyway-core' @@ -68,8 +68,9 @@ dependencies { implementation 'org.springframework.kafka:spring-kafka' testImplementation 'org.springframework.kafka:spring-kafka-test' - // WebSocket - implementation 'org.springframework.boot:spring-boot-starter-websocket' + // firebase cloud messaging(FCM) + // https://mvnrepository.com/artifact/com.google.firebase/firebase-admin + implementation("com.google.firebase:firebase-admin:9.4.3") } tasks.named('test') { diff --git a/src/main/java/com/factoreal/backend/domain/abnormalLog/api/AbnormalController.java b/src/main/java/com/factoreal/backend/domain/abnormalLog/api/AbnormalController.java index 291ac94c..2f9bc177 100644 --- a/src/main/java/com/factoreal/backend/domain/abnormalLog/api/AbnormalController.java +++ b/src/main/java/com/factoreal/backend/domain/abnormalLog/api/AbnormalController.java @@ -1,8 +1,11 @@ package com.factoreal.backend.domain.abnormalLog.api; import com.factoreal.backend.domain.abnormalLog.application.AbnormalLogService; +import com.factoreal.backend.domain.abnormalLog.dto.TargetType; import com.factoreal.backend.domain.abnormalLog.dto.request.AbnormalPagingRequest; import com.factoreal.backend.domain.abnormalLog.dto.response.AbnormalLogResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; @@ -11,23 +14,26 @@ @RestController @RequestMapping("/api/abnormal") @RequiredArgsConstructor +@Tag(name = "상태 로그 조회 API", description = "작업자/환경/설비 이상의 로그를 조회하는 API") public class AbnormalController { - private final AbnormalLogService abnormalLogService; // 전체 로그 조회 @GetMapping + @Operation(summary = "전체 로그 조회", description = "페이징 인자를 기준으로 로그를 요정 개수 만큼 반환해줍니다.") public Page getAllAbnormalLogs(@ModelAttribute AbnormalPagingRequest pagingDto) { return abnormalLogService.findAllAbnormalLogs(pagingDto); } @GetMapping("/unread") + @Operation(summary = "미확인 로그 조회", description = "페이징 인자를 기준으로 읽지 않은 로그를 요정 개수 만큼 반환해줍니다.") public Page getAllAbnormalLogsUnRead(@ModelAttribute AbnormalPagingRequest pagingDto) { return abnormalLogService.findAllAbnormalLogsUnRead(pagingDto); } // 특정 이상 유형으로 필터링 @GetMapping("/type/{abnormalType}") + @Operation(summary = "위험별 로그 조회", description = "페이징 인자를 기준으로 위험별 로그를 요정 개수 만큼 반환해줍니다.") public Page getAbnormalLogsByType( @ModelAttribute AbnormalPagingRequest pagingDto, @PathVariable String abnormalType) { @@ -36,22 +42,25 @@ public Page getAbnormalLogsByType( // 특정 타겟 ID로 필터링 @GetMapping("/target/{targetType}/{targetId}") + @Operation(summary = "유형별 로그 조회", description = "페이징 인자를 기준으로 유형별 로그를 요정 개수 만큼 반환해줍니다.") public Page getAbnormalLogsByTarget( @ModelAttribute AbnormalPagingRequest pagingDto, - @PathVariable String targetType, + @PathVariable TargetType targetType, @PathVariable String targetId) { return abnormalLogService.findAbnormalLogsByTargetId(pagingDto, targetType, targetId); } // 알람 읽음 처리 @PostMapping("/{abnormalId}/read") + @Operation(summary = "로그 읽음 처리", description = "abnormalId와 일치하는 로그를 읽음 처리합니다.") public ResponseEntity markAlarmAsRead(@PathVariable Long abnormalId) { boolean success = abnormalLogService.readCheck(abnormalId); return success ? ResponseEntity.ok().build() : ResponseEntity.notFound().build(); } - + // 웹 페이지 첫 렌더링 시 호출되는 api // 읽지 않은 알람 개수 반환 -> websocket으로 전부 보내주기 @GetMapping("/unread-count") + @Operation(summary = "미확인 로그 개수 조회", description = "미확인 로그 개수를 반환합니다. 페이지이 첫 렌더링 시(웹소켓으로 정보를 받기전) 호출합니다.") public ResponseEntity getUnreadAlarmCount() { Long count = abnormalLogService.readRequired(); return ResponseEntity.ok(count); diff --git a/src/main/java/com/factoreal/backend/domain/abnormalLog/application/AbnormalLogService.java b/src/main/java/com/factoreal/backend/domain/abnormalLog/application/AbnormalLogService.java index f4acddb9..08515f92 100644 --- a/src/main/java/com/factoreal/backend/domain/abnormalLog/application/AbnormalLogService.java +++ b/src/main/java/com/factoreal/backend/domain/abnormalLog/application/AbnormalLogService.java @@ -1,16 +1,20 @@ package com.factoreal.backend.domain.abnormalLog.application; -import com.factoreal.backend.domain.abnormalLog.dto.LogType; +import com.factoreal.backend.domain.abnormalLog.dao.AbnLogRepository; +import com.factoreal.backend.domain.abnormalLog.dto.TargetType; import com.factoreal.backend.domain.abnormalLog.dto.request.AbnormalPagingRequest; import com.factoreal.backend.domain.abnormalLog.dto.response.AbnormalLogResponse; -import com.factoreal.backend.domain.sensor.dto.SensorKafkaDto; import com.factoreal.backend.domain.abnormalLog.entity.AbnormalLog; -import com.factoreal.backend.domain.zone.dao.ZoneRepository; +import com.factoreal.backend.domain.sensor.dto.SensorKafkaDto; +import com.factoreal.backend.domain.zone.application.ZoneHistoryService; +import com.factoreal.backend.domain.zone.application.ZoneService; import com.factoreal.backend.domain.zone.entity.Zone; -import com.factoreal.backend.domain.abnormalLog.dao.AbnLogRepository; +import com.factoreal.backend.domain.zone.entity.ZoneHist; +import com.factoreal.backend.messaging.kafka.dto.WearableKafkaDto; import com.factoreal.backend.messaging.kafka.strategy.alarmMessage.RiskMessageProvider; import com.factoreal.backend.messaging.kafka.strategy.enums.RiskLevel; import com.factoreal.backend.messaging.kafka.strategy.enums.SensorType; +import com.factoreal.backend.messaging.kafka.strategy.enums.WearableDataType; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -23,28 +27,36 @@ import org.springframework.web.server.ResponseStatusException; import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; @Service @Slf4j @RequiredArgsConstructor public class AbnormalLogService { private final AbnLogRepository abnLogRepository; - private final ZoneRepository zoneRepository; + private final ZoneService zoneService; private final RiskMessageProvider riskMessageProvider; private final ObjectMapper objectMapper; - - // 알람 객체를 받아와서 로그 객체 생성. + private final ZoneHistoryService zoneHistoryService; + + /** + * 센서 데이터 기반의 알람 로그 생성. + * @param sensorKafkaDto kafka에서 EQUIPMENT 및 ENVIRONMENT 토픽으로 들어오는 DTO + * @param sensorType 센서 종류: current, dust, temp, humid, vibration, voc + * @param riskLevel 위험 레벨: 시스템 로그에 저장할 메세지를 조회하기 위해 필요(센서별 위험도에 해당되는 메세지) + * @param targetType 타겟 종류 : Sensor(공간), Worker(작업자), Equip(설비-머신러닝) + * @return + * @throws Exception + */ @Transactional(rollbackFor = Exception.class) - public AbnormalLog saveAbnormalLogFromKafkaDto( - SensorKafkaDto sensorKafkaDto, - SensorType sensorType, - RiskLevel riskLevel, - LogType targetType - ) throws Exception{ - - - Zone zone = zoneRepository.findByZoneId(sensorKafkaDto.getZoneId()); - + public AbnormalLog saveAbnormalLogFromSensorKafkaDto( + SensorKafkaDto sensorKafkaDto, + SensorType sensorType, + RiskLevel riskLevel, + TargetType targetType + ) throws Exception{ + Zone zone = zoneService.findByZoneId(sensorKafkaDto.getZoneId()); if (zone == null) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "존재하지 않는 공간 ID: " + sensorKafkaDto.getZoneId()); @@ -58,8 +70,9 @@ public AbnormalLog saveAbnormalLogFromKafkaDto( AbnormalLog abnormalLog = AbnormalLog.builder() .targetId(sensorKafkaDto.getSensorId()) .targetType(targetType) - .abnormalType(riskMessageProvider.getMessage(sensorType,riskLevel)) + .abnormalType(riskMessageProvider.getRiskMessageBySensor(sensorType,riskLevel)) .abnVal(sensorKafkaDto.getVal()) + .dangerLevel(riskLevel.getPriority()) .zone(zone) .detectedAt(LocalDateTime.parse(sensorKafkaDto.getTime())) .isRead(false) @@ -68,63 +81,76 @@ public AbnormalLog saveAbnormalLogFromKafkaDto( return abnLogRepository.save(abnormalLog); } + /** + * 센서 데이터 기반의 알람 로그 생성. + * @param wearableKafkaDto kafka에서 WEARABLE 토픽으로 들어오는 DTO + * @param wearableDataType 생체 데이터 종류: 현재는 heartRate 만 보내는 중(확장성 고려해서 해당 객체 사용) + * @param riskLevel 위험 레벨: 시스템 로그에 저장할 메세지를 조회하기 위해 필요(생체 데이터 별 위험도에 해당되는 메세지) + * @param targetType 타겟 종류 : Sensor(공간), Worker(작업자), Equip(설비-머신러닝) + * @return + */ + @Transactional(rollbackFor = Exception.class) + public AbnormalLog saveAbnormalLogFromWearableKafkaDto( + WearableKafkaDto wearableKafkaDto, + WearableDataType wearableDataType, + RiskLevel riskLevel, + TargetType targetType + ){ + // workerId에 해당되는 사람이 제일 최근에 있던 공간 조회 + ZoneHist zonehist = zoneHistoryService. + findByWorker_WorkerIdAndExistFlag(wearableKafkaDto.getWorkerId(), 1); + Zone zone; + if (zonehist == null){ + zone = zoneService.findByZoneId("00000000000000-000"); + }else{ + zone = zonehist.getZone(); + } + + AbnormalLog abnormalLog = AbnormalLog.builder() + .targetId(wearableKafkaDto.getWearableDeviceId()) + .targetType(targetType) + .abnormalType(riskMessageProvider.getRiskMessageByWearble(wearableDataType,riskLevel)) + .abnVal(Double.valueOf(wearableKafkaDto.getVal())) + .detectedAt(LocalDateTime.parse(wearableKafkaDto.getTime())) + .dangerLevel(riskLevel.getPriority()) + .zone(zone) + .isRead(false) + .build(); + return abnLogRepository.save(abnormalLog); + } public Page findAllAbnormalLogs(AbnormalPagingRequest abnormalPagingDto) { // 한번에 DB전체를 주는 것이 아닌 구간 나눠서 전달하기 위함 Pageable pageable = getPageable(abnormalPagingDto); Page abnormalLogs = abnLogRepository.findAll(pageable); - return abnormalLogs.map(abn_log -> - AbnormalLogResponse.builder() - .id(abn_log.getId()) - .targetType(abn_log.getTargetType()) - .targetId(abn_log.getTargetId()) - .abnormalType(abn_log.getAbnormalType()) - .abnVal(abn_log.getAbnVal()) - .detectedAt(abn_log.getDetectedAt()) - .zoneId(abn_log.getZone().getZoneId()) - .zoneName(abn_log.getZone().getZoneName()) - .build() - ); + return abnormalLogs.map(AbnormalLog::fromEntity); } public Page findAllAbnormalLogsUnRead(AbnormalPagingRequest abnormalPagingRequest) { // 한번에 DB전체를 주는 것이 아닌 구간 나눠서 전달하기 위함 Pageable pageable = getPageable(abnormalPagingRequest); - Page abnormalLogs = abnLogRepository.findAllByIsReadIsFalse(pageable); - return abnormalLogs.map(abn_log -> - AbnormalLogResponse.builder() - .id(abn_log.getId()) - .targetType(abn_log.getTargetType()) - .targetId(abn_log.getTargetId()) - .abnormalType(abn_log.getAbnormalType()) - .abnVal(abn_log.getAbnVal()) - .detectedAt(abn_log.getDetectedAt()) - .zoneId(abn_log.getZone().getZoneId()) - .zoneName(abn_log.getZone().getZoneName()) - .build() - ); + Page abnormalLogs = abnLogRepository.findAllByIsReadIsFalseOrderByDetectedAtDesc(pageable); + return abnormalLogs.map(AbnormalLog::fromEntity); } public Page findAbnormalLogsByAbnormalType(AbnormalPagingRequest abnormalPagingRequest, String abnormalType){ // 한번에 DB전체를 주는 것이 아닌 구간 나눠서 전달하기 위함 Pageable pageable = getPageable(abnormalPagingRequest); Page abnormalLogs = abnLogRepository.findAbnormalLogsByAbnormalType(abnormalType,pageable); - return abnormalLogs.map(abn_log -> - AbnormalLogResponse.builder() - .id(abn_log.getId()) - .targetType(abn_log.getTargetType()) - .targetId(abn_log.getTargetId()) - .abnormalType(abn_log.getAbnormalType()) - .abnVal(abn_log.getAbnVal()) - .detectedAt(abn_log.getDetectedAt()) - .zoneId(abn_log.getZone().getZoneId()) - .zoneName(abn_log.getZone().getZoneName()) - .build() - ); + return abnormalLogs.map(AbnormalLog::fromEntity); } - // - public Page findAbnormalLogsByTargetId(AbnormalPagingRequest abnormalPagingRequest, String targetType, String targetId){ + public List findLatestAbnormalLogsForTargets(TargetType targetType, List targetIds) { + return targetIds.stream() + .map(targetId -> + abnLogRepository.findFirstByTargetTypeAndTargetIdOrderByDetectedAtDesc(targetType, targetId) + .map(AbnormalLog::fromEntity) // 또는 objectMapper.convertValue(...) + .orElse(null) // 값이 없으면 null + ) + .filter(Objects::nonNull) // null 제거 + .toList(); + } + public Page findAbnormalLogsByTargetId(AbnormalPagingRequest abnormalPagingRequest, TargetType targetType, String targetId){ // 한번에 DB전체를 주는 것이 아닌 구간 나눠서 전달하기 위함 Pageable pageable = getPageable(abnormalPagingRequest); Page abnormalLogs = abnLogRepository.findAbnormalLogsByTargetTypeAndTargetId( diff --git a/src/main/java/com/factoreal/backend/domain/abnormalLog/dao/AbnLogRepository.java b/src/main/java/com/factoreal/backend/domain/abnormalLog/dao/AbnLogRepository.java index 04b39da9..5323fc5f 100644 --- a/src/main/java/com/factoreal/backend/domain/abnormalLog/dao/AbnLogRepository.java +++ b/src/main/java/com/factoreal/backend/domain/abnormalLog/dao/AbnLogRepository.java @@ -1,17 +1,24 @@ package com.factoreal.backend.domain.abnormalLog.dao; +import com.factoreal.backend.domain.abnormalLog.dto.TargetType; import com.factoreal.backend.domain.abnormalLog.entity.AbnormalLog; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface AbnLogRepository extends JpaRepository { Page findAbnormalLogsByAbnormalType(String abnormalType, Pageable pageable); - Page findAbnormalLogsByTargetTypeAndTargetId(String targetType, String targetId, Pageable pageable); + Page findAbnormalLogsByTargetTypeAndTargetId(TargetType targetType, String targetId, Pageable pageable); + // Pageable 객체 없이도 사용할 수 있도록 오버라이딩 + Optional findFirstByTargetTypeAndTargetIdOrderByDetectedAtDesc(TargetType targetType, String targetId); + + long countByIsReadFalse(); // 읽지 않은 로그의 개수 반환 - Page findAllByIsReadIsFalse(Pageable pageable); + Page findAllByIsReadIsFalseOrderByDetectedAtDesc(Pageable pageable); // zoneId로 페이징 처리된 로그 조회 Page findByZone_ZoneIdOrderByDetectedAtDesc(String zoneId, Pageable pageable); diff --git a/src/main/java/com/factoreal/backend/domain/abnormalLog/dto/LogType.java b/src/main/java/com/factoreal/backend/domain/abnormalLog/dto/TargetType.java similarity index 78% rename from src/main/java/com/factoreal/backend/domain/abnormalLog/dto/LogType.java rename to src/main/java/com/factoreal/backend/domain/abnormalLog/dto/TargetType.java index d79e6efc..101a9a02 100644 --- a/src/main/java/com/factoreal/backend/domain/abnormalLog/dto/LogType.java +++ b/src/main/java/com/factoreal/backend/domain/abnormalLog/dto/TargetType.java @@ -1,6 +1,6 @@ package com.factoreal.backend.domain.abnormalLog.dto; -public enum LogType { +public enum TargetType { Sensor, Worker, Equip diff --git a/src/main/java/com/factoreal/backend/domain/abnormalLog/dto/response/AbnormalLogResponse.java b/src/main/java/com/factoreal/backend/domain/abnormalLog/dto/response/AbnormalLogResponse.java index 209f88f1..241974bc 100644 --- a/src/main/java/com/factoreal/backend/domain/abnormalLog/dto/response/AbnormalLogResponse.java +++ b/src/main/java/com/factoreal/backend/domain/abnormalLog/dto/response/AbnormalLogResponse.java @@ -1,6 +1,6 @@ package com.factoreal.backend.domain.abnormalLog.dto.response; -import com.factoreal.backend.domain.abnormalLog.dto.LogType; +import com.factoreal.backend.domain.abnormalLog.dto.TargetType; import lombok.Builder; import lombok.Data; @@ -10,10 +10,11 @@ @Builder public class AbnormalLogResponse { private Long id; - private LogType targetType; + private TargetType targetType; private String targetId; private String abnormalType; private Double abnVal; + private Integer dangerLevel; private LocalDateTime detectedAt; private String zoneId; // zone 정보를 자세히 담고 싶으면 zoneName 등 추가 가능 private String zoneName; // 예: "공장 A의 2번 존" diff --git a/src/main/java/com/factoreal/backend/domain/abnormalLog/entity/AbnormalLog.java b/src/main/java/com/factoreal/backend/domain/abnormalLog/entity/AbnormalLog.java index a1918ae5..c32778fe 100644 --- a/src/main/java/com/factoreal/backend/domain/abnormalLog/entity/AbnormalLog.java +++ b/src/main/java/com/factoreal/backend/domain/abnormalLog/entity/AbnormalLog.java @@ -1,7 +1,8 @@ package com.factoreal.backend.domain.abnormalLog.entity; +import com.factoreal.backend.domain.abnormalLog.dto.response.AbnormalLogResponse; import com.factoreal.backend.domain.zone.entity.Zone; -import com.factoreal.backend.domain.abnormalLog.dto.LogType; +import com.factoreal.backend.domain.abnormalLog.dto.TargetType; import jakarta.persistence.*; import lombok.*; import org.springframework.data.annotation.CreatedDate; @@ -24,7 +25,7 @@ public class AbnormalLog { @Enumerated(EnumType.STRING) @Column(name = "target_type", length = 50) - private LogType targetType; // 구분 분류 : Sensor(공간- 012 rule-base), Worker, Equip(설비-머신러닝) 구분 + private TargetType targetType; // 구분 분류 : Sensor(공간- 012 rule-base), Worker, Equip(설비-머신러닝) 구분 @Column(name = "target_id", length = 100) private String targetId; // 고유 ID : 센서ID, WorkerID, EquipID @@ -36,6 +37,9 @@ public class AbnormalLog { @Column(name = "abn_val") private Double abnVal; // 이상치 값 + @Column(name = "danger_level") + private Integer dangerLevel; + @Column(name = "detected_at") @CreatedDate private LocalDateTime detectedAt; // 이상 감지 시간 @@ -48,4 +52,17 @@ public class AbnormalLog { @Column(name = "is_read") @Builder.Default private Boolean isRead = false; + + public AbnormalLogResponse fromEntity() { + return AbnormalLogResponse.builder() + .id(this.getId()) + .targetType(this.getTargetType()) + .targetId(this.getTargetId()) + .abnormalType(this.getAbnormalType()) + .abnVal(this.getAbnVal()) + .detectedAt(this.getDetectedAt()) + .zoneId(this.getZone().getZoneId()) + .zoneName(this.getZone().getZoneName()) + .build(); + } } diff --git a/src/main/java/com/factoreal/backend/domain/notifyLog/api/NotificationController.java b/src/main/java/com/factoreal/backend/domain/notifyLog/api/NotificationController.java index a9e4d49c..44a85dc5 100644 --- a/src/main/java/com/factoreal/backend/domain/notifyLog/api/NotificationController.java +++ b/src/main/java/com/factoreal/backend/domain/notifyLog/api/NotificationController.java @@ -1,28 +1,43 @@ package com.factoreal.backend.domain.notifyLog.api; +import com.factoreal.backend.messaging.kafka.strategy.enums.AlarmEventDto; +import com.factoreal.backend.messaging.sender.WebSocketSender; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.messaging.simp.SimpMessagingTemplate; 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; +import java.time.LocalDateTime; +import java.util.Date; import java.util.Map; @RestController +@RequestMapping("/api/notify") +@Tag(name = "알람 직접 발생 API", description = "작업자 호출(FCM), 문자(aws 리전으로 SMS 지원안되서 Slack으로 대체)을 위한 API") @RequiredArgsConstructor public class NotificationController { - private final SimpMessagingTemplate simpMessagingTemplate; + private final WebSocketSender webSocketSender; // 웹소켓 테스트용 사이트 // 1. 스프링 실행후 아래사이트에서 http://localhost:8080/websocket 으로 연결 (sockjs,stomp 체크) // 2. /topic/notify 구독 // 3. swagger에서 아래 api 호출 // https://jiangxy.github.io/websocket-debug-tool/ - @PostMapping("/api/notify") - @Deprecated - public ResponseEntity notify(@RequestBody Map body) { - String message = body.get("message"); - simpMessagingTemplate.convertAndSend("/topic/notify", message); + @PostMapping("/test") + @Operation(summary = "테스트용", description = "화면이 없을 때 사용한 웹소켓 테스트용 api - 일괄 삭제 예정") + public ResponseEntity notify(@RequestBody AlarmEventDto alarmEventDto) { + alarmEventDto.setTime(LocalDateTime.now().toString()); + webSocketSender.sendDangerAlarm(alarmEventDto); return ResponseEntity.ok("Message sent"); } + + @PostMapping("/send/fcm") + @Operation(summary = "FCM 전송", description = "WokerId에 해당되는 FCm토큰을 조회하여 전송") + public ResponseEntity sendFcm() { + return ResponseEntity.ok("FCM 전송"); + } } diff --git a/src/main/java/com/factoreal/backend/domain/worker/api/WorkerController.java b/src/main/java/com/factoreal/backend/domain/worker/api/WorkerController.java index 20c2b97c..6b48d3b6 100644 --- a/src/main/java/com/factoreal/backend/domain/worker/api/WorkerController.java +++ b/src/main/java/com/factoreal/backend/domain/worker/api/WorkerController.java @@ -1,14 +1,17 @@ package com.factoreal.backend.domain.worker.api; +import com.factoreal.backend.domain.worker.application.WorkerService; +import com.factoreal.backend.domain.worker.dto.response.WorkerDetailResponse; import com.factoreal.backend.domain.worker.dto.response.WorkerInfoResponse; import com.factoreal.backend.domain.worker.dto.response.ZoneManagerResponse; -import com.factoreal.backend.domain.worker.application.WorkerService; - import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.*; +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 java.util.List; @@ -22,7 +25,7 @@ public class WorkerController { @Operation(summary = "전체 작업자 목록 조회", description = "전체 작업자 목록을 조회합니다.") @GetMapping - public List getAllWorkers() { + public List getAllWorkers() { log.info("전체 작업자 목록 조회 요청"); return workerService.getAllWorkers(); } diff --git a/src/main/java/com/factoreal/backend/domain/worker/application/WorkerService.java b/src/main/java/com/factoreal/backend/domain/worker/application/WorkerService.java index 452f52ce..b368109a 100644 --- a/src/main/java/com/factoreal/backend/domain/worker/application/WorkerService.java +++ b/src/main/java/com/factoreal/backend/domain/worker/application/WorkerService.java @@ -1,5 +1,9 @@ package com.factoreal.backend.domain.worker.application; +import com.factoreal.backend.domain.abnormalLog.application.AbnormalLogService; +import com.factoreal.backend.domain.abnormalLog.dto.TargetType; +import com.factoreal.backend.domain.abnormalLog.dto.response.AbnormalLogResponse; +import com.factoreal.backend.domain.worker.dto.response.WorkerDetailResponse; import com.factoreal.backend.domain.worker.dto.response.WorkerInfoResponse; import com.factoreal.backend.domain.worker.dto.response.ZoneManagerResponse; import com.factoreal.backend.domain.zone.dao.ZoneHistoryRepository; @@ -15,6 +19,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @Slf4j @@ -24,14 +29,47 @@ public class WorkerService { private final WorkerRepository workerRepository; private final ZoneHistoryRepository zoneHistoryRepository; private final WorkerZoneRepository workerZoneRepository; - + private final AbnormalLogService abnormalLogService; @Transactional(readOnly = true) - public List getAllWorkers() { + public List getAllWorkers() { log.info("전체 작업자 목록 조회"); List workers = workerRepository.findAll(); + // workerId 목록 + List workerIds = workers.stream() + .map(Worker::getWorkerId) + .toList(); + + // AbnormalLog 에서 작업자 상태 조회 + List statusList = abnormalLogService. + findLatestAbnormalLogsForTargets(TargetType.Worker,workerIds); + // HistZone에서 작업자 위치 조회 + List zoneHistsList = workerIds.stream() + .map(workerId -> zoneHistoryRepository.findByWorker_WorkerIdAndExistFlag(workerId,1)) + .toList(); + + // 상태 Map + Map statusMap = statusList.stream() + .collect(Collectors.toMap(AbnormalLogResponse::getTargetId, AbnormalLogResponse::getDangerLevel)); + + // 위치 Map + Map zoneMap = workerIds.stream() + .collect(Collectors.toMap( + workerId -> workerId, + workerId -> { + ZoneHist zh = zoneHistoryRepository.findByWorker_WorkerIdAndExistFlag(workerId, 1); + if (zh == null || zh.getZone() == null) { + return "대기실"; // 기본 ZoneId + } + return zh.getZone().getZoneName(); // zoneName이 아니라 zoneId로 변경 + } + )); return workers.stream() - .map(worker -> WorkerInfoResponse.from(worker, false)) - .collect(Collectors.toList()); + .map(worker -> { + Integer status = statusMap.getOrDefault(worker.getWorkerId(), 0); // 기본값 예: 정상 + String zone = zoneMap.getOrDefault(worker.getWorkerId(), "대기실"); + return WorkerDetailResponse.from(worker, false, status.toString(), zone); + }) + .collect(Collectors.toList()); } /** @@ -66,6 +104,23 @@ public ZoneManagerResponse getZoneManagerWithLocation(String zoneId) { return ZoneManagerResponse.from(manager, currentZone); } + /** + * workerId에 해당하는 작업자 조회 + */ + @Transactional(readOnly = true) + public Worker getWorkerByWorkerId(String workerId) { + return workerRepository.findById(workerId).orElseThrow(); + } + + /** + * FCM 발송용 토큰을 추가하기 위한 메서드 + * @param worker + * @return + */ + @Transactional + public Worker saveWorker(Worker worker) { + return workerRepository.save(worker); + } } // TODO. 수정되어야 할 로직. 현재는 WorkerZone 테이블에서 공간id로 필터링 되는 모든 작업자를 끌고왔는데, diff --git a/src/main/java/com/factoreal/backend/domain/worker/dto/response/WorkerDetailResponse.java b/src/main/java/com/factoreal/backend/domain/worker/dto/response/WorkerDetailResponse.java new file mode 100644 index 00000000..fb90c905 --- /dev/null +++ b/src/main/java/com/factoreal/backend/domain/worker/dto/response/WorkerDetailResponse.java @@ -0,0 +1,29 @@ +package com.factoreal.backend.domain.worker.dto.response; + +import com.factoreal.backend.domain.worker.entity.Worker; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@SuperBuilder +public class WorkerDetailResponse extends WorkerInfoResponse{ + // 작업자의 위치와 상태 추가 + private String status; + private String zone; + public static WorkerDetailResponse from(Worker worker, Boolean isManager, String status, String zone) { + return WorkerDetailResponse.builder() + .workerId(worker.getWorkerId()) + .name(worker.getName()) + .phoneNumber(worker.getPhoneNumber()) + .email(worker.getEmail()) + .fcmToken(worker.getFcmToken()) + .status(status) + .zone(zone) + .isManager(isManager) + .build(); + } +} diff --git a/src/main/java/com/factoreal/backend/domain/worker/dto/response/WorkerInfoResponse.java b/src/main/java/com/factoreal/backend/domain/worker/dto/response/WorkerInfoResponse.java index 1c0a7f29..fd0e71cf 100644 --- a/src/main/java/com/factoreal/backend/domain/worker/dto/response/WorkerInfoResponse.java +++ b/src/main/java/com/factoreal/backend/domain/worker/dto/response/WorkerInfoResponse.java @@ -2,19 +2,20 @@ import com.factoreal.backend.domain.worker.entity.Worker; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; @Getter @AllArgsConstructor @NoArgsConstructor -@Builder +@SuperBuilder public class WorkerInfoResponse { private String workerId; private String name; private String phoneNumber; private String email; + private String fcmToken; private Boolean isManager; public static WorkerInfoResponse from(Worker worker, Boolean isManager) { @@ -23,6 +24,7 @@ public static WorkerInfoResponse from(Worker worker, Boolean isManager) { .name(worker.getName()) .phoneNumber(worker.getPhoneNumber()) .email(worker.getEmail()) + .fcmToken(worker.getFcmToken()) .isManager(isManager) .build(); } diff --git a/src/main/java/com/factoreal/backend/domain/worker/dto/response/ZoneManagerResponse.java b/src/main/java/com/factoreal/backend/domain/worker/dto/response/ZoneManagerResponse.java index 32a713c3..c64f7887 100644 --- a/src/main/java/com/factoreal/backend/domain/worker/dto/response/ZoneManagerResponse.java +++ b/src/main/java/com/factoreal/backend/domain/worker/dto/response/ZoneManagerResponse.java @@ -3,12 +3,12 @@ import com.factoreal.backend.domain.worker.entity.Worker; import com.factoreal.backend.domain.zone.entity.Zone; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; @Getter -@Builder +@SuperBuilder @NoArgsConstructor @AllArgsConstructor public class ZoneManagerResponse { diff --git a/src/main/java/com/factoreal/backend/domain/worker/dto/response/ZoneManagerResponseDto.java b/src/main/java/com/factoreal/backend/domain/worker/dto/response/ZoneManagerResponseDto.java new file mode 100644 index 00000000..6c655707 --- /dev/null +++ b/src/main/java/com/factoreal/backend/domain/worker/dto/response/ZoneManagerResponseDto.java @@ -0,0 +1,29 @@ +package com.factoreal.backend.domain.worker.dto.response; + +import com.factoreal.backend.domain.worker.entity.Worker; +import com.factoreal.backend.domain.zone.entity.Zone; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class ZoneManagerResponseDto extends ZoneManagerResponse{ + private String status; // 공간 담당자의 현재 상태 + private String zone; // 공간 담당자의 현재 위치 + public static ZoneManagerResponseDto from(Worker worker, Zone currentZone, String status, String zone) { + return ZoneManagerResponseDto.builder() + .workerId(worker.getWorkerId()) + .name(worker.getName()) + .phoneNumber(worker.getPhoneNumber()) + .email(worker.getEmail()) + .currentZoneId(currentZone != null ? currentZone.getZoneId() : null) + .currentZoneName(currentZone != null ? currentZone.getZoneName() : null) + .status(status) + .zone(zone) + .build(); + } +} diff --git a/src/main/java/com/factoreal/backend/domain/worker/entity/Worker.java b/src/main/java/com/factoreal/backend/domain/worker/entity/Worker.java index 3c42e417..e424dcf9 100644 --- a/src/main/java/com/factoreal/backend/domain/worker/entity/Worker.java +++ b/src/main/java/com/factoreal/backend/domain/worker/entity/Worker.java @@ -24,4 +24,7 @@ public class Worker { @Column(name = "email", length = 100) private String email; + + @Column(name = "fcm_token", length = 200) + private String fcmToken; } \ No newline at end of file diff --git a/src/main/java/com/factoreal/backend/domain/zone/application/ZoneHistoryService.java b/src/main/java/com/factoreal/backend/domain/zone/application/ZoneHistoryService.java index 9ae17a76..c40eec68 100644 --- a/src/main/java/com/factoreal/backend/domain/zone/application/ZoneHistoryService.java +++ b/src/main/java/com/factoreal/backend/domain/zone/application/ZoneHistoryService.java @@ -60,4 +60,10 @@ public void updateWorkerLocation(String workerId, String zoneId, LocalDateTime t currentLocation != null ? currentLocation.getZone().getZoneId() : "없음", zoneId); } + + public ZoneHist findByWorker_WorkerIdAndExistFlag(String workerid, int existFlag) { + return zoneHistRepository.findByWorker_WorkerIdAndExistFlag(workerid,existFlag); + } + + } \ No newline at end of file diff --git a/src/main/java/com/factoreal/backend/domain/zone/application/ZoneService.java b/src/main/java/com/factoreal/backend/domain/zone/application/ZoneService.java index 0c08699b..07566321 100644 --- a/src/main/java/com/factoreal/backend/domain/zone/application/ZoneService.java +++ b/src/main/java/com/factoreal/backend/domain/zone/application/ZoneService.java @@ -74,7 +74,9 @@ public List getAllZones() { .map(ZoneInfoResponse::from) .collect(Collectors.toList()); } - + public Zone findByZoneId(String zoneId) { + return zoneRepository.findByZoneId(zoneId); + } @Transactional public List getZoneItems() { diff --git a/src/main/java/com/factoreal/backend/domain/zone/dto/response/ZoneLogResponse.java b/src/main/java/com/factoreal/backend/domain/zone/dto/response/ZoneLogResponse.java index 2246a303..621ed7a0 100644 --- a/src/main/java/com/factoreal/backend/domain/zone/dto/response/ZoneLogResponse.java +++ b/src/main/java/com/factoreal/backend/domain/zone/dto/response/ZoneLogResponse.java @@ -1,6 +1,6 @@ package com.factoreal.backend.domain.zone.dto.response; -import com.factoreal.backend.domain.abnormalLog.dto.LogType; +import com.factoreal.backend.domain.abnormalLog.dto.TargetType; import com.factoreal.backend.domain.abnormalLog.entity.AbnormalLog; import lombok.AllArgsConstructor; import lombok.Builder; @@ -36,8 +36,8 @@ public static ZoneLogResponse from (AbnormalLog abnormalLog) { .build(); } - private static String convertLogTypeToKorean(LogType logType) { - return switch (logType) { + private static String convertLogTypeToKorean(TargetType targetType) { + return switch (targetType) { case Sensor -> "환경"; case Worker -> "작업자"; case Equip -> "설비"; diff --git a/src/main/java/com/factoreal/backend/domain/zone/entity/ZoneHist.java b/src/main/java/com/factoreal/backend/domain/zone/entity/ZoneHist.java index 1ce251c1..ac6ac76a 100644 --- a/src/main/java/com/factoreal/backend/domain/zone/entity/ZoneHist.java +++ b/src/main/java/com/factoreal/backend/domain/zone/entity/ZoneHist.java @@ -17,6 +17,7 @@ public class ZoneHist { @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) private Long id; // ID diff --git a/src/main/java/com/factoreal/backend/global/common/response/CommonResponseAdvice.java b/src/main/java/com/factoreal/backend/global/common/response/CommonResponseAdvice.java index b879b54a..f41a7035 100644 --- a/src/main/java/com/factoreal/backend/global/common/response/CommonResponseAdvice.java +++ b/src/main/java/com/factoreal/backend/global/common/response/CommonResponseAdvice.java @@ -2,6 +2,8 @@ import jakarta.servlet.http.HttpServletResponse; import org.springframework.core.MethodParameter; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.core.io.Resource; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -11,31 +13,42 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + @RestControllerAdvice(basePackages = "com.factoreal.backend") -public class CommonResponseAdvice implements ResponseBodyAdvice { +public class CommonResponseAdvice implements ResponseBodyAdvice { + @Override public boolean supports(MethodParameter returnType, Class converterType) { - return true; // 어떤 응답을 가로채서 반환할 것인지 -> 모든 응답 가로채기 + String pkgName = returnType.getDeclaringClass().getPackageName(); + if (pkgName.startsWith("org.springdoc") || pkgName.contains("swagger") || pkgName.contains("springfox")) { + return false; + } + return true; } @Override public Object beforeBodyWrite( - Object body, - MethodParameter returnType, - MediaType selectedContentType, - Class selectedConverterType, - ServerHttpRequest request, - ServerHttpResponse response) { - HttpServletResponse httpServletResponse = - ((ServletServerHttpResponse) response).getServletResponse(); - int status = httpServletResponse.getStatus(); - HttpStatus resolve = HttpStatus.resolve(status); - - if (resolve == null || body instanceof String || body instanceof Resource) { + Object body, + MethodParameter returnType, + MediaType selectedContentType, + Class selectedConverterType, + ServerHttpRequest request, + ServerHttpResponse response) { + + String uri = request.getURI().toString(); + if (uri.contains("/v3/api-docs") || uri.contains("/swagger") || uri.contains("/swagger-ui")) { return body; } - if (resolve.is2xxSuccessful()) { + if (body instanceof String || body instanceof Resource) { + return body; + } + + HttpServletResponse servletResponse = ((ServletServerHttpResponse) response).getServletResponse(); + int status = servletResponse.getStatus(); + HttpStatus httpStatus = HttpStatus.resolve(status); + + if (httpStatus != null && httpStatus.is2xxSuccessful()) { return CommonResponse.onSuccess(status, body); } diff --git a/src/main/java/com/factoreal/backend/messaging/config/FirebaseConfig.java b/src/main/java/com/factoreal/backend/messaging/config/FirebaseConfig.java new file mode 100644 index 00000000..ccf83f73 --- /dev/null +++ b/src/main/java/com/factoreal/backend/messaging/config/FirebaseConfig.java @@ -0,0 +1,38 @@ +package com.factoreal.backend.messaging.config; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; + +import java.io.IOException; +import java.io.InputStream; + + +// 아래 블로그를 참고하여 구현 +// https://velog.io/@joeun-01/Spring-Boot-FCM%EC%9C%BC%EB%A1%9C-%ED%91%B8%EC%8B%9C-%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0 +@Configuration +public class FirebaseConfig { + + // FCM 푸시 알림 객체를 싱글톤으로 사용 + @Bean + public FirebaseMessaging firebaseMessaging(){ + try{ + InputStream serviceAccount = new ClassPathResource("fcm_root_key/monitory-28aad-firebase-adminsdk-fbsvc-0d7886e5f5.json").getInputStream(); + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(serviceAccount)) + .build(); + + if(FirebaseApp.getApps().isEmpty()) { + FirebaseApp.initializeApp(options); + } + + return FirebaseMessaging.getInstance(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/factoreal/backend/messaging/fcm/api/FCMController.java b/src/main/java/com/factoreal/backend/messaging/fcm/api/FCMController.java new file mode 100644 index 00000000..ebb0f475 --- /dev/null +++ b/src/main/java/com/factoreal/backend/messaging/fcm/api/FCMController.java @@ -0,0 +1,33 @@ +package com.factoreal.backend.messaging.fcm.api; + +import com.factoreal.backend.messaging.fcm.dto.FCMTokenRegistDto; +import com.factoreal.backend.messaging.fcm.service.FCMService; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +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; + +@RestController +@RequestMapping("/api/fcm") +@Tag(name = "모바일 앱 FCm 등록용 API",description = "모바일 앱에서 FCM 수신용 토큰을 등록하는 API") +@Slf4j +@RequiredArgsConstructor +public class FCMController { + private final FCMService fcmService; + @PostMapping + public ResponseEntity sendMessage(@RequestBody FCMTokenRegistDto message) { + try{ + String response = fcmService.saveToken(message.getWorkerId(),message.getToken()); + if(response.equals(message.getToken())){ + return ResponseEntity.ok().body(response); + } + return ResponseEntity.badRequest().body(response); + }catch(Exception e){ + return ResponseEntity.badRequest().body(e.getMessage()); + } + } +} diff --git a/src/main/java/com/factoreal/backend/messaging/fcm/dto/FCMTokenRegistDto.java b/src/main/java/com/factoreal/backend/messaging/fcm/dto/FCMTokenRegistDto.java new file mode 100644 index 00000000..9ddc3865 --- /dev/null +++ b/src/main/java/com/factoreal/backend/messaging/fcm/dto/FCMTokenRegistDto.java @@ -0,0 +1,15 @@ +package com.factoreal.backend.messaging.fcm.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; + +@Getter +public class FCMTokenRegistDto { + @NotBlank(message="토큰을 입력해야 합니다.") + @Schema(description = "FCM Token", example = "") + String token; + @NotBlank(message="작업자ID를 입력해야 합니다.") + @Schema(description = "Worker Id", example = "") + String workerId; +} diff --git a/src/main/java/com/factoreal/backend/messaging/fcm/service/FCMService.java b/src/main/java/com/factoreal/backend/messaging/fcm/service/FCMService.java new file mode 100644 index 00000000..6f19fffd --- /dev/null +++ b/src/main/java/com/factoreal/backend/messaging/fcm/service/FCMService.java @@ -0,0 +1,44 @@ +package com.factoreal.backend.messaging.fcm.service; + +import com.factoreal.backend.domain.worker.application.WorkerService; +import com.factoreal.backend.domain.worker.entity.Worker; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.Notification; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Slf4j +@RequiredArgsConstructor +public class FCMService { + private final FirebaseMessaging firebaseMessaging; + private final WorkerService workerService; + + @Transactional + public String saveToken(String workerId, String token) throws Exception { + Worker worker = workerService.getWorkerByWorkerId(workerId); + if (worker == null) { + throw new Exception("Worker not found"); + } + worker.setFcmToken(token); + return workerService.saveWorker(worker).getFcmToken(); + } + + public void sendMessage(String token, String title, String body) throws Exception { + String message = firebaseMessaging.send( + Message.builder() + .setNotification( + Notification.builder() + .setTitle(title) + .setBody(body) + .build() + ) + .setToken(token) + .build() + ); + log.info("Message sent to firebase: {}",message); + } +} diff --git a/src/main/java/com/factoreal/backend/messaging/kafka/KafkaConsumerD.java b/src/main/java/com/factoreal/backend/messaging/kafka/KafkaConsumerD.java index a7427e1c..3bfc078a 100644 --- a/src/main/java/com/factoreal/backend/messaging/kafka/KafkaConsumerD.java +++ b/src/main/java/com/factoreal/backend/messaging/kafka/KafkaConsumerD.java @@ -1,7 +1,7 @@ package com.factoreal.backend.messaging.kafka; import com.factoreal.backend.domain.sensor.dto.SensorKafkaDto; -import com.factoreal.backend.domain.abnormalLog.dto.LogType; +import com.factoreal.backend.domain.abnormalLog.dto.TargetType; import com.factoreal.backend.domain.abnormalLog.entity.AbnormalLog; import com.factoreal.backend.domain.zone.dao.ZoneRepository; import com.factoreal.backend.messaging.common.dto.SystemLogDto; @@ -85,11 +85,11 @@ public void consume(String message) { log.error("SensorType not found"); throw new Exception("SensorType not found"); } - AbnormalLog abnormalLog = abnormalLogService.saveAbnormalLogFromKafkaDto( + AbnormalLog abnormalLog = abnormalLogService.saveAbnormalLogFromSensorKafkaDto( dto, sensorType, riskLevel, - LogType.Sensor); + TargetType.Sensor); // ################################# // 웹 앱 SMS 알람 로직 diff --git a/src/main/java/com/factoreal/backend/messaging/kafka/consumer/KafkaConsumer.java b/src/main/java/com/factoreal/backend/messaging/kafka/consumer/KafkaConsumer.java index 4e888aff..a869a3b5 100644 --- a/src/main/java/com/factoreal/backend/messaging/kafka/consumer/KafkaConsumer.java +++ b/src/main/java/com/factoreal/backend/messaging/kafka/consumer/KafkaConsumer.java @@ -2,6 +2,9 @@ import com.factoreal.backend.domain.sensor.dto.SensorKafkaDto; import com.factoreal.backend.messaging.kafka.processor.SensorEventProcessor; +import com.factoreal.backend.messaging.kafka.dto.WearableKafkaDto; +import com.factoreal.backend.messaging.kafka.processor.WearableEventProcessor; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -20,6 +23,7 @@ public class KafkaConsumer { private final ObjectMapper objectMapper; private final SensorEventProcessor sensorEventProcessor; + private final WearableEventProcessor wearableEventProcessor; // 설비 센서 관련 Kafka 메시지 처리 // Todo : 설비 머신러닝 끝나고 수정 예정 @@ -30,12 +34,17 @@ public void consumeEquipment(String message) { } // 공간 센서 관련 Kafka 메시지 처리 - @KafkaListener(topics = "ENVIRONMENT", groupId = "environment-consumer-group") + @KafkaListener(topics = "ENVIRONMENT", groupId = "environment-consumer-group-kwy") public void consumeEnvironment(String message) { log.info("📩 [ENVIRONMENT] Kafka 메시지 수신: {}", message); handleMessage(message, "ENVIRONMENT"); } + @KafkaListener(topics = "WEARABLE", groupId = "environment-consumer-group-kwy") + public void consumeWearable(String message) { + log.info("📩 [WEARABLE] Kafka 메시지 수신: {}", message); + handleWearableMessage(message, "WEARABLE"); + } // 공통 메시지 파싱 및 처리 전달 private void handleMessage(String message, String topic) { try { @@ -48,5 +57,17 @@ private void handleMessage(String message, String topic) { log.error("❌ Kafka 메시지 파싱 또는 처리 실패: {}", message, e); } } + // 웨어러블 센서 데이터 파싱 및 처리 전달 + private void handleWearableMessage(String message, String topic) { + try{ + WearableKafkaDto dto = objectMapper.readValue(message, WearableKafkaDto.class); + log.info("✅ Kafka 메시지 파싱 완료: deviceId={}, workerId={}, status={}, heartRate={}", + dto.getWearableDeviceId(),dto.getWorkerId(),dto.getDangerLevel(),dto.getVal()); + wearableEventProcessor.process(dto, topic); // 어차피 WEARABLE 토픽만 구독하지만 일관성위해 넣었음. + log.info("✅ Kafka 메시지 처리 위임 완료: topic={}", topic); + }catch (JsonProcessingException e){ + log.error("❌ Kafka 메시지 파싱 또는 처리 실패: {}", message, e); + } + } } \ No newline at end of file diff --git a/src/main/java/com/factoreal/backend/messaging/kafka/dto/WearableKafkaDto.java b/src/main/java/com/factoreal/backend/messaging/kafka/dto/WearableKafkaDto.java new file mode 100644 index 00000000..029c6b74 --- /dev/null +++ b/src/main/java/com/factoreal/backend/messaging/kafka/dto/WearableKafkaDto.java @@ -0,0 +1,22 @@ +package com.factoreal.backend.messaging.kafka.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +/** + * Kafka Wearable 데이터 mapper용 DTO + */ +public class WearableKafkaDto { + private String wearableDeviceId; + private String workerId; + private String sensorType; + private Long val; + private Integer dangerLevel; // 0 이면 정상 1이면 비정상 + private String time; // 센서 탐지 시간 +} diff --git a/src/main/java/com/factoreal/backend/messaging/kafka/processor/SensorEventProcessor.java b/src/main/java/com/factoreal/backend/messaging/kafka/processor/SensorEventProcessor.java index b4c89ced..727e3885 100644 --- a/src/main/java/com/factoreal/backend/messaging/kafka/processor/SensorEventProcessor.java +++ b/src/main/java/com/factoreal/backend/messaging/kafka/processor/SensorEventProcessor.java @@ -1,7 +1,7 @@ package com.factoreal.backend.messaging.kafka.processor; import com.factoreal.backend.domain.sensor.dto.SensorKafkaDto; -import com.factoreal.backend.domain.abnormalLog.dto.LogType; +import com.factoreal.backend.domain.abnormalLog.dto.TargetType; import com.factoreal.backend.domain.abnormalLog.entity.AbnormalLog; import com.factoreal.backend.messaging.sender.WebSocketSender; import com.factoreal.backend.domain.abnormalLog.application.AbnormalLogService; @@ -56,14 +56,15 @@ public void process(SensorKafkaDto dto, String topic) { int dangerLevel = dto.getDangerLevel(); SensorType sensorType = SensorType.getSensorType(dto.getSensorType()); RiskLevel riskLevel = RiskLevel.fromPriority(dangerLevel); - LogType logType = topicToLogType(topic); + TargetType targetType = topicToLogType(topic); // 자동 제어 메시지 판단 (Todo - 진행중) autoControlService.evaluate(dto, dangerLevel); // 이상 로그 저장 - AbnormalLog abnLog = abnormalLogService.saveAbnormalLogFromKafkaDto( - dto, sensorType, riskLevel, logType); + AbnormalLog abnLog = abnormalLogService.saveAbnormalLogFromSensorKafkaDto( + dto, sensorType, riskLevel, targetType + ); // 읽지 않은 알림 수 조회 @@ -73,7 +74,6 @@ public void process(SensorKafkaDto dto, String topic) { // 1. 히트맵 전송 webSocketSender.sendDangerLevel(dto.getZoneId(), dto.getSensorType(), dangerLevel); // 2. 위험 알림 전송 -> 위험도별 Websocket + wearable + Slack(SMS 대체) - // Todo : (As-is) 전략 기반 startAlarm() 메서드 담당자 확인 필요 // webSocketSender.sendDangerAlarm(abnLog.toAlarmEventDto()); alarmEventService.startAlarm(dto,abnLog,dangerLevel); // 3. 읽지 않은 수 전송 @@ -103,10 +103,10 @@ private int getDangerLevel(String sensorType, double val) { } // topic enum 변경하기 - private LogType topicToLogType(String topic) { + private TargetType topicToLogType(String topic) { return switch (topic.toUpperCase()) { - case "EQUIPMENT" -> LogType.Equip; - case "ENVIRONMENT" -> LogType.Sensor; + case "EQUIPMENT" -> TargetType.Equip; + case "ENVIRONMENT" -> TargetType.Sensor; default -> throw new IllegalArgumentException("지원하지 않는 Kafka 토픽: " + topic); }; } diff --git a/src/main/java/com/factoreal/backend/messaging/kafka/processor/WearableEventProcessor.java b/src/main/java/com/factoreal/backend/messaging/kafka/processor/WearableEventProcessor.java new file mode 100644 index 00000000..ca92cec4 --- /dev/null +++ b/src/main/java/com/factoreal/backend/messaging/kafka/processor/WearableEventProcessor.java @@ -0,0 +1,74 @@ +package com.factoreal.backend.messaging.kafka.processor; + +import com.factoreal.backend.domain.abnormalLog.dto.TargetType; +import com.factoreal.backend.messaging.kafka.dto.WearableKafkaDto; +import com.factoreal.backend.domain.abnormalLog.entity.AbnormalLog; +import com.factoreal.backend.messaging.kafka.strategy.enums.AlarmEventDto; +import com.factoreal.backend.messaging.kafka.strategy.enums.RiskLevel; +import com.factoreal.backend.messaging.kafka.strategy.enums.WearableDataType; +import com.factoreal.backend.messaging.sender.WebSocketSender; +import com.factoreal.backend.domain.abnormalLog.application.AbnormalLogService; +import com.factoreal.backend.messaging.service.AlarmEventService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + + +/** + * WearableEventProcessor 클래스는 Kafka로부터 전달받은 생체 데이터를 처리하는 클래스입니다. + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class WearableEventProcessor { + private final AbnormalLogService abnormalLogService; + private final WebSocketSender webSocketSender; + private final AlarmEventService alarmEventService; + + /** + * kafka 메시지 처리 + * topic은 고정값 하나만 받으므로 생략함. + * @param wearableKafkaDto 생체 데이터 + * @param topic Kafka 토픽명(WEARABLE) + */ + public void process(WearableKafkaDto wearableKafkaDto, String topic) { + try{ + // 위험도는 0:정상, 1:비정상으로 나뉨 + // wearable자체에서 rule-based 기반으로 할당되어 송신됨. + int dangerLevel = wearableKafkaDto.getDangerLevel(); // 0: 정상, 1: 비정상 + + WearableDataType wearableDataType = WearableDataType.getWearableType(wearableKafkaDto.getSensorType()); + RiskLevel riskLevel = RiskLevel.fromPriority(dangerLevel); + + // 타겟타입이 항상 WEARABLE이므로 TargetType.Worker 바로 사용 + // 1. abnormalLog 기록 + AbnormalLog abnormalLog = abnormalLogService.saveAbnormalLogFromWearableKafkaDto( + wearableKafkaDto, wearableDataType, riskLevel, TargetType.Worker + ); + + // 읽지 않은 알림 수 조회 + Long count = abnormalLogService.readRequired(); + + // WebSocket 알림 전송 + // 1. 히트맵 전송 + webSocketSender.sendDangerLevel( + abnormalLog.getZone().getZoneId(), + WearableDataType.heartRate.name(), + dangerLevel + ); + // 2. 상세 화면으로 웹소켓 보내는 것을 생략 + // 3. 위험 알림 전송 -> 팝업으로 알려주기 + AlarmEventDto alarmEventDto = alarmEventService.generateAlarmDto(wearableKafkaDto,abnormalLog,riskLevel); + webSocketSender.sendDangerAlarm(alarmEventDto); + // 4. 읽지 않은 알림 전송 + webSocketSender.sendUnreadCount(count); + + }catch (Exception e){ + log.error( + "❌ 웨어러블 이벤트 처리 실패: sensorId={}, zoneId={}", + wearableKafkaDto.getWearableDeviceId(), + wearableKafkaDto.getWorkerId() + ); + } + } +} diff --git a/src/main/java/com/factoreal/backend/messaging/kafka/strategy/alarmList/AppPushNotificationStrategy.java b/src/main/java/com/factoreal/backend/messaging/kafka/strategy/alarmList/AppPushNotificationStrategy.java index 61bc0814..b38b85e8 100644 --- a/src/main/java/com/factoreal/backend/messaging/kafka/strategy/alarmList/AppPushNotificationStrategy.java +++ b/src/main/java/com/factoreal/backend/messaging/kafka/strategy/alarmList/AppPushNotificationStrategy.java @@ -1,18 +1,36 @@ package com.factoreal.backend.messaging.kafka.strategy.alarmList; +import com.factoreal.backend.domain.worker.application.WorkerService; +import com.factoreal.backend.domain.worker.dto.response.WorkerInfoResponse; +import com.factoreal.backend.messaging.fcm.service.FCMService; import com.factoreal.backend.messaging.kafka.strategy.enums.AlarmEventDto; import com.factoreal.backend.messaging.kafka.strategy.enums.RiskLevel; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import java.util.List; + @Slf4j @Component("APP") +@RequiredArgsConstructor public class AppPushNotificationStrategy implements NotificationStrategy { - + private final FCMService fcmService; + private final WorkerService workerService; // TODO FCM 전송 로직 @Override public void send(AlarmEventDto alarmEventDto) { log.info("📲 App Push Notification Strategy."); + // FCM 작업중 자러감... + // 같은 공간에 있는 작업자에게 FCM 푸시 알람 전송 + List workerList = workerService.getWorkersByZoneId(alarmEventDto.getZoneId()); + workerList.forEach(worker -> { + try { + fcmService.sendMessage(worker.getFcmToken(), alarmEventDto.getZoneName(),alarmEventDto.getMessageBody()); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); } @Override diff --git a/src/main/java/com/factoreal/backend/messaging/kafka/strategy/alarmMessage/DefaultRiskMessageProvider.java b/src/main/java/com/factoreal/backend/messaging/kafka/strategy/alarmMessage/DefaultRiskMessageProvider.java index 3e8315dc..1dc2fc5e 100644 --- a/src/main/java/com/factoreal/backend/messaging/kafka/strategy/alarmMessage/DefaultRiskMessageProvider.java +++ b/src/main/java/com/factoreal/backend/messaging/kafka/strategy/alarmMessage/DefaultRiskMessageProvider.java @@ -2,12 +2,13 @@ import com.factoreal.backend.messaging.kafka.strategy.enums.RiskLevel; import com.factoreal.backend.messaging.kafka.strategy.enums.SensorType; +import com.factoreal.backend.messaging.kafka.strategy.enums.WearableDataType; import org.springframework.stereotype.Component; @Component public class DefaultRiskMessageProvider implements RiskMessageProvider { @Override - public String getMessage(SensorType sensorType, RiskLevel riskLevel) { + public String getRiskMessageBySensor(SensorType sensorType, RiskLevel riskLevel) { return switch (sensorType) { case temp -> switch (riskLevel) { case INFO -> "온도가 정상 범위입니다."; @@ -41,4 +42,17 @@ public String getMessage(SensorType sensorType, RiskLevel riskLevel) { }; }; } + + @Override + public String getRiskMessageByWearble(WearableDataType wearableDataType, RiskLevel riskLevel) { + return switch (wearableDataType) { + case heartRate -> switch (riskLevel) { + case INFO -> "작업자 심박수 정상."; + case WARNING -> "작업자 심박수 140 초과."; + case CRITICAL -> null; + }; + }; + } + + } diff --git a/src/main/java/com/factoreal/backend/messaging/kafka/strategy/alarmMessage/RiskMessageProvider.java b/src/main/java/com/factoreal/backend/messaging/kafka/strategy/alarmMessage/RiskMessageProvider.java index 9e0ffd2b..69222c0e 100644 --- a/src/main/java/com/factoreal/backend/messaging/kafka/strategy/alarmMessage/RiskMessageProvider.java +++ b/src/main/java/com/factoreal/backend/messaging/kafka/strategy/alarmMessage/RiskMessageProvider.java @@ -1,8 +1,11 @@ package com.factoreal.backend.messaging.kafka.strategy.alarmMessage; + import com.factoreal.backend.messaging.kafka.strategy.enums.RiskLevel; import com.factoreal.backend.messaging.kafka.strategy.enums.SensorType; +import com.factoreal.backend.messaging.kafka.strategy.enums.WearableDataType; public interface RiskMessageProvider { - String getMessage(SensorType sensorType, RiskLevel riskLevel); + String getRiskMessageBySensor(SensorType sensorType, RiskLevel riskLevel); + String getRiskMessageByWearble(WearableDataType wearableDataType, RiskLevel riskLevel); } diff --git a/src/main/java/com/factoreal/backend/messaging/kafka/strategy/enums/AlarmEventDto.java b/src/main/java/com/factoreal/backend/messaging/kafka/strategy/enums/AlarmEventDto.java index e62cca36..54617611 100644 --- a/src/main/java/com/factoreal/backend/messaging/kafka/strategy/enums/AlarmEventDto.java +++ b/src/main/java/com/factoreal/backend/messaging/kafka/strategy/enums/AlarmEventDto.java @@ -1,5 +1,6 @@ package com.factoreal.backend.messaging.kafka.strategy.enums; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; @@ -9,23 +10,45 @@ */ @Builder @Data +@Schema(description = "Kafka 알람 이벤트 DTO. 센서 및 시스템 이벤트에 대한 경보 정보를 포함합니다.") public class AlarmEventDto { + // 1. 필수 공통 정보 - private Long eventId; // 알람 이벤트 고유 ID (추적용) -> BE에서 할당 + + @Schema(description = "알람 이벤트 고유 ID (추적용)", example = "1001") + private Long eventId; + + @Schema(description = "구역 ID", example = "zone-001") private String zoneId; + + @Schema(description = "장비 ID", example = "equip-xyz") private String equipId; + + @Schema(description = "센서 또는 웨어러블 ID", example = "sensor-abc") private String sensorId; - private String sensorType; // 알람 종류 (예: "HIGH_HEART_RATE", "LOW_BATTERY", "SERVER_DOWN") - 분류 및 라우팅에 사용 - private double sensorValue; // 이상치 값 - private RiskLevel riskLevel; // 알람 심각도 (예: CRITICAL, WARNING, INFO) - 채널 선택, 표현 방식 결정에 사용 - private String time; // 알람 발생 시각 -> BE에서 할당 + @Schema(description = "센서 타입 또는 알람 종류", example = "HIGH_HEART_RATE") + private String sensorType; + + @Schema(description = "센서에서 감지된 값", example = "145.0") + private double sensorValue; + + @Schema(description = "위험 수준", implementation = RiskLevel.class) + private RiskLevel riskLevel; + + @Schema(description = "알람 발생 시각 (ISO 8601)", example = "2025-05-25T12:34:56Z") + private String time; + + // 3. 내용 정보 + + @Schema(description = "알람 본문 메시지", example = "환자의 심박수가 위험 수준을 초과했습니다.") + private String messageBody; + + // 4. 부가 정보 - // 3. 내용 정보 (프로토콜별로 활용 방식이 다름) -// String title, // 알람 제목 (푸시 제목, 이메일 제목 등에 활용) - private String messageBody; // 알람 본문 (푸시 내용, SMS 내용, 이메일 본문 등에 활용) + @Schema(description = "알람 발생 출처 서비스", example = "WearableSensorService") + private String source; - // 4. 부가 정보 (선택적) - private String source; // 알람 발생 출처 (예: "WearableSensorService", "BatchJobMonitor") + @Schema(description = "구역 이름", example = "응급실 A구역") private String zoneName; -} \ No newline at end of file +} diff --git a/src/main/java/com/factoreal/backend/messaging/kafka/strategy/enums/WearableDataType.java b/src/main/java/com/factoreal/backend/messaging/kafka/strategy/enums/WearableDataType.java new file mode 100644 index 00000000..a311e9b2 --- /dev/null +++ b/src/main/java/com/factoreal/backend/messaging/kafka/strategy/enums/WearableDataType.java @@ -0,0 +1,8 @@ +package com.factoreal.backend.messaging.kafka.strategy.enums; + +public enum WearableDataType { + heartRate; + public static WearableDataType getWearableType(String wearableDataType) { + return WearableDataType.valueOf(wearableDataType); + } +} diff --git a/src/main/java/com/factoreal/backend/messaging/service/AlarmEventService.java b/src/main/java/com/factoreal/backend/messaging/service/AlarmEventService.java index 0b462454..8dab749e 100644 --- a/src/main/java/com/factoreal/backend/messaging/service/AlarmEventService.java +++ b/src/main/java/com/factoreal/backend/messaging/service/AlarmEventService.java @@ -2,12 +2,14 @@ import com.factoreal.backend.domain.sensor.dto.SensorKafkaDto; import com.factoreal.backend.domain.abnormalLog.entity.AbnormalLog; -import com.factoreal.backend.domain.zone.dao.ZoneRepository; +import com.factoreal.backend.domain.zone.application.ZoneService; import com.factoreal.backend.messaging.kafka.strategy.alarmList.NotificationStrategy; import com.factoreal.backend.messaging.kafka.strategy.NotificationStrategyFactory; import com.factoreal.backend.messaging.kafka.strategy.enums.AlarmEventDto; import com.factoreal.backend.messaging.kafka.strategy.enums.RiskLevel; import com.factoreal.backend.messaging.kafka.strategy.enums.SensorType; +import com.factoreal.backend.messaging.kafka.dto.WearableKafkaDto; +import com.factoreal.backend.messaging.kafka.strategy.enums.WearableDataType; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -21,7 +23,7 @@ public class AlarmEventService { // 위험 레벨별 알람 전략을 가져오기 위한 팩토리 서비스 private final NotificationStrategyFactory notificationStrategyFactory; - private final ZoneRepository zoneRepository; + private final ZoneService zoneService; // Todo 추후 Flink에서 SensorKafkaDto에 dangerLevel을 포함하면 제거 public void startAlarm(SensorKafkaDto sensorData, AbnormalLog abnormalLog, int dangerLevel) { AlarmEventDto alarmEventDto; @@ -42,16 +44,33 @@ public void startAlarm(SensorKafkaDto sensorData, AbnormalLog abnormalLog, int d log.info("alarmEvent: {}", alarmEventDto.toString()); processAlarmEvent(alarmEventDto); } catch (Exception e) { - log.error("Error converting Kafka message: {}", e); + log.error("Error converting Kafka message: {}", e.getMessage()); // TODO: 기타 처리 오류 처리 } } + public AlarmEventDto generateAlarmDto(WearableKafkaDto data, AbnormalLog abnormalLog, RiskLevel riskLevel) { + String source = "웨어러블"; + + // 알람 이벤트 객체 반환 + return AlarmEventDto.builder() + .eventId(abnormalLog.getId()) + .sensorId(data.getWearableDeviceId()) // 웨어러블 디바이스 Id + .equipId(abnormalLog.getZone().getZoneId()) + .zoneId(abnormalLog.getZone().getZoneId()) + .sensorType(WearableDataType.heartRate.toString()) + .messageBody(abnormalLog.getAbnormalType()) // 이상에 대한 메세지 + .source(source) + .time(data.getTime()) + .riskLevel(riskLevel) + .zoneName(abnormalLog.getZone().getZoneName()) + .build(); + } private AlarmEventDto generateAlarmDto(SensorKafkaDto data, AbnormalLog abnormalLog, RiskLevel riskLevel) throws Exception { String source = data.getZoneId().equals(data.getEquipId()) ? "공간 센서" : "설비 센서"; SensorType sensorType = SensorType.valueOf(data.getSensorType()); - String zoneName = zoneRepository.findByZoneId(data.getZoneId()).getZoneName(); + String zoneName = zoneService.findByZoneId(data.getZoneId()).getZoneName(); // 알람 이벤트 객체 반환 return AlarmEventDto.builder() .eventId(abnormalLog.getId()) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index bcf2cce3..24b96980 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -47,5 +47,8 @@ logging: # Swagger URL springdoc: + api-docs: + path: /v3/api-docs + resolve-extensions-properties: false swagger-ui: - path: /docs \ No newline at end of file + path: /swagger-ui.html diff --git a/src/main/resources/db/migration/V7__alter_zone_hist_insert_zone_default_data.sql b/src/main/resources/db/migration/V7__alter_zone_hist_insert_zone_default_data.sql new file mode 100644 index 00000000..200f1770 --- /dev/null +++ b/src/main/resources/db/migration/V7__alter_zone_hist_insert_zone_default_data.sql @@ -0,0 +1,7 @@ +# JPA에서 zone_hist 삽입시에 PK가 없어서 오류 발생 -> JPA GenerationType.IDENTITY 추가에 따른 스키마 수정 +ALTER TABLE `zone_hist` +MODIFY COLUMN `id` BIGINT(20) NOT NULL AUTO_INCREMENT; + +# 초기 작업자의 위치를 표시하기 위한 빈공간 생성 +INSERT INTO zone_info (zone_id, zone_name) VALUES + ('00000000000000-000', '대기실') \ No newline at end of file diff --git a/src/main/resources/db/migration/V8_1__alter_control_log_table_int_to_float.sql b/src/main/resources/db/migration/V8_1__alter_control_log_table_int_to_float.sql new file mode 100644 index 00000000..335a61f4 --- /dev/null +++ b/src/main/resources/db/migration/V8_1__alter_control_log_table_int_to_float.sql @@ -0,0 +1,2 @@ +ALTER TABLE `control_log` + MODIFY COLUMN `control_val` float4; \ No newline at end of file diff --git a/src/main/resources/db/migration/V8__alter_worker_table_add_fcmToken.sql b/src/main/resources/db/migration/V8__alter_worker_table_add_fcmToken.sql new file mode 100644 index 00000000..e35b9b8f --- /dev/null +++ b/src/main/resources/db/migration/V8__alter_worker_table_add_fcmToken.sql @@ -0,0 +1,6 @@ +ALTER TABLE worker_info + ADD COLUMN fcm_token VARCHAR(200); + + +ALTER TABLE abn_log + ADD COLUMN danger_level smallint; \ No newline at end of file diff --git a/src/main/resources/fcm_root_key/.gitkeep b/src/main/resources/fcm_root_key/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/test/java/com/factoreal/backend/service/WorkerServiceTest.java b/src/test/java/com/factoreal/backend/service/WorkerServiceTest.java index fa6354f2..0b0fcca3 100644 --- a/src/test/java/com/factoreal/backend/service/WorkerServiceTest.java +++ b/src/test/java/com/factoreal/backend/service/WorkerServiceTest.java @@ -1,6 +1,8 @@ package com.factoreal.backend.service; +import com.factoreal.backend.domain.abnormalLog.application.AbnormalLogService; import com.factoreal.backend.domain.worker.application.WorkerService; +import com.factoreal.backend.domain.worker.dto.response.WorkerDetailResponse; import com.factoreal.backend.domain.worker.dto.response.WorkerInfoResponse; import com.factoreal.backend.domain.worker.entity.Worker; import com.factoreal.backend.domain.zone.dao.ZoneHistoryRepository; @@ -25,7 +27,9 @@ public class WorkerServiceTest { @Mock private WorkerRepository workerRepository; - + + @Mock + private AbnormalLogService abnormalLogService; @Mock private ZoneHistoryRepository zoneHistoryRepository; @@ -79,7 +83,7 @@ public void testGetAllWorkers() { when(workerRepository.findAll()).thenReturn(Arrays.asList(worker1, worker2)); // 서비스 메소드 호출 - List result = workerService.getAllWorkers(); + List result = workerService.getAllWorkers(); // 결과 검증 assertNotNull(result);