10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

(非エンジニア向け)「AI」にどうスクリプトを作らせているか「Backlogの課題一括削除スクリプト」

10
Last updated at Posted at 2025-08-26

非エンジニアの方もスクリプト書いてほしい

あちこちでAIこすってるキャラを演じさせていただいておりまして、「AIって実務でどう使ってるんですか?」という質問を受けます、ありがたいことに。どちらかというと非エンジニアの方からですかね。
個人的にはツールとして使いこなすの一つに、もう「スクリプトを書かせてルーティンをさせる」があるなと思います。ちょっとした実例があったので、先日つくったBacklog課題一括削除をとりあげました。

大したやり取りもしてないし、スクリプトも生のまま張り付けてあるので参考になると嬉しいなーと思います。

前提

スクリプト書かせるのはやっぱり「ChatGPT5」「Claude」「Google AIStudio」のいずれかが安心だなーと思ています。Geminiのチャットモードがなんか精度イマイチなことがおおい気持ちなので、その前提でいることをご認識いただけたらと思います。

何をやったか

「テスト用に作った課題が300件以上あって、全部消したいんだけど...」

画面でポチポチ削除?いや、それはさすがに人生の時間がもったいない。

AIは魔法の杖じゃないけども

いうてもAIって「完璧なコードを一発で生成してくれる魔法のツール」ではないですが、「雑なお願いを形にしてくれる相棒」としては十分でして。

特にこういう「めんどくさい系タスク」こそ、AIの真価が発揮される場面だと思うんです。

実際どうやったか

最初の雑な指示

backlog api の仕様を元に、指定したプロジェクトの課題一覧を指定の条件取得→スプレッドシートに一覧記載→一覧上に記載のある課題を一括削除するGASスクリプトを作成してください。

https://developer.nulab.com/ja/docs/backlog/api/2/get-issue-list/
https://developer.nulab.com/ja/docs/backlog/api/2/delete-issue/

これで返ってきたコードを実行すると...ちょっとエラー。APIのレスポンスが想定と違ったみたい。

エラーメッセージをそのまま投げる

ここがポイントなんですが、エラーメッセージをそのままClaudeに投げます。

このエラーが出た:
TypeError: Cannot read property 'id' of undefined

あと、レスポンスの中身はこんな感じだった:
[長いので割愛]

追加の要望も雑に伝える

削除する前に課題のタイトル一覧を表示して、
本当に削除するか確認したい。
あと進捗状況も見たい。「100件中30件削除済み」のように。

そんなこんなで5分後

私自身、Backlog APIは詳しくないんですが、3往復くらいのやり取りで完璧に動くスクリプトができます。

最終的にできたのがこんな感じ:

/**
 * Backlog課題一括削除スクリプト
 * 1. 指定条件で課題一覧を取得
 * 2. スプレッドシートに一覧を記載
 * 3. 記載された課題を一括削除
 */

// ========================
// 設定値(環境に合わせて変更してください)
// ========================
const CONFIG = {
  // BacklogのスペースID(例: 'yourspace' → https://yourspace.backlog.jp)
  SPACE_ID: 'YOUR_SPACE_ID',
  
  // Backlog APIキー(プロジェクト設定から取得)
  API_KEY: 'YOUR_API_KEY',
  
  // ドメイン(.backlog.jp, .backlog.com, .backlogtool.comのいずれか)
  DOMAIN: '.backlog.jp',
  
  // スプレッドシートID(記録用のスプレッドシート)
  SPREADSHEET_ID: 'YOUR_SPREADSHEET_ID',
  
  // シート名
  SHEET_NAME: '削除対象課題一覧'
};

// ========================
// メイン処理
// ========================

/**
 * メイン実行関数
 * 課題の取得→記録→削除を実行
 */
