kone
코딩해요
코딩해요

댓글 알림 시스템

05/17/2025, 19:25:14
JS
5345 views · 3 likes
// ==UserScript==
// @name         KoneGG 댓글 알림 시스템
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  kone.gg 사이트에서 특정 게시물의 댓글을 모니터링하고 현재 사용자명이 언급되면 알림을 표시합니다
// @author       Your Name
// @match        https://kone.gg/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    // CSS 스타일 추가
    GM_addStyle(`
        .kone-notifier-container {
            position: fixed;
            top: 60px;
            right: 20px;
            z-index: 9999;
            display: flex;
            flex-direction: column;
            gap: 8px;
            font-family: 'Pretendard', sans-serif;
        }

        .kone-notifier-icon-wrapper {
            position: relative;
            display: flex;
            align-items: center;
        }

        .kone-notifier-bell-count {
            position: absolute;
            top: 1px;
            right: 1px;
            background-color: #e11d48;
            color: white;
            border-radius: 50%;
            width: 18px;
            height: 18px;
            font-size: 10px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-weight: bold;
        }

        .kone-notifier-notification {
            padding: 12px;
            border-radius: 8px;
            background-color: white;
            border: 1px solid #e5e7eb;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
            width: 300px;
            animation: slide-in 0.3s ease;
            display: flex;
            flex-direction: column;
            gap: 8px;
        }

        .dark .kone-notifier-notification {
            background-color: #27272a;
            border-color: #3f3f46;
            color: #e4e4e7;
        }

        .kone-notifier-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            font-weight: 600;
        }

        .kone-notifier-close {
            cursor: pointer;
            opacity: 0.6;
        }

        .kone-notifier-close:hover {
            opacity: 1;
        }

        .kone-notifier-body {
            font-size: 14px;
            word-break: break-word;
        }

        .kone-notifier-footer {
            display: flex;
            justify-content: space-between;
            font-size: 12px;
            color: #71717a;
        }

        .kone-notifier-link {
            color: #3b82f6;
            text-decoration: none;
            cursor: pointer;
        }

        .kone-notifier-link:hover {
            text-decoration: underline;
        }

        .kone-notifier-list {
            position: fixed;
            top: 60px;
            right: 20px;
            z-index: 9999;
            border-radius: 8px;
            background-color: white;
            border: 1px solid #e5e7eb;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
            width: 350px;
            max-height: 500px;
            overflow-y: auto;
            display: none;
            font-family: 'Pretendard', sans-serif;
        }

        .dark .kone-notifier-list {
            background-color: #27272a;
            border-color: #3f3f46;
            color: #e4e4e7;
        }

        .kone-notifier-list-header {
            padding: 12px 16px;
            border-bottom: 1px solid #e5e7eb;
            font-weight: 600;
            display: flex;
            justify-content: space-between;
            align-items: center;
            position: sticky;
            top: 0;
            background-color: white;
            z-index: 1;
        }

        .dark .kone-notifier-list-header {
            border-color: #3f3f46;
            background-color: #27272a;
        }

        .kone-notifier-list-content {
            max-height: 450px;
            overflow-y: auto;
        }

        .kone-notifier-list-item {
            padding: 12px 16px;
            border-bottom: 1px solid #e5e7eb;
            cursor: pointer;
        }

        .dark .kone-notifier-list-item {
            border-color: #3f3f46;
        }

        .kone-notifier-list-item:hover {
            background-color: #f9fafb;
        }

        .dark .kone-notifier-list-item:hover {
            background-color: #3f3f46;
        }

        .kone-notifier-list-item-header {
            display: flex;
            justify-content: space-between;
            margin-bottom: 4px;
        }

        .kone-notifier-list-item-title {
            font-weight: 600;
            font-size: 14px;
            margin-bottom: 4px;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }

        .kone-notifier-list-item-time {
            font-size: 12px;
            color: #71717a;
        }

        .kone-notifier-list-item-comment {
            font-size: 13px;
            margin-bottom: 4px;
            color: #52525b;
            display: -webkit-box;
            -webkit-line-clamp: 2;
            -webkit-box-orient: vertical;
            overflow: hidden;
        }

        .dark .kone-notifier-list-item-comment {
            color: #a1a1aa;
        }

        .kone-notifier-list-empty {
            padding: 16px;
            text-align: center;
            color: #71717a;
            font-size: 14px;
        }

        .kone-notifier-clear-all {
            background: none;
            border: none;
            color: #71717a;
            cursor: pointer;
            font-size: 12px;
            padding: 0;
        }

        .kone-notifier-clear-all:hover {
            color: #ef4444;
        }

        .kone-notifier-settings {
            position: fixed;
            bottom: 20px;
            right: 20px;
            background-color: white;
            border: 1px solid #e5e7eb;
            border-radius: 8px;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
            padding: 16px;
            width: 300px;
            z-index: 9999;
            font-family: 'Pretendard', sans-serif;
            display: none;
        }

        .dark .kone-notifier-settings {
            background-color: #27272a;
            border-color: #3f3f46;
            color: #e4e4e7;
        }

        .kone-notifier-settings-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 12px;
            font-weight: 600;
        }

        .kone-notifier-settings-content {
            display: flex;
            flex-direction: column;
            gap: 12px;
        }

        .kone-notifier-settings-row {
            display: flex;
            flex-direction: column;
            gap: 4px;
        }

        .kone-notifier-settings-label {
            font-size: 13px;
            font-weight: 500;
        }

        .kone-notifier-settings-input {
            padding: 8px;
            border-radius: 6px;
            border: 1px solid #e5e7eb;
            background-color: white;
            font-size: 13px;
        }

        .dark .kone-notifier-settings-input {
            background-color: #3f3f46;
            border-color: #52525b;
            color: #e4e4e7;
        }

        .kone-notifier-settings-button {
            background-color: #3b82f6;
            color: white;
            border: none;
            border-radius: 6px;
            padding: 8px 12px;
            font-size: 13px;
            cursor: pointer;
            font-weight: 500;
            margin-top: 6px;
        }

        .kone-notifier-settings-button:hover {
            background-color: #2563eb;
        }

        .kone-notifier-settings-button:disabled {
            background-color: #94a3b8;
            cursor: not-allowed;
        }

        .kone-notifier-settings-link {
            font-size: 13px;
            color: #3b82f6;
            text-decoration: none;
            cursor: pointer;
            align-self: flex-end;
        }

        .kone-notifier-settings-link:hover {
            text-decoration: underline;
        }

        .kone-notifier-button {
            background: none;
            border: none;
            cursor: pointer;
            padding: 0;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        @keyframes slide-in {
            from {
                transform: translateX(100%);
                opacity: 0;
            }
            to {
                transform: translateX(0);
                opacity: 1;
            }
        }

        @keyframes fade-in {
            from {
                opacity: 0;
            }
            to {
                opacity: 1;
            }
        }
    `);

    // 전역 변수 및 상태
    let username = null;
    let monitoredArticles = GM_getValue('monitoredArticles', []);
    let notifications = GM_getValue('notifications', []);
    let isListVisible = false;
    let isSettingsVisible = false;
    let darkMode = document.documentElement.classList.contains('dark');
    let checkIntervalId = null;
    let checkInterval = GM_getValue('checkInterval', 30); // 기본값 30초

    // DOM 요소 생성 함수
    function createElements() {
        // 헤더 버튼 컨테이너 찾기
        const headerButtonsContainer = document.querySelector('header .flex.items-center.gap-2');

        // 알림 컨테이너
        const container = document.createElement('div');
        container.className = 'kone-notifier-container';
        document.body.appendChild(container);

        // 알림 종 아이콘 (서브 개설 버튼 옆에 추가)
        const bellContainer = document.createElement('div');
        bellContainer.className = 'kone-notifier-icon-wrapper';
        bellContainer.innerHTML = `
            <button data-slot="button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:text-accent-foreground size-9 text-gray-600 hover:bg-gray-100 dark:text-zinc-300 dark:hover:bg-zinc-700 cursor-pointer relative">
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bell">
                    <path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"></path>
                    <path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"></path>
                </svg>
                <div class="kone-notifier-bell-count" style="display: none;">0</div>
            </button>
        `;

        // 설정 버튼 (벨 아이콘 옆에 추가)
        const settingsButton = document.createElement('div');
        settingsButton.className = 'kone-notifier-icon-wrapper';
        settingsButton.innerHTML = `
            <button data-slot="button" class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:text-accent-foreground size-9 text-gray-600 hover:bg-gray-100 dark:text-zinc-300 dark:hover:bg-zinc-700 cursor-pointer">
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-settings">
                    <path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path>
                    <circle cx="12" cy="12" r="3"></circle>
                </svg>
            </button>
        `;

        // 헤더 버튼 컨테이너에 추가
        if (headerButtonsContainer) {
            // 서브 개설 버튼 다음에 우리 버튼들 추가
            headerButtonsContainer.appendChild(bellContainer);
            headerButtonsContainer.appendChild(settingsButton);
        } else {
            // 컨테이너를 찾지 못한 경우 기존 방식으로 추가
            document.body.appendChild(bellContainer);
            document.body.appendChild(settingsButton);
        }

        // 알림 목록
        const notificationList = document.createElement('div');
        notificationList.className = 'kone-notifier-list';
        notificationList.innerHTML = `
            <div class="kone-notifier-list-header">
                알림 목록
                <button class="kone-notifier-clear-all">모두 지우기</button>
            </div>
            <div class="kone-notifier-list-content"></div>
        `;
        document.body.appendChild(notificationList);

        // 설정 패널
        const settingsPanel = document.createElement('div');
        settingsPanel.className = 'kone-notifier-settings';
        settingsPanel.innerHTML = `
            <div class="kone-notifier-settings-header">
                알림 설정
                <div class="kone-notifier-close">
                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x">
                        <path d="M18 6 6 18"></path>
                        <path d="m6 6 12 12"></path>
                    </svg>
                </div>
            </div>
            <div class="kone-notifier-settings-content">
                <div class="kone-notifier-settings-row">
                    <label class="kone-notifier-settings-label">서브명</label>
                    <input type="text" class="kone-notifier-settings-input" id="sub-name-input" placeholder="예: programming">
                </div>
                <div class="kone-notifier-settings-row">
                    <label class="kone-notifier-settings-label">게시글 ID</label>
                    <input type="text" class="kone-notifier-settings-input" id="article-id-input" placeholder="예: cBkjsueQ_7SWT6w6TX35yb">
                </div>
                <div class="kone-notifier-settings-row">
                    <label class="kone-notifier-settings-label">확인 주기 (초)</label>
                    <input type="number" class="kone-notifier-settings-input" id="check-interval-input" value="${checkInterval}" min="10">
                </div>
                <button class="kone-notifier-settings-button" id="save-settings">저장</button>
                <a class="kone-notifier-settings-link" id="view-monitored">모니터링 중인 게시글 보기</a>
            </div>
        `;
        document.body.appendChild(settingsPanel);

        return {
            container,
            bellContainer,
            notificationList,
            settingsPanel,
            settingsButton
        };
    }

    // 현재 사용자명 가져오기
    function getCurrentUsername() {
        // 프로필 이미지 관련 요소 찾기
        const profileButton = document.querySelector('button[aria-haspopup="menu"]');
        if (profileButton) {
            // 이미지 alt에서 사용자명 추출 시도
            const profileImg = profileButton.querySelector('img');
            if (profileImg && profileImg.alt) {
                return profileImg.alt;
            }
        }

        // 로컬 스토리지에서 저장된 사용자명 확인
        if (localStorage.getItem('username')) {
            return localStorage.getItem('username');
        }

        // 아직 추출하지 못했다면 사용자에게 입력 요청
        const inputUsername = prompt('사용자명을 입력해주세요:');
        if (inputUsername) {
            localStorage.setItem('username', inputUsername);
            return inputUsername;
        }

        return null;
    }

    // 게시글 댓글 가져오기 함수
    async function fetchArticleComments(article) {
        try {
            const response = await fetch(`https://kone.gg/s/${article.subName}/${article.id}`);
            const html = await response.text();
            return parseComments(html, article);
        } catch (error) {
            console.error('댓글을 가져오는 중 오류가 발생했습니다:', error);
            return [];
        }
    }

    // HTML에서 댓글 추출
    function parseComments(html, article) {
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, 'text/html');

        // 게시글 제목 추출
        const articleTitle = doc.querySelector('h1.text-2xl.font-bold')?.textContent || '제목 없음';

        // 댓글 컨테이너 찾기
        const commentContainers = doc.querySelectorAll('div.relative.px-4.py-2');
        const comments = [];

        commentContainers.forEach(container => {
            // 댓글 ID 추출
            const commentIdSpan = container.querySelector('span[id^="c_"]');
            if (!commentIdSpan) return;

            const commentId = commentIdSpan.id.replace('c_', '');

            // 댓글 작성자 정보
            const authorLink = container.querySelector('a.flex.items-center.gap-1.text-sm.font-medium');
            const author = authorLink ? authorLink.textContent.trim() : null;

            // 댓글 작성 시간
            const timeSpan = container.querySelector('div.flex.gap-1.text-xs.text-zinc-500 span');
            const createdAt = timeSpan ? timeSpan.textContent.trim() : null;

            // 댓글 내용
            const contentP = container.querySelector('p.text-sm.max-w-xl.whitespace-pre-wrap');
            const content = contentP ? contentP.textContent.trim() : null;

            // 삭제된 댓글 확인
            const isDeleted = container.querySelector('p.text-sm.opacity-50')?.textContent.includes('삭제된 댓글입니다') || false;

            if (!isDeleted && content) {
                comments.push({
                    id: commentId,
                    articleId: article.id,
                    subName: article.subName,
                    articleTitle,
                    author,
                    content,
                    createdAt,
                    timestamp: new Date().getTime()
                });
            }
        });

        return {
            articleId: article.id,
            subName: article.subName,
            articleTitle,
            comments
        };
    }

    // 알림 표시 함수
    function showNotification(notification) {
        const { container } = elements;

        const notificationElement = document.createElement('div');
        notificationElement.className = 'kone-notifier-notification';
        notificationElement.innerHTML = `
            <div class="kone-notifier-header">
                <div>새 댓글 알림</div>
                <div class="kone-notifier-close">
                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x">
                        <path d="M18 6 6 18"></path>
                        <path d="m6 6 12 12"></path>
                    </svg>
                </div>
            </div>
            <div class="kone-notifier-body">
                <strong>${notification.author}</strong>님이 댓글에서 당신을 언급했습니다:
                <p>${notification.content}</p>
            </div>
            <div class="kone-notifier-footer">
                <div>${notification.createdAt || '방금 전'}</div>
                <a href="/s/${notification.subName}/${notification.articleId}#c_${notification.commentId}" class="kone-notifier-link">게시글로 이동</a>
            </div>
        `;

        container.appendChild(notificationElement);

        // 닫기 버튼 이벤트
        const closeButton = notificationElement.querySelector('.kone-notifier-close');
        closeButton.addEventListener('click', () => {
            container.removeChild(notificationElement);
        });

        // 자동으로 5초 후 사라지게 설정
        setTimeout(() => {
            if (container.contains(notificationElement)) {
                container.removeChild(notificationElement);
            }
        }, 5000);
    }

    // 알림 목록 업데이트
    function updateNotificationList() {
        const notificationListContent = elements.notificationList.querySelector('.kone-notifier-list-content');
        notificationListContent.innerHTML = '';

        if (notifications.length === 0) {
            notificationListContent.innerHTML = '<div class="kone-notifier-list-empty">알림이 없습니다</div>';
            return;
        }

        // 시간순 정렬
        const sortedNotifications = [...notifications].sort((a, b) => b.timestamp - a.timestamp);

        sortedNotifications.forEach(notification => {
            const notificationItem = document.createElement('div');
            notificationItem.className = 'kone-notifier-list-item';
            notificationItem.innerHTML = `
                <div class="kone-notifier-list-item-header">
                    <div class="kone-notifier-list-item-title">${notification.articleTitle}</div>
                    <div class="kone-notifier-list-item-time">${formatDate(notification.timestamp)}</div>
                </div>
                <div class="kone-notifier-list-item-comment">
                    <strong>${notification.author}</strong>: ${notification.content}
                </div>
            `;

            notificationItem.addEventListener('click', () => {
                window.location.href = `/s/${notification.subName}/${notification.articleId}#c_${notification.commentId}`;
            });

            notificationListContent.appendChild(notificationItem);
        });
    }

    // 날짜 포맷 함수
    function formatDate(timestamp) {
        const date = new Date(timestamp);
        const now = new Date();
        const diffMs = now - date;
        const diffSec = Math.floor(diffMs / 1000);
        const diffMin = Math.floor(diffSec / 60);
        const diffHour = Math.floor(diffMin / 60);
        const diffDay = Math.floor(diffHour / 24);

        if (diffSec < 60) {
            return '방금 전';
        } else if (diffMin < 60) {
            return `${diffMin}분 전`;
        } else if (diffHour < 24) {
            return `${diffHour}시간 전`;
        } else if (diffDay < 7) {
            return `${diffDay}일 전`;
        } else {
            return `${date.getFullYear()}.${date.getMonth() + 1}.${date.getDate()}`;
        }
    }

    // 알림 카운트 업데이트
    function updateNotificationCount() {
        const bellCount = elements.bellContainer.querySelector('.kone-notifier-bell-count');
        if (notifications.length > 0) {
            bellCount.textContent = notifications.length > 99 ? '99+' : notifications.length;
            bellCount.style.display = 'flex';
        } else {
            bellCount.style.display = 'none';
        }
    }

    // 모니터링 중인 게시글 목록 표시
    function showMonitoredArticles() {
        // 기존 목록 삭제
        const existingList = document.getElementById('monitored-articles-list');
        if (existingList) {
            existingList.remove();
        }

        // 새 목록 생성
        const list = document.createElement('div');
        list.id = 'monitored-articles-list';
        list.className = 'kone-notifier-settings';
        list.style.display = 'block';
        list.style.bottom = '80px';
        list.innerHTML = `
            <div class="kone-notifier-settings-header">
                모니터링 중인 게시글
                <div class="kone-notifier-close">
                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x">
                        <path d="M18 6 6 18"></path>
                        <path d="m6 6 12 12"></path>
                    </svg>
                </div>
            </div>
            <div class="kone-notifier-settings-content">
                ${monitoredArticles.length === 0 ?
                    '<div class="kone-notifier-list-empty">모니터링 중인 게시글이 없습니다</div>' :
                    '<div class="kone-notifier-settings-row" style="max-height: 200px; overflow-y: auto;">'}
                    ${monitoredArticles.map(article => `
                        <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
                            <div style="max-width: 230px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
                                <a href="/s/${article.subName}/${article.id}" class="kone-notifier-link">${article.subName} / ${article.id}</a>
                            </div>
                            <button class="kone-notifier-button remove-article" data-id="${article.id}" data-sub="${article.subName}">
                                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash-2">
                                    <path d="M3 6h18"></path>
                                    <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
                                    <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
                                    <line x1="10" x2="10" y1="11" y2="17"></line>
                                    <line x1="14" x2="14" y1="11" y2="17"></line>
                                </svg>
                            </button>
                        </div>
                    `).join('')}
                ${monitoredArticles.length === 0 ? '' : '</div>'}
            </div>
        `;

        document.body.appendChild(list);

        // 닫기 버튼 이벤트
        list.querySelector('.kone-notifier-close').addEventListener('click', () => {
            list.remove();
        });

        // 게시글 삭제 버튼 이벤트
        list.querySelectorAll('.remove-article').forEach(button => {
            button.addEventListener('click', (e) => {
                const id = e.currentTarget.dataset.id;
                const sub = e.currentTarget.dataset.sub;
                monitoredArticles = monitoredArticles.filter(article => !(article.id === id && article.subName === sub));
                GM_setValue('monitoredArticles', monitoredArticles);
                showMonitoredArticles(); // 목록 갱신
            });
        });
    }

    // 댓글 모니터링 함수
    async function checkComments() {
        if (!username || monitoredArticles.length === 0) return;

        for (const article of monitoredArticles) {
            try {
                const data = await fetchArticleComments(article);
                if (!data || !data.comments) continue;

                // 기존 알림에 있는 댓글 ID 목록
                const existingCommentIds = notifications.map(n => n.commentId);

                // 새 댓글 중 사용자명이 언급된 것 찾기
                const newNotifications = data.comments.filter(comment => {
                    // 이미 알림이 있는 댓글은 제외
                    if (existingCommentIds.includes(comment.id)) return false;

                    // 내용에 사용자명이 있는지 확인 (대소문자 구분 없이)
                    return comment.content.toLowerCase().includes(username.toLowerCase());
                }).map(comment => ({
                    commentId: comment.id,
                    articleId: data.articleId,
                    subName: data.subName,
                    articleTitle: data.articleTitle,
                    author: comment.author,
                    content: comment.content,
                    createdAt: comment.createdAt,
                    timestamp: comment.timestamp || new Date().getTime()
                }));

                // 새 알림이 있으면 표시 및 저장
                if (newNotifications.length > 0) {
                    // 알림 표시
                    newNotifications.forEach(notification => {
                        showNotification(notification);
                    });

                    // 알림 목록에 추가
                    notifications = [...notifications, ...newNotifications];
                    GM_setValue('notifications', notifications);

                    // 알림 카운트 업데이트
                    updateNotificationCount();
                }
            } catch (error) {
                console.error(`게시글 ${article.subName}/${article.id} 댓글 확인 중 오류 발생:`, error);
            }
        }
    }

    // 이벤트 핸들러 설정
    function setupEventHandlers() {
        const { bellContainer, notificationList, settingsPanel, settingsButton } = elements;

        // 알림 벨 클릭 이벤트
        bellContainer.querySelector('button').addEventListener('click', () => {
            // 알림 목록 토글
            isListVisible = !isListVisible;

            if (isListVisible) {
                updateNotificationList();
                notificationList.style.display = 'block';

                // 설정 패널이 열려있으면 닫기
                if (isSettingsVisible) {
                    settingsPanel.style.display = 'none';
                    isSettingsVisible = false;
                }
            } else {
                notificationList.style.display = 'none';
            }
        });

        // 설정 버튼 클릭 이벤트
        settingsButton.querySelector('button').addEventListener('click', () => {
            // 설정 패널 토글
            isSettingsVisible = !isSettingsVisible;

            if (isSettingsVisible) {
                settingsPanel.style.display = 'block';

                // 알림 목록이 열려있으면 닫기
                if (isListVisible) {
                    notificationList.style.display = 'none';
                    isListVisible = false;
                }
            } else {
                settingsPanel.style.display = 'none';
            }
        });

        // 설정 저장 버튼 클릭 이벤트
        document.getElementById('save-settings').addEventListener('click', () => {
            const subNameInput = document.getElementById('sub-name-input');
            const articleIdInput = document.getElementById('article-id-input');
            const checkIntervalInput = document.getElementById('check-interval-input');

            const subName = subNameInput.value.trim();
            const articleId = articleIdInput.value.trim();
            const interval = parseInt(checkIntervalInput.value, 10);

            if (subName && articleId && interval >= 10) {
                // 중복 확인 (서브명+게시글ID 조합으로 확인)
                if (!monitoredArticles.some(article => article.id === articleId && article.subName === subName)) {
                    monitoredArticles.push({ id: articleId, subName: subName });
                    GM_setValue('monitoredArticles', monitoredArticles);
                }

                // 주기 저장
                checkInterval = interval;
                GM_setValue('checkInterval', checkInterval);

                // 모니터링 재시작
                if (checkIntervalId) {
                    clearInterval(checkIntervalId);
                }
                checkIntervalId = setInterval(checkComments, checkInterval * 1000);

                // 입력 필드 초기화
                subNameInput.value = '';
                articleIdInput.value = '';

                // 알림
                alert('설정이 저장되었습니다.');
            } else {
                if (!subName) {
                    alert('서브명을 입력해주세요.');
                } else if (!articleId) {
                    alert('게시글 ID를 입력해주세요.');
                } else if (interval < 10) {
                    alert('확인 주기는 최소 10초 이상이어야 합니다.');
                }
            }
        });

        // 모니터링 중인 게시글 보기 버튼 클릭 이벤트
        document.getElementById('view-monitored').addEventListener('click', (e) => {
            e.preventDefault();
            showMonitoredArticles();
        });

        // 알림 목록 모두 지우기 버튼 클릭 이벤트
        const clearAllButton = notificationList.querySelector('.kone-notifier-clear-all');
        clearAllButton.addEventListener('click', () => {
            notifications = [];
            GM_setValue('notifications', notifications);
            updateNotificationList();
            updateNotificationCount();
        });

        // 설정 패널 닫기 버튼 이벤트
        settingsPanel.querySelector('.kone-notifier-close').addEventListener('click', () => {
            settingsPanel.style.display = 'none';
            isSettingsVisible = false;
        });

        // 문서 클릭 이벤트 (목록/설정 패널 외부 클릭 시 닫기)
        document.addEventListener('click', (e) => {
            if (isListVisible &&
                !notificationList.contains(e.target) &&
                !bellContainer.contains(e.target)) {
                notificationList.style.display = 'none';
                isListVisible = false;
            }

            if (isSettingsVisible &&
                !settingsPanel.contains(e.target) &&
                !settingsButton.contains(e.target) &&
                !e.target.closest('#monitored-articles-list')) {
                settingsPanel.style.display = 'none';
                isSettingsVisible = false;
            }
        });
    }

    // 초기화 함수
    function init() {
        // DOM 요소 생성
        const elements = createElements();
        window.elements = elements;

        // 사용자명 가져오기
        username = getCurrentUsername();

        // 이벤트 핸들러 설정
        setupEventHandlers();

        // 알림 카운트 초기화
        updateNotificationCount();

        // 주기적 확인 시작
        if (username && monitoredArticles.length > 0) {
            checkIntervalId = setInterval(checkComments, checkInterval * 1000);
            // 페이지 로드 시 즉시 한 번 확인
            checkComments();
        }

        // 다크 모드 감지 및 대응
        const observer = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                if (mutation.attributeName === 'class') {
                    darkMode = document.documentElement.classList.contains('dark');
                }
            });
        });

        observer.observe(document.documentElement, { attributes: true });

        console.log('KoneGG 댓글 알림 시스템이 초기화되었습니다.');
    }

    // 페이지 로드 완료 후 초기화
    if (document.readyState === 'complete') {
        init();
    } else {
        window.addEventListener('load', init);
    }
})();




