국회의견 자동입력

국회의견을 자동으로 입력해줍니다.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         국회의견 자동입력
// @namespace    https://pal.assembly.go.kr/
// @version      1.0.0
// @description  국회의견을 자동으로 입력해줍니다.
// @match        https://pal.assembly.go.kr/*
// @run-at       document-start
// @inject-into  page
// @all-frames   true
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  /* ===== 공통 옵션 ===== */
  const QUIET_MS = 2000;
  const DEBUG = true;
  const log = (...a)=>{ if (DEBUG) try{console.log('[PAL]', ...a)}catch{} };

  /* ===== 라우팅 패턴 ===== */
  const VIEW_PAGE_PATTERNS = [
    '/lgsltpaSearch/view.do',
    '/lgsltpa/lgsltpaOngoing/view.do',
    '/napal/lgsltpa/lgsltpaSearch/view.do',
    '/napal/lgsltpa/lgsltpaOngoing/view.do',
  ];
  const INSERT_PAGE_PATTERNS = [
    '/lgsltpa/lgsltpaOpn/forInsert.do',
    '/napal/lgsltpa/lgsltpaOpn/insert.do',
  ];
  const LIST_PAGE_PATTERNS = [
    '/lgsltpa/lgsltpaOpn/list.do',
    '/napal/lgsltpa/lgsltpaOpn/list.do',
  ];

  const SELECTORS = {
    title:   '[name="sj"]',
    body:    '[name="cn"]',
    captchaImg:  'img[alt*="보안문자"], img[src*="captcha"], img[id*="captcha"]',
    captchaInputCandidates: ['#catpchaAnswer', '#captchaAnswer', 'input[name="catpchaAnswer"]', 'input[name="captchaAnswer"]'],
    submitBtnCandidates:    ['#btn_opnReg', 'button#btn_opnReg', 'button[type="submit"]', 'a.btn_submit'],
    openRegBtn: 'button.btn_opnReg',
  };

  /* ===== 유틸 ===== */
  const qs  = s => document.querySelector(s);
  const qsa = s => Array.from(document.querySelectorAll(s));
  function setInputValue(target, value){
    const el = typeof target === 'string' ? qs(target) : target;
    if (!el) return;
    el.value = value;
    el.dispatchEvent(new Event('input',  { bubbles:true }));
    el.dispatchEvent(new Event('change', { bubbles:true }));
  }
  function absolutizeUrl(u){ try{ return new URL(u, location.href).href }catch{ return u } }
  function safeClose(reason=''){
    log('🔚 창 닫기 시도:', reason);
    let closed=false;
    try{ window.close(); closed=true }catch{}
    if(!closed){ try{ window.open('', '_self').close(); closed=true }catch{} }
    if(!closed){ if(history.length>1) history.back(); else location.href='about:blank' }
  }
  const scheduleQuietClose = (()=> {
    let t=null;
    return ()=>{ try{ if(t) clearTimeout(t) }catch{}; t=setTimeout(()=>safeClose('quiet-after-dialogs'), QUIET_MS) }
  })();

  /* =================================================================
   * [A] alert/confirm/prompt "하드락"
   * ================================================================= */
  (function hardLockDialogs() {
    function makeLocked(fn){
      try{ Object.freeze(fn) }catch{}
      return fn;
    }
    const fakeAlert   = makeLocked(function(){ try{scheduleQuietClose()}catch{}; return true; });
    const fakeConfirm = makeLocked(function(){ try{scheduleQuietClose()}catch{}; return true; });
    const fakePrompt  = makeLocked(function(){ try{scheduleQuietClose()}catch{}; return ''  ; });

    function lockOn(obj){
      if(!obj) return;
      try{
        try{ obj.alert   = fakeAlert   }catch{}
        try{ obj.confirm = fakeConfirm }catch{}
        try{ obj.prompt  = fakePrompt  }catch{}
        const lock = (key, val) => {
          try{
            Object.defineProperty(obj, key, {
              configurable: false,
              get: ()=>val,
              set: ()=>{},
            });
          }catch{}
        };
        lock('alert',   fakeAlert);
        lock('confirm', fakeConfirm);
        lock('prompt',  fakePrompt);
      }catch{}
    }

    function lockEverywhere(root){
      try{ lockOn(root) }catch{}
      try{ lockOn(root.top) }catch{}
      try{ lockOn(root.parent) }catch{}
      try{
        const proto = root.Window && root.Window.prototype;
        if (proto){
          const lockProto = (k, v)=>{
            try{
              Object.defineProperty(proto, k, {
                configurable: false,
                get: ()=>v,
                set: ()=>{},
              });
            }catch{}
          };
          lockProto('alert',   fakeAlert);
          lockProto('confirm', fakeConfirm);
          lockProto('prompt',  fakePrompt);
        }
      }catch{}
      try{
        const ifr = root.document && root.document.getElementsByTagName('iframe');
        for (const f of ifr||[]){
          try{
            const cw = f.contentWindow;
            const src = f.getAttribute('src') || '';
            const same = !src || new URL(src, location.href).origin === location.origin;
            if (cw && same) lockOn(cw);
          }catch{}
        }
      }catch{}
    }

    lockEverywhere(window);
    let i=0;
    const again = setInterval(()=>{
      lockEverywhere(window);
      if(++i>=60) clearInterval(again);
    }, 50);

    const origOpen = window.open;
    window.open = function(...args){
      const w = origOpen.apply(this, args);
      if (!w) return w;
      try{ lockEverywhere(w) }catch{}
      try{ w.addEventListener('load', ()=>lockEverywhere(w), { once:true }) }catch{}
      setTimeout(()=>{ try{ lockEverywhere(w) }catch{} }, 120);
      return w;
    };
  })();

  /* =================================================================
   * [B] URL 변경 감시
   * ================================================================= */
  const UrlWatch = (function(){
    let last = location.href;
    const listeners = new Set();
    function emit(reason){
      const href = location.href;
      if (href === last) return;
      const prev = last; last = href;
      log('🔎 URL 변경:', reason, '\n   from:', prev, '\n   to  :', href);
      listeners.forEach(fn=>{ try{ fn(href, prev, reason) }catch(e){ console.warn(e) } });
    }
    ['pushState','replaceState'].forEach(k=>{
      const orig = history[k];
      history[k] = function(...a){
        const r = orig.apply(this, a);
        emit('history:'+k);
        return r;
      };
    });
    window.addEventListener('hashchange', ()=>emit('hashchange'));
    window.addEventListener('popstate',   ()=>emit('popstate'));
    setInterval(()=>emit('poll'), 120);
    return { onChange: fn=>listeners.add(fn), prime: ()=>emit('prime') };
  })();

  /* =================================================================
   * [C] 라우팅 헬퍼
   * ================================================================= */
  const hasAny = (href, arr)=>arr.some(p=>href.includes(p));
  const isView   = href => hasAny(href, VIEW_PAGE_PATTERNS);
  const isInsert = href => hasAny(href, INSERT_PAGE_PATTERNS);
  const isList   = href => hasAny(href, LIST_PAGE_PATTERNS) && new URL(href).searchParams.has('lgsltPaId');

  const pageState = { clickedOpenReg:false, ranInsert:false };

  /* =================================================================
   * [D] 상세/진행중: 의견등록 버튼 자동 클릭
   * ================================================================= */
  function setupAutoClickOpenReg(){
    if (pageState.clickedOpenReg) return;
    const clickNow = ()=>{
      const btn = qs(SELECTORS.openRegBtn);
      if (btn){ btn.click(); pageState.clickedOpenReg = true; log('✅ 의견등록 버튼 자동 클릭'); }
    };
    clickNow();
    const attach = ()=>{
      if (!document.body) return;
      const mo = new MutationObserver(muts=>{
        if (pageState.clickedOpenReg) return;
        for (const m of muts) for (const n of m.addedNodes){
          if (n.nodeType!==1) continue;
          if (n.matches?.(SELECTORS.openRegBtn) || n.querySelector?.(SELECTORS.openRegBtn)){
            setTimeout(clickNow, 80); return;
          }
        }
      });
      mo.observe(document.body, { childList:true, subtree:true });
    };
    (document.readyState==='loading')
      ? document.addEventListener('DOMContentLoaded', attach, { once:true })
      : attach();
  }

  /* =================================================================
   * [E] insert.do: 자동입력 + 캡차 팝업(찬반 선택) → 제출
   * ================================================================= */
  function saveToLocalServer(value, base64OrUrl){
    fetch('http://localhost:8000/upload', {
      method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({ value, base64: base64OrUrl })
    }).then(()=>log('✅ 캡차 전송 OK')).catch(e=>log('⚠️ 로컬 전송 실패(무시):', e));
  }

  function getChoiceFromStorage(){
    const v = localStorage.getItem('pal_choice');
    return (v === 'pros' || v === 'cons') ? v : 'cons'; // 기본=반대(cons)
  }
  function setChoiceToStorage(choice){
    try{ localStorage.setItem('pal_choice', choice) }catch{}
  }

  function makeTexts(choice){
    if (choice === 'pros'){
      return {
        title: '찬성합니다',
        body:  '발의된 이 법안에 찬성합니다. 조속한 통과를 요청드립니다.'
      };
    }
    // default: cons
    return {
      title: '반대합니다',
      body:  '발의된 이 법안에 반대합니다. 충분한 재검토를 요청드립니다.'
    };
  }

  function showCaptchaPopup(imageUrl, onSubmit){
    qs('#captcha-popup')?.remove();
    const remembered = getChoiceFromStorage();

    const w = document.createElement('div');
    w.id='captcha-popup';
    w.style = `
      position:fixed; top:30%; left:50%; transform:translate(-50%,-30%);
      background:#fff; border:2px solid #444; padding:16px; z-index:99999;
      box-shadow:0 0 10px rgba(0,0,0,.4); text-align:center; min-width:280px; max-width:92vw; font-family:system-ui,apple sd gothic neo,Segoe UI,Malgun Gothic,sans-serif;
    `;
    w.innerHTML = `
      <h3 style="margin:0 0 10px;">🛡️ 보안문자 입력</h3>
      <img src="${imageUrl}" alt="captcha" style="display:block;margin:0 auto 8px;max-width:100%;max-height:50vh;object-fit:contain;image-rendering:pixelated;"/>
      <div id="choiceBox" style="display:flex;gap:12px;justify-content:center;align-items:center;margin:6px 0 10px;">
        <label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
          <input type="radio" name="pal_choice" value="cons" ${remembered==='cons'?'checked':''}/>
          <span>반대</span>
        </label>
        <label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
          <input type="radio" name="pal_choice" value="pros" ${remembered==='pros'?'checked':''}/>
          <span>찬성</span>
        </label>
      </div>
      <input type="text" id="captchaInputField" placeholder="보안문자 (4~5자리)" maxlength="5" style="padding:8px;font-size:16px;width:160px;border:1px solid #aaa;border-radius:6px;"/>
      <div id="err" style="color:#c00;font-size:12px;min-height:14px;margin-top:6px;"></div>
      <div style="margin-top:10px;display:flex;gap:8px;justify-content:center;">
        <button id="captchaSubmitBtn" style="padding:8px 16px;font-size:14px;cursor:pointer;border:1px solid #333;border-radius:6px;background:#f3f3f3;">확인</button>
        <button id="captchaCancelBtn" style="padding:8px 16px;font-size:14px;cursor:pointer;border:1px solid #999;border-radius:6px;background:#fff;">취소</button>
      </div>
    `;
    document.body.appendChild(w);

    const input = w.querySelector('#captchaInputField');
    const btnSubmit = w.querySelector('#captchaSubmitBtn');
    const btnCancel = w.querySelector('#captchaCancelBtn');
    const err = w.querySelector('#err');

    input.addEventListener('keydown', e=>{ if(e.key==='Enter') btnSubmit.click() });

    btnCancel.addEventListener('click', ()=>{ w.remove(); });

    btnSubmit.addEventListener('click', ()=>{
      const val = input.value.trim();
      if (val.length < 4){
        err.textContent = '4자리 이상 입력해주세요.';
        input.focus();
        return;
      }
      const choice = (w.querySelector('input[name="pal_choice"]:checked')?.value) || 'cons';
      setChoiceToStorage(choice);
      w.remove();
      onSubmit(val, choice);
    });

    input.focus();
  }

  function runInsertFlow(){
    if (pageState.ranInsert) return;
    pageState.ranInsert = true;
    log('🧩 insert 페이지 감지: 자동 입력 시작');

    const start = ()=>{
      const img = qs(SELECTORS.captchaImg);
      const inputEl = SELECTORS.captchaInputCandidates.map(qs).find(Boolean);
      const submit  = SELECTORS.submitBtnCandidates.map(qs).find(Boolean);
      const titleEl = qs(SELECTORS.title);
      const bodyEl  = qs(SELECTORS.body);

      if(!img || !inputEl || !submit || !titleEl || !bodyEl){
        log('⚠️ 필수 요소 미발견', {img:!!img, input:!!inputEl, submit:!!submit, title:!!titleEl, body:!!bodyEl});
        return;
      }

      // 초기에는 저장된 선택값 기준으로 미리 채워둠(사용자가 팝업에서 변경 가능)
      const initChoice = getChoiceFromStorage();
      const initTexts = makeTexts(initChoice);
      setInputValue(titleEl, initTexts.title);
      setInputValue(bodyEl,  initTexts.body);

      const url = absolutizeUrl(img.getAttribute('src') || img.src);

      showCaptchaPopup(url, (val, choice)=>{
        try{ saveToLocalServer(val, url) }catch{}
        // 최종 선택값으로 내용 갱신 후 제출
        const { title, body } = makeTexts(choice);
        setInputValue(titleEl, title);
        setInputValue(bodyEl,  body);
        setInputValue(inputEl,  val);

        inputEl.focus();
        setTimeout(()=>submit.click(), 300);
      });
    };

    (document.readyState==='loading')
      ? document.addEventListener('DOMContentLoaded', start, { once:true })
      : start();
  }

  /* =================================================================
   * [F] 목록 도착 시 자동 닫기
   * ================================================================= */
  function closeIfList(href, why='arrived-list'){
    if (isList(href)){
      setTimeout(()=>safeClose(why), 120);
      return true;
    }
    return false;
  }

  /* =================================================================
   * [G] URL 라우터
   * ================================================================= */
  function route(href){
    if (closeIfList(href)) return;
    if (isView(href)){
      pageState.clickedOpenReg=false;
      setupAutoClickOpenReg();
      return;
    }
    if (isInsert(href)){
      runInsertFlow();
      return;
    }
  }

  /* 부팅 & 감시 */
  route(location.href);
  UrlWatch.onChange(href=>route(href));
  UrlWatch.prime();
})();