【AI開発】初心者でもできる！AIエージェントを使った簡単アプリ開発「自分だけの食べログ」

### はじめに
こういうアプリがほしいなーと思ったら、まずは聞いてみる。AIエージェントにね！
すでに調べものとかでAIエージェント（Microsoft Copilotとか）を利用してますが、個人開発でAIエージェントを使ってみたのでご紹介します。

#### 利用したAIエージェント
* **Microsoft Copolot**

https://copilot.microsoft.com/

* **Cline**

https://github.com/cline/cline

なお、自分でコードは一切書いていません！
**Microsoft Copilot**と**Cline**を両方使ってみての所感も最後の方に書いています。

### 「自分だけの食べログ」の概要

:::note warn
あくまでも個人開発のため、個人で利用できる範囲の制約や考え方となります。
:::

趣味が食べ歩きなので、食べログで色々調べて新しいお店を見つけては食べに行ってます。
色々食べ歩いていると、「あれっ、このお店行ったっけ？」とか、
友人から「この近くでおすすめの場所ない？」とかがあります。
そんなとき自分だけの食べログがあると便利だなー！と思い、作ってみました。

### プロンプト一覧
Microsoft Copilotに以下のプロンプトを与えました。
（もちろん他にもこまごまとした指示は与えてますが、ざっくりとは以下の感じです。）
　
:boy:「画面の**左側にお店の一覧**を表示し、画面の**右側に地図**を表示してください。」
:robot:「OK！**index.html**と**style.css**と**script.js**を作るね！」
　
:boy:「**お店の追加ボタン**を配置してください。お店の追加ボタンで**ポップアップ**を表示してください。」
:robot:「OK！ポップアップ画面に**登録ボタン**と**キャンセルボタン**も追加しとくね！」
　
:boy:「お店の追加ポップアップでは、**お店の名前**、**お店の住所**、**星の評価**、**メモ**を入力項目としてください。」
:robot:「OK！**星の評価は数値入力**にしとくね！**メモは複数行入力**できるようにしとくね！」
　
:boy:「お店の住所は**地図と連動**するようにしてください。」
:robot:「OK！Google Mapと連動するなら**Maps JavaScript APIが必要**だよ！」
　
:boy:「了解。**Maps JavaScript API**用意しますね。」

https://developers.google.com/maps/documentation/javascript/get-api-key?hl=ja
 
:boy:「**APIキーを直に書くのは嫌**だから別ファイルにしてください。」
:robot:「OK！**別ファイルを参照**するようにしたよ！」

:::note warn
**別ファイルにしただけではAPIキーの漏洩は防げません。セキュリティ上よくありません。**
今回はあくまでも個人での利用に限定することとしています。
:::
 
:boy:「お店の一覧に登録した**お店の情報を表示**してください。」
:robot:「OK！お店の一覧に**リスト形式**で表示するね！」
　
:boy:「お店の一覧で登録した情報は**修正**、**削除**できるようにしてください。」
:robot:「OK！**修正ボタン**と**削除ボタン**を追加しとくね！修正ボタンでお店の登録と同じポップアップを開くよ！」
　
:boy:「修正ボタンは**鉛筆マーク**、削除ボタンは**ゴミ箱マーク**にしてください。」
:robot:「OK！**アイコン**を使えるようにしとくよ！」
　
:boy:「お店の一覧に**フィルタ機能**を追加してください。」
:robot:「OK！**実装したよ！**」
:boy:「**いいね！**」
 
:boy:「**エクスポートボタン**を配置してください。エクスポートボタンでお店の情報を**csv形式で出力**してください。」
:robot:「OK！**実装したよ！**」
:boy:「**早っ！**」
　 
:boy:「**インポートボタン**を配置してください。インポートボタンで**csvファイルを読み込んで**お店の一覧情報を更新してください。」
:robot:「OK！**実装したよ！**」
:boy:「**えっ、もう！？**」
 
:boy:「お店の一覧をクリックしたら**地図にお店の場所をピンで表示**してください。」
:robot:「OK！**実装したよ！**」
:boy:「**すごっ。。。**」
 
