체크박스로 선택한 채팅을 서버에서 삭제
// ==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();
})();