function main() {
  try {
    console.log('処理を開始します...');
    
    // 1. 課題一覧を取得
    const searchParams = getSearchParameters();
    const issues = fetchIssues(searchParams);
    
    if (issues.length === 0) {
      console.log('該当する課題がありません');
      return;
    }
    
    console.log(`${issues.length}件の課題を取得しました`);
    
    // 2. スプレッドシートに記載
    writeToSpreadsheet(issues);
    console.log('スプレッドシートへの記載が完了しました');
    
    // 3. 削除確認
    const shouldDelete = confirmDeletion(issues.length);
    
    if (shouldDelete) {
      // 4. 一括削除実行
      const results = bulkDeleteIssues(issues);
      
      // 5. 削除結果をスプレッドシートに記録
      updateDeletionStatus(results);
      
      console.log('削除処理が完了しました');
      showSummary(results);
    } else {
      console.log('削除処理をキャンセルしました');
    }
    
  } catch (error) {
    console.error('エラーが発生しました:', error);
    throw error;
  }
}

/**
 * 検索パラメータを設定
 * ※必要に応じてカスタマイズしてください
 */
function getSearchParameters() {
  return {
    // プロジェクトID(複数指定可)
    projectId: [123], // ← 対象プロジェクトのIDを指定
    
    // 状態ID(例: 1=未対応, 2=処理中, 3=処理済み, 4=完了)
    // statusId: [4],
    
    // 期限日の期間(例: 3ヶ月前より古い課題)
    // dueDateUntil: Utilities.formatDate(new Date(Date.now() - 90 * 24 * 60 * 60 * 1000), 'JST', 'yyyy-MM-dd'),
    
    // 更新日の期間(例: 6ヶ月以上更新されていない課題)
    // updatedUntil: Utilities.formatDate(new Date(Date.now() - 180 * 24 * 60 * 60 * 1000), 'JST', 'yyyy-MM-dd'),
    
    // 取得件数
    count: 100,
    
    // ソート順
    sort: 'created',
    order: 'asc'
  };
}

// ========================
// API通信関数
// ========================

/**
 * 課題一覧を取得
 */
function fetchIssues(params) {
  const baseUrl = `https://${CONFIG.SPACE_ID}${CONFIG.DOMAIN}/api/v2/issues`;
  const allIssues = [];
  let offset = 0;
  const limit = params.count || 100;
  
  while (true) {
    // URLパラメータを構築
    const urlParams = buildUrlParams({
      ...params,
      apiKey: CONFIG.API_KEY,
      offset: offset,
      count: limit
    });
    
    const url = `${baseUrl}?${urlParams}`;
    
    try {
      const response = UrlFetchApp.fetch(url, {
        method: 'get',
        muteHttpExceptions: true,
        headers: {
          'Content-Type': 'application/json'
        }
      });
      
      if (response.getResponseCode() !== 200) {
        throw new Error(`APIエラー: ${response.getResponseCode()} - ${response.getContentText()}`);
      }
      
      const issues = JSON.parse(response.getContentText());
      
      if (issues.length === 0) {
        break;
      }
      
      allIssues.push(...issues);
      
      // 取得件数が上限未満の場合は終了
      if (issues.length < limit) {
        break;
      }
      
      offset += limit;
      
      // API制限対策のため少し待機
      Utilities.sleep(500);
      
    } catch (error) {
      console.error('課題取得エラー:', error);
      throw error;
    }
  }
  
  return allIssues;
}

/**
 * 課題を削除
 */
function deleteIssue(issueKey) {
  const url = `https://${CONFIG.SPACE_ID}${CONFIG.DOMAIN}/api/v2/issues/${issueKey}?apiKey=${CONFIG.API_KEY}`;
  
  try {
    const response = UrlFetchApp.fetch(url, {
      method: 'delete',
      muteHttpExceptions: true,
      headers: {
        'Content-Type': 'application/json'
      }
    });
    
    if (response.getResponseCode() === 200) {
      return {
        success: true,
        issueKey: issueKey,
        message: '削除成功'
      };
    } else {
      return {
        success: false,
        issueKey: issueKey,
        message: `エラー: ${response.getResponseCode()} - ${response.getContentText()}`
      };
    }
    
  } catch (error) {
    return {
      success: false,
      issueKey: issueKey,
      message: `エラー: ${error.toString()}`
    };
  }
}

