알구몬 키워드 필터 스크립트를 만들었습니다

제미니랑 챗지피티를 써서 해당 기능을 구현한 Tampermonkey 스크립트를 만들었습니다. 저만 쓰기 아까워 공유합니다.
모바일 브라우저는 Mises 앱을 쓰면 Tampermonkey를 비롯한 크롬 확장 프로그램들 쓸 수 있습니다.
최신 정보, 알구몬 랭킹, 댓글들 화면에 적용됩니다.
많이 샀어요는 구UI라 그런지 여러번 수정해도 적용이 잘 안돼서 포기했습니다.

// ==UserScript==
// @name         알구몬 키워드 필터
// @namespace    local.algumon.keyword.filter
// @version      0.3
// @match        https://www.algumon.com/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function () {
‘use strict’;

const DEBUG = true;
const SETTINGS_URL = '/n/settings/ignored-keywords';
const STORAGE_KEY = 'algumon_ignored_keywords_cache_v03';

function log(...args) {
    if (DEBUG) console.log('[AlgumonKeywordFilter]', ...args);
}

function normalize(text) {
    return String(text || '')
        .replace(/\s+/g, ' ')
        .trim()
        .toLowerCase();
}

function saveCache(keywords) {
    try {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(keywords));
    } catch (e) {
        log('cache save failed', e);
    }
}

function loadCache() {
    try {
        const raw = localStorage.getItem(STORAGE_KEY);
        if (!raw) return [];
        const parsed = JSON.parse(raw);
        return Array.isArray(parsed) ? parsed : [];
    } catch (e) {
        log('cache load failed', e);
        return [];
    }
}

async function getKeywords() {
    try {
        const res = await fetch(SETTINGS_URL, {
            credentials: 'include',
            cache: 'no-store'
        });

        const html = await res.text();
        const doc = new DOMParser().parseFromString(html, 'text/html');

        const keywords = [];

        for (const el of doc.querySelectorAll('input, textarea, div, span, p, li')) {
            const text = (el.value || el.getAttribute?.('value') || el.textContent || '').trim();

            if (!text) continue;
            if (text.length < 2 || text.length > 40) continue;
            if (text.includes('\n')) continue;
            if (!/[0-9A-Za-z가-힣]/.test(text)) continue;
            if ([
                '설정',
                '추가',
                '키워드 추가',
                '예외 키워드 관리',
                '예외 키워드 등록 팁',
                '로그아웃'
            ].includes(text)) continue;

            keywords.push(normalize(text));
        }

        const unique = [...new Set(keywords)];

        if (unique.length) {
            saveCache(unique);
            log('keywords loaded', unique);
            return unique;
        }

        throw new Error('no keywords found');
    } catch (e) {
        const cached = loadCache();
        log('using cache', cached, e);
        return cached;
    }
}

function shouldHide(title, keywords) {
    const t = normalize(title);
    return keywords.some(k => t.includes(k));
}

function hideCard(card) {
    card.style.setProperty('display', 'none', 'important');
}

function showCard(card) {
    card.style.removeProperty('display');
}

function filterDealPage(keywords) {
    const cards = document.querySelectorAll('[id^="deal-"]');

    cards.forEach(card => {
        const titleEl =
            card.querySelector('h3') ||
            card.querySelector('a[href*="/deal/"]') ||
            card.querySelector('a[href*="/n/deal/"]') ||
            card.querySelector('.card-title') ||
            card.querySelector('.title') ||
            card.querySelector('.subject');

        const title = titleEl ? titleEl.textContent.trim() : '';
        if (!title) return;

        if (shouldHide(title, keywords)) {
            hideCard(card);
        } else {
            showCard(card);
        }
    });
}

function filterBoughtTodayPage(keywords) {
const cards = document.querySelectorAll(‘[id^=“post-”]’);

cards.forEach(card => {
    const candidates = [
        card.querySelector('h3'),
        card.querySelector('h4'),
        card.querySelector('.title'),
        card.querySelector('.subject'),
        card.querySelector('.product'),
        card.querySelector('a[href*="/deal/"]'),
        card.querySelector('a[href*="/post/"]'),
        card.querySelector('a')
    ].filter(Boolean);

    let title = '';

    for (const el of candidates) {
        const text = (el.textContent || '').trim();
        if (text.length >= 2) {
            title = text;
            break;
        }
    }

    if (!title) {
        const allText = (card.innerText || '')
            .split('\n')
            .map(v => v.trim())
            .filter(Boolean);

        title = allText.find(v =>
            v.length >= 4 &&
            !v.includes('사고싶다') &&
            !v.includes('샀어요') &&
            !v.includes('댓글') &&
            !v.includes('공유') &&
            !/^\d+[,.]?\d*원/.test(v) &&
            !/배송/.test(v)
        ) || '';
    }

    if (!title) return;

    if (shouldHide(title, keywords)) {
        hideCard(card);
    } else {
        showCard(card);
    }
});

}

function filterCommentPage(keywords) {
    const cards = document.querySelectorAll('[id^="comment-"]');

    cards.forEach(card => {
        const titleEl =
            card.querySelector('a[href*="/deal/"]') ||
            card.querySelector('a[href*="/n/deal/"]') ||
            card.querySelector('h3') ||
            card.querySelector('.title') ||
            card.querySelector('.subject');

        const title = titleEl ? titleEl.textContent.trim() : '';
        if (!title) return;

        if (shouldHide(title, keywords)) {
            hideCard(card);
        } else {
            showCard(card);
        }
    });
}

function applyFilter(keywords) {
    if (!keywords.length) return;

    const path = location.pathname;

    if (path.startsWith('/n/comment')) {
        filterCommentPage(keywords);
        return;
    }

    if (path.startsWith('/deal/bought-today')) {
        filterBoughtTodayPage(keywords);
        return;
    }

    if (path.startsWith('/n/deal')) {
        filterDealPage(keywords);
        return;
    }
}

async function run() {
    const keywords = await getKeywords();

    const observer = new MutationObserver(() => {
        applyFilter(keywords);
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    applyFilter(keywords);
}

run();

})();
5개의 좋아요

헉…! 고수시네요!

1개의 좋아요

스크립트를 보니 설정 페이지에서 예외 키워드를 가져와서 제목에 해당 키워드가 포함된 게시글은 display: none으로 숨기는 방식이네요 ㅎㅎ

1개의 좋아요