본문 바로가기

FastAPI

로또 번호 맞추기(FastAPI)

1. 개요: 가장 최근까지 1등 당첨번호들을 수집하여, 가장 당첨번호에 많이 포함되었던 당첨 빈도수가 높은 번호들을 자기가 원하는 갯수만큼 지정하고(6개에서 45개까지 가능함), 그 중에서 당첨 예상번호 6개를 무작위로 추출하는 방식임(예를 들자면, 사용자가 가장 당첨빈도수가 높은 번호 10개를 지정했을때,  그 10개가 다음의 숫자가 가장 당첨빈도수가 높은 10개라고 한다면(예, 1,10,24,22,16, 32,18,21,16, 8), 이 10개의 숫자를 무작위로 돌려 6개의 당첨번호를 예측하는 프로그램 로직이다.)

 

2. 매주 당첨번호 업데이트:  가장 최근의 당첨번호를 업데이트 하는 것은 스케줄러를 등록하여, 매주 토요일 밤 자정에 업데이트하도록 하였다.

# Project_folder/app/lottos/models.py

from typing import Optional

from sqlalchemy import Integer, String, DateTime, func, Text
from sqlalchemy.orm import Mapped, mapped_column

from app.core.database import BaseModel

STATUS = ('old', 'latest')


class LottoNum(BaseModel):
    __tablename__ = 'lottos'

    title: Mapped[str] = mapped_column(String(20), unique=True, index=True, nullable=False)
    status: Mapped[str] = mapped_column(String(20), default=STATUS[1], nullable=False)
    latest_round_num: Mapped[str] = mapped_column(String(100), nullable=False)
    extract_num: Mapped[str] = mapped_column(String(100), nullable=False)
    lotto_num_list: Mapped[Optional[str]] = mapped_column(Text, nullable=True)

    def __repr__(self):
        return f"<LottoNum(id={self.id}, title='{self.title}')>"
# Project_folder/app/lottos/utils.py

import random
from lxml import etree
import requests
from bs4 import BeautifulSoup
import ast
import numpy as np
import pandas as pd
from sqlalchemy import select

from app.core.settings import LOTTO_FILEPATH, LOTTO_LATEST_URL
from app.lottos.models import LottoNum, STATUS


async def old_latest_update(old_latest: LottoNum, db):
    old_latest.status = STATUS[0]
    db.add(old_latest)
    await db.commit()
    await db.refresh(old_latest)
    # # 세션에서 분리하여 순환 참조 방지
    # await db.expunge(old_latest)


async def new_lotto_num_save(latest_page, top10_list, lotto_num_list, db):
    new = LottoNum()
    new.title = latest_page + "회차"
    new.latest_round_num = latest_page
    new.extract_num = str(top10_list)  # map_str_extract_num
    new.lotto_num_list = str(lotto_num_list)
    db.add(new)
    await db.commit()
    await db.refresh(new)


async def excell2lotto_list():
	# 첫 DB 데이터 만들때 사용된다.
    df = pd.read_excel(LOTTO_FILEPATH, sheet_name='lotto')
    # 'ColumnName' 열을 리스트로 변환하기
    column_list = df['list'].tolist()
    """ # column list안의 요소들이 문자열이다. 이것을 nested list로 아래 처럼 한번 바꿔줘야 한다."""
    result_list = [ast.literal_eval(s) for s in column_list]
    return result_list


async def latest_win_num():
    html = requests.get(LOTTO_LATEST_URL).text
    soup = BeautifulSoup(html, 'lxml')

    soup_lottos = soup.select("span.ball_645")[:6]
    lotto_nums = [int(soup_lotto.get_text()) for soup_lotto in soup_lottos]
    return lotto_nums


async def latest_lotto(db):
    query = select(LottoNum).where(LottoNum.status == STATUS[1])
    result = await db.execute(query)
    _latest_lotto = result.scalar_one_or_none()
    print("_latest_lotto:", _latest_lotto)
    # # 관계가 있는 경우 detach하여 세션에서 분리
    # if _latest_lotto:
    #     await db.expunge(_latest_lotto)

    return _latest_lotto


async def extract_latest_round():
    latest_html = requests.get(LOTTO_LATEST_URL).text
    soup = BeautifulSoup(latest_html, 'lxml')
    list_select = soup.find("select", id="dwrNoList")

    selected_soup = BeautifulSoup(f"""{list_select}""", 'lxml')
    latest_round = selected_soup.find_all('option', selected=True)[0].get_text()
    print("마지막 회차:: ", int(latest_round))

    select_html = etree.HTML(f"""{list_select}""")
    selected = select_html.xpath('//option[@selected]')
    # print(selected)  # 여기서 selected value 만 가져오는 방법을 못찾겠다.

    return latest_round