:boy:「画面を**モダナイズ**してください。」（抽象的でごめんなさい。）
:robot:「OK！画面の**見た目を見やすく**、色は**オレンジと白を基調**に修正したよ！」
:boy:「**最高かよ。**」
 
:boy:「PCでもスマホでも見られるように**レスポンシブルデザイン**にしてください。」
:robot:「OK！**画面のサイズに合わせてボタンの大きさを変更**できるようにしたよ！」
:boy:「**完璧です。**」

---
:robot:（AIエージェント）さんが上記のように言ったかは定かではありませんが、
途中から :boy: （自分）は上記のようなリアクションをとってました。

### 完成しました
そして完成したのがこちら:point_down:
（後で作成したソースは全部載せます。）

index.htmlを開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288944/5ce5f9cf-6d3f-4812-9424-f28346592413.png)

「お店の登録」ボタンをクリックすると、お店の登録ポップアップ画面が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288944/f3cc867e-88b9-4d19-9597-f132039fb240.png)

住所入力でMaps JavaScript APIと連動して、Place Autocompleteが有効になっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288944/186cafa5-e5e6-46c8-b534-ccf8a7e39302.png)

お店情報を登録すると、お店一覧にお店の情報が表示されます。
お店の情報をクリックすると、地図と連動してお店の住所のところにピンを表示してくれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288944/393e4a7c-6609-4be7-a1ed-df9b64e88c75.png)

エクスポートボタンをクリックすると、csv形式でお店の情報を出力できます。
もちろん、インポートボタンでcsvファイルを指定してお店の一覧を表示してくれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288944/eedc8a1e-99a6-4954-92b5-aeb83510fa67.png)

画面の横幅を小さくするとこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288944/1e666ec3-4c7e-48e9-8b0f-99a77057bd14.png)

#### 工夫したところ

* 必要な機能のみ（お店の追加、修正、削除、地図表示）で**シンプル**に！
* **地図と連動！**　住所入力をラクに。お店の住所を地図で表示できるように。
* 登録データはエクスポートすることで**DBを使わない！**　すべてフロントエンドで完結。
* **見た目（UI）をモダナイズ**！
* PCとスマホでもみれるように**レスポンシブルデザイン**に！

#### ソースコード

<details><summary>index.html</summary>

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>my食べログ</title>
    <link rel="stylesheet" href="styles.css">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
    <script src="loadGoogleMaps.js"></script>
</head>
<body>
    <div class="container">
        <div class="sidebar">
            <h1>お店一覧</h1>
            <div class="action-container">
                <button id="add-store-btn">お店を登録</button>
                <button id="export-btn">エクスポート</button>
                <button id="import-btn">インポート</button>
            </div>
            <div class="filter-container">
                <input type="text" id="filter-input" placeholder="お店の名前を入力してください">
            </div>
            <ul id="store-list">
                <!-- 登録されたお店がここに表示されます -->
            </ul>
        </div>
        <div id="map" class="map">
            <h1>地図</h1>
            <div id="map-container"></div>
        </div>
    </div>

    <div id="popup" class="hidden">
        <div class="popup-content">
            <h2>お店を登録する</h2>
            <form id="store-form">
                <label for="store-name">お店の名前:</label>
                <input type="text" id="store-name" name="store-name" required>

                <label for="store-address">住所:</label>
                <input type="text" id="store-address" name="store-address" required>

                <label for="store-rating">評価:</label>
                <input type="number" id="store-rating" name="store-rating" min="1" max="5" required>

                <label for="store-notes">メモ:</label>
                <textarea id="store-notes" name="store-notes"></textarea>

                <button type="submit">登録</button>
                <button type="button" id="close-popup">キャンセル</button>
            </form>
        </div>
    </div>
    <script src="script.js"></script>
</body>
</html>

