0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【初手】サ〇ーウォーズの誕生日当てで分かるFastAPIとVSCodeなど_3.完成編

0
Last updated at Posted at 2025-05-18

これは何の記事

サ〇ーウォーズの誕生日当て Web アプリを通して,
以下のことを学んでいく記事です

  • VSCode で FastAPI を使った開発をする方法
  • フロントからバックまでの開発方法をざっくり

今回の内容

いよいよ完成編です
これをつくります

UI_first.png

UI_after.png

バックエンド

まずフォルダとファイルを作ります

  1. routers フォルダに birthday フォルダを作ってください
  2. birthday フォルダに birthday.py を作ってください
  3. birthday フォルダに lib フォルダを作ってください
  4. lib フォルダに __init__.py を作ってください
  5. lib フォルダに birthday_module.py を作ってください

routers_birthday.png

birthday.py

こちらをコピペしてください
中身はだいたいこんなことをしています

  • URL が http://localhost:8765/birthday だった時の処理をここにまとめる
  • 送られてくるデータが何型の何かを BirthdayModel として作っておく
  • 自作のモジュール( birthday_module.py) を使って曜日を算出して返す
# 標準ライブラリ
import math

# サードパーティライブラリ
from fastapi import APIRouter
from pydantic import BaseModel


# 自作ライブラリ
from routers.birthday.lib.birthday_module import Birthday


router = APIRouter(
    prefix="/birthday",
    tags=["birthday"],
    responses={404: {"description": "Not found"}},
)


class BirthdayModel(BaseModel):
    month: int
    day: int
    year_name: str
    year: int


@router.post("/")
def get_birthday(body: BirthdayModel):

    birthday_instance = Birthday(body.month, body.day, body.year_name, body.year)
    day_of_week = birthday_instance.get_day_of_week()

    return {"status_code": 200, "message": "Success", "day_of_week": day_of_week}

init.py

何も書いていないままで大丈夫です
パッケージとして Python に扱わせるための特別なファイルです

birthday_module.py

以下をコピペしてください
内容はこんな感じです

  • 和暦から西暦に変換する関数を用意
  • ツェラーの公式を使って何曜日かを算出
import math


class Birthday:
    WEEK_LIST = {0: "", 1: "", 2: "", 3: "", 4: "", 5: "", 6: ""}
    ERA_LIST = {
        "明治": "18730101",
        "大正": "19120730",
        "昭和": "19261226",
        "平成": "19890108",
        "令和": "20190501",
    }

    def __init__(self, month: int, day: int, year_name: str, year: int):
        self.month = month
        self.day = day
        self.year_name = year_name
        self.year = year

    def get_day_of_week(self):
        """産まれた日が何曜日だったかを返す"""

        # 和暦から西暦に変換する
        yyyymmdd_str = self._wareki_to_ymd()

        year = int(yyyymmdd_str[:4])
        month = int(yyyymmdd_str[4:6])
        day = int(yyyymmdd_str[6:])

        # 曜日を算出して返す
        return self._calc_day_of_birthday(year, month, day)

    def _calc_day_of_birthday(self, year: int, month: int, day: int) -> str:
        """西暦の日付から曜日を求める (Zeller's congruence)

        Args:
            year (int): 西暦年 (e.g., 2023)
            month (int): 月 (1-12)
            day (int): 日 (1-31)

        Returns:
            "" のような曜日の文字列
        """
        q = day
        m = month
        Y = year

        # January and February are treated as months 13 and 14 of the previous year.
        if m < 3:  # m == 1 (Jan) or m == 2 (Feb)
            m += 12
            Y -= 1

        # Year of the century
        K = Y % 100
        # Century
        J = Y // 100

        # Zeller's congruence formula for the Gregorian calendar:
        # h = (q + floor(13*(m+1)/5) + K + floor(K/4) + floor(J/4) - 2*J) mod 7
        # or h = (q + floor(13*(m+1)/5) + K + floor(K/4) + 5*J + floor(J/4)) mod 7
        # The term math.floor(26 * (m + 1) / 10) is equivalent to math.floor(13 * (m + 1) / 5)

        h_val = (
            q
            + math.floor((13 * (m + 1)) / 5)
            + K
            + math.floor(K / 4)
            + math.floor(J / 4)
            - 2 * J
        )

        day_of_week_index = h_val % 7

        # Zeller's congruence: 0 = Saturday, 1 = Sunday, ..., 6 = Friday
        return self.WEEK_LIST[day_of_week_index]

    def _wareki_to_ymd(self) -> str:
        """
        和暦から西暦に変換する関数

        Returns:
        yyyymmdd 形式の数字
        Raises:
            ValueError: If self.year_name is not a supported era.
        """

        if self.year_name not in self.ERA_LIST:
            raise ValueError(f"Unsupported era name: {self.year_name}")

        era_start_date_str = self.ERA_LIST[self.year_name]
        era_start_year = int(era_start_date_str[:4])

        # yearの変換
        yyyy = str(era_start_year + int(self.year) - 1)
        return f"{yyyy}{self.month:02}{self.day:02}"