async def extract_frequent_num(_list: list, num: int):
    # 다차원 배열을 1차원 배열로 만들기 (개수를 세기 위해서)
    lotto_countlist = np.ravel(_list, order='C').tolist()

    # 1~45까지 카운트한 횟수를 넣는 리스트
    lotto_count_value = []
    for i in range(1, 46):
        lotto_count_value.append(lotto_countlist.count(i))

    # 카운트한 값을 데이터프레임으로 만들기
    data = np.array(lotto_count_value)
    index_num = [i for i in range(1, 46)]
    columns_list = ["count"]
    df_lotto_count = pd.DataFrame(data, index=index_num, columns=columns_list)

    # num이 전체 후보 개수(45)를 넘으면 45로 캡핑. 빈도수 높은 숫자가 46개이상은 나올 수가 없다.
    total_candidates = len(df_lotto_count)  # == 45
    k = min(int(num), total_candidates)

    # 가장 많이 나온 로또 번호 num개 추출
    wanted_top = df_lotto_count.nlargest(k, 'count')
    
    # num개 추출한 것 리스트로 만들기
    wanted_top_list = wanted_top.index.tolist()
    print("wanted_top_list:", wanted_top_list)
    print("len(wanted_top_list):", len(wanted_top_list))

    # random.sample 표본 크기도 안전하게 제한 (최대 6)
    sample_size = min(6, len(wanted_top_list))
    lotto_random_num = sorted(random.sample(wanted_top_list, sample_size))
    print("lotto_random_num:", lotto_random_num)

    return wanted_top_list, lotto_random_num



async def extract_first_win_num(db, num: int = 10):
    old_latest = await latest_lotto(db)
    """1등번호 10개 추출하기"""
    if old_latest: # 역대 로또 번호를 저장하는 리스트에 추가한다.
        lotto_num_list = ast.literal_eval(old_latest.lotto_num_list)
        latest_lotto_num = await latest_win_num()
        lotto_num_list.append(latest_lotto_num)
    else: # 최초 데이터 저장시에는 엑셀파일에서 총 로또 번호를 뽑아내서 저장한다.
        lotto_num_list = await excell2lotto_list()

    top10_list, lotto_random_num = await extract_frequent_num(lotto_num_list, num)

    return lotto_num_list, top10_list
# Project_folder/app/utils/apschedulers.py

from apscheduler.schedulers.asyncio import AsyncIOScheduler

# 스케줄러 인스턴스 생성
scheduler = AsyncIOScheduler()


async def scheduled_lotto_update():
    """스케줄된 로또 업데이트 함수"""
    from app.core.database import get_db
    from app.lottos.utils import extract_latest_round, extract_first_win_num, latest_lotto
    from app.lottos.models import LottoNum, STATUS

    # 데이터베이스 세션 생성
    async for db in get_db():
        try:
            old_latest = await latest_lotto(db)
            latest_page = await extract_latest_round()

            # 기존 로직과 동일하게 처리
            if old_latest and old_latest.latest_round_num == latest_page:
                print(f"이미 최신 회차({latest_page})가 저장되어 있습니다.")
                return

            if int(latest_page):  # 최신 회차가 있다면
                lotto_num_list, top10_list = await extract_first_win_num(db)

                if old_latest:
                    old_latest.status = STATUS[0]
                    db.add(old_latest)
                    await db.commit()
                    await db.refresh(old_latest)

                new = LottoNum()
                new.title = latest_page + "회차"
                new.latest_round_num = latest_page
                new.extract_num = str(top10_list)
                new.lotto_num_list = str(lotto_num_list)
                db.add(new)
                await db.commit()
                await db.refresh(new)

                print(f"새로운 회차({latest_page}) 데이터가 저장되었습니다.")

        except Exception as e:
            print(f"스케줄된 업데이트 중 오류 발생: {e}")
        finally:
            await db.close()

 

# inits.py의 lifespan함수에 scheduled_lotto_update를 등록하여 "" 매주 토요일 오후 11시 59분 ""에 자동으로 가장 최근 마지막 로또 당첨번호 반영된 LottoNum 테이블 업데이트할 수 있도록 함

@asynccontextmanager
async def lifespan(app: FastAPI):
    print("Initializing database......")
    # FastAPI 인스턴스 기동시 필요한 작업 수행.
    scheduler.add_job(
        scheduled_lotto_update,
        CronTrigger(day_of_week='sat', hour=23, minute=59, timezone=KST),  # 매주 토요일 오후 11시 59분
        id='lotto_update_job',
        replace_existing=True
    )
    scheduler.start()
    print("Starting Scheduler......")

    try:
        await redis_client.ping() # Redis 연결 테스트
        print("Redis connection established......")
    except redis.exceptions.ConnectionError:
        print("Failed to connect to Redis......")
    print("Starting up...")
    yield
    # FastAPI 인스턴스 종료시 필요한 작업 수행
    await redis_client.aclose()
    print("Redis connection closed......")
    print("Shutting down...")
    await ASYNC_ENGINE.dispose()
    scheduler.shutdown()