/**
 * 課題を一括削除
 */
function bulkDeleteIssues(issues) {
  const results = [];
  const total = issues.length;
  
  issues.forEach((issue, index) => {
    console.log(`削除中... (${index + 1}/${total}) ${issue.issueKey}`);
    
    const result = deleteIssue(issue.issueKey);
    results.push(result);
    
    // API制限対策のため少し待機
    Utilities.sleep(1000);
  });
  
  return results;
}

// ========================
// スプレッドシート操作関数
// ========================

/**
 * スプレッドシートに課題一覧を記載
 */
function writeToSpreadsheet(issues) {
  const spreadsheet = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
  let sheet = spreadsheet.getSheetByName(CONFIG.SHEET_NAME);
  
  // シートが存在しない場合は作成
  if (!sheet) {
    sheet = spreadsheet.insertSheet(CONFIG.SHEET_NAME);
  }
  
  // 既存データをクリア
  sheet.clear();
  
  // ヘッダー行を設定
  const headers = [
    '削除状態',
    '課題キー',
    'ID',
    'タイトル',
    '種別',
    '状態',
    '優先度',
    '担当者',
    '作成日',
    '更新日',
    '期限日',
    'エラーメッセージ'
  ];
  
  const headerRange = sheet.getRange(1, 1, 1, headers.length);
  headerRange.setValues([headers]);
  headerRange.setBackground('#4285F4');
  headerRange.setFontColor('#FFFFFF');
  headerRange.setFontWeight('bold');
  
  // データ行を設定
  if (issues.length > 0) {
    const data = issues.map(issue => [
      '未削除',  // 削除状態
      issue.issueKey,
      issue.id,
      issue.summary,
      issue.issueType ? issue.issueType.name : '',
      issue.status ? issue.status.name : '',
      issue.priority ? issue.priority.name : '',
      issue.assignee ? issue.assignee.name : '',
      issue.created,
      issue.updated,
      issue.dueDate || '',
      ''  // エラーメッセージ
    ]);
    
    sheet.getRange(2, 1, data.length, headers.length).setValues(data);
  }
  
  // 列幅を自動調整
  sheet.autoResizeColumns(1, headers.length);
  
  // 実行時刻を記録
  sheet.getRange(issues.length + 3, 1).setValue('取得日時:');
  sheet.getRange(issues.length + 3, 2).setValue(new Date());
}

/**
 * 削除結果をスプレッドシートに更新
 */
function updateDeletionStatus(results) {
  const spreadsheet = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
  const sheet = spreadsheet.getSheetByName(CONFIG.SHEET_NAME);
  
  if (!sheet) {
    console.error('シートが見つかりません');
    return;
  }
  
  // 各削除結果を更新
  results.forEach((result, index) => {
    const rowIndex = index + 2; // ヘッダー行を考慮
    
    if (result.success) {
      sheet.getRange(rowIndex, 1).setValue('削除済み');
      sheet.getRange(rowIndex, 1).setBackground('#34A853');
      sheet.getRange(rowIndex, 1).setFontColor('#FFFFFF');
    } else {
      sheet.getRange(rowIndex, 1).setValue('削除失敗');
      sheet.getRange(rowIndex, 1).setBackground('#EA4335');
      sheet.getRange(rowIndex, 1).setFontColor('#FFFFFF');
      sheet.getRange(rowIndex, 12).setValue(result.message);
    }
  });
  
  // 削除実行日時を記録
  const lastRow = sheet.getLastRow();
  sheet.getRange(lastRow + 2, 1).setValue('削除実行日時:');
  sheet.getRange(lastRow + 2, 2).setValue(new Date());
}

