// ==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
// ==/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"); // 클릭한 위치에서 가장 가까운 <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() {
// 첫 번째 요소: <span class="title-comment-count">[0]</span>
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;
}
}
// 두 번째 요소: <span class="body comment-count">0</span>
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);
*/
///////////////////////////////////////////////////////////////////////////////////////
// 페이지의 모든 <t> 요소의 날짜 형식을 동적으로 업데이트하는 함수
function updateDynamicDateFormat() {
// datetime 속성을 가진 모든 <t> 요소를 찾음
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} 처리 시작`);
// 댓글 내의 시간 요소는 보통 <time> 태그에 datetime 속성이 있음
const timeElement = comment.querySelector('time[datetime]');
console.log(`댓글 ${index + 1}: 시간 요소`, timeElement);
if (timeElement) {
const datetimeAttr = timeElement.getAttribute('datetime');
console.log(`댓글 ${index + 1}: datetime 속성 값: ${datetimeAttr}`);
const commentTime = new Date(datetimeAttr);
console.log(`댓글 ${index + 1}: 파싱된 시간: ${commentTime}`);
const tempVisitTime = new Date(tempLastVisitTime);
console.log(`나의 마지막 방문 시간: ${tempVisitTime}`);
if (commentTime > tempVisitTime) {
console.log(`댓글 ${index + 1}: 새로운 댓글로 판단되어 배경색 변경`);
const message = comment.querySelector('.content .message');
if (message) {
message.style.backgroundColor = '#FFFFE0';
}
} else {
console.log(`댓글 ${index + 1}: 새 댓글이 아님`);
}
} else {
console.log(`댓글 ${index + 1}: 시간 요소를 찾을 수 없음`);
}
});
}
function getSidebarPosts() {
const sidebarContainer = document.getElementById('adjacent-posts-container');
if (!sidebarContainer) return [];
return Array.from(sidebarContainer.querySelectorAll('a.vrow.column:not(.notice)'));
}
function getActiveIndexInSidebar(posts) {
return 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;
}
});
}
function findClosestUnreadAbove(posts, activeIndex) {
for (let i = activeIndex - 1; i >= 0; i--) {
const url = posts[i].href.split('?')[0];
if (!isPageVisited(url)) {
return posts[i];
}
}
return null;
}
function findClosestUnreadBelow(posts, activeIndex) {
for (let i = activeIndex + 1; i < posts.length; i++) {
const url = posts[i].href.split('?')[0];
if (!isPageVisited(url)) {
return posts[i];
}
}
return null;
}
function isArticlePage() {
return document.querySelector('.article-view') !== null;
}
function goToClosestUnreadAbove() {
if (!isArticlePage()) return;
const posts = getSidebarPosts();
let activeIndex = getActiveIndexInSidebar(posts);
if (activeIndex === -1) return;
let unreadPost = findClosestUnreadAbove(posts, activeIndex);
// 현재 글과 같은 URL은 건너뛰기
while (unreadPost && unreadPost.href === window.location.href) {
activeIndex = posts.indexOf(unreadPost);
unreadPost = findClosestUnreadAbove(posts, activeIndex);
}
if (unreadPost) {
window.location.href = unreadPost.href;
}
}
function goToClosestUnreadBelow() {
if (!isArticlePage()) return; // 게시글 페이지가 아닌 경우 종료
const posts = getSidebarPosts(); // 사이드바 게시글 목록 가져오기
let activeIndex = getActiveIndexInSidebar(posts); // 현재 활성 게시글 인덱스 찾기
if (activeIndex === -1) {
console.log("활성 게시글을 찾을 수 없습니다.");
return;
}
let unreadPost = findClosestUnreadBelow(posts, activeIndex); // 아래쪽 안 읽은 게시글 찾기
if (unreadPost) {
window.location.href = unreadPost.href; // 안 읽은 게시글이 있으면 이동
} else {
console.log("아래에 안 읽은 게시글이 없습니다."); // 없으면 이동하지 않음
}
}
////////////////////////////////////////////////////////////////////////////////////////
async function fetchAdjacentPage(page) {
const url = new URL(window.location.href); // 현재 URL 기반으로
url.searchParams.set('p', page); // 페이지 번호 변경
const response = await fetch(url); // HTML 요청
const html = await response.text(); // 텍스트로 받기
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html'); // DOM으로 변환
return doc.querySelectorAll('a.vrow.column:not(.notice)'); // 게시글만 추출
}
function addShortcutGuide() {
const guide = document.createElement('div');
guide.id = 'shortcut-guide';
guide.style.position = 'fixed';
guide.style.bottom = '10px';
guide.style.right = '10px';
guide.style.background = '#fff';
guide.style.padding = '10px';
guide.style.border = '1px solid #ccc';
guide.innerHTML = `
<style>
kbd {
background-color: #FFC0CB; /* 분홍색 배경 */
color: #000000; /* 검은색 글씨 */
border: 1px solid #ccc;
border-radius: 3px;
padding: 2px 4px;
font-family: monospace;
}
</style>
<h3>단축키 안내 (<kbd>Shift + H</kbd>로 토글)</h3>
<h4>단축키</h4>
<ul>
<li><kbd>f</kbd>: 추천 (하드모드)</li>
<li><kbd>d</kbd>: 아래로 스크롤(빠름/느림)</li>
<li><kbd>n</kbd>: 세로 모드 요소 숨기기</li>
<li><kbd>g</kbd>: 게시판 첫 글/새 댓글 새로고침</li>
<li><kbd>h</kbd>: 익명화 토글</li>
</ul>
<h4>Shift + 단축키</h4>
<ul>
<li><kbd>Shift + Q</kbd>: 홀로라이브 채널로 이동</li>
<li><kbd>Shift + D</kbd>: 위로 스크롤(빠름/느림)</li>
<li><kbd>Shift + A</kbd>: 위쪽 안 읽은 글</li>
<li><kbd>Shift + S</kbd>: 아래쪽 안 읽은 글</li>
</ul>
`;
document.body.appendChild(guide);
guide.style.display = 'none';
document.addEventListener('keydown', (e) => {
if (e.shiftKey && e.key === 'H') {
guide.style.display = guide.style.display === 'none' ? 'block' : 'none';
}
});
}
addShortcutGuide();
})();