[ChatGPT/Claude] 현재 대화 내용 PDF 추출 스크립트

dev asdf·2025년 8월 18일
0

ChatGPT

(async () => {
  const sleep = (ms)=>new Promise(r=>setTimeout(r,ms));

  // 1) 무한 스크롤로 전체 로드
  let last=-1, same=0;
  for (let i=0;i<2000;i++){
    window.scrollBy(0,2000);
    await sleep(160);
    const h=document.documentElement.scrollHeight;
    if(h===last){ if(++same>=5) break; } else { same=0; last=h; }
  }
  window.scrollTo(0,0);

  // 2) "Show more/더보기" 자동 펼치기
  const openTexts=['더보기','전체 보기','펼치기','Show more','Show More','Read more','Expand','View more'];
  document.querySelectorAll('button,a,[role="button"]').forEach(el=>{
    const t=(el.innerText||el.ariaLabel||'').trim().toLowerCase();
    if(t && openTexts.some(x=>t.includes(x.toLowerCase()))){ try{el.click();}catch{} }
  });

  // 3) 최상위 Turn만 선택
  const turns = Array.from(document.querySelectorAll('article[data-testid^="conversation-turn-"], [data-testid^="conversation-turn-"]'))
    .filter(el => el.offsetParent !== null);
  if (!turns.length) { alert('대화 메시지를 찾지 못했습니다.'); return; }

  // 4) 새 문서
  const w = window.open('', '_blank'); if (!w){ alert('팝업 차단 해제 후 재시도'); return; }
  const css = `
    @page { margin: 12mm; }
    body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Apple SD Gothic Neo,Malgun Gothic,Arial,sans-serif;
         background:#fff;color:#111;-webkit-print-color-adjust:exact;print-color-adjust:exact;margin:0;}
    .wrap{max-width:900px;margin:0 auto;padding:8mm 0;}
    .msg{margin:14px 0;page-break-inside:avoid;break-inside:avoid;}
    .from{font-weight:700;margin:2px 0 6px;}
    .bubble{border:1px solid #ddd;border-radius:10px;padding:12px 14px;}
    /* 코드/프리포맷은 '원본 유지'를 위해 white-space 강제 변경하지 않음 */
    img,video,canvas,svg{max-width:100%;height:auto;display:block;}
    a[href]::after{content:"";}
    .sr-only,[aria-hidden="true"],[hidden]{display:none !important;}
    /* 리스트 간격 최소화 */
    ul, ol { margin: 0 0 0 1.5em !important; padding: 0 !important; }
    li { margin: 0 !important; padding: 0 !important; }
    li + li { margin-top: 0.2em !important; }
  `;
  w.document.write(`<!doctype html><html><head><meta charset="utf-8"><title>ChatGPT Export</title><style>${css}</style></head><body><div class="wrap" id="root"></div></body></html>`);
  const root = w.document.getElementById('root');

  // 5) 유틸
  const isUserTurn = (el) =>
    el.getAttribute('data-message-author-role') === 'user' ||
    !!el.querySelector('[data-message-author-role="user"]');
  const isAssistantTurn = (el) =>
    el.getAttribute('data-message-author-role') === 'assistant' ||
    !!el.querySelector('[data-message-author-role="assistant"]');

  const contentSelectors = [
    '.markdown',
    '[data-message-author-role] .whitespace-pre-wrap',
    '[data-testid="message-content"]',
    '.prose','.content','.text-base'
  ];
  const isVisible = (node) => {
    const s = window.getComputedStyle(node);
    if (s.display === 'none' || s.visibility === 'hidden' || parseFloat(s.opacity) === 0) return false;
    const r = node.getBoundingClientRect(); return (r.width>0 && r.height>0);
  };
  const pickContent = (turn) => {
    for (const sel of contentSelectors) {
      const n = turn.querySelector(sel);
      if (n && n.innerHTML.trim() && isVisible(n)) return n;
    }
    return turn;
  };

  // AI reasoning/trace 패널 제거
  function stripReasoning(clone) {
    clone.querySelectorAll('[data-testid*="reasoning"],[data-testid*="chain"],[data-testid*="work"]').forEach(n=>n.remove());
    const keywords = ['reasoning','chain-of-thought','show work','scratchpad','추론','사고 과정','작업 내역','생각'];
    clone.querySelectorAll('details, section, div').forEach(node=>{
      const sum = node.querySelector?.('summary');
      const label = (sum?.innerText || node.getAttribute?.('aria-label') || '').toLowerCase();
      if (label && keywords.some(k=>label.includes(k))) node.remove();
    });
    clone.querySelectorAll('[style*="height: 0"],[style*="opacity: 0"]').forEach(n=>{
      if (!n.querySelector('.markdown') && (n.innerText||'').trim().length<10) n.remove();
    });
    return clone;
  }

  const seenIds = new Set();
  const seenHashes = new Set();
  const hash = (s) => { let h=0; for (let i=0;i<s.length;i++){ h=(h<<5)-h + s.charCodeAt(i); h|=0; } return h; };

  // 6) 순서대로 삽입
  for (const turn of turns) {
    const msgId = turn.getAttribute('data-message-id') || turn.dataset?.messageId;
    if (msgId && seenIds.has(msgId)) continue;

    const role = isUserTurn(turn) ? 'You' : (isAssistantTurn(turn) ? 'AI' : 'AI');
    const node = pickContent(turn);
    let clone = node.cloneNode(true);
    if (role === 'AI') clone = stripReasoning(clone);

    // 버튼류 제거
    clone.querySelectorAll('button,[role="button"],[data-testid*="toolbar"],[data-testid*="actions"]').forEach(b=>b.remove());

    let html;
    if (role === 'You') {
      // ✅ YOU: 전체 줄바꿈 보존, 단 코드블록은 예외(원본 유지)
      const codeBlocks = [];
      clone.querySelectorAll('pre, code').forEach((el,i)=>{
        codeBlocks[i] = el.outerHTML;            // 원본 보관
        el.outerHTML = `%%%CODEBLOCK_${i}%%%`;   // 마커로 대체
      });
      html = clone.innerHTML.split('\n').map(line=>line||'&nbsp;').join('<br>');
      codeBlocks.forEach((code,i)=>{
        html = html.replace(`%%%CODEBLOCK_${i}%%%`, code); // 원복
      });
    } else {
      // ✅ AI: '원본 그대로' 유지 → 코드블록도 변환하지 않음
      html = clone.innerHTML.trim();
    }

    const h = hash(html);
    if (seenHashes.has(h)) continue;
    if (msgId) seenIds.add(msgId);
    seenHashes.add(h);

    const sec = w.document.createElement('section'); sec.className='msg';
    const from = w.document.createElement('div'); from.className='from'; from.textContent = role;
    const bubble = w.document.createElement('div'); bubble.className='bubble'; bubble.innerHTML = html;
    sec.appendChild(from); sec.appendChild(bubble); root.appendChild(sec);
  }

  // 7) 인쇄
  try { await w.document.fonts.ready; } catch {}
  await Promise.allSettled(Array.from(w.document.images).map(img => img.decode?.().catch(()=>{})));
  w.focus(); w.print();
})();

