🗑️ crack chat delete

체크박스로 선택한 채팅을 서버에서 삭제

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey to install this script.

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         🗑️ crack chat delete
// @namespace    http://tampermonkey.net/
// @version      2.30
// @description  체크박스로 선택한 채팅을 서버에서 삭제
// @author       gpt야 수고했다
// @match        https://crack.wrtn.ai/stories/*/episodes/*
// @grant        GM_addStyle
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const API_BASE = 'https://crack-api.wrtn.ai/crack-gen/v3';
    const DELETE_BUTTON_ID = 'delete-action-button';
    const DEBUG = true;

    let currentChatId = null;

    function log(...args) {
        if (DEBUG) console.log('[crack-delete]', ...args);
    }

    function extractChatIdFromUrl(url) {
        if (!url || typeof url !== 'string') return null;

        const patterns = [
            /\/chats\/([a-f0-9]{24})(?:\/|$)/i,
            /"chatId":"([a-f0-9]{24})"/i
        ];

        for (const pattern of patterns) {
            const match = url.match(pattern);
            if (match) return match[1];
        }

        return null;
    }

    function setCurrentChatId(chatId, source = '') {
        if (!chatId) return;
        if (currentChatId !== chatId) {
            currentChatId = chatId;
            log('chatId detected:', chatId, source);
        }
    }

    function hookNetworkForChatId() {
        const originalFetch = window.fetch;
        window.fetch = async function (...args) {
            try {
                const input = args[0];
                const url =
                    typeof input === 'string'
                        ? input
                        : input?.url || '';

                const detected = extractChatIdFromUrl(url);
                if (detected) setCurrentChatId(detected, 'fetch');
            } catch (e) {
                log('fetch hook parse error', e);
            }

            return originalFetch.apply(this, args);
        };

        const originalOpen = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function (method, url, ...rest) {
            try {
                const detected = extractChatIdFromUrl(String(url || ''));
                if (detected) setCurrentChatId(detected, 'xhr');
            } catch (e) {
                log('xhr hook parse error', e);
            }
            return originalOpen.call(this, method, url, ...rest);
        };
    }

    function getCookie(name) {
        const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        const match = document.cookie.match(new RegExp('(?:^|; )' + escaped + '=([^;]*)'));
        return match ? decodeURIComponent(match[1]) : null;
    }

    function getAccessToken() {
        return getCookie('access_token');
    }

    function getCommonHeaders() {
        const token = getAccessToken();
        const headers = {
            accept: 'application/json, text/plain, */*',
            platform: 'web',
            'wrtn-locale': 'ko-KR'
        };

        if (token) headers.authorization = `Bearer ${token}`;

        const wrtnId = getCookie('__w_id');
        if (wrtnId) headers['x-wrtn-id'] = wrtnId;

        const mixpanelId = getCookie('Mixpanel-Distinct-Id');
        if (mixpanelId) headers['mixpanel-distinct-id'] = mixpanelId;

        return headers;
    }

    function findChatId() {
        if (currentChatId) return currentChatId;

        const html = document.documentElement.innerHTML;

        const match1 = html.match(/\/chats\/([a-f0-9]{24})\/messages/i);
        if (match1) {
            setCurrentChatId(match1[1], 'html:/chats/.../messages');
            return match1[1];
        }

        const match2 = html.match(/"chatId":"([a-f0-9]{24})"/i);
        if (match2) {
            setCurrentChatId(match2[1], 'html:chatId');
            return match2[1];
        }

        const scripts = Array.from(document.scripts).map(s => s.textContent || '').join('\n');
        const match3 = scripts.match(/\/chats\/([a-f0-9]{24})\//i);
        if (match3) {
            setCurrentChatId(match3[1], 'scripts');
            return match3[1];
        }

        return null;
    }

    function findMessageIdFromGroup(groupEl) {
        return groupEl.getAttribute('data-message-group-id') || null;
    }

    async function deleteMessageFromServer(chatId, messageId) {
        const res = await fetch(`${API_BASE}/chats/${chatId}/messages/${messageId}`, {
            method: 'DELETE',
            credentials: 'include',
            headers: getCommonHeaders()
        });

        if (!res.ok) {
            const text = await res.text().catch(() => '');
            throw new Error(`삭제 실패 (${res.status}) ${text}`);
        }

        return await res.json().catch(() => ({}));
    }

    function injectCheckboxes() {
        const groups = document.querySelectorAll('div[data-message-group-id]');
        log('message groups:', groups.length);

        groups.forEach(group => {
            if (group.querySelector(':scope > .delete-checkbox-container')) return;

            group.style.position = 'relative';

            const container = document.createElement('div');
            container.className = 'delete-checkbox-container';
            container.style.cssText = `
                position: absolute;
                right: 8px;
                top: 8px;
                z-index: 999;
                background: rgba(0,0,0,0.08);
                border-radius: 999px;
                padding: 2px;
                display: flex;
                align-items: center;
                justify-content: center;
            `;

            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.className = 'delete-checkbox';
            checkbox.style.cssText = `
                width: 18px;
                height: 18px;
                cursor: pointer;
                accent-color: #ff4d4f;
            `;

            const messageId = findMessageIdFromGroup(group);
            if (messageId) checkbox.dataset.messageId = messageId;

            container.appendChild(checkbox);
            group.appendChild(container);
        });
    }

    function getSelectedMessageGroups() {
        return Array.from(document.querySelectorAll('div[data-message-group-id]')).filter(group => {
            const checkbox = group.querySelector('.delete-checkbox');
            return checkbox && checkbox.checked;
        });
    }

    function findInputArea() {
        const selectors = [
            '.flex.items-center.space-x-2',
            'form button[type="submit"]',
            'textarea',
            '[contenteditable="true"]'
        ];

        for (const selector of selectors) {
            const el = document.querySelector(selector);
            if (!el) continue;

            if (selector === 'form button[type="submit"]') {
                return el.closest('form') || el.parentElement;
            }

            if (selector === 'textarea' || selector === '[contenteditable="true"]') {
                return el.parentElement?.parentElement || el.parentElement;
            }

            return el;
        }

        return null;
    }

    function createDeleteButton() {
        if (document.getElementById(DELETE_BUTTON_ID)) return;

        const inputArea = findInputArea();
        if (!inputArea) {
            log('input area not found');
            return;
        }

        const btn = document.createElement('button');
        btn.id = DELETE_BUTTON_ID;
        btn.type = 'button';
        btn.title = '선택한 대화 삭제';
        btn.innerHTML = `
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18">
                <path fill="currentColor" d="M9 3h6l1 2h4v2H4V5h4l1-2zm1 7h2v8h-2v-8zm4 0h2v8h-2v-8zM7 10h2v8H7v-8zm-1 11a2 2 0 0 1-2-2V8h16v11a2 2 0 0 1-2 2H6z"/>
            </svg>
        `;
        btn.style.cssText = `
            width: 32px;
            height: 32px;
            min-width: 32px;
            border: 1px solid rgba(255,255,255,0.12);
            border-radius: 9999px;
            background: transparent;
            color: inherit;
            cursor: pointer;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            margin-right: 8px;
        `;

        btn.addEventListener('click', handleDeleteSelectedMessages);

        inputArea.prepend(btn);
        log('delete button created');
    }

    async function handleDeleteSelectedMessages() {
        const selectedGroups = getSelectedMessageGroups();

        if (selectedGroups.length === 0) {
            alert('삭제할 채팅을 하나 이상 선택해주세요.');
            return;
        }

        const chatId = findChatId();
        if (!chatId) {
            alert('chatId를 아직 찾지 못했습니다. 페이지 새로고침 후 다시 시도해주세요.');
            return;
        }

        const targets = selectedGroups.map(group => ({
            group,
            messageId: findMessageIdFromGroup(group)
        }));

        const invalid = targets.filter(v => !v.messageId);
        if (invalid.length > 0) {
            alert('일부 선택 항목에서 messageId를 찾지 못했습니다.');
            return;
        }

        const ok = confirm(`선택한 채팅 ${targets.length}개를 실제 삭제할까요?`);
        if (!ok) return;

        const btn = document.getElementById(DELETE_BUTTON_ID);
        const oldHtml = btn?.innerHTML;
        if (btn) {
            btn.disabled = true;
            btn.textContent = '...';
        }

        const failed = [];

        try {
            for (const { group, messageId } of targets) {
                try {
                    await deleteMessageFromServer(chatId, messageId);
                    group.remove();
                } catch (err) {
                    console.error('[crack-delete] delete failed', { chatId, messageId, err });
                    failed.push({ messageId, error: err.message });
                }
            }

            if (failed.length === 0) {
                alert('선택한 채팅을 모두 삭제했습니다.');
            } else {
                alert(`일부만 삭제됨\n성공: ${targets.length - failed.length}개\n실패: ${failed.length}개`);
            }
        } finally {
            if (btn) {
                btn.disabled = false;
                btn.innerHTML = oldHtml;
            }
        }
    }

    function startObservers() {
        const observer = new MutationObserver(() => {
            injectCheckboxes();
            createDeleteButton();
            findChatId();
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    function init() {
        hookNetworkForChatId();

        window.addEventListener('load', () => {
            log('init');
            findChatId();
            injectCheckboxes();
            createDeleteButton();
            startObservers();

            setInterval(() => {
                findChatId();
                injectCheckboxes();
                createDeleteButton();
            }, 1500);
        });
    }

    init();
})();