Skip to content

Commit bbe4b4c

Browse files
authored
Merge pull request #238 from FunD-StockProject/refactor/sector-paging-korea
Refactor: 섹터별 국내 종목 추천 페이징으로 여러 개 리턴하도록 변경
2 parents 7ea01f9 + 6e52f9c commit bbe4b4c

2 files changed

Lines changed: 94 additions & 25 deletions

File tree

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

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
import com.fund.stockProject.stock.domain.DomesticSector;
66
import com.fund.stockProject.stock.domain.OverseasSector;
77
import com.fund.stockProject.stock.dto.response.*;
8-
import com.fund.stockProject.stock.entity.Stock;
98
import com.fund.stockProject.stock.service.StockService;
10-
import com.fund.stockProject.stock.service.SecurityService;
119
import com.fund.stockProject.shortview.dto.ShortViewResponse;
1210
import com.fund.stockProject.common.dto.PageResponse;
1311

@@ -39,7 +37,6 @@
3937
public class StockController {
4038

4139
private final StockService stockService;
42-
private final SecurityService securityService;
4340

4441
@GetMapping("/search/{searchKeyword}/{country}")
4542
@Operation(summary = "주식 종목 검색 API", description = "주식 종목 및 인간지표 데이터 검색")
@@ -116,10 +113,12 @@ ResponseEntity<Mono<List<String>>> getSummarys(@PathVariable("symbol") String sy
116113
}
117114

118115
@GetMapping("/sector/domestic/{sector}/recommend")
119-
@Operation(summary = "국내 섹터별 주식 추천", description = "특정 국내 섹터의 주식을 점수 기반으로 추천합니다. 실시간 시세 조회 실패 시 가격 필드는 null로 반환됩니다.")
120-
public ResponseEntity<ShortViewResponse> getRecommendationByDomesticSector(
116+
@Operation(summary = "국내 섹터별 주식 추천 (페이징)", description = "특정 국내 섹터의 주식을 점수 기반으로 추천합니다. 기본 정렬은 인간지표(score) 내림차순이며, 각 항목의 가격은 가능하면 실시간으로 조회해 포함하고 실패 시 null로 반환됩니다.")
117+
public ResponseEntity<PageResponse<ShortViewResponse>> getRecommendationByDomesticSector(
121118
@io.swagger.v3.oas.annotations.Parameter(description = "추천할 국내 섹터", example = "RETAIL", required = true)
122-
@PathVariable String sector
119+
@PathVariable String sector,
120+
@RequestParam(required = false, defaultValue = "0") int page,
121+
@RequestParam(required = false, defaultValue = "20") int size
123122
) {
124123
try {
125124
// DomesticSector enum으로 변환
@@ -131,25 +130,13 @@ public ResponseEntity<ShortViewResponse> getRecommendationByDomesticSector(
131130
return ResponseEntity.badRequest().build();
132131
}
133132

134-
log.info("DomesticSector({})별 추천을 요청했습니다.", sectorEnum);
135-
136-
Stock recommendedStock = stockService.getRecommendedStockByDomesticSector(sectorEnum);
137-
if (recommendedStock != null) {
138-
log.info("DomesticSector({})에서 주식({})을 추천했습니다.", sectorEnum, recommendedStock.getSymbolName());
139-
140-
// 실시간 가격 정보를 동기적으로 가져오기
141-
try {
142-
var stockInfo = securityService.getRealTimeStockPrice(recommendedStock).block();
143-
return ResponseEntity.ok(ShortViewResponse.fromEntityWithPrice(recommendedStock, stockInfo));
144-
} catch (Exception e) {
145-
log.warn("실시간 가격 조회 실패, 기본 정보로 응답합니다. stock_id: {}, error: {}",
146-
recommendedStock.getId(), e.getMessage());
147-
return ResponseEntity.ok(ShortViewResponse.fromEntity(recommendedStock));
148-
}
149-
} else {
150-
log.warn("DomesticSector({})에 대한 추천 주식을 찾을 수 없습니다. (유효한 주식이 없거나 점수가 없음)", sectorEnum);
151-
return ResponseEntity.noContent().build();
152-
}
133+
log.info("DomesticSector({})별 추천(paged) 요청: page={}, size={}", sectorEnum, page, size);
134+
135+
int safeSize = Math.max(1, Math.min(size, 100));
136+
int safePage = Math.max(0, page);
137+
138+
var pageResp = stockService.getRecommendedStocksByDomesticSectorPaged(sectorEnum, safePage, safeSize);
139+
return ResponseEntity.ok(pageResp);
153140
} catch (Exception e) {
154141
log.error("DomesticSector별 추천 중 오류 발생: {}", sector, e);
155142
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();

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

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,88 @@ public Stock getRecommendedStockByDomesticSector(DomesticSector sector) {
10241024
return selectWeightedRandom(stocksWithWeight, random);
10251025
}
10261026

1027+
/**
1028+
* 국내 섹터 추천을 페이징으로 제공합니다. 정렬 기준은 인간지표(점수) 내림차순입니다.
1029+
* 가격은 가능하면 실시간으로 조회해 포함하고 실패 시 null로 반환됩니다.
1030+
*/
1031+
public PageResponse<ShortViewResponse> getRecommendedStocksByDomesticSectorPaged(
1032+
DomesticSector sector, int page, int size) {
1033+
if (sector == null || sector == DomesticSector.UNKNOWN) {
1034+
return PageResponse.<ShortViewResponse>builder()
1035+
.items(java.util.List.of())
1036+
.page(page)
1037+
.size(size)
1038+
.totalElements(0)
1039+
.totalPages(0)
1040+
.build();
1041+
}
1042+
1043+
List<Stock> validStocks = stockRepository.findValidStocksByDomesticSector(sector);
1044+
if (validStocks.isEmpty()) {
1045+
return PageResponse.<ShortViewResponse>builder()
1046+
.items(java.util.List.of())
1047+
.page(page)
1048+
.size(size)
1049+
.totalElements(0)
1050+
.totalPages(0)
1051+
.build();
1052+
}
1053+
1054+
List<Integer> candidateStockIds = validStocks.stream().map(Stock::getId).toList();
1055+
LocalDate today = LocalDate.now();
1056+
List<Score> todayScores = scoreRepository.findTodayScoresByStockIds(candidateStockIds, today);
1057+
List<Score> latestScores = scoreRepository.findLatestScoresByStockIds(candidateStockIds);
1058+
1059+
Map<Integer, Score> scoreMap = new HashMap<>();
1060+
todayScores.forEach(s -> scoreMap.put(s.getStockId(), s));
1061+
latestScores.forEach(s -> scoreMap.putIfAbsent(s.getStockId(), s));
1062+
1063+
// Filter stocks with valid score (not 9999)
1064+
List<Stock> stocksWithScore = validStocks.stream()
1065+
.filter(stock -> {
1066+
Score sc = scoreMap.get(stock.getId());
1067+
if (sc == null) return false;
1068+
int val = getScoreByCountry(sc, stock.getExchangeNum());
1069+
return val != 9999;
1070+
})
1071+
.toList();
1072+
1073+
// sort by score desc
1074+
List<Stock> sorted = stocksWithScore.stream()
1075+
.sorted((a, b) -> {
1076+
int sa = getScoreByCountry(scoreMap.get(a.getId()), a.getExchangeNum());
1077+
int sb = getScoreByCountry(scoreMap.get(b.getId()), b.getExchangeNum());
1078+
return Integer.compare(sb, sa);
1079+
})
1080+
.toList();
1081+
1082+
final int total = sorted.size();
1083+
final int totalPages = (int) Math.ceil((double) total / size);
1084+
final int from = Math.min(page * size, total);
1085+
final int to = Math.min(from + size, total);
1086+
1087+
List<ShortViewResponse> items = sorted.subList(from, to).stream()
1088+
.map(stock -> {
1089+
try {
1090+
var stockInfo = securityService.getRealTimeStockPrice(stock).block();
1091+
if (stockInfo != null && stockInfo.getPrice() != null) {
1092+
return ShortViewResponse.fromEntityWithPrice(stock, stockInfo);
1093+
}
1094+
} catch (Exception e) {
1095+
log.debug("실시간 가격 조회 실패(무시). stockId={}, error={}", stock.getId(), e.getMessage());
1096+
}
1097+
return ShortViewResponse.fromEntity(stock);
1098+
})
1099+
.toList();
1100+
return PageResponse.<ShortViewResponse>builder()
1101+
.items(items)
1102+
.page(page)
1103+
.size(size)
1104+
.totalElements(total)
1105+
.totalPages(totalPages)
1106+
.build();
1107+
}
1108+
10271109
/**
10281110
* 특정 OverseasSector의 주식을 추천합니다.
10291111
* 점수 기반 가중치 랜덤 추천을 사용합니다.

0 commit comments

Comments
 (0)