Claude

(async () => {
  const sleep = (ms)=>new Promise(r=>setTimeout(r,ms));

  // 1) 무한 스크롤 로드
  let last=-1, same=0;
  for (let i=0;i<2000;i++){
    window.scrollBy(0,2000);
    await sleep(150);
    const h=document.documentElement.scrollHeight;
    if(h===last){ if(++same>=5) break; } else { same=0; last=h; }
  }
  window.scrollTo(0,0);

  // 2) "더보기/Show more" 자동 펼치기
  const openTexts=['더보기','전체 보기','펼치기','Show more','Show More','Read more','Expand','View more'];
  for (const el of document.querySelectorAll('button,a,[role="button"]')) {
    const t=(el.innerText||el.ariaLabel||'').trim();
    if (t && openTexts.some(x=>t.includes(x))) { try{ el.click(); await sleep(30); }catch{} }
  }

  // 3) 새 문서
  const w = window.open('', '_blank');
  if (!w) { alert('팝업 차단을 해제하세요.'); return; }
  const css = `
    @page { margin: 12mm; }
    body { font-family: system-ui,-apple-system,Segoe UI,Roboto,Apple SD Gothic Neo,Malgun Gothic,Arial,sans-serif;
           background:#fff;color:#111;-webkit-print-color-adjust:exact;print-color-adjust:exact;margin:0; }
    .wrap { max-width: 900px; margin: 0 auto; padding: 8mm 0; }
    .msg { margin: 14px 0; page-break-inside: avoid; break-inside: avoid; }
    .from { font-weight: 700; margin: 2px 0 6px; }
    .bubble { border: 1px solid #ddd; border-radius: 10px; padding: 12px 14px; }

    pre, code { white-space: pre-wrap; word-break: break-word; }
    img,video,canvas,svg { max-width:100%; height:auto; display:block; }
    a[href]::after { content:""; }

    /* 리스트 간격 최소화 */
    ul, ol { margin: 0 0 0 1.5em !important; padding: 0 !important; }
    li { margin: 0 !important; padding: 0 !important; }
    li + li { margin-top: 0.2em !important; }

    /* === 가로 스크롤 방지 === */
    .wrap, .bubble { max-width: 100% !important; overflow-x: hidden !important; }
    pre, code, table {
      max-width: 100% !important;
      white-space: pre-wrap !important;
      word-wrap: break-word !important;
      overflow-x: visible !important;
    }
    img, svg { max-width: 100% !important; height: auto !important; }
  `;
  w.document.write(`<!doctype html><html><head><meta charset="utf-8"><title>Claude Export</title><style>${css}</style></head><body><div class="wrap" id="root"></div></body></html>`);
  const root = w.document.getElementById('root');

  // 4) 본문 루트
  const srcRoot = document.querySelector('main, [role="main"], .conversation, .messages, .messages-container') || document.body;

  // 5) AI 추론/계획 패널 제거
  function stripReasoning(clone) {
    clone.querySelectorAll('button[class*="group/row"]').forEach(btn=>{
      const box = btn.closest('.transition-all, .rounded-lg, .flex, .font-ui') || btn.parentElement;
      if (box && box.parentElement) box.remove();
    });
    clone.querySelectorAll('.overflow-hidden.shrink-0, [tabindex="-1"][style*="height: 0"]').forEach(el=>el.remove());
    clone.querySelectorAll('[data-testid*="trace"], .trace, .reasoning, .hidden-trace').forEach(el=>el.remove());
    return clone;
  }

  // 6) 순서대로 메시지 복제
  const walker = document.createTreeWalker(srcRoot, NodeFilter.SHOW_ELEMENT, null);
  while (walker.nextNode()) {
    const el = walker.currentNode;
    if (!el.matches) continue;
    if (!el.matches('[data-testid="user-message"], .font-claude-response')) continue;

    const isUser = el.matches('[data-testid="user-message"]');
    let clone = el.cloneNode(true);
    if (!isUser) clone = stripReasoning(clone);

    let html;
    if (isUser) {
      // ✅ YOU: 무조건 줄바꿈 보존, 단 코드블록 예외
      const codeBlocks = [];
      clone.querySelectorAll('pre, code').forEach((el,i)=>{
        codeBlocks[i] = el.outerHTML;
        el.outerHTML = `%%%CODEBLOCK_${i}%%%`;
      });
      html = clone.innerHTML.split('\n').map(line=>line||'&nbsp;').join('<br>');
      codeBlocks.forEach((code,i)=>{
        html = html.replace(`%%%CODEBLOCK_${i}%%%`, code);
      });
    } else {
      // ✅ AI: 현상태 유지, 코드블록만 줄바꿈 보존
      clone.querySelectorAll('pre, code').forEach(codeEl => {
        const raw = codeEl.textContent;
        if (raw && raw.includes('\n')) {
          const replaced = raw.split('\n').map(line => line || '&nbsp;').join('<br>');
          codeEl.innerHTML = replaced;
        }
      });
      html = clone.innerHTML.trim();
    }

    const sec = w.document.createElement('section'); sec.className='msg';
    const from = w.document.createElement('div'); from.className='from'; from.textContent = isUser ? 'You' : 'AI';
    const bubble = w.document.createElement('div'); bubble.className='bubble'; bubble.innerHTML = html;
    sec.appendChild(from); sec.appendChild(bubble); root.appendChild(sec);
  }

  // 7) 인쇄
  try { await w.document.fonts.ready; } catch {}
  await Promise.allSettled(Array.from(w.document.images).map(img => img.decode?.().catch(()=>{})));
  w.focus(); w.print();
})();

클로드 exporter가 횟수 제한 걸려서... GPT한테 대화 내용 추출하는 스크립트 만들라고 시켰다.

F12 + console 창에 위 스크립트를 실행시키면 PDF 파일로 저장이 가능하다.

단, CSS 유지는 되지 않는다.

0개의 댓글