Skip to content

Commit 4ada68f

Browse files
authored
Merge pull request #247 from FunD-StockProject/refactor/sector-avg-batch
Refactor: 섹터 평균 점수 배치 방식으로 전환
2 parents 1228412 + 6100f2b commit 4ada68f

5 files changed

Lines changed: 210 additions & 64 deletions

File tree

src/main/java/com/fund/stockProject/global/scheduler/ScoreUpdateScheduler.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.fund.stockProject.global.scheduler;
22

33
import com.fund.stockProject.score.service.ScoreBatchService;
4+
import com.fund.stockProject.stock.service.SectorScoreSnapshotService;
45
import com.fund.stockProject.stock.domain.COUNTRY;
56
import lombok.RequiredArgsConstructor;
67
import lombok.extern.slf4j.Slf4j;
@@ -13,6 +14,7 @@
1314
public class ScoreUpdateScheduler {
1415

1516
private final ScoreBatchService scoreBatchService;
17+
private final SectorScoreSnapshotService sectorScoreSnapshotService;
1618

1719
/**
1820
* 해외 점수&키워드 업데이트 스케줄러
@@ -22,6 +24,7 @@ public void processScoresOversea() {
2224
log.info("Starting oversea score batch scheduler");
2325
try {
2426
scoreBatchService.runCountryBatch(COUNTRY.OVERSEA);
27+
sectorScoreSnapshotService.saveDailySnapshot(COUNTRY.OVERSEA, java.time.LocalDate.now());
2528
log.info("Oversea score batch scheduler completed successfully");
2629
} catch (Exception e) {
2730
log.error("Oversea score batch scheduler failed", e);
@@ -37,6 +40,7 @@ public void processScoresKorea() {
3740
log.info("Starting korea score batch scheduler");
3841
try {
3942
scoreBatchService.runCountryBatch(COUNTRY.KOREA);
43+
sectorScoreSnapshotService.saveDailySnapshot(COUNTRY.KOREA, java.time.LocalDate.now());
4044
log.info("Korea score batch scheduler completed successfully");
4145
} catch (Exception e) {
4246
log.error("Korea score batch scheduler failed", e);
@@ -58,4 +62,4 @@ public void processIndexScores() {
5862
throw e;
5963
}
6064
}
61-
}
65+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.fund.stockProject.stock.entity;
2+
3+
import com.fund.stockProject.global.entity.Core;
4+
import com.fund.stockProject.stock.domain.COUNTRY;
5+
import jakarta.persistence.Column;
6+
import jakarta.persistence.Entity;
7+
import jakarta.persistence.EnumType;
8+
import jakarta.persistence.Enumerated;
9+
import jakarta.persistence.GeneratedValue;
10+
import jakarta.persistence.GenerationType;
11+
import jakarta.persistence.Id;
12+
import jakarta.persistence.Table;
13+
import jakarta.persistence.UniqueConstraint;
14+
import java.time.LocalDate;
15+
import lombok.AccessLevel;
16+
import lombok.Getter;
17+
import lombok.NoArgsConstructor;
18+
19+
@Entity
20+
@Getter
21+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
22+
@Table(
23+
name = "sector_score_snapshot",
24+
uniqueConstraints = @UniqueConstraint(
25+
name = "uk_sector_score_snapshot",
26+
columnNames = {"snapshot_date", "country", "sector"}
27+
)
28+
)
29+
public class SectorScoreSnapshot extends Core {
30+
31+
@Id
32+
@GeneratedValue(strategy = GenerationType.IDENTITY)
33+
private Long id;
34+
35+
@Column(name = "snapshot_date", nullable = false)
36+
private LocalDate date;
37+
38+
@Enumerated(EnumType.STRING)
39+
@Column(nullable = false)
40+
private COUNTRY country;
41+
42+
@Column(nullable = false, length = 50)
43+
private String sector;
44+
45+
@Column(nullable = false, length = 100)
46+
private String sectorName;
47+
48+
@Column(nullable = false)
49+
private Integer averageScore;
50+
51+
@Column(nullable = false)
52+
private Integer count;
53+
54+
public SectorScoreSnapshot(LocalDate date, COUNTRY country, String sector, String sectorName,
55+
Integer averageScore, Integer count) {
56+
this.date = date;
57+
this.country = country;
58+
this.sector = sector;
59+
this.sectorName = sectorName;
60+
this.averageScore = averageScore;
61+
this.count = count;
62+
}
63+
64+
public void update(Integer averageScore, Integer count, String sectorName) {
65+
this.averageScore = averageScore;
66+
this.count = count;
67+
this.sectorName = sectorName;
68+
}
69+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.fund.stockProject.stock.repository;
2+
3+
import com.fund.stockProject.stock.domain.COUNTRY;
4+
import com.fund.stockProject.stock.entity.SectorScoreSnapshot;
5+
import java.time.LocalDate;
6+
import java.util.List;
7+
import java.util.Optional;
8+
import org.springframework.data.jpa.repository.JpaRepository;
9+
import org.springframework.data.jpa.repository.Query;
10+
import org.springframework.data.repository.query.Param;
11+
import org.springframework.stereotype.Repository;
12+
13+
@Repository
14+
public interface SectorScoreSnapshotRepository extends JpaRepository<SectorScoreSnapshot, Long> {
15+
16+
@Query("SELECT MAX(s.date) FROM SectorScoreSnapshot s WHERE s.country = :country")
17+
Optional<LocalDate> findLatestDateByCountry(@Param("country") COUNTRY country);
18+
19+
List<SectorScoreSnapshot> findByCountryAndDate(COUNTRY country, LocalDate date);
20+
21+
Optional<SectorScoreSnapshot> findByCountryAndDateAndSector(COUNTRY country, LocalDate date, String sector);
22+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package com.fund.stockProject.stock.service;
2+
3+
import com.fund.stockProject.score.entity.Score;
4+
import com.fund.stockProject.score.repository.ScoreRepository;
5+
import com.fund.stockProject.stock.domain.COUNTRY;
6+
import com.fund.stockProject.stock.domain.DomesticSector;
7+
import com.fund.stockProject.stock.domain.OverseasSector;
8+
import com.fund.stockProject.stock.dto.response.SectorAverageResponse;
9+
import com.fund.stockProject.stock.entity.SectorScoreSnapshot;
10+
import com.fund.stockProject.stock.entity.Stock;
11+
import com.fund.stockProject.stock.repository.SectorScoreSnapshotRepository;
12+
import java.time.LocalDate;
13+
import java.util.EnumMap;
14+
import java.util.List;
15+
import java.util.Map;
16+
import lombok.RequiredArgsConstructor;
17+
import lombok.extern.slf4j.Slf4j;
18+
import org.springframework.stereotype.Service;
19+
20+
@Slf4j
21+
@Service
22+
@RequiredArgsConstructor
23+
public class SectorScoreSnapshotService {
24+
25+
private final ScoreRepository scoreRepository;
26+
private final SectorScoreSnapshotRepository snapshotRepository;
27+
28+
public List<SectorAverageResponse> getLatestSectorAverages(COUNTRY country) {
29+
return snapshotRepository.findLatestDateByCountry(country)
30+
.map(date -> snapshotRepository.findByCountryAndDate(country, date).stream()
31+
.map(snapshot -> SectorAverageResponse.builder()
32+
.sector(snapshot.getSector())
33+
.sectorName(snapshot.getSectorName())
34+
.averageScore(snapshot.getAverageScore())
35+
.count(snapshot.getCount())
36+
.build())
37+
.toList())
38+
.orElseGet(List::of);
39+
}
40+
41+
public void saveDailySnapshot(COUNTRY country, LocalDate date) {
42+
if (country == COUNTRY.KOREA) {
43+
List<Score> scores = scoreRepository.findLatestValidScoresByCountryKorea();
44+
Map<DomesticSector, long[]> stats = new EnumMap<>(DomesticSector.class);
45+
for (Score score : scores) {
46+
Stock stock = score.getStock();
47+
if (stock == null) {
48+
continue;
49+
}
50+
DomesticSector sector = stock.getDomesticSector();
51+
if (sector == null || sector == DomesticSector.UNKNOWN) {
52+
continue;
53+
}
54+
long[] acc = stats.computeIfAbsent(sector, key -> new long[2]);
55+
acc[0] += score.getScoreKorea();
56+
acc[1] += 1;
57+
}
58+
upsertSnapshots(country, date, stats);
59+
return;
60+
}
61+
62+
List<Score> scores = scoreRepository.findLatestValidScoresByCountryOversea();
63+
Map<OverseasSector, long[]> stats = new EnumMap<>(OverseasSector.class);
64+
for (Score score : scores) {
65+
Stock stock = score.getStock();
66+
if (stock == null) {
67+
continue;
68+
}
69+
OverseasSector sector = stock.getOverseasSector();
70+
if (sector == null || sector == OverseasSector.UNKNOWN) {
71+
continue;
72+
}
73+
long[] acc = stats.computeIfAbsent(sector, key -> new long[2]);
74+
acc[0] += score.getScoreOversea();
75+
acc[1] += 1;
76+
}
77+
upsertSnapshots(country, date, stats);
78+
}
79+
80+
private void upsertSnapshots(COUNTRY country, LocalDate date, Map<? extends Enum<?>, long[]> stats) {
81+
for (Map.Entry<? extends Enum<?>, long[]> entry : stats.entrySet()) {
82+
Enum<?> sector = entry.getKey();
83+
long[] acc = entry.getValue();
84+
long count = acc[1];
85+
if (count == 0) {
86+
continue;
87+
}
88+
int average = (int) Math.round(acc[0] / (double) count);
89+
String sectorKey = sector.name();
90+
String sectorName = resolveSectorName(sector);
91+
92+
SectorScoreSnapshot snapshot = snapshotRepository
93+
.findByCountryAndDateAndSector(country, date, sectorKey)
94+
.orElseGet(() -> new SectorScoreSnapshot(date, country, sectorKey, sectorName, average, (int) count));
95+
96+
snapshot.update(average, (int) count, sectorName);
97+
snapshotRepository.save(snapshot);
98+
}
99+
100+
log.info("Sector score snapshot saved: country={}, date={}, sectors={}", country, date, stats.size());
101+
}
102+
103+
private String resolveSectorName(Enum<?> sector) {
104+
if (sector instanceof DomesticSector domestic) {
105+
return domestic.getName();
106+
}
107+
if (sector instanceof OverseasSector overseas) {
108+
return overseas.getName();
109+
}
110+
return sector.name();
111+
}
112+
}

src/main/java/com/fund/stockProject/stock/service/StockService.java

Lines changed: 2 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import java.time.format.DateTimeFormatter;
3131
import java.util.ArrayList;
3232
import java.util.Comparator;
33-
import java.util.EnumMap;
3433
import java.util.HashMap;
3534
import java.util.List;
3635
import java.util.Map;
@@ -64,6 +63,7 @@ public class StockService {
6463
private final ObjectMapper objectMapper;
6564
private final KeywordRepository keywordRepository;
6665
private final SearchKeywordService searchKeywordService;
66+
private final SectorScoreSnapshotService sectorScoreSnapshotService;
6767

6868
private final int LIMITS = 9;
6969

@@ -822,13 +822,7 @@ public StockDetailResponse getStockDetailInfo(Integer id, COUNTRY country) {
822822
}
823823

824824
public List<SectorAverageResponse> getSectorAverageScores(COUNTRY country) {
825-
if (country == COUNTRY.KOREA) {
826-
List<Score> scores = scoreRepository.findLatestValidScoresByCountryKorea();
827-
return buildDomesticSectorAverages(scores);
828-
}
829-
830-
List<Score> scores = scoreRepository.findLatestValidScoresByCountryOversea();
831-
return buildOverseasSectorAverages(scores);
825+
return sectorScoreSnapshotService.getLatestSectorAverages(country);
832826
}
833827

834828
public SectorPercentileResponse getSectorPercentile(Integer stockId) {
@@ -875,61 +869,6 @@ public SectorPercentileResponse getSectorPercentile(Integer stockId) {
875869
return buildSectorPercentileResponse(stockId, sectorKey, sectorName, targetScore, sectorScores, false);
876870
}
877871

878-
private List<SectorAverageResponse> buildDomesticSectorAverages(List<Score> scores) {
879-
Map<DomesticSector, long[]> stats = new EnumMap<>(DomesticSector.class);
880-
for (Score score : scores) {
881-
Stock stock = score.getStock();
882-
if (stock == null) {
883-
continue;
884-
}
885-
DomesticSector sector = stock.getDomesticSector();
886-
if (sector == null || sector == DomesticSector.UNKNOWN) {
887-
continue;
888-
}
889-
int value = score.getScoreKorea();
890-
long[] acc = stats.computeIfAbsent(sector, key -> new long[2]);
891-
acc[0] += value;
892-
acc[1] += 1;
893-
}
894-
895-
return stats.entrySet().stream()
896-
.map(entry -> buildSectorAverageResponse(entry.getKey().name(), entry.getKey().getName(), entry.getValue()))
897-
.toList();
898-
}
899-
900-
private List<SectorAverageResponse> buildOverseasSectorAverages(List<Score> scores) {
901-
Map<OverseasSector, long[]> stats = new EnumMap<>(OverseasSector.class);
902-
for (Score score : scores) {
903-
Stock stock = score.getStock();
904-
if (stock == null) {
905-
continue;
906-
}
907-
OverseasSector sector = stock.getOverseasSector();
908-
if (sector == null || sector == OverseasSector.UNKNOWN) {
909-
continue;
910-
}
911-
int value = score.getScoreOversea();
912-
long[] acc = stats.computeIfAbsent(sector, key -> new long[2]);
913-
acc[0] += value;
914-
acc[1] += 1;
915-
}
916-
917-
return stats.entrySet().stream()
918-
.map(entry -> buildSectorAverageResponse(entry.getKey().name(), entry.getKey().getName(), entry.getValue()))
919-
.toList();
920-
}
921-
922-
private SectorAverageResponse buildSectorAverageResponse(String sectorKey, String sectorName, long[] acc) {
923-
long count = acc[1];
924-
int avgScore = count == 0 ? 0 : (int) Math.round(acc[0] / (double) count);
925-
return SectorAverageResponse.builder()
926-
.sector(sectorKey)
927-
.sectorName(sectorName)
928-
.averageScore(avgScore)
929-
.count((int) count)
930-
.build();
931-
}
932-
933872
private SectorPercentileResponse buildSectorPercentileResponse(
934873
Integer stockId,
935874
String sectorKey,

0 commit comments

Comments
 (0)