```
</details>


<details><summary>script.js</summary>

```js:script.js
document.addEventListener('DOMContentLoaded', () => {
    const mapContainer = document.getElementById('map-container');
    mapContainer.innerHTML = '地図を表示する場所 (マッピングライブラリを導入してください)';

    const addStoreBtn = document.getElementById('add-store-btn');
    const filterInput = document.getElementById('filter-input'); // フィルタ入力
    const exportBtn = document.getElementById('export-btn'); // エクスポートボタン
    const importBtn = document.getElementById('import-btn'); // インポートボタン
    const popup = document.getElementById('popup');
    const closePopupBtn = document.getElementById('close-popup');
    const form = document.getElementById('store-form');
    const storeList = document.getElementById('store-list');
    let storeData = []; // お店情報を格納する配列
    let editTarget = null; // 修正対象アイテムを追跡

    // 初期状態でポップアップを非表示に設定
    popup.classList.add('hidden');

    // エンターキーでポップアップが閉じないようにする
    form.addEventListener('keydown', (event) => {
        if (event.key === 'Enter') {
            event.preventDefault();
        }
    });

    // フィルタ入力のイベントリスナーを設定
    filterInput.addEventListener('input', () => {
        const filterValue = filterInput.value.toLowerCase(); // 小文字に変換して比較
        refreshStoreList(filterValue); // フィルタリングを適用
    });
    
    // 「お店を登録する」ボタンでポップアップを表示
    addStoreBtn.addEventListener('click', () => {
        popup.classList.remove('hidden');
        editTarget = null; // 新規登録モード
        form.reset(); // フォームをリセット
    });

    // 「キャンセル」ボタンでポップアップを閉じる
    closePopupBtn.addEventListener('click', () => {
        popup.classList.add('hidden');
    });

    // フォーム送信処理
    form.addEventListener('submit', (event) => {
        event.preventDefault(); // ページリロードを防ぐ

        const name = document.getElementById('store-name').value;
        const address = document.getElementById('store-address').value; // 住所を取得
        const rating = parseInt(document.getElementById('store-rating').value);
        const notes = document.getElementById('store-notes').value;
        const storeEntry = { name, address, rating, notes };

        if (editTarget) {
            const index = Array.from(storeList.children).indexOf(editTarget);
            if (index >= 0) {
                storeData[index] = storeEntry; // 配列内の該当項目を更新
            }
            editTarget.querySelector('.store-name').textContent = name;
            editTarget.querySelector('.store-details').innerHTML = `
                <span class="star-rating">${'★'.repeat(rating)}${'☆'.repeat(5 - rating)}</span> | メモ: ${notes}
            `;
            editTarget.querySelector('.store-address').textContent = `住所: ${address}`; // 住所を更新
        } else {
            storeData.push(storeEntry); // 新規データを追加
            refreshStoreList(); // リストを更新
        }

        form.reset();
        popup.classList.add('hidden'); // ポップアップを閉じる
    });

    // 一覧をリフレッシュしてすべてのリストアイテムを再描画
    const refreshStoreList = (filterValue = '') => {
        storeList.innerHTML = ''; // 既存のリストをクリア
        storeData
            .filter(store => store.name.toLowerCase().includes(filterValue)) // フィルタリング処理
            .forEach(store => {
            const newStore = document.createElement('li');
            const storeTop = document.createElement('div');
            storeTop.classList.add('store-top');

            const storeName = document.createElement('p');
            storeName.classList.add('store-name');
            storeName.textContent = store.name;

            const actionButtons = document.createElement('div');
            actionButtons.classList.add('action-buttons');

            const editButton = document.createElement('button');
            editButton.classList.add('edit-btn');
            //editButton.textContent = '修正';
            editButton.innerHTML = '<i class="fas fa-pencil-alt"></i>'; // 鉛筆マーク
            editButton.addEventListener('click', () => {
                editTarget = newStore;
                popup.classList.remove('hidden');
                const index = Array.from(storeList.children).indexOf(newStore);
                const storeEntry = storeData[index]; // 修正対象のデータを取得
                document.getElementById('store-name').value = storeEntry.name;
                document.getElementById('store-address').value = storeEntry.address;
                document.getElementById('store-rating').value = storeEntry.rating;
                document.getElementById('store-notes').value = storeEntry.notes;
            });

            const deleteButton = document.createElement('button');
            deleteButton.classList.add('delete-btn');
            //deleteButton.textContent = '削除';
            deleteButton.innerHTML = '<i class="fas fa-trash-alt"></i>'; // ゴミ箱マーク
            deleteButton.addEventListener('click', () => {
                storeList.removeChild(newStore);
                storeData = storeData.filter(item => item !== store); // データから削除
                refreshStoreList(); // 再描画
            });

            actionButtons.appendChild(editButton);
            actionButtons.appendChild(deleteButton);

            storeTop.appendChild(storeName);
            storeTop.appendChild(actionButtons);

            const storeDetails = document.createElement('p');
            storeDetails.classList.add('store-details');
            storeDetails.innerHTML = `
                <span class="star-rating">${'★'.repeat(store.rating)}${'☆'.repeat(5 - store.rating)}</span> | メモ: ${store.notes}
            `;

            const storeAddress = document.createElement('p');
            storeAddress.classList.add('store-address');
            storeAddress.textContent = `住所: ${store.address}`; // 住所を表示

            newStore.appendChild(storeTop);
            newStore.appendChild(storeDetails);
            newStore.appendChild(storeAddress);

            // リストアイテムにクリックイベントを追加
            newStore.addEventListener('click', () => {
                const index = Array.from(storeList.children).indexOf(newStore);
                const storeEntry = storeData[index]; // 最新のデータを取得
                showLocationOnMap(storeEntry.address); // 最新の住所で地図を表示
            });
            storeList.appendChild(newStore);
        });
    };

    // エクスポート機能
    exportBtn.addEventListener('click', () => {
        if (storeData.length === 0) {
            alert("エクスポートするデータがありません。");
            return;
        }

        const csvContent = storeData.map(store =>
            `${store.name},${store.address},${store.rating},${store.notes}`
        ).join("\n");
        const blob = new Blob([csvContent], { type: "text/csv" });
        const url = URL.createObjectURL(blob);

        const a = document.createElement("a");
        a.href = url;
        a.download = "store_data.csv";
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url); // URLを解放
    });

    // インポート機能
    importBtn.addEventListener('click', () => {
        const fileInput = document.createElement('input');
        fileInput.type = 'file';
        fileInput.accept = '.csv';
        fileInput.style.display = 'none';

        fileInput.addEventListener('change', (event) => {
            const file = event.target.files[0];
            if (file) {
                const reader = new FileReader();
                reader.onload = (e) => {
                    const rows = e.target.result.split("\n");
                    storeData = []; // 既存のデータをクリア
                    rows.forEach(row => {
                        const [name, address, rating, notes] = row.split(",");
                        const storeEntry = { name, address, rating: parseInt(rating), notes };
                        storeData.push(storeEntry);
                    });
                    refreshStoreList(); // 一覧をリフレッシュ
                };
                reader.readAsText(file);
            }
        });

        // ファイル選択ダイアログを開く
        document.body.appendChild(fileInput);
        fileInput.click();
        document.body.removeChild(fileInput);
    });
});

