오늘은 LottoService 와 LottoController에 로또 통계 기능에 대한 로직을 구현해볼 것이다.
1. WinningLottoRepository
우선 WinningLottoRepositoy를 살짝 수정하였다
@Repository
public interface WinningLottoRepository extends JpaRepository<WinningLotto, Long> {
Optional<WinningLotto> findByDrawDateBetween(LocalDate start, LocalDate end);
}
특정 날짜 하루가 아니라 기간(주간) 내의 당첨 번호를 찾기 위해 findByDrawDateBetween으로 변경하였다.
2. LottoService
핵심 로직 1: 주간(Week) 날짜 계산
사용자가 수요일 날짜로 조회하더라도, 시스템은 그 주 토요일의 당첨 번호를 찾아내야 한다. (왜냐면 로또 추첨은 토요일이고 해당 주에 산 모든 로또는 그 주 당첨번호로 결과가 나오기 때문)
- 문제: 입력된 date가 당첨일(토요일)과 다를 수 있다
- 해결: Java Time API의 TemporalAdjusters를 사용
- previousOrSame(DayOfWeek.MONDAY): 해당 주의 시작일(월) 계산
- nextOrSame(DayOfWeek.SUNDAY): 해당 주의 종료일(일) 계산
- 사용자가 그 주의 어떤 요일을 입력해도, 정확하게 해당 주차의 데이터(구매 목록 + 당첨 번호)를 조회할 수 있게 되었다.
핵심 로직 2: 통계 계산
3주차 피드백에 따라 LottoService가 직접 번호를 하나하나 꺼내서 비교하는 절차지향적 방식을 피했다.
- 구현 방식: Lotto 객체에게 물어보기
- userLotto.matchCount(winningNumbers): 당첨번호랑 몇 개 겹치는 지
- userLotto.hasBonusNum(bonusNum): 보너스 번호 가지고 있는 지
핵심 로직 3: 통계 및 수익률
3주차 미션때 구현하였던 LottoRank 와 LottoResult를 활용해 기능을 완성하였다!
// TODO: 4. 당첨 결과 및 통계 (Statistics) 기능 구현
// 당첨 번호 및 보너스 번호 등록
public void createWinningLotto(WinningLottoCreateRequestDTO request) {
Lotto lotto = new Lotto(request.getNumbers());
WinningLotto winningLotto = new WinningLotto(
lotto,
request.getBonusNum(),
request.getDrawDate()
);
winningLottoRepository.save(winningLotto);
}
// 입력받은 날짜가 포함된 '주(Week)'의 구매 내역과 당첨 결과를 비교하여 통계를 반환
@Transactional(readOnly = true)
public StatisticsResponseDTO getStatistics(LocalDate queryDate) {
LocalDate startOfWeek = queryDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
LocalDate endOfWeek = queryDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));
WinningLotto winningLotto = winningLottoRepository.findByDrawDateBetween(startOfWeek, endOfWeek)
.orElseThrow(() -> new IllegalArgumentException(
String.format("[ERROR] 해당 주차(%s ~ %s)의 당첨 번호가 입력되지 않았습니다.", startOfWeek, endOfWeek)));
List<PurchasedLotto> purchasedLottos = purchasedLottoRepository.findAllByPurchaseDateBetween(startOfWeek, endOfWeek);
Map<LottoRank, Integer> statisticsMap = calculateRankStatistics(purchasedLottos, winningLotto);
LottoResult lottoResult = new LottoResult(statisticsMap);
int purchaseAmount = purchasedLottos.size() * 1000;
double rateOfReturn = lottoResult.getRateOfReturn(purchaseAmount);
return StatisticsResponseDTO.builder()
.drawDate(winningLotto.getDrawDate())
.winningNumbers(winningLotto.getWinningLotto().getNumbers())
.bonusNum(winningLotto.getBonusNum())
.rankCounts(lottoResult.getStatistics())
.rateOfReturn(rateOfReturn)
.build();
}
private Map<LottoRank, Integer> calculateRankStatistics(List<PurchasedLotto> tickets, WinningLotto winningLotto) {
Map<LottoRank, Integer> stats = new EnumMap<>(LottoRank.class);
for (LottoRank rank : LottoRank.values()) {
stats.put(rank, 0);
}
Lotto winningNumbers = winningLotto.getWinningLotto();
int bonusNumber = winningLotto.getBonusNum();
for (PurchasedLotto ticket : tickets) {
Lotto userLotto = ticket.getPurchasedLotto(); // 사용자 로또
int matchCount = userLotto.matchCount(winningNumbers);
boolean hasBonus = userLotto.hasBonusNum(bonusNumber);
LottoRank rank = LottoRank.find(matchCount, hasBonus);
stats.put(rank, stats.get(rank) + 1);
}
return stats;
}
3. LottoController
저번에 구현해본 것을 토대로 컨트롤러는 사용자의 요청을 받아 적절한 서비스 로직을 호출하고, 규격화된 DTO로 응답하는 역할에 집중해서 구현했다.
- RESTful 설계:
- 등록(POST): /im-minji/winning-lotto 를 통해 당첨 번호를 등록한다.
- 조회(GET): /im-minji/statistics 를 사용하되, Query String(?date=2025-11-12)을 활용하여 유연한 날짜 조회를 가능하게 했다.
// 당첨 번호 입력 (POST /im-minji/winning-lotto)
@PostMapping("/winning-lotto")
public ResponseEntity<Void> createWinningLotto(@RequestBody WinningLottoCreateRequestDTO request) {
lottoService.createWinningLotto(request);
return ResponseEntity.ok().build();
}
// 당첨 통계 조회 (GET /im-minji/statistics?date=2025-11-18)
@GetMapping("/statistics")
public ResponseEntity<StatisticsResponseDTO> getStatistics(@RequestParam("date") LocalDate date) {
return ResponseEntity.ok(lottoService.getStatistics(date));
}
4. API 테스트
1) 특정 날짜로 구매 로또 준비

