본문 바로가기

우테코/오픈미션

오픈미션 16일차

오늘은 오픈미션을 마무리 짓는 날이다🥹 프로젝트는 오늘로서 마무리하고 남은 시간동안은 제출할 자료라던지 소감문을 작성해서 제출할 것이다! 아자자 

 

우선 어제 로또 결과 페이지까지 만들고 마무리하였는데 오늘 다시 들어가서 사용자의 관점에서 보니 당첨 결과를 등록만 하고 볼 수가 없는 구조였다...? 

그래서 몇 개의 수정을 설계했다. 

1. 당첨 번호를 나만의 로또, 구매 로또 처럼 당첨 로또를 목록 형식으로 확인할 수 있도록 하기

2. 결과(통계) 내용은 각 주차별 당첨 번호를 클릭 시 결과 페이지로 넘어가서 확인할 수 있도록 하기 

3. 구매한 로또 중 어느 로또가 해당 결과에 들어갔고 그 로또가 몇등인지를 좀 더 자세히 보여주도록 하기 

 

위와 같이 요구사항을 정의하고 나니 당첨 결과 DTO, Service, Controller는 수정이 필요했고 당첨 번호와 결과에 대해서 추가적인 DTO가 필요했다. 

 


1. StatisticsResponseDTO (수정)

package com.woowa.lotto.dto.response;

import com.woowa.lotto.domain.LottoRank;
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDate;
import java.util.List;
import java.util.Map;

@Getter
@Builder
public class StatisticsResponseDTO {
    private final LocalDate drawDate;  // 추첨일
    private final List<Integer> winningNumbers;  // 해당 주차의 당첨 번호
    private final Integer bonusNum;  // 보너스 번호
    private final Map<LottoRank, Integer> rankCounts;  // 등수별 당첨 횟수
    private final double rateOfReturn;  // 수익률

    // 1. 총 구매 금액
    private final long totalPurchaseAmount;

    // 2. 총 당첨 금액
    private final long totalWinningMoney;

    // 3. 개별 로또 상세 결과 리스트 
    private final List<LottoResultResponseDTO> lottoResults;
}

 

2. WinningLottoResponseDTO (추가)

package com.woowa.lotto.dto.response;

import com.woowa.lotto.domain.WinningLotto;
import lombok.Builder;
import lombok.Getter;
import java.time.LocalDate;
import java.util.List;

@Getter
@Builder
public class WinningLottoResponseDTO {
    private Long id;
    private LocalDate drawDate;
    private List<Integer> numbers;
    private Integer bonusNum;

    public static WinningLottoResponseDTO from(WinningLotto entity) {
        return WinningLottoResponseDTO.builder()
                .id(entity.getId())
                .drawDate(entity.getDrawDate())
                .numbers(entity.getWinningLotto().getNumbers())
                .bonusNum(entity.getBonusNum())
                .build();
    }
}

 

3. LottoResultResponseDTO (추가)

package com.woowa.lotto.dto.response;

import com.woowa.lotto.domain.LottoRank;
import lombok.Builder;
import lombok.Getter;
import java.time.LocalDate;
import java.util.List;

@Getter
@Builder
public class LottoResultResponseDTO {
    private Long id;                // 로또 ID
    private LocalDate purchaseDate; // 구매 날짜
    private List<Integer> numbers;  // 로또 번호
    private LottoRank rank;         // 당첨 등수
}

 

4. LottoService (수정)

public void createWinningLotto(WinningLottoCreateRequestDTO request) {
    Lotto lotto = new Lotto(request.getNumbers());
    WinningLotto winningLotto = new WinningLotto(
            lotto,
            request.getBonusNum(),
            request.getDrawDate()
    );
    winningLottoRepository.save(winningLotto);
}

@Transactional(readOnly = true)
public List<WinningLottoResponseDTO> findAllWinningLottos() {
    return winningLottoRepository.findAll().stream()
            // 추첨일 내림차순 정렬 (최신 날짜가 위로)
            .sorted((a, b) -> b.getDrawDate().compareTo(a.getDrawDate()))
            .map(WinningLottoResponseDTO::from)
            .collect(Collectors.toList());
}

