gasファイルの「分析.js」を以下に差し替えてください。
// ==================== 投稿一覧取得(追記式・自動実行用)【引用偏差値対応版】 ====================
function fetchThreadsListAuto() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const settingsSheet = ss.getSheetByName(CONFIG.SHEET_NAME_SETTINGS);
// アクセストークンを取得
const accessToken = settingsSheet.getRange(CONFIG.CELL.ACCESS_TOKEN).getValue();
if (!accessToken) {
console.log('❌ アクセストークンが設定されていません');
return;
}
try {
console.log('🔥 投稿一覧の自動取得開始');
// バックエンド経由で投稿一覧を取得
const threads = fetchThreadsListViaBackend(accessToken, 25);
const data = { data: threads };
// 投稿一覧シートを取得または作成
let threadsListSheet = ss.getSheetByName(CONFIG.SHEET_NAME_THREADS_LIST);
if (!threadsListSheet) {
threadsListSheet = ss.insertSheet(CONFIG.SHEET_NAME_THREADS_LIST);
// ヘッダー行を作成(16列に変更)
threadsListSheet.getRange(1, 1, 1, 16).setValues([
['投稿ID', '投稿日時', '投稿内容', '投稿URL', '閲覧数', 'いいね数', '返信数', 'リポスト数', '引用数', '閲覧偏差値', 'いいね偏差値', '返信偏差値', 'リポスト偏差値', '引用偏差値', '総合偏差値', '好反応フラグ']
]);
threadsListSheet.getRange(1, 1, 1, 16).setFontWeight('bold').setBackground('#4a86e8').setFontColor('#ffffff');
}
// 既存の投稿ID一覧を取得
const existingIds = new Set();
const lastRow = threadsListSheet.getLastRow();
if (lastRow > 1) {
const existingData = threadsListSheet.getRange(2, 1, lastRow - 1, 1).getValues();
existingData.forEach(row => {
if (row[0]) {
// 文字列に変換し、先頭のシングルクォートを除去
const idStr = String(row[0]).replace(/^'/, '').trim();
existingIds.add(idStr);
}
});
}
console.log(`既存投稿数: ${existingIds.size}件`);
// 新しい投稿のみをフィルタリング
const newPosts = [];
if (data.data && data.data.length > 0) {
data.data.forEach(thread => {
// IDを文字列化してチェック
const threadIdStr = String(thread.id);
if (!existingIds.has(threadIdStr)) {
const timestamp = thread.timestamp ? new Date(thread.timestamp) : '';
newPosts.push([
"'" + threadIdStr, // A: 投稿ID (文字列として強制)
timestamp ? Utilities.formatDate(timestamp, 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss') : '', // B: 投稿日時
thread.text || '', // C: 投稿内容
thread.permalink || '', // D: 投稿URL
'', // E: 閲覧数(後で取得)
'', // F: いいね数(後で取得)
'', // G: 返信数(後で取得)
'', // H: リポスト数(後で取得)
'', // I: 引用数(後で取得)
'', // J: 閲覧偏差値(後で計算)
'', // K: いいね偏差値(後で計算)
'', // L: 返信偏差値(後で計算)
'', // M: リポスト偏差値(後で計算)
'', // N: 引用偏差値(後で計算)← 新規追加!
'', // O: 総合偏差値(後で計算)
'' // P: 好反応フラグ(後で判定)
]);
}
});
}
// 🔥 重要:古い順に並び替え(reverse)
newPosts.reverse();
// 新しい投稿を追記
if (newPosts.length > 0) {
const nextRow = threadsListSheet.getLastRow() + 1;
// ID列(A列)を書式なしテキストに設定
threadsListSheet.getRange(nextRow, 1, newPosts.length, 1).setNumberFormat('@');
threadsListSheet.getRange(nextRow, 1, newPosts.length, 16).setValues(newPosts);
console.log(`✅ ${newPosts.length}件の新規投稿を追加しました`);
} else {
console.log('✅ 新規投稿なし');
}
} catch (error) {
console.log('❌ 投稿一覧取得失敗: ' + error.message);
showError('投稿一覧取得エラー:\n\n' + error.message);
throw error; // 上位関数にエラーを伝播
}
}
// ==================== メトリクス自動取得(設定時間経過後) ====================
function fetchMetricsAuto() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const settingsSheet = ss.getSheetByName(CONFIG.SHEET_NAME_SETTINGS);
const threadsListSheet = ss.getSheetByName(CONFIG.SHEET_NAME_THREADS_LIST);
// アクセストークンを取得
const accessToken = settingsSheet.getRange(CONFIG.CELL.ACCESS_TOKEN).getValue();
if (!accessToken) {
console.log('❌ アクセストークンが設定されていません');
return { success: false, error: 'NO_TOKEN' };
}
if (!threadsListSheet) {
console.log('❌ 投稿履歴シートが見つかりません');
return { success: false, error: 'NO_SHEET' };
}
try {
console.log('🔥 メトリクス自動取得開始');
// メトリクス取得条件(時間)を取得(デフォルト6時間)
const metricsDelayHours = Number(settingsSheet.getRange(CONFIG.CELL.METRICS_CONDITION).getValue()) || 6;
console.log(`メトリクス取得条件: 投稿後${metricsDelayHours}時間経過`);
const lastRow = threadsListSheet.getLastRow();
if (lastRow < 2) {
console.log('投稿データがありません');
return { success: true, updatedCount: 0 };
}
// 全データを取得
const data = threadsListSheet.getRange(2, 1, lastRow - 1, 15).getValues();
const now = new Date();
let updatedCount = 0;
let permissionErrorCount = 0; // 権限エラーカウント
for (let i = 0; i < data.length; i++) {
const row = data[i];
const postId = row[0]; // A列: 投稿ID
const postDateStr = row[1]; // B列: 投稿日時
const viewsValue = row[4]; // E列: 閲覧数
// スキップ条件:投稿IDがない、または既にメトリクス取得済み
if (!postId || viewsValue !== '') {
continue;
}
// 投稿日時をパース
let postDate;
try {
postDate = new Date(postDateStr);
} catch (error) {
console.log(`行${i + 2}: 日付パースエラー - ${error.message}`);
continue;
}
// 設定時間経過チェック
const hoursPassed = (now - postDate) / (1000 * 60 * 60);
if (hoursPassed < metricsDelayHours) {
console.log(`行${i + 2}: まだ${metricsDelayHours}時間経過していません (${hoursPassed.toFixed(1)}時間)`);
continue;
}
// メトリクスを取得
console.log(`行${i + 2}: メトリクス取得開始 - 投稿ID: ${postId}`);
try {
const metrics = fetchInsights(postId, accessToken);
if (metrics) {
// E〜I列にメトリクスを記録
const rowIndex = i + 2;
threadsListSheet.getRange(rowIndex, 5).setValue(metrics.views || 0); // E列: 閲覧数
threadsListSheet.getRange(rowIndex, 6).setValue(metrics.likes || 0); // F列: いいね数
threadsListSheet.getRange(rowIndex, 7).setValue(metrics.replies || 0); // G列: 返信数
threadsListSheet.getRange(rowIndex, 8).setValue(metrics.reposts || 0); // H列: リポスト数
threadsListSheet.getRange(rowIndex, 9).setValue(metrics.quotes || 0); // I列: 引用数
updatedCount++;
console.log(`✅ 行${rowIndex}: メトリクス記録完了`);
// API制限対策:少し待機
Utilities.sleep(500);
}
} catch (error) {
console.log(`❌ 行${i + 2}: メトリクス取得エラー - ${error.message}`);
const rowIndex = i + 2;
// 投稿が存在しない場合(削除済み等)は「取得不可」と記録してスキップ
if (error.message.includes('does not exist')) {
console.log(`⏭️ 行${rowIndex}: 投稿が存在しないため「取得不可」を記録`);
threadsListSheet.getRange(rowIndex, 5).setValue('取得不可');
continue;
}
// その他の権限エラーを検出
if (error.message.toLowerCase().includes('permission') ||
error.message.includes('does not have permission')) {
permissionErrorCount++;
// 連続で権限エラーが発生したら処理を中断
if (permissionErrorCount >= 2) {
console.log('❌ 権限エラーが連続発生 - 処理を中断');
return {
success: false,
error: 'PERMISSION_DENIED',
message: '分析機能を使用するには、追加の権限が必要です。\n\n管理者にお問い合わせください。'
};
}
}
}
}
console.log(`✅ メトリクス取得完了: ${updatedCount}件を更新`);
return { success: true, updatedCount: updatedCount };
} catch (error) {
console.log('❌ メトリクス取得失敗: ' + error.message);
return { success: false, error: error.message };
}
}
// ==================== Threads Insights APIからメトリクスを取得(バックエンド経由) ====================
function fetchInsights(mediaId, accessToken) {
// バックエンド経由でメトリクスを取得
return fetchMetricsViaBackend(accessToken, mediaId);
}
// ==================== 投稿一覧取得(手動実行用) ====================
function fetchThreadsList() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const settingsSheet = ss.getSheetByName(CONFIG.SHEET_NAME_SETTINGS);
// アクセストークンを取得
const accessToken = settingsSheet.getRange(CONFIG.CELL.ACCESS_TOKEN).getValue();
if (!accessToken) {
showError(' エラー\n\nアクセストークンが設定されていません。\n先にOAuth認証を完了してください。');
return;
}
try {
// 自動取得関数を呼び出す
fetchThreadsListAuto();
// 結果をダイアログで表示
const threadsListSheet = ss.getSheetByName(CONFIG.SHEET_NAME_THREADS_LIST);
const totalPosts = threadsListSheet ? threadsListSheet.getLastRow() - 1 : 0; // ヘッダー除く
showSuccess(' 投稿一覧取得完了!\n\n現在の総投稿数: ' + totalPosts + '件');
console.log('✅ 投稿一覧取得完了(手動実行)');
} catch (error) {
// エラーは既にfetchThreadsListAutoで表示されている
console.log('❌ 投稿一覧取得失敗(手動実行): ' + error.message);
}
}
// ==================== メトリクス取得(手動実行用) ====================
function fetchMetricsManual() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const settingsSheet = ss.getSheetByName(CONFIG.SHEET_NAME_SETTINGS);
// アクセストークンを取得
const accessToken = settingsSheet.getRange(CONFIG.CELL.ACCESS_TOKEN).getValue();
if (!accessToken) {
showError(' エラー\n\nアクセストークンが設定されていません。\n先にOAuth認証を完了してください。');
return;
}
// 自動取得関数を呼び出す
const result = fetchMetricsAuto();
// 権限エラーの場合はユーザーに通知
if (result && result.error === 'PERMISSION_DENIED') {
showError('⚠️ 分析機能エラー\n\n' + result.message);
console.log('❌ メトリクス取得失敗(権限エラー)');
return;
}
if (result && result.success) {
showSuccess(` メトリクス取得完了!\n\n${result.updatedCount}件を更新しました。`);
console.log('✅ メトリクス取得完了(手動実行)');
} else {
showError(' メトリクス取得エラー\n\n詳細はログを確認してください。');
console.log('❌ メトリクス取得失敗(手動実行)');
}
}
// ==================== 偏差値計算&好反応判定(自動実行用)【引用偏差値対応版】 ====================
function calculateDeviationsAndJudge() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const threadsListSheet = ss.getSheetByName(CONFIG.SHEET_NAME_THREADS_LIST);
if (!threadsListSheet) {
console.log('❌ 投稿履歴シートが見つかりません');
return;
}
try {
console.log('🔥 偏差値計算&好反応判定開始');
const lastRow = threadsListSheet.getLastRow();
if (lastRow < 2) {
console.log('投稿データがありません');
return;
}
// 全データを取得(16列)
const data = threadsListSheet.getRange(2, 1, lastRow - 1, 16).getValues();
// メトリクスが記録されている投稿を収集
const postsWithMetrics = collectPostsWithMetrics_(data);
if (postsWithMetrics.length < 5) {
console.log('⚠️ メトリクスが記録されている投稿が5件未満です。判定を実行しません。');
return;
}
console.log(`メトリクス記録済み投稿数: ${postsWithMetrics.length}件`);
// 計算対象データを取得(過去14日間 or 全件)
const baseData = getCalculationBaseData_(postsWithMetrics);
console.log(`計算対象データ数: ${baseData.length}件`);
// 統計値を計算
const stats = calculateMetricsStatistics_(baseData);
console.log(`中央値 - 閲覧:${stats.median.views}, いいね:${stats.median.likes}, 返信:${stats.median.replies}, リポスト:${stats.median.reposts}, 引用:${stats.median.quotes}`);
console.log(`標準偏差 - 閲覧:${stats.stdDev.views.toFixed(2)}, いいね:${stats.stdDev.likes.toFixed(2)}, 返信:${stats.stdDev.replies.toFixed(2)}, リポスト:${stats.stdDev.reposts.toFixed(2)}, 引用:${stats.stdDev.quotes.toFixed(2)}`);
// 各投稿について偏差値と判定を計算
let updatedCount = 0;
for (const post of postsWithMetrics) {
updatePostDeviations_(threadsListSheet, post, stats);
updatedCount++;
}
console.log(`✅ 偏差値計算&判定完了: ${updatedCount}件を更新`);
} catch (error) {
console.log('❌ 偏差値計算&判定失敗: ' + error.message);
}
}
/** メトリクスが記録されている投稿を収集 */
function collectPostsWithMetrics_(data) {
const postsWithMetrics = [];
for (let i = 0; i < data.length; i++) {
const row = data[i];
const postDateStr = row[1]; // B列: 投稿日時
const views = row[4]; // E列: 閲覧数
// メトリクスが記録されている行のみ(「取得不可」などの文字列は除外)
if (views !== '' && !isNaN(Number(views)) && postDateStr) {
postsWithMetrics.push({
rowIndex: i + 2,
postDate: new Date(postDateStr),
views: Number(views) || 0,
likes: Number(row[5]) || 0, // F列: いいね数
replies: Number(row[6]) || 0, // G列: 返信数
reposts: Number(row[7]) || 0, // H列: リポスト数
quotes: Number(row[8]) || 0 // I列: 引用数
});
}
}
return postsWithMetrics;
}
/** 計算対象データを取得(設定期間 or 全件) */
function getCalculationBaseData_(postsWithMetrics) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const settingsSheet = ss.getSheetByName(CONFIG.SHEET_NAME_SETTINGS);
// 偏差値取得期間(日数)を取得(デフォルト14日)
const deviationPeriodDays = Number(settingsSheet.getRange(CONFIG.CELL.DEVIATION_PERIOD).getValue()) || 14;
const now = new Date();
const periodAgo = new Date(now.getTime() - (deviationPeriodDays * 24 * 60 * 60 * 1000));
const pastPeriodData = postsWithMetrics.filter(post => post.postDate >= periodAgo);
if (pastPeriodData.length < 5) {
console.log(`⚠️ 過去${deviationPeriodDays}日間の投稿が5件未満です。全データを使用します。`);
return postsWithMetrics;
}
console.log(`偏差値計算対象: 過去${deviationPeriodDays}日間`);
return pastPeriodData;
}
/** 統計値(平均・標準偏差・中央値)を計算 */
function calculateMetricsStatistics_(baseData) {
// 各指標の配列を作成
const arrays = {
views: baseData.map(p => p.views),
likes: baseData.map(p => p.likes),
replies: baseData.map(p => p.replies),
reposts: baseData.map(p => p.reposts),
quotes: baseData.map(p => p.quotes)
};
// 平均値を計算
const avg = {};
for (const key in arrays) {
avg[key] = arrays[key].reduce((sum, v) => sum + v, 0) / arrays[key].length;
}
// 標準偏差を計算
const stdDev = {};
for (const key in arrays) {
stdDev[key] = calculateStdDev(arrays[key], avg[key]);
}
// 中央値を計算
const median = {};
for (const key in arrays) {
median[key] = calculateMedian(arrays[key]);
}
return { avg, stdDev, median };
}
/** 投稿の偏差値を計算してシートに記録 */
function updatePostDeviations_(sheet, post, stats) {
// 偏差値を計算
const deviations = {
views: calculateDeviation(post.views, stats.avg.views, stats.stdDev.views),
likes: calculateDeviation(post.likes, stats.avg.likes, stats.stdDev.likes),
replies: calculateDeviation(post.replies, stats.avg.replies, stats.stdDev.replies),
reposts: calculateDeviation(post.reposts, stats.avg.reposts, stats.stdDev.reposts),
quotes: calculateDeviation(post.quotes, stats.avg.quotes, stats.stdDev.quotes)
};
const totalDeviation = (deviations.views + deviations.likes + deviations.replies + deviations.reposts + deviations.quotes) / 5;
// 好反応判定(5項目のうち1つ以上が中央値超え)
const isHighPerformance =
post.views > stats.median.views ||
post.likes > stats.median.likes ||
post.replies > stats.median.replies ||
post.reposts > stats.median.reposts ||
post.quotes > stats.median.quotes;
// スプレッドシートに記録
const rowIndex = post.rowIndex;
sheet.getRange(rowIndex, 10).setValue(Math.round(deviations.views * 10) / 10); // J列: 閲覧偏差値
sheet.getRange(rowIndex, 11).setValue(Math.round(deviations.likes * 10) / 10); // K列: いいね偏差値
sheet.getRange(rowIndex, 12).setValue(Math.round(deviations.replies * 10) / 10); // L列: 返信偏差値
sheet.getRange(rowIndex, 13).setValue(Math.round(deviations.reposts * 10) / 10); // M列: リポスト偏差値
sheet.getRange(rowIndex, 14).setValue(Math.round(deviations.quotes * 10) / 10); // N列: 引用偏差値
sheet.getRange(rowIndex, 15).setValue(Math.round(totalDeviation * 10) / 10); // O列: 総合偏差値
sheet.getRange(rowIndex, 16).setValue(isHighPerformance ? 'はい' : 'いいえ'); // P列: 好反応フラグ
}
// ==================== 偏差値計算&好反応判定(手動実行用) ====================
function calculateDeviationsManual() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const settingsSheet = ss.getSheetByName(CONFIG.SHEET_NAME_SETTINGS);
// アクセストークンチェック(念のため)
const accessToken = settingsSheet.getRange(CONFIG.CELL.ACCESS_TOKEN).getValue();
if (!accessToken) {
showError(' エラー\n\nアクセストークンが設定されていません。\n先にOAuth認証を完了してください。');
return;
}
// 計算実行
calculateDeviationsAndJudge();
showSuccess(' 偏差値計算&好反応判定完了!\n\n実行ログで詳細を確認してください。');
console.log('✅ 偏差値計算&判定完了(手動実行)');
}
// ==================== 中央値計算 ====================
function calculateMedian(numbers) {
if (numbers.length === 0) return 0;
const sorted = numbers.slice().sort((a, b) => a - b);
const middle = Math.floor(sorted.length / 2);
if (sorted.length % 2 === 0) {
// 偶数個の場合は中央2つの平均
return (sorted[middle - 1] + sorted[middle]) / 2;
} else {
// 奇数個の場合は中央値
return sorted[middle];
}
}
// ==================== 標準偏差計算 ====================
function calculateStdDev(numbers, average) {
if (numbers.length === 0) return 0;
const squareDiffs = numbers.map(value => {
const diff = value - average;
return diff * diff;
});
const avgSquareDiff = squareDiffs.reduce((sum, value) => sum + value, 0) / numbers.length;
return Math.sqrt(avgSquareDiff);
}
// ==================== 偏差値計算 ====================
function calculateDeviation(value, average, stdDev) {
if (stdDev === 0) return 50; // 標準偏差が0の場合は50
return 50 + ((value - average) / stdDev) * 10;
}
// ==================== 投稿履歴&メトリクス&デイリーサマリー統合実行(トリガー用) ====================
function executeAnalysisRoutine() {
console.log('🔥 分析ルーチン開始');
// 1. 投稿一覧を取得
fetchThreadsListAuto();
// 2. メトリクスを取得
Utilities.sleep(2000); // 2秒待機
const metricsResult = fetchMetricsAuto();
// 権限エラーの場合はログに記録(自動実行なのでダイアログは出さない)
if (metricsResult && metricsResult.error === 'PERMISSION_DENIED') {
console.log('⚠️ メトリクス取得: 権限エラー - 分析機能を使用するには追加の権限が必要です');
}
// 3. 偏差値計算&好反応判定
Utilities.sleep(1000); // 1秒待機
calculateDeviationsAndJudge();
// 4. デイリーサマリー作成
Utilities.sleep(1000); // 1秒待機
createDailySummary();
// 5. 高偏差値投稿をループ投稿に自動移設
Utilities.sleep(1000); // 1秒待機
transferHighDeviationToLoopAuto();
// 6. 投稿方法(予約/手動)を判定・記録
Utilities.sleep(1000); // 1秒待機
updatePostingMethod();
console.log('🔥 分析ルーチン完了(高偏差値移設・投稿方法判定含む)');
}
// ==================== 投稿履歴&分析の統合トリガー設定 ====================
function setupAnalysisTrigger() {
// 既存のトリガーを削除
const triggers = ScriptApp.getProjectTriggers();
triggers.forEach(trigger => {
if (trigger.getHandlerFunction() === 'executeAnalysisRoutine' ||
trigger.getHandlerFunction() === 'fetchThreadsListAuto') {
ScriptApp.deleteTrigger(trigger);
}
});
// 新しいトリガーを作成(1時間ごと)
ScriptApp.newTrigger('executeAnalysisRoutine')
.timeBased()
.everyHours(1)
.create();
showSuccess(' 投稿履歴&分析トリガー設定完了\n\n1時間ごとに以下を自動実行します:\n・投稿一覧取得\n・メトリクス取得(6時間経過後)\n・偏差値計算&好反応判定\n・デイリーサマリー作成 ← NEW!');
console.log('✅ 投稿履歴&分析トリガー設定完了(デイリーサマリー含む)');
}
// ==================== 投稿履歴&分析の統合トリガー削除 ====================
function deleteAnalysisTrigger() {
const triggers = ScriptApp.getProjectTriggers();
let count = 0;
triggers.forEach(trigger => {
if (trigger.getHandlerFunction() === 'executeAnalysisRoutine' ||
trigger.getHandlerFunction() === 'fetchThreadsListAuto') {
ScriptApp.deleteTrigger(trigger);
count++;
}
});
showSuccess(` 投稿履歴&分析トリガー削除完了\n\n${count}個のトリガーを削除しました。`);
console.log(`✅ 投稿履歴&分析トリガー削除完了: ${count}個`);
}
// ==================== デイリーサマリー作成 ====================
function createDailySummary() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sourceSheet = ss.getSheetByName('投稿履歴');
const targetSheet = ss.getSheetByName('デイリーサマリー');
if (!sourceSheet) {
console.log('エラー: 投稿履歴シートが見つかりません');
return;
}
if (!targetSheet) {
console.log('エラー: デイリーサマリーシートが見つかりません');
return;
}
const today = Utilities.formatDate(new Date(), 'JST', 'yyyy/MM/dd');
const todayRowIndex = findTodayRowIndex_(targetSheet, today);
console.log(`今日の日付: ${today}, 既存行: ${todayRowIndex}`);
const followersCount = getFollowersCount();
const dailyData = groupPostsByDate_(sourceSheet);
if (!dailyData[today]) {
console.log('今日の投稿データがまだありません');
return;
}
const todayData = buildDailySummaryData_(today, followersCount, dailyData[today]);
if (todayRowIndex > 0) {
targetSheet.getRange(todayRowIndex, 1, 1, 19).setValues([todayData]);
console.log(`デイリーサマリー更新完了: ${today} (行${todayRowIndex})`);
} else {
targetSheet.appendRow(todayData);
console.log(`デイリーサマリー追加完了: ${today} (新規)`);
}
}
/** 指定日付の既存行インデックスを探す */
function findTodayRowIndex_(sheet, today) {
const lastRow = sheet.getLastRow();
if (lastRow <= 1) return -1;
const existingDates = sheet.getRange(2, 1, lastRow - 1, 1).getValues();
for (let i = 0; i < existingDates.length; i++) {
const dateStr = typeof existingDates[i][0] === 'string'
? existingDates[i][0]
: Utilities.formatDate(existingDates[i][0], 'JST', 'yyyy/MM/dd');
if (dateStr === today) {
return i + 2;
}
}
return -1;
}
/** 投稿履歴を日付ごとにグループ化 */
function groupPostsByDate_(sourceSheet) {
const data = sourceSheet.getDataRange().getValues();
const rows = data.slice(1);
const dailyData = {};
rows.forEach(row => {
const dateTimeStr = row[1];
if (!dateTimeStr) return;
const dateStr = typeof dateTimeStr === 'string'
? dateTimeStr.split(' ')[0]
: Utilities.formatDate(dateTimeStr, 'JST', 'yyyy/MM/dd');
if (!dailyData[dateStr]) {
dailyData[dateStr] = [];
}
const toNum = (v) => (typeof v === 'number') ? v : (isNaN(Number(v)) ? 0 : Number(v));
dailyData[dateStr].push({
views: toNum(row[4]),
likes: toNum(row[5]),
replies: toNum(row[6]),
reposts: toNum(row[7]),
quotes: toNum(row[8]),
viewScore: toNum(row[9]),
likeScore: toNum(row[10]),
replyScore: toNum(row[11]),
repostScore: toNum(row[12]),
quoteScore: toNum(row[13]),
totalScore: toNum(row[14])
});
});
return dailyData;
}
/** デイリーサマリーデータを構築 */
function buildDailySummaryData_(today, followersCount, dayPosts) {
const postCount = dayPosts.length;
const metrics = calculateDailyMetrics_(dayPosts, postCount);
const scores = calculateDailyScoreAverages_(dayPosts);
return [
today,
followersCount,
postCount,
metrics.totalViews,
metrics.totalLikes,
metrics.totalReplies,
metrics.totalReposts,
metrics.totalQuotes,
metrics.avgViews,
metrics.avgLikes,
metrics.avgReplies,
metrics.avgReposts,
metrics.avgQuotes,
scores.avgViewScore,
scores.avgLikeScore,
scores.avgReplyScore,
scores.avgRepostScore,
scores.avgQuoteScore,
scores.avgTotalScore
];
}
/** 日別の集計値を計算 */
function calculateDailyMetrics_(dayPosts, postCount) {
const totalViews = dayPosts.reduce((sum, post) => sum + post.views, 0);
const totalLikes = dayPosts.reduce((sum, post) => sum + post.likes, 0);
const totalReplies = dayPosts.reduce((sum, post) => sum + post.replies, 0);
const totalReposts = dayPosts.reduce((sum, post) => sum + post.reposts, 0);
const totalQuotes = dayPosts.reduce((sum, post) => sum + post.quotes, 0);
return {
totalViews,
totalLikes,
totalReplies,
totalReposts,
totalQuotes,
avgViews: Math.round(totalViews / postCount * 10) / 10,
avgLikes: Math.round(totalLikes / postCount * 10) / 10,
avgReplies: Math.round(totalReplies / postCount * 10) / 10,
avgReposts: Math.round(totalReposts / postCount * 10) / 10,
avgQuotes: Math.round(totalQuotes / postCount * 10) / 10
};
}
/** スコア平均を計算 */
function calculateDailyScoreAverages_(dayPosts) {
const calcAvg = (posts, key) => {
const valid = posts.filter(post => post[key] > 0);
return valid.length > 0
? Math.round(valid.reduce((sum, post) => sum + post[key], 0) / valid.length * 10) / 10
: 0;
};
return {
avgViewScore: calcAvg(dayPosts, 'viewScore'),
avgLikeScore: calcAvg(dayPosts, 'likeScore'),
avgReplyScore: calcAvg(dayPosts, 'replyScore'),
avgRepostScore: calcAvg(dayPosts, 'repostScore'),
avgQuoteScore: calcAvg(dayPosts, 'quoteScore'),
avgTotalScore: calcAvg(dayPosts, 'totalScore')
};
}
// ==================== デイリーサマリー作成(手動実行用) ====================
function createDailySummaryManual() {
try {
createDailySummary();
showSuccess(' デイリーサマリー作成完了!\n\nデイリーサマリーシートを確認してください。');
console.log('✅ デイリーサマリー作成完了(手動実行)');
} catch (error) {
showError(' デイリーサマリー作成失敗\n\n' + error.message);
console.log('❌ デイリーサマリー作成失敗: ' + error.message);
}
}
// ==================== 高偏差値投稿をループ投稿に移設(自動実行用) ====================
/**
* トリガーから呼び出される自動実行版(ダイアログ表示なし)
*/
function transferHighDeviationToLoopAuto() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const historySheet = ss.getSheetByName(CONFIG.SHEET_NAME_THREADS_LIST);
const loopSheet = ss.getSheetByName(LOOP_CONFIG.SHEET_NAME);
const settingsSheet = ss.getSheetByName(CONFIG.SHEET_NAME_SETTINGS);
if (!historySheet || !loopSheet || !settingsSheet) {
console.log('❌ 必要なシートが見つかりません');
return;
}
try {
const prefixTextRaw = settingsSheet.getRange(CONFIG.CELL.INTRO_WORD).getValue() || '';
const transferThreshold = Number(settingsSheet.getRange(CONFIG.CELL.TRANSFER_THRESHOLD).getValue()) || 65;
console.log(`移設基準偏差値: ${transferThreshold}`);
const historyLastRow = historySheet.getLastRow();
if (historyLastRow < 2) {
console.log('⏭️ 投稿履歴にデータなし');
return;
}
const historyData = historySheet.getRange(2, 1, historyLastRow - 1, 17).getValues();
const targetRows = [];
const notRequiredRows = []; // 移設不要の行
for (let i = 0; i < historyData.length; i++) {
const row = historyData[i];
const postId = row[0];
const postContent = row[2]; // C列: 投稿内容
const totalDeviation = row[14];
const transferFlag = row[16];
// Q列が空で偏差値が記録されている行のみ処理
if (postId && totalDeviation !== '' && (!transferFlag || transferFlag === '')) {
if (Number(totalDeviation) >= transferThreshold) {
// 基準値以上 → 移設対象
targetRows.push({
rowIndex: i + 2,
postId: postId.toString().replace(/^'/, ''),
postContent: postContent || '',
deviation: totalDeviation
});
} else {
// 基準値未満 → 移設不要
notRequiredRows.push(i + 2);
}
}
}
// 移設不要フラグを設定
if (notRequiredRows.length > 0) {
for (const rowIndex of notRequiredRows) {
historySheet.getRange(rowIndex, 17).setValue('移設不要');
}
console.log(`📝 移設不要フラグ設定: ${notRequiredRows.length}件`);
}
if (targetRows.length === 0) {
console.log('⏭️ 移設対象なし');
return;
}
let loopLastRow = loopSheet.getLastRow();
let transferCount = 0;
for (const target of targetRows) {
// 誘導パワーワードをランダム選択(カンマ区切り対応)
const prefixText = selectRandomPowerWord_(prefixTextRaw);
const newContent = prefixText ? `${prefixText}\n${target.postId}` : target.postId;
loopLastRow++;
loopSheet.getRange(loopLastRow, LOOP_CONFIG.COLUMN.NO).setValue(loopLastRow - 1);
loopSheet.getRange(loopLastRow, LOOP_CONFIG.COLUMN.THREAD_ID).setValue(target.postContent); // F列: 投稿内容
loopSheet.getRange(loopLastRow, LOOP_CONFIG.COLUMN.CONTENT).setValue(newContent);
loopSheet.getRange(loopLastRow, LOOP_CONFIG.COLUMN.CHAR_COUNT).setFormula(`=LEN(G${loopLastRow})`);
historySheet.getRange(target.rowIndex, 17).setValue('移設完了');
console.log(`✅ 自動移設: 行${target.rowIndex}(偏差値: ${target.deviation})`);
transferCount++;
}
console.log(`🔥 高偏差値投稿の自動移設完了: ${transferCount}件`);
} catch (error) {
console.log(`❌ 自動移設エラー: ${error.message}`);
}
}
// ==================== 高偏差値投稿をループ投稿に移設(手動実行用) ====================
/**
* 投稿履歴シートのO列(総合偏差値)が65以上の投稿を
* ループ投稿管理シートのG列に移設する
*
* 処理内容:
* 1. 投稿履歴O列が65以上 かつ Q列が空の行を検索
* 2. A列の投稿IDを取得
* 3. 自動生成設定B35のテキスト + 投稿ID をループ投稿管理G列に追加
* 4. 投稿履歴Q列に「移設完了」を記録
*/
function transferHighDeviationToLoop() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const historySheet = ss.getSheetByName(CONFIG.SHEET_NAME_THREADS_LIST); // 投稿履歴
const loopSheet = ss.getSheetByName(LOOP_CONFIG.SHEET_NAME); // ループ投稿管理
const settingsSheet = ss.getSheetByName(CONFIG.SHEET_NAME_SETTINGS); // 設定
if (!historySheet) {
showError('エラー\n\n「投稿履歴」シートが見つかりません');
return;
}
if (!loopSheet) {
showError('エラー\n\n「ループ投稿管理」シートが見つかりません');
return;
}
if (!settingsSheet) {
showError('エラー\n\n「設定」シートが見つかりません');
return;
}
try {
console.log('🔥 高偏差値投稿の移設開始');
// 設定値を取得
const prefixTextRaw = settingsSheet.getRange(CONFIG.CELL.INTRO_WORD).getValue() || '';
const transferThreshold = Number(settingsSheet.getRange(CONFIG.CELL.TRANSFER_THRESHOLD).getValue()) || 65;
console.log(`誘導パワーワード: ${prefixTextRaw}`);
console.log(`移設基準偏差値: ${transferThreshold}`);
// 投稿履歴の全データを取得(17列:A〜Q)
const historyLastRow = historySheet.getLastRow();
if (historyLastRow < 2) {
showInfo('情報\n\n投稿履歴にデータがありません');
return;
}
const historyData = historySheet.getRange(2, 1, historyLastRow - 1, 17).getValues();
// 基準値以上 かつ Q列が空の行を抽出
const targetRows = [];
const notRequiredRows = []; // 移設不要の行
for (let i = 0; i < historyData.length; i++) {
const row = historyData[i];
const postId = row[0]; // A列: 投稿ID
const postContent = row[2]; // C列: 投稿内容
const totalDeviation = row[14]; // O列: 総合偏差値(15番目、0始まり)
const transferFlag = row[16]; // Q列: 移設フラグ(17番目、0始まり)
// Q列が空で偏差値が記録されている行のみ処理
if (postId && totalDeviation !== '' && (!transferFlag || transferFlag === '')) {
if (Number(totalDeviation) >= transferThreshold) {
// 基準値以上 → 移設対象
targetRows.push({
rowIndex: i + 2, // ヘッダー行を考慮
postId: postId.toString().replace(/^'/, ''), // 先頭のシングルクォートを除去
postContent: postContent || '',
deviation: totalDeviation
});
} else {
// 基準値未満 → 移設不要
notRequiredRows.push(i + 2);
}
}
}
// 移設不要フラグを設定
if (notRequiredRows.length > 0) {
for (const rowIndex of notRequiredRows) {
historySheet.getRange(rowIndex, 17).setValue('移設不要');
}
console.log(`📝 移設不要フラグ設定: ${notRequiredRows.length}件`);
}
if (targetRows.length === 0) {
showInfo('情報\n\n移設対象の投稿がありません\n(偏差値65以上かつ未移設の投稿なし)');
return;
}
console.log(`移設対象: ${targetRows.length}件`);
// ループ投稿管理の最終行を取得
let loopLastRow = loopSheet.getLastRow();
// 各対象行を処理
let transferCount = 0;
for (const target of targetRows) {
// 誘導パワーワードをランダム選択(カンマ区切り対応)
const prefixText = selectRandomPowerWord_(prefixTextRaw);
// ループ投稿管理に追加する本文を作成
const newContent = prefixText ? `${prefixText}\n${target.postId}` : target.postId;
// ループ投稿管理に新しい行を追加
loopLastRow++;
loopSheet.getRange(loopLastRow, LOOP_CONFIG.COLUMN.NO).setValue(loopLastRow - 1);
loopSheet.getRange(loopLastRow, LOOP_CONFIG.COLUMN.THREAD_ID).setValue(target.postContent); // F列: 投稿内容
loopSheet.getRange(loopLastRow, LOOP_CONFIG.COLUMN.CONTENT).setValue(newContent);
// 文字数カウント数式を設定
loopSheet.getRange(loopLastRow, LOOP_CONFIG.COLUMN.CHAR_COUNT).setFormula(`=LEN(G${loopLastRow})`);
// 投稿履歴のQ列に「移設完了」を記録
historySheet.getRange(target.rowIndex, 17).setValue('移設完了');
console.log(`✅ 移設完了: 行${target.rowIndex} → ループ行${loopLastRow}(偏差値: ${target.deviation})`);
transferCount++;
}
showSuccess(`移設完了!\n\n${transferCount}件の高偏差値投稿をループ投稿管理に追加しました`);
console.log(`🔥 移設完了: ${transferCount}件`);
} catch (error) {
const errorMsg = `エラー: ${error.message}\n\nStack: ${error.stack}`;
showError(errorMsg);
console.log(errorMsg);
}
}
// ==================== 投稿方法判定(予約/手動)====================
/**
* 投稿履歴のR列に「予約」または「手動」を記録
*
* ロジック:
* 1. 投稿履歴シートのA列(投稿ID)を取得
* 2. 自由投稿管理・固定投稿管理シートのM列(投稿ID)と照合
* 3. マッチ → 「予約」、マッチしない → 「手動」
* 4. R列(18列目)に結果を記録
*/
function updatePostingMethod() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const historySheet = ss.getSheetByName(CONFIG.SHEET_NAME_THREADS_LIST);
const freeSheet = ss.getSheetByName(CONFIG.SHEET_NAME_POSTS_FREE);
const fixedSheet = ss.getSheetByName(CONFIG.SHEET_NAME_POSTS_FIXED);
if (!historySheet) {
console.log('❌ 投稿履歴シートが見つかりません');
return;
}
try {
console.log('🔥 投稿方法判定開始');
// R列(18列目)のヘッダーを確認・設定
const header = historySheet.getRange(1, 18).getValue();
if (!header || header !== '投稿方法') {
historySheet.getRange(1, 18).setValue('投稿方法');
historySheet.getRange(1, 18).setFontWeight('bold').setBackground('#4a86e8').setFontColor('#ffffff');
}
// 自由投稿管理・固定投稿管理から投稿IDを収集
const scheduledPostIds = new Set();
// 自由投稿管理のM列(13列目)から投稿IDを取得
if (freeSheet) {
const freeLastRow = freeSheet.getLastRow();
if (freeLastRow > 1) {
const freeIds = freeSheet.getRange(2, CONFIG.COLUMN.POST_ID, freeLastRow - 1, 1).getValues();
freeIds.forEach(row => {
if (row[0]) {
const idStr = String(row[0]).replace(/^'/, '').trim();
if (idStr) scheduledPostIds.add(idStr);
}
});
}
}
// 固定投稿管理のM列(13列目)から投稿IDを取得
if (fixedSheet) {
const fixedLastRow = fixedSheet.getLastRow();
if (fixedLastRow > 1) {
const fixedIds = fixedSheet.getRange(2, CONFIG.COLUMN.POST_ID, fixedLastRow - 1, 1).getValues();
fixedIds.forEach(row => {
if (row[0]) {
const idStr = String(row[0]).replace(/^'/, '').trim();
if (idStr) scheduledPostIds.add(idStr);
}
});
}
}
console.log(`予約投稿ID数: ${scheduledPostIds.size}件`);
// 投稿履歴のA列とR列を取得
const historyLastRow = historySheet.getLastRow();
if (historyLastRow < 2) {
console.log('⏭️ 投稿履歴にデータなし');
return;
}
// A列(投稿ID)とR列(投稿方法)を取得
const historyIds = historySheet.getRange(2, 1, historyLastRow - 1, 1).getValues();
const postingMethods = historySheet.getRange(2, 18, historyLastRow - 1, 1).getValues();
let updatedCount = 0;
for (let i = 0; i < historyIds.length; i++) {
const postId = historyIds[i][0];
const currentMethod = postingMethods[i][0];
// 既に記録済みならスキップ
if (currentMethod && currentMethod !== '') {
continue;
}
// 投稿IDがない場合はスキップ
if (!postId) {
continue;
}
const postIdStr = String(postId).replace(/^'/, '').trim();
const method = scheduledPostIds.has(postIdStr) ? '予約投稿' : '手動投稿';
historySheet.getRange(i + 2, 18).setValue(method);
updatedCount++;
}
console.log(`✅ 投稿方法判定完了: ${updatedCount}件更新`);
} catch (error) {
console.log(`❌ 投稿方法判定エラー: ${error.message}`);
}
}
// ==================== 投稿方法判定(手動実行用)====================
function updatePostingMethodManual() {
updatePostingMethod();
showSuccess('投稿方法判定完了!\n\n投稿履歴のR列を確認してください。');
}
// ==================== 誘導パワーワードのランダム選択 ====================
/**
* カンマ区切りの誘導パワーワードから1つをランダムに選択
* @param {string} rawText - カンマ区切りのテキスト(例:「見てね,チェック,要確認」)
* @return {string} - ランダムに選択された1つのワード(空白トリム済み)
*/
function selectRandomPowerWord_(rawText) {
if (!rawText || rawText.toString().trim() === '') {
return '';
}
// カンマで分割して配列化
const words = rawText.toString().split(',').map(w => w.trim()).filter(w => w !== '');
if (words.length === 0) {
return '';
}
if (words.length === 1) {
return words[0];
}
// ランダムで1つ選択
const randomIndex = Math.floor(Math.random() * words.length);
return words[randomIndex];
}