본문 바로가기

우테코/오픈미션

오픈미션 15일차

오늘은 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 페이지에서 당첨 번호 등록 및 통계 확인

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

 

엥 그러니까 바로 성공했다.

 

근데 이렇게 만들고 보니 결과 페이지가 살짝 부족한 것 같다. 우선 그 주 구매한 로또의 총 금액과 당첨된 금액의 정보를 보여주면서 수익률을 보여줘야 좋을 것 같다. 그리고 어떤 로또가 몇 등이 되었는 지가 눈에 안보이니 별로인 것 같아서 이 부분은 수정을 해야할 것 같다. 

 


원래는 오늘 서비스와 컨트롤러만 구현하려고 했는 데 만들다보니 얼른 결과를 보고 싶어서 시간 가는 줄 모르고 다 만들어 버린 것 같다. 😁 완성은 했으나 결과에 보이는 정보들이 마음에 들지 않아서 조금(?) 더 수정해서 (수정할 게 좀 있을 것 같다ㅎ) 결과 페이지까지 확실하게 마무리하면 프로젝트 코드들에 대해서 한 번 정리하고 리드미를 정리하고 소감문을 작성할 것 같다. 계획이 변경되긴 했지만 밀린 게 아니라 오히려 다음에 할 일을 해버린 것이니 기분이 좋다 ㅎㅎ 남은 날도 힘내자 아자자!! 

'우테코 > 오픈미션' 카테고리의 다른 글

오픈미션 16일차  (0) 2025.11.19
오픈미션 14일차  (0) 2025.11.17
오픈미션 13일차  (0) 2025.11.16
오픈미션 12일차  (0) 2025.11.15
오픈미션 11일차  (0) 2025.11.14