@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);

    // 통계 계산 및 상세 결과 리스트 생성
    List<LottoResultResponseDTO> lottoResults = new ArrayList<>();
    Map<LottoRank, Integer> rankCounts = new EnumMap<>(LottoRank.class);

    for (LottoRank rank : LottoRank.values()) {
        rankCounts.put(rank, 0);
    }

    long totalWinningMoney = 0; // 총 당첨 금액

    for (PurchasedLotto ticket : purchasedLottos) {
        // 등수 판별
        LottoRank rank = checkRank(ticket, winningLotto);

        // 통계 누적
        rankCounts.put(rank, rankCounts.get(rank) + 1);
        totalWinningMoney += rank.getWinningPrize();

        // 상세 결과 DTO 생성 및 추가
        lottoResults.add(LottoResultResponseDTO.builder()
                .id(ticket.getId())
                .purchaseDate(ticket.getPurchaseDate())
                .numbers(ticket.getPurchasedLotto().getNumbers())
                .rank(rank)
                .build());
    }

    // 수익률 계산
    long totalPurchaseAmount = purchasedLottos.size() * 1000L;
    double rateOfReturn = 0.0;
    if (totalPurchaseAmount > 0) {
        rateOfReturn = ((double) totalWinningMoney / totalPurchaseAmount) * 100.0;
    }

    // DTO 반환
    return StatisticsResponseDTO.builder()
            .drawDate(winningLotto.getDrawDate())
            .winningNumbers(winningLotto.getWinningLotto().getNumbers())
            .bonusNum(winningLotto.getBonusNum())
            .rankCounts(rankCounts)
            .rateOfReturn(rateOfReturn)
            .totalPurchaseAmount(totalPurchaseAmount)
            .totalWinningMoney(totalWinningMoney)
            .lottoResults(lottoResults)
            .build();
}

// 등수 판별
private LottoRank checkRank(PurchasedLotto ticket, WinningLotto winningLotto) {
    Lotto userLotto = ticket.getPurchasedLotto();
    Lotto winningNumbers = winningLotto.getWinningLotto();

    int matchCount = userLotto.matchCount(winningNumbers);
    boolean hasBonus = userLotto.hasBonusNum(winningLotto.getBonusNum());

    return LottoRank.find(matchCount, hasBonus);
}

 

5. LottoController (수정)

// 당첨 번호 목록 조회
@GetMapping("/winning-lotto")
public ResponseEntity<List<WinningLottoResponseDTO>> getWinningLottoList() {
    return ResponseEntity.ok(lottoService.findAllWinningLottos());
}

 


이렇게 백엔드를 수정한 다음 winning-lotto.html 페이지에서 다 보여줬던 정보를 페이지를 나눴다. 당첨 번호 목록을 보여주는 페이지, 당첨 번호를 등록하는 페이지, 당첨 결과를 확인하는 페이지 이렇게 3개로 나눠서 구현하였다. 

 

1. winning-lotto.html (당첨 목록 페이지)

class="main-content">
    <h1 class="content-title">당첨 회차 목록</h1>
    <p class="content-subtitle">등록된 당첨 번호 목록입니다. '결과 확인'을 눌러 상세 통계를 확인하세요.</p>

    <div style="text-align: right; margin-bottom: 1rem;">
        <a href="winning-lotto-add.html" class="btn btn-primary">새 당첨 번호 등록</a>
    </div>

    <table class="lotto-table">
        <thead>
        <tr>
            <th>추첨일 (기준일)</th>
            <th>당첨 번호</th>
            <th>보너스</th>
            <th>상세보기</th>
        </tr>
        </thead>
        <tbody id="winning-list-body">
        <tr><td colspan="4">데이터를 불러오는 중...</td></tr>
        </tbody>
    </table>
</main>

<footer class="site-footer">
    <p>Copyright &copy; 2025 Im-minji</p>
</footer>

<script>
    document.addEventListener('DOMContentLoaded', async () => {
        const tableBody = document.getElementById('winning-list-body');

        try {
            // [API 호출] 목록 조회
            const response = await fetch('/im-minji/winning-lotto');
            if (!response.ok) throw new Error('목록을 불러오는데 실패했습니다.');

            const list = await response.json();
            tableBody.innerHTML = ''; // 로딩 문구 제거

            if (list.length === 0) {
                tableBody.innerHTML = '<tr><td colspan="4">등록된 당첨 번호가 없습니다.</td></tr>';
                return;
            }

            // 리스트 렌더링
            list.forEach(item => {
                const tr = document.createElement('tr');
                // item: { id, drawDate, numbers: [], bonusNum }
                tr.innerHTML = `
                    <td>${item.drawDate}</td>
                    <td style="letter-spacing: 2px; font-weight: bold; color: #003458;">
                        ${item.numbers.join(', ')}
                    </td>
                    <td style="color: #d9534f; font-weight: bold;">${item.bonusNum}</td>
                    <td>
                        <a href="winning-lotto-result.html?date=${item.drawDate}" class="btn btn-small btn-secondary">결과 확인</a>
                    </td>
                `;
                tableBody.appendChild(tr);
            });
        } catch (error) {
            tableBody.innerHTML = `<tr><td colspan="4" class="error-text">${error.message}</td></tr>`;
        }
    });