# Project_folder/app/core/inits.py

from contextlib import asynccontextmanager

import pytz
import redis
from apscheduler.triggers.cron import CronTrigger
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from fastapi_csrf_jinja.middleware import FastAPICSRFJinjaMiddleware
from starlette.exceptions import HTTPException as StarletteHTTPException

from app.apis import accounts as apis_accounts
from app.apis.articles import articles as apis_articles
from app.apis.articles import comments as apis_articles_comments
from app.apis import auth as apis_auth
from app.apis import wysiwyg as apis_wysiwyg
from app.core.database import ASYNC_ENGINE
from app.core.redis import redis_client
from app.core.settings import STATIC_DIR, MEDIA_DIR, CONFIG, templates
from app.utils import exc_handler
from app.utils.apschedulers import scheduler, scheduled_lotto_update
from app.utils.commons import to_kst, num_format, urlencode_filter
from app.utils.middleware import AccessTokenSetCookieMiddleware
from app.views import index
from app.views import accounts as views_accounts
from app.views import articles as views_articles
from app.lottos import views as views_lotto

# 한국 시간대 명시적 지정
KST = pytz.timezone('Asia/Seoul')

@asynccontextmanager
async def lifespan(app: FastAPI):
    print("Initializing database......")
    # FastAPI 인스턴스 기동시 필요한 작업 수행.
    scheduler.add_job(
        scheduled_lotto_update,
        CronTrigger(day_of_week='sat', hour=23, minute=59, timezone=KST),  # 매주 토요일 오후 11시 59분
        id='lotto_update_job',
        replace_existing=True
    )
    scheduler.start()
    print("Starting Scheduler......")

    try:
        await redis_client.ping() # Redis 연결 테스트
        print("Redis connection established......")
    except redis.exceptions.ConnectionError:
        print("Failed to connect to Redis......")
    print("Starting up...")
    yield
    # FastAPI 인스턴스 종료시 필요한 작업 수행
    await redis_client.aclose()
    print("Redis connection closed......")
    print("Shutting down...")
    await ASYNC_ENGINE.dispose()
    scheduler.shutdown()


def including_middleware(app):
    app.add_middleware(CORSMiddleware,
                       allow_origins=[
                           "http://localhost:5173",  # 프론트엔드 origin (예시)
                           "http://127.0.0.1:5500",  # 필요시 추가
                           "http://localhost:8000",  # 백엔드가 템플릿 제공하는 경우
                       ], # 실제 프론트 주소
                       allow_methods=["*"],
                       allow_headers=["*"],
                       allow_credentials=True,
                       max_age=-1)
    app.add_middleware(FastAPICSRFJinjaMiddleware,
                       secret=CONFIG.SECRET_KEY,
                       cookie_name="csrf_token",
                       header_name="X-CSRF-Token",)
                       # swagger를 CSRF_TOKEN적용에서 제외시키는 방법: 라이브러리에서 제공하는 예외 옵션 이름에 맞춰 사용하세요.
                       # (예: exclude_paths, exempt_paths, exempt_urls 등)
                       # 이 프로젝트는 swagger로 커스터마이징해서 csrf_token을 적용시켰다.
                       # exempt_urls=["/swagger/custom/docs", "/swagger/custom/redoc", "/swagger/custom/openapi.json"])
    """ AccessTokenSetCookieMiddleware: access_token이 만료되면, 
    get_current_user 리프레시로 폴백하면서 액세스토큰을 만들때 가로채서 쿠키에 심는다."""
    app.add_middleware(AccessTokenSetCookieMiddleware)

def including_exception_handler(app):
    app.add_exception_handler(StarletteHTTPException,
                              exc_handler.custom_http_exception_handler)

def including_router(app):
    app.include_router(index.router, prefix="", tags=["IndexViews"])
    app.include_router(apis_accounts.router, prefix="/apis/accounts", tags=["AccountsAPI"])
    app.include_router(apis_auth.router, prefix="/apis/auth", tags=["AuthAPI"])
    # from app.apis.articles import articles as apis_articles # articles 폴더로 refactoring
    app.include_router(apis_articles.router, prefix="/apis/articles", tags=["ArticlesAPI"])
    app.include_router(apis_articles_comments.router, prefix="/apis/articles/comments", tags=["ArticlesCommentsAPI"])
    app.include_router(apis_wysiwyg.router, prefix="/apis/wysiwyg", tags=["WysiwygAPI"])
    app.include_router(views_accounts.router, prefix="/views/accounts", tags=["AccountsViews"])
    app.include_router(views_articles.router, prefix="/views/articles", tags=["ArticlesViews"])

    app.include_router(views_lotto.router, prefix="/lotto", tags=["Lotto"])


