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

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

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

## 今回の内容
いよいよ完成編です
これをつくります

![UI_first.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294646/85aacd29-dc87-4a09-bb3d-b01832319ac3.png)

![UI_after.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294646/fc12c9c6-bbbe-4dac-92ae-be54aabf3092.png)

## バックエンド
まずフォルダとファイルを作ります
1. `routers` フォルダに `birthday` フォルダを作ってください
1. `birthday` フォルダに `birthday.py` を作ってください
1. `birthday` フォルダに `lib` フォルダを作ってください
1. `lib` フォルダに `__init__.py` を作ってください
1. `lib` フォルダに `birthday_module.py` を作ってください

![routers_birthday.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294646/43212777-f67b-4fd3-937a-d4d04020c8d3.png)

### birthday.py

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

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

```python
# 標準ライブラリ
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

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

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

```python
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` も書き換えます

```python
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` フォルダを作ってください
1. `front` フォルダの中に `index.html` を作ってください
1. `front` フォルダの中に `style.css` を作ってください
1. `front` フォルダの中に `script.js` を作ってください

![front_folder.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294646/c04406c0-9c72-4f95-b89f-a4765d62969a.png)

### index.html
内容は気にせずコピペしましょう

```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
こちらも内容は気にせずコピペしましょう

```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/`
    1. body に画面から取ってきた月，日，年をセット
1. 値が返ってきたら画面に反映する

```javascript
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起動
`VSCode` で `F5` を押してください

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

### 2. フロントを表示
エクスプローラーを開いて，
`index.html` をダブルクリックしてください

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

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

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

![UI_first.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294646/5bb17ae2-168f-4322-8096-665889b4f751.png)