main.py

また,main.py も書き換えます

import fastapi
from fastapi.middleware.cors import CORSMiddleware


from routers import learn_break_point
from routers.birthday import birthday #  追加


app = fastapi.FastAPI()
app.include_router(learn_break_point.router)
app.include_router(birthday.router) #  追加

# CORS の設定
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.get("/")
def get_hello():
    return {"message": "Hello World"}

フロント

実際に見るアプリの画面を作ります
まずはフォルダとファイルを作りましょう

  1. front フォルダを作ってください
  2. front フォルダの中に index.html を作ってください
  3. front フォルダの中に style.css を作ってください
  4. front フォルダの中に script.js を作ってください

front_folder.png

index.html

内容は気にせずコピペしましょう

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="style.css">
    <title>Sumxer wars</title>
</head>

<body>
    <main>
        <div class="container">
            <div class="item">
                <div class="message">先輩の誕生日いつですか?</div>
                <div class="icon icon-a"></div>
            </div>

            <div class="item item-reverse">
                <div id="send"></div>
                <div class="message">
                    <label for="birth" name="birth">
                        あたし?
                        <!--
                            7 月 19 日 平成 4 年
                        -->
                        <select name="month" id="month">
                            <option value="1">1</option>
                            <option value="2">2</option>
                            <option value="3">3</option>
                            <option value="4">4</option>
                            <option value="5">5</option>
                            <option value="6">6</option>
                            <option value="7" selected>7</option>
                            <option value="8">8</option>
                            <option value="9">9</option>
                            <option value="10">10</option>
                            <option value="11">11</option>
                            <option value="12">12</option>
                        </select><select name="day" id="day">
                            <option value="1">1</option>
                            <option value="2">2</option>
                            <option value="3">3</option>
                            <option value="4">4</option>
                            <option value="5">5</option>
                            <option value="6">6</option>
                            <option value="7">7</option>
                            <option value="8">8</option>
                            <option value="9">9</option>
                            <option value="10">10</option>
                            <option value="11">11</option>
                            <option value="12">12</option>
                            <option value="13">13</option>
                            <option value="14">14</option>
                            <option value="15">15</option>
                            <option value="16">16</option>
                            <option value="17">17</option>
                            <option value="18">18</option>
                            <option value="19" selected>19</option>
                            <option value="20">20</option>
                            <option value="21">21</option>
                            <option value="22">22</option>
                            <option value="23">23</option>
                            <option value="24">24</option>
                            <option value="25">25</option>
                            <option value="26">26</option>
                            <option value="27">27</option>
                            <option value="28">28</option>
                            <option value="29">29</option>
                            <option value="30">30</option>
                            <option value="31">31</option>
                        </select><select name="year_name" id="year_name">
                            <option value="明治">明治</option>
                            <option value="大正">大正</option>
                            <option value="昭和">昭和</option>
                            <option value="平成" selected>平成</option>
                            <option value="令和">令和</option>
                        </select>
                        <input type="number" name="year" id="year" value="4"></label>
                </div>
                <div class="icon icon-b"></div>
            </div>
        </div>
    </main>
    <script src="script.js"></script>
</body>

</html>

style.css

こちらも内容は気にせずコピペしましょう

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;

  --outer-background-cyan: hsl(194, 50%, 50%);
  --outer-background-magenta: hsl(303, 50%, 50%);
  
  --main-background: hsl(190, 30%, 50%);
  --main-background-light: hsla(190, 30%, 70%, 0.5);
  --main-background-shadow: hsla(190, 30%, 20%, 0.5);

  --common-light: hsla(0, 0%, 100%, 0.2);
  --common-shadow: hsla(0, 0%, 0%, 0.2);
}

