// ==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와 서브 채널명 넣고 폴링 주기 설정하면
주기적으로 해당 게시글 확인해서 멘션 있으면 알림 띄워줌