Wrtn Chat Exporter (v1.24 - No Tags)

[No Tags Update] AI/User 태그를 제거하고 텍스트만 추출합니다. 모든 채팅 타입을 구조적으로 감지합니다.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Wrtn Chat Exporter (v1.24 - No Tags)
// @namespace    http://tampermonkey.net/
// @version      1.24
// @description  [No Tags Update] AI/User 태그를 제거하고 텍스트만 추출합니다. 모든 채팅 타입을 구조적으로 감지합니다.
// @author       You
// @match        https://crack.wrtn.ai/stories/*
// @match        https://wrtn.ai/stories/*
// @grant        none
// @license      CC-BY-NC-ND-4.0
// ==/UserScript==

(function() {
    'use strict';

    // --- 설정: 구조적/의미적 선택자 사용 ---
    const CHAT_AREA_SELECTOR = "div[class*='ChatList__ChatListWrapper'], div.css-nyz712";
    const COMMON_MESSAGE_WRAPPER = "div.css-8atqhb";
    const AI_SEMANTIC_IDENTIFIER = 'img[alt="model"]';
    const AI_TEXT_CONTAINER = 'div.css-ae5fn1';
    const USER_TEXT_CONTAINER = 'div.css-192kozn';

    // --- 버튼 생성 로직 (Fallback 포함) ---
    window.addEventListener('load', createFloatingExportButtonIfNotExists);
    let lastUrl = location.href;
    new MutationObserver(() => {
     const url = location.href;
     if (url !== lastUrl) {
      lastUrl = url;
      setTimeout(createFloatingExportButtonIfNotExists, 1000);
     }
    }).observe(document, {subtree: true, childList: true});
    setInterval(createFloatingExportButtonIfNotExists, 2000); // 2초마다 버튼 존재 확인

    function createFloatingExportButtonIfNotExists() {
         const isTargetPage = window.location.href.includes("/stories/");
         const existingButton = document.getElementById('chat-export-button');
         if (isTargetPage) {
             if (!existingButton) createFloatingExportButton();
         } else {
             if (existingButton) {
                 existingButton.remove();
                 console.log("[Wrtn Exporter] Target page left. Button removed.");
             }
         }
    }

     function createFloatingExportButton() {
        if (document.getElementById('chat-export-button')) return;
        const button = document.createElement('button');
        button.id = 'chat-export-button';
        button.innerHTML = '📜 채팅 내용 저장';
        button.style.cssText = `
            position: fixed; bottom: 20px; right: 20px; z-index: 99999;
            padding: 10px 15px; font-size: 14px; font-weight: bold; color: white;
            background-color: #5E35B1; border: none; border-radius: 25px;
            cursor: pointer; box-shadow: 0 4px 8px rgba(0,0,0,0.3);
            transition: all 0.2s ease-in-out;
        `;
        button.onmouseover = () => button.style.transform = 'scale(1.05)';
        button.onmouseout = () => button.style.transform = 'scale(1)';

        button.addEventListener('click', async () => {
            console.log("--- [Wrtn Exporter] 버튼 클릭됨! 채팅 추출을 시작합니다. ---");
            button.disabled = true;
            button.innerHTML = '...내용 불러오는 중...';
            try {
                await exportChat();
            } catch (e) {
                console.error("[Wrtn Exporter] 스크립트 실행 중 오류 발생:", e);
                button.innerHTML = `⚠️ 오류! (F12 > Console 확인)`;
            } finally {
                setTimeout(() => {
                    const currentButton = document.getElementById('chat-export-button');
                    if(currentButton) {
                        currentButton.disabled = false;
                        currentButton.innerHTML = '📜 채팅 내용 저장';
                    }
                }, 3000);
            }
        });
        document.body.appendChild(button);
        console.log("[Wrtn Exporter] 채팅 내용 저장 버튼 생성됨.");
     }


    // 채팅 내용 추출 및 저장 함수
    async function exportChat() {
        console.log("1. 채팅 구역을 찾습니다...");
        const chatArea = findChatArea(COMMON_MESSAGE_WRAPPER);
        if (!chatArea) {
            console.error("오류: 채팅 구역을 찾을 수 없습니다! (CHAT_AREA_SELECTOR 또는 COMMON_MESSAGE_WRAPPER가 변경되었을 수 있습니다)");
            alert("채팅 스크롤 영역을 찾을 수 없습니다. 사이트 구조가 변경되었을 수 있습니다. 개발자 도구(F12) > Console 탭에서 자세한 오류를 확인하세요.");
            return;
        }
        console.log("채팅 구역 찾기 성공!", chatArea);

        // --- 1. 모든 내용 로드를 위해 스크롤 ---
        console.log("2. 모든 내용을 불러오기 위해 스크롤을 시작합니다.");
        let lastHeight = 0;
        let noChangeCount = 0;
        const MAX_NO_CHANGE = 3;
        while (noChangeCount < MAX_NO_CHANGE) {
            chatArea.scrollTop = 0; // 스크롤을 맨 위로 올림
            await new Promise(resolve => setTimeout(resolve, 1500)); // 로딩 대기
            const newHeight = chatArea.scrollHeight;
            if (newHeight === lastHeight) {
                noChangeCount++;
                console.log(`스크롤 높이 변화 없음 (${noChangeCount}/${MAX_NO_CHANGE})`);
            } else {
                noChangeCount = 0;
                lastHeight = newHeight;
                console.log("스크롤 중... 현재 높이:", newHeight);
            }
        }
        console.log("스크롤 완료: 더 이상 새로운 내용이 없거나 로딩이 완료되었습니다.");

        // --- 2. [수정됨] 텍스트 추출 (태그 제거) ---
        console.log("3. 채팅 구역 내 모든 공통 메시지 래퍼의 텍스트를 찾습니다. 선택자:", COMMON_MESSAGE_WRAPPER);
        const allMessageWrappers = chatArea.querySelectorAll(COMMON_MESSAGE_WRAPPER);
        console.log(allMessageWrappers.length, "개의 공통 메시지 래퍼를 찾았습니다.");

        if (allMessageWrappers.length === 0) {
            console.error(`오류: 공통 메시지 래퍼(${COMMON_MESSAGE_WRAPPER})를 하나도 찾을 수 없습니다! 사이트 구조가 크게 변경되었을 수 있습니다.`);
            alert(`메시지 래퍼(${COMMON_MESSAGE_WRAPPER})를 찾을 수 없습니다. 사이트 구조가 변경되었을 수 있습니다.`);
            return;
        }

        const messageTexts = [];
        allMessageWrappers.forEach(wrapper => {
            // [수정] prefix 변수 자체가 필요 없어짐
            let textContentElement = null;

            const isAI = wrapper.querySelector(AI_SEMANTIC_IDENTIFIER);

            if (isAI) {
                // AI 메시지인 경우
                textContentElement = wrapper.querySelector(AI_TEXT_CONTAINER);
                if (!textContentElement) {
                    console.warn("AI_TEXT_CONTAINER를 찾지 못함. 래퍼 전체로 대체.", wrapper);
                    textContentElement = wrapper;
                }
            } else {
                // 사용자 메시지로 간주
                textContentElement = wrapper.querySelector(USER_TEXT_CONTAINER);
                if (!textContentElement) {
                    console.warn("USER_TEXT_CONTAINER를 찾지 못함. 래퍼 전체로 대체.", wrapper);
                    textContentElement = wrapper;
                }
            }

            // 텍스트 추출 및 추가
            if (textContentElement) {
                const text = textContentElement.innerText.trim();
                if (text) {
                    // [수정] prefix 없이 text만 push
                    messageTexts.push(text);
                }
            } else {
                 console.warn("알 수 없는 메시지 유형 발견 (무시됨):", wrapper);
            }
        });

        // 시간 순서(오래된->최신)로 맞추기 위해 뒤집습니다.
        messageTexts.reverse();

        // 각 메시지 사이에 구분선 추가
        const finalText = messageTexts.join('\n\n' + '-'.repeat(30) + '\n\n');


        // --- 3. 파일로 다운로드 ---
        console.log("4. 파일 다운로드를 시작합니다...");
        const timestamp = new Date().toISOString().slice(0, 19).replace('T', '_').replace(/:/g, '-');
        const fileName = `chat_log_${timestamp}.txt`;
        const blob = new Blob([finalText], { type: 'text/plain;charset=utf-8' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = fileName;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
        console.log("파일 다운로드 완료!");
    }

    // 채팅 스크롤 영역을 찾는 함수
    function findChatArea(messageSelector) {
        let area = document.querySelector(CHAT_AREA_SELECTOR);
        if (area) {
            if (area.querySelector(messageSelector)) {
                 console.log("지정된 채팅 영역(CHAT_AREA_SELECTOR) 찾기 성공:", area);
                return area;
            }
            console.warn("지정된 채팅 영역은 찾았으나, 내부에 메시지(COMMON_MESSAGE_WRAPPER)가 없습니다. 자동 탐색을 시도합니다.");
        }

        console.log("지정된 선택자를 찾지 못했습니다. 스크롤 가능한 영역을 탐색합니다...");
        const divs = document.querySelectorAll('div');
        for (let div of divs) {
            if (div.scrollHeight > div.clientHeight && div.clientHeight > 100 && window.getComputedStyle(div).overflowY === 'scroll') {
                 if (div.querySelector(messageSelector)) {
                     console.log("스크롤 가능하고 메시지 컨테이너를 포함하는 영역 발견:", div);
                     return div;
                 }
            }
        }
        console.log("스크롤 영역 자동 탐색 실패.");
        return null;
    }

})();