Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,5 @@ $RECYCLE.BIN/

*.html

custom-kibana/
custom-kibana/
/src/main/resources/fcm_root_key/monitory-28aad-firebase-adminsdk-fbsvc-0d7886e5f5.json
7 changes: 4 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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') {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<AbnormalLogResponse> getAllAbnormalLogs(@ModelAttribute AbnormalPagingRequest pagingDto) {
return abnormalLogService.findAllAbnormalLogs(pagingDto);
}

@GetMapping("/unread")
@Operation(summary = "๋ฏธํ™•์ธ ๋กœ๊ทธ ์กฐํšŒ", description = "ํŽ˜์ด์ง• ์ธ์ž๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ฝ์ง€ ์•Š์€ ๋กœ๊ทธ๋ฅผ ์š”์ • ๊ฐœ์ˆ˜ ๋งŒํผ ๋ฐ˜ํ™˜ํ•ด์ค๋‹ˆ๋‹ค.")
public Page<AbnormalLogResponse> getAllAbnormalLogsUnRead(@ModelAttribute AbnormalPagingRequest pagingDto) {
return abnormalLogService.findAllAbnormalLogsUnRead(pagingDto);
}

// ํŠน์ • ์ด์ƒ ์œ ํ˜•์œผ๋กœ ํ•„ํ„ฐ๋ง
@GetMapping("/type/{abnormalType}")
@Operation(summary = "์œ„ํ—˜๋ณ„ ๋กœ๊ทธ ์กฐํšŒ", description = "ํŽ˜์ด์ง• ์ธ์ž๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์œ„ํ—˜๋ณ„ ๋กœ๊ทธ๋ฅผ ์š”์ • ๊ฐœ์ˆ˜ ๋งŒํผ ๋ฐ˜ํ™˜ํ•ด์ค๋‹ˆ๋‹ค.")
public Page<AbnormalLogResponse> getAbnormalLogsByType(
@ModelAttribute AbnormalPagingRequest pagingDto,
@PathVariable String abnormalType) {
Expand All @@ -36,22 +42,25 @@ public Page<AbnormalLogResponse> getAbnormalLogsByType(

// ํŠน์ • ํƒ€๊ฒŸ ID๋กœ ํ•„ํ„ฐ๋ง
@GetMapping("/target/{targetType}/{targetId}")
@Operation(summary = "์œ ํ˜•๋ณ„ ๋กœ๊ทธ ์กฐํšŒ", description = "ํŽ˜์ด์ง• ์ธ์ž๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์œ ํ˜•๋ณ„ ๋กœ๊ทธ๋ฅผ ์š”์ • ๊ฐœ์ˆ˜ ๋งŒํผ ๋ฐ˜ํ™˜ํ•ด์ค๋‹ˆ๋‹ค.")
public Page<AbnormalLogResponse> 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<Void> 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<Long> getUnreadAlarmCount() {
Long count = abnormalLogService.readRequired();
return ResponseEntity.ok(count);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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());
Expand All @@ -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)
Expand All @@ -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<AbnormalLogResponse> findAllAbnormalLogs(AbnormalPagingRequest abnormalPagingDto) {
// ํ•œ๋ฒˆ์— DB์ „์ฒด๋ฅผ ์ฃผ๋Š” ๊ฒƒ์ด ์•„๋‹Œ ๊ตฌ๊ฐ„ ๋‚˜๋ˆ ์„œ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•จ
Pageable pageable = getPageable(abnormalPagingDto);
Page<AbnormalLog> 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<AbnormalLogResponse> findAllAbnormalLogsUnRead(AbnormalPagingRequest abnormalPagingRequest) {
// ํ•œ๋ฒˆ์— DB์ „์ฒด๋ฅผ ์ฃผ๋Š” ๊ฒƒ์ด ์•„๋‹Œ ๊ตฌ๊ฐ„ ๋‚˜๋ˆ ์„œ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•จ
Pageable pageable = getPageable(abnormalPagingRequest);
Page<AbnormalLog> 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<AbnormalLog> abnormalLogs = abnLogRepository.findAllByIsReadIsFalseOrderByDetectedAtDesc(pageable);
return abnormalLogs.map(AbnormalLog::fromEntity);
}

public Page<AbnormalLogResponse> findAbnormalLogsByAbnormalType(AbnormalPagingRequest abnormalPagingRequest, String abnormalType){
// ํ•œ๋ฒˆ์— DB์ „์ฒด๋ฅผ ์ฃผ๋Š” ๊ฒƒ์ด ์•„๋‹Œ ๊ตฌ๊ฐ„ ๋‚˜๋ˆ ์„œ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•จ
Pageable pageable = getPageable(abnormalPagingRequest);
Page<AbnormalLog> 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<AbnormalLogResponse> findAbnormalLogsByTargetId(AbnormalPagingRequest abnormalPagingRequest, String targetType, String targetId){
public List<AbnormalLogResponse> findLatestAbnormalLogsForTargets(TargetType targetType, List<String> 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<AbnormalLogResponse> findAbnormalLogsByTargetId(AbnormalPagingRequest abnormalPagingRequest, TargetType targetType, String targetId){
// ํ•œ๋ฒˆ์— DB์ „์ฒด๋ฅผ ์ฃผ๋Š” ๊ฒƒ์ด ์•„๋‹Œ ๊ตฌ๊ฐ„ ๋‚˜๋ˆ ์„œ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•จ
Pageable pageable = getPageable(abnormalPagingRequest);
Page<AbnormalLog> abnormalLogs = abnLogRepository.findAbnormalLogsByTargetTypeAndTargetId(
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AbnormalLog,Long> {
Page<AbnormalLog> findAbnormalLogsByAbnormalType(String abnormalType, Pageable pageable);

Page<AbnormalLog> findAbnormalLogsByTargetTypeAndTargetId(String targetType, String targetId, Pageable pageable);
Page<AbnormalLog> findAbnormalLogsByTargetTypeAndTargetId(TargetType targetType, String targetId, Pageable pageable);
// Pageable ๊ฐ์ฒด ์—†์ด๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์˜ค๋ฒ„๋ผ์ด๋”ฉ
Optional<AbnormalLog> findFirstByTargetTypeAndTargetIdOrderByDetectedAtDesc(TargetType targetType, String targetId);


long countByIsReadFalse(); // ์ฝ์ง€ ์•Š์€ ๋กœ๊ทธ์˜ ๊ฐœ์ˆ˜ ๋ฐ˜ํ™˜

Page<AbnormalLog> findAllByIsReadIsFalse(Pageable pageable);
Page<AbnormalLog> findAllByIsReadIsFalseOrderByDetectedAtDesc(Pageable pageable);

// zoneId๋กœ ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ๋œ ๋กœ๊ทธ ์กฐํšŒ
Page<AbnormalLog> findByZone_ZoneIdOrderByDetectedAtDesc(String zoneId, Pageable pageable);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.factoreal.backend.domain.abnormalLog.dto;

public enum LogType {
public enum TargetType {
Sensor,
Worker,
Equip
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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๋ฒˆ ์กด"
Expand Down
Loading