def initialize_app():
    app = FastAPI(title=CONFIG.APP_NAME,
                  version=CONFIG.APP_VERSION,
                  description=CONFIG.APP_DESCRIPTION,
                  lifespan=lifespan,
                  docs_url=None, redoc_url=None, openapi_url="/swagger/custom/openapi.json",
                  )
    app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
    app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media")

    templates.env.globals["STATIC_URL"] = "/static"
    templates.env.globals["MEDIA_URL"] = "/media"
    templates.env.filters["to_kst"] = to_kst
    templates.env.filters["num_format"] = num_format
    templates.env.filters["urlencode"] = urlencode_filter

    including_middleware(app)
    including_exception_handler(app)
    including_router(app)

    return app
# Project_folder/app/lottos/views.py

from typing import Optional

from fastapi import Request, APIRouter, Depends, Form
import random
import ast
import numpy as np
import pandas as pd

from sqlalchemy.ext.asyncio import AsyncSession

from app.core.database import get_db
from app.core.settings import templates, ADMINS
from app.dependencies.auth import get_optional_current_user, allow_usernames
from app.lottos.models import LottoNum, STATUS
from app.lottos.utils import extract_latest_round, extract_first_win_num, latest_lotto, extract_frequent_num
from app.models.users import User
from app.utils.accounts import is_admin
from app.utils.commons import get_times
from app.utils.exc_handler import CustomErrorException

router = APIRouter()


@router.get("/random")
async def random_lotto(request: Request,
                       num: str = None,
                       db: AsyncSession = Depends(get_db),
                       current_user: Optional[User] = Depends(get_optional_current_user)):

    old_latest = await latest_lotto(db)

    if old_latest:
        latest_round_num = old_latest.latest_round_num
        if num:
            if int(num) < 6:
                message = f"6이상의 숫자를 입력하세요! 우선 빈도에 관계없이 무작위로 추출했어요!"
                now_time_utc, now_time = get_times()
                _NOW_TIME_UTC = now_time_utc.strftime('%Y-%m-%d %H:%M:%S.%f')
                _NOW_TIME = now_time.strftime('%Y-%m-%d %H:%M:%S.%f')
                context = {"variable": sorted(random.sample(range(1, 46), 6)),
                           "latest": int(latest_round_num),
                           "message": message,
                           'current_user': current_user,
                           "now_time_utc": _NOW_TIME_UTC,
                           "now_time": _NOW_TIME,
                           'admin': is_admin(current_user)}
                return templates.TemplateResponse(
                    request=request,
                    name="lottos/lotto.html",
                    context=context
                )
            elif int(num) >= 45:
                message = f"45이상은 빈도에 관계없이 무작위로 추출하는 것과 같아요!"
                now_time_utc, now_time = get_times()
                _NOW_TIME_UTC = now_time_utc.strftime('%Y-%m-%d %H:%M:%S.%f')
                _NOW_TIME = now_time.strftime('%Y-%m-%d %H:%M:%S.%f')
                context = {"variable": sorted(random.sample(range(1, 46), 6)),
                           "latest": int(latest_round_num),
                           "message": message,
                           'current_user': current_user,
                           "now_time_utc": _NOW_TIME_UTC,
                           "now_time": _NOW_TIME,
                           'admin': is_admin(current_user)}
                return templates.TemplateResponse(
                    request=request,
                    name="lottos/lotto.html",
                    context=context
                )
            else:
                lotto_num_list = ast.literal_eval(old_latest.lotto_num_list)
                wanted_top_list, lotto_random_num = await extract_frequent_num(lotto_num_list, int(num))
                message = f"당첨 빈도가 높은 번호 {num}개중 6개를 무작위로 추출"
                now_time_utc, now_time = get_times()
                _NOW_TIME_UTC = now_time_utc.strftime('%Y-%m-%d %H:%M:%S.%f')
                _NOW_TIME = now_time.strftime('%Y-%m-%d %H:%M:%S.%f')
                context = {"input_num": num,
                           "variable": lotto_random_num,
                           "latest": int(latest_round_num),
                           "message": message,
                           'current_user': current_user,
                           "now_time_utc": _NOW_TIME_UTC,
                           "now_time": _NOW_TIME,
                           'admin': is_admin(current_user)}
                return templates.TemplateResponse(
                    request=request,
                    name="lottos/lotto.html",
                    context=context
                )

        message = f"당첨 빈도에 관계없이 6개의 숫자를 무작위로 추출"
        now_time_utc, now_time = get_times()
        _NOW_TIME_UTC = now_time_utc.strftime('%Y-%m-%d %H:%M:%S.%f')
        _NOW_TIME = now_time.strftime('%Y-%m-%d %H:%M:%S.%f')
        context = {"variable": sorted(random.sample(range(1, 46), 6)),
                   "latest": latest_round_num,
                   "message": message,
                   'current_user': current_user,
                   "now_time_utc": _NOW_TIME_UTC,
                   "now_time": _NOW_TIME,
                   'admin': is_admin(current_user)}
        return templates.TemplateResponse(
            request=request,
            name="lottos/lotto.html",
            context=context
        )
    else:
        message = f"당첨 빈도에 관계없이 6개의 숫자를 무작위로 추출"
        now_time_utc, now_time = get_times()
        _NOW_TIME_UTC = now_time_utc.strftime('%Y-%m-%d %H:%M:%S.%f')
        _NOW_TIME = now_time.strftime('%Y-%m-%d %H:%M:%S.%f')
        context = {"variable": sorted(random.sample(range(1, 46), 6)),
                   "latest": "0000",
                   "message": message,
                   'current_user': current_user,
                   "now_time_utc": _NOW_TIME_UTC,
                   "now_time": _NOW_TIME,
                   'admin': is_admin(current_user)}
        return templates.TemplateResponse(
            request=request,
            name="lottos/lotto.html",
            context=context
        )