main {
  display: flex;
  flex-direction: column;
  align-self: center;
  justify-content: center;
  height: 100vh;
  width: 100vw;

  color: hsl(60, 80%, 80%);
  background-image: linear-gradient(
    45deg,
    var(--outer-background-cyan),
    var(--outer-background-magenta)
  );
}

.container {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;

  gap: 24px;

  width: 900px;
  height: 600px;
  border-radius: 16px;

  margin: 0 auto;

  background-color: var(--main-background);
  box-shadow: 
    -8px -8px 8px hsla(0, 0%, 100%, 0.2), /* 光 */
    8px 8px 8px hsla(0, 0%, 0%, 0.2); /* 影 */
}

.item {
  display: flex;
  justify-content: end;
  align-items: center;

  gap: 16px;
  padding: 8px;

  animation-name: up;
  animation-duration: 1s;
  animation-timing-function: ease-in-out;
}

@keyframes up {
  0% {
    transform: translateY(20px);
  }
  100% {
    transform: translateY(0px);
  }
}

.item-reverse {
  justify-content: start;
  flex-direction: row-reverse;
}

.message {
  display: flex;
  justify-content: center;
  align-items: center;

  min-width: 400px;

  font-size: 24px;
  color: #333;

  padding: 8px;
  background-color: var(--main-background);
  box-shadow:
    -8px -8px 8px var(--main-background-light), /* 光 */
    8px 8px 8px var(--main-background-shadow); /* 影 */

  border-radius: 8px;
}

.icon {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 50px;
  height: 50px;
  border-radius: 999px;

  box-shadow: 
    -4px -4px 4px var(--common-light), /* 光 */
    4px 4px 4px var(--common-shadow); /* 影 */
}

.icon-a {
  background-color: var(--outer-background-cyan);
}

.icon-b {
  background-color: var(--outer-background-magenta);
}

#year {
  width: 3rem;
}

#send {
  width: 40px;
  height: 30px;
  background-color: hsl(60, 20%, 20%);
  clip-path: polygon(0 0, 0 100%, 100% 50%);
  opacity: 0.8;
  transition: .5s;
  cursor: pointer;

  box-shadow: 
    -4px -4px 4px var(--common-light), /* 光 */
    4px 4px 4px var(--common-shadow); /* 影 */
}

#send:hover {
  opacity: 1;
}

select, input {
  display: inline-block;
  background-color: var(--main-background);
  opacity: 0.8;
  border: none;

  width: 3rem;

  outline: none;
  margin: auto 8px;
}

script.js

こちらをコピペしましょう
内容はこんな感じです

  1. API を実行する
    1. URL は http://localhost:8765/birthday/
    2. body に画面から取ってきた月,日,年をセット
  2. 値が返ってきたら画面に反映する
const container = document.querySelector('.container');
const sendBtn = document.querySelector('#send')

const month = document.querySelector('#month')
const day = document.querySelector('#day')
const yearName = document.querySelector('#year_name')
const year = document.querySelector('#year')


sendBtn.addEventListener('click', async () => {
  console.log(`${month.value}, ${day.value}, ${yearName.value}, ${year.value}`)
  const response = await fetch(
    'http://localhost:8765/birthday/',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        month: parseInt(month.value),
        day: parseInt(day.value),
        year_name: yearName.value,
        year: parseInt(year.value),
      }),
    }

  )
  const data = await response.json()
  createItem(data)
});

const createItem = (data) => {
  const item = document.createElement('div')
  item.classList.add('item')

  const message = document.createElement('div')
  message.classList.add('message')
  message.innerText = `${data.day_of_week}曜日です!`

  const icon = document.createElement('div')
  icon.classList.add('icon', 'icon-a')


  item.appendChild(message)
  item.appendChild(icon)

  container.appendChild(item)
}

実行

1. FastAPI起動

VSCodeF5 を押してください

main.py を表示している状態だと安心です

2. フロントを表示

エクスプローラーを開いて,
index.html をダブルクリックしてください

どのアプリで開くか聞かれたら,
お好みのブラウザ(Chrome)を選択してください

するとこのような画面が表示されます
色々月や日などを変えて送信ボタンを押してみてください

FastAPIで作ったバックエンドが頑張って曜日を計算してくれます

UI_first.png

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?