// ========================
// ユーティリティ関数
// ========================

/**
 * URLパラメータを構築
 */
function buildUrlParams(params) {
  const paramArray = [];
  
  for (const key in params) {
    const value = params[key];
    
    if (value === undefined || value === null) {
      continue;
    }
    
    if (Array.isArray(value)) {
      value.forEach(v => {
        paramArray.push(`${key}[]=${encodeURIComponent(v)}`);
      });
    } else {
      paramArray.push(`${key}=${encodeURIComponent(value)}`);
    }
  }
  
  return paramArray.join('&');
}

/**
 * 削除確認ダイアログ
 */
function confirmDeletion(count) {
  const ui = SpreadsheetApp.getUi();
  const response = ui.alert(
    '削除確認',
    `${count}件の課題を削除します。\n本当に削除しますか?\n\n※この操作は取り消せません`,
    ui.ButtonSet.OK_CANCEL
  );
  
  return response === ui.Button.OK;
}

/**
 * 処理結果サマリーを表示
 */
function showSummary(results) {
  const successCount = results.filter(r => r.success).length;
  const failCount = results.filter(r => !r.success).length;
  
  const ui = SpreadsheetApp.getUi();
  ui.alert(
    '処理完了',
    `削除処理が完了しました。\n\n成功: ${successCount}件\n失敗: ${failCount}件\n\n詳細はスプレッドシートをご確認ください。`,
    ui.ButtonSet.OK
  );
}

// ========================
// テスト用関数
// ========================

/**
 * 課題取得のみをテスト
 */
function testFetchIssues() {
  const params = getSearchParameters();
  const issues = fetchIssues(params);
  console.log(`取得件数: ${issues.length}`);
  
  if (issues.length > 0) {
    console.log('最初の課題:', issues[0]);
  }
}

/**
 * スプレッドシート書き込みのみをテスト
 */
function testWriteToSpreadsheet() {
  const params = getSearchParameters();
  const issues = fetchIssues(params);
  writeToSpreadsheet(issues);
  console.log('スプレッドシートへの書き込みが完了しました');
}

で、何が言いたいかというと

プロンプトエンジニアリングとか難しいこと考えなくていいんですよ。
むしろ:

  • 「こんな感じで動いてほしい」を雑に伝える
  • エラーが出たらそのまま投げる
  • 「あ、これも欲しい」と思ったら追加で頼む

このトライアンドエラーのサイクルを高速で回す方が、実務では圧倒的に効率的。

長期的に見ても価値がある

「AIに依存しすぎじゃない?」という声も聞こえてきそうですが、私はむしろ逆だと思ってて。

めんどくさい作業から解放されることで、本来やるべき創造的な仕事に時間を使えるようになる。
その辺で脳みそを鍛えようという寸法です。

まとめ:カジュアルに使い倒せ

AIツールって、もっとカジュアルに使っていいと思うんです。特に非エンジニアの方とか。

「いい感じにできない?」くらいの気持ちで投げて、返ってきたものを見て「あ、ここ違う」って修正依頼する。この繰り返しで、なんだかんだつくれるのが現代のすばらしさ。

遠慮せず、遠い存在だと思わず、日々の「めんどくさい」をAIに丸投げして、解決しましょう。

おわり。

いえらぶでは一緒に最速でサービス開発する仲間を募集しています

AIを活用しまくっているのは、つまるところ業界に対して1つでも多く・早くプロダクトを提供するのが目的です。共感してくれる方を広く募集しています。

カジュアル面談しましょう

DMいつでもお待ちしております。採用拘わらず、AIについてでもマネジメントについてでも、何でもお話ししたいです。

新卒採用サイト

大学生向けインターンシップ「いえらぶAIブートキャンプ」募集ページ

10
4
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
10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?