</script>

 

2. winning-lotto-add.html (당첨 번호 추가 페이지)

<main class="main-content">
    <h1 class="content-title">당첨 번호 등록</h1>
    <p class="content-subtitle">이번 주 당첨 번호를 등록해주세요.</p>

    <div class="save-form-box" style="text-align: center; max-width: 600px; margin-top: 2rem;">
        <div class="input-group">
            <label class="input-label" style="text-align: center;">당첨 번호 6개</label>
            <div class="lotto-inputs">
                <input type="number" class="lotto-input win-num" min="1" max="45">
                <input type="number" class="lotto-input win-num" min="1" max="45">
                <input type="number" class="lotto-input win-num" min="1" max="45">
                <input type="number" class="lotto-input win-num" min="1" max="45">
                <input type="number" class="lotto-input win-num" min="1" max="45">
                <input type="number" class="lotto-input win-num" min="1" max="45">
            </div>
        </div>

        <div class="input-group">
            <label class="input-label" style="text-align: center; color: #b91c1c;">보너스 번호</label>
            <div class="lotto-inputs">
                <input type="number" id="bonus-num" class="lotto-input bonus" min="1" max="45">
            </div>
        </div>

        <div class="input-group">
            <label class="input-label" style="text-align: center;">추첨일 (기준 날짜)</label>
            <input type="date" id="draw-date" class="input-field date-input">
        </div>

        <div class="button-group" style="margin-top: 2rem;">
            <button id="btn-register" class="btn btn-primary">등록하기</button>
            <a href="winning-lotto.html" class="btn btn-secondary">취소</a>
        </div>
    </div>
</main>

<footer class="site-footer">
    <p>Copyright &copy; 2025 Im-minji</p>
</footer>

<script>
    document.addEventListener('DOMContentLoaded', () => {
        // 오늘 날짜 기본 세팅
        document.getElementById('draw-date').value = new Date().toISOString().split('T')[0];

        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(i => parseInt(i.value));
            const bonusNum = parseInt(bonusInput.value);
            const drawDate = dateInput.value;

            // 유효성 검사
            if (numbers.some(isNaN) || isNaN(bonusNum) || !drawDate) {
                alert('모든 번호와 날짜를 입력해주세요.');
                return;
            }

            // 중복 검사 (Client Side)
            if (new Set(numbers).size !== 6) {
                alert('당첨 번호 6개는 중복될 수 없습니다.');
                return;
            }

            try {
                // [API 호출] 당첨 번호 등록
                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('성공적으로 등록되었습니다!');
                // 목록 페이지로 이동
                window.location.href = 'winning-lotto.html';

            } catch (error) {
                alert(error.message);
            }
        });
    });
</script>

 

 

3. winning-lotto-result.html (당첨 결과 확인 페이지)


        <div style="max-width: 800px; margin: 40px auto 0 auto;">
            <h3 style="text-align: left; margin-bottom: 10px;">🧾 이번 주 구매 로또 상세 결과</h3>
            <table class="lotto-table">
                <thead>
                <tr>
                    <th>구매일</th>
                    <th>로또 번호</th>
                    <th>결과</th>
                </tr>
                </thead>
                <tbody id="detail-list-body">
                </tbody>
            </table>
        </div>

        <div style="margin-top: 3rem;">
            <a href="winning-lotto.html" class="btn btn-secondary">목록으로 돌아가기</a>
        </div>
    </div>
</main>

<footer class="site-footer">
    <p>Copyright &copy; 2025 Im-minji</p>
</footer>

