カクヨム平均スコア表示(数値だけ色と太字・精密比較)

平均スコアの数値のみ色分け(2.80・2.90含め正確に対応)

// ==UserScript==
// @name         カクヨム平均スコア表示(数値だけ色と太字・精密比較)
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  平均スコアの数値のみ色分け(2.80・2.90含め正確に対応)
// @match        https://kakuyomu.jp/search*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const handledWorks = new Set();

    window.addEventListener('load', () => {
        setTimeout(() => {
            const links = Array.from(document.querySelectorAll('a[href^="/works/"]'));
            const uniqueLinks = [...new Set(links.map(a => a.href))];

            for (const url of uniqueLinks) {
                const workId = url.split('/works/')[1];
                if (!workId || handledWorks.has(workId)) continue;
                handledWorks.add(workId);
                fetchWorkAverage(url, workId);
            }
        }, 2000);
    });

    async function fetchWorkAverage(url, workId) {
        try {
            const res = await fetch(url);
            const html = await res.text();

            const jsonText = html.match(/<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/)?.[1];
            if (!jsonText) throw new Error('JSONデータが見つかりません');

            const json = JSON.parse(jsonText);
            const state = json.props?.pageProps?.__APOLLO_STATE__;
            const workKey = Object.keys(state).find(k => k.startsWith('Work:') && k.includes(workId));

            if (!workKey || !state[workKey]) throw new Error('Workデータが見つかりません');

            const reviewCount = Number(state[workKey].reviewCount);
            const totalReviewPoint = Number(state[workKey].totalReviewPoint);
            if (reviewCount === 0) return;

            const avgRaw = totalReviewPoint / reviewCount;
            const avgRounded = Math.round(avgRaw * 100) / 100; // 数値として丸める
            const avgDisplay = avgRounded.toFixed(2); // 表示用文字列

            const anchor = document.querySelector(`a[href="/works/${workId}"]`);
            if (!anchor) return;

            const info = document.createElement('div');
            info.style.fontSize = '13px';
            info.style.marginTop = '4px';
            info.style.color = '#444';

            const spanScore = document.createElement('span');
            spanScore.textContent = avgDisplay;

            // 数値条件を精密に評価
            if (avgRounded >= 2.90) {
                spanScore.style.color = 'red';
                spanScore.style.fontWeight = 'bold';
            } else if (avgRounded >= 2.80) {
                spanScore.style.color = 'green';
                spanScore.style.fontWeight = 'bold';
            }

            info.appendChild(document.createTextNode('📊 平均スコア: '));
            info.appendChild(spanScore);
            info.appendChild(document.createTextNode(`(${reviewCount}件)`));

            const parent = anchor.closest('div[class^="WorkSummaryItem_root"]') || anchor.parentElement;
            if (parent) parent.appendChild(info);

        } catch (e) {
            console.warn(`❌ ${workId} の取得に失敗しました:`, e);
        }
    }
})();