// @name Instagram 추천게시물 무한 스크롤 차단
// @namespace http://tampermonkey.net/
// @version 2.0
// @description 인스타그램에서 추천게시물 무한 스크롤을 차단합니다
// @author You
// @match https://www.instagram.com/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
let endOfFeedReached = false;
let isScrollBlocked = false;
let lastScrollY = 0;
let scrollCheckDelay = null;
// 현재 페이지가 메인 피드인지 확인 (더 관대한 조건)
function isMainFeed() {
const currentUrl = window.location.pathname;
const hostname = window.location.hostname;
// 인스타그램 메인 도메인이 아니면 false
if (!hostname.includes('instagram.com')) {
return false;
}
// 명확히 제외해야 하는 페이지들만 체크
const excludePatterns = [
'/stories/', // 스토리만 명확히 제외
'/p/', // 개별 포스트
'/reel/', // 개별 릴스
'/direct/', // 다이렉트 메시지
'/accounts/', // 계정 설정
];
// 제외 패턴에 해당하는지 확인
for (const pattern of excludePatterns) {
if (currentUrl.includes(pattern)) {
return false;
}
}
// 나머지는 모두 허용 (홈 피드, 탐색, 프로필 등)
return true;
}
// 스토리 모달이 열려있는지 확인 (더 정확한 감지)
function isStoryModalOpen() {
// URL에 스토리 관련 정보가 있는지 먼저 확인
if (window.location.pathname.includes('/stories/')) {
return true;
}
// 스토리 모달의 특징적인 요소들 확인
const storyElements = document.querySelectorAll([
'div[role="dialog"][style*="transform"]', // 스토리 모달
'section[role="dialog"]', // 스토리 섹션
'div[data-testid="story-viewer"]', // 스토리 뷰어
'canvas[style*="position: absolute"]' // 스토리 캔버스
].join(','));
// 스토리 특유의 큰 모달이 있는지 확인
for (const element of storyElements) {
const style = window.getComputedStyle(element);
const rect = element.getBoundingClientRect();
// 전체 화면을 덮는 요소이고 보이는 상태인지 확인
if (style.display !== 'none' &&
style.visibility !== 'hidden' &&
rect.width > window.innerWidth * 0.7 &&
rect.height > window.innerHeight * 0.7) {
return true;
}
}
return false;
}
// 원래 fetch 함수 저장
const originalFetch = window.fetch;
// fetch 요청 가로채기 (추천게시물 API 차단) - 메인 피드에서만
window.fetch = function(...args) {
const url = args[0];
if (endOfFeedReached && typeof url === 'string' && isMainFeed() && !isStoryModalOpen()) {
// 추천게시물 관련 API 요청 차단
if (url.includes('/feed/timeline/') ||
url.includes('/feed/reels_media/') ||
url.includes('__a=1') ||
url.includes('suggested_users') ||
url.includes('explore/grid')) {
console.log('추천게시물 API 요청 차단됨:', url);
return Promise.reject(new Error('Blocked by user script'));
}
}
return originalFetch.apply(this, args);
};
// 현재 스크롤이 페이지 하단에 근접했는지 확인
function isNearBottom() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
// 하단에서 200px 이내에 있는지 확인
return (scrollTop + windowHeight) >= (documentHeight - 200);
}
// 추천게시물 영역에 있는지 확인
function isInSuggestedSection() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const windowHeight = window.innerHeight;
// "모두 확인했습니다" 메시지를 찾아서 그 위치 확인
const endMessages = [
"모두 확인했습니다",
"최근 3일 동안 새롭게 올라온 게시물을 모두 확인했습니다"
];
let endMessagePosition = null;
// 페이지의 모든 요소를 확인하여 끝 메시지 위치 찾기
const allElements = document.querySelectorAll('*');
for (const element of allElements) {
const text = element.textContent || element.innerText || '';
if (endMessages.some(message => text.includes(message))) {
const rect = element.getBoundingClientRect();
endMessagePosition = rect.top + scrollTop;
break;
}
}
// 끝 메시지가 발견되고, 현재 스크롤 위치가 그 메시지보다 아래에 있으면 추천 영역
if (endMessagePosition !== null) {
return scrollTop > endMessagePosition - windowHeight;
}
return false;
}
// 페이지에서 끝 메시지가 존재하는지 확인 (위치 무관)
function hasEndMessage() {
const endMessages = [
"모두 확인했습니다",
"최근 3일 동안 새롭게 올라온 게시물을 모두 확인했습니다",
"회원님을 위한 추천 게시물을 더 둘러보려면 계속 확인해보세요"
];
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
while (node = walker.nextNode()) {
const text = node.textContent.trim();
if (endMessages.some(message => text.includes(message))) {
return true;
}
}
return false;
}
// 특정 요소가 화면에 보이는지 확인
function isElementVisible(element) {
const rect = element.getBoundingClientRect();
const windowHeight = window.innerHeight;
// 요소가 화면 하단 근처에 보이는지 확인
return rect.top < windowHeight && rect.bottom > windowHeight * 0.7;
}
// 피드 끝 메시지 감지 (정확한 텍스트 매칭 + 스크롤 위치 확인) - 메인 피드에서만
function checkForEndOfFeed() {
// 메인 피드가 아니거나 스토리가 열려있으면 체크하지 않음
if (!isMainFeed() || isStoryModalOpen()) {
return;
}
// 이미 추천 영역에 있거나 하단 근처에 있을 때만 체크
if (!isNearBottom() && !isInSuggestedSection()) {
return;
}
const endMessages = [
"모두 확인했습니다",
"최근 3일 동안 새롭게 올라온 게시물을 모두 확인했습니다",
"회원님을 위한 추천 게시물을 더 둘러보려면 계속 확인해보세요",
"You're all caught up",
"You've seen all new posts"
];
let foundEndMessage = false;
let endMessageElement = null;
// 모든 텍스트 노드에서 검색
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
while (node = walker.nextNode()) {
const text = node.textContent.trim();
if (endMessages.some(message => text.includes(message))) {
// 해당 텍스트를 포함한 부모 요소 찾기
let parent = node.parentElement;
while (parent && parent !== document.body) {
// 스크롤 위치와 관계없이 메시지가 존재하면 감지
foundEndMessage = true;
endMessageElement = parent;
break;
}
if (foundEndMessage) break;
}
}
// 특정 요소들도 확인
if (!foundEndMessage) {
const endIndicators = document.querySelectorAll([
'div[style*="text-align: center"]',
'div[role="button"]',
'span',
'div'
].join(','));
for (let element of endIndicators) {
const text = element.textContent || element.innerText || '';
if (endMessages.some(message => text.includes(message))) {
foundEndMessage = true;
endMessageElement = element;
break;
}
}
}
if (foundEndMessage && !endOfFeedReached) {
console.log('피드 끝 메시지 감지됨');
console.log('감지된 요소:', endMessageElement);
console.log('현재 상태 - 메인 피드:', isMainFeed(), '스토리 열림:', isStoryModalOpen());
console.log('하단 근처:', isNearBottom(), '추천 영역:', isInSuggestedSection());
endOfFeedReached = true;
activateBlocking();
}
}
// 차단 활성화
function activateBlocking() {
// 메인 피드가 아니거나 스토리가 열려있으면 차단하지 않음
if (!isMainFeed() || isStoryModalOpen()) {
console.log('차단 조건 불만족 - 메인 피드:', isMainFeed(), '스토리 열림:', isStoryModalOpen());
return;
}
// "모두 확인했습니다" 메시지 위치 찾기
const endMessagePosition = getEndMessagePosition();
if (endMessagePosition === null) {
console.log('끝 메시지 위치를 찾을 수 없어 일반 차단 적용');
// 끝 메시지를 찾을 수 없으면 현재 위치에서 차단
const currentScrollY = window.scrollY;
applyScrollBlocking(currentScrollY);
} else {
console.log('끝 메시지 위치에서 차단 적용:', endMessagePosition);
// 끝 메시지 위치를 기준으로 차단
applyScrollBlocking(endMessagePosition);
}
// 추천게시물 섹션 제거
removeRecommendationSections();
isScrollBlocked = true;
}
// "모두 확인했습니다" 메시지의 Y 위치 반환
function getEndMessagePosition() {
const endMessages = [
"모두 확인했습니다",
"최근 3일 동안 새롭게 올라온 게시물을 모두 확인했습니다"
];
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
while (node = walker.nextNode()) {
const text = node.textContent.trim();
if (endMessages.some(message => text.includes(message))) {
let parent = node.parentElement;
while (parent && parent !== document.body) {
const rect = parent.getBoundingClientRect();
if (rect.height > 0 && rect.width > 0) {
// 요소의 상단 위치를 페이지 절대 좌표로 변환
return rect.top + window.pageYOffset;
}
parent = parent.parentElement;
}
}
}
return null;
}
// 실제 스크롤 차단 적용
function applyScrollBlocking(blockPosition) {
// 여유분 추가 (메시지 아래로 약간 더 스크롤 가능)
const allowedPosition = blockPosition + 100;
// 스크롤 이벤트 차단
function preventScroll(e) {
if (window.scrollY > allowedPosition) {
window.scrollTo(0, allowedPosition);
e.preventDefault();
e.stopPropagation();
return false;
}
}
// 휠 이벤트 차단 (마우스 스크롤)
function preventWheel(e) {
if (e.deltaY > 0 && window.scrollY >= allowedPosition - 50) {
e.preventDefault();
e.stopPropagation();
return false;
}
}
// 터치 이벤트 차단 (모바일)
let startY = 0;
function preventTouchStart(e) {
startY = e.touches[0].clientY;
}
function preventTouchMove(e) {
const currentY = e.touches[0].clientY;
if (startY > currentY && window.scrollY >= allowedPosition - 50) {
e.preventDefault();
e.stopPropagation();
return false;
}
}
// 이벤트 리스너 등록
document.addEventListener('scroll', preventScroll, { passive: false, capture: true });
document.addEventListener('wheel', preventWheel, { passive: false, capture: true });
document.addEventListener('touchstart', preventTouchStart, { passive: false, capture: true });
document.addEventListener('touchmove', preventTouchMove, { passive: false, capture: true });
console.log(`스크롤 차단 활성화 - 허용 위치: ${allowedPosition}px`);
}
// 추천게시물 섹션 제거
function removeRecommendationSections() {
// 다양한 선택자로 추천게시물 섹션 찾기
const selectors = [
'article[data-testid*="post"]',
'div[role="button"]:has-text("관심 없음")',
'section[aria-label*="피드"]',
'div[style*="padding"]'
];
// "회원님을 위한 추천" 텍스트가 있는 요소들 숨기기
const allElements = document.querySelectorAll('*');
allElements.forEach(element => {
const text = element.textContent || element.innerText || '';
if (text.includes('회원님을 위한 추천') ||
text.includes('추천 게시물') ||
text.includes('Suggested for you') ||
text.includes('More posts from')) {
// 해당 요소의 상위 컨테이너 찾기
let parent = element;
for (let i = 0; i < 5; i++) {
parent = parent.parentElement;
if (!parent) break;
// article이나 큰 컨테이너 발견시 숨기기
if (parent.tagName === 'ARTICLE' ||
parent.classList.contains('_ac7v') ||
parent.style.minHeight) {
parent.style.display = 'none';
console.log('추천게시물 섹션 숨김');
break;
}
}
}
});
}
// DOM 변화 감지 - 메인 피드에서만
const observer = new MutationObserver((mutations) => {
if (!endOfFeedReached && isMainFeed() && !isStoryModalOpen()) {
clearTimeout(scrollCheckDelay);
scrollCheckDelay = setTimeout(checkForEndOfFeed, 500);
} else if (endOfFeedReached && isMainFeed()) {
// 피드 끝 도달 후 새로운 컨텐츠 로딩 방지
removeRecommendationSections();
}
});
// 스크롤 이벤트로도 체크 (하단 근처나 추천 영역에서, 메인 피드에서만)
let scrollTimeout;
window.addEventListener('scroll', () => {
if (!endOfFeedReached && isMainFeed() && !isStoryModalOpen() &&
(isNearBottom() || isInSuggestedSection())) {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(checkForEndOfFeed, 500);
}
}, { passive: true });
// 초기화
function initialize() {
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true
});
// 주기적 체크 (하단 근처나 추천 영역에서, 메인 피드에서만)
const intervalCheck = setInterval(() => {
if (!endOfFeedReached && isMainFeed() && !isStoryModalOpen() &&
(isNearBottom() || isInSuggestedSection())) {
checkForEndOfFeed();
} else if (endOfFeedReached && isMainFeed() && !isStoryModalOpen()) {
removeRecommendationSections();
}
}, 1000);
console.log('📱 Instagram 추천게시물 차단 스크립트 v2.6 활성화');
console.log('현재 페이지:', window.location.pathname);
console.log('메인 피드 여부:', isMainFeed());
console.log('스토리 모달 여부:', isStoryModalOpen());
console.log('끝 메시지 존재:', hasEndMessage());
// 끝 메시지 위치 표시
const endPos = getEndMessagePosition();
if (endPos) {
console.log('끝 메시지 위치:', endPos + 'px');
}
// 페이지 언로드 시 정리
window.addEventListener('beforeunload', () => {
clearInterval(intervalCheck);
observer.disconnect();
});
}
// 페이지 로드 대기
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
// SPA 페이지 이동 감지 및 상태 관리
let currentUrl = location.href;
let currentPathname = location.pathname;
let wasInStory = false;
const urlCheckInterval = setInterval(() => {
const newUrl = location.href;
const newPathname = location.pathname;
const currentlyInStory = isStoryModalOpen();
// URL이나 pathname이 변경되었거나, 스토리 상태가 변경된 경우
if (newUrl !== currentUrl || newPathname !== currentPathname || wasInStory !== currentlyInStory) {
const prevUrl = currentUrl;
const prevPathname = currentPathname;
currentUrl = newUrl;
currentPathname = newPathname;
// 스토리에서 나왔거나 페이지가 변경된 경우 상태 리셋
if (wasInStory && !currentlyInStory) {
console.log('🔄 스토리에서 메인 피드로 복귀 - 상태 리셋');
resetState();
} else if (prevPathname !== newPathname) {
console.log('🔄 페이지 이동 감지:', prevPathname, '->', newPathname);
resetState();
}
wasInStory = currentlyInStory;
// 상태 로깅
console.log('페이지 상태 업데이트:', {
url: newPathname,
isMainFeed: isMainFeed(),
isStoryOpen: currentlyInStory,
endOfFeedReached: endOfFeedReached
});
}
}, 500); // 더 자주 체크 (0.5초마다)
// 상태 리셋 함수
function resetState() {
endOfFeedReached = false;
isScrollBlocked = false;
// 스토리에서 복귀했을 때 추천게시물이 이미 로딩되어 있는지 확인
setTimeout(() => {
if (isMainFeed() && !isStoryModalOpen() && hasEndMessage()) {
console.log('🔍 스토리 복귀 후 이미 추천게시물이 로딩된 상태 감지');
checkForEndOfFeed();
}
}, 1000);
console.log('상태 리셋 완료');
}
})();