네이버 카페 댓글 모아보기

네이버 카페 게시물에서 특정 유저 댓글을 모아보기

// ==UserScript==
// @name         네이버 카페 댓글 모아보기
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  네이버 카페 게시물에서 특정 유저 댓글을 모아보기 
// @author       로시커여워
// @match        https://cafe.naver.com/*
// @icon         https://littledeep.com/wp-content/uploads/2020/09/naver-icon-style.png
// @license      MIT
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const DEFAULT_USERS = ["챈나", "주머기", "정키마"];

    function loadUsers() {
        const saved = localStorage.getItem("targetUsers");
        return saved ? JSON.parse(saved) : [...DEFAULT_USERS];
    }

    function saveUsers(users) {
        localStorage.setItem("targetUsers", JSON.stringify(users));
    }

    let targetUsers = loadUsers();
    let listContainer = null;
    const seenComments = new Set();
    const userColors = new Map();
    let collapsed = false;

    function getUserColor(name) {
        if (userColors.has(name)) return userColors.get(name);
        const color = `hsl(${Math.floor(Math.random()*360)}, 70%, 50%)`;
        userColors.set(name, color);
        return color;
    }

    function removeModal() {
        const modal = window.top.document.querySelector("#commentModal");
        if (modal) modal.remove();
        const settings = window.top.document.querySelector("#settingsModal");
        if (settings) settings.remove();
        listContainer = null;
        seenComments.clear();
    }

    function openSettings() {
        if (window.top.document.querySelector("#settingsModal")) return;

        const settingsBox = window.top.document.createElement("div");
        settingsBox.id = "settingsModal";
        settingsBox.style.cssText = `
            position: fixed !important;
            top: 160px;
            left: 50%;
            transform: translateX(-50%);
            width: 360px;
            height: 280px;
            background: #fff;
            border: 2px solid #444;
            border-radius: 8px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.3);
            z-index: 2147483647 !important;
            display: flex;
            flex-direction: column;
            font-family: sans-serif;
        `;

        settingsBox.innerHTML = `
            <div id="settingsHeader" style="
                cursor: move;
                padding: 10px;
                background: #2c7;
                color: #fff;
                font-weight: bold;
                display: flex;
                justify-content: space-between;
                align-items: center;
                user-select: none;
            ">
                <span>멤버 설정</span>
                <button id="closeSettingsBtn" style="background:none;border:none;color:#fff;font-size:14px;cursor:pointer;">✕</button>
            </div>
            <div style="flex:1; padding:10px; display:flex; flex-direction:column; gap:10px;">
                <textarea id="userListInput" style="flex:1; resize:none; font-size:14px; padding:5px;">${targetUsers.join("\n")}</textarea>
                <button id="saveUsersBtn" style="align-self:flex-end; padding:5px 12px; cursor:pointer; border:1px solid #ccc; border-radius:4px; background:#f5f5f5;">저장</button>
            </div>
        `;

        window.top.document.body.appendChild(settingsBox);

        settingsBox.querySelector("#saveUsersBtn").addEventListener("click", () => {
            const newList = settingsBox.querySelector("#userListInput").value
                .split("\n")
                .map(s => s.trim())
                .filter(Boolean);
            targetUsers = newList;
            saveUsers(newList);
            settingsBox.remove();
        });

        settingsBox.querySelector("#closeSettingsBtn").addEventListener("click", () => {
            settingsBox.remove();
        });

        const header = settingsBox.querySelector("#settingsHeader");
        let isDragging = false, offsetX = 0, offsetY = 0;

        header.addEventListener("mousedown", (e) => {
            if (e.target.tagName === "BUTTON") return;
            isDragging = true;
            const rect = settingsBox.getBoundingClientRect();
            offsetX = e.clientX - rect.left;
            offsetY = e.clientY - rect.top;
            window.top.document.body.style.userSelect = "none";
        });

        window.top.document.addEventListener("mousemove", (e) => {
            if (isDragging) {
                settingsBox.style.left = `${e.clientX - offsetX}px`;
                settingsBox.style.top = `${e.clientY - offsetY}px`;
                settingsBox.style.right = "auto";
                settingsBox.style.transform = "none";
            }
        });

        window.top.document.addEventListener("mouseup", () => {
            isDragging = false;
            window.top.document.body.style.userSelect = "";
        });
    }

    function openModal() {
        if (window.top.document.querySelector("#commentModal")) return;

        const modalBox = window.top.document.createElement("div");
        modalBox.id = "commentModal";
        modalBox.style.cssText = `
            position: fixed !important;
            top: 100px;
            right: 10px;
            width: 420px;
            height: 600px;
            background: #fff;
            border: 2px solid #444;
            border-radius: 8px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.3);
            z-index: 2147483647 !important;
            display: flex;
            flex-direction: column;
            font-family: sans-serif;
            transition: height 0.3s ease;
        `;

        modalBox.innerHTML = `
            <div id="modalHeader" style="
                cursor: move;
                padding: 10px;
                background: #2c7;
                color: #fff;
                font-weight: bold;
                display: flex;
                justify-content: space-between;
                align-items: center;
                user-select: none;
            ">
                <span>멤버 댓글 모아보기</span>
                <div>
                    <button id="openSettingsBtn" style="background:none;border:none;color:#fff;font-size:14px;cursor:pointer;margin-right:5px;">멤버설정⚙</button>
                    <button id="toggleModal" style="background:none;border:none;color:#fff;font-size:14px;cursor:pointer;">⬆숨기기</button>
                </div>
            </div>
            <div id="comment-list" style="flex:1; overflow-y:auto; padding:5px; font-size:14px; line-height:1.4;"></div>
        `;

        window.top.document.body.appendChild(modalBox);

        listContainer = modalBox.querySelector("#comment-list");

        modalBox.querySelector("#openSettingsBtn").addEventListener("click", openSettings);

        modalBox.querySelector("#toggleModal").addEventListener("click", (e) => {
            collapsed = !collapsed;
            if (collapsed) {
                modalBox.style.height = "50px";
                modalBox.style.overflow = "hidden";
                e.target.innerText = "⬇펼치기";
            } else {
                modalBox.style.height = "600px";
                modalBox.style.overflow = "visible";
                e.target.innerText = "⬆숨기기";
            }
        });

        const header = modalBox.querySelector("#modalHeader");
        let isDragging = false, offsetX = 0, offsetY = 0;

        header.addEventListener("mousedown", (e) => {
            if (e.target.tagName === "BUTTON") return;
            isDragging = true;
            const rect = modalBox.getBoundingClientRect();
            offsetX = e.clientX - rect.left;
            offsetY = e.clientY - rect.top;
            window.top.document.body.style.userSelect = "none";
        });

        window.top.document.addEventListener("mousemove", (e) => {
            if (isDragging) {
                modalBox.style.left = `${e.clientX - offsetX}px`;
                modalBox.style.top = `${e.clientY - offsetY}px`;
                modalBox.style.right = "auto";
            }
        });

        window.top.document.addEventListener("mouseup", () => {
            isDragging = false;
            window.top.document.body.style.userSelect = "";
        });
    }

    function collectComments() {
        openModal();

        const comments = document.querySelectorAll(".comment_area, li.CommentItem, div.CommentItem");

        comments.forEach(comment => {
            const nicknameEl = comment.querySelector('[class*="nickname"], .comment_nickname');
            const textEl = comment.querySelector('[class*="text"], .text_comment');
            const timeEl = comment.querySelector('[class*="date"], .comment_date, .comment_info_date');
            const avatarEl = comment.querySelector("img");

            const nickname = (nicknameEl?.innerText || "").trim();
            const content = (textEl?.innerText || "").trim();
            const time = (timeEl?.innerText || "").trim();

            if (nickname && content && targetUsers.includes(nickname)) {
                const key = nickname + "::" + content + "::" + time;
                if (seenComments.has(key)) return;
                seenComments.add(key);

                const color = getUserColor(nickname);

                const item = document.createElement("div");
                item.style.borderBottom = "1px solid #eee";
                item.style.padding = "5px 0";

                const id = "comment-" + Math.random().toString(36).slice(2);
                comment.setAttribute("data-comment-id", id);

                let avatarHTML = "";
                if (avatarEl) {
                    avatarHTML = `<img src="${avatarEl.src}" style="width:25px;height:25px;border-radius:50%;margin-right:3px;">`;
                }

                item.innerHTML = `
                    <div style="font-weight:bold;margin-bottom:3px;display:flex;align-items:center;gap:6px;">
                        ${avatarHTML}
                        <span style="color:${color};">${nickname}</span>
                        <span style="color:#888;font-size:12px;margin-left:auto;">${time}</span>
                        <button style="margin-left:6px;padding:2px 6px;font-size:12px;cursor:pointer;border:1px solid #ccc;border-radius:4px;background:#f5f5f5;" data-target="${id}">이동</button>
                    </div>
                    <div>${content}</div>
                `;

                listContainer.appendChild(item);

                item.querySelector("button").addEventListener("click", (e) => {
                    const targetId = e.target.getAttribute("data-target");
                    const targetEl = document.querySelector(`[data-comment-id="${targetId}"]`);
                    if (targetEl) {
                        targetEl.scrollIntoView({behavior: "smooth", block: "center"});
                        targetEl.style.backgroundColor = "yellow";
                        setTimeout(() => targetEl.style.backgroundColor = "", 1000);
                    }
                });
            }
        });
    }

    function debounce(func, delay) {
        let timer;
        return function(...args) {
            clearTimeout(timer);
            timer = setTimeout(() => func.apply(this, args), delay);
        };
    }

    let lastUrl = location.href;
    new MutationObserver(() => {
        if (location.href !== lastUrl) {
            lastUrl = location.href;
            removeModal();
        }
    }).observe(document, {subtree: true, childList: true});

    window.addEventListener("load", () => {
        openModal();
        collectComments();

        const commentContainer = document.querySelector(".comment_list_area") || document.body;
        const observer = new MutationObserver(
            debounce(() => {
                collectComments();
            }, 500)
        );
        observer.observe(commentContainer, { childList: true, subtree: true });
    });
})();