"""# TOP10으로 로또번호를 추출하는 함수"""
@router.get("/top10")
async def top10_lotto(request: Request,
                      num: str = None,
                      db: AsyncSession = Depends(get_db),
                      current_user: Optional[User] = Depends(get_optional_current_user)):
    old_latest = await latest_lotto(db)
    if num:
        print("num: ", num)
        lotto_num_list = ast.literal_eval(old_latest.lotto_num_list)
        latest_round_num = old_latest.latest_round_num
        wanted_top_list, lotto_random_num = await extract_frequent_num(lotto_num_list, int(num))
        message = f"당첨 빈도가 높은 번호 {num}개중 6개를 무작위로 추출"
        now_time_utc, now_time = get_times()
        _NOW_TIME_UTC = now_time_utc.strftime('%Y-%m-%d %H:%M:%S.%f')
        _NOW_TIME = now_time.strftime('%Y-%m-%d %H:%M:%S.%f')
        context = {"variable": lotto_random_num,
                   "latest": int(latest_round_num),
                   "message": message,
                   'current_user': current_user,
                   "now_time_utc": _NOW_TIME_UTC,
                   "now_time": _NOW_TIME,
                   'admin': is_admin(current_user)
                   }
        return templates.TemplateResponse(
            request=request,
            name="lottos/lotto.html",
            context=context
        )
    if old_latest:
        latest_round_num = old_latest.latest_round_num
        """string 로 저장된 최다빈도 번호를 integer list 로 다시 변환하고, 번호 6개 무작위 추출"""
        lotto_top10 = ast.literal_eval(old_latest.extract_num)
        lotto_random_num = sorted(random.sample(lotto_top10, 6))
    else:
        latest_round_num = 1193
        lotto_top10 = [34, 12, 13, 18, 27, 14, 40, 45, 33, 37]
        lotto_random_num = sorted(random.sample(lotto_top10, 6))
    message = f"당첨 빈도가 높은 번호 10개중 6개를 무작위로 추출"
    now_time_utc, now_time = get_times()
    _NOW_TIME_UTC = now_time_utc.strftime('%Y-%m-%d %H:%M:%S.%f')
    _NOW_TIME = now_time.strftime('%Y-%m-%d %H:%M:%S.%f')
    context = {"variable": lotto_random_num,
               "latest": int(latest_round_num),
               "message": message,
               'current_user': current_user,
               "now_time_utc": _NOW_TIME_UTC,
               "now_time": _NOW_TIME,
                   'admin': is_admin(current_user)
               }
    return templates.TemplateResponse(
        request=request,
        name="lottos/lotto.html",
        context=context
    )


@router.get("/win/extract")
async def win_extract_lotto(request: Request,
                            db: AsyncSession = Depends(get_db),
                            current_user: Optional[User] = Depends(get_optional_current_user)
                            ):
    old_latest = await latest_lotto(db)
    if old_latest:
        '''string 로 저장된 최다빈도 번호를 integer list 로 다시 변환'''
        full_int_list = ast.literal_eval(old_latest.extract_num)
    else:
        full_int_list = []

    now_time_utc, now_time = get_times()
    _NOW_TIME_UTC = now_time_utc.strftime('%Y-%m-%d %H:%M:%S.%f')
    _NOW_TIME = now_time.strftime('%Y-%m-%d %H:%M:%S.%f')

    context = {"old_extract": old_latest,
               "old_extract_num": full_int_list,
               'current_user': current_user,
               "now_time_utc": _NOW_TIME_UTC,
               "now_time": _NOW_TIME,
               'admin': is_admin(current_user)
               }
    return templates.TemplateResponse(
        request=request,
        name="lottos/extract.html",
        context=context
    )


