Next.jsとRiot Games APIを使用してLeague of Legendsのサモナー統計ダッシュボードアプリを作成する

## 自己紹介
はじめまして。
株式会社シーエー・アドバンスに勤めている當銘塁と申します。
最近 ``League of Legends`` にハマりまくっています！
どのくらいハマりまくっているかというと、
 ### 一日のスケジュールの三分の一が ``League of Legends`` になるほどハマりまくっています！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3726073/1106fe7f-509d-52e0-1ba0-dcad2553b040.png)



 ## なぜLeague of Legendsのサモナー統計ダッシュボードアプリを作成したのか？
 入社後半年が経過し、普段Next.jsで開発していたので、自分の趣味である``League of Legends（LoL）``を題材に何か作れないかと考えました。

 ## アプリの概要
 このアプリでは、Riot Games APIを使って以下の情報を取得して表示します

 - サモナーの基本情報
     - サモナー名、レベルなど
 - ランク情報
     - ソロランクやフレックスランクのティア、LP、勝敗など
 - チャンピオン統計
    - 使用頻度、勝率、KDA
 - レーン統計
    - レーン別のプレイ状況（勝率やKDA）

 ## Riot Games APIを取得する
 https://developer.riotgames.com 

1\. 上記のURLにアクセスすると、ログインが求めらるので、ログインする

2\. ログインすると下記の画面が表示されるので、APIキーの生成ボタンを押す

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3726073/16f096c1-491c-87be-965e-38ad2e44774a.png)

### APIキー取得完了🎉🎉🎉

:::note warn
上記の方法で取得したAPIキーは、開発環境でのみ使用して良いとのことなので、注意してください！
:::


 ## サモナー名とタグラインをサーバーに送信する
 formタグの中にサモナー名とタグ名のinputを用意し、統計情報を取得ボタンを押すと、handleSubmit関数が走り、サーバーサイドに送信されて、サーバーサイドで処理されたデータが返ってくるという仕組みです！
 ```
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setLoading(true);
    setError(null);

    try {
      const data = await fetchSummonerStats(gameName, tagLine);
      setStats(data);
    } catch (err) {
      console.error(err);
      setError(
        err instanceof Error
          ? err.message
          : "サモナーの統計情報の取得に失敗しました"
      );
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="container mx-auto p-4 bg-gray-100 min-h-screen">
      <h1 className="text-4xl font-bold text-center text-blue-600 mb-8">
        サモナー統計
      </h1>

      <form
        onSubmit={handleSubmit}
        className="mb-8 bg-white shadow-md rounded-lg p-6"
      >
        <div className="flex flex-col sm:flex-row gap-4">
          <input
            type="text"
            value={gameName}
            onChange={(e) => setGameName(e.target.value)}
            placeholder="サモナー名を入力"
            required
            className="p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
          />
          <div className="mt-3 font-bold">#</div>
          <input
            type="text"
            value={tagLine}
            onChange={(e) => setTagLine(e.target.value)}
            placeholder="タグラインを入力"
            required
            className="p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
          />
          <button
            type="submit"
            disabled={loading}
            className="bg-blue-500 text-white p-3 rounded-lg hover:bg-blue-600 disabled:bg-gray-400 transition duration-300 ease-in-out"
          >
            {loading ? "読み込み中..." : "統計情報を取得"}
          </button>
        </div>
      </form>

```


 ## Riot Games API を使った統計情報の取得

 サーバーサイドの関数 fetchSummonerStats は以下のように実装しています。

 ### 概要

1\. サモナー名とタグラインからPUUID（プレイヤー固有ID）を取得。
2\. PUUIDを使ってサモナーの基本情報（レベル、ランク）を取得。
3\. 最新のマッチ履歴を取得して試合結果を解析。
4\. チャンピオンごとの統計（使用頻度、勝率、KDA）を計算。
5\. レーンごとの統計（勝率、KDA）を計算。

 ### 実装の流れ
 
1\. **Riot IDからPUUIDを取得**
Riot Games APIでは、Riot ID（サモナー名 + タグライン）を元にPUUIDを取得できます。
 ```
const accountResponse = await fetch(
  `${RIOT_ACCOUNT_URL}/riot/account/v1/accounts/by-riot-id/${encodeURIComponent(
    gameName
  )}/${encodeURIComponent(tagLine)}`,
  { headers: { "X-Riot-Token": RIOT_API_KEY! } }
);
const accountData = await accountResponse.json();
const puuid = accountData.puuid;

```
***
2\. **サモナー情報の取得**
PUUIDを使用して、サモナーのレベルなどの基本情報を取得します。

