您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
디시인사이드 말머리 차단 기능
// ==UserScript== // @name HORUS // @namespace HORUS-DEADlock // @version 1.0.0 // @description 디시인사이드 말머리 차단 기능 // @author DEADlock // @match https://gall.dcinside.com/*/board/lists* // @match https://gall.dcinside.com/board/lists* // @match https://gall.dcinside.com/*/board/view* // @match https://gall.dcinside.com/board/view* // @icon https://i.imgur.com/LypOzKK.png // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_xmlhttpRequest // @connect dcinside.com // ==/UserScript== (function() { 'use strict'; const CONFIG = { mainModalId: 'horus-blocker-modal', mgmtModalId: 'horus-management-modal', settingsKey: 'horus_gallery_settings', darkThemeClass: 'horus-dark-theme', }; const Utils = { getSettings: () => JSON.parse(GM_getValue(CONFIG.settingsKey, '{}')), saveSettings: (settings) => GM_setValue(CONFIG.settingsKey, JSON.stringify(settings)), getCurrentGalleryId: () => { const params = new URLSearchParams(window.location.search); return params.get('id'); }, getCurrentGalleryName: () => { const selectors = ['.gall_name_inner .gall_name a', '.page_head h2 a', '.gall_name .text']; for (const selector of selectors) { const el = document.querySelector(selector); if (el) { const clone = el.cloneNode(true); clone.querySelectorAll('em, span').forEach(tag => tag.remove()); return clone.textContent.trim(); } } return '알 수 없는 갤러리'; }, applyTheme: () => document.body.classList.toggle(CONFIG.darkThemeClass, !!document.getElementById('css-darkmode')), fetchAvailableFlairs: (galleryId) => { return new Promise((resolve, reject) => { const pathname = window.location.pathname; const boardIndex = pathname.indexOf('/board/'); let galleryPath; if (boardIndex > 0) { const prefix = pathname.substring(1, boardIndex); galleryPath = `${prefix}/board`; } else { galleryPath = 'board'; } const listUrl = `https://gall.dcinside.com/${galleryPath}/lists/?id=${galleryId}`; GM_xmlhttpRequest({ method: 'GET', url: listUrl, onload: function(response) { if (response.status >= 200 && response.status < 300) { const parser = new DOMParser(); const doc = parser.parseFromString(response.responseText, 'text/html'); const flairElements = doc.querySelectorAll('.list_array_option .inner ul li a'); const flairs = new Set([...flairElements].map(el => el.innerText.trim()).filter(Boolean)); resolve(flairs); } else { reject(new Error(`Failed to fetch flairs. Status: ${response.status}, URL: ${listUrl}`)); } }, onerror: function(error) { reject(new Error(`Network error while fetching flairs. URL: ${listUrl}`)); } }); }); } }; const UI = { createModal: function({ id, title, bodyHTML, footerHTML }) { const existingModal = document.getElementById(id); if (existingModal) existingModal.remove(); const modalWrapper = document.createElement('div'); modalWrapper.innerHTML = ` <div id="${id}" class="${id}"> <div class="horus-modal-header"> <span>${title}</span> <button class="horus-modal-close-btn">×</button> </div> <div class="horus-modal-body">${bodyHTML}</div> ${footerHTML ? `<div class="horus-modal-footer">${footerHTML}</div>` : ''} </div> `; const modalElement = modalWrapper.firstElementChild; document.body.appendChild(modalElement); Utils.applyTheme(); return modalElement; }, openGalleryManagementModal: function() { let idsToDelete = new Set(); const renderList = () => { const settings = Utils.getSettings(); const galleryIds = Object.keys(settings); const modal = document.getElementById(CONFIG.mgmtModalId); if (!modal) return; const description = modal.querySelector('.horus-modal-description'); const galleryListDiv = modal.querySelector('.horus-gallery-list'); const footer = modal.querySelector('.horus-modal-footer'); description.textContent = galleryIds.length > 0 ? '설정을 초기화할 갤러리를 선택해 주세요' : '말머리 차단이 설정된 갤러리가 없습니다'; if (galleryIds.length === 0) { galleryListDiv.innerHTML = ''; if (footer) footer.style.display = 'none'; return; } if (footer) footer.style.display = 'flex'; galleryListDiv.innerHTML = galleryIds.map(id => `<div class="horus-gallery-list-item ${idsToDelete.has(id) ? 'marked-for-deletion' : ''}" data-id="${id}"><span>${settings[id].name || '이름 정보 없음'}</span></div>`).join(''); galleryListDiv.querySelectorAll('.horus-gallery-list-item').forEach(item => { item.addEventListener('click', () => { const id = item.dataset.id; idsToDelete.has(id) ? idsToDelete.delete(id) : idsToDelete.add(id); item.classList.toggle('marked-for-deletion'); modal.querySelector('.horus-save-status').style.display = 'none'; }); }); }; const bodyHTML = `<p class="horus-modal-description"></p><div class="horus-gallery-list"></div>`; const footerHTML = `<button class="horus-btn horus-remove-all-btn">모두 선택</button><div class="horus-button-group"><span class="horus-save-status"></span><button class="horus-btn horus-confirm-btn">저장</button></div>`; const modal = this.createModal({ id: CONFIG.mgmtModalId, title: '갤러리 설정 관리', bodyHTML, footerHTML }); modal.querySelector('.horus-modal-close-btn').addEventListener('click', () => { if (idsToDelete.size > 0 && !confirm('저장되지 않은 변경사항이 있습니다\n계속하시겠습니까?')) return; modal.remove(); }); modal.querySelector('.horus-remove-all-btn').addEventListener('click', () => { const allIds = Object.keys(Utils.getSettings()); allIds.length === idsToDelete.size ? idsToDelete.clear() : allIds.forEach(id => idsToDelete.add(id)); renderList(); }); modal.querySelector('.horus-confirm-btn').addEventListener('click', () => { if (idsToDelete.size > 0) { const currentSettings = Utils.getSettings(); idsToDelete.forEach(id => delete currentSettings[id]); Utils.saveSettings(currentSettings); idsToDelete.clear(); Core.run(); renderList(); } modal.querySelector('.horus-save-status').textContent = '저장됨'; modal.querySelector('.horus-save-status').style.display = 'inline'; }); renderList(); }, openSettingsModal: async function() { const galleryId = Utils.getCurrentGalleryId(); if (!galleryId) { alert('갤러리 ID를 가져올 수 없습니다'); return; } const loadingModal = this.createModal({ id: CONFIG.mainModalId, title: 'HORUS 설정', bodyHTML: '<p style="padding: 40px; text-align: center;">말머리 목록을 불러오는 중입니다...</p>', footerHTML: '' }); try { const availableFlairs = await Utils.fetchAvailableFlairs(galleryId); loadingModal.remove(); const settings = Utils.getSettings(); const blockedFlairs = new Set(settings[galleryId]?.blocked || []); const flairItemsHTML = [...availableFlairs].map(flair => ` <label class="horus-list-item"> <input type="checkbox" value="${flair}" ${!blockedFlairs.has(flair) ? 'checked' : ''}> ${flair} </label> `).join(''); const labelText = availableFlairs.size > 0 ? '페이지에 노출할 말머리를 선택해 주세요' : '말머리가 존재하지 않습니다'; const bodyHTML = ` <button class="horus-btn horus-manage-btn horus-manage-btn-top">갤러리 설정 관리</button> <div class="horus-form-group"> <label class="horus-form-label normal-weight">${labelText}</label> <div class="horus-flair-list"> ${flairItemsHTML} </div> </div> `; const footerHTML = ` <button class="horus-btn horus-reset-btn">기본값</button> <div class="horus-button-group"> <span class="horus-save-status"></span> <button class="horus-btn horus-confirm-btn">저장</button> </div> `; const modal = this.createModal({ id: CONFIG.mainModalId, title: 'HORUS 설정', bodyHTML, footerHTML }); let isDirty = false; const saveStatus = modal.querySelector('.horus-save-status'); const markDirty = () => { isDirty = true; saveStatus.style.display = 'none'; }; modal.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.addEventListener('change', markDirty)); modal.querySelector('.horus-modal-close-btn').addEventListener('click', () => { if (isDirty && !confirm('저장되지 않은 변경사항이 있습니다\n계속하시겠습니까?')) return; modal.remove(); }); modal.querySelector('.horus-manage-btn').addEventListener('click', () => { modal.remove(); this.openGalleryManagementModal(); }); modal.querySelector('.horus-confirm-btn').addEventListener('click', () => { const flairsToBlock = [...modal.querySelectorAll('input:not(:checked)')].map(input => input.value); const currentSettings = Utils.getSettings(); const galleryName = Utils.getCurrentGalleryName() || settings[galleryId]?.name || galleryId; if (flairsToBlock.length > 0) { currentSettings[galleryId] = { name: galleryName, blocked: flairsToBlock }; } else { delete currentSettings[galleryId]; } Utils.saveSettings(currentSettings); isDirty = false; saveStatus.textContent = '저장됨'; saveStatus.style.display = 'inline'; Core.run(); }); modal.querySelector('.horus-reset-btn').addEventListener('click', () => { modal.querySelectorAll('input[type="checkbox"]:not(:checked)').forEach(input => input.checked = true); markDirty(); }); } catch (error) { console.error('[HORUS]', error); loadingModal.querySelector('.horus-modal-body').innerHTML = `<p style="padding: 40px; text-align: center;">말머리 목록을 불러오지 못했습니다.</p>`; } }, }; const Core = { filterPosts: function() { const settings = Utils.getSettings(); const galleryId = Utils.getCurrentGalleryId(); const blockedFlairs = settings[galleryId]?.blocked || []; if (blockedFlairs.length === 0) return; const posts = document.querySelectorAll('tr.ub-content'); posts.forEach(post => { const subjectCell = post.querySelector('td.gall_subject'); let isBlocked = false; if (subjectCell) { const displayedFlair = subjectCell.textContent.trim(); isBlocked = blockedFlairs.some(fullBlockedFlair => fullBlockedFlair.length <= 3 ? displayedFlair.startsWith(fullBlockedFlair) : displayedFlair.startsWith(fullBlockedFlair.substring(0, 3)) ); } post.style.display = isBlocked ? 'none' : ''; }); }, filterFlairDropdown: function() { const settings = Utils.getSettings(); const galleryId = Utils.getCurrentGalleryId(); if (!galleryId || !settings[galleryId]) { const allFlairItems = document.querySelectorAll('.list_array_option .inner ul li'); allFlairItems.forEach(item => { item.style.display = ''; }); return; }; const blockedFlairs = new Set(settings[galleryId]?.blocked || []); const flairItems = document.querySelectorAll('.list_array_option .inner ul li'); flairItems.forEach(item => { const flairAnchor = item.querySelector('a'); if (flairAnchor) { const flairText = flairAnchor.textContent.trim(); item.style.display = blockedFlairs.has(flairText) ? 'none' : ''; } }); }, handlePostView: function() { const settings = Utils.getSettings(); const galleryId = Utils.getCurrentGalleryId(); const blockedFlairs = new Set(settings[galleryId]?.blocked || []); if (blockedFlairs.size === 0) return; const flairElement = document.querySelector('.gall_writer.ub-writer'); if (!flairElement) return; const postFlair = flairElement.dataset.headtext; if (postFlair && blockedFlairs.has(postFlair)) { document.body.style.overflow = 'hidden'; const notice = document.createElement('div'); notice.innerHTML = ` <div style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); z-index: 10002; display: flex; justify-content: center; align-items: center; color: white; font-size: 20px; font-family: 'Malgun Gothic', sans-serif;"> 차단된 말머리의 게시글입니다. 2초 후 목록으로 돌아갑니다. </div> `; document.body.appendChild(notice); setTimeout(() => { history.back(); }, 2000); } }, run: function() { if (window.location.href.includes('/board/view')) { this.handlePostView(); } this.filterPosts(); this.filterFlairDropdown(); }, init: function() { GM_addStyle(` :root { --font-main: 'Malgun Gothic', sans-serif; --font-size-base: 13px; --font-size-header: 16px; --color-bg: #fff; --color-border: #ddd; --color-border-light: #e9e9e9; --color-border-dark: #ccc; --color-text-primary: #333; --color-text-secondary: #777; --color-text-inverse: #fff; --color-btn-confirm-bg: #333; --color-btn-confirm-text: #fff; --color-btn-cancel-bg: #fff; --color-btn-cancel-text: #333; --color-input-bg: #fff; --color-input-text: #555; --color-row-hover: #f5f5f5; } body.${CONFIG.darkThemeClass} { --color-bg: #1f1f1f; --color-border: #4a4b4f; --color-border-light: #444549; --color-border-dark: #555; --color-text-primary: #ccc; --color-text-secondary: #aaa; --color-btn-confirm-bg: #eee; --color-btn-confirm-text: #333; --color-btn-cancel-bg: #444; --color-btn-cancel-text: #e8e8e8; --color-input-bg: #1f1f1f; --color-input-text: #ccc; --color-row-hover: #2a2b2d; } .${CONFIG.mainModalId}, .${CONFIG.mgmtModalId} { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10001; display: block; font-family: var(--font-main); background-color: var(--color-bg); border: 1px solid var(--color-border); color: var(--color-text-primary); border-radius: 0; box-shadow: 0 5px 25px rgba(0,0,0,.2); transition: background-color .3s, color .3s, border-color .3s; } .${CONFIG.mainModalId} { width: 400px; } .${CONFIG.mgmtModalId} { width: 500px; } .horus-modal-header { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; border-bottom: 1px solid var(--color-border-light); font-size: var(--font-size-header); font-weight: 700; } .horus-modal-close-btn { background: transparent; border: none; font-size: 24px; font-weight: 700; color: var(--color-text-secondary); cursor: pointer; transition: color .2s; } .horus-modal-close-btn:hover { color: var(--color-text-primary); } .horus-modal-body { padding: 20px; padding-bottom: 10px; } .horus-modal-footer { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; gap: 10px; } .horus-form-group { margin-bottom: 10px; } .horus-form-label { display: block; font-weight: 700; font-size: var(--font-size-base); color: var(--color-text-primary); margin-bottom: 8px; } .horus-form-label.normal-weight { font-weight: normal; } .horus-flair-list { display: block; max-height: 330px; overflow-y: auto; } .horus-list-item { display: flex; align-items: center; cursor: pointer; font-size: var(--font-size-base); padding: 8px 12px; border-bottom: 1px solid #f0f0f0; transition: background-color .2s; } body.${CONFIG.darkThemeClass} .horus-list-item { border-bottom-color: var(--color-border-light); } .horus-list-item:last-child { border-bottom: none; } .horus-list-item:hover { background-color: var(--color-row-hover); } .horus-list-item input[type="checkbox"] { margin-right: 10px; transform: scale(1.2); cursor: pointer; } .horus-btn { padding: 8px 25px; border: 1px solid var(--color-border-dark); border-radius: 4px; cursor: pointer; font-weight: 700; font-size: var(--font-size-base); text-align: center; transition: filter .2s, opacity .2s; } .horus-btn:hover:not(:disabled) { filter: brightness(.9); } .horus-confirm-btn { background-color: var(--color-btn-confirm-bg); color: var(--color-btn-confirm-text); border-color: var(--color-btn-confirm-bg); } body.${CONFIG.darkThemeClass} .horus-confirm-btn:hover:not(:disabled) { filter: brightness(.85); } .horus-reset-btn, .horus-manage-btn, .horus-remove-all-btn { background-color: var(--color-btn-cancel-bg); color: var(--color-btn-cancel-text); border-color: var(--color-border-dark); } body.${CONFIG.darkThemeClass} .horus-reset-btn:hover:not(:disabled), body.${CONFIG.darkThemeClass} .horus-manage-btn:hover:not(:disabled), body.${CONFIG.darkThemeClass} .horus-remove-all-btn:hover:not(:disabled) { filter: brightness(.8); } .horus-manage-btn-top { width: 100%; margin-bottom: 20px; } .horus-save-status { color: var(--color-text-secondary); font-size: var(--font-size-base); margin-right: auto; display: none; } .horus-button-group { display: flex; align-items: center; gap: 10px; } .horus-modal-description { font-size: var(--font-size-base); color: var(--color-text-secondary); margin-bottom: 15px; text-align: left; } .horus-gallery-list { display: flex; flex-wrap: wrap; align-content: flex-start; gap: 8px; max-height: 340px; overflow-y: auto; } .horus-gallery-list-item { display: inline-flex; align-items: center; background-color: var(--color-row-hover); border: 1px solid var(--color-border-light); border-radius: 16px; padding: 5px 12px; font-size: var(--font-size-base); color: var(--color-text-primary); cursor: pointer; transition: background-color .2s, border-color .2s, opacity .2s; } .horus-gallery-list-item:hover { background-color: var(--color-border-light); } .horus-gallery-list-item.marked-for-deletion { opacity: 0.6; text-decoration: line-through; } `); GM_registerMenuCommand('말머리 설정', UI.openSettingsModal.bind(UI)); const themeObserver = new MutationObserver(Utils.applyTheme); themeObserver.observe(document.head, { childList: true }); const mainObserver = new MutationObserver((mutations) => { const listContainer = document.querySelector('.list_wrap, .gall_list_wrap'); if (listContainer && !listContainer.hasAttribute('data-horus-observed')) { listContainer.setAttribute('data-horus-observed', 'true'); this.filterPosts(); const postListObserver = new MutationObserver(() => this.filterPosts()); postListObserver.observe(listContainer, { childList: true, subtree: true }); } }); mainObserver.observe(document.body, { childList: true, subtree: true }); window.addEventListener('load', () => { Utils.applyTheme(); this.run(); }); } }; Core.init(); })();