@router.post("/win/top10/post")
async def lotto_top10_post(request: Request,
                           latest_round: str = Form(...),
                           db: AsyncSession = Depends(get_db),
                           admin_user = Depends(allow_usernames(ADMINS))
                           ):
    print("admin_user: ", admin_user.username)
    print(f"latest_round: {latest_round}")
    old_latest = await latest_lotto(db) # db에 저장된 것
    latest_page = await extract_latest_round() # 로또사이트의 마지막 회차
    if old_latest:
        if old_latest.latest_round_num == latest_page:
            if latest_round == latest_page:
                raise CustomErrorException(status_code=499, detail="Conflict")
            elif int(latest_round) > int(old_latest.latest_round_num):
                raise CustomErrorException(status_code=499, detail="No Event")

    if int(latest_round) == int(latest_page):
        lotto_num_list, top10_list = await extract_first_win_num(db)
        if old_latest:
            old_latest.status = STATUS[0]
            db.add(old_latest)
            await db.commit()
            await db.refresh(old_latest)

        new = LottoNum()
        new.title = latest_page + "회차"
        new.latest_round_num = latest_page
        new.extract_num = str(top10_list)  # map_str_extract_num
        new.lotto_num_list = str(lotto_num_list)
        db.add(new)
        await db.commit()
        await db.refresh(new)

        """string 로 저장된 최다빈도 번호를 integer list 로 다시 변환"""
        from typing import cast
        extract_num = cast(str, new.extract_num) # type(extract_num) str
        new_extract_num = ast.literal_eval(extract_num) # type(new_extract_num) list

        # "current_user": admin_user 이것을 넘길 때, jsonable_encoder : from . import v2에러가 발생한다.
        # admin_user는 user를 반환해서 사용할 수 있는데, 왜 에러가 난거지?
        return {"latest": str(latest_page), "top10_list": str(top10_list)}
    else:
        print("입력하신 회차는 마지막 회차가 아니에요...")
        raise CustomErrorException(status_code=415, detail="Not Last")
# Project_folder/app/templates/lottos/extract.html

{% extends "base.html" %}

{% block title %}
    로또
{% endblock %}

{% block sub_css %}
    <style>

    </style>
    <link rel="stylesheet" href="{{ STATIC_URL }}/statics/css/custom/lotto.css">
{% endblock %}

{% block head_js %}
    <script>
    </script>
{% endblock %}

{% block main %}
    <article class="main flex-item contents-container">
        <div class="form-container">
            <h4 class="mb-20">당첨로또 최다번호 추출</h4>
            {% if old_extract_num %}
                <div id="exist">
                    <div class="mt-10" style="text-align: center">현재 <span id="last-title">{{ old_extract.title }}</span>까지 최다 당첨번호 10개</div>
                    <div style="text-align: center"><span id="lotto-num">{{ old_extract_num }}</span></div>
                    <br>
                </div>
            {% else %}
                <div id="first" class="mt-10" style="text-align: center">1등 당첨 다빈도 번호를 추출하세요!</div><br>
                <div id="exist" style="display: none">
                    <div class="mt-10" style="text-align: center">현재 <span id="last-title"></span>까지 최다 당첨번호 10개</div>
                    <div style="text-align: center"><span id="lotto-num"></span></div>
                    <br>
                </div>
            {% endif %}
            <div>아래에 마지막 회차를 입력하면 그때까지의 당첨 최다번호를 추출하여 로또 번호 예측에 사용됩니다.</div>
            <div class="uk-margin" id="errorTag"><!--js에서 넘어온 오류 메시지를 넣는다.--></div>
            <form id="extractForm" class="mt-20">
                <input class="uk-input" type="text" name="latest_round" placeholder="마지막회차(숫자만) 입력">
                <div class="mt-20">
                    <button class="uk-button uk-button-default uk-align-right" type="submit">추출</button>
                </div>
                <div class="mt-20 pt-5">
                    <a href="https://dhlottery.co.kr/gameResult.do?method=byWin" target="_blank">마지막회차 알아보기</a>
                </div>

            </form>
            <script type="module" src="{{ STATIC_URL }}/statics/js/custom/lotto.js"></script>

            <br>
        </div>
    </article>
{% endblock %}
# Project_folder/app/lottos/lotto.html

{% extends "base.html" %}

