페이지 | 역할 | 기능 |
---|---|---|
로그인, 회원가입 | 유저 관리 | - 로그인 / 로그아웃 - 회원가입 |
메인 | 로또 티켓 구매 | - 번호 추출 타입 선택 - 로또 구입 |
로또 번호 추출 페이지 | 추출한 랜덤 번호 확인 | - 요청한 랜덤 번호 리스트 확인(페이징) - 메인으로 돌아가기 - 회차별 당첨자 조회 페이지 이동 |
이번주 동행복권 당첨번호 | 동행복권 당첨 결과 확인 사이트 기준 당첨 결과 확인 | - 이번주 동행복권 추첨 결과 확인 - 사이트 기준 순위, 당첨금액, 참여 수, 총 판매 금액 |
이번주 내번호 당첨확인 | 이번주 참여 결과 확인 | - 이번주 내번호 당첨 결과 확인 - 이번주 추출 전체 리스트 (구입 티켓 수, 참여 회차, 로또 번호, 보너스 번호, 추첨일) |
회차별 당첨 결과 확인 | 회차 선택 기능 | - 이번주 동행복권 추첨 결과 확인 - 사이트 기준 순위, 당첨금액, 참여 수, 총 판매 금액 |
📌 핵심 📌
: 동행복권 api를 그때그때 불러서 사용하지 않고, 데이터를 전부 DB에 저장하는 이유는
동행복권 사이트에 의존하지 않기 위해서이다.
- Thread.sleep();
- gson
https://www.dhlottery.co.kr/common.do?method=getLottoNumber&drwNo=마지막회차번호
try {//로직의 시간을 지연시키는 것
Thread.sleep(100);//0.1초
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
api를 많이 호출하면 디도스 공격으로 오해받아 block 당할 수 있다.
따라서 Thread.sleep()메서드를 하나씩 디비에 저장할 때 스레드를 일정 시간을 멈추어 해당 이슈를 방지했다.
데이터 파싱은 Gson의 라이브러리를 사용하여
json 문자열을 lottoApi 객체로 생성하여 테이블에 저장한다
📌 핵심 📌
: 번호 추출 후 추출 회차를 저장해야하기 때문에 해당 회차 번호가 필요하다
필요시마다 데이터베이스에 요청하지 않고, 프로젝트 구동시 한번 가져오는 방법을 선택했다
- @PostContruct
@PostConstruct
public void init() {
//마지막 회차 번호 얻기
Long lastCycleNum = isTestProperties() ? 1L : lottoApiService.getLastCycleNum();
memory.put(LAST_CYCLE_NUM, lastCycleNum);
}
@Component
@RequiredArgsConstructor
public class MemoryContext {
public static Map<MemoryKey, Long> memory = new HashMap<>();
private final LottoApiService lottoApiService;
private final Environment environment;
@PostConstruct
public void init() {
//마지막 회차 번호 얻기
Long lastCycleNum = isTestProperties() ? 1L : lottoApiService.getLastCycleNum();
memory.put(LAST_CYCLE_NUM, lastCycleNum);
}
@RequiredArgsConstructor
public enum MemoryKey {
LAST_CYCLE_NUM("lastCycleNum");
private final String name;
}
private boolean isTestProperties() {
String activeProfile = environment.getActiveProfiles()[0];
return activeProfile.equals("local") || activeProfile.equals("test");
}
}
주기적으로 매주 마지막 회차가 변경되므로 api를 불러와야하는데
예상한 3가지 방법중 자동화를 선택하였다.
1. 운영자가 수동으로 (엔드포인트로 계속 요청)
2. 운영자 페이지를 만들어서 동기화 버튼을 통해 변경 (관리자 페이지 생성)
3. 자동화 (스케줄러에 등록)
@Scheduled(cron = "0 30 21 * * SAT")//토요일 오후 9시30분
public void lottoLastCycleFetch() {
//1. 현재 등록된 마지막 회차 번호 가져오기
Long lastCycleNum = lottoApiService.getLastCycleNum();
//2. 이번주 회차번호로 업데이트 (기존 회차번호 + 1)
memory.put(LAST_CYCLE_NUM, (lastCycleNum += 1));
//3. 가져온 회차 api DB 저장 로직
lottoApiService.insertOne(lastCycleNum);
System.out.println("lastCycleNum = " + lastCycleNum);
}
- spring scurity
📌 핵심 📌
: 전체자동, 반자동, 전체수동 (ALLAUTO, SELECTNUM, ALLSELECT)
- 사용자 입력값 검증
- 타입(enum)에 따른 랜덤 번호 결과 반환
http://localhost:8080/machine/lottoNum?userNo=2&lottotype=ALLSELECT&price=1000&inputNum=5,12,13,31,32,41&storageCycle=1026
http://localhost:8080/machine/lottoNum?userNo=1&lottotype=ALLAUTO&price=1000&storageCycle=1026
http://localhost:8080/machine/lottoNum?userNo=2&lottotype=SELECTNUM&price=1000&inputNum=2,3,5,6&storageCycle=1550
📌 핵심 📌
HashSet<Ball> hashSet
private static final int LENGTH = 6;
public SixBall() {
this.list = new ArrayList<>(makeBallSet());
}
public SixBall(List<Integer> inputNumList) {
this.list = new ArrayList<>(requestUserNum(inputNumList));
}
📌 핵심 📌
- new Random().nextInt(MAX) + 1
- AttributeConverter
public class Ball {
private static final int MAX = 45;
private final Integer value;
//값 객체
public Ball() {
this(new Random().nextInt(MAX) + 1);//메인 생성자 호출
//1~45
}
public Ball(Integer value) {//메인 생성자
if (value < 1 || value > MAX) {
throw new IllegalArgumentException("1~45 사이의 숫자만 가능합니다");
}
this.value = value;
}
}
public class BallConverter implements AttributeConverter<Ball, Integer> {
@Override
//ball을 받아서 integer로 타입을 변환
public Integer convertToDatabaseColumn(Ball attribute) {
return attribute.getValue();
}
@Override
public Ball convertToEntityAttribute(Integer dbData) {
return new Ball(dbData);
}
}
@Convert(converter = BallConverter.class)
@Column(name = "ball_01")
private Ball ball1;
@Scheduled(cron = "* * 22 * * SAT")//매주 토요일 당첨자 확인
public void checkWeekWinner() {
log.info("당첨자 선별 후 저장하기");
//1. 마지막 회차 번호 가져오기
Long lastCycleNum = memory.get(LAST_CYCLE_NUM);
//2. 당첨자 선별 후 DB 저장 및 당첨자 리스트 가져오기
List<WinnerCommand> winnerList = winnerService.saveWinner(lastCycleNum);
log.info("화면에 뿌려줄 winningInfo 저장하기");
//3. 등위별 당첨금 계산
WinningResult winningMap = winningAmountService.calcAmount(lastCycleNum);
//4. 화면에 보낼 당첨자 정보(winningInfo) 만들기
winningService.makeWinningInfo(winningMap);
}
Controller 에서 @GetMapping("/result/user/{userNo}")
사용자 번호를
@PathVariable("userNo") Long userNo로 받는다
해당 사용자 추첨 결과 정보와 현재까지의 구입 정보를 확인할 수 있다.
if (winningInfoList.size() == 0) {
gameResultMap.put("gameResult", "resultEmpty");
return gameResultMap;
}
public List<MachineCycleStorage> getUserCycleStorage(Long cycleNum, Long userNo) {
Optional<User> getUser = userJpaRepository.findById(userNo);
if (getUser == null) {
throw new NullPointerException("로그인을 해주세요.");
}
List<MachineCycleStorage> userCycleStorageList = winnerDbRepository.getUserCycleStorage(cycleNum, getUser.get().getNo());
return userCycleStorageList;
}