```
const summonerResponse = await fetch(
  `${RIOT_API_BASE_URL}/lol/summoner/v4/summoners/by-puuid/${puuid}`,
  { headers: { "X-Riot-Token": RIOT_API_KEY! } }
);
const summonerData = await summonerResponse.json();
```
***
3\. **ランク情報の取得**
サモナーのランクデータを取得します。
```
const rankResponse = await fetch(
  `${RIOT_API_BASE_URL}/lol/league/v4/entries/by-summoner/${summonerData.id}`,
  { headers: { "X-Riot-Token": RIOT_API_KEY! } }
);
const rankData = await rankResponse.json();
```
***
4\. **マッチ履歴の取得と解析**
最新10試合のマッチデータを取得し、それを元にチャンピオン統計やレーン統計を計算します。

```
const matchListResponse = await fetch(
  `${RIOT_MATCH_URL}/lol/match/v5/matches/by-puuid/${puuid}/ids?start=0&count=10`,
  { headers: { "X-Riot-Token": RIOT_API_KEY! } }
);
const matchIds = await matchListResponse.json();

const matchDataPromises = matchIds.map((matchId: string) =>
  fetch(`${RIOT_MATCH_URL}/lol/match/v5/matches/${matchId}`, {
    headers: { "X-Riot-Token": RIOT_API_KEY! },
  }).then((res) => res.json())
);
const matchesData = await Promise.all(matchDataPromises);
```
***
5\. **チャンピオン統計とレーン統計の計算**
各試合のデータから、チャンピオンごとの使用頻度、勝率、KDA、またレーンごとの統計を計算します。

```
matchesData.forEach((match: any) => {
  const participant = match.info.participants.find(
    (p: any) => p.puuid === puuid
  );
  if (participant) {
    totalGames++;
    if (participant.win) totalWins++;

    // チャンピオン統計
    const championName = participant.championName;
    if (!championStats[championName]) {
      championStats[championName] = {
        games: 0,
        wins: 0,
        kills: 0,
        deaths: 0,
        assists: 0,
      };
    }
    championStats[championName].games++;
    championStats[championName].wins += participant.win ? 1 : 0;
    championStats[championName].kills += participant.kills;
    championStats[championName].deaths += participant.deaths;
    championStats[championName].assists += participant.assists;

    // レーン統計
    const lane = participant.teamPosition || "UNKNOWN";
    if (!laneStats[lane]) {
      laneStats[lane] = {
        games: 0,
        wins: 0,
        kills: 0,
        deaths: 0,
        assists: 0,
      };
    }
    laneStats[lane].games++;
    laneStats[lane].wins += participant.win ? 1 : 0;
    laneStats[lane].kills += participant.kills;
    laneStats[lane].deaths += participant.deaths;
    laneStats[lane].assists += participant.assists;
  }
});
```

 ## クライアントサイドでの表示
取得した統計情報をReactのコンポーネントで表示します。

**基本情報の表示** 
```
<StatsCard
  title="基本情報"
  icon={<FaUser className="text-blue-500" />}
  content={
    <>
      <p className="text-lg">
        <span className="font-medium">名前:</span>
        {stats.accountData.gameName}#{stats.accountData.tagLine}
      </p>
      <p className="text-lg">
        <span className="font-medium">レベル:</span>
        {stats.summonerData.summonerLevel}
      </p>
    </>
  }
/>
```
***
**チャンピオン統計の表示**
```
<StatsCard
  title="チャンピオン統計"
  icon={<FaGamepad className="text-purple-500" />}
  content={
    <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
      {Object.entries(stats.championStats).map(([champion, data]) => (
        <div
          key={champion}
          className="bg-gray-100 p-3 rounded-lg hover:shadow-md transition duration-300"
        >
          <h4 className="font-medium text-lg">{champion}</h4>
          <p>ゲーム数: {data.games}</p>
          <p>勝率: {((data.wins / data.games) * 100).toFixed(2)}%</p>
          <p>
            KDA: {((data.kills + data.assists) / data.deaths).toFixed(2)}
          </p>
        </div>
      ))}
    </div>
  }
/>
```


 ## そして、ついに完成したものがこちら🎉🎉🎉
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3726073/a939653b-c204-cb0e-bd34-2b6886fff206.png)


 ## 自分のアカウントを検索してみると....
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3726073/5db0b451-47a6-f2b2-8218-4fe12d4ef39f.png)

 ### 正常に表示されました🎉🎉🎉

 自分のプレイヤー名が厨二病ってことと、TOPレーナーってことやイラオイOTPってことがバレました！！

 ## まとめ
 自分の大好きな``League of Legends（LoL）``を題材に開発ができて、よかったです。
 下記のイベントに参加することから、急いでChatGPTを活用して開発をしました。
 1日で完成することができたので、上出来なのではないかと思っています。
 今後も追加したい機能があるので、趣味で開発していきたいと思います！
 
 
 https://qiita.com/advent-calendar/2024

 ## 参考

 https://qiita.com/himrox/items/8e99ee4a8451add5f332

 https://orolog.hatenablog.jp/entry/2018/02/19/221553

 https://qiita.com/hikozuma/items/247a9f4a4f9c68319e12

 https://zenn.dev/cloud_ace/articles/llm-sugests-lol-meta-builds