나무위키 주석 추출기 (iOS 버전)

주석 번호와 설명을 복사할 수 있는 패널을 작고 세련된 UI로 제공하며, 페이지 내용과 URL 변경 시 자동 갱신합니다. iOS 호환 복사 기능 포함.

// ==UserScript==
// @name         나무위키 주석 추출기 (iOS 버전)
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  주석 번호와 설명을 복사할 수 있는 패널을 작고 세련된 UI로 제공하며, 페이지 내용과 URL 변경 시 자동 갱신합니다. iOS 호환 복사 기능 포함.
// @match        https://namu.wiki/*
// ==/UserScript==

(function () {
    'use strict';

    // 공통 색상
    const green = '#4CAF50';
    const greenTransparent = 'rgba(76, 175, 80, 0.8)';

    // UI 관련 전역 변수
    let collected = [];
    let panel, copyBtn, pre;
    let fixedContainer, smallButton, expandBtn;

    // 주석 수집 및 UI 업데이트 함수
    function collectAndUpdate() {
        const bodyText = document.body.innerText;
        const notes = [...bodyText.matchAll(/\[(\d+)\]/g)];
        const uniqueNotes = [...new Set(notes.map(m => m[0]))];
        const newCollected = [];

        uniqueNotes.forEach(n => {
            const parts = bodyText.split(n);
            if (parts.length >= 3) {
                const afterSecond = parts[2].trim();
                const nextLine = afterSecond.split(/\n/)[0];
                newCollected.push(`${n} ${nextLine}`);
            }
        });

        if (JSON.stringify(newCollected) !== JSON.stringify(collected)) {
            collected = newCollected;
            if (collected.length > 0) {
                pre.innerText = collected.join('\n');
            } else {
                pre.innerText = '(복사할 주석이 없습니다)';
            }
        }
    }

    // 복사 함수 (iOS Safari 대응)
    function copyText(text) {
        try {
            if (typeof GM_setClipboard === 'function') {
                GM_setClipboard(text);
            } else {
                const ta = document.createElement('textarea');
                ta.value = text;
                ta.style.position = 'fixed';
                ta.style.top = '-9999px';
                document.body.appendChild(ta);
                ta.focus();
                ta.select();
                document.execCommand('copy');
                document.body.removeChild(ta);
            }
        } catch (e) {
            alert('복사 실패: ' + e.message);
        }
    }

    // UI 생성 함수
    function createUI() {
        panel = document.createElement('div');
        panel.style.position = 'fixed';
        panel.style.bottom = '20px';
        panel.style.right = '20px';
        panel.style.zIndex = 9999;
        panel.style.background = 'white';
        panel.style.border = '1px solid #ccc';
        panel.style.padding = '10px';
        panel.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
        panel.style.maxWidth = '300px';
        panel.style.maxHeight = '400px';
        panel.style.overflow = 'auto';
        panel.style.fontSize = '13px';
        panel.style.lineHeight = '1.4';
        panel.style.display = 'none';

        copyBtn = document.createElement('button');
        copyBtn.innerText = '주석 복사';
        copyBtn.style.background = green;
        copyBtn.style.color = 'white';
        copyBtn.style.border = 'none';
        copyBtn.style.padding = '6px 12px';
        copyBtn.style.fontSize = '13px';
        copyBtn.style.borderRadius = '4px';
        copyBtn.style.cursor = 'pointer';

        copyBtn.onclick = () => {
            if (collected.length > 0) {
                copyText(collected.join('\n'));
                copyBtn.innerText = '복사됨!';
                setTimeout(() => { copyBtn.innerText = '주석 복사'; }, 1500);
            }
        };

        pre = document.createElement('pre');
        pre.style.whiteSpace = 'pre-wrap';
        pre.innerText = '(복사할 주석이 없습니다)';

        panel.appendChild(copyBtn);
        panel.appendChild(document.createElement('hr'));
        panel.appendChild(pre);

        const collapseBtn = document.createElement('div');
        collapseBtn.innerHTML = `
            <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="white" viewBox="0 0 24 24">
                <path d="M19 13H5v-2h14v2z"/>
            </svg>`;
        collapseBtn.style.position = 'absolute';
        collapseBtn.style.top = '6px';
        collapseBtn.style.right = '6px';
        collapseBtn.style.width = '20px';
        collapseBtn.style.height = '20px';
        collapseBtn.style.background = 'rgba(255, 0, 0, 0.8)';
        collapseBtn.style.borderRadius = '4px';
        collapseBtn.style.display = 'flex';
        collapseBtn.style.alignItems = 'center';
        collapseBtn.style.justifyContent = 'center';
        collapseBtn.style.cursor = 'pointer';
        collapseBtn.title = '최소화';

        collapseBtn.onclick = () => {
            panel.style.display = 'none';
            fixedContainer.style.display = 'flex';
        };

        panel.appendChild(collapseBtn);

        fixedContainer = document.createElement('div');
        fixedContainer.style.position = 'fixed';
        fixedContainer.style.bottom = '20px';
        fixedContainer.style.right = '20px';
        fixedContainer.style.zIndex = 9998;
        fixedContainer.style.display = 'flex';
        fixedContainer.style.alignItems = 'center';
        fixedContainer.style.gap = '4px';

        smallButton = document.createElement('button');
        smallButton.innerText = '주석 복사';
        smallButton.style.background = green;
        smallButton.style.color = 'white';
        smallButton.style.border = 'none';
        smallButton.style.padding = '6px 12px';
        smallButton.style.fontSize = '13px';
        smallButton.style.borderRadius = '4px';
        smallButton.style.cursor = 'pointer';
        smallButton.style.flexShrink = '0';
        smallButton.style.height = '28px';

        smallButton.onclick = () => {
            if (collected.length > 0) {
                copyText(collected.join('\n'));
                smallButton.innerText = '복사됨!';
                setTimeout(() => { smallButton.innerText = '주석 복사'; }, 1500);
            }
        };

        expandBtn = document.createElement('button');
        expandBtn.title = '펼치기';
        expandBtn.style.background = greenTransparent;
        expandBtn.style.border = 'none';
        expandBtn.style.borderRadius = '4px';
        expandBtn.style.height = '28px';
        expandBtn.style.width = '28px';
        expandBtn.style.padding = '0';
        expandBtn.style.display = 'flex';
        expandBtn.style.alignItems = 'center';
        expandBtn.style.justifyContent = 'center';
        expandBtn.style.cursor = 'pointer';
        expandBtn.style.flexShrink = '0';

        expandBtn.innerHTML = `
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="white" viewBox="0 0 24 24">
                <path d="M4 4h6v2H6v4H4V4zm16 0v6h-2V6h-4V4h6zm0 16h-6v-2h4v-4h2v6zM4 20v-6h2v4h4v2H4z"/>
            </svg>`;

        expandBtn.onclick = () => {
            panel.style.display = 'block';
            fixedContainer.style.display = 'none';
        };

        fixedContainer.appendChild(smallButton);
        fixedContainer.appendChild(expandBtn);
        document.body.appendChild(panel);
        document.body.appendChild(fixedContainer);
    }

    // URL 변경 감지용 함수
    function onUrlChange(callback) {
        window.addEventListener('popstate', callback);

        const pushState = history.pushState;
        history.pushState = function (...args) {
            const result = pushState.apply(this, args);
            callback();
            return result;
        };

        const replaceState = history.replaceState;
        history.replaceState = function (...args) {
            const result = replaceState.apply(this, args);
            callback();
            return result;
        };
    }

    // 초기 실행
    createUI();
    collectAndUpdate();

    // DOM 변화 감지
    const observer = new MutationObserver(() => {
        collectAndUpdate();
    });
    observer.observe(document.body, { childList: true, subtree: true, characterData: true });

    // URL 변경 시 수집
    onUrlChange(() => {
        setTimeout(collectAndUpdate, 500);
    });
})();