정이용

// @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('상태 리셋 완료');
    }

})();
이 게시물에 이메일로 답장하기