League Of Legendsの配信オーバーレイでトップバーを作る【JavaScript編】

トップバー配信オーバーレイ LeagueOfLegends
トップバー配信オーバーレイ

 配信でぱっとゲーム状況がわかる配信オーバーレイがあるといいですよね。

 JavaScriptでLeague Of Legendsの配信オーバーレイを作れることが確認できたので、KDAやオブジェクト取得状況を表示するトップバーを作ってみます。

はじめに

 League Of Legendsの配信を見ていると、「あれ?今は勝ってるのかな・・・?」とぱっと見た限り、わからないことないですか?

 プレイ画面にはKDAしか表示されておらず、ぱっと表示されるスコアボードからオブジェクトの状況を確認するは難しいですよね。
 配信者側もコメントしてくれたリスナーに状況を都度説明するのは大変ですし、冗長になりがちです。

 そこで常にKDAやオブジェクト状況を表示するトップバーがあれば、配信を見に来てくれたリスナーに状況がすぐに伝えれます。
 また、JavaScript+HTMLで作ることで配信者のイメージに合わせたオーバーレイに修正することができます。

 この記事では、LoLのLiveClientData APIから試合中のイベント情報を取得し、JavaScriptとHTMLで動作するトップバー型の配信オーバーレイを作る方法を紹介します。

前提

 JavaScript+HTMLで配信オーバーレイを作る場合、CORS対策が必要となります。

 以下記事でCORS対策APIプロキシについて解説しているので参考にしてみてください。
 本記事でも補足としてソースコードを記載しています。

構想

 トップバーを作成するには、必要な情報をLiveClientDataAPIから集めてくる必要があります。

 ここでは情報の取得方法と集計方法を説明します。
 「LiveClientDataAPIってどんな情報が含まれているの?」という方は以下記事を参考にしてください。

LoLの試合情報をどう取得するか

 LoLの配信オーバーレイを自作するにあたって、まず考えるべきは「どこから、どんな情報を取得するか」という点です。
 幸い、LoLにはローカルで動作する LiveClientData API が用意されており、試合中のさまざまな情報をリアルタイムに取得できます。

 今回のトップバーで表示させるのは、以下です

  • チームキル数
  • タワー破壊数
  • ヴォイドクラブ取得数
  • ヘラルド取得数
  • ドラゴン取得数
  • バロン取得数

 これらの情報はLiveClientDataAPIのeventsから取得できます。 eventsは、試合中に発生したイベント(キル、オブジェクト取得、建物破壊など)を時系列で返してくれるため、イベントを解析してチームごとに集計することで、トップバーに必要な情報を集めることができます。

どう集計するか

 イベントはプレイヤー単位で発生するため、どのプレイヤーがどのチームに属しているかを把握する必要があります。

 そのため、 allgamedata からプレイヤーとチームのマッピングを作成し、イベントが発生するたびに「このイベントはどちらのチームのものか?」を判定して、チームごとのスコアに反映していきます。