// Google Maps 初期化コード
let map;
let marker;

function initMap() {
    map = new google.maps.Map(document.getElementById("map"), {
        center: { lat: 35.6895, lng: 139.6917 },
        zoom: 13,
    });

    const input = document.getElementById("store-address");
    const autocomplete = new google.maps.places.Autocomplete(input);

    autocomplete.addListener("place_changed", () => {
        const place = autocomplete.getPlace();
        if (!place.geometry || !place.geometry.location) {
            alert("有効な場所を選択してください");
            return;
        }

        map.setCenter(place.geometry.location);
        map.setZoom(15);

        if (marker) {
            marker.setMap(null);
        }
        marker = new google.maps.Marker({
            position: place.geometry.location,
            map: map,
        });
    });
}

// 地図に住所の場所を表示する関数
function showLocationOnMap(address) {
    const geocoder = new google.maps.Geocoder(); // Geocodingサービスを利用
    geocoder.geocode({ address: address }, (results, status) => {
        if (status === 'OK' && results[0]) {
            const location = results[0].geometry.location;
            map.setCenter(location); // 地図の中心を設定
            map.setZoom(15);

            if (marker) {
                marker.setMap(null); // 既存のマーカーを削除
            }
            marker = new google.maps.Marker({
                position: location,
                map: map,
            });
        } else {
            alert("住所が見つかりませんでした: " + status);
        }
    });
}
```
</details>

<details><summary>style.css</summary>

```css:styles.css
/* 全体の基本スタイル */
body {
    margin: 0;
    font-family: 'Roboto', sans-serif;
    background-color: #ffffff;
    color: #333;
}

