Skip to content

Commit c0b33c7

Browse files
authored
Merge pull request #250 from FunD-StockProject/feat/stock-month-avg
Feat/stock month avg
2 parents 0a8052f + 1a036bc commit c0b33c7

5 files changed

Lines changed: 156 additions & 0 deletions

File tree

src/main/java/com/fund/stockProject/score/repository/ScoreRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ public interface ScoreRepository extends JpaRepository<Score, Integer> {
105105
""")
106106
List<Score> findLatestScoresByStockIds(@Param("stockIds") List<Integer> stockIds);
107107

108+
List<Score> findByStockIdAndDateBetween(Integer stockId, LocalDate startDate, LocalDate endDate);
109+
108110
/**
109111
* 각 stock별 최신 유효 Score 데이터를 조회 (국내 종목용)
110112
* scoreOversea = 9999 이며 scoreKorea != 9999 인 데이터 중 최신

src/main/java/com/fund/stockProject/stock/controller/StockController.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@ ResponseEntity<StockDetailResponse> getStockInfo(final @PathVariable("id") Integ
9494
return ResponseEntity.ok().body(stockService.getStockDetailInfo(id, country));
9595
}
9696

97+
@GetMapping("/{id}/average/month")
98+
@Operation(summary = "월 평균 인간지표 점수", description = "지정한 월(yyyy-MM)의 평균 인간지표 점수를 반환합니다. 미지정 시 현재 월.")
99+
ResponseEntity<StockMonthlyAverageResponse> getMonthlyAverageScore(
100+
final @PathVariable("id") Integer id,
101+
@RequestParam(required = false) String yearMonth
102+
) {
103+
return ResponseEntity.ok().body(stockService.getMonthlyAverageScore(id, yearMonth));
104+
}
105+
97106
@GetMapping("/category/{category}/{country}")
98107
@Operation(summary = "종목 차트별 인간지표 api", description = "종목 차트별 인간지표 api")
99108
ResponseEntity<Mono<List<StockCategoryResponse>>> getCategoryStocks(final @PathVariable("category") CATEGORY category, final @PathVariable("country") COUNTRY country) {
@@ -181,6 +190,31 @@ public ResponseEntity<List<SectorAverageResponse>> getSectorAverageScores(
181190
return ResponseEntity.ok(stockService.getSectorAverageScores(country));
182191
}
183192

193+
@GetMapping("/sector/average/{country}/{sector}")
194+
@Operation(summary = "섹터 평균 점수(단일)", description = "선택한 섹터의 최신 평균 인간지표 점수를 반환합니다.")
195+
public ResponseEntity<SectorAverageResponse> getSectorAverageScore(
196+
@PathVariable("country") COUNTRY country,
197+
@PathVariable("sector") String sector
198+
) {
199+
String normalized = sector == null ? null : sector.trim().toUpperCase();
200+
if (normalized == null || normalized.isEmpty()) {
201+
return ResponseEntity.badRequest().build();
202+
}
203+
204+
try {
205+
if (country == COUNTRY.KOREA) {
206+
DomesticSector.valueOf(normalized);
207+
} else {
208+
OverseasSector.valueOf(normalized);
209+
}
210+
} catch (IllegalArgumentException e) {
211+
log.warn("잘못된 섹터 값: country={}, sector={}", country, sector);
212+
return ResponseEntity.badRequest().build();
213+
}
214+
215+
return ResponseEntity.ok(stockService.getSectorAverageScore(country, normalized));
216+
}
217+
184218
@GetMapping("/{id}/sector/percentile")
185219
@Operation(summary = "섹터 내 상위 퍼센트", description = "특정 종목이 해당 섹터에서 상위 몇 %인지 반환합니다.")
186220
public ResponseEntity<SectorPercentileResponse> getSectorPercentile(
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.fund.stockProject.stock.dto.response;
2+
3+
import com.fund.stockProject.stock.domain.COUNTRY;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
7+
@Getter
8+
public class StockMonthlyAverageResponse {
9+
10+
private final Integer stockId;
11+
private final String symbolName;
12+
private final COUNTRY country;
13+
private final String yearMonth; // yyyy-MM
14+
private final Integer dataCount;
15+
private final Double averageScore;
16+
17+
@Builder
18+
public StockMonthlyAverageResponse(Integer stockId, String symbolName, COUNTRY country,
19+
String yearMonth, Integer dataCount, Double averageScore) {
20+
this.stockId = stockId;
21+
this.symbolName = symbolName;
22+
this.country = country;
23+
this.yearMonth = yearMonth;
24+
this.dataCount = dataCount;
25+
this.averageScore = averageScore;
26+
}
27+
}

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,39 @@ public List<SectorAverageResponse> getLatestSectorAverages(COUNTRY country) {
3737
.toList())
3838
.orElseGet(List::of);
3939
}
40+
public SectorAverageResponse getLatestSectorAverage(COUNTRY country, String sectorKey) {
41+
String normalizedSector = sectorKey == null ? null : sectorKey.trim().toUpperCase();
42+
if (normalizedSector == null || normalizedSector.isEmpty()) {
43+
return SectorAverageResponse.builder()
44+
.sector(sectorKey)
45+
.sectorName(null)
46+
.averageScore(0)
47+
.count(0)
48+
.build();
49+
}
50+
51+
String sectorName = resolveSectorNameByKey(country, normalizedSector);
52+
return snapshotRepository.findLatestDateByCountry(country)
53+
.map(date -> snapshotRepository.findByCountryAndDateAndSector(country, date, normalizedSector)
54+
.map(snapshot -> SectorAverageResponse.builder()
55+
.sector(snapshot.getSector())
56+
.sectorName(snapshot.getSectorName())
57+
.averageScore(snapshot.getAverageScore())
58+
.count(snapshot.getCount())
59+
.build())
60+
.orElseGet(() -> SectorAverageResponse.builder()
61+
.sector(normalizedSector)
62+
.sectorName(sectorName)
63+
.averageScore(0)
64+
.count(0)
65+
.build()))
66+
.orElseGet(() -> SectorAverageResponse.builder()
67+
.sector(normalizedSector)
68+
.sectorName(sectorName)
69+
.averageScore(0)
70+
.count(0)
71+
.build());
72+
}
4073

4174
public void saveDailySnapshot(COUNTRY country, LocalDate date) {
4275
if (country == COUNTRY.KOREA) {
@@ -109,4 +142,14 @@ private String resolveSectorName(Enum<?> sector) {
109142
}
110143
return sector.name();
111144
}
145+
private String resolveSectorNameByKey(COUNTRY country, String sectorKey) {
146+
try {
147+
if (country == COUNTRY.KOREA) {
148+
return DomesticSector.valueOf(sectorKey).getName();
149+
}
150+
return OverseasSector.valueOf(sectorKey).getName();
151+
} catch (IllegalArgumentException e) {
152+
return sectorKey;
153+
}
154+
}
112155
}

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,17 @@ private Double parseDouble(String value) {
545545
}
546546
}
547547

548+
private double roundTo1Decimal(double value) {
549+
return Math.round(value * 10.0) / 10.0;
550+
}
551+
552+
private java.time.YearMonth parseYearMonthOrNow(String value) {
553+
if (value == null || value.isBlank()) {
554+
return java.time.YearMonth.now();
555+
}
556+
return java.time.YearMonth.parse(value.trim());
557+
}
558+
548559
/**
549560
* 국내/해외 떡상 지표 반환
550561
* @param country 국내/해외 분류
@@ -821,10 +832,49 @@ public StockDetailResponse getStockDetailInfo(Integer id, COUNTRY country) {
821832
.build();
822833
}
823834

835+
public StockMonthlyAverageResponse getMonthlyAverageScore(Integer id, String yearMonth) {
836+
Stock stock = stockRepository.findStockById(id)
837+
.orElseThrow(() -> new RuntimeException("no stock found"));
838+
839+
COUNTRY country = getCountryFromExchangeNum(stock.getExchangeNum());
840+
java.time.YearMonth targetMonth = parseYearMonthOrNow(yearMonth);
841+
LocalDate startDate = targetMonth.atDay(1);
842+
LocalDate endDate = targetMonth.atEndOfMonth();
843+
844+
List<Score> scores = scoreRepository.findByStockIdAndDateBetween(stock.getId(), startDate, endDate);
845+
846+
int sum = 0;
847+
int count = 0;
848+
if (scores != null) {
849+
for (Score score : scores) {
850+
int value = country == COUNTRY.KOREA ? score.getScoreKorea() : score.getScoreOversea();
851+
if (value == 9999) {
852+
continue;
853+
}
854+
sum += value;
855+
count++;
856+
}
857+
}
858+
859+
Double average = count == 0 ? null : roundTo1Decimal(sum / (double) count);
860+
return StockMonthlyAverageResponse.builder()
861+
.stockId(stock.getId())
862+
.symbolName(stock.getSymbolName())
863+
.country(country)
864+
.yearMonth(targetMonth.toString())
865+
.dataCount(count)
866+
.averageScore(average)
867+
.build();
868+
}
869+
824870
public List<SectorAverageResponse> getSectorAverageScores(COUNTRY country) {
825871
return sectorScoreSnapshotService.getLatestSectorAverages(country);
826872
}
827873

874+
public SectorAverageResponse getSectorAverageScore(COUNTRY country, String sectorKey) {
875+
return sectorScoreSnapshotService.getLatestSectorAverage(country, sectorKey);
876+
}
877+
828878
public SectorPercentileResponse getSectorPercentile(Integer stockId) {
829879
Stock stock = stockRepository.findStockById(stockId)
830880
.orElseThrow(() -> new RuntimeException("no stock found"));

0 commit comments

Comments
 (0)