2) 당첨 로또 준비

3) 구매 로또와 당첨 로또의 통계 결과 테스트

이렇게 당첨 번호를 등록하고 그 주에 구매했던 로또와 당첨 번호를 비교해서 수익률과 결과를 받아오는 것까지 성공적으로 테스트하였다!!
5. winning-lotto.html
원래는 구매 로또 페이지 안에다가 만들 계획이었으나 기능을 구현해보니 따로 페이지를 구성하는 게 좋을 것 같아서 당첨 번호를 입력하고 결과를 볼 수 있는 페이지를 따로 만들어보았다.


당첨번호와 보너스 번호를 입력해서 날짜를 설정할 수 있다. 그 후 당첨 번호 등록하기 버튼을 누르면 POST 요청으로 당첨 번호를 등록한다. 그 다음 확인 하고 싶은 날짜를 선택하고 나서 결과 확인 버튼을 누르면 GET으로 결과를 불러와 그 주차의 결과를 보여준다.
<script>
document.addEventListener('DOMContentLoaded', () => {
const today = new Date().toISOString().split('T')[0];
document.getElementById('draw-date').value = today;
document.getElementById('check-date').value = today;
document.getElementById('btn-register').addEventListener('click', async () => {
const winNumInputs = document.querySelectorAll('.win-num');
const bonusInput = document.getElementById('bonus-num');
const dateInput = document.getElementById('draw-date');
const numbers = Array.from(winNumInputs).map(input => parseInt(input.value));
const bonusNum = parseInt(bonusInput.value);
const drawDate = dateInput.value;
if (numbers.some(isNaN) || isNaN(bonusNum) || !drawDate) {
alert('모든 번호와 날짜를 입력해주세요.');
return;
}
const uniqueNumbers = new Set(numbers);
if (uniqueNumbers.size !== 6) {
alert('당첨 번호 6개는 중복될 수 없습니다.');
return;
}
// API 호출
try {
const response = await fetch('/im-minji/winning-lotto', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ numbers, bonusNum, drawDate })
});
if (!response.ok) {
throw new Error('등록에 실패했습니다. (번호 범위나 중복을 확인해주세요)');
}
alert('당첨 번호가 성공적으로 등록되었습니다!');
} catch (error) {
alert(error.message);
}
});
document.getElementById('btn-check').addEventListener('click', async () => {
const dateInput = document.getElementById('check-date').value;
if (!dateInput) {
alert('날짜를 선택해주세요.');
return;
}
try {
const response = await fetch(`/im-minji/statistics?date=${dateInput}`);
if (!response.ok) {
throw new Error('결과를 불러올 수 없습니다.\n해당 주차의 당첨 번호가 등록되었는지 확인해주세요.');
}
const data = await response.json();
renderStatistics(data);
} catch (error) {
alert(error.message);
}
});
// 화면 렌더링 함수
function renderStatistics(data) {
const resultArea = document.getElementById('result-area');
resultArea.classList.remove('hidden'); // 숨김 클래스 제거
// 상단 정보 표시
document.getElementById('result-week-info').innerText =
`기준 추첨일: ${data.drawDate} | 보너스 번호: ${data.bonusNum}`;
// 등수별 개수 업데이트 (DTO의 rankCounts 맵 Key: FIRST, SECOND...)
const ranks = ['FIFTH', 'FOURTH', 'THIRD', 'SECOND', 'FIRST'];
ranks.forEach(rank => {
const count = data.rankCounts[rank] || 0;
document.getElementById(`count-${rank}`).innerText = `${count}개`;
});
// 수익률
const roiElement = document.getElementById('roi');
roiElement.innerText = data.rateOfReturn.toFixed(1) + '%'; // 소수점 1자리까지
}
});
</script>
6. 웹페이지에서 테스트
1) 구매 로또 페이지에서 수동으로 로또 여러 개 구매

2) winningLotto 페이지에서 당첨 번호 등록 및 통계 확인

원래는 이러면 결과가 나와야 하는데 아무 반응이 없는 것이다. 아무런... 분명 포스트맨에서는 테스트가 잘 되었는데ㅠㅠ!! 그래서 우선 콘솔에서 어디가 문제인지 확인해보기 위해서 코드를 수정해서 다시 테스트 해보았다.
엥 그러니까 바로 성공했다.

근데 이렇게 만들고 보니 결과 페이지가 살짝 부족한 것 같다. 우선 그 주 구매한 로또의 총 금액과 당첨된 금액의 정보를 보여주면서 수익률을 보여줘야 좋을 것 같다. 그리고 어떤 로또가 몇 등이 되었는 지가 눈에 안보이니 별로인 것 같아서 이 부분은 수정을 해야할 것 같다.
원래는 오늘 서비스와 컨트롤러만 구현하려고 했는 데 만들다보니 얼른 결과를 보고 싶어서 시간 가는 줄 모르고 다 만들어 버린 것 같다. 😁 완성은 했으나 결과에 보이는 정보들이 마음에 들지 않아서 조금(?) 더 수정해서 (수정할 게 좀 있을 것 같다ㅎ) 결과 페이지까지 확실하게 마무리하면 프로젝트 코드들에 대해서 한 번 정리하고 리드미를 정리하고 소감문을 작성할 것 같다. 계획이 변경되긴 했지만 밀린 게 아니라 오히려 다음에 할 일을 해버린 것이니 기분이 좋다 ㅎㅎ 남은 날도 힘내자 아자자!!