#popup.hidden {
    display: none;
}

.container {
    display: flex;
    flex-direction: column; /* デフォルトでは縦並びに変更 */
    height: 100vh;
}

.sidebar {
    width: 100%; /* デフォルトで幅を100%に変更 */
    background-color: #ffffff;
    padding: 20px;
    border-bottom: 1px solid #f0ad4e; /* 横線を追加 */
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

.map {
    flex: 1;
    padding: 20px;
    background-color: #ffffff;
}

/* サイドバーの中身 */
.sidebar h1 {
    font-size: 1.5em;
    margin: 0;
    color: #f97316;
    text-align: center; /* 中央寄せに変更 */
}

.action-container {
    display: flex;
    flex-wrap: wrap; /* スマホ対応の折り返し */
    gap: 10px;
    justify-content: center; /* ボタンを中央揃え */
}

.action-container button {
    padding: 10px;
    font-size: 0.9em;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    background-color: #f97316;
    color: #ffffff;
    transition: background-color 0.3s;
    flex: 1; /* ボタンが均等に並ぶよう調整 */
}

.action-container button:hover {
    background-color: #e66a12;
}

.filter-container {
    margin: 20px auto;
    width: 90%;
    text-align: center; /* フィルタを中央揃え */
}

.filter-container input {
    width: 100%;
    padding: 10px;
    font-size: 1em;
    border: 1px solid #f0ad4e;
    border-radius: 5px;
    outline: none;
}

.filter-container input:focus {
    border-color: #f97316;
    box-shadow: 0 0 5px rgba(249, 115, 22, 0.5);
}

.sidebar ul {
    list-style: none;
    padding: 0;
    margin: 0;
}

.sidebar ul li {
    background-color: #fcf8f2;
    margin-bottom: 10px;
    padding: 10px;
    border-radius: 5px;
    border: 1px solid #f0ad4e;
    transition: transform 0.3s, box-shadow 0.3s;
}

.sidebar ul li:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

.store-top {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.store-name {
    font-weight: bold;
    font-size: 1.2em;
    margin: 0;
    color: #333;
}

.store-details {
    font-size: 0.9em;
    color: #999;
    margin: 5px 0 0;
}

.star-rating {
    color: #999;
    font-size: 1em;
    margin: 0;
}

.store-address {
    font-size: 0.9em;
    color: #777;
    margin: 5px 0 0;
}

#map-container {
    width: 100%;
    height: calc(100% - 50px);
    background-color: #fcf8f2;
    border-radius: 10px;
    border: 2px solid #f0ad4e;
}

/* ポップアップスタイル */
#popup {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.6);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 10;
}

.popup-content {
    background-color: #ffffff;
    padding: 30px;
    border-radius: 10px;
    width: 400px;
    box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
    animation: fadeIn 0.3s ease-in-out;
}

.popup-content h2 {
    margin-top: 0;
    color: #f97316;
}

.popup-content form {
    display: flex;
    flex-direction: column;
    gap: 15px;
}

.popup-content form label {
    font-weight: bold;
    color: #555;
}

.popup-content form input,
.popup-content form textarea {
    padding: 10px;
    font-size: 1em;
    border: 1px solid #f0ad4e;
    border-radius: 5px;
    background-color: #ffffff;
    outline: none;
}

.popup-content form input:focus,
.popup-content form textarea:focus {
    border-color: #f97316;
    box-shadow: 0 0 5px rgba(249, 115, 22, 0.5);
}

.popup-content button {
    padding: 10px;
    font-size: 1em;
    color: #ffffff;
    background-color: #f97316;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s;
}

.popup-content button:hover {
    background-color: #e66a12;
}