{% block title %}
    로또
{% endblock %}

{% block sub_css %}
    <link rel="stylesheet" href="{{ STATIC_URL }}/statics/css/custom/lotto.css">
{% endblock %}

{% block head_js %}
    <script>
    </script>
{#    <script src="{{ STATIC_URL }}/uikit/js/uikit.min.js"></script>#}
{#    <script src="{{ STATIC_URL }}/uikit/js/uikit-icons.min.js"></script>#}

{% endblock %}

{% block main %}
    <article class="main flex-item contents-container">
        <div class="mb-30">
            <div class="main-width padding10">
                <br>
                <div class="uk-text-center">
                    <h3 class="mb-10"><strong>순전히 재미로 로또 맞추기</strong>
                        {% if current_user %}
                            {% if admin %}
                                <a href="/lotto/win/extract">[TOP10 업데이트]</a>
                            {% endif %}
                        {% endif %}
                    </h3>
                </div>
            </div>
        </div>

        <div class="bg-100">
            <div class="main-width lotto mb-20">
                <div class="grid">
                    <div class="uk-align-center lotto-btn">
                        <button class="uk-button uk-button-default mr-10" onclick="window.location.href='/lotto/random';">랜덤 로또</button>
                        {#                    <button class="uk-button uk-button-default" onclick="window.location.href='/lotto/top10';">탑10 로또</button>#}

                    </div>
                </div>
                <div class="uk-margin">
                    <form class="uk-flex uk-align-center wanted-form">
                        {% if input_num %}
                            <input class="uk-input" type="text" name="num" value="{{ input_num }}">
                        {% else %}
                            <input class="uk-input" type="text" name="num" placeholder=" 숫자" required>
                        {% endif %}
                        <button class="uk-button uk-button-default" onclick="window.location.href='';">지정 로또</button>
                    </form>

                </div>
            </div>
            {% if message %}
                <div class="uk-alert-primary uk-text-center message" uk-alert>
                    <a href class="uk-alert-close" uk-close></a>
                    <p>{{ message }}</p>
                </div>
            {% endif %}
            <hr class="ml-10 mr-10">
            <div class="lotto mb-20">
                <div class="newball_container uk-align-center">
                    <div class="grid lotto" uk-grid>

                        {% for num in variable %}
                            <div class="stage">
                                <div class="ball ball{{ loop.index }}" tabIndex="-1">
                                    <div class="number">{{ num }}</div>
                                </div>
                            </div>
                        {% endfor %}

                    </div>
                </div>
                <br>
            </div>
        </div>
        <div class="mt-15">
            <div class="main-width padding10">
                <div>
                    <p class="mb-10"><strong>랜덤 로또</strong>는 빈도수에 관계없이, 무작위로 6개를 뽑아내는 방법입니다.
                        {#                    <strong>탑10 로또</strong>는 {{ latest }}회까지 가장 빈도수가 높은 당첨번호 10개를 뽑아, 그 10개 번호중에서 6개를 무작위로 추출하는 방법입니다.#}
                        <strong>지정 로또</strong>는 빈칸에 숫자를 입력한 후에 클릭하면, {{ latest }}회까지 가장 빈도수가 높은 당첨번호를 입력한 숫자만큼 뽑아, 그 번호들중에서 6개의 로또번호를 무작위로 추출하는 알고리즘으로 되어 있습니다.
                        (당첨 로또 번호는 6개이므로 6이상을 입력해야하고, 로또번호는 1-45까지의 숫자이므로 빈도수 높은 숫자가 46이상은 나올 수 없습니다. 6-45사이의 숫자를 입력해야 합니다.)</p>
                    <div>
                        <div class="mb-5">당첨빈도수가 높은 번호를 몇 개 지정하는냐에 따라, 로또번호를 6개를 무작위 추출하는 경우의 수는 <strong>6개 지정할 때 경우의 수는 1개 / 7개 지정할 때 경우의 수는 7개 / 8개 지정할 때 경우의 수는 28개 / 9개 지정할 때 경우의 수는 84개 / 10개 지정할 때
                            경우의 수는 210개</strong>.....입니다.
                        </div>
                    </div>

                </div>
            </div>
        </div>

    </article>
{% endblock %}
# Project_folder/app/static/statics/css/custom/lotto.css

.form-container {max-width: 500px; border: solid 1px #ced6e0; border-radius: 5px; padding: 2rem; margin: 2rem auto}
.form-container form .mt-20.pt-5 {padding-left: 2px}
.bg-100 {background: #130f40; }
.main-width {max-width: 1280px; margin: 0 auto; padding: 0 20px}
.main-width.lotto {background: #130f40; padding-top: 30px}

.wanted-form {width: 340px; justify-content: center;}
.wanted-form input {width: 20%; height: 50px; margin-right: 5px; text-align: center;}
.wanted-form button {background: #dfe4ea ; height: 50px; border: solid 5px white; font-size: 18px; font-weight: bold}
.wanted-form button:hover {background: white; border: solid 5px white}

.message p {color: #3742fa}

.lotto-btn {margin-bottom: 0; text-align: center;}
.lotto-btn button {width:200px; background: #dfe4ea ; height: 50px; border: solid 5px white; font-size: 18px; font-weight: bold}
.lotto-btn button:hover {background: white; border: solid 5px white}
.newball_container {display: flex; justify-content: center}

.stage {
  /*width: 100px;*/
  height: 100px;
  display: flex;
  perspective: 1200px;
  perspective-origin: 50% 50%;
    /*margin-right: 20px;*/
    /*margin-top: 50px;*/
}

.ball {
  display: flex;
  background: #000;
  border-radius: 50%;
  height: 100px;
  width: 100px;
  margin: 0;
  background: radial-gradient(circle at 150px 150px, #000, #666);
  position: relative;
  /*// overflow: hidden;*/
  outline: none;
  cursor: pointer;
}

.ball:before {
  content: "";
  position: absolute;
  top: 1%;
  left: 5%;
  width: 90%;
  height: 90%;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 0px, #ffffff, rgba(255, 255, 255, 0) 38%);
  filter: blur(5px);
  z-index: 2;
}


.number {
  background: #fff;
  border-radius: 30%;
  height: 60px;
  width: 60px;
  margin: 0;
  background: radial-gradient(circle at 150px 150px, #ccc, #fff);
  position: absolute;
  top: calc(70% - 50px);
  left: calc(70% - 50px);
  display: flex;
  color: #000;
  font-size: 40px;
  align-items: center;
  justify-content: center;
  font-weight: bold;
}

.ball1 {
    background: radial-gradient(circle at 150px 150px, #FBC400, #FBC400);
}

.ball2 {
    background: radial-gradient(circle at 150px 150px, #69c8f2, #69c8f2);
}

.ball3 {
    background: radial-gradient(circle at 150px 150px, #ff7272, #ff7272);
}

.ball4 {
    background: radial-gradient(circle at 150px 150px, #aaa, #aaa);
}

.ball5 {
    background: radial-gradient(circle at 150px 150px, #b0d840, #b0d840);
}

.ball6 {
    background: radial-gradient(circle at 150px 150px, #000, #666);
}

/*.ball {width: 70px; height: 70px; border-radius: 50%; padding: 13px}*/
.ball1{
    background: #fbc400;
    text-shadow: 0 0 3px rgba(73, 57, 0, 0.8);
    background: radial-gradient(circle at 65% 15%, white 1px,  #FDFAA0 3%,  #f6c000 60%,  #FDFAA0 100%); }
.ball2{
    background: #69c8f2;
    text-shadow: 0 0 3px rgba(0, 49, 70, 0.8);
    background: radial-gradient(circle at 65% 15%, white 1px,  #7FF9F7 3%,  #69c8f2 60%,  #7FF9F7 100%); }
.ball3{
    background: #ff7272;
    text-shadow: 0 0 3px rgba(64, 0, 0, 0.8);
    background: radial-gradient(circle at 65% 15%, white 1px,  #FAC0C0 3%,  #ff7272 60%,  #FAC0C0 100%); }
.ball4{
    background: #aaa;
    text-shadow: 0 0 3px rgba(61, 61, 61, 0.8);
    background: radial-gradient(circle at 65% 15%, white 1px,  #F1F1F1 3%,  #aaa 60%,  #F1F1F1 100%); }
.ball5{
    background: #b0d840;
    text-shadow: 0 0 3px rgba(41, 56, 0, 0.8);
    background: radial-gradient(circle at 65% 15%, white 1px,  #DBF74C 3%,  #b0d840 60%,  #DBF74C 100%); }
.ball6{
    background: #1e272e;
    text-shadow: 0 0 3px rgba(41, 56, 0, 0.8);
    background: radial-gradient(circle at 65% 15%, white 1px,  #7bed9f 3%,  #3742fa 60%,  #303952 100%); }
/*.number {background: white; font-size: 20px; font-weight: bold; width: 42px; height: 42px; border-radius: 50%; display: flex; justify-content: center; align-items: center}*/

@media screen and (max-width: 800px) {
    .ball {width: 50px; height: 50px}
    .number {width: 30px; height: 30px; font-size: 16px; top: calc(120% - 50px); left: calc(120% - 50px);}
    .stage {padding-left: 20px; height: 20px}
}

@media screen and (max-width: 450px) {
    .stage {padding-left: 7px}
    .grid.lotto {margin-left: 0}
}