대충 만들었음

확장 활성화하고 사용자 아이디 먼저 입력한뒤에

게시글 ID와 서브 채널명 넣고 폴링 주기 설정하면

주기적으로 해당 게시글 확인해서 멘션 있으면 알림 띄워줌


1



3
5 comments
05/17/25 Edited 05/17/25

Deleted comment.

05/17/25 Edited 05/17/25

Deleted comment.

05/17/25 Edited 05/17/25

Deleted comment.

05/17/25 Edited 05/17/25

Deleted comment.

05/17/25 Edited 05/17/25

Deleted comment.

XSS 2
🔐
comi-soft
05/22/25 4980 0
간단한 대사 넘기기
JS
comi-soft
05/22/25 4995 2
이거슨 코딩인가 아닌가
기타언어
LTS
05/21/25 3937 0
XSS
🔐
55555
05/21/25 2628 1
404 Not Found
일반
binary는 호남선
05/21/25 4337 0
shell도 언어야 언어
기타언어
ubuntu
05/20/25 3956 0
안녕하세요 코딩에 ㅋ의 한 획도 모르는 뉴비입니다.
일반
yoont
05/20/25 2944 0
배울래오
일반
firne2300
05/20/25 1803 1
idrive e2 쓰는 사람 있음?
일반
Ghost
05/20/25 1808 0
html 삽입 테스트
🔐
55555
05/20/25 2227 0
여기 아직 여러 잔버그 있나보네
일반
로1슈
05/19/25 3162 0
오 댓글 API 신기하네
일반
Ghost
05/19/25 3864 0
댓글 더 불러오기가 생겼네
일반
Ghost
05/19/25 2484 0
미니 pc가 뒤져버림
일반
Ghost
05/19/25 2264 0
근데 사이트가 진짜 딱 돌아갈 수준의 기능만 만들어두고 오픈했나보네
일반
ㅇㅇ
05/19/25 2901 0
ㅎㅇ AI 개발자임
일반
ㅇㅇ
05/19/25 3374 0
안녕하세요? 코딩 1도 모르는 백수 질문글 입니다!
일반
shashasha
05/18/25 4909 0
한명이 여러 서브 못열게 막았네
일반
Ghost
05/18/25 2057 0
<script>alert('XSS');</script>
🔐
Witch
05/18/25 5838 0
바이브코딩 말고 노예코딩은 없나요?
일반
이다알
05/18/25 1756 0
앱이나 웹사이트 API 요청을 리버싱 해서 swagger로 나타내게 해주는 도구
🔐
Ghost
05/18/25 2660 0
HTML로 코딩해요. 같은 글이 없네
일반
LTS
05/18/25 2396 0
네이버로 가입해서 야짤올리는애들은 뭔깡임?
일반
alalllall
05/18/25 4422 0
전공자/비전공자 비율
일반
보성녹차드링커
05/18/25 3304 0
Kone 프론트 잠깐 뜯어봤는데
일반
binary는 호남선
05/18/25 6248 4
제목/내용/작성자명 검색 시스템
JS
Ghost
05/18/25 4788 2
댓글 개선 시스템
JS
Ghost
05/17/25 2972 4
우리 학과에는 무서운 괴담이 하나 있다
일반
tommy5801
05/17/25 2728 1
댓글 알림 시스템
JS
Ghost
05/17/25 5345 3
API 재업
PY
Ghost
05/17/25 4067 2