-
Notifications
You must be signed in to change notification settings - Fork 0
Feat: 주점 매출 순위 등락 기능 추가 #109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
029e5aa
feat: Redis 사용을 위한 의존성 추가
Jjiggu 83d543b
feat: QueryDSL 사용의 위한 의존성 추가
Jjiggu 2e547cb
feat(Statistics): Statistics 에러 메시지 추가
Jjiggu 502d1e3
feat(Statistics): 랭킹 repository Redis 구현 및 랭킹 조회/추가 기능 추가
Jjiggu 97169dd
feat(Statistics): 랭킹 조회 서비스 구현 및 등락 계산 로직 추가
Jjiggu 2518c63
feat(Statistics): 랭킹 통계 조회 서비스 구현 및 Redis-DB 연동
Jjiggu 465396f
feat(Statistics): 통계 커스텀 repository 구현 및 매출/랭킹 쿼리 추가
Jjiggu d94b13b
feat(Statistics): 통계 API 컨트롤러 구현 및 매출, 랭킹 조회 엔드포인트 추가
Jjiggu 079897e
feat(Statistics): 랭킹 스냅샷 주기적 갱신 스케줄러 구현
Jjiggu 8b9c3c6
feat(Statistics): 랭킹 스냅샷용 Redis 키 유틸 클래스 추가
Jjiggu cb73635
feat(Statistics): StatisticViewUnauthorizedException 추가
Jjiggu ed56093
feat(Statistics): RankingEntry DTO 클래스 추가
Jjiggu 3b96246
feat(Statistics): StoreInfo DTO 클래스 추가
Jjiggu d86d465
feat(Statistics): StoreSales DTO 클래스 추가
Jjiggu 65c0e4b
feat(Statistics): StoreRankingDto DTO 클래스 추가
Jjiggu ac16554
refactor(Order): 통계 관련 엔드포인트 삭제
Jjiggu a4a2b9b
refactor(Order): OrderCustomRepository 상속 제거
Jjiggu 2fbf75a
chore(Order): statistic 하위로 이동
Jjiggu ba33b26
chore(Order): statistic 하위로 이동
Jjiggu 5b8260a
refactor: redis 연결 설정 추가
Jjiggu ae8328a
refactor: null 안정성 문제 해결
Jjiggu c141084
refactor: StoreInfo DTO final 불변성 보장
Jjiggu eacc152
refactor: 메서드 분리 및 예외처리 추가
Jjiggu e04cb53
chore: QueryDSL Q클래스 생성 관련 TODO 어노테이션 추가
Jjiggu 5fca343
refactor: Redis 키 교체 과정에서 원자성 보장
Jjiggu c3c530a
refactor: 스케쥴러 설정 변경
Jjiggu 987130e
refactor: 크론 표현식 수정
Jjiggu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
61 changes: 61 additions & 0 deletions
61
.../src/main/java/com/nowait/applicationadmin/statistic/controller/StatisticsController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<StoreRankingDto> response = rankingService.getStatisticsRankings(memberDetails); | ||
| return ResponseEntity | ||
| .status(HttpStatus.OK) | ||
| .body( | ||
| ApiUtils.success( | ||
| response | ||
| ) | ||
| ); | ||
| } | ||
| } |
25 changes: 25 additions & 0 deletions
25
...pp-admin-api/src/main/java/com/nowait/applicationadmin/statistic/dto/StoreRankingDto.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
| } | ||
97 changes: 97 additions & 0 deletions
97
...rc/main/java/com/nowait/applicationadmin/statistic/scheduler/RankingRefreshScheduler.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| 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.RedisCallback; | ||
| 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; | ||
| 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; | ||
|
|
||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| @Slf4j | ||
| public class RankingRefreshScheduler { | ||
|
|
||
| private final StatisticCustomRepository statisticCustomRepository; | ||
| private final RankingQueryRepository rankingQueryRepository; | ||
| private final RedisTemplate<String, String> redis; | ||
|
|
||
| @PostConstruct | ||
| public void init() { | ||
| // 초기화 작업 | ||
| refresh(); | ||
| } | ||
|
|
||
| @Scheduled(cron = "0 */5 * * * *") // 매 5분마다 실행 | ||
| 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(); | ||
|
|
||
| // 1) 다음 스냅샷 키 초기화 | ||
| redis.delete(nextKey); | ||
|
|
||
| // 2) DB에서 매출 합계 가져와 ZADD | ||
| List<StoreSales> salesList = statisticCustomRepository.findTotalSales(); | ||
|
|
||
| if (salesList.isEmpty()) { | ||
| log.warn("매출 데이터가 없습니다. 다음 스냅샷 키를 초기화합니다."); | ||
| return; | ||
| } | ||
|
|
||
| salesList.forEach(s -> | ||
| rankingQueryRepository.addToRanking(nextKey, s.getStoreId(), s.getTotalSales()) | ||
| ); | ||
|
|
||
| // 3) 현재 스냅샷 키를 이전 스냅샷 키로 이동 | ||
| rotateKeys(currentKey, previousKey, nextKey); | ||
| } | ||
|
|
||
| private void rotateKeys(String currentKey, String previousKey, String nextKey) { | ||
| try { | ||
| redis.execute((RedisCallback<Object>)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); | ||
| throw new RuntimeException("랭킹 데이터 갱신 실패", e); | ||
| } | ||
| } | ||
| } |
10 changes: 10 additions & 0 deletions
10
...admin-api/src/main/java/com/nowait/applicationadmin/statistic/service/RankingService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<StoreRankingDto> getStatisticsRankings(MemberDetails memberDetails); | ||
| } |
75 changes: 75 additions & 0 deletions
75
.../src/main/java/com/nowait/applicationadmin/statistic/service/impl/RankingServiceImpl.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<StoreRankingDto> 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<RankingEntry> entries = rankingQuery.getRankings(userStoreId, 5); | ||
|
|
||
| // 2) DB에서 store 정보 가져오기 | ||
| List<Long> storeIds = entries.stream() | ||
| .map(RankingEntry::getStoreId) | ||
| .toList(); | ||
|
|
||
| List<StoreInfo> infos = statisticCustomRepository.findStoreInfoByIds(storeIds); | ||
|
|
||
| // StoreInfo를 storeId로 매핑 | ||
| Map<Long, StoreInfo> 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()); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
생성자에 null 체크 및 검증 로직 추가를 권장합니다.
현재 생성자에서 매개변수에 대한 null 체크나 유효성 검증이 없어 런타임 오류가 발생할 수 있습니다.
public StoreRankingDto(Long storeId, String storeName, Long departmentId, String departmentName, Integer totalSales, Long currentRank, Integer delta) { + Objects.requireNonNull(storeId, "storeId는 null일 수 없습니다"); + Objects.requireNonNull(storeName, "storeName은 null일 수 없습니다"); + Objects.requireNonNull(departmentId, "departmentId는 null일 수 없습니다"); + Objects.requireNonNull(departmentName, "departmentName은 null일 수 없습니다"); + Objects.requireNonNull(totalSales, "totalSales는 null일 수 없습니다"); + Objects.requireNonNull(currentRank, "currentRank는 null일 수 없습니다"); + Objects.requireNonNull(delta, "delta는 null일 수 없습니다"); this.storeId = storeId; this.storeName = storeName; this.departmentId = departmentId; this.departmentName = departmentName; this.totalSales = totalSales; this.currentRank = currentRank; this.delta = delta; }추가로 java.util.Objects import가 필요합니다:
+import java.util.Objects;🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
추후 해결 예정
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
추후 해결 예정
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Jjiggu 네, 이해했습니다. 현재 PR 범위를 벗어난 개선사항으로 추후 해결하실 계획이시군요.
혹시 이 null 체크 및 검증 로직 추가 작업을 추적하기 위해 새로운 이슈를 생성하시겠습니까? 추후 작업 시 참조하실 수 있도록 도와드릴 수 있습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Jjiggu 알겠습니다! 현재 PR의 핵심 기능에 집중하시는 것이 좋겠네요.
추후 이 개선사항을 추적하기 위해 별도 이슈를 생성해드릴까요? 아니면 나중에 직접 처리하실 예정인가요?
🐰
/| /|
( ✌️ ✌️ )
) (
( () )
( ( ) ) )
())
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이슈 만들어줘
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Length of output: 668
@Jjiggu 이슈가 성공적으로 생성되었습니다!
이슈 #110: StoreRankingDto 생성자에 null 체크 및 검증 로직 추가
🔗 #110
이슈에는 현재 PR과 코멘트 링크가 포함되어 있어 나중에 작업하실 때 참조하실 수 있습니다. 담당자로 설정해드렸습니다.