From a1a7cd9ba30a5bfed85571cd1fe4b3707adfc42b Mon Sep 17 00:00:00 2001 From: midday2612 Date: Sun, 8 Jun 2025 15:09:43 +0900 Subject: [PATCH 1/5] =?UTF-8?q?KW-416/feat:=20building=20enter=20log=20dom?= =?UTF-8?q?ain=EC=97=90=20visit=20category=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../log/consumer/BuildingEnterLogConsumer.java | 5 ++++- .../domain/log/domain/BuildingEnterLog.java | 13 +++++++++++-- .../dto/request/CreateBuildingEnterLogRequest.java | 4 +++- .../producer/BuildingEnterLogStreamProducer.java | 1 + 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/doubleo/passservice/domain/log/consumer/BuildingEnterLogConsumer.java b/src/main/java/com/doubleo/passservice/domain/log/consumer/BuildingEnterLogConsumer.java index 6f89e5f..bb436f1 100644 --- a/src/main/java/com/doubleo/passservice/domain/log/consumer/BuildingEnterLogConsumer.java +++ b/src/main/java/com/doubleo/passservice/domain/log/consumer/BuildingEnterLogConsumer.java @@ -3,6 +3,7 @@ import com.doubleo.passservice.domain.log.domain.BuildingEnterLog; import com.doubleo.passservice.domain.log.domain.Direction; import com.doubleo.passservice.domain.log.repository.BuildingEnterLogRepository; +import com.doubleo.passservice.domain.pass.enums.VisitCategory; import java.util.List; import java.util.Map; import javax.annotation.PostConstruct; @@ -60,7 +61,9 @@ public void consumeMessages() { Long.parseLong((String) data.get("memberId")), (String) data.get("memberName"), Long.parseLong((String) data.get("passId")), - Direction.valueOf(((String) data.get("direction")).toUpperCase())); + Direction.valueOf(((String) data.get("direction")).toUpperCase()), + VisitCategory.valueOf( + ((String) data.get("visitCategory")).toUpperCase())); buildingEnterLogRepository.save(buildingEnterLog); redisTemplate.opsForStream().acknowledge(GROUP, msg); diff --git a/src/main/java/com/doubleo/passservice/domain/log/domain/BuildingEnterLog.java b/src/main/java/com/doubleo/passservice/domain/log/domain/BuildingEnterLog.java index 6243fff..9c29de3 100644 --- a/src/main/java/com/doubleo/passservice/domain/log/domain/BuildingEnterLog.java +++ b/src/main/java/com/doubleo/passservice/domain/log/domain/BuildingEnterLog.java @@ -1,6 +1,7 @@ package com.doubleo.passservice.domain.log.domain; import com.doubleo.passservice.domain.common.model.BaseEntity; +import com.doubleo.passservice.domain.pass.enums.VisitCategory; import jakarta.persistence.*; import lombok.*; @@ -32,6 +33,10 @@ public class BuildingEnterLog extends BaseEntity { @Column(name = "direction", nullable = false, length = 10) private Direction direction; + @Column(name = "visit_category") + @Enumerated(EnumType.STRING) + private VisitCategory visitCategory; + @Builder(access = AccessLevel.PRIVATE) private BuildingEnterLog( String tenantId, @@ -39,13 +44,15 @@ private BuildingEnterLog( Long memberId, String memberName, Long passId, - Direction direction) { + Direction direction, + VisitCategory visitCategory) { this.tenantId = tenantId; this.buildingId = buildingId; this.memberId = memberId; this.memberName = memberName; this.passId = passId; this.direction = direction; + this.visitCategory = visitCategory; } public static BuildingEnterLog createBuildingEnterLog( @@ -54,7 +61,8 @@ public static BuildingEnterLog createBuildingEnterLog( Long memberId, String memberName, Long passId, - Direction direction) { + Direction direction, + VisitCategory visitCategory) { return BuildingEnterLog.builder() .tenantId(tenantId) .buildingId(buildingId) @@ -62,6 +70,7 @@ public static BuildingEnterLog createBuildingEnterLog( .memberName(memberName) .passId(passId) .direction(direction) + .visitCategory(visitCategory) .build(); } } diff --git a/src/main/java/com/doubleo/passservice/domain/log/dto/request/CreateBuildingEnterLogRequest.java b/src/main/java/com/doubleo/passservice/domain/log/dto/request/CreateBuildingEnterLogRequest.java index 541529b..12cfbeb 100644 --- a/src/main/java/com/doubleo/passservice/domain/log/dto/request/CreateBuildingEnterLogRequest.java +++ b/src/main/java/com/doubleo/passservice/domain/log/dto/request/CreateBuildingEnterLogRequest.java @@ -1,6 +1,7 @@ package com.doubleo.passservice.domain.log.dto.request; import com.doubleo.passservice.domain.log.domain.Direction; +import com.doubleo.passservice.domain.pass.enums.VisitCategory; public record CreateBuildingEnterLogRequest( String tenantId, @@ -8,4 +9,5 @@ public record CreateBuildingEnterLogRequest( Long memberId, String memberName, Long passId, - Direction direction) {} + Direction direction, + VisitCategory visitCategory) {} diff --git a/src/main/java/com/doubleo/passservice/domain/log/producer/BuildingEnterLogStreamProducer.java b/src/main/java/com/doubleo/passservice/domain/log/producer/BuildingEnterLogStreamProducer.java index 2080384..d7094e5 100644 --- a/src/main/java/com/doubleo/passservice/domain/log/producer/BuildingEnterLogStreamProducer.java +++ b/src/main/java/com/doubleo/passservice/domain/log/producer/BuildingEnterLogStreamProducer.java @@ -22,6 +22,7 @@ public void sendBuildingEnterLogToStream(CreateBuildingEnterLogRequest request) message.put("memberName", request.memberName()); message.put("passId", request.passId().toString()); message.put("direction", request.direction().name()); + message.put("visitCategory", request.visitCategory().name()); message.put("timestamp", LocalDateTime.now().toString()); redisTemplate.opsForStream().add("building:enter:stream", message); From 0924bce1eacc3b42a49f5327d8342c8647501823 Mon Sep 17 00:00:00 2001 From: midday2612 Date: Sun, 8 Jun 2025 15:23:54 +0900 Subject: [PATCH 2/5] =?UTF-8?q?KW-416/feat:=20dailyRetainedSnapshot=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../log/domain/DailyRetainedSnapshot.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/main/java/com/doubleo/passservice/domain/log/domain/DailyRetainedSnapshot.java diff --git a/src/main/java/com/doubleo/passservice/domain/log/domain/DailyRetainedSnapshot.java b/src/main/java/com/doubleo/passservice/domain/log/domain/DailyRetainedSnapshot.java new file mode 100644 index 0000000..adb70e8 --- /dev/null +++ b/src/main/java/com/doubleo/passservice/domain/log/domain/DailyRetainedSnapshot.java @@ -0,0 +1,39 @@ +package com.doubleo.passservice.domain.log.domain; + +import com.doubleo.passservice.domain.common.model.BaseEntity; +import com.doubleo.passservice.domain.pass.enums.VisitCategory; +import jakarta.persistence.*; +import java.time.LocalDate; +import lombok.*; + +@Entity +@Table( + name = "daily_retained_snapshot", + uniqueConstraints = { + @UniqueConstraint(columnNames = {"snapshot_date", "tenant_id", "visit_category"}) + }) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +public class DailyRetainedSnapshot extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "snapshot_id") + private Long id; + + @Column(name = "snapshot_date", nullable = false) + private LocalDate snapshotDate; + + @Enumerated(EnumType.STRING) + @Column(name = "visit_category", nullable = false) + private VisitCategory visitCategory; + + @Column(name = "retained_count", nullable = false) + private int retainedCount; + + public void updateRetainedCount(int retainedCount) { + this.retainedCount = retainedCount; + } +} From 8954094cf990fd987b2c3c1e8465926203383379 Mon Sep 17 00:00:00 2001 From: midday2612 Date: Sun, 8 Jun 2025 16:29:34 +0900 Subject: [PATCH 3/5] =?UTF-8?q?KW-416/feat:=20=EB=8C=80=EC=8B=9C=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=EC=9E=94=EB=A5=98=EC=9D=B8=EC=9B=90=20api=20?= =?UTF-8?q?=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../log/consumer/RetainedCountConsumer.java | 78 +++++++++++++++++++ .../DailyRetainedSnapshotRepository.java | 13 ++++ .../domain/pass/enums/VisitCategory.java | 3 +- .../controller/EntryStatsController.java | 7 +- .../response/RetainedStatusInfoResponse.java | 6 ++ .../domain/stats/service/StatsService.java | 2 + .../stats/service/StatsServiceImpl.java | 38 +++++++++ 7 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/doubleo/passservice/domain/log/consumer/RetainedCountConsumer.java create mode 100644 src/main/java/com/doubleo/passservice/domain/log/repository/DailyRetainedSnapshotRepository.java create mode 100644 src/main/java/com/doubleo/passservice/domain/stats/dto/response/RetainedStatusInfoResponse.java diff --git a/src/main/java/com/doubleo/passservice/domain/log/consumer/RetainedCountConsumer.java b/src/main/java/com/doubleo/passservice/domain/log/consumer/RetainedCountConsumer.java new file mode 100644 index 0000000..689638c --- /dev/null +++ b/src/main/java/com/doubleo/passservice/domain/log/consumer/RetainedCountConsumer.java @@ -0,0 +1,78 @@ +package com.doubleo.passservice.domain.log.consumer; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.stream.*; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class RetainedCountConsumer { + + private final RedisTemplate redisTemplate; + + private static final String STREAM_KEY = "building:enter:stream"; + private static final String GROUP = "retained-counter-group"; + private static final String CONSUMER = "retained-counter-consumer"; + + @PostConstruct + public void createGroup() { + try { + redisTemplate.opsForStream().createGroup(STREAM_KEY, ReadOffset.latest(), GROUP); + } catch (Exception e) { + if (!e.getMessage().contains("BUSYGROUP")) { + log.error("Failed to create Redis Stream Group", e); + } + } + } + + @Scheduled(fixedDelay = 300000) + public void consume() { + List> records = + redisTemplate + .opsForStream() + .read( + Consumer.from(GROUP, CONSUMER), + StreamReadOptions.empty().count(10).block(Duration.ofSeconds(1)), + StreamOffset.create(STREAM_KEY, ReadOffset.lastConsumed())); + + if (records == null) return; + + for (MapRecord record : records) { + try { + Map map = + record.getValue().entrySet().stream() + .collect( + Collectors.toMap( + e -> e.getKey().toString(), + e -> e.getValue().toString())); + + String tenantId = map.get("tenantId"); + String direction = map.get("direction"); + String visitCategory = map.get("visitCategory"); + String timestamp = map.get("timestamp"); + + String date = timestamp.substring(0, 10); + String redisKey = + String.format( + "visit:count:%s:%s:%s:%s", + tenantId, date, visitCategory, direction); + + redisTemplate.opsForValue().increment(redisKey); + + redisTemplate.opsForStream().acknowledge(STREAM_KEY, GROUP, record.getId()); + + } catch (Exception e) { + log.error("Error processing stream message", e); + } + } + } +} diff --git a/src/main/java/com/doubleo/passservice/domain/log/repository/DailyRetainedSnapshotRepository.java b/src/main/java/com/doubleo/passservice/domain/log/repository/DailyRetainedSnapshotRepository.java new file mode 100644 index 0000000..e2075d8 --- /dev/null +++ b/src/main/java/com/doubleo/passservice/domain/log/repository/DailyRetainedSnapshotRepository.java @@ -0,0 +1,13 @@ +package com.doubleo.passservice.domain.log.repository; + +import com.doubleo.passservice.domain.log.domain.DailyRetainedSnapshot; +import com.doubleo.passservice.domain.pass.enums.VisitCategory; +import java.time.LocalDate; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface DailyRetainedSnapshotRepository + extends JpaRepository { + Optional findBySnapshotDateAndTenantIdAndVisitCategory( + LocalDate snapshotDate, String tenantId, VisitCategory visitCategory); +} diff --git a/src/main/java/com/doubleo/passservice/domain/pass/enums/VisitCategory.java b/src/main/java/com/doubleo/passservice/domain/pass/enums/VisitCategory.java index 557dd81..67f62e5 100644 --- a/src/main/java/com/doubleo/passservice/domain/pass/enums/VisitCategory.java +++ b/src/main/java/com/doubleo/passservice/domain/pass/enums/VisitCategory.java @@ -2,6 +2,5 @@ public enum VisitCategory { PATIENT, - GUARDIAN, - TEMPORARY + GUARDIAN } diff --git a/src/main/java/com/doubleo/passservice/domain/stats/controller/EntryStatsController.java b/src/main/java/com/doubleo/passservice/domain/stats/controller/EntryStatsController.java index dcd5a76..dee604a 100644 --- a/src/main/java/com/doubleo/passservice/domain/stats/controller/EntryStatsController.java +++ b/src/main/java/com/doubleo/passservice/domain/stats/controller/EntryStatsController.java @@ -42,7 +42,12 @@ public List lastWeekCategoryStatsListGet( } @GetMapping("/building") - public List lastWeekBuilidngStatsListGet() { + public List lastWeekBuildingStatsListGet() { return statsService.getLastWeekBuildingStats(); } + + @GetMapping("/dashboard-summary") + public List currentRetainedStatusGet() { + return statsService.getCurrentRetainedStatus(); + } } diff --git a/src/main/java/com/doubleo/passservice/domain/stats/dto/response/RetainedStatusInfoResponse.java b/src/main/java/com/doubleo/passservice/domain/stats/dto/response/RetainedStatusInfoResponse.java new file mode 100644 index 0000000..9c59746 --- /dev/null +++ b/src/main/java/com/doubleo/passservice/domain/stats/dto/response/RetainedStatusInfoResponse.java @@ -0,0 +1,6 @@ +package com.doubleo.passservice.domain.stats.dto.response; + +import com.doubleo.passservice.domain.pass.enums.VisitCategory; + +public record RetainedStatusInfoResponse( + VisitCategory category, int entered, int exited, int remaining) {} diff --git a/src/main/java/com/doubleo/passservice/domain/stats/service/StatsService.java b/src/main/java/com/doubleo/passservice/domain/stats/service/StatsService.java index 47dcf58..2b7deba 100644 --- a/src/main/java/com/doubleo/passservice/domain/stats/service/StatsService.java +++ b/src/main/java/com/doubleo/passservice/domain/stats/service/StatsService.java @@ -17,4 +17,6 @@ public interface StatsService { List getLastWeekCategoryStats(); List getLastWeekBuildingStats(); + + List getCurrentRetainedStatus(); } diff --git a/src/main/java/com/doubleo/passservice/domain/stats/service/StatsServiceImpl.java b/src/main/java/com/doubleo/passservice/domain/stats/service/StatsServiceImpl.java index 8218ecd..11e355b 100644 --- a/src/main/java/com/doubleo/passservice/domain/stats/service/StatsServiceImpl.java +++ b/src/main/java/com/doubleo/passservice/domain/stats/service/StatsServiceImpl.java @@ -1,13 +1,17 @@ package com.doubleo.passservice.domain.stats.service; +import com.doubleo.passservice.domain.log.domain.DailyRetainedSnapshot; import com.doubleo.passservice.domain.log.dto.response.HourlyEntryResponse; import com.doubleo.passservice.domain.log.repository.BuildingEnterLogRepository; +import com.doubleo.passservice.domain.log.repository.DailyRetainedSnapshotRepository; +import com.doubleo.passservice.domain.pass.enums.VisitCategory; import com.doubleo.passservice.domain.stats.domain.EntryStatsDaily; import com.doubleo.passservice.domain.stats.dto.response.*; import com.doubleo.passservice.domain.stats.repository.EntryStatsDailyRepository; import com.doubleo.passservice.domain.stats.repository.EntryStatsMonthlyRepository; import com.doubleo.passservice.domain.stats.repository.EntryStatsWeeklyRepository; import com.doubleo.passservice.global.util.TenantValidator; +import com.doubleo.tenantcontext.TenantContextHolder; import java.time.DayOfWeek; import java.time.Duration; import java.time.LocalDate; @@ -32,6 +36,7 @@ public class StatsServiceImpl implements StatsService { private final EntryStatsWeeklyRepository entryStatsWeeklyRepository; private final EntryStatsMonthlyRepository entryStatsMonthlyRepository; private final BuildingEnterLogRepository buildingEnterLogRepository; + private final DailyRetainedSnapshotRepository dailyRetainedSnapshotRepository; private final TenantValidator tenantValidator; private final RedisTemplate redisTemplate; @@ -183,4 +188,37 @@ public List getLastWeekBuildingStats() { }) .collect(Collectors.toList()); } + + public List getCurrentRetainedStatus() { + String tenantId = TenantContextHolder.getTenantId(); + LocalDate today = LocalDate.now(); + + List result = new ArrayList<>(); + + for (VisitCategory category : VisitCategory.values()) { + + int base = + dailyRetainedSnapshotRepository + .findBySnapshotDateAndTenantIdAndVisitCategory( + today, tenantId, category) + .map(DailyRetainedSnapshot::getRetainedCount) + .orElse(0); + + String inKey = + String.format("visit:count:%s:%s:%s:IN", tenantId, today, category.name()); + String outKey = + String.format("visit:count:%s:%s:%s:OUT", tenantId, today, category.name()); + + String inVal = redisTemplate.opsForValue().get(inKey); + String outVal = redisTemplate.opsForValue().get(outKey); + + int entered = (inVal != null && !inVal.isBlank()) ? Integer.parseInt(inVal) : 0; + int exited = (outVal != null && !outVal.isBlank()) ? Integer.parseInt(outVal) : 0; + int remaining = base + entered - exited; + + result.add(new RetainedStatusInfoResponse(category, entered, exited, remaining)); + } + + return result; + } } From d286dd59deb997346831181f614acd8761a23989 Mon Sep 17 00:00:00 2001 From: midday2612 Date: Sun, 8 Jun 2025 16:45:09 +0900 Subject: [PATCH 4/5] =?UTF-8?q?KW-416/chore:=20redis=20key=20ttl=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../passservice/domain/log/consumer/RetainedCountConsumer.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/doubleo/passservice/domain/log/consumer/RetainedCountConsumer.java b/src/main/java/com/doubleo/passservice/domain/log/consumer/RetainedCountConsumer.java index 689638c..acc4edd 100644 --- a/src/main/java/com/doubleo/passservice/domain/log/consumer/RetainedCountConsumer.java +++ b/src/main/java/com/doubleo/passservice/domain/log/consumer/RetainedCountConsumer.java @@ -68,6 +68,8 @@ public void consume() { redisTemplate.opsForValue().increment(redisKey); + redisTemplate.expire(redisKey, Duration.ofDays(1)); + redisTemplate.opsForStream().acknowledge(STREAM_KEY, GROUP, record.getId()); } catch (Exception e) { From 5f825533362e0403308d7123d4018de6527b403c Mon Sep 17 00:00:00 2001 From: midday2612 Date: Sun, 8 Jun 2025 17:24:31 +0900 Subject: [PATCH 5/5] =?UTF-8?q?KW-416/feat:=20daily=20snapshot=20=EC=8A=A4?= =?UTF-8?q?=EC=BC=80=EC=A4=84=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BuildingEnterLogRepository.java | 36 +++++++++++++++++++ .../domain/DailyRetainedSnapshot.java | 12 +++++-- .../DailyRetainedSnapshotRepository.java | 4 +-- .../stats/scheduler/EntryStatsScheduler.java | 7 ++++ .../stats/service/StatsBatchService.java | 2 ++ .../stats/service/StatsBatchServiceImpl.java | 35 ++++++++++++++++++ .../stats/service/StatsServiceImpl.java | 4 +-- 7 files changed, 93 insertions(+), 7 deletions(-) rename src/main/java/com/doubleo/passservice/domain/{log => stats}/domain/DailyRetainedSnapshot.java (75%) rename src/main/java/com/doubleo/passservice/domain/{log => stats}/repository/DailyRetainedSnapshotRepository.java (77%) diff --git a/src/main/java/com/doubleo/passservice/domain/log/repository/BuildingEnterLogRepository.java b/src/main/java/com/doubleo/passservice/domain/log/repository/BuildingEnterLogRepository.java index 398e968..f4a5c10 100644 --- a/src/main/java/com/doubleo/passservice/domain/log/repository/BuildingEnterLogRepository.java +++ b/src/main/java/com/doubleo/passservice/domain/log/repository/BuildingEnterLogRepository.java @@ -52,4 +52,40 @@ Long countEnteredBetween( @Param("start") LocalDateTime start, @Param("end") LocalDateTime end, @Param("tenantId") String tenantId); + + @Query( + """ + SELECT COUNT(b) + FROM BuildingEnterLog b + JOIN Pass p ON b.passId = p.id + WHERE b.direction = 'IN' + AND b.createdDt >= :start + AND b.createdDt < :end + AND b.tenantId = :tenantId + AND p.visitCategory = :visitCategory +""") + int countEnteredByCategory( + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end, + @Param("tenantId") String tenantId, + @Param("visitCategory") + com.doubleo.passservice.domain.pass.enums.VisitCategory visitCategory); + + @Query( + """ + SELECT COUNT(b) + FROM BuildingEnterLog b + JOIN Pass p ON b.passId = p.id + WHERE b.direction = 'OUT' + AND b.createdDt >= :start + AND b.createdDt < :end + AND b.tenantId = :tenantId + AND p.visitCategory = :visitCategory +""") + int countExitedByCategory( + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end, + @Param("tenantId") String tenantId, + @Param("visitCategory") + com.doubleo.passservice.domain.pass.enums.VisitCategory visitCategory); } diff --git a/src/main/java/com/doubleo/passservice/domain/log/domain/DailyRetainedSnapshot.java b/src/main/java/com/doubleo/passservice/domain/stats/domain/DailyRetainedSnapshot.java similarity index 75% rename from src/main/java/com/doubleo/passservice/domain/log/domain/DailyRetainedSnapshot.java rename to src/main/java/com/doubleo/passservice/domain/stats/domain/DailyRetainedSnapshot.java index adb70e8..931126a 100644 --- a/src/main/java/com/doubleo/passservice/domain/log/domain/DailyRetainedSnapshot.java +++ b/src/main/java/com/doubleo/passservice/domain/stats/domain/DailyRetainedSnapshot.java @@ -1,4 +1,4 @@ -package com.doubleo.passservice.domain.log.domain; +package com.doubleo.passservice.domain.stats.domain; import com.doubleo.passservice.domain.common.model.BaseEntity; import com.doubleo.passservice.domain.pass.enums.VisitCategory; @@ -15,7 +15,6 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) -@Builder public class DailyRetainedSnapshot extends BaseEntity { @Id @@ -33,7 +32,14 @@ public class DailyRetainedSnapshot extends BaseEntity { @Column(name = "retained_count", nullable = false) private int retainedCount; - public void updateRetainedCount(int retainedCount) { + public DailyRetainedSnapshot( + String tenantId, + LocalDate snapshotDate, + VisitCategory visitCategory, + int retainedCount) { + this.tenantId = tenantId; + this.snapshotDate = snapshotDate; + this.visitCategory = visitCategory; this.retainedCount = retainedCount; } } diff --git a/src/main/java/com/doubleo/passservice/domain/log/repository/DailyRetainedSnapshotRepository.java b/src/main/java/com/doubleo/passservice/domain/stats/repository/DailyRetainedSnapshotRepository.java similarity index 77% rename from src/main/java/com/doubleo/passservice/domain/log/repository/DailyRetainedSnapshotRepository.java rename to src/main/java/com/doubleo/passservice/domain/stats/repository/DailyRetainedSnapshotRepository.java index e2075d8..3492fcb 100644 --- a/src/main/java/com/doubleo/passservice/domain/log/repository/DailyRetainedSnapshotRepository.java +++ b/src/main/java/com/doubleo/passservice/domain/stats/repository/DailyRetainedSnapshotRepository.java @@ -1,7 +1,7 @@ -package com.doubleo.passservice.domain.log.repository; +package com.doubleo.passservice.domain.stats.repository; -import com.doubleo.passservice.domain.log.domain.DailyRetainedSnapshot; import com.doubleo.passservice.domain.pass.enums.VisitCategory; +import com.doubleo.passservice.domain.stats.domain.DailyRetainedSnapshot; import java.time.LocalDate; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/com/doubleo/passservice/domain/stats/scheduler/EntryStatsScheduler.java b/src/main/java/com/doubleo/passservice/domain/stats/scheduler/EntryStatsScheduler.java index 8e0db4b..e7ea50e 100644 --- a/src/main/java/com/doubleo/passservice/domain/stats/scheduler/EntryStatsScheduler.java +++ b/src/main/java/com/doubleo/passservice/domain/stats/scheduler/EntryStatsScheduler.java @@ -33,4 +33,11 @@ public void runMonthlyStatsUpdate() { statsBatchService.updateMonthlyStats(); log.info("[EntryStatsScheduler] Completed monthly entry stats aggregation"); } + + @Scheduled(cron = "0 5 0 * * *") + public void runDailyRetainedSnapshotSave() { + log.info("[EntryStatsScheduler] Starting daily retained snapshot save"); + statsBatchService.saveDailyRetainedSnapshot(); + log.info("[EntryStatsScheduler] Completed daily retained snapshot save"); + } } diff --git a/src/main/java/com/doubleo/passservice/domain/stats/service/StatsBatchService.java b/src/main/java/com/doubleo/passservice/domain/stats/service/StatsBatchService.java index 7c12074..a87dae8 100644 --- a/src/main/java/com/doubleo/passservice/domain/stats/service/StatsBatchService.java +++ b/src/main/java/com/doubleo/passservice/domain/stats/service/StatsBatchService.java @@ -7,4 +7,6 @@ public interface StatsBatchService { void updateWeeklyStats(); void updateMonthlyStats(); + + void saveDailyRetainedSnapshot(); } diff --git a/src/main/java/com/doubleo/passservice/domain/stats/service/StatsBatchServiceImpl.java b/src/main/java/com/doubleo/passservice/domain/stats/service/StatsBatchServiceImpl.java index 7cf0a99..526e6b9 100644 --- a/src/main/java/com/doubleo/passservice/domain/stats/service/StatsBatchServiceImpl.java +++ b/src/main/java/com/doubleo/passservice/domain/stats/service/StatsBatchServiceImpl.java @@ -1,10 +1,13 @@ package com.doubleo.passservice.domain.stats.service; import com.doubleo.passservice.domain.log.repository.BuildingEnterLogRepository; +import com.doubleo.passservice.domain.pass.enums.VisitCategory; +import com.doubleo.passservice.domain.stats.domain.DailyRetainedSnapshot; import com.doubleo.passservice.domain.stats.domain.EntryStatsDaily; import com.doubleo.passservice.domain.stats.domain.EntryStatsMonthly; import com.doubleo.passservice.domain.stats.domain.EntryStatsWeekly; import com.doubleo.passservice.domain.stats.dto.request.UpdateDailyEntryStatsRequest; +import com.doubleo.passservice.domain.stats.repository.DailyRetainedSnapshotRepository; import com.doubleo.passservice.domain.stats.repository.EntryStatsDailyRepository; import com.doubleo.passservice.domain.stats.repository.EntryStatsMonthlyRepository; import com.doubleo.passservice.domain.stats.repository.EntryStatsWeeklyRepository; @@ -28,6 +31,7 @@ public class StatsBatchServiceImpl implements StatsBatchService { private final EntryStatsDailyRepository entryStatsDailyRepository; private final EntryStatsWeeklyRepository entryStatsWeeklyRepository; private final EntryStatsMonthlyRepository entryStatsMonthlyRepository; + private final DailyRetainedSnapshotRepository dailyRetainedSnapshotRepository; private final TenantValidator tenantValidator; public void updateDailyStats() { @@ -95,4 +99,35 @@ public void updateMonthlyStats() { entryStatsMonthlyRepository.save(monthly); } + + @Override + public void saveDailyRetainedSnapshot() { + String tenantId = tenantValidator.getTenantId(); + + LocalDate snapshotDate = LocalDate.now().minusDays(1); + LocalDateTime start = snapshotDate.atStartOfDay(); + LocalDateTime end = start.plusDays(1); + + for (VisitCategory category : VisitCategory.values()) { + int base = + dailyRetainedSnapshotRepository + .findBySnapshotDateAndTenantIdAndVisitCategory( + snapshotDate.minusDays(1), tenantId, category) + .map(DailyRetainedSnapshot::getRetainedCount) + .orElse(0); + + int inCount = + buildingEnterLogRepository.countEnteredByCategory( + start, end, tenantId, category); + int outCount = + buildingEnterLogRepository.countExitedByCategory( + start, end, tenantId, category); + int retained = base + inCount - outCount; + + DailyRetainedSnapshot snapshot = + new DailyRetainedSnapshot(tenantId, snapshotDate, category, retained); + + dailyRetainedSnapshotRepository.save(snapshot); + } + } } diff --git a/src/main/java/com/doubleo/passservice/domain/stats/service/StatsServiceImpl.java b/src/main/java/com/doubleo/passservice/domain/stats/service/StatsServiceImpl.java index 11e355b..b4c07b0 100644 --- a/src/main/java/com/doubleo/passservice/domain/stats/service/StatsServiceImpl.java +++ b/src/main/java/com/doubleo/passservice/domain/stats/service/StatsServiceImpl.java @@ -1,12 +1,12 @@ package com.doubleo.passservice.domain.stats.service; -import com.doubleo.passservice.domain.log.domain.DailyRetainedSnapshot; import com.doubleo.passservice.domain.log.dto.response.HourlyEntryResponse; import com.doubleo.passservice.domain.log.repository.BuildingEnterLogRepository; -import com.doubleo.passservice.domain.log.repository.DailyRetainedSnapshotRepository; import com.doubleo.passservice.domain.pass.enums.VisitCategory; +import com.doubleo.passservice.domain.stats.domain.DailyRetainedSnapshot; import com.doubleo.passservice.domain.stats.domain.EntryStatsDaily; import com.doubleo.passservice.domain.stats.dto.response.*; +import com.doubleo.passservice.domain.stats.repository.DailyRetainedSnapshotRepository; import com.doubleo.passservice.domain.stats.repository.EntryStatsDailyRepository; import com.doubleo.passservice.domain.stats.repository.EntryStatsMonthlyRepository; import com.doubleo.passservice.domain.stats.repository.EntryStatsWeeklyRepository;