@keyframes fadeIn {
    from {
        opacity: 0;
        transform: scale(0.9);
    }
    to {
        opacity: 1;
        transform: scale(1);
    }
}

/* メディアクエリでレスポンシブデザインを調整 */
@media (min-width: 768px) {
    .container {
        flex-direction: row;
    }

    .sidebar {
        width: 30%;
        border-bottom: none;
        border-right: 1px solid #f0ad4e;
    }

    .map {
        flex: 1;
    }
}

@media (min-width: 1200px) {
    .sidebar h1 {
        font-size: 1.8em;
    }

    .action-container button {
        font-size: 1em;
    }

    .filter-container input {
        font-size: 1.1em;
    }
}

.action-buttons button {
    padding: 5px;
    font-size: 1.2em; /* アイコンサイズを調整 */
    border: none;
    border-radius: 5px;
    cursor: pointer;
    background-color: transparent; /* 背景色を透明に */
    color: #f97316; /* アイコンカラー */
    transition: color 0.3s;
}

.action-buttons button:hover {
    color: #e66a12; /* ホバー時の色を変更 */
}
```
</details>


<details><summary>loadGoogleMaps.js</summary>

```js:loadGoogleMaps.js
// Google Maps APIキーを設定する
const apiKey = 'YOUR-API-KEY'; // ご自身のAPIキーを指定してください。

// Google Mapsのスクリプトを動的に読み込む
function loadGoogleMaps() {
    const script = document.createElement('script');
    script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places&callback=initMap`;
    script.async = true;
    document.head.appendChild(script);
}

// スクリプトを読み込む関数を実行
loadGoogleMaps();
```
</details>

:::note info
「loadGoogleMaps.js」ではご自身のAPIキーを指定してください。
```js:loadGoogleMaps.js
const apiKey = 'YOUR-API-KEY'; // ご自身のAPIキーを指定してください。
```
:::

### AIエージェントを利用してみて

* アプリ開発にかかった時間は**1日間程度**です。
* Microsoft Copilotのため**無料**です。
* Maps JavaScript APIも**無料**の範囲内での実装です。
　
* **指示はなるべく細かく伝えた方が修正の範囲も限定できて良い**。
* ときどき指示した内容で修正しようと元のファイルを大幅に書き換えることがあるので、都度都度**自分のタイミングでバックアップを取っておいたほうが良い**。
　 
* 特に**見た目（UI）をモダナイズしたいときはとても活躍してくれます！**
**これが一番のGood Point！**:raised_hands:

#### **Cline**で開発した場合

* 完成したのがこちら:point_down:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288944/ae3fdde4-84a4-4e6c-b8ce-837e043752e2.png)

　なお、API Providerは「Google Gemini」を利用しました。
　VSCodeの拡張で「Cline」をインストールしてAPI Providerを設定します。
 
　![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288944/9363490e-cd81-412a-b2b5-b28508b9a036.png)


* Clineでの開発はAPIのトークン数とにらめっこしながら修正。
でもシンプルなアプリ開発なら**無料**の範囲内で実装できる。
* Clineもときどき元のファイルを大幅に書き換えることがあるので都度都度**自分のタイミングでバックアップを取っておいた方が負い**。
* Clineは**一度の指示ですべてのファイルを書き換えてくれる**。
* Clineは**エラーや不整合があると自ら再度修正を実行してくれる**。
でもそのおかげで無限ループに入るときがある。（その時は新しいタスクでやり直す。）

#### Microsoft CopilotとClineの比較

| | Microsoft Copilot | Cline |
|:-|:-|:-|
| **Good**なところ  | 料金を気にせず利用できる | 一度に複数ファイルを修正してくれる |
| **Not Good**なところ  | トークン数の上限ですべて出力されないことがある | ちまちました修正のためにトークンが消費される |

なお、**Cline**はMCPサーバを利用するなど外部APIと連携できるため、使い方によってはもっと恩恵を受けることができると思います！

### おわりに
* これからも面白そうなアプリを思いついたら、AIエージェントと仲良く開発をしていく。
* ハッピーなAIエージェントライフを！