From 029e5aaf38b16a33fe5ec64f463cb00857f3e63b Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 19:35:59 +0900 Subject: [PATCH 01/27] =?UTF-8?q?feat:=20Redis=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nowait-app-admin-api/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nowait-app-admin-api/build.gradle b/nowait-app-admin-api/build.gradle index f6f6a509..028c8818 100644 --- a/nowait-app-admin-api/build.gradle +++ b/nowait-app-admin-api/build.gradle @@ -21,6 +21,7 @@ dependencies { implementation project(':nowait-infra') implementation project(':nowait-domain:domain-admin-rdb') implementation project(':nowait-domain:domain-core-rdb') + implementation project(':nowait-domain:domain-redis') // Spring Boot Starter implementation 'org.springframework.boot:spring-boot-starter-web' @@ -36,6 +37,9 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' + // redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + // SWAGGER implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' From 83d543b8b07fcf73d43b819a3059c50a037601c7 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 19:36:13 +0900 Subject: [PATCH 02/27] =?UTF-8?q?feat:=20QueryDSL=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9D=98=20=EC=9C=84=ED=95=9C=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nowait-domain/domain-admin-rdb/build.gradle | 21 ++++++++++++++++++--- nowait-domain/domain-core-rdb/build.gradle | 11 ++++++----- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/nowait-domain/domain-admin-rdb/build.gradle b/nowait-domain/domain-admin-rdb/build.gradle index bf111954..21a07f1f 100644 --- a/nowait-domain/domain-admin-rdb/build.gradle +++ b/nowait-domain/domain-admin-rdb/build.gradle @@ -27,12 +27,27 @@ dependencies { api 'org.springframework.boot:spring-boot-starter-data-jpa' api 'jakarta.persistence:jakarta.persistence-api:3.1.0' + // SPRING SECURITY + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + implementation 'org.springframework.boot:spring-boot-starter-validation' + + //QueryDsl + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + + // SWAGGER + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' + compileOnly 'org.projectlombok:lombok:1.18.26' annotationProcessor 'org.projectlombok:lombok:1.18.26' + // Jackson + api 'com.fasterxml.jackson.core:jackson-databind:2.15.2' + api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.2' + testImplementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' - - // SWAGGER - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' } diff --git a/nowait-domain/domain-core-rdb/build.gradle b/nowait-domain/domain-core-rdb/build.gradle index daa8ad3d..48bcad51 100644 --- a/nowait-domain/domain-core-rdb/build.gradle +++ b/nowait-domain/domain-core-rdb/build.gradle @@ -31,11 +31,6 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' implementation 'org.springframework.boot:spring-boot-starter-validation' - //QueryDsl - implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' - annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" - annotationProcessor "jakarta.annotation:jakarta.annotation-api" - annotationProcessor "jakarta.persistence:jakarta.persistence-api" // SWAGGER implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' @@ -43,6 +38,12 @@ dependencies { compileOnly 'org.projectlombok:lombok:1.18.26' annotationProcessor 'org.projectlombok:lombok:1.18.26' + //QueryDsl + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + // Jackson api 'com.fasterxml.jackson.core:jackson-databind:2.15.2' api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.2' From 2e547cbcf2e9841305b5c9c520a7297f565d3311 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 19:36:36 +0900 Subject: [PATCH 03/27] =?UTF-8?q?feat(Statistics):=20Statistics=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/nowait/common/exception/ErrorMessage.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nowait-common/src/main/java/com/nowait/common/exception/ErrorMessage.java b/nowait-common/src/main/java/com/nowait/common/exception/ErrorMessage.java index b64c5e37..fa83423b 100644 --- a/nowait-common/src/main/java/com/nowait/common/exception/ErrorMessage.java +++ b/nowait-common/src/main/java/com/nowait/common/exception/ErrorMessage.java @@ -50,7 +50,6 @@ public enum ErrorMessage { STORE_WAITING_DISABLED("해당 주점은 대기 비활성화된 주점입니다.", "store006"), // storePayment - STORE_PAYMENT_PARAMETER_EMPTY("주점 결제 생성 시 파라미터 정보가 없습니다.", "storePayment001"), STORE_PAYMENT_NOT_FOUND("해당 주점 결제 정보를 찾을 수 없습니다.", "storePayment002"), STORE_PAYMENT_VIEW_UNAUTHORIZED("주점 결제 정보 보기 권한이 없습니다.(슈퍼계정 or 주점 관리자만 가능)", "storePayment003"), @@ -59,6 +58,8 @@ public enum ErrorMessage { STORE_PAYMENT_DELETE_UNAUTHORIZED("주점 결제 정보 삭제 권한이 없습니다.(슈퍼계정 or 주점 관리자만 가능)", "storePayment005"), STORE_PAYMENT_ALREADY_EXISTS("이미 존재하는 주점 결제 정보입니다.", "storePayment006"), + // Statistics + STATISTIC_VIEW_UNAUTHORIZED("통계 보기 권한이 없습니다.(슈퍼계정 or 주점 관리자만 가능)", "statistics001"), // image IMAGE_FILE_EMPTY("이미지 파일을 업로드 해주세요", "image001"), From 502d1e322eea23ec04686129c56a8f55501bca92 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 19:42:43 +0900 Subject: [PATCH 04/27] =?UTF-8?q?feat(Statistics):=20=EB=9E=AD=ED=82=B9=20?= =?UTF-8?q?repository=20Redis=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EB=9E=AD?= =?UTF-8?q?=ED=82=B9=20=EC=A1=B0=ED=9A=8C/=EC=B6=94=EA=B0=80=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/RankingQueryRepository.java | 16 +++++ .../RankingQueryRepositoryImpl.java | 65 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/repository/RankingQueryRepository.java create mode 100644 nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/repository/RankingQueryRepositoryImpl.java diff --git a/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/repository/RankingQueryRepository.java b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/repository/RankingQueryRepository.java new file mode 100644 index 00000000..f056b146 --- /dev/null +++ b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/repository/RankingQueryRepository.java @@ -0,0 +1,16 @@ +package com.nowait.domaincoreredis.rank.repository; + +import java.util.List; + +import com.nowait.domaincoreredis.rank.dto.RankingEntry; + +public interface RankingQueryRepository { + + void addToRanking(String key, Long storeId, Integer totalSales); + + Long findPrevRank(String key, Long storeId); + + List findTopStores(String key, int topN); + + List findTopStoresWithUser(String key, Long userStoreId, int topN, List entries); +} diff --git a/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/repository/RankingQueryRepositoryImpl.java b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/repository/RankingQueryRepositoryImpl.java new file mode 100644 index 00000000..9fcc721d --- /dev/null +++ b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/repository/RankingQueryRepositoryImpl.java @@ -0,0 +1,65 @@ +package com.nowait.domaincoreredis.rank.repository; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import com.nowait.domaincoreredis.rank.dto.RankingEntry; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class RankingQueryRepositoryImpl implements RankingQueryRepository { + + private final StringRedisTemplate redisTemplate; + + @Override + public void addToRanking(String key, Long storeId, Integer totalSales) { + double totalSalesDouble = totalSales != null ? totalSales.doubleValue() : 0.0; + redisTemplate.opsForZSet().add(key, String.valueOf(storeId), totalSalesDouble); + } + + @Override + public Long findPrevRank(String key, Long storeId) { + return redisTemplate.opsForZSet().reverseRank(key, String.valueOf(storeId)); + } + + @Override + public List findTopStores(String key, int topN) { + var topTuples = redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, topN - 1); + + List entries = topTuples.stream() + .map(tuple -> new RankingEntry( + Long.parseLong(tuple.getValue()), + tuple.getScore().intValue(), + redisTemplate.opsForZSet().reverseRank(key, tuple.getValue()) + 1, + 0 + )) + .collect(Collectors.toCollection(ArrayList::new)); + + return entries; + } + + @Override + public List findTopStoresWithUser(String key, Long userStoreId, int topN, List entries) { + Long userZero = redisTemplate.opsForZSet().reverseRank(key, userStoreId.toString()); + + if (userZero != null && userZero >= topN) { + List top4 = entries.subList(0, topN - 1); + Integer userSales = redisTemplate.opsForZSet().score(key, userStoreId.toString()).intValue(); + + top4.add(new RankingEntry( + userStoreId, + userSales, + userZero + 1, + 0 // 초기 등락 값은 0으로 설정 + )); + entries = top4; + } + return entries; + } +} From 97169dd3bfba7831fdf5cd1add31559680443a7f Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 19:43:13 +0900 Subject: [PATCH 05/27] =?UTF-8?q?feat(Statistics):=20=EB=9E=AD=ED=82=B9=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20=EB=93=B1=EB=9D=BD=20=EA=B3=84=EC=82=B0?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rank/service/RankingQueryService.java | 13 ++++++ .../rank/service/RankingQueryServiceImpl.java | 44 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/service/RankingQueryService.java create mode 100644 nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/service/RankingQueryServiceImpl.java diff --git a/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/service/RankingQueryService.java b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/service/RankingQueryService.java new file mode 100644 index 00000000..5b0eb5b5 --- /dev/null +++ b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/service/RankingQueryService.java @@ -0,0 +1,13 @@ +package com.nowait.domaincoreredis.rank.service; + +import java.util.List; + +import com.nowait.domaincoreredis.rank.dto.RankingEntry; + +public interface RankingQueryService { + + /** + * 로그인한 사용자의 주점을 포함하여 상위 topN 순위와 등락 정보를 반환한다. + */ + List getRankings(Long userStoreId, int topN); +} diff --git a/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/service/RankingQueryServiceImpl.java b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/service/RankingQueryServiceImpl.java new file mode 100644 index 00000000..d3bc0050 --- /dev/null +++ b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/service/RankingQueryServiceImpl.java @@ -0,0 +1,44 @@ +package com.nowait.domaincoreredis.rank.service; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.nowait.domaincoreredis.common.util.RedisKeyUtils; +import com.nowait.domaincoreredis.rank.dto.RankingEntry; +import com.nowait.domaincoreredis.rank.repository.RankingQueryRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class RankingQueryServiceImpl implements RankingQueryService { + + private final RankingQueryRepository rankingQueryRepository; + + @Override + public List getRankings(Long userStoreId, int topN) { + // 1) 현재 스냅샷에서 상위 topN 주점 조회 + List entries = rankingQueryRepository.findTopStores(RedisKeyUtils.buildCurrentKey(), topN); + + // 2) 사용자 주점이 topN 밖이면 4위까지 + 사용자 주점 + entries = rankingQueryRepository.findTopStoresWithUser(RedisKeyUtils.buildCurrentKey(), userStoreId, topN, entries); + + // 3) 이전 스냅샷에서 등락 정보 조회 + for (int i = 0; i < entries.size(); i++) { + var entry = entries.get(i); + Long prevZero = rankingQueryRepository.findPrevRank(RedisKeyUtils.buildPreviousKey(), entry.getStoreId()); + long prevRank = (prevZero == null ? entry.getCurrentRank() : prevZero + 1); + int delta = (int)(prevRank - entry.getCurrentRank()); + + entries.set(i, new RankingEntry( + entry.getStoreId(), + entry.getTotalSales(), + entry.getCurrentRank(), + delta + )); + } + + return entries; + } +} From 2518c63a2a27dcfdfc2b712494df08a41a1dffa4 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 19:43:38 +0900 Subject: [PATCH 06/27] =?UTF-8?q?feat(Statistics):=20=EB=9E=AD=ED=82=B9=20?= =?UTF-8?q?=ED=86=B5=EA=B3=84=20=EC=A1=B0=ED=9A=8C=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20Redis-DB=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistic/service/RankingService.java | 10 +++ .../service/impl/RankingServiceImpl.java | 75 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/service/RankingService.java create mode 100644 nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/service/impl/RankingServiceImpl.java diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/service/RankingService.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/service/RankingService.java new file mode 100644 index 00000000..b34f2f8b --- /dev/null +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/service/RankingService.java @@ -0,0 +1,10 @@ +package com.nowait.applicationadmin.statistic.service; + +import java.util.List; + +import com.nowait.applicationadmin.statistic.dto.StoreRankingDto; +import com.nowait.domaincorerdb.user.entity.MemberDetails; + +public interface RankingService { + List getStatisticsRankings(MemberDetails memberDetails); +} diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/service/impl/RankingServiceImpl.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/service/impl/RankingServiceImpl.java new file mode 100644 index 00000000..9b5d764b --- /dev/null +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/service/impl/RankingServiceImpl.java @@ -0,0 +1,75 @@ +package com.nowait.applicationadmin.statistic.service.impl; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.nowait.applicationadmin.statistic.dto.StoreRankingDto; +import com.nowait.applicationadmin.statistic.service.RankingService; +import com.nowait.common.enums.Role; +import com.nowait.domainadminrdb.statistic.dto.StoreInfo; +import com.nowait.domainadminrdb.statistic.exception.StatisticViewUnauthorizedException; +import com.nowait.domainadminrdb.statistic.repository.StatisticCustomRepository; +import com.nowait.domaincorerdb.user.entity.MemberDetails; +import com.nowait.domaincorerdb.user.entity.User; +import com.nowait.domaincorerdb.user.exception.UserNotFoundException; +import com.nowait.domaincorerdb.user.repository.UserRepository; +import com.nowait.domaincoreredis.rank.dto.RankingEntry; +import com.nowait.domaincoreredis.rank.service.RankingQueryService; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class RankingServiceImpl implements RankingService { + + private final StatisticCustomRepository statisticCustomRepository; + private final UserRepository userRepository; + private final RankingQueryService rankingQuery; + + + @Override + @Transactional(readOnly = true) + public List getStatisticsRankings(MemberDetails memberDetails) { + User user = userRepository.findById(memberDetails.getId()).orElseThrow(UserNotFoundException::new); + Long userStoreId = user.getStoreId(); + + if (!Role.SUPER_ADMIN.equals(user.getRole()) && !user.getStoreId().equals(userStoreId)) { + throw new StatisticViewUnauthorizedException(); + } + + // 1) Redis에서 Top4+내주점: storeId, totalSales, currentRank, delta + List entries = rankingQuery.getRankings(userStoreId, 5); + + // 2) DB에서 store 정보 가져오기 + List storeIds = entries.stream() + .map(RankingEntry::getStoreId) + .toList(); + + List infos = statisticCustomRepository.findStoreInfoByIds(storeIds); + + // StoreInfo를 storeId로 매핑 + Map infoMap = infos.stream() + .collect(Collectors.toMap(StoreInfo::getStoreId, Function.identity())); + + // 3) 매핑 → 최종 DTO + return entries.stream() + .map(e -> { + StoreInfo info = infoMap.get(e.getStoreId()); + return new StoreRankingDto( + e.getStoreId(), + info.getStoreName(), + info.getDepartmentId(), + info.getDepartmentName(), + e.getTotalSales(), + e.getCurrentRank(), + e.getDelta() + ); + }) + .collect(Collectors.toList()); + } +} From 465396ff2de8ea567a51d03eb90b32af9e6a37fa Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 19:44:24 +0900 Subject: [PATCH 07/27] =?UTF-8?q?feat(Statistics):=20=ED=86=B5=EA=B3=84=20?= =?UTF-8?q?=EC=BB=A4=EC=8A=A4=ED=85=80=20repository=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20=EB=B0=8F=20=EB=A7=A4=EC=B6=9C/=EB=9E=AD=ED=82=B9=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/StatisticCustomRepository.java | 23 +++++++ .../StatisticCustomRepositoryImpl.java} | 66 +++++++++++++++++-- 2 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/repository/StatisticCustomRepository.java rename nowait-domain/{domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/repository/OrderCustomRepositoryImpl.java => domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/repository/StatisticCustomRepositoryImpl.java} (81%) diff --git a/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/repository/StatisticCustomRepository.java b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/repository/StatisticCustomRepository.java new file mode 100644 index 00000000..8c03bcdd --- /dev/null +++ b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/repository/StatisticCustomRepository.java @@ -0,0 +1,23 @@ +package com.nowait.domainadminrdb.statistic.repository; + +import java.util.List; + +import com.nowait.domainadminrdb.statistic.dto.OrderSalesSumDetail; +import com.nowait.domainadminrdb.statistic.dto.StoreInfo; +import com.nowait.domainadminrdb.statistic.dto.StoreSales; +import com.nowait.domainadminrdb.statistic.dto.TopSalesStoresDetail; + + + +public interface StatisticCustomRepository { + + OrderSalesSumDetail findSalesSumByStoreId(Long storeId); + + List getTop4PlusMine(Long storeId); + + + // redis 사용하는 부분 + List findTotalSales(); + + List findStoreInfoByIds(List storeIds); +} diff --git a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/repository/OrderCustomRepositoryImpl.java b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/repository/StatisticCustomRepositoryImpl.java similarity index 81% rename from nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/repository/OrderCustomRepositoryImpl.java rename to nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/repository/StatisticCustomRepositoryImpl.java index 341940bb..440c13a6 100644 --- a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/repository/OrderCustomRepositoryImpl.java +++ b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/repository/StatisticCustomRepositoryImpl.java @@ -1,6 +1,7 @@ -package com.nowait.domaincorerdb.order.repository; +package com.nowait.domainadminrdb.statistic.repository; import static com.nowait.domaincorerdb.order.entity.OrderStatus.*; +import static com.nowait.domaincorerdb.store.entity.QStore.*; import java.time.LocalDate; import java.time.LocalDateTime; @@ -12,24 +13,29 @@ import java.util.Set; import java.util.stream.Collectors; +import org.springframework.stereotype.Repository; + +import com.nowait.domainadminrdb.statistic.dto.OrderSalesSumDetail; +import com.nowait.domainadminrdb.statistic.dto.StoreInfo; +import com.nowait.domainadminrdb.statistic.dto.StoreSales; +import com.nowait.domainadminrdb.statistic.dto.TopSalesStoresDetail; import com.nowait.domaincorerdb.department.entity.QDepartment; -import com.nowait.domaincorerdb.order.dto.OrderSalesSumDetail; -import com.nowait.domaincorerdb.order.dto.TopSalesStoresDetail; import com.nowait.domaincorerdb.order.entity.QUserOrder; import com.nowait.domaincorerdb.store.entity.QStore; import com.querydsl.core.Tuple; import com.querydsl.jpa.impl.JPAQueryFactory; -public class OrderCustomRepositoryImpl implements OrderCustomRepository { +@Repository +public class StatisticCustomRepositoryImpl implements StatisticCustomRepository { private final JPAQueryFactory queryFactory; - public OrderCustomRepositoryImpl(JPAQueryFactory queryFactory) { + public StatisticCustomRepositoryImpl(JPAQueryFactory queryFactory) { this.queryFactory = queryFactory; } private static final QUserOrder u = QUserOrder.userOrder; - private static final QStore s = QStore.store; + private static final QStore s = store; private static final QDepartment d = QDepartment.department; @Override @@ -255,4 +261,52 @@ private Map getDepartmentNames(Set departmentIds) { } return departmentNameMap; } + + + + // redis 사용하는 부분 + @Override + public List findTotalSales() { + // 오늘 자정 + LocalDate today = LocalDate.now(); + LocalDateTime todayStart = today.atStartOfDay(); + LocalDateTime todayEnd = today.plusDays(1).atStartOfDay(); // 내일 00:00:00 + + List rows = queryFactory + .select(u.store.storeId, u.totalPrice.sum().coalesce(0)) + .from(u) + .where( + u.createdAt.goe(todayStart), + u.createdAt.lt(todayEnd), + u.status.eq(COOKED) + ) + .groupBy(u.store.storeId) + .fetch(); + + return rows.stream() + .map(t -> new StoreSales( + t.get(u.store.storeId), + t.get(u.totalPrice.sum().coalesce(0)) + )) + .collect(Collectors.toList()); + } + + @Override + public List findStoreInfoByIds(List storeIds) { + List tuples = queryFactory + .select(s.storeId, s.name, store.departmentId, d.name) + .from(store) + .join(d).on(store.departmentId.eq(d.id)) + .where(store.storeId.in(storeIds)) + .fetch(); + + return tuples.stream() + .map(t -> new StoreInfo( + t.get(store.storeId), + t.get(store.name), + t.get(store.departmentId), + t.get(d.name) + )) + .collect(Collectors.toList()); + } } From d94b13b8c179b6670fff78c290f12625d06ca21f Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 19:44:57 +0900 Subject: [PATCH 08/27] =?UTF-8?q?feat(Statistics):=20=ED=86=B5=EA=B3=84=20?= =?UTF-8?q?API=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20=EB=B0=8F=20=EB=A7=A4=EC=B6=9C,=20=EB=9E=AD=ED=82=B9=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StatisticsController.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/controller/StatisticsController.java diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/controller/StatisticsController.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/controller/StatisticsController.java new file mode 100644 index 00000000..0ad9f0e2 --- /dev/null +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/controller/StatisticsController.java @@ -0,0 +1,61 @@ +package com.nowait.applicationadmin.statistic.controller; + +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.nowait.applicationadmin.order.service.OrderService; +import com.nowait.applicationadmin.statistic.dto.StoreRankingDto; +import com.nowait.applicationadmin.statistic.service.RankingService; +import com.nowait.common.api.ApiUtils; +import com.nowait.domainadminrdb.statistic.dto.OrderSalesSumDetail; +import com.nowait.domaincorerdb.user.entity.MemberDetails; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@Tag(name = "Statistics API", description = "통계 API") +@RestController +@RequestMapping("/admin/statistics") +@RequiredArgsConstructor +public class StatisticsController { + + private final OrderService orderService; + private final RankingService rankingService; + + @GetMapping("/sales") + @Operation(summary = "오늘의 매출 조회", description = "오늘의 매출을 조회합니다.") + @ApiResponse(responseCode = "200", description = "오늘의 매출 조회 성공") + public ResponseEntity getTodaySales(@AuthenticationPrincipal MemberDetails memberDetails) { + OrderSalesSumDetail sales = orderService.getSaleSumByStoreId(memberDetails); + + return ResponseEntity + .status(HttpStatus.OK) + .body( + ApiUtils.success( + sales + ) + ); + } + + @GetMapping("/top-sales") + @Operation(summary = "주점별 매출 통계 조회", description = "주점별 매출 통계를 조회합니다.") + @ApiResponse(responseCode = "200", description = "주점별 매출 통계 조회 성공") + public ResponseEntity getTopSalesStores(@AuthenticationPrincipal MemberDetails memberDetails) { + List response = rankingService.getStatisticsRankings(memberDetails); + return ResponseEntity + .status(HttpStatus.OK) + .body( + ApiUtils.success( + response + ) + ); + } +} From 079897ef6c61791426f69ee64b37dca1618bce7b Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 19:45:22 +0900 Subject: [PATCH 09/27] =?UTF-8?q?feat(Statistics):=20=EB=9E=AD=ED=82=B9=20?= =?UTF-8?q?=EC=8A=A4=EB=83=85=EC=83=B7=20=EC=A3=BC=EA=B8=B0=EC=A0=81=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0=20=EC=8A=A4=EC=BC=80=EC=A4=84=EB=9F=AC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scheduler/RankingRefreshScheduler.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java new file mode 100644 index 00000000..bd1f47a8 --- /dev/null +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java @@ -0,0 +1,62 @@ +package com.nowait.applicationadmin.statistic.scheduler; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + +import com.nowait.domainadminrdb.statistic.dto.StoreSales; +import com.nowait.domainadminrdb.statistic.repository.StatisticCustomRepository; +import com.nowait.domaincoreredis.common.util.RedisKeyUtils; +import com.nowait.domaincoreredis.rank.repository.RankingQueryRepository; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Configuration +@EnableScheduling +@RequiredArgsConstructor +@Slf4j +public class RankingRefreshScheduler { + + private final StatisticCustomRepository statisticCustomRepository; + private final RankingQueryRepository rankingQueryRepository; + private final RedisTemplate redis; + + @PostConstruct + public void init() { + // 초기화 작업 + refresh(); + } + + @Scheduled(cron = "0 */5 * * * *") // 매 5분마다 실행 + public void refresh() { + log.info("RankingRefreshScheduler.refresh() called at {}", LocalDateTime.now()); + + String nextKey = RedisKeyUtils.buildNextKey(); + String currentKey = RedisKeyUtils.buildCurrentKey(); + String previousKey = RedisKeyUtils.buildPreviousKey(); + + // 1) 다음 스냅샷 키 초기화 + redis.delete(nextKey); + + // 2) DB에서 매출 합계 가져와 ZADD + List salesList = statisticCustomRepository.findTotalSales(); + + salesList.forEach(s -> + rankingQueryRepository.addToRanking(nextKey, s.getStoreId(), s.getTotalSales()) + ); + + // 3) 현재 스냅샷 키를 이전 스냅샷 키로 이동 + if (redis.hasKey(currentKey)) { + redis.rename(currentKey, previousKey); + } + if (redis.hasKey(nextKey)) { + redis.rename(nextKey, currentKey); + } + } +} From 8b9c3c698b7451d7c85f82966bb68bc0e061ee18 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 19:45:34 +0900 Subject: [PATCH 10/27] =?UTF-8?q?feat(Statistics):=20=EB=9E=AD=ED=82=B9=20?= =?UTF-8?q?=EC=8A=A4=EB=83=85=EC=83=B7=EC=9A=A9=20Redis=20=ED=82=A4=20?= =?UTF-8?q?=EC=9C=A0=ED=8B=B8=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/util/RedisKeyUtils.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java diff --git a/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java new file mode 100644 index 00000000..fd05a3cb --- /dev/null +++ b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java @@ -0,0 +1,24 @@ +package com.nowait.domaincoreredis.common.util; + +public class RedisKeyUtils { + + private static final String KEY_CURRENT = "nowait:store:rank:current"; + private static final String KEY_PREVIOUS = "nowait:store:rank:previous"; + private static final String KEY_NEXT = "nowait:store:rank:next"; + + private RedisKeyUtils() { + throw new UnsupportedOperationException("유틸리티 서비스는 인스턴스화 할 수 없습니다."); + } + + public static String buildCurrentKey() { + return KEY_CURRENT; + } + + public static String buildPreviousKey() { + return KEY_PREVIOUS; + } + + public static String buildNextKey() { + return KEY_NEXT; + } +} From cb736355fb9fcdc389877aaebb15591ea934979f Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 19:45:51 +0900 Subject: [PATCH 11/27] =?UTF-8?q?feat(Statistics):=20StatisticViewUnauthor?= =?UTF-8?q?izedException=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/StatisticViewUnauthorizedException.java | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/exception/StatisticViewUnauthorizedException.java diff --git a/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/exception/StatisticViewUnauthorizedException.java b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/exception/StatisticViewUnauthorizedException.java new file mode 100644 index 00000000..0757f643 --- /dev/null +++ b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/exception/StatisticViewUnauthorizedException.java @@ -0,0 +1,9 @@ +package com.nowait.domainadminrdb.statistic.exception; + +import com.nowait.common.exception.ErrorMessage; + +public class StatisticViewUnauthorizedException extends RuntimeException { + public StatisticViewUnauthorizedException() { + super(ErrorMessage.STATISTIC_VIEW_UNAUTHORIZED.getMessage()); + } +} From ed56093639d777ec0972cee93af6f71bd4489dad Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 19:46:15 +0900 Subject: [PATCH 12/27] =?UTF-8?q?feat(Statistics):=20RankingEntry=20DTO=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domaincoreredis/rank/dto/RankingEntry.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/dto/RankingEntry.java diff --git a/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/dto/RankingEntry.java b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/dto/RankingEntry.java new file mode 100644 index 00000000..9dcc2e45 --- /dev/null +++ b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/dto/RankingEntry.java @@ -0,0 +1,18 @@ +package com.nowait.domaincoreredis.rank.dto; + +import lombok.Getter; + +@Getter +public class RankingEntry { + private final Long storeId; + private final Integer totalSales; + private final Long currentRank; + private final Integer delta; + + public RankingEntry(Long storeId, Integer totalSales, Long currentRank, Integer delta) { + this.storeId = storeId; + this.totalSales = totalSales; + this.currentRank = currentRank; + this.delta = delta; + } +} From 3b96246cf7ba668d8c6f53342f1690c94246233c Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 19:46:23 +0900 Subject: [PATCH 13/27] =?UTF-8?q?feat(Statistics):=20StoreInfo=20DTO=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistic/dto/StoreInfo.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/dto/StoreInfo.java diff --git a/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/dto/StoreInfo.java b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/dto/StoreInfo.java new file mode 100644 index 00000000..5dd322a8 --- /dev/null +++ b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/dto/StoreInfo.java @@ -0,0 +1,18 @@ +package com.nowait.domainadminrdb.statistic.dto; + +import lombok.Getter; + +@Getter +public class StoreInfo { + private Long storeId; + private String storeName; + private Long departmentId; + private String departmentName; + + public StoreInfo(Long storeId, String storeName, Long departmentId, String departmentName) { + this.storeId = storeId; + this.storeName = storeName; + this.departmentId = departmentId; + this.departmentName = departmentName; + } +} From d86d465c7d9cd8c392d0445444b32642977900b9 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 19:46:29 +0900 Subject: [PATCH 14/27] =?UTF-8?q?feat(Statistics):=20StoreSales=20DTO=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domainadminrdb/statistic/dto/StoreSales.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/dto/StoreSales.java diff --git a/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/dto/StoreSales.java b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/dto/StoreSales.java new file mode 100644 index 00000000..442eacc8 --- /dev/null +++ b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/dto/StoreSales.java @@ -0,0 +1,14 @@ +package com.nowait.domainadminrdb.statistic.dto; + +import lombok.Getter; + +@Getter +public class StoreSales { + private final Long storeId; + private final Integer totalSales; + + public StoreSales(Long storeId, Integer totalSales) { + this.storeId = storeId; + this.totalSales = totalSales; + } +} From 65c0e4be291344e3e87ff333f8bfdd42768e7210 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 19:46:34 +0900 Subject: [PATCH 15/27] =?UTF-8?q?feat(Statistics):=20StoreRankingDto=20DTO?= =?UTF-8?q?=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistic/dto/StoreRankingDto.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/dto/StoreRankingDto.java diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/dto/StoreRankingDto.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/dto/StoreRankingDto.java new file mode 100644 index 00000000..a5d0601a --- /dev/null +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/dto/StoreRankingDto.java @@ -0,0 +1,25 @@ +package com.nowait.applicationadmin.statistic.dto; + +import lombok.Getter; + +@Getter +public class StoreRankingDto { + private final Long storeId; + private final String storeName; + private final Long departmentId; + private final String departmentName; + private final Integer totalSales; + private final Long currentRank; + private final Integer delta; + + public StoreRankingDto(Long storeId, String storeName, Long departmentId, String departmentName, Integer totalSales, + Long currentRank, Integer delta) { + this.storeId = storeId; + this.storeName = storeName; + this.departmentId = departmentId; + this.departmentName = departmentName; + this.totalSales = totalSales; + this.currentRank = currentRank; + this.delta = delta; + } +} From ac1655474e096add74f403b8823b0bd002507740 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 19:47:03 +0900 Subject: [PATCH 16/27] =?UTF-8?q?refactor(Order):=20=ED=86=B5=EA=B3=84=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8?= =?UTF-8?q?=ED=8A=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order/controller/OrderController.java | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java index 16246fbc..7022486b 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java @@ -17,8 +17,6 @@ import com.nowait.applicationadmin.order.dto.OrderStatusUpdateResponseDto; import com.nowait.applicationadmin.order.service.OrderService; import com.nowait.common.api.ApiUtils; -import com.nowait.domaincorerdb.order.dto.OrderSalesSumDetail; -import com.nowait.domaincorerdb.order.dto.TopSalesStoresDetail; import com.nowait.domaincorerdb.user.entity.MemberDetails; import io.swagger.v3.oas.annotations.Operation; @@ -65,34 +63,4 @@ public ResponseEntity updateOrderStatus( .status(HttpStatus.OK) .body(ApiUtils.success(response)); } - - @GetMapping("/sales") - @Operation(summary = "오늘의 매출 조회", description = "오늘의 매출을 조회합니다.") - @ApiResponse(responseCode = "200", description = "오늘의 매출 조회 성공") - public ResponseEntity getTodaySales(@AuthenticationPrincipal MemberDetails memberDetails) { - OrderSalesSumDetail sales = orderService.getSaleSumByStoreId(memberDetails); - - return ResponseEntity - .status(HttpStatus.OK) - .body( - ApiUtils.success( - sales - ) - ); - } - - @GetMapping("/top-sales") - @Operation(summary = "오늘의 매출 상위 5개 주점 조회", description = "오늘의 매출이 가장 높은 상위 5개 주점을 조회합니다.") - @ApiResponse(responseCode = "200", description = "오늘의 매출 상위 5개 주점 조회 성공") - public ResponseEntity getTopSalesStores(@AuthenticationPrincipal MemberDetails memberDetails) { - List topSalesStoresDetail = orderService.getTop5StoresBySalesToday(memberDetails); - - return ResponseEntity - .status(HttpStatus.OK) - .body( - ApiUtils.success( - topSalesStoresDetail - ) - ); - } } From a4a2b9beb4770cb172ae1f27068b4e2886c1761c Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 19:47:16 +0900 Subject: [PATCH 17/27] =?UTF-8?q?refactor(Order):=20OrderCustomRepository?= =?UTF-8?q?=20=EC=83=81=EC=86=8D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nowait/domaincorerdb/order/repository/OrderRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/repository/OrderRepository.java b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/repository/OrderRepository.java index 54c2b7b6..b6b0a12d 100644 --- a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/repository/OrderRepository.java +++ b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/repository/OrderRepository.java @@ -9,7 +9,7 @@ import com.nowait.domaincorerdb.order.entity.UserOrder; @Repository -public interface OrderRepository extends JpaRepository, OrderCustomRepository { +public interface OrderRepository extends JpaRepository { boolean existsBySignatureAndCreatedAtAfter(String signature, LocalDateTime createdAt); List findByStore_StoreIdAndTableIdAndSessionId(Long storeId, Long tableId, String sessionId); From 2fbf75aa060b8d00ff5ccd848cbc8d6cf7ce7875 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 19:48:24 +0900 Subject: [PATCH 18/27] =?UTF-8?q?chore(Order):=20statistic=20=ED=95=98?= =?UTF-8?q?=EC=9C=84=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order/service/OrderService.java | 10 ++++++---- .../statistic}/dto/OrderSalesSumDetail.java | 2 +- .../statistic}/dto/TopSalesStoresDetail.java | 2 +- .../order/repository/OrderCustomRepository.java | 13 ------------- .../service/StoreRankCacheService.java | 10 ---------- 5 files changed, 8 insertions(+), 29 deletions(-) rename nowait-domain/{domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order => domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic}/dto/OrderSalesSumDetail.java (91%) rename nowait-domain/{domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order => domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic}/dto/TopSalesStoresDetail.java (92%) delete mode 100644 nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/repository/OrderCustomRepository.java delete mode 100644 nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/service/StoreRankCacheService.java diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java index c0d3db14..9f808caa 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java @@ -9,8 +9,9 @@ import com.nowait.applicationadmin.order.dto.OrderResponseDto; import com.nowait.applicationadmin.order.dto.OrderStatusUpdateResponseDto; import com.nowait.common.enums.Role; -import com.nowait.domaincorerdb.order.dto.OrderSalesSumDetail; -import com.nowait.domaincorerdb.order.dto.TopSalesStoresDetail; +import com.nowait.domainadminrdb.statistic.dto.OrderSalesSumDetail; +import com.nowait.domainadminrdb.statistic.dto.TopSalesStoresDetail; +import com.nowait.domainadminrdb.statistic.repository.StatisticCustomRepository; import com.nowait.domaincorerdb.order.entity.OrderStatus; import com.nowait.domaincorerdb.order.entity.UserOrder; import com.nowait.domaincorerdb.order.exception.OrderNotFoundException; @@ -30,6 +31,7 @@ @RequiredArgsConstructor public class OrderService { private final OrderRepository orderRepository; + private final StatisticCustomRepository statisticCustomRepository; private final UserRepository userRepository; private final StoreRepository storeRepository; @@ -67,7 +69,7 @@ public OrderSalesSumDetail getSaleSumByStoreId(MemberDetails memberDetails) { throw new OrderViewUnauthorizedException(); } - return orderRepository.findSalesSumByStoreId(storeId); + return statisticCustomRepository.findSalesSumByStoreId(storeId); } @Transactional(readOnly = true) @@ -79,6 +81,6 @@ public List getTop5StoresBySalesToday(MemberDetails member throw new OrderViewUnauthorizedException(); } - return orderRepository.getTop4PlusMine(storeId); + return statisticCustomRepository.getTop4PlusMine(storeId); } } diff --git a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/dto/OrderSalesSumDetail.java b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/dto/OrderSalesSumDetail.java similarity index 91% rename from nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/dto/OrderSalesSumDetail.java rename to nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/dto/OrderSalesSumDetail.java index fe3b25ee..e3ac3ef8 100644 --- a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/dto/OrderSalesSumDetail.java +++ b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/dto/OrderSalesSumDetail.java @@ -1,4 +1,4 @@ -package com.nowait.domaincorerdb.order.dto; +package com.nowait.domainadminrdb.statistic.dto; import lombok.Builder; import lombok.Getter; diff --git a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/dto/TopSalesStoresDetail.java b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/dto/TopSalesStoresDetail.java similarity index 92% rename from nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/dto/TopSalesStoresDetail.java rename to nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/dto/TopSalesStoresDetail.java index acc0d2d3..156e0249 100644 --- a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/dto/TopSalesStoresDetail.java +++ b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/dto/TopSalesStoresDetail.java @@ -1,4 +1,4 @@ -package com.nowait.domaincorerdb.order.dto; +package com.nowait.domainadminrdb.statistic.dto; import lombok.Builder; import lombok.Getter; diff --git a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/repository/OrderCustomRepository.java b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/repository/OrderCustomRepository.java deleted file mode 100644 index 77d2747d..00000000 --- a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/repository/OrderCustomRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.nowait.domaincorerdb.order.repository; - -import java.util.List; - -import com.nowait.domaincorerdb.order.dto.OrderSalesSumDetail; -import com.nowait.domaincorerdb.order.dto.TopSalesStoresDetail; - -public interface OrderCustomRepository { - - OrderSalesSumDetail findSalesSumByStoreId(Long storeId); - - List getTop4PlusMine(Long storeId); -} diff --git a/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/service/StoreRankCacheService.java b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/service/StoreRankCacheService.java deleted file mode 100644 index 444aedcf..00000000 --- a/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/service/StoreRankCacheService.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.nowait.domaincoreredis.service; - -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Service; - -@Service -public class StoreRankCacheService { - - private RedisTemplate redisTemplate; -} From ba33b2657bb9fe7ac01f6e1dcdcf82a2967ed57d Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 19:48:28 +0900 Subject: [PATCH 19/27] =?UTF-8?q?chore(Order):=20statistic=20=ED=95=98?= =?UTF-8?q?=EC=9C=84=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/nowait/domainadminrdb}/config/QueryDslConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename nowait-domain/{domain-core-rdb/src/main/java/com/nowait/domaincorerdb => domain-admin-rdb/src/main/java/com/nowait/domainadminrdb}/config/QueryDslConfig.java (91%) diff --git a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/config/QueryDslConfig.java b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/config/QueryDslConfig.java similarity index 91% rename from nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/config/QueryDslConfig.java rename to nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/config/QueryDslConfig.java index f75d497b..6c04df39 100644 --- a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/config/QueryDslConfig.java +++ b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/config/QueryDslConfig.java @@ -1,4 +1,4 @@ -package com.nowait.domaincorerdb.config; +package com.nowait.domainadminrdb.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; From 5b8260a6963f5d154b84888567097f011fdf503e Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 20:18:56 +0900 Subject: [PATCH 20/27] =?UTF-8?q?refactor:=20redis=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/nowait/domaincoreredis/config/RedisConfig.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/config/RedisConfig.java b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/config/RedisConfig.java index 57e8b919..b0967e0f 100644 --- a/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/config/RedisConfig.java +++ b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/config/RedisConfig.java @@ -4,6 +4,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.StringRedisSerializer; @@ -16,9 +17,16 @@ public class RedisConfig { @Value("${spring.data.redis.port}") private int port; + @Value("${spring.data.redis.password}") + private String password; + @Bean public RedisConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory(host, port); + RedisStandaloneConfiguration redisConfiguration = new RedisStandaloneConfiguration(); + redisConfiguration.setHostName(host); + redisConfiguration.setPort(port); + redisConfiguration.setPassword(password); + return new LettuceConnectionFactory(redisConfiguration); } @Bean From ae8328ac0b55257e256df8750bfc2762c9b3682a Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 20:50:51 +0900 Subject: [PATCH 21/27] =?UTF-8?q?refactor:=20null=20=EC=95=88=EC=A0=95?= =?UTF-8?q?=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rank/repository/RankingQueryRepositoryImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/repository/RankingQueryRepositoryImpl.java b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/repository/RankingQueryRepositoryImpl.java index 9fcc721d..fa792f2f 100644 --- a/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/repository/RankingQueryRepositoryImpl.java +++ b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/repository/RankingQueryRepositoryImpl.java @@ -50,7 +50,8 @@ public List findTopStoresWithUser(String key, Long userStoreId, in if (userZero != null && userZero >= topN) { List top4 = entries.subList(0, topN - 1); - Integer userSales = redisTemplate.opsForZSet().score(key, userStoreId.toString()).intValue(); + Double userScore = redisTemplate.opsForZSet().score(key, userStoreId.toString()); + Integer userSales = userScore != null ? userScore.intValue() : 0; top4.add(new RankingEntry( userStoreId, From c14108410384c5647ed4926259c1d8577f5c7c60 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 20:51:58 +0900 Subject: [PATCH 22/27] =?UTF-8?q?refactor:=20StoreInfo=20DTO=20final=20?= =?UTF-8?q?=EB=B6=88=EB=B3=80=EC=84=B1=20=EB=B3=B4=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nowait/domainadminrdb/statistic/dto/StoreInfo.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/dto/StoreInfo.java b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/dto/StoreInfo.java index 5dd322a8..893503bf 100644 --- a/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/dto/StoreInfo.java +++ b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/statistic/dto/StoreInfo.java @@ -4,10 +4,10 @@ @Getter public class StoreInfo { - private Long storeId; - private String storeName; - private Long departmentId; - private String departmentName; + private final Long storeId; + private final String storeName; + private final Long departmentId; + private final String departmentName; public StoreInfo(Long storeId, String storeName, Long departmentId, String departmentName) { this.storeId = storeId; From eacc152622ce086b0d3f5cc99d7e3f5d321f6169 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 20:52:12 +0900 Subject: [PATCH 23/27] =?UTF-8?q?refactor:=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EC=98=88=EC=99=B8=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scheduler/RankingRefreshScheduler.java | 34 +++++++++++-- .../rank/service/RankingQueryServiceImpl.java | 50 +++++++++++-------- 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java index bd1f47a8..ebe363d5 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java @@ -37,6 +37,15 @@ public void init() { public void refresh() { log.info("RankingRefreshScheduler.refresh() called at {}", LocalDateTime.now()); + try { + doRefresh(); + } catch (Exception e) { + log.error("랭킹 데이터 갱신 중 오류 발생", e); + // 예외 발생 시 알림 또는 로깅 처리 + } + } + + private void doRefresh() { String nextKey = RedisKeyUtils.buildNextKey(); String currentKey = RedisKeyUtils.buildCurrentKey(); String previousKey = RedisKeyUtils.buildPreviousKey(); @@ -47,16 +56,31 @@ public void refresh() { // 2) DB에서 매출 합계 가져와 ZADD List salesList = statisticCustomRepository.findTotalSales(); + if (salesList.isEmpty()) { + log.warn("매출 데이터가 없습니다. 다음 스냅샷 키를 초기화합니다."); + return; + } + salesList.forEach(s -> rankingQueryRepository.addToRanking(nextKey, s.getStoreId(), s.getTotalSales()) ); // 3) 현재 스냅샷 키를 이전 스냅샷 키로 이동 - if (redis.hasKey(currentKey)) { - redis.rename(currentKey, previousKey); - } - if (redis.hasKey(nextKey)) { - redis.rename(nextKey, currentKey); + rotateKeys(currentKey, previousKey, nextKey); + } + + private void rotateKeys(String currentKey, String previousKey, String nextKey) { + try { + if (redis.hasKey(currentKey)) { + redis.rename(currentKey, previousKey); + } + if (redis.hasKey(nextKey)) { + redis.rename(nextKey, currentKey); + } + log.info("Keys rotated: current -> {}, previous -> {}, next -> {}", currentKey, previousKey, nextKey); + } catch (Exception e) { + log.error("Redis 키 교체 중 오류 발생", e); + throw new RuntimeException("랭킹 데이터 갱신 실패", e); } } } diff --git a/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/service/RankingQueryServiceImpl.java b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/service/RankingQueryServiceImpl.java index d3bc0050..7718f358 100644 --- a/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/service/RankingQueryServiceImpl.java +++ b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/service/RankingQueryServiceImpl.java @@ -1,6 +1,7 @@ package com.nowait.domaincoreredis.rank.service; import java.util.List; +import java.util.stream.Collectors; import org.springframework.stereotype.Service; @@ -19,26 +20,33 @@ public class RankingQueryServiceImpl implements RankingQueryService { @Override public List getRankings(Long userStoreId, int topN) { // 1) 현재 스냅샷에서 상위 topN 주점 조회 - List entries = rankingQueryRepository.findTopStores(RedisKeyUtils.buildCurrentKey(), topN); - - // 2) 사용자 주점이 topN 밖이면 4위까지 + 사용자 주점 - entries = rankingQueryRepository.findTopStoresWithUser(RedisKeyUtils.buildCurrentKey(), userStoreId, topN, entries); - - // 3) 이전 스냅샷에서 등락 정보 조회 - for (int i = 0; i < entries.size(); i++) { - var entry = entries.get(i); - Long prevZero = rankingQueryRepository.findPrevRank(RedisKeyUtils.buildPreviousKey(), entry.getStoreId()); - long prevRank = (prevZero == null ? entry.getCurrentRank() : prevZero + 1); - int delta = (int)(prevRank - entry.getCurrentRank()); - - entries.set(i, new RankingEntry( - entry.getStoreId(), - entry.getTotalSales(), - entry.getCurrentRank(), - delta - )); - } - - return entries; + List entries = getCurrentRankings(userStoreId, topN); + // 2) 이전 스냅샷에서 등락 정보 조회 + return calculateRankingDeltas(entries); + } + + private List getCurrentRankings(Long userStoreId, int topN) { + String currentKey = RedisKeyUtils.buildCurrentKey(); + List entries = rankingQueryRepository.findTopStores(currentKey, topN); + return rankingQueryRepository.findTopStoresWithUser(currentKey, userStoreId, topN, entries); + } + + private List calculateRankingDeltas(List entries) { + String previousKey = RedisKeyUtils.buildPreviousKey(); + return entries.stream() + .map(entry -> calculateDeltaForEntry(entry, previousKey)) + .collect(Collectors.toList()); + } + + private RankingEntry calculateDeltaForEntry(RankingEntry entry, String previousKey) { + Long prevZero = rankingQueryRepository.findPrevRank(previousKey, entry.getStoreId()); + long prevRank = (prevZero == null ? entry.getCurrentRank() : prevZero + 1); + int delta = (int)(prevRank - entry.getCurrentRank()); + return new RankingEntry( + entry.getStoreId(), + entry.getTotalSales(), + entry.getCurrentRank(), + delta + ); } } From e04cb537fa37f7812a02f78b23e6cd99f151e05b Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 20:54:02 +0900 Subject: [PATCH 24/27] =?UTF-8?q?chore:=20QueryDSL=20Q=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=83=9D=EC=84=B1=20=EA=B4=80=EB=A0=A8=20TODO=20?= =?UTF-8?q?=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nowait-domain/domain-core-rdb/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/nowait-domain/domain-core-rdb/build.gradle b/nowait-domain/domain-core-rdb/build.gradle index 48bcad51..954fbd42 100644 --- a/nowait-domain/domain-core-rdb/build.gradle +++ b/nowait-domain/domain-core-rdb/build.gradle @@ -39,6 +39,7 @@ dependencies { annotationProcessor 'org.projectlombok:lombok:1.18.26' //QueryDsl + // TODO Q클래스 생성시 오류 발생 해결 필요 implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" annotationProcessor "jakarta.annotation:jakarta.annotation-api" From 5fca343d4153f34b5942fcbd552f4e7fdb5a820d Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 20:58:14 +0900 Subject: [PATCH 25/27] =?UTF-8?q?refactor:=20Redis=20=ED=82=A4=20=EA=B5=90?= =?UTF-8?q?=EC=B2=B4=20=EA=B3=BC=EC=A0=95=EC=97=90=EC=84=9C=20=EC=9B=90?= =?UTF-8?q?=EC=9E=90=EC=84=B1=20=EB=B3=B4=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scheduler/RankingRefreshScheduler.java | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java index ebe363d5..4e398c64 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java @@ -4,6 +4,7 @@ import java.util.List; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; @@ -33,7 +34,7 @@ public void init() { refresh(); } - @Scheduled(cron = "0 */5 * * * *") // 매 5분마다 실행 + @Scheduled(cron = "* */5 * * * *") // 매 5분마다 실행 public void refresh() { log.info("RankingRefreshScheduler.refresh() called at {}", LocalDateTime.now()); @@ -71,12 +72,21 @@ private void doRefresh() { private void rotateKeys(String currentKey, String previousKey, String nextKey) { try { - if (redis.hasKey(currentKey)) { - redis.rename(currentKey, previousKey); - } - if (redis.hasKey(nextKey)) { - redis.rename(nextKey, currentKey); - } + redis.execute((RedisCallback)connection -> { + // 현재 스냅샷 키를 이전 스냅샷 키로 이동하고, 다음 스냅샷 키를 현재 스냅샷 키로 이동 + if (redis.hasKey(previousKey)) { + redis.delete(previousKey); + } + // 현재 스냅샷 키를 다음 스냅샷 키로 이동 + if (redis.hasKey(currentKey)) { + redis.rename(currentKey, previousKey); + } + // 다음 스냅샷 키가 존재하면 현재 스냅샷 키로 이동 + if (redis.hasKey(nextKey)) { + redis.rename(nextKey, currentKey); + } + return null; + }); log.info("Keys rotated: current -> {}, previous -> {}, next -> {}", currentKey, previousKey, nextKey); } catch (Exception e) { log.error("Redis 키 교체 중 오류 발생", e); From c3c530a8a1d1fefdcfa4cf6a6717bb023de434f8 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 22:52:27 +0900 Subject: [PATCH 26/27] =?UTF-8?q?refactor:=20=EC=8A=A4=EC=BC=80=EC=A5=B4?= =?UTF-8?q?=EB=9F=AC=20=EC=84=A4=EC=A0=95=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/nowait/ApiAdminApplication.java | 2 ++ .../statistic/scheduler/RankingRefreshScheduler.java | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/nowait-app-admin-api/src/main/java/com/nowait/ApiAdminApplication.java b/nowait-app-admin-api/src/main/java/com/nowait/ApiAdminApplication.java index 1098599d..b538931a 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/ApiAdminApplication.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/ApiAdminApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; @EnableJpaAuditing +@EnableScheduling @SpringBootApplication public class ApiAdminApplication { public static void main(String[] args) { diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java index 4e398c64..c8de3f59 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java @@ -8,6 +8,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; import com.nowait.domainadminrdb.statistic.dto.StoreSales; import com.nowait.domainadminrdb.statistic.repository.StatisticCustomRepository; @@ -18,8 +19,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -@Configuration -@EnableScheduling + +@Component @RequiredArgsConstructor @Slf4j public class RankingRefreshScheduler { From 987130e24cb455a620818b4709d3ef57638df9f7 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Wed, 16 Jul 2025 22:56:01 +0900 Subject: [PATCH 27/27] =?UTF-8?q?refactor:=20=ED=81=AC=EB=A1=A0=20?= =?UTF-8?q?=ED=91=9C=ED=98=84=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistic/scheduler/RankingRefreshScheduler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java index c8de3f59..ed81d051 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java @@ -35,7 +35,7 @@ public void init() { refresh(); } - @Scheduled(cron = "* */5 * * * *") // 매 5분마다 실행 + @Scheduled(cron = "0 */5 * * * *") // 매 5분마다 실행 public void refresh() { log.info("RankingRefreshScheduler.refresh() called at {}", LocalDateTime.now());