<script>
    document.addEventListener('DOMContentLoaded', async () => {
        // URL 파라미터에서 날짜 추출
        const params = new URLSearchParams(window.location.search);
        const date = params.get('date'); // ?date=2025-11-19

        if (!date) {
            alert('잘못된 접근입니다.');
            window.location.href = 'winning-lotto.html';
            return;
        }

        document.getElementById('date-info').innerText = `기준일: ${date} 주차의 결과입니다.`;

        try {
            // 통계 조회
            const response = await fetch(`/im-minji/statistics?date=${date}`);

            if (!response.ok) {
                // 해당 날짜에 당첨 번호가 없거나 구매 내역 조회 실패 시
                throw new Error('결과를 불러올 수 없습니다.');
            }

            const data = await response.json();
            renderResults(data);

        } catch (error) {
            alert(error.message);
            document.getElementById('date-info').innerText = "데이터 조회 실패";
        }
    });

    function renderResults(data) {
        const formatter = new Intl.NumberFormat('ko-KR', { style: 'currency', currency: 'KRW' });

        // 영역 표시
        document.getElementById('result-area').classList.remove('hidden');

        // 1. 요약 정보 바인딩
        document.getElementById('total-purchase').innerText = formatter.format(data.totalPurchaseAmount);
        document.getElementById('total-win').innerText = formatter.format(data.totalWinningMoney);
        document.getElementById('roi').innerText = data.rateOfReturn.toFixed(1) + '%';

        // 2. 등수별 카운트 바인딩
        const ranks = ['FIRST', 'SECOND', 'THIRD', 'FOURTH', 'FIFTH', 'NONE'];
        ranks.forEach(rank => {
            const el = document.getElementById(`count-${rank}`);
            if (el) el.innerText = (data.rankCounts[rank] || 0) + '개';
        });

        // 3. 상세 로또 리스트 바인딩 (lottoResults 사용)
        const tbody = document.getElementById('detail-list-body');
        tbody.innerHTML = '';

        if (!data.lottoResults || data.lottoResults.length === 0) {
            tbody.innerHTML = '<tr><td colspan="3">해당 주차에 구매한 로또가 없습니다.</td></tr>';
            return;
        }

        data.lottoResults.forEach(ticket => {
            const tr = document.createElement('tr');

            let badge = `<span style="color: #aaa;">꽝</span>`;
            if (ticket.rank === 'FIRST') badge = `<span style="color: red; font-weight: bold;">1등</span>`;
            else if (ticket.rank === 'SECOND') badge = `<span style="color: orange; font-weight: bold;">2등</span>`;
            else if (ticket.rank === 'THIRD') badge = `<span style="color: gold; font-weight: bold;">3등</span>`;
            else if (ticket.rank === 'FOURTH') badge = `<span style="color: blue; font-weight: bold;">4등</span>`;
            else if (ticket.rank === 'FIFTH') badge = `<span style="color: green; font-weight: bold;">5등</span>`;

            tr.innerHTML = `
                <td>${ticket.purchaseDate}</td>
                <td style="letter-spacing: 1px;">${ticket.numbers.join(', ')}</td>
                <td>${badge}</td>
            `;
            tbody.appendChild(tr);
        });
    }
</script>

이제 완성된 페이지를 확인해보면 

winning-lotto 페이지가 목록 형식으로 바뀌었고 여기서 새 당첨 번호 등록 버튼을 누르면 원래 winning-lotto 페이지의 step1 내용이 나타난다. 

당첨 번호를 추가하면 목록에 추가된다. 

당첨 번호를 추가하고 나서 이제 테스트를 위해 구매 로또를 추가해두고 

 

결과 확인 버튼을 누르면 결과 페이지로 이동한다.

이 페이지에서는 예전과 달리 총 구매 금액과 당첨 금액과 함께 수익률을 제공한다. 또한 이번 주 구매 로또 상세 결과를 통해 어떤 로또가 몇 등을 했는 지를 확인할 수 있다. 

 


이렇게 해서 스프링부트 로또 웹 프로젝트는 끝이 났다..!! 5주가 짧은 시간은 아니였지만 프리코스를 진행하는 동안에는 과제 하나를 열심히 끝내고 나면 한 주가 후딱 후딱 지나가 있어서 지금까지와는 다르게 정말 빠르게 지나간 것 같다. 분명 나중에 이 프로젝트를 보면 아쉬운 부분이 있을 수도 있지만 지금의 나한테는 정말 엄청난 도전이었고 솔직히 2주 안에 끝내지 못할까봐 걱정을 많이 했는데 이렇게 마무리했다는 것만으로도 뿌듯하다고 자신있게 말할 수 있다. 또한 나는 기록하는 것을 안한 지 꽤 되었는데 이번 오픈 미션 1일차에 말한대로 오픈 미션을 진행하는 동안 매일 기록하기라는 내 자신과의 약속도 끝까지 지켜냈다!! 나와의 도전이 연속이였던 2주였고 타협하기 보다는 계속해서 계획을 세우고 실천하려고 노력하며 성장하려고 노력했다. 앞으로도 이러한 정신을 잊지않고 나에게 계속 적용시킬 것이다. 난 결국 해냈다!! 할 수 있다. 이제 제출할 준비를 하고 마무리되면 제출을 하고 결과를 기다릴 것이다. 떨리지만 후회없이 열심히 했으니 미련은 없다 아자자 끝까지 집중해서 잘 마무리하자 화이팅!! 💪🏼

 

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

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