◆キルイベントの例
    "events": {
        "Events": [
            {
      ~中略~
            },
            {
                "Assisters": [
                    "SUMMONER_NAME_X",
                ],
                "EventID": 1,
                "EventName": "ChampionKill",
                "EventTime": 57.96607208251953,
                "KillerName": "SUMMONER_NAME_A",
                "VictimName": "SUMMONER_NAME_A"
            },

 上記のようにSUMMONER_NAME_AChampionKillをしていますが、どちらのチームがキルしたのか判断できません。
 そのため、allplayersからプレイヤー(riotIdGameName)とチーム(team)を取得し、マッピングを作成しておきます。

◆allplayersの例
    {
        "championName": "フィオラ",
        "isBot": false,
        "isDead": false,
        "items": [
            ・・・            ],
        "level": 6,
        "position": "TOP",
        "rawChampionName": "game_character_displayname_Fiora",
        "rawSkinName": "game_character_skin_displayname_Fiora_89",
        "respawnTimer": 0.0,
        "riotId": "SUMMONER_NAME_A#TAG",
        "riotIdGameName": "SUMMONER_NAME_A",
        "riotIdTagLine": "TAG",
        "runes": {
            ・・・
        },
        "scores": {
            ・・・            },
        "skinID": 89,
        "skinName": "バトルクイーン フィオラ",
        "summonerName": "SUMMONER_NAME_A#TAG",
        "summonerSpells": {
            ・・・            },
        "team": "ORDER"
    }

 集計方法としてはキルした人(KillerName)とプレイヤー(riotIdGameName)が一致するチーム(team)でイベント集計します。

実装

 この章では、LiveClientData API の eventdata を使って、KDAやオブジェクト取得状況をリアルタイムに集計・表示する方法を解説します。

イベントの集計方法

 イベントには「誰が何をしたか」は記録されていますが、「そのプレイヤーがどちらのチームに属しているか」は明示されていません。
 そのため、まずは allgamedata からプレイヤー名とチーム名(ORDER / CHAOS)を対応づけるマップを作成します。

// riotIdGameName からチームを特定するためのマップを作製
async function initPlayerTeamMap() {
    // APIから全プレイヤーデータを取得
    const res = await fetch(ALLGANEDATA_URL);

    // JSONレスポンスからプレイヤーデータを抽出
    const dataRoot = await res.json();

    // allPlayersを取得
    const players = dataRoot.data?.allPlayers ?? [];

    // プレイヤーデータをループして、riotIdGameNameをキーにチームをマッピング
    players.forEach(p => {
        // 1. riotIdGameName があるなら登録
        if (p.riotIdGameName) {
            playerTeamMap[p.riotIdGameName] = p.team;
        }

        // 2. summonerName も登録(プラクティスモード対策)
        if (p.summonerName) {
            playerTeamMap[p.summonerName] = p.team;
        }
    });

    console.log("playerTeamMap:", playerTeamMap);
}

 基本的にはriotIdGameNameで足りますが、プラクティスモードの場合、うまく動作しないケースがあるため、summonerNameも合わせて登録しています。 

 次に、events から取得したイベントを走査し、必要な情報だけを抽出します。

// 定期的にイベントをポーリングして更新する関数
async function updateEvents() {
    try {

        // APIから全ゲームデータを取得
        const res = await fetch(ALLGANEDATA_URL);

        // JSONレスポンスからゲームデータを抽出
        const dataRoot = await res.json();

        // eventsを取得
        const events = dataRoot.data?.events?.Events ?? [];

        // 新規イベントだけ処理
        for (const ev of events) {
            if (ev.EventID > CurrentEventID) {
                // 新しいイベントのみ集計
                applyEvent(ev);
            }
        }

        // 最新のイベントIDを更新
        if (events.length > 0) {
            CurrentEventID = events[events.length - 1].EventID;
        }

    } catch (e) {
        console.error("updateEvents error:", e);
    }
}

// イベントデータを処理してチームの統計を更新する関数
function applyEvent(ev) {
    // KillerNameからチームを特定
    const killer = ev.KillerName;
    const team = playerTeamMap[killer];
    if (!team) return;

    // チームの統計オブジェクトを取得
    const t = teamStats[team];

    // イベントの種類に応じて統計を更新
    switch (ev.EventName) {
        case "HordeKill":
            t.HordeKill++;
            break;
        case "HeraldKill":
            t.HeraldKill++;
            break;
        case "DragonKill":
            t.DragonKill++;
            break;
        case "BaronKill":
            t.BaronKill++;
            break;
        case "TurretKilled":
            t.TurretKilled++;
            break;
        case "ChampionKill":
            t.ChampionKill++;
            break;
    }
}

上記のように、イベントをチームごとに集計していきます。

表示の仕組み

 集計したスコアは、HTMLとJavaScriptで構築したトップバーに反映します。
 以下は、スコアを更新する関数の一例です。

    // APIから取得したデータをトップバーに反映する関数
    function updateTopbar(data) {
        // 青(ORDER)側
        document.getElementById('order-void').textContent = data.order.voidClub;
        document.getElementById('order-herald').textContent = data.order.herald;
        document.getElementById('order-dragon').textContent = data.order.dragon;
        document.getElementById('order-baron').textContent = data.order.baron;
        document.getElementById('order-tower').textContent = data.order.tower;
        document.getElementById('order-teamkills').textContent = data.order.teamKills;
        // 赤(CHAOS)側
        document.getElementById('chaos-void').textContent = data.chaos.voidClub;
        document.getElementById('chaos-herald').textContent = data.chaos.herald;
        document.getElementById('chaos-dragon').textContent = data.chaos.dragon;
        document.getElementById('chaos-baron').textContent = data.chaos.baron;
        document.getElementById('chaos-tower').textContent = data.chaos.tower;
        document.getElementById('chaos-teamkills').textContent = data.chaos.teamKills;
    }

 この関数を setInterval() で定期的に呼び出すことで、リアルタイムにスコアが更新されるトップバーが完成します。
 実際は以下のような呼び出しを行い、表示を更新しています。

    // 定期的にAPIからデータを取得してトップバーを更新する関数
    async function pollEvents() {
        try {
            // eventsを集計する関数を呼び出し
            await updateEvents();

            // updateTopbar 用にマッピング
            const mapped = {
                order: {
                    voidClub: teamStats.ORDER.HordeKill,
                    herald: teamStats.ORDER.HeraldKill,
                    dragon: teamStats.ORDER.DragonKill,
                    baron: teamStats.ORDER.BaronKill,
                    tower: teamStats.ORDER.TurretKilled,
                    teamKills: teamStats.ORDER.ChampionKill,
                    teamGold: teamStats.ORDER.teamGold
                },
                chaos: {
                    voidClub: teamStats.CHAOS.HordeKill,
                    herald: teamStats.CHAOS.HeraldKill,
                    dragon: teamStats.CHAOS.DragonKill,
                    baron: teamStats.CHAOS.BaronKill,
                    tower: teamStats.CHAOS.TurretKilled,
                    teamKills: teamStats.CHAOS.ChampionKill,
                    teamGold: teamStats.CHAOS.teamGold
                }
            };

            // トップバーを更新
            await updateTopbar(mapped);

        } catch (e) {
            console.error("pollEvents error:", e);
        }
    }

    // ページが読み込まれたときに初期化と定期的なイベントのポーリングを開始
    (async () => {
        // プレイヤーとチームのマッピングを初期化
        await initPlayerTeamMap();

        // 5秒ごとにイベントをポーリングしてトップバーを更新
        setInterval(pollEvents, 5000);
    })();

補足

 JavaScript+HTMLで配信オーバーレイを作る場合、CORS対策が必要になります。

 前提記事で解説していますが、ここにも参考としてソースを残しておきます。

from flask import Flask, jsonify
import requests
import urllib3

# 証明書の警告を無効化
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# Flaskアプリケーションのセットアップ
app = Flask(__name__)

# requestsセッションの作成(接続の再利用のため)
session = requests.Session()

# LiveClientDataAPI(LoLクライアント)エンドポイント
BASE = "https://127.0.0.1:2999/liveclientdata"

# LoLクライアントからデータを取得する関数
def fetch_lcu(path):
    try:
        r = session.get(f"{BASE}/{path}", verify=False, timeout=0.3)
        return {"ok": True, "data": r.json()}
    except Exception as e:
        return {"ok": False, "error": str(e)}

# CORS対策のため、レスポンスにヘッダーを追加
@app.after_request
def add_cors_headers(response):
    response.headers["Access-Control-Allow-Origin"] = "*"
    return response

# LoLクライアントから全ゲームデータ(allgamedata)を取得するエンドポイント
@app.route("/allgamedata")
def allgamedata():
    return jsonify(fetch_lcu("allgamedata"))

# app.run()は、Flaskアプリケーションを起動するための関数
# ポート3000でデバッグモードを有効にしてアプリケーションを実行
if __name__ == "__main__":
    app.run(port=3000, debug=True)

動作確認

 ここまでの実装をもとに、実際に動作しているトップバーオーバーレイの様子を紹介します。

オーバーレイの表示

 今回のトップバーは以下の情報がリアルタイムで更新されます。

  • チームキル
  • タワー破壊
  • バロン
  • ドラゴン
  • ヘラルド
  • ヴォイドクラブ

 イベントが発生する度に、JavaScriptでeventsから情報を取得し、トップバーに反映しています。例えば、青(ORDER)側がキルを獲得すると”32″と表示されるようになります。

トップバー配信オーバーレイ

OBSの設定方法

 OBS上の設定方法について説明します。

 OBSは特に難しいことはなく、ブラウザソースで追加します。
 以下あたりを設定しておくといい感じです。

  • 表示されていないときにソースをシャットダウンする
  • シーンがアクティブになったときにブラウザの表示を更新する
配信オーバーレイのOBS設定

サンプルコード:全量

 ポイントを絞ってサンプルコードを展開していたので、改めて全量をこの章に残します。

 トップバー配信オーバーレイは大きく3つで構成されています。

  • LoL_TopBar_blog.html:トップバー配信オーバーレイの本体
  • LoL_Api_blog.js:LiveClientDataAPIアクセス用のJavaScript
  • LoL_APIProxy.py:CORS対策用APIプロキシ

LoL_TopBar_blog.html:トップバー配信オーバーレイの本体

 トップバー配信オーバーレイのサンプルコード全量です。

 cssを変更することで見栄えが変更できます。
 bodyを変更することで不要な項目を削除できます。
 更新頻度を変更したい場合はsetIntervalの数値を変更してください。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>LoL Top Bar</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: transparent;
        }

        .topbar {
            display: flex;
            justify-content: space-between;
            align-items: center;
            background: rgba(0, 0, 0, 0.9); /* semi-transparent black */
            border: 2px solid #000;
            border-radius: 8px;
            padding: 6px 12px;
            color: #fff;
            width: 100%;
            max-width: 900px;
            margin: 0 auto;
            font-size: 1.1em;
        }

        .side {
            display: flex;
            gap: 12px;
            align-items: center;
        }

        .order {
            justify-content: flex-start;
            color: #66b3ff;
        }

        .chaos {
            justify-content: flex-end;
            color: #ff6666;
        }

        .entry {
            display: flex;
            flex-direction: column;
            align-items: center;
            min-width: 60px;
        }

        .label {
            font-size: 0.75em;
            opacity: 0.8;
        }

        .value {
            font-size: 1.2em;
            font-weight: bold;
        }


</style>
</head>
<body>
    <!-- トップバーのHTML構造 -->
    <div class="topbar">
        <!-- 左側(Order)と右側(Chaos)に分けて表示 -->
        <!-- Orderサイドの情報を表示するセクション -->
        <div class="side order" id="orderSide">
            <div class="entry"><div class="label">VoidClub</div><div class="value" id="order-void">0</div></div>
            <div class="entry"><div class="label">Herald</div><div class="value" id="order-herald">0</div></div>
            <div class="entry"><div class="label">Dragon</div><div class="value" id="order-dragon">0</div></div>
            <div class="entry"><div class="label">Baron</div><div class="value" id="order-baron">0</div></div>
            <div class="entry"><div class="label">Tower</div><div class="value" id="order-tower">0</div></div>
            <div class="entry"><div class="label">Kills</div><div class="value" id="order-teamkills">0</div></div>
        </div>
        <!-- Chaosサイドの情報を表示するセクション -->
        <div class="side chaos" id="chaosSide">
            <div class="entry"><div class="label">Kills</div><div class="value" id="chaos-teamkills">0</div></div>
            <div class="entry"><div class="label">Tower</div><div class="value" id="chaos-tower">0</div></div>
            <div class="entry"><div class="label">Baron</div><div class="value" id="chaos-baron">0</div></div>
            <div class="entry"><div class="label">Dragon</div><div class="value" id="chaos-dragon">0</div></div>
            <div class="entry"><div class="label">Herald</div><div class="value" id="chaos-herald">0</div></div>
            <div class="entry"><div class="label">VoidClub</div><div class="value" id="chaos-void">0</div></div>
        </div>
    </div>

    <!--  LoL_APIProxy.pyで提供されるAPIを呼び出すためのJavaScriptコード -->
    <script src="LoL_Api_blog.js"></script>

    <!--  トップバーを更新するためのJavaScriptコード -->
    <script>

    // APIから取得したデータをトップバーに反映する関数
    function updateTopbar(data) {
        // 青(ORDER)側
        document.getElementById('order-void').textContent = data.order.voidClub;
        document.getElementById('order-herald').textContent = data.order.herald;
        document.getElementById('order-dragon').textContent = data.order.dragon;
        document.getElementById('order-baron').textContent = data.order.baron;
        document.getElementById('order-tower').textContent = data.order.tower;
        document.getElementById('order-teamkills').textContent = data.order.teamKills;
        // 赤(CHAOS)側
        document.getElementById('chaos-void').textContent = data.chaos.voidClub;
        document.getElementById('chaos-herald').textContent = data.chaos.herald;
        document.getElementById('chaos-dragon').textContent = data.chaos.dragon;
        document.getElementById('chaos-baron').textContent = data.chaos.baron;
        document.getElementById('chaos-tower').textContent = data.chaos.tower;
        document.getElementById('chaos-teamkills').textContent = data.chaos.teamKills;
    }

    // ページ読み込み時にトップバーを初期化(全て0で表示)
    updateTopbar({
        order: { voidClub:0, herald:0, dragon:0, baron:0, tower:0, teamKills:0 },
        chaos: { voidClub:0, herald:0, dragon:0, baron:0, tower:0, teamKills:0 }
    });

    // 定期的にAPIからデータを取得してトップバーを更新する関数
    async function pollEvents() {
        try {

            // APIから全ゲームデータを取得
            await updateEvents();

            // updateTopbar 用にマッピング
            const mapped = {
                order: {
                    voidClub: teamStats.ORDER.HordeKill,
                    herald: teamStats.ORDER.HeraldKill,
                    dragon: teamStats.ORDER.DragonKill,
                    baron: teamStats.ORDER.BaronKill,
                    tower: teamStats.ORDER.TurretKilled,
                    teamKills: teamStats.ORDER.ChampionKill,
                    teamGold: teamStats.ORDER.teamGold
                },
                chaos: {
                    voidClub: teamStats.CHAOS.HordeKill,
                    herald: teamStats.CHAOS.HeraldKill,
                    dragon: teamStats.CHAOS.DragonKill,
                    baron: teamStats.CHAOS.BaronKill,
                    tower: teamStats.CHAOS.TurretKilled,
                    teamKills: teamStats.CHAOS.ChampionKill,
                    teamGold: teamStats.CHAOS.teamGold
                }
            };

            // トップバーを更新
            await updateTopbar(mapped);

        } catch (e) {
            console.error("pollEvents error:", e);
        }
    }

    // ページが読み込まれたときに初期化と定期的なイベントのポーリングを開始
    (async () => {
        // プレイヤーとチームのマッピングを初期化
        await initPlayerTeamMap();

        // 5秒ごとにイベントをポーリングしてトップバーを更新
        setInterval(pollEvents, 1000);
    })();

    </script>
</body>
</html>

LoL_Api_blog.js:LiveClientDataAPIアクセス用のJavaScript

 LiveClientDataAPIアクセス用のJavaScriptです。

 JavaScriptを改変することでそのほかの情報にもアクセスできます。
 難易度は少し高いですが、スコアボードのようなオーバーレイも作ることができます。

const ALLGANEDATA_URL = "http://localhost:3000/allgamedata";

let playerTeamMap = {};
let CurrentEventID = 0;

let teamStats = {
    ORDER: {HordeKill: 0, HeraldKill: 0, DragonKill: 0,BaronKill: 0, TurretKilled: 0, ChampionKill: 0},
    CHAOS: {HordeKill: 0, HeraldKill: 0, DragonKill: 0,BaronKill: 0, TurretKilled: 0, ChampionKill: 0}
};


// riotIdGameName からチームを特定するためのマップを作製
async function initPlayerTeamMap() {
    // APIから全プレイヤーデータを取得
    const res = await fetch(ALLGANEDATA_URL);

    // JSONレスポンスからプレイヤーデータを抽出
    const dataRoot = await res.json();

    // allPlayersを取得
    const players = dataRoot.data?.allPlayers ?? [];

    // プレイヤーデータをループして、riotIdGameNameをキーにチームをマッピング
    players.forEach(p => {
        // 1. riotIdGameName があるなら登録
        if (p.riotIdGameName) {
            playerTeamMap[p.riotIdGameName] = p.team;
        }

        // 2. summonerName も登録(events の KillerName はこっち)
        if (p.summonerName) {
            playerTeamMap[p.summonerName] = p.team;
        }
    });

    console.log("playerTeamMap:", playerTeamMap);
}

// 定期的にイベントをポーリングして更新する関数
async function updateEvents() {
    try {

        // APIから全ゲームデータを取得
        const res = await fetch(ALLGANEDATA_URL);

        // JSONレスポンスからゲームデータを抽出
        const dataRoot = await res.json();

        // eventsを取得
        const events = dataRoot.data?.events?.Events ?? [];

        // 新規イベントだけ処理
        for (const ev of events) {
            if (ev.EventID > CurrentEventID) {
                // 新しいイベントのみ集計
                applyEvent(ev);
            }
        }

        // 最新のイベントIDを更新
        if (events.length > 0) {
            CurrentEventID = events[events.length - 1].EventID;
        }
    

    } catch (e) {
        console.error("updateEvents error:", e);
    }
}

// イベントデータを処理してチームの統計を更新する関数
function applyEvent(ev) {
    // KillerNameからチームを特定
    const killer = ev.KillerName;
    const team = playerTeamMap[killer];

    if (!team) return;

    // チームの統計オブジェクトを取得
    const t = teamStats[team];

    // イベントの種類に応じて統計を更新
    switch (ev.EventName) {
        case "HordeKill":
            t.HordeKill++;
            break;
        case "HeraldKill":
            t.HeraldKill++;
            break;
        case "DragonKill":
            t.DragonKill++;
            break;
        case "BaronKill":
            t.BaronKill++;
            break;
        case "TurretKilled":
            t.TurretKilled++;
            break;
        case "ChampionKill":
            t.ChampionKill++;
            break;
    }
}

LoL_APIProxy.py:CORS対策用APIプロキシ

 CORS対策用APIプロキシのソースになります。
 Node.jsなどでも代替できますが、一旦Pythonで作成しています。

from flask import Flask, jsonify
import requests
import urllib3

# 証明書の警告を無効化
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# Flaskアプリケーションのセットアップ
app = Flask(__name__)

# requestsセッションの作成(接続の再利用のため)
session = requests.Session()

# LiveClientDataAPI(LoLクライアント)エンドポイント
BASE = "https://127.0.0.1:2999/liveclientdata"

# LoLクライアントからデータを取得する関数
def fetch_lcu(path):
    try:
        r = session.get(f"{BASE}/{path}", verify=False, timeout=0.3)
        return {"ok": True, "data": r.json()}
    except Exception as e:
        return {"ok": False, "error": str(e)}

# CORS対策のため、レスポンスにヘッダーを追加
@app.after_request
def add_cors_headers(response):
    response.headers["Access-Control-Allow-Origin"] = "*"
    return response

# LoLクライアントから全ゲームデータ(allgamedata)を取得するエンドポイント
@app.route("/allgamedata")
def allgamedata():
    return jsonify(fetch_lcu("allgamedata"))

# app.run()は、Flaskアプリケーションを起動するための関数
# ポート3000でデバッグモードを有効にしてアプリケーションを実行
if __name__ == "__main__":
    app.run(port=3000, debug=True)

今後の発展:スコアボードオーバーレイを作る

 トップバーのオーバーレイが完成したので、次は「スコアボード配信オーバーレイ」を目指します。

 各プレイヤーのチャンピオン・KDA・アイテム・サモナースペル・ルーンや獲得ゴールドを表示するもので、視聴者にとって情報量が多く、一目で試合の理解を深めるのに役立つはずです。

おわり

 今回は、League of Legends の LiveClientData API を活用して、KDAやオブジェクト取得状況をリアルタイムに表示するトップバー型の配信オーバーレイを作成する方法を紹介しました。

 APIからのデータ取得、イベントの集計、HTML+JavaScriptによる描画まで、一見複雑に見えるかもしれませんが、仕組みを理解すれば意外とシンプルに構築できます。 何より、自分の配信スタイルに合わせて自由にデザインや機能をカスタマイズできるのが、自作オーバーレイの最大の魅力です。

 今後は、より情報量の多いスコアボード型のオーバーレイや、アニメーション・チームロゴ表示など、さらにリッチな演出にも挑戦していく予定です。 この記事が、あなたの配信を一歩進化させるきっかけになれば嬉しいです!

コメント