// ==UserScript== // @name ArcaLive PostSidebar // @namespace ArcaLive PostSidebar // @version 1.2 // @description 이 스크립트는 Arcalive 웹 페이지에서 우측에 인접 게시글 패널을 생성합니다. 사용자 익명화, 키보드 단축키 추가 등의 기능을 포함하며, 특정 요소를 숨깁니다. // @author Hess // @match https://arca.live/* // @run-at document-idle // @icon https://i.namu.wiki/i/uDNhs7D-YhK4rVCOjzk6NLNzbC58cvwSpMHw-b0mG8XGgPA1uxFI1JqUFBE1gLHvSWhq1LNrXuwchq6TPh1WIg.svg // @grant GM_getValue // @grant GM_setValue // @license MIT // @downloadURL https://update.greasyfork.cloud/scripts/529392/ArcaLive%20PostSidebar.user.js // @updateURL https://update.greasyfork.cloud/scripts/529392/ArcaLive%20PostSidebar.meta.js // ==/UserScript== // https://greasyfork.org/ko/scripts/529392-arcalive-postsidebar-%EB%B0%B0%ED%8F%AC%EC%9A%A9 // 최우선 목표: 함수화하기 // iframe 2개만 불러오게 만들기 iframe 로드 완료 감지 // 로딩 시간 최소화 // 더 압축할 거리 찾기 // 더 넣을 기능 찾기 // 단축키 설명서? // 저장소 내보내기 / 가져오기 // 저번에 안본 (저장소에 카운트 안된) 댓글은 (최근 방문일을 기준으로) 따로 표시 (완, 맨 밑은 파란색이라 덮어짐) // 검색창 위에도 만들기 (완, 목록 페이지만) (function() { 'use strict'; // 로드 되면 새 댓글 색깔 바꾸기 window.onload = function() {colorNewComment();}; const hideMore = true // 세로일 때 제거 요소가 더 많아짐 // 세로 모드이면 일부 요소 제거 hideElementsInPortrait(detectScreenMode(), hideMore); // 세로로 인식시키고 강제로 제거 // hideElementsInPortrait("Portrait", hideMore); // 검색창을 위에 복사 const targetElement = document.querySelector("body > div.root-container > div.content-wrapper.clearfix > article > div"); const elementToCopy = document.querySelector("body > div.root-container > div.content-wrapper.clearfix > article > div > form.form-inline.search-form.justify-content-end"); if (targetElement && elementToCopy) { const clonedElement = elementToCopy.cloneNode(true); // true는 자식 노드까지 복사 clonedElement.style.paddingBottom = "7px"; // 원하는 패딩 값 적용 clonedElement.style.paddingRight = "15px"; // 오른쪽 패딩 추가 targetElement.parentNode.insertBefore(clonedElement, targetElement); } const mouseRecommendHardMode = true; const keyRecommendHardMode = true; const maxGauge = 5; // 추천 버튼을 눌러야하는 횟수, 1 이상의 값으로 변경 가능 const mouseNotRecommendHardMode = true; const maxGauge2 = 10; // 비추 버튼을 클릭해야하는 횟수, 1 이상의 값으로 변경 가능 if (window.self === window.top) { window.currentPrevPage = (function() { const urlParams = new URLSearchParams(window.location.search); const pageFromUrl = parseInt(urlParams.get('p')); const pageFromDOM = getCurrentPageNumber(); return pageFromUrl || pageFromDOM || 1; })(); window.prevLoadCount = 0; window.MAX_PREV_LOAD_COUNT = 3; } // 현재 페이지의 방문 기록을 저장 // storeCurrentPage(); // 페이지를 떠날 떄 현재 방문 시간을 업데이트 (예: 페이지 unload 시 업데이트) window.addEventListener("beforeunload", () => { storeCurrentPage(); }); // 가로 모드면 우측에 인접 게시글 n개 생성 const n = 15; if (detectScreenMode() === "Landscape") createAdjacentPostsSection(n); // 세로 모드면 기존 게시판 위에 인접 게시글 m개 삽입 // const m = 11; // insertDistributedAdjacentPostsAboveBoard(m); // 이전, 현재, 다음 게시물들 사이에 경계 넣기 const makeBorder = true; // 기본 익명화 최초 설정 값 (이후 스크립트에 저장함, h키로 토글하여 변경 가능) let DEFAULT_ANONYMIZE_SETTING = false; // 로컬 스토리지에서 익명화 설정 값을 불러오거나, 없으면 기본값 사용 let anonymizeSetting = GM_getValue("anonymizeSetting", DEFAULT_ANONYMIZE_SETTING); // 닉네임 익명화 let anony = false; // 글 작성자, 댓글 작성자, 사이드바 게시물 익명화 let anony2 = false; // 기존 게시글 목록 익명화 anony = anonymizeSetting; anony2 = anonymizeSetting; // 새로운 키 기능 추가 const keyActionsEnabled = true; const myLink = 'https://arca.live/b/holopro'; // Shift + Q 단축키로 이동할 링크 // 댓글 입력창에 색 넣기 const replyColoring = true; const els = { recommendButton: document.querySelector('button#rateUp.item'), // 추천 버튼 notRecommendButton: document.querySelector('button#rateDown.item'), // 비추천 버튼 pressedRecommendButton: document.querySelector('button#rateUp.item.already'), pressedNotRecommendButton: document.querySelector('button#rateDown.item.already'), commentCounter: document.querySelector('.article-comment.position-relative .title'), // 댓글 수 표시 writeBtn: document.querySelector('#comment .title .btn-arca-article-write'), // 댓글 작성 버튼 mainBoard: document.querySelector('.article-list') || document.querySelector('.board-article-list'), // 게시판 목록 }; let recommendCount = 0, notRecommendCount = 0; let recommendButton = els.recommendButton; let notRecommendButton = els.notRecommendButton; let pressedRecommendButton = els.pressedRecommendButton; let pressedNotRecommendButton = els.pressedNotRecommendButton; /////////////////////////////////////////////////////////////////////////////////////////////////////////// // 버튼 색 설정 (비추 기본색은 흰색이라 생략) if (pressedRecommendButton) { recommendButton = pressedRecommendButton; recommendButton.style.backgroundColor = 'Azure'; recommendCount = maxGauge; } else if (recommendButton) { recommendButton.style.backgroundColor = '#F5F5F5'; } if (pressedNotRecommendButton) { notRecommendButton = pressedNotRecommendButton; notRecommendButton.style.backgroundColor = 'pink'; notRecommendCount = maxGauge2; } // recommendButton이 존재할 때만 스타일 변경 if (recommendButton) { recommendButton.style.zIndex = '1'; } function checkButtonVisibility(selector) { const button = document.querySelector(selector); // 1. 존재하지 않으면 false 반환 if (!button) { console.log(false); return false; } // 2. 크기 확인 (width와 height가 0이면 보이지 않는 것으로 판단) const rect = button.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) { console.log(false); return false; } // 3. 모든 부모 요소의 display와 visibility를 체크 let currentElement = button; while (currentElement) { const style = getComputedStyle(currentElement); if (style.display === 'none' || style.visibility === 'hidden') { console.log(false); return false; } currentElement = currentElement.parentElement; } // 위 모든 체크를 통과하면, 보이는 상태로 판단 console.log("요소가 있습니다."); return true; } /////////////////////////////////////////////////////////////////////////////////////////////////////////// // fillGauge의 사용 function fillRecommendGauge(recommendCount) { fillGauge(recommendButton, recommendCount, maxGauge, "Azure"); } function fillNotRecommendGauge(notRecommendCount) { fillGauge(notRecommendButton, notRecommendCount, maxGauge2, "pink"); } ///////////////////////////////////////////////////////////////////////////////////////////////////////// function fillGauge(button, count, maxCount, color) { if (maxCount === 1) return; const borderRadius = parseInt(getComputedStyle(button).borderRadius) - 0.5; const height = button.offsetHeight - 2; const width = button.offsetWidth; const newHeight = height * count / maxCount; const newBorderRadius = Math.min(borderRadius, ((newHeight - 0) / 2)); const newLeft = Math.max(0, borderRadius - newBorderRadius); let newWidth; let newBottom = 0; const widthMap = { 10: 85.2, 8: 85.2, 6: 85.2, 5: 85.2, 4: 85.1, 3.5: 85.6, 3: 85.2, 2.5: 85.78, 2.2: 85.38, 2: 85.48, 1.8: 84.68, 1.6: 84.18, 1.5: 86.18, 1.33: 85.18, 1: 85.18, 0.67: 86.08, 0.5: 88.08 }; function getWidth(ratio) { return widthMap[ratio] ?? 85.22; } const deviceRatio = parseFloat(window.devicePixelRatio.toFixed(2)); let baseWidth = getWidth(deviceRatio); let newWidth2 = width - 2 * (borderRadius - newBorderRadius) - 1.8 - 0.4 - 88.08 + 4 + 85.44; const nonRecommendButton = checkButtonVisibility('button#rateDown.item'); if (color === "pink" || (color === "Azure" && nonRecommendButton)) { // 비추 버튼, 옆의 추천 버튼 보정 if (deviceRatio === 5.00) { } else if (deviceRatio === 2.20) { newWidth2 -= 2.3; } else if (deviceRatio === 2.00) { newWidth2 -= 2.1; baseWidth += 0.2; } else if (deviceRatio === 1.33) { newWidth2 -= 0.7; } else if (deviceRatio === 1.00) { newWidth2 -= 1; } else if (deviceRatio === 0.67) { newWidth2 -= 2; newBottom += 0.5; } else if (deviceRatio === 0.50) { newWidth2 -= 4; newBottom += 0.9; } newWidth = newHeight >= 10 ? baseWidth : newWidth2; } else { // 비추 숨김일 때 추천 버튼 보정 if (deviceRatio === 10.00) { newWidth2 -= 0.3; } else if (deviceRatio === 8.00) { newWidth2 -= 0.3; } else if (deviceRatio === 5.00) { newWidth2 -= 0.3; } else if (deviceRatio === 4.00) { newWidth2 -= 1.2; } else if (deviceRatio === 3.00) { baseWidth += 0.2; } else if (deviceRatio === 2.00) { newWidth2 -= 2.2; } else if (deviceRatio === 1.80) { baseWidth += 0.5; } else if (deviceRatio === 1.60) { newWidth2 -= 0.3; baseWidth += 0.5; } else if (deviceRatio === 1.50) { newWidth2 -= 0.7; } else if (deviceRatio === 1.33) { newWidth2 -= 0.1; baseWidth += 0.4; } else if (deviceRatio === 1.00) { newWidth2 -= 0.1; } else if (deviceRatio === 0.67) { newWidth2 -= 2; baseWidth -= 0.5; } else if (deviceRatio === 0.50) { newWidth2 -= 2; } newWidth = newHeight >= 10 ? baseWidth : newWidth2; } if (count < maxCount) { const newBackground = document.createElement('div'); Object.assign(newBackground.style, { position: 'absolute', width: newWidth + 'px', height: newHeight + 'px', backgroundColor: color, bottom: newBottom + 'px', left: newLeft + 'px', zIndex: '-2', borderRadius: newBorderRadius + (deviceRatio === 5.00 ? -0.3 : 0) + 'px' }); button.appendChild(newBackground); button.style.zIndex = '2'; } } /////////////////////////////////////////////////////////////////////////////////////////////////////////// // 아카 리프레셔의 비추천 안누름 버튼이 보이자마자 클릭해버리기 const targetSelector = 'button.MuiButtonBase-root.MuiButton-root.MuiButton-contained.MuiButton-containedPrimary'; // 추천 또는 비추 버튼 // 게이지가 다 차기 직전이 아니면 비추 확인 창이 뜨자마자 꺼버림 const observer = new MutationObserver((mutationsList) => { mutationsList.forEach(mutation => { mutation.addedNodes.forEach(node => { // ELEMENT_NODE인지 확인 (텍스트 노드 등은 제외) if (node.nodeType === Node.ELEMENT_NODE) { // 해당 노드 내에 조건에 맞는 요소가 있는지 확인 const button = node.querySelector(targetSelector); if (button && notRecommendCount < maxGauge2) { button.click(); console.log("현재의 notRecommendCount", notRecommendCount + 1); // 비추 누른 횟수만 카운트 } } }); }); }); // body 전체를 감시해 자식 요소 변화와 모든 하위 노드의 변화를 체크함 observer.observe(document.body, { childList: true, subtree: true }); let allowClick = false; // 복제된 버튼에서의 클릭은 허용하기 위한 플래그 function beechuClick(e) { if (event.target !== notRecommendButton) return; console.log(notRecommendCount + 1, "비추 클릭"); if (notRecommendCount < maxGauge2 - 1) return; const yea = document.querySelectorAll('button.MuiButtonBase-root.MuiButton-root.MuiButton-outlined.MuiButton-outlinedPrimary')[1]; if (notRecommendCount === maxGauge2 - 1 && yea && e.target === yea) { interceptClick(e, fillNotRecommendGauge, maxGauge2); } } function interceptClick(e, func, variable) { // 복제된 버튼에서의 클릭이면 그냥 플래그를 리셋하고 정상 실행하도록 함. if (allowClick) { allowClick = false; return; } // 원래 버튼에서 발생한 클릭 이벤트는 잠시 차단 e.stopImmediatePropagation(); e.preventDefault(); func(variable); const targetSelector3 = document.querySelectorAll('button.MuiButtonBase-root.MuiButton-root.MuiButton-outlined.MuiButton-outlinedPrimary')[1]; targetSelector3.click(); // 이후 복제된 버튼에서는 기존 이벤트를 실행할 수 있도록 플래그를 켜줌. allowClick = true; // 약간의 지연 후(0ms라도 좋음) 복제된 버튼에 클릭 이벤트를 강제로 발생시킵니다. //setTimeout(() => { // button.style.backgroundColor = 'pink'; //}, 0); } // 캡처링 단계에서 클릭 이벤트를 가로채도록 true 옵션 사용 document.addEventListener("click", beechuClick, true); /////////////////////////////////////////////////////////////////////////////////////////////////////////// function pressRecommendButton() { recommendButton.style.backgroundColor = 'Azure'; recommendButton.click(); } function pressNotRecommendButton() { recommendButton.style.backgroundColor = 'pink'; notRecommendButton.click(); } if (mouseRecommendHardMode) { document.addEventListener('click', function(event) { if (!recommendButton) return; if (recommendCount >= maxGauge) return; if (recommendButton.contains(event.target)) { event.preventDefault(); event.stopPropagation(); recommendCount++ fillRecommendGauge(recommendCount); if (recommendCount >= maxGauge) { pressRecommendButton(); console.log("추천에 성공했습니다"); } } }); } if (mouseNotRecommendHardMode) { document.addEventListener('click', function(event) { if (!notRecommendButton) return; if (notRecommendCount >= maxGauge2 - 1) return; if (notRecommendButton.contains(event.target)) { event.preventDefault(); event.stopPropagation(); notRecommendCount++; fillNotRecommendGauge(notRecommendCount); } }); } function triggerRecommend() { if (keyRecommendHardMode) { recommendCount++ fillRecommendGauge(recommendCount); if (recommendCount >= maxGauge) { pressRecommendButton(); console.log("추천에 성공했습니다"); } } } //////////////////////////////////////////////////////////////////////////////////////////// // 키 동작 기능 const keyHandlers = { keydown: { "f": () => triggerRecommend(), "d": () => scrollHandler('down'), "n": () => hideElementsInPortrait("Portrait"), "g": () => { if (/^https:\/\/arca\.live\/b\/[^\/?]+(?:\?p=[1-9]\d*)?$/.test(window.location.href)) { // 이 조건은 글 페이지, 목록 페이지 구분법 가져와도 되긴 함 const firstPost = document.querySelector('a.vrow.column:not(.notice)'); window.location.href = firstPost.href; } else { // 새로운 댓글 버튼 클릭 기능 const newCommentButton = document.querySelector('a.newcomment-alert.w-100.fetch-comment.d-block'); if (newCommentButton) { const precount = getCommentCount(); // 댓글 갱신 이전의 개수 ????? newCommentButton.click(); applyBackgroundColors1(); console.log("댓글 갱신이 진행됩니다"); const newCommentAlert = 'a.newcomment-alert.w-100.fetch-comment.d-block'; hideAll(newCommentAlert); setTimeout(() => { const count = getCommentCount(); console.log("새로운 댓글 개수:", count); storeCurrentPage(); // 바뀐 댓글 개수 저장 setTimeout(() => { applyBackgroundColors2(); // 댓글 아랫쪽 색상 변경 }, 2000); // 1+2초 후 호출 }, 1000); // 1초 후 호출 cloneAndOverlayLastComment(); function runCloneAndOverlayFor3Seconds() { const interval = setInterval(() => { cloneAndOverlayLastComment(); }, 10); setTimeout(() => { clearInterval(interval); }, 500); } runCloneAndOverlayFor3Seconds(); storeCurrentPage(); // 최근 방문 시간, 댓글 수 새로 저장 } else { console.log("댓글 추가 없음"); // 마지막 댓글을 찾아 복사 후 파란색으로 강조하여 표시하고, 수정/답글 이벤트를 재등록하며, 버튼을 반투명하게 설정 const comments = document.querySelectorAll('.comment-wrapper'); if (comments.length > 0) { const lastComment = comments[comments.length - 1]; // 마지막 댓글 선택 const clonedComment = lastComment.cloneNode(true); // 깊은 복사 clonedComment.id = 'clonedComment-userScript'; // ID 부여 // 원본 댓글 숨기기 lastComment.style.display = "none"; // 배경색 변경 (파란색 강조) const infoRow = clonedComment.querySelector('.content .info-row.clearfix'); const message = clonedComment.querySelector('.content .message'); if (infoRow) { infoRow.style.backgroundColor = 'skyblue'; infoRow.style.setProperty("transition", "none", "important"); } if (message) { message.style.backgroundColor = 'Azure'; message.style.setProperty("transition", "none", "important"); } // 수정 버튼 이벤트 재등록 및 반투명 스타일 적용 const cloneCompose = clonedComment.querySelector('.icon.ion-compose'); if (cloneCompose) { // 버튼 반투명 적용 cloneCompose.parentNode.style.opacity = "0.2"; cloneCompose.parentNode.addEventListener('click', function(event) { event.preventDefault(); // 원한다면 클릭 시 반투명 스타일을 원래대로 복원할 수 있습니다. // cloneCompose.parentNode.style.opacity = "1"; const hiddenComment = document.querySelector('.comment-wrapper[style*="display: none"]'); if (hiddenComment) { hiddenComment.style.display = ''; clonedComment.style.display = 'none'; hiddenComment.querySelector('.icon.ion-compose').parentNode.click(); } }); } // 답글 버튼 이벤트 재등록 및 반투명 스타일 적용 const cloneReply = clonedComment.querySelector('.icon.ion-reply'); if (cloneReply) { // 버튼 반투명 적용 cloneReply.parentNode.style.opacity = "0.2"; cloneReply.parentNode.addEventListener('click', function(event) { event.preventDefault(); // cloneReply.parentNode.style.opacity = "1"; // 필요 시 원래대로 복원 const hiddenComment = document.querySelector('.comment-wrapper[style*="display: none"]'); if (hiddenComment) { hiddenComment.style.display = ''; clonedComment.style.display = 'none'; hiddenComment.querySelector('.icon.ion-reply').parentNode.click(); } }); } // 클론된 댓글을 원본 댓글이 있던 부모에 추가 lastComment.parentNode.appendChild(clonedComment); } } } }, "h": () => { const toggle = toggleAnonymizeSetting(); let anony = toggle; // 글 작성자, 댓글 작성자, 사이드바 게시물 익명화 let anony2 = toggle; // 기존 게시글 목록 익명화 location.reload(); // 새로고침 }, }, keydownShift: { "Q": () => {window.location.href = myLink;}, "D": () => scrollHandler('up'), // 위로 빠르게, 위로 느리게, 멈춤 // "P": () => {cleanOldVisitedPages(1);}, "A": () => { event.preventDefault(); // 기본 동작 막기 event.stopPropagation(); // 버블링 막기 goToClosestUnreadAbove(); // 위쪽 안 읽은 글로 이동 }, "S": () => { event.preventDefault(); // 기본 동작 막기 event.stopPropagation(); // 버블링 막기 goToClosestUnreadBelow(); // 아래쪽 안 읽은 글로 이동 }, }, }; ////////////////////////////////////// // 전역 변수 선언 let scrollDirection = null; // 'up' 또는 'down' let scrollSpeed = 0; // 0: 정지, 1: 빠른 스크롤, 2: 느린 스크롤 let scrollInterval = null; let stopTimeout = null; let loadFinished = false; var visitedPages = {}; ////////////////////////////////////// // 익명화 설정 값을 변경하고 로컬 스토리지에 저장하는 함수 function setAnonymizeSetting(newSetting) { if (typeof newSetting === "boolean") { anonymizeSetting = newSetting; GM_setValue("anonymizeSetting", anonymizeSetting); console.log("익명화 설정이 업데이트되었습니다:", anonymizeSetting); } else { console.error("익명화 설정은 boolean 값이어야 합니다."); } } function toggleAnonymizeSetting() { // 기존 설정 값을 로컬 스토리지에서 불러옴 let currentSetting = GM_getValue("anonymizeSetting", DEFAULT_ANONYMIZE_SETTING); // 값을 반대로 토글 let newSetting = !currentSetting; // 로컬 스토리지에 저장 및 전역 변수 업데이트 GM_setValue("anonymizeSetting", newSetting); anonymizeSetting = newSetting; console.log("익명화 설정이 토글되었습니다:", newSetting); return newSetting; } ////////////////////////////////////// // 사이드바 아이템 제거 document.querySelectorAll('.sidebar-item').forEach(element => element.remove()); // *ㅎㅎ 공지 제거 const notices = document.querySelectorAll('a.vrow.column.notice'); const filteredElements = Array.from(notices).filter(element => element.textContent.includes('*ㅎㅎ')); filteredElements.forEach(element => {element.remove();}); // 광고 제거 ['.sticky-container .ad', '.banner'].forEach(selector => document.querySelector(selector)?.remove()); document.querySelector('.ad#svQazR5NHC3xCQr3')?.remove(); // iframe이면 여기서 종료 function isIframe() {return window.self !== window.top;} if (isIframe()) return; // 🔄 스크립트 실행 시 스타일 추가 const style = document.createElement('style'); style.textContent = ` .my-script-hidden-post { display: none; /* 처음엔 보이지 않음 */ } `; document.head.appendChild(style); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// function updateReplyColors() { if (replyColoring) { applyBackgroundColors1(); // 댓글 윗쪽 색상 변경 applyBackgroundColors2(); // 댓글 아랫쪽 색상 변경 } } // 댓글 입력창 색칠 (색칠 여부는 자동 판단) updateReplyColors(); // 이후 코드에서 클릭의 경우도 감지, g키도 확인 // 댓글이 0개인 경우 댓글창 헤드 색칠 + 댓글 입력창 상단부 function applyBackgroundColors1() { // 댓글 입력창 상단부 색칠 const elements = [ { selector: '.reply-form .reply-form__container .reply-form__user-info', color: 'lightgreen' }, { selector: '.reply-form-button-container', color: 'lightgreen' }, { selector: '.reply-form-arcacon-button.btn-namlacon', color: '#32CD32' } ]; elements.forEach(({ selector, color }) => { const element = document.querySelector(selector); if (element) element.style.backgroundColor = color; }); // 댓글 0개일 때 댓글 개수 칸을 하늘색으로 칠함 // 댓글 개수 칸 선택 const commentCounterBar = els.commentCounter; const writeButton = els.writeBtn; if (!commentCounterBar) return; // 없으면 목록 페이지이고, 색칠할 필요 없음 const startTime = Date.now(); const interval = setInterval(() => { const currentTime = Date.now(); if (currentTime - startTime >= 3000) { clearInterval(interval); // 길어야 3초 후 종료 return; } // 댓글 개수 칸 색 변경 const newColor = getCommentCount() === 0 ? 'rgb(130, 206, 235)' : 'rgba(0, 0, 0, 0)'; const newColor2 = getCommentCount() === 0 ? 'rgb(50, 148, 235)' : 'rgba(0, 0, 0, 0)'; let previousColor = window.getComputedStyle(commentCounterBar).backgroundColor; if (newColor !== previousColor) { commentCounterBar.style.backgroundColor = newColor; writeButton.style.backgroundColor = newColor2; writeButton.style.borderColor = newColor2; clearInterval(interval); // 색을 변경했으니 종료 } }, 50); } // 댓글이 하나 이상 있는 경우 // 댓글을 새로 불러오면 기존의 댓글들이 싹 새로 불러와져서 색을 되돌리는 과정은 필요없음 function applyBackgroundColors2() { const comments = document.querySelectorAll('.comment-wrapper'); if (comments.length > 0) { const lastComment = comments[comments.length - 1]; // 마지막 댓글 선택 const infoRow = lastComment.querySelector('.content .info-row.clearfix'); const message = lastComment.querySelector('.content .message'); // 색 변경 if (infoRow) infoRow.style.backgroundColor = 'skyblue'; infoRow.style.setProperty("transition", "none", "important"); if (message) message.style.backgroundColor = 'Azure'; // AliceBlue, Azure 추천 message.style.setProperty("transition", "none", "important"); } } // 댓글 갱신 function cloneAndOverlayLastComment() { const comments = document.querySelectorAll('.comment-wrapper'); if (comments.length === 0) return; const lastComment = comments[comments.length - 1]; const clone = lastComment.cloneNode(true); clone.id = 'clonedComment-userScript'; lastComment.style.display = "none"; ////////////////////////////////////////////////////////////// // 원본 스타일 복사 const computedStyle = window.getComputedStyle(lastComment); clone.style.textAlign = computedStyle.textAlign; clone.style.fontSize = computedStyle.fontSize; clone.style.animation = "none"; clone.querySelectorAll('img').forEach(imgClone => { // 이미지 크기 유지 const originalImg = lastComment.querySelector(`img[src="${imgClone.src}"]`); if (originalImg) { const originalImgStyle = window.getComputedStyle(originalImg); imgClone.style.width = originalImgStyle.width; imgClone.style.height = originalImgStyle.height; } }); // 위치와 크기 복사 // const rect = lastComment.getBoundingClientRect(); // 원본 스타일 유지 clone.querySelectorAll('[id]').forEach(el => el.removeAttribute('id')); clone.querySelectorAll('img').forEach(imgClone => { const originalImg = lastComment.querySelector(`img[src="${imgClone.src}"]`); if (originalImg) { const originalImgStyle = window.getComputedStyle(originalImg); imgClone.style.width = originalImgStyle.width; imgClone.style.height = originalImgStyle.height; } }); // 신고 버튼 구현 const cloneAlert = clone.querySelector('.icon.ion-alert'); cloneAlert.parentNode.onclick = function () { }; // 삭제 버튼 구현 const cloneDelete = clone.querySelector('.icon.ion-trash-b'); if (cloneDelete) { cloneDelete.parentNode.addEventListener('click', function(event) { }); }; // 수정 버튼 구현 const cloneCompose = clone.querySelector('.icon.ion-compose'); if (cloneCompose) { cloneCompose.parentNode.addEventListener('click', function(event) { event.preventDefault(); const list = document.querySelectorAll('#clonedComment-userScript'); list.forEach((element, index) => { if (index > 0 && list[index - 1].style.display === 'none') element.remove(); }); list[list.length - 1].style.display = ''; const hiddenComment = Array.from(document.querySelectorAll('.comment-wrapper')) .find(element => getComputedStyle(element).display === 'none'); hiddenComment.style.display = ''; clone.style.display = 'none'; hiddenComment.querySelector('.icon.ion-compose').parentNode.click(); }); }; // 답글 버튼 구현 const cloneReply = clone.querySelector('.icon.ion-reply'); if (cloneReply) { cloneReply.parentNode.addEventListener('click', function(event) { event.preventDefault(); const list = document.querySelectorAll('#clonedComment-userScript'); list.forEach((element, index) => { if (index > 0 && list[index - 1].style.display === 'none') element.remove(); }); list[list.length - 1].style.display = ''; const hiddenComment = Array.from(document.querySelectorAll('.comment-wrapper')) .find(element => getComputedStyle(element).display === 'none'); hiddenComment.style.display = ''; clone.style.display = 'none'; hiddenComment.querySelector('.icon.ion-reply').parentNode.click(); }); }; const fadein = lastComment.querySelector('.content-item fadein'); const infoRow = clone.querySelector('.content .info-row.clearfix'); infoRow.style.animation = "none"; const message = clone.querySelector('.content .message'); // 색 변경 if (infoRow) infoRow.style.backgroundColor = 'skyblue'; infoRow.style.setProperty("transition", "none", "important"); if (message) message.style.backgroundColor = 'Azure'; // AliceBlue, Azure 추천 message.style.setProperty("transition", "none", "important"); lastComment.parentNode.style.setProperty("transition", "none", "important"); if (message) message.style.backgroundColor = 'Azure'; // AliceBlue, Azure 추천 // orange lastComment.parentNode.appendChild(clone); comments.forEach(comment => { }); }/////////////////////////////////////////////////////////////////////////// let commentNumberChanged = false; const newCommentAlert = 'a.newcomment-alert.w-100.fetch-comment.d-block'; function hideFirst(selector) { const elements = document.querySelectorAll(selector); if (elements.length >= 2) { elements[0].style.backgroundColor = 'pink'; // 작동함!! elements[0].style.display = 'none'; // 요소를 완전히 숨김 elements[0].style.setProperty("display", "none", "important"); } } function hideAll(selector) { document.querySelectorAll(selector).forEach(element => { element.style.setProperty("display", "none", "important"); }); } // MutationObserver를 설정하는 함수 function observeDOMChanges(targetNode) { if (!(targetNode instanceof Node)) { console.error("오류: MutationObserver를 실행할 대상이 유효한 Node가 아닙니다.", targetNode); return; } const observer = new MutationObserver(() => { hideFirst(newCommentAlert); }); observer.observe(targetNode, { childList: true, subtree: true }); } document.addEventListener("DOMContentLoaded", () => { observeDOMChanges(document.body); }); function observeAndCloneNewCommentButton() { const callback = (mutationsList, observer) => { const newCommentButton = document.querySelector('a.newcomment-alert.w-100.fetch-comment.d-block'); // "새로운 댓글이 달렸습니다" 버튼 if (newCommentButton) { const clone = newCommentButton.cloneNode(true); const comments = document.querySelectorAll('.comment-wrapper'); const lastComment = comments[comments.length - 1]; if (lastComment && !commentNumberChanged) { lastComment.parentNode.appendChild(clone); commentNumberChanged = true; } } else commentNumberChanged = false; }; const observer = new MutationObserver(callback); observer.observe(document.body, { childList: true, subtree: true }); } // 감시 시작 observeAndCloneNewCommentButton(); ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// function scrollHandler(inputDirection) { if (scrollSpeed === 0 || scrollDirection !== inputDirection) { scrollDirection = inputDirection; scrollSpeed = 1; } else { scrollSpeed = scrollSpeed === 1 ? 2 : 0; if (scrollSpeed === 0) scrollDirection = null; } clearInterval(scrollInterval); clearTimeout(stopTimeout); if (scrollSpeed === 0) return; // 스크롤 이동량 및 간격 설정 // 빠른 스크롤: 이동량 2, 인터벌 4ms / 느린 스크롤: 이동량 1, 인터벌 5ms let moveAmount = (scrollSpeed === 1) ? 2 : 1; let intervalDelay = (scrollSpeed === 1) ? 4 : 5; // 방향에 따라 양수(아래) 또는 음수(위) 적용 let scrollAmount = (scrollDirection === 'down') ? moveAmount : -moveAmount; scrollInterval = setInterval(() => { window.scrollBy({ top: scrollAmount, left: 0 }); }, intervalDelay); // 일정 시간(예: 8000ms) 후에 자동 정지 처리 stopTimeout = setTimeout(() => { clearInterval(scrollInterval); scrollSpeed = 0; scrollDirection = null; }, 8000); } // 가로세로 판별 함수 function detectScreenMode() { return window.innerWidth >= 992 ? "Landscape" : "Portrait"; // 가로 : 세로 } // 세로일 때 몇몇 요소 지우기 function hideElementsInPortrait(isPortrait = "Portrait", hideMore) { // Set default value for 'hideMore' inside the function body. if (hideMore === undefined) { hideMore = false; } if (isPortrait === "Portrait") { const elementsToHide = ["nav.navbar", "div.board-title", "div#vote.vote-area", "div.article-menu.mt-2", "div.edit-menu", "div.alert.alert-info", "div.article-link", "a.vrow.column.notice notice-unfilter"]; // 숨길 요소의 선택자 목록 elementsToHide.forEach(selector => { const element = document.querySelector(selector); if (element) element.style.display = "none"; }); elementsToHide.forEach(selector => { const element = document.querySelector(selector); if (element) element.style.display = "none"; }); if (hideMore) { //////////////////////////////////////////// const container = document.querySelector("div#comment.article-comment.position-relative"); //console.log("컨테이너 선택"); if (container) { //console.log("컨테이너는 있음"); const title = container.querySelector(".title"); title.remove(); const title1 = container.querySelector(".reply-form.write"); title1.remove(); } const container2 = document.querySelector("div.btns-board"); if (container) { //console.log("컨테이너는 있음2"); const title = container2.querySelector(".float-right"); title.remove(); const title1 = container2.querySelector(".float-left"); title1.remove(); } document.querySelector("div.board-category-wrapper").remove(); const history = document.querySelector("div.channel-visit-history"); if (history) history.remove(); // document.querySelector("div.btns-board").remove(); // document.querySelector("div.board-Btns").remove(); } document.querySelectorAll("a.vrow.column.notice").forEach(element => { element.style.display = "none"; }); const vrowInner = document.querySelector('div.vrow-inner'); if (vrowInner) { const parent = vrowInner.parentElement; if (parent) parent.style.display = 'none'; } const allAds = document.querySelectorAll('div.ad'); // 모든 요소를 순회하면서 작업 수행 allAds.forEach(ad => { ad.remove(); // 예시: 요소 없애기 }); // MutationObserver를 사용하여 DOM 변경 시 자동으로 notice 요소 제거 const targetSelectors = ["a.vrow.column.notice", "a.vrow.column.notice.notice-unfilter"]; const observer = new MutationObserver(mutations => { mutations.forEach(m => { m.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { targetSelectors.forEach(sel => { if (node.matches(sel)) node.remove(); }); targetSelectors.forEach(sel => { node.querySelectorAll(sel).forEach(el => el.remove()); }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); } } const handleKeyEvent = (event) => { // 반복 이벤트 무시, 직접 입력이 아니면 무시 if (event.repeat || !event.isTrusted) return; // 텍스트 입력창에 포커스가 있으면 키 입력 무시 const activeElement = document.activeElement; const tagName = activeElement.tagName.toLowerCase(); const isTextInput = ( tagName === "textarea" || (tagName === "input" && ["text", "password", "email", "search", "tel", "url", "number", "date", "time"].includes(activeElement.type)) || activeElement.isContentEditable ); if (isTextInput) return; // 키 입력 if (event.shiftKey) { if (event.type === "keydown") keyHandlers.keydownShift?.[event.key]?.(event); else if (event.type === "keyup") keyHandlers.keyupShift?.[event.key]?.(event); } else { if (event.type === "keydown") keyHandlers.keydown?.[event.key]?.(event); else if (event.type === "keyup") keyHandlers.keyup?.[event.key]?.(event); } }; if (keyActionsEnabled) { window.addEventListener("keydown", handleKeyEvent); window.addEventListener("keyup", handleKeyEvent); } //////////////////////////////////////////////////////////////////////////////////////////////////// const waitForElementState = (selector, desiredState = "present", timeout = 10000) => { return new Promise((resolve, reject) => { // 조건 체크 함수: "present"이면 요소가 존재하는지, "removed"이면 요소가 없는지 판단 const checkCondition = () => { const element = document.querySelector(selector); if (desiredState === "present") return element || null; if (desiredState === "removed") return element ? null : true; }; // 초기 상태 검사 const initialResult = checkCondition(); if ((desiredState === "present" && initialResult) || (desiredState === "removed" && initialResult === true)) { return resolve(initialResult); } const observer = new MutationObserver(() => { const result = checkCondition(); if ((desiredState === "present" && result) || (desiredState === "removed" && result === true)) { observer.disconnect(); resolve(result); } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] }); setTimeout(() => { observer.disconnect(); reject(new Error(`Desired state "${desiredState}" not achieved within the maximum wait time.`)); }, timeout); }); }; // 특정 요소가 화면에 보이는지 확인하는 함수 function isElementVisible(element) { const rect = element.getBoundingClientRect(); return rect.bottom > 0 && rect.top < window.innerHeight; } ////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 방문 페이지 정보를 저장할 객체 (정렬 없이, URL을 키로 사용) visitedPages = {}; // 보관 기간 (예: 30일). 이후 방문 기록은 정리합니다. var retentionPeriod = 10 * 365 * 24 * 60 * 60 * 1000; // 10년 // 저장소에서 방문 기록을 불러옵니다. // visitedPages 전역변수를 불러옴. function getVisitedPages() { let stored = GM_getValue("visitedPages"); if (stored === undefined) { visitedPages = {}; } else { try { visitedPages = JSON.parse(stored); } catch (e) { visitedPages = {}; } } return visitedPages; } getVisitedPages(); // 방문 기록에 페이지 정보를 추가하거나 업데이트합니다. function addVisitedPage(pageUrl = window.location.origin + window.location.pathname, time = Date.now(), noSave = false) { getVisitedPages(); const comment = getCommentCount(); if (!visitedPages[pageUrl]) { // 새로 방문한 페이지이면, 최초 방문 및 최근 방문 시각, 댓글 수를 모두 기록 visitedPages[pageUrl] = { firstVisit: time, lastVisit: time, comment: comment }; } else { // 이미 존재하면 최신 방문 시각, 댓글만 업데이트 visitedPages[pageUrl].lastVisit = time; visitedPages[pageUrl].comment = comment; } if (!noSave) { GM_setValue("visitedPages", JSON.stringify(visitedPages)); // console.log("저장했습니다", pageUrl, visitedPages[pageUrl]); } } function delVisitedPage(pageUrl, noSave = false) { getVisitedPages(); // 저장소에서 기존 방문 기록 불러오기 if (visitedPages[pageUrl]) { delete visitedPages[pageUrl]; if (!noSave) { GM_setValue("visitedPages", JSON.stringify(visitedPages)); } } } // 보관 기간(retentionPeriod)을 넘긴 방문 기록을 정리합니다. function cleanOldVisitedPages(retentionPeriod = retentionPeriod, noSave = false) { const now = Date.now(); let changed = false; for (const pageUrl in visitedPages) { if (visitedPages.hasOwnProperty(pageUrl)) { // 마지막 방문 시각이 현재보다 retentionPeriod보다 오래 전이면 삭제 if (now - visitedPages[pageUrl].lastVisit > retentionPeriod) { delete visitedPages[pageUrl]; changed = true; } } } if (changed && !noSave) { GM_setValue("visitedPages", JSON.stringify(visitedPages)); } } function getBaseUrl(url) { try { const urlObj = new URL(url); const pathSegments = urlObj.pathname.split('/'); // 필요한 경우 첫 4개 세그먼트만 사용 if (pathSegments.length > 4) { urlObj.pathname = pathSegments.slice(0, 4).join('/'); } return urlObj.origin + urlObj.pathname; } catch (e) { return url; } } function storeCurrentPage(noSave = false) { let baseUrl = window.location.origin + window.location.pathname; if (window.location.pathname.split('/').length > 4) { baseUrl = window.location.origin + window.location.pathname.split('/').slice(0, 4).join('/'); } addVisitedPage(baseUrl, Date.now(), noSave); } function isPageVisited(pageUrl) { getVisitedPages(); return visitedPages.hasOwnProperty(pageUrl); } document.addEventListener("contextmenu", function (event) { if (event.ctrlKey) { // 컨트롤 + 우클릭을 누른 상태에서만 실행 const target = event.target.closest("a"); // 클릭한 위치에서 가장 가까운 태그 탐색 if (target) { const href = target.href.split('?')[0]; console.log("쿼리 제거 후 URL:", href); event.preventDefault(); // 기본 우클릭 메뉴 방지 delVisitedPage(href); // 저장소에서 URL 제거 } } }); function getCurrentPageNumber() { const element = document.querySelector('.page-item.active'); return Number(element.textContent.trim()); } // 위에도 검색창 만들기 const searchBar = document.querySelector("body > div.root-container > div.content-wrapper.clearfix > article > div > form.form-inline.search-form.justify-content-end"); getVisitedPages(); /////////////////////////////////////////////////////////////////////////////////////////////////// // 댓글 개수를 세는 함수 function getCommentCount() { // 첫 번째 요소: [0] let element = document.querySelector('span.title-comment-count'); if (element) { // 대괄호([])를 제거하고 숫자만 추출합니다. const text = element.textContent.replace(/[\[\]]/g, '').trim(); const count = parseInt(text, 10); if (!isNaN(count)) { return count; } } // 두 번째 요소: 0 element = document.querySelector('span.body.comment-count'); if (element) { const text = element.textContent.trim(); const count = parseInt(text, 10); if (!isNaN(count)) { return count; } } return 0; } async function createGeneralPostSectionFromAdjacentPage(direction = "curr", postCount, adjacentClonedItem = document.getElementById("adjacent-posts-container")) { const currentPage = window.currentPrevPage; let targetPage; if (direction === "prev") { if (window.prevLoadCount >= window.MAX_PREV_LOAD_COUNT) { console.warn("최대 이전 페이지 로드 횟수에 도달하여 로드를 중단합니다."); return; } if (currentPage > 1) { targetPage = currentPage - 1; window.currentPrevPage = targetPage; // 부모 변수 업데이트 window.prevLoadCount++; } else { return; } } else if (direction === "curr") { targetPage = currentPage; } else if (direction === "next") { targetPage = currentPage + 1; } else { console.error("유효한 방향('prev', 'curr' 또는 'next')을 입력하세요."); return; } // 사이드바(또는 body)에 붙이기 const sidebarContainer = document.querySelector('div.sidebar-item')?.parentElement; // 컨테이너 생성 adjacentClonedItem = adjacentClonedItem ? adjacentClonedItem : document.getElementById("adjacent-posts-container"); if (!adjacentClonedItem) { // console.log("새 인접 게시글 컨테이너 생성"); adjacentClonedItem = document.createElement('div'); adjacentClonedItem.id = "adjacent-posts-container"; adjacentClonedItem.style.backgroundColor = 'white'; adjacentClonedItem.style.maxHeight = '600px'; adjacentClonedItem.style.overflowY = 'auto'; adjacentClonedItem.style.marginTop = '10px'; adjacentClonedItem.style.width = '310px'; adjacentClonedItem.classList.add('my-script-hidden-post'); // 상단 구분선 추가 const topSeparator = document.createElement('div'); topSeparator.style.height = '1px'; topSeparator.style.backgroundColor = 'gray'; topSeparator.style.margin = '0'; adjacentClonedItem.appendChild(topSeparator); if (sidebarContainer) { sidebarContainer.appendChild(adjacentClonedItem); } else { document.body.appendChild(adjacentClonedItem); } } // curr의 경우 iframe 없이 바로 처리 if (direction === "curr") { // 현재 페이지에서 active(현재 보고 있는 글)의 위치를 찾음 let posts = Array.from(document.querySelectorAll('a.vrow.column:not(.notice)')); const activeIndex = posts.findIndex(post => { try { const currentUrl = new URL(window.location.href); const postUrl = new URL(post.href, window.location.origin); return postUrl.pathname === currentUrl.pathname; } catch (e) { return false; } }); if (activeIndex !== -1) { let start = Math.max(0, activeIndex - Math.floor(postCount / 2)); let end = Math.min(posts.length, start + postCount); posts = posts.slice(Math.max(0, end - postCount), end); } else { posts = posts.slice(0, postCount); } await extractAndAppendPosts(document, adjacentClonedItem, direction, postCount, posts); finalizeAdjacentSection(adjacentClonedItem); return; } // prev 또는 next의 경우 hidden iframe을 생성하여 targetPage 로드 const iframe = document.createElement('iframe'); iframe.style.display = 'none'; const currentUrl = new URL(location.href); const originUrl = window.location.origin; const pathName = window.location.pathname.split('/').slice(0, 3).join('/'); const baseUrl = `${originUrl}${pathName}`; // 현재 페이지의 URL 객체 생성 currentUrl.searchParams.delete("p"); let otherParams = currentUrl.searchParams.toString(); if (otherParams) { iframe.src = `${baseUrl}?p=${targetPage}&${otherParams}`; // 다른 파라미터가 있으면 추가 } else { iframe.src = `${baseUrl}?p=${targetPage}`; // 없으면 그냥 p만 붙임 } console.log("iframe src:", iframe.src); document.body.appendChild(iframe); iframe.onload = function() { const thickSeparator = document.createElement('div'); thickSeparator.style.height = '2px'; thickSeparator.style.backgroundColor = 'gray'; thickSeparator.style.margin = '0'; if (makeBorder && direction === "next") { adjacentClonedItem.appendChild(thickSeparator); } console.log("iframe 로드 완료, targetPage =", targetPage); const doc = iframe.contentDocument || iframe.contentWindow.document; extractAndAppendPosts(doc, adjacentClonedItem, direction, postCount); iframe.remove(); finalizeAdjacentSection(adjacentClonedItem); if (makeBorder && direction === "prev") { adjacentClonedItem.appendChild(thickSeparator); } }; iframe.onerror = function() { console.error(`페이지 ${targetPage}의 iframe 로드 중 오류 발생.`); iframe.remove(); finalizeAdjacentSection(adjacentClonedItem); }; function finalizeAdjacentSection(container) { // 위치 설정 및 고정 container.style.position = 'sticky'; container.style.top = '10px'; } } // 게시글 추출 및 컨테이너에 추가 async function extractAndAppendPosts(doc, container, direction, postCount, posts = null) { const containerElement = doc.querySelector('div.article-list'); if (!containerElement) { console.error("게시글 컨테이너를 찾을 수 없습니다."); return; } if (!posts) { posts = Array.from(containerElement.querySelectorAll('a.vrow.column:not(.notice)')); posts = direction === "prev" ? posts.slice(-postCount) : direction === "next" ? posts.slice(0, postCount) : posts; } posts.forEach(post => { const clonedPost = post.cloneNode(true); clonedPost.querySelectorAll('.vrow-preview').forEach(preview => preview.remove()); // 배경색 확인 const computedStyle = window.getComputedStyle(post); const backgroundColor = computedStyle.backgroundColor; let url = clonedPost.href; const baseUrl = getBaseUrl(url); // iframe의 기존 게시판에 있는 글일테니 ?만 해결하면 됨 (아님, getBaseUrl 만들어서 사용) if (backgroundColor === 'rgb(208, 208, 208)' || backgroundColor === 'rgb(238, 238, 238)' || isPageVisited(baseUrl)) { clonedPost.style.color = 'lightgray'; } const idElement = clonedPost.querySelector('.vcol.col-id'); if (idElement) idElement.remove(); const viewElement = clonedPost.querySelector('.vcol.col-view'); if (viewElement) { const viewSpan = document.createElement('span'); viewSpan.innerText = '조회수 '; viewElement.parentNode.insertBefore(viewSpan, viewElement); } const rateElement = clonedPost.querySelector('.vcol.col-rate'); if (rateElement && viewElement) { const rateSpan = document.createElement('span'); rateSpan.innerText = '추천 '; viewElement.parentNode.insertBefore(rateSpan, rateElement); } clonedPost.style.fontSize = '11px'; if (clonedPost.classList.contains('active')) { clonedPost.style.backgroundColor = '#d0d0d0'; clonedPost.style.zIndex = '2'; clonedPost.style.color = 'lightgray'; // 텍스트 색상을 회색으로 설정 ///////// const activeBackgroundDiv = document.createElement('div'); activeBackgroundDiv.style.position = 'absolute'; activeBackgroundDiv.style.top = '0'; activeBackgroundDiv.style.left = '0'; activeBackgroundDiv.style.width = '100%'; activeBackgroundDiv.style.height = '100%'; activeBackgroundDiv.style.backgroundColor = '#EEEEEE'; activeBackgroundDiv.style.zIndex = '-1'; clonedPost.style.position = 'relative'; // active 클래스를 제거하여 이후 키 이벤트 등에 영향이 없도록 합니다. clonedPost.classList.remove('active'); clonedPost.appendChild(activeBackgroundDiv); } container.appendChild(clonedPost); const separator = document.createElement('div'); separator.style.height = '1px'; separator.style.backgroundColor = 'gray'; separator.style.margin = '0'; container.appendChild(separator); }); container.classList.add(`${direction}-loaded`); } // 게시글 개수 분배 function calculateAboveBelowNext(pageNumber, activeIndex, length = 45, count = 15) { const half = Math.floor(count/2); // = 7 // 1페이지인 경우 이전 페이지에서 가져올 게시글은 없으므로 prev는 항상 0 if (pageNumber === 1) { // active가 0-indexed 기준으로 후반(8번째 이상)이 아니면 if (activeIndex < length - half) { return [0, count, 0]; // 전부 현재 페이지에서 사용 } else { const curr = count + (length - half -1) - activeIndex; return [0, curr, count - curr]; } } else { // 2페이지 이상인 경우 if (activeIndex < half) { return [half - activeIndex, count - (half - activeIndex), 0]; // 페이지 상단일 때 } else if (activeIndex < length - half) { return [0, 15, 0]; // 중간 부분이면 현재 페이지 전체 15개 사용 } else { const curr = count + (length - half -1) - activeIndex; return [0, curr, count - curr]; } } } // 최종 목표: 우측 컨테이너를 인접 게시글로 채우기!!! async function createAdjacentPostsSection(postCount) { // if (detectScreenMode() === "Portrait") return; const posts = Array.from(document.querySelectorAll('a.vrow.column:not(.notice)')); let activeIndex = posts.findIndex(post => { try { const currentUrl = new URL(window.location.href); const postUrl = new URL(post.href, window.location.origin); return postUrl.pathname === currentUrl.pathname; } catch (e) { console.error("findIndex 오류:", e); return false; } }); const urlParams = new URLSearchParams(window.location.search); const currentPage = getCurrentPageNumber() || parseInt(urlParams.get('p')) || 1; // yyy const postDistribution = calculateAboveBelowNext(currentPage, Math.max(activeIndex, 0), posts.length, postCount); // 이전, 현재, 다음 페이지 섹션 로드 및 조건 기반 대기 try { (async () => { if (postDistribution[0] > 0) { // console.log("이전 페이지에서", postDistribution[0], "개 게시글 로드"); await createGeneralPostSectionFromAdjacentPage("prev", postDistribution[0]); await waitForCondition(() => document.querySelector('.prev-loaded') !== null, 10000); // 타임아웃 오류가 뜨면 이 숫자 등을 늘릴 것 } if (postDistribution[1] > 0) { // console.log("현재 페이지에서", postDistribution[1], "개 게시글 로드"); await createGeneralPostSectionFromAdjacentPage("curr", postDistribution[1]); await waitForCondition(() => document.querySelector('.curr-loaded') !== null, 2000); } if (postDistribution[2] > 0) { // console.log("다음 페이지에서", postDistribution[2], "개 게시글 로드"); await createGeneralPostSectionFromAdjacentPage("next", postDistribution[2]); await waitForCondition(() => document.querySelector('.next-loaded') !== null, 2000); } // console.log("모든 게시글 로드 완료"); let isHidden = true; function revealPosts() { // 🔵 모두 완료된 후 한꺼번에 보이기 document.querySelectorAll('.my-script-hidden-post').forEach(post => { post.classList.remove('my-script-hidden-post'); // 숨김 속성 클래스 제거 isHidden = false; }); if (!isHidden) { clearInterval(intervalId); // 반복 멈춤 loadFinished = true; } } const intervalId = setInterval(revealPosts, 200); // 0.2초마다 반복 })(); } catch (error) { console.warn("조건 기반 대기 중 오류 발생:", error); } } function waitForCondition(predicate, timeout = 2000, interval = 50) { return new Promise((resolve, reject) => { const startTime = Date.now(); const timer = setInterval(() => { if (predicate()) { clearInterval(timer); resolve(); } else if (Date.now() - startTime >= timeout) { clearInterval(timer); reject(new Error("조건이 충족되지 않음 (타임아웃)")); // 타임아웃 오류는 여기 } }, interval); }); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// async function insertDistributedAdjacentPostsAboveBoard(postCount = 9) { // 세로 모드(Portrait)에서만 실행 if (detectScreenMode() !== "Portrait") return; // 메인 게시판 컨테이너 찾기 const mainBoard = els.mainBoard; if (!mainBoard) { console.warn("메인 게시판 컨테이너를 찾을 수 없습니다."); return; } // 게시글 목록(공지 제외) 가져오기 const posts = Array.from(mainBoard.querySelectorAll('a.vrow.column:not(.notice)')); if (posts.length === 0) return; // 현재 페이지 번호 가져오기 const urlParams = new URLSearchParams(window.location.search); const currentPage = getCurrentPageNumber() || parseInt(urlParams.get('p')) || 1; // 현재 페이지의 active 게시글 인덱스 찾기 let activeIndex = posts.findIndex(post => { try { const currentUrl = new URL(window.location.href); const postUrl = new URL(post.href, window.location.origin); return postUrl.pathname === currentUrl.pathname; } catch (e) { return false; } }); if (activeIndex < 0) activeIndex = 0; // 게시글 분배 계산 const [numPrev, numCurr, numNext] = calculateAboveBelowNext(currentPage, activeIndex, posts.length, postCount); //console.log([numPrev, numCurr, numNext]); // 게시판 위에 추가할 컨테이너 생성 const container = document.createElement('div'); container.id = "distributed-adjacent-posts"; container.style.backgroundColor = '#fff'; container.style.padding = '5px'; container.style.marginBottom = '5px'; container.style.border = '1px solid #ccc'; container.style.width = '100%'; container.style.maxHeight = 'none'; try { //console.log(numPrev); if (numPrev > 0) { await createGeneralPostSectionFromAdjacentPage("prev", numPrev, container); //console.log("✅ prev 로딩 완료"); await delay(1000); // 🔹 prev 완료 후 1000ms 대기 } //console.log(numCurr); if (numCurr > 0) { await createGeneralPostSectionFromAdjacentPage("curr", numCurr, container); // console.log("✅ curr 로딩 완료"); await delay(1000); // 🔹 curr 완료 후 1000ms 대기 } //console.log(numNext); if (numNext > 0) { await createGeneralPostSectionFromAdjacentPage("next", numNext, container); // console.log("✅ next 로딩 완료"); } } catch (error) { console.warn("게시글 로딩 중 오류 발생:", error); } // 메인 게시판 위에 컨테이너 삽입 mainBoard.parentNode.insertBefore(container, mainBoard); loadFinished = true; // 세로 모드에서도 로딩 완료 상태로 표시 } // 🔹 지정한 시간(ms) 동안 대기하는 함수 function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// function clonePostsWithOriginalStyle(doc, container, postsToClone) { postsToClone.forEach(post => { // 기존 게시글 요소를 수정 없이 그대로 클론 const clonedPost = post.cloneNode(true); container.appendChild(clonedPost); // 구분선이 필요하다면 추가 (원본과 유사한 방식) const separator = document.createElement('div'); separator.style.height = '1px'; separator.style.backgroundColor = 'gray'; separator.style.margin = '0'; container.appendChild(separator); }); } function clonePostsPreservingStyle(doc, container, postCount, posts = null) { // doc: 원본 문서, container: 삽입할 컨테이너 // posts가 없으면 원본 문서에서 게시글을 선택 (공지 제외) if (!posts) { posts = Array.from(doc.querySelectorAll('a.vrow.column:not(.notice)')); posts = posts.slice(0, postCount); } posts.forEach(post => { // 원본 요소를 그대로 복제 (스타일, 클래스, 인라인 스타일 모두 유지) const clonedPost = post.cloneNode(true); container.appendChild(clonedPost); // 필요에 따라 구분선을 추가 (원본과 동일하게) const separator = document.createElement('div'); separator.style.height = '1px'; separator.style.backgroundColor = 'gray'; separator.style.margin = '0'; container.appendChild(separator); }); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // (A) 인접 페이지의 게시글을 가져오는 함수 async function createGeneralPostSectionFromAdjacentPage2(direction, postCount, container) { const currentPage = getCurrentPageNumber(); let targetPage; if (direction === "prev") { if (currentPage <= 1) return; targetPage = currentPage - 1; } else if (direction === "next") { targetPage = currentPage + 1; } else { targetPage = currentPage; } const currentUrl = new URL(location.href); currentUrl.searchParams.delete("p"); const baseUrl = currentUrl.origin + currentUrl.pathname; const otherParams = currentUrl.searchParams.toString(); const targetUrl = otherParams ? `${baseUrl}?p=${targetPage}&${otherParams}` : `${baseUrl}?p=${targetPage}`; const iframe = document.createElement('iframe'); iframe.style.display = 'none'; iframe.src = targetUrl; document.body.appendChild(iframe); return new Promise((resolve, reject) => { iframe.onload = function() { const doc = iframe.contentDocument || iframe.contentWindow.document; const boardContainer = doc.querySelector('.article-list') || doc.querySelector('.board-article-list'); if (!boardContainer) { iframe.remove(); return reject(new Error("게시글 컨테이너를 찾을 수 없습니다.")); } let posts = Array.from(boardContainer.querySelectorAll('a.vrow.column:not(.notice)')); if (direction === "prev") { posts = posts.slice(-postCount); } else if (direction === "next") { posts = posts.slice(0, postCount); } posts.forEach(post => { container.appendChild(post.cloneNode(true)); }); iframe.remove(); resolve(); }; iframe.onerror = function() { iframe.remove(); reject(new Error("iframe 로드 오류")); }; }); } ///////////////////////////////////////////////////////////////////////// // (B) 메인 게시판에 인접 게시글을 추가하는 함수 async function addAdjacentPostsToMainBoard() { const mainBoard = els.mainBoard; if (!mainBoard) { console.warn("메인 게시판을 찾을 수 없습니다."); return; } const currentPage = getCurrentPageNumber(); // 이전 페이지의 마지막 2개 게시글 추가 if (currentPage > 1) { const prevContainer = document.createElement('div'); await createGeneralPostSectionFromAdjacentPage2("prev", 2, prevContainer); const prevPosts = prevContainer.querySelectorAll('a.vrow.column:not(.notice)'); if (prevPosts.length > 0) { const fragment = document.createDocumentFragment(); prevPosts.forEach(prevPost => { const clonedPost = prevPost.cloneNode(true); fixDateFormat(clonedPost); // 각 클론된 게시글의 날짜 변환 clonedPost.style.backgroundColor = "Azure"; // : "#e6f7ff"; // 색상 적용 const url = clonedPost.href; const baseUrl = getBaseUrl(url); if (isPageVisited(baseUrl)) clonedPost.style.color = 'lightgray'; fragment.appendChild(clonedPost); }); const firstPost = mainBoard.querySelector('a.vrow.column:not(.notice)'); const thickSeparator = document.createElement('div'); thickSeparator.style.height = '2px'; thickSeparator.style.backgroundColor = 'gray'; thickSeparator.style.margin = '0'; if (firstPost && firstPost.parentNode) { firstPost.parentNode.insertBefore(fragment, firstPost); // firstPost.parentNode.insertBefore(thickSeparator, firstPost); } else { mainBoard.insertBefore(fragment, mainBoard.firstChild); // mainBoard.insertBefore(thickSeparator, mainBoard.firstChild); } } // console.log("34434"); } // 다음 페이지의 처음 2개 게시글 추가 const nextContainer = document.createElement('div'); await createGeneralPostSectionFromAdjacentPage2("next", 2, nextContainer); const nextPosts = nextContainer.querySelectorAll('a.vrow.column:not(.notice)'); if (nextPosts.length > 0) { const lastPost = mainBoard.querySelectorAll('a.vrow.column:not(.notice)')[mainBoard.querySelectorAll('a.vrow.column:not(.notice)').length - 1]; const thickSeparator = document.createElement('div'); thickSeparator.style.height = '2px'; thickSeparator.style.backgroundColor = 'gray'; thickSeparator.style.margin = '0'; if (lastPost && lastPost.parentNode) { // lastPost.parentNode.appendChild(thickSeparator); nextPosts.forEach(nextPost => { const clonedPost = nextPost.cloneNode(true); fixDateFormat(clonedPost); // 각 클론된 게시글의 날짜 변환 clonedPost.style.backgroundColor = 'rgb(255, 230, 235)'; // 색상 적용 const url = clonedPost.href; const baseUrl = getBaseUrl(url); if (isPageVisited(baseUrl)) clonedPost.style.color = 'lightgray'; lastPost.parentNode.appendChild(clonedPost); }); } else { // mainBoard.appendChild(thickSeparator); nextPosts.forEach(nextPost => { const clonedPost = nextPost.cloneNode(true); fixDateFormat(clonedPost); // 날짜 형식 수정 clonedPost.style.backgroundColor = "pink"; // 색상 적용 mainBoard.appendChild(clonedPost); }); } } } /////////////////////////////////////////////////////////////////////////////////////// // 사이드바 관리 모듈 const SidebarManager = { container: null, init: () => { SidebarManager.container = document.getElementById('adjacent-posts-container') || SidebarManager.createContainer(); }, createContainer: () => { const div = document.createElement('div'); div.id = 'adjacent-posts-container'; div.style.position = 'fixed'; div.style.top = '10px'; div.style.right = '10px'; document.body.appendChild(div); return div; }, getPosts: () => SidebarManager.container.querySelectorAll('a.vrow.column:not(.notice)'), addPost: (post) => { const link = document.createElement('a'); link.href = post.href; link.textContent = post.textContent || '게시글'; link.className = 'vrow column'; SidebarManager.container.appendChild(link); }, highlightPost: (post) => { post.style.transition = 'background-color 0.3s'; post.style.backgroundColor = '#ffeb3b'; setTimeout(() => {post.style.backgroundColor = ''}, 300); }, }; // 탐색 모듈 (fetch와 연동) const Navigation = { currentPage: 1, // 현재 페이지 번호 (실제로는 URL에서 가져와야 함) init: () => { Navigation.currentPage = new URL(window.location.href).searchParams.get('p') || 1; }, goToClosestUnreadBelow: async () => { const posts = SidebarManager.getPosts(); let currentIndex = Navigation.getActiveIndex(posts); if (currentIndex === -1) { // 현재 페이지에 해당 게시글이 없으면 인접 페이지 로드 const nextPosts = await fetchAdjacentPage(Navigation.currentPage + 1); nextPosts.forEach(post => SidebarManager.addPost(post)); } currentIndex = Navigation.getActiveIndex(SidebarManager.getPosts()); const unreadPost = Navigation.findClosestUnreadBelow(SidebarManager.getPosts(), currentIndex); if (unreadPost) { SidebarManager.highlightPost(unreadPost); setTimeout(() => {window.location.href = unreadPost.href}, 300); } }, getActiveIndex: (posts) => Array.from(posts).findIndex(post => post.href === window.location.href), findClosestUnreadBelow: (posts, startIndex) => { for (let i = startIndex + 1; i < posts.length; i++) { if (!Navigation.isPageVisited(posts[i].href.split('?')[0])) return posts[i]; } return null; }, isPageVisited: (url) => localStorage.getItem(url) === 'visited', // 방문 여부 확인 (예시) }; // 설정 관리 모듈 const Settings = { maxGauge: GM_getValue('maxGauge', 5), // Tampermonkey 값 가져오기 예시 anonymizeSetting: GM_getValue('anonymizeSetting', false), save: () => { GM_setValue('maxGauge', Settings.maxGauge); GM_setValue('anonymizeSetting', Settings.anonymizeSetting); }, }; // 초기화 및 단축키 설정 SidebarManager.init(); /* Navigation.init(); document.addEventListener('keydown', (e) => { if (e.shiftKey && e.key === 'S') { Navigation.goToClosestUnreadBelow(); } }); // 이후에는 Navigation.currentPage 사용 console.log(Navigation.currentPage); */ /////////////////////////////////////////////////////////////////////////////////////// // 페이지의 모든 요소의 날짜 형식을 동적으로 업데이트하는 함수 function updateDynamicDateFormat() { // datetime 속성을 가진 모든 요소를 찾음 const timeElements = document.querySelectorAll('time[datetime]'); timeElements.forEach(element => { // 요소가 댓글 컨테이너(.comment-wrapper) 내에 있는지 확인 if (!element.closest('.comment-wrapper')) { // 댓글 컨테이너 밖에 있는 경우에만 날짜 업데이트 수행 const datetime = element.getAttribute('datetime'); const postDate = new Date(datetime); // ISO 8601 형식 파싱 (예: 2025-03-16T18:03:46.000Z) const now = new Date(); // 현재 시간 const diffInHours = (now - postDate) / (1000 * 60 * 60); // 시간 차이 계산 (단위: 시간) // console.log(diffInHours); let formattedDate; if (diffInHours < 24) { // 24시간 이내: "HH:mm" 형식으로 표시 formattedDate = postDate.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' // 이게 중요 }); // 예: "18:03" // console.log(formattedDate); } else { // 24시간 초과: "YYYY. MM. DD" 형식으로 표시 const year = postDate.getFullYear(); const month = String(postDate.getMonth() + 1).padStart(2, '0'); // 월은 0부터 시작하므로 +1 const day = String(postDate.getDate()).padStart(2, '0'); formattedDate = `${year}. ${month}. ${day}`; // 예: "2025. 03. 16" } // 변환된 날짜로 텍스트 업데이트 element.textContent = formattedDate; } }); } // 페이지가 로드될 때 함수 실행 updateDynamicDateFormat(); let countT = 0; const intervalId = setInterval(() => { if (countT < 10) { // console.log(`작동: ${countT + 1}`); // 원하는 작업 실행 updateDynamicDateFormat(); countT++; } else { clearInterval(intervalId); // 10초 작동 후 멈춤 } }, 100); // 0.1초마다 실행 // 모든 게시글의 날짜를 변환하는 함수 function fixAllPostDates() { const allPosts = document.querySelectorAll('.article-list .vrow.column'); allPosts.forEach(post => { fixDateFormat(post); // console.log(post); }); } ////////////////////////////////////////////////////////////////////////////////////////// // 날짜 파싱 함수 function parseBoardDate(dateString) { // 예: "2025-03-17 02:54:01" 형식 처리 const [datePart, timePart] = dateString.split(' '); const [year, month, day] = datePart.split('-').map(Number); const [hour, minute, second] = timePart.split(':').map(Number); return new Date(year, month - 1, day, hour, minute, second); } // 날짜 형식 변환 함수 function formatPostDate(dateString) { const postDate = parseBoardDate(dateString); // 날짜를 Date 객체로 변환 const now = new Date(); // 현재 시간 const diffInHours = (now - postDate) / (1000 * 60 * 60); // 시간 차이 계산 if (diffInHours < 24) { // 24시간 이내: "HH:mm" 형식 return postDate.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit' }); } else { // 24시간 이상: "YYYY.MM.DD" 형식 const year = postDate.getFullYear(); const month = String(postDate.getMonth() + 1).padStart(2, '0'); const day = String(postDate.getDate()).padStart(2, '0'); return `${year}.${month}.${day}`; } } // 메인 게시판의 날짜 업데이트 function fixDateFormat(postElement) { const dateElem = postElement.querySelector('.col-date'); // 날짜가 있는 요소 if (dateElem) { const originalDate = dateElem.textContent; // 원래 날짜 문자열 const formattedDate = formatPostDate(originalDate); // 변환된 날짜 dateElem.textContent = formattedDate; // 화면에 반영 } } // 모든 게시글에 적용 document.querySelectorAll('.article-list .vrow.column').forEach(post => { fixDateFormat(post); }); function parseDateString(dateString) { const [datePart, timePart] = dateString.split(' '); const [year, month, day] = datePart.split('-').map(Number); const [hour, minute, second] = timePart.split(':').map(Number); return new Date(year, month - 1, day, hour, minute, second); } function toLocalDate(dateString) { const date = parseDateString(dateString); const offset = date.getTimezoneOffset() * 60000; // 로컬 시간대 오프셋 (밀리초) return new Date(date.getTime() - offset); // 로컬 시간으로 변환 } // (C) DOMContentLoaded 이벤트에서 실행하도록 추가 document.addEventListener("DOMContentLoaded", () => { addAdjacentPostsToMainBoard().catch(err => console.error(err)); }); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// let i = 1; // 작성자, 댓글 작성자, 사이드바 게시물 작성자 익명화 if (anony) { // loadFinished 상태를 주기적으로 확인하는 함수 let checkLoadFinished = setInterval(function() { if (loadFinished) { clearInterval(checkLoadFinished); // 주기적인 상태 확인 중지 const sidePosts = document.querySelectorAll('.user-info'); // 여러 필터링 조건을 한 번에 적용 const filteredSidePosts = Array.from(sidePosts).reduce((acc, post) => { if ( (!post.closest('.article-view') || !post.closest('.board-title')) && !post.closest('.board-article-list') && !post.closest('.included-article-list') && !post.closest('.nav') ) { acc.push(post); } return acc; }, []); filteredSidePosts.forEach( name => { name.style.whiteSpace = "pre"; // 전후의 공백 유지 name.textContent = '홀붕이 ' + i + ' '; i++; } ); } }, 100); } let j = i; if (anony2) { // 메인 페이지 게시글 익명화 function anonymizePosts() { // loadFinished 상태를 주기적으로 확인하는 함수 let checkLoadFinished = setInterval(function() { if (loadFinished) { clearInterval(checkLoadFinished); // 주기적인 상태 확인 중지 const sidePosts = document.querySelectorAll('.user-info'); // 여러 필터링 조건을 한 번에 적용 const filteredSidePosts = Array.from(sidePosts).reduce((acc, post) => { if ( (post.closest('.article-list') && !post.closest('.board-title'))|| (post.closest('.board-article-list') && !post.closest('.board-title')) || post.closest('.included-article-list') // && ) { acc.push(post); } return acc; }, []); filteredSidePosts.forEach( (element, index) => { if (element.textContent.trim() !== "*ㅎㅎ") { element.textContent = `홀붕이 ${j}`; j++; } } ); j = i; } }, 100); } // MutationObserver로 DOM 변화를 감지하여 anonymizePosts 함수 실행 const observer = new MutationObserver((mutationsList, observer) => { let loadFinished = false; mutationsList.forEach((mutation) => { if (mutation.type === 'childList' || mutation.type === 'attributes') { loadFinished = true; } }); if (loadFinished) { anonymizePosts(); } }); // 감시할 대상 노드와 옵션 설정 const config = { childList: true, subtree: true, attributes: true }; // 대상 노드 설정 (body 요소를 감시) observer.observe(document.body, config); // 초기 호출 anonymizePosts(); } /////////////////////////////////////////////////////////////////////////////////////////////////// getVisitedPages(); // 마지막으로 메인 게시글들 읽음 여부 다시 설정 let postPage = document.querySelector('.article-view'); // 있으면 글 페이지 const element44 = els.commentCounter; // 있으면 글 페이지 let boerdPage = document.querySelector('.board-article-list'); // 있으면 목록 페이지 const mainPage = postPage || boerdPage; let mainPosts = Array.from(mainPage.querySelectorAll(' a.vrow.column:not(.notice)')); //console.log(mainPosts); mainPosts.forEach((post) => { post.querySelectorAll('.vrow-preview').forEach(preview => preview.remove()); let url = post.href; const baseUrl = url.split('?')[0]; // 현재 페이지의 번호 추출 const currentPath = window.location.pathname; const currentParts = currentPath.split('/'); const currentNumber = currentParts[currentParts.length - 1]; // 게시글의 번호 추출 const postPath = new URL(baseUrl).pathname; const postParts = postPath.split('/'); const postNumber = postParts[postParts.length - 1]; // 읽음 여부 확인 + 번호 일치 여부 확인 if (isPageVisited(baseUrl) || currentNumber === postNumber) { post.style.color = 'lightgray'; } else { post.style.color = 'black'; } }); // 별의 색상 다시 지정 const starElement = document.querySelector('.ion-android-star'); if (starElement) { const style = document.createElement('style'); style.textContent = ` .ion-android-star::before { color: orange; } `; document.head.appendChild(style); } // 안읽은 답글 숫자 지정 //????? const allPosts = Array.from(document.querySelectorAll(' a.vrow.column:not(.notice)')); allPosts.forEach((post) => { if (getBaseUrl(post.href) !== getBaseUrl(location.href)) { const count = post.querySelector('.comment-count'); let comments; if (count === null) comments = 0; else comments = count.textContent.match(/\d+/)[0]; const url = post.href.split('?')[0]; getVisitedPages(); const recordedComments = visitedPages[url]; if (!recordedComments) return; if (comments > recordedComments.comment) { let colorDetermine = comments - recordedComments.comment; count.style.color = colorDetermine === 1 ? 'rgb(255,155,77)' : 'red'; // 댓글 숫자가 이 색(주황, 빨강)으로 표시됨 if (colorDetermine > 2) count.style.fontWeight = "bold"; // 3개 이상 쌓이면 굵은 글씨로 표시됨 } } }); // 새 댓글 색깔 바꾸기 function colorNewComment () { // 저장된 visitedPages를 객체로 불러오기 (기본값은 빈 객체) let stored = GM_getValue("visitedPages", "{}"); try { stored = JSON.parse(stored); } catch (e) { stored = {}; } const pageUrl = window.location.origin + window.location.pathname; // 저장된 데이터에 현재 페이지 방문 기록이 없으면 종료 if (!(stored[pageUrl] && stored[pageUrl].lastVisit)) { console.log("Your First Visit!"); return; } let tempLastVisitTime = stored[pageUrl].lastVisit; // 댓글 색칠 작업 (tempLastVisitTime을 기준으로) // 댓글 컨테이너 선택자를 '.comment-wrapper'로 변경 const comments = document.querySelectorAll('.comment-wrapper'); console.log("찾은 댓글 수:", comments.length); comments.forEach((comment, index) => { console.log(`댓글 ${index + 1} 처리 시작`); // 댓글 내의 시간 요소는 보통