// ==UserScript== // @name Picky Advanced // @namespace https://github.com/hooray804/Picky // @version 2.4.0 // @description Web Element Inspector & CSS Selector Tool with Ad Block // @author hooray804 // @license MPL-2.0 // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @homepage https://github.com/hooray804/Picky // @updateURL https://raw.githubusercontent.com/hooray804/Picky/main/PickyAdvanced.user.js // @downloadURL https://raw.githubusercontent.com/hooray804/Picky/main/PickyAdvanced.user.js // @supportURL https://github.com/hooray804/Picky/issues // ==/UserScript== (function() { 'use strict'; if (window.self !== window.top) { if (window.innerWidth < 150 || window.innerHeight < 150) { return; } } const P_ID = 'picky-tool'; const P_HOST = 'picky-root'; const P_HL = 'picky-hl'; const P_ISO_B = 'picky-iso-body'; const P_ISO_P = 'picky-iso-path'; const P_SHIELD = 'picky-shield'; let touchMoved = false; let initialTouchedEl = null; const MOVE_THRESHOLD = 15; if (document.getElementById(P_HOST)) { window.Picky?.end(); } const ICONS = { close: ``, settings: ``, modeCycle: ``, modeFull: ``, back: ``, copy: ``, parent: ``, child: ``, eyeOpen: ``, eyeClosed: ``, reset: ``, code: ``, dot: ``, }; const P = { ui: { host: null, shadow: null, tool: null }, st: { el: null, rootEl: null, path: [], selInfo: { selector: '', root: document }, view: 'initial', size: 'full', min: true, hidden: false, isolate: false, hiddenEls: [], origDisp: new Map(), matchCount: 0, autoClose: true, pos: 'bottom', cfg: { useId: true, useClasses: true, classCount: 2, useNthOfType: true, intelligentMode: true, unstableClasses: ['active', 'select', 'focus', 'open', 'js-', 'ui-', 'hover', 'disabled', 'checked', 'selected', '--is-', '_is-'], stableAttrs: ['data-testid', 'data-cy', 'data-test-id', 'data-test', 'name'], maxClimb: 7, shadowDomSupport: false } }, Blocker: { init() { this.apply(); }, getRules() { const rules = GM_getValue('picky_blocked_rules', {}); return rules[window.location.hostname] || []; }, add(selector) { if (!selector) return; const rules = GM_getValue('picky_blocked_rules', {}); const host = window.location.hostname; if (!rules[host]) rules[host] = []; if (!rules[host].includes(selector)) { rules[host].push(selector); GM_setValue('picky_blocked_rules', rules); this.apply(); return true; } return false; }, remove(selector) { const rules = GM_getValue('picky_blocked_rules', {}); const host = window.location.hostname; if (rules[host]) { rules[host] = rules[host].filter(r => r !== selector); if (rules[host].length === 0) delete rules[host]; GM_setValue('picky_blocked_rules', rules); this.apply(); return true; } return false; }, apply() { const rules = this.getRules(); const styleId = 'picky-blocker-style'; let style = document.getElementById(styleId); if (!rules.length) { if (style) style.remove(); return; } if (!style) { style = document.createElement('style'); style.id = styleId; document.head.appendChild(style); } style.textContent = rules.join(', ') + ' { display: none !important; }'; }, reset() { const rules = GM_getValue('picky_blocked_rules', {}); if (rules[window.location.hostname]) { delete rules[window.location.hostname]; GM_setValue('picky_blocked_rules', rules); const style = document.getElementById('picky-blocker-style'); if (style) style.remove(); alert('이 사이트의 차단 규칙이 초기화되었습니다. 페이지를 새로고침합니다.'); location.reload(); } else { alert('저장된 차단 규칙이 없습니다.'); } } }, getParent(el) { if (!el) return null; if (!this.st.cfg.shadowDomSupport) return el.parentElement; if (el.parentElement) return el.parentElement; const root = el.getRootNode(); return (root instanceof ShadowRoot) ? root.host : null; }, getChildren(el) { if (!el) return []; if (!this.st.cfg.shadowDomSupport || !el.shadowRoot) return Array.from(el.children); return Array.from(el.shadowRoot.children); }, getElementFromPointDeep(x, y) { let element = document.elementFromPoint(x, y); while (element && element.shadowRoot) { const deeperElement = element.shadowRoot.elementFromPoint(x, y); if (deeperElement) { element = deeperElement; } else { break; } } return element; }, Modal: { el: null, show(title, content, isHtml = false) { this.hide(); const o = document.createElement('div'); o.className = 'picky-modal-overlay'; o.innerHTML = `
`; o.querySelector('.picky-modal-title').textContent = title; const b = o.querySelector('.picky-modal-body'); if (isHtml) { b.innerHTML = content; } else { b.innerHTML = ``; b.querySelector('textarea').textContent = content; } P.ui.shadow.appendChild(o); this.el = o; this.el.addEventListener('click', (e) => { if (e.target.closest('[data-action="closeModal"]') || e.target === this.el) { this.hide(); } }); setTimeout(() => this.el.classList.add('visible'), 10); }, hide() { if (!this.el) return; this.el.classList.remove('visible'); setTimeout(() => { this.el?.remove(); this.el = null; }, 300); } }, css(el) { const c = this.st.cfg; if (!el || el.nodeType !== 1) return { selector: '', root: document }; const toolStateClasses = [P_HL, P_ISO_P]; const rootNode = this.st.cfg.shadowDomSupport ? el.getRootNode() : document; const queryContext = rootNode === document ? document : rootNode; const getSelectorPath = (currentEl) => { const parts = []; let current = currentEl; let climbCount = 0; while (current && current.tagName && climbCount < c.maxClimb) { if (this.st.cfg.shadowDomSupport && current === queryContext) break; const tagName = current.tagName.toLowerCase(); if (tagName === 'body' || tagName === 'html') break; let part = tagName; if (c.useClasses) { const stableClasses = Array.from(current.classList).filter(cls => !toolStateClasses.includes(cls) && !(!cls || /\d{4,}/.test(cls) || /[a-f0-9]{6,}/i.test(cls) || c.unstableClasses.some(unstable => cls.toLowerCase().includes(unstable))) ).slice(0, c.classCount); if (stableClasses.length > 0) part += '.' + stableClasses.map(cls => CSS.escape(cls)).join('.'); } if (c.useNthOfType) { const parent = this.getParent(current); if (parent) { const siblings = this.getChildren(parent); const sameTagSiblings = siblings.filter(sib => sib.tagName === current.tagName); if (sameTagSiblings.length > 1) { const index = sameTagSiblings.indexOf(current) + 1; if (index > 0) part += `:nth-of-type(${index})`; } } } parts.unshift(part); if (c.intelligentMode) { const tempSelector = parts.join(' > '); try { if (queryContext.querySelectorAll(tempSelector).length === 1) { return parts.join(' > '); } } catch (e) {} } current = this.getParent(current); climbCount++; } return parts.join(' > '); }; if (c.intelligentMode) { if (c.useId && el.id) { const id = el.id, escapedId = CSS.escape(id); if (!/^\d+$/.test(id) && !id.startsWith('ember') && !id.includes(':')) { try { if (queryContext.querySelectorAll(`#${escapedId}`).length === 1) return { selector: `#${escapedId}`, root: queryContext }; } catch (e) {} } } for (const attr of c.stableAttrs) { const val = el.getAttribute(attr); if (val) { const selector = `[${attr}="${CSS.escape(val)}"]`; try { if (queryContext.querySelectorAll(selector).length === 1) return { selector: selector, root: queryContext }; } catch (e) {} } } } else { if (c.useId && el.id) { const id = el.id, escapedId = CSS.escape(id); if (!/^\d+$/.test(id) && !id.startsWith('ember') && !id.includes(':')) { try { if (queryContext.querySelectorAll(`#${escapedId}`).length === 1) return { selector: `#${escapedId}`, root: queryContext }; } catch (e) {} } } } return { selector: getSelectorPath(el), root: queryContext }; }, upd8() { if (!this.st.el) { this.st.matchCount = 0; return; } this.st.selInfo = this.css(this.st.el); const { selector, root } = this.st.selInfo; if (!selector) { this.st.matchCount = 0; return; } try { this.st.matchCount = root.querySelectorAll(selector).length; } catch (e) { this.st.matchCount = 0; } if (this.ui.match) this.ui.match.textContent = `${this.st.matchCount}개 일치`; if (this.ui.disp) { let displayText = selector; if (this.st.cfg.shadowDomSupport && root instanceof ShadowRoot) { displayText += ` (in Shadow DOM)`; } this.ui.disp.textContent = displayText; } }, getToolCss() { return `:host{--pk-pri:#007aff;--pk-on-pri:#ffffff;--pk-pri-cont:#007aff;--pk-on-pri-cont:#ffffff;--pk-sec-cont:#e9e9eb;--pk-on-sec-cont:#1d1d1f;--pk-surf-var:#f0f0f2;--pk-on-surf-var:#333333;--pk-outl:#d1d1d6;--pk-surf:#f9f9f9;--pk-on-surf:#1d1d1f;--pk-succ:#34c759;--pk-err:#ff3b30;all:initial;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;position:fixed;top:0;left:0;z-index:2147483647;width:0;height:0}#${P_ID}{position:fixed;left:50%;transform:translateX(-50%);z-index:2147483646;width:calc(100% - 24px);max-width:400px;background-color:rgba(248,248,248,.75);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border-radius:20px;box-shadow:0 8px 32px rgba(0,0,0,.15);border:1px solid rgba(0,0,0,.1);padding:12px;box-sizing:border-box;transition:transform .4s cubic-bezier(.4,0,.2,1),opacity .4s,top .4s,bottom .4s,width .3s,height .3s,border-radius .3s;user-select:none;-webkit-user-select:none;font-size:14px;color:#000}#${P_ID}.top{top:-200%;opacity:0}#${P_ID}.bottom{bottom:-200%;opacity:0}#${P_ID}.visible.top{top:12px;opacity:1}#${P_ID}.visible.bottom{bottom:12px;opacity:1}#${P_ID} .picky-icon-button{display:flex;align-items:center;justify-content:center;background:0 0;border:none;padding:4px;color:var(--pk-on-surf);cursor:pointer;border-radius:50%;transition:background-color .2s}#${P_ID} .picky-icon-button:hover{background-color:rgba(0,0,0,.08)}#${P_ID} .picky-icon-button svg{width:24px;height:24px;background:transparent!important;fill:currentColor!important;display:block;}#${P_ID} .picky-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;color:var(--pk-on-surf)}#${P_ID} .picky-header-title{font-size:16px;font-weight:600}#${P_ID} .picky-header-actions{display:flex;gap:8px}#${P_ID} .picky-selector-box{background-color:var(--pk-surf-var);padding:8px 12px;border-radius:12px;margin-bottom:12px}#${P_ID} .picky-selector-box-title{font-size:11px;color:var(--pk-on-surf-var);margin-bottom:4px;display:flex;justify-content:space-between}#${P_ID} .picky-selector-display{font-family:'SF Mono','Menlo',monospace;font-size:12px;color:var(--pk-on-surf);word-break:break-all;max-height:7em;overflow-y:auto}#${P_ID} .picky-button-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(60px,1fr));gap:8px}#${P_ID} hr{border:none;border-top:1px solid var(--pk-surf-var);margin:10px 0}#${P_ID} button{padding:8px 10px;border:none;border-radius:20px;font-size:13px;font-weight:500;cursor:pointer;background-color:var(--pk-sec-cont);color:var(--pk-on-sec-cont);transition:background-color .2s,transform .1s;display:flex;align-items:center;justify-content:center;gap:4px}#${P_ID} button:active{transform:scale(.96)}#${P_ID} button.primary{background-color:var(--pk-pri-cont);color:var(--pk-on-pri-cont)}#${P_ID} button.copied{background-color:var(--pk-succ);color:#fff}#${P_ID}.minimized{left:auto;right:20px;transform:none;width:28px;height:28px;border-radius:50%;padding:0;cursor:pointer}#${P_ID}.minimized .picky-content{display:none}#${P_ID} .picky-maximize-button{display:none}#${P_ID}.minimized .picky-maximize-button{display:flex;width:100%;height:100%;align-items:center;justify-content:center}#${P_ID}.minimal{padding:6px;height:auto}#${P_ID}.minimal .picky-content{display:flex;justify-content:space-around;gap:4px}#${P_ID}.minimal button{background:0 0}#${P_ID}.minimal button:hover{background-color:rgba(0,0,0,.08)}#${P_SHIELD}{position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:2147483645;background:transparent;display:none}#${P_ID} .picky-setting-item,#${P_ID} .picky-setting-item span{color:var(--pk-on-surf)}#${P_ID} .picky-setting-title{font-weight:500;font-size:15px;margin:8px 0 4px;color:var(--pk-on-surf)}#${P_ID} .picky-setting-item{display:flex;justify-content:space-between;align-items:center;padding:4px;border-bottom:1px solid var(--pk-surf-var)}#${P_ID} .picky-switch{position:relative;display:inline-block;width:44px;height:24px}#${P_ID} .picky-switch input{opacity:0;width:0;height:0}#${P_ID} .picky-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:var(--pk-outl);transition:.4s;border-radius:24px}#${P_ID} .picky-slider:before{position:absolute;content:"";height:18px;width:18px;left:3px;bottom:3px;background-color:#fff;transition:.4s;border-radius:50%}#${P_ID} input:checked+.picky-slider{background-color:var(--pk-pri)}#${P_ID} input:checked+.picky-slider:before{transform:translateX(20px)}.picky-modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.6);z-index:2147483647;backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px);opacity:0;transition:opacity .3s}.picky-modal-overlay.visible{opacity:1}.picky-modal-content{position:fixed;top:50%;left:50%;width:calc(100% - 32px);max-width:600px;max-height:80vh;background-color:var(--pk-surf);border-radius:16px;box-shadow:0 8px 32px rgba(0,0,0,.4);display:flex;flex-direction:column;opacity:0;transform:translate(-50%,-45%);transition:opacity .3s,transform .3s}.picky-modal-overlay.visible .picky-modal-content{opacity:1;transform:translate(-50%,-50%)}.picky-modal-header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid var(--pk-outl);flex-shrink:0}.picky-modal-title{font-size:16px;font-weight:600;color:var(--pk-on-surf);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.picky-modal-body{padding:16px;overflow-y:auto}.picky-modal-body textarea{width:100%;height:50vh;background:var(--pk-surf-var);border:none;border-radius:8px;color:var(--pk-on-surf);font-family:'SF Mono',monospace;font-size:12px;padding:8px;box-sizing:border-box;resize:vertical}.picky-child-list,.picky-cookie-table{list-style:none;padding:0;margin:0;width:100%;border-collapse:collapse}.picky-child-list li{padding:10px;border-bottom:1px solid var(--pk-outl);cursor:pointer;transition:background-color .2s;font-family:'SF Mono',monospace;font-size:12px;color:var(--pk-on-surf-var)}.picky-child-list li:hover{background-color:var(--pk-surf-var)}.picky-child-list li:last-child{border-bottom:none}.picky-cookie-table th,.picky-cookie-table td{padding:8px;text-align:left;border-bottom:1px solid var(--pk-outl);font-size:12px}.picky-cookie-table th{color:var(--pk-on-surf);font-weight:600}.picky-cookie-table td{color:var(--pk-on-surf-var);word-break:break-all}.picky-cookie-table .cookie-actions{display:flex;gap:8px}.picky-cookie-table .cookie-actions button{padding:4px 8px;font-size:11px;border-radius:8px}.picky-cookie-table .cookie-actions button.delete{background-color:var(--pk-err);color:#fff}#picky-nav-slider-container{padding:8px 0}#picky-nav-slider{width:100%;-webkit-appearance:none;appearance:none;background:var(--pk-outl);height:5px;border-radius:3px;outline:none;cursor:pointer}#picky-nav-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:22px;height:22px;background:var(--pk-pri);border-radius:50%;cursor:pointer}#picky-nav-slider::-moz-range-thumb{width:22px;height:22px;background:var(--pk-pri);border-radius:50%;cursor:pointer}.picky-code-tabs{display:flex;border-bottom:1px solid var(--pk-outl);margin-bottom:12px}.picky-code-tab{padding:8px 16px;cursor:pointer;color:var(--pk-on-surf-var);border-bottom:2px solid transparent}.picky-code-tab.active{color:var(--pk-pri);border-bottom-color:var(--pk-pri)}.picky-code-panel{display:none}.picky-code-panel.active{display:block}.picky-code-panel pre{white-space:pre-wrap;word-break:break-all;font-family:'SF Mono',monospace;font-size:12px;padding:8px;background:var(--pk-surf-var);border-radius:8px;max-height:50vh;overflow:auto}`; }, injectGlobalStyles() { const css = `.${P_HL}{outline:2px dotted #ff453a!important;outline-offset:2px;box-shadow:0 0 0 9999px rgba(0,0,0,.4)!important;transition:outline .1s,box-shadow .1s}html.${P_ISO_B} > body{visibility:hidden!important}html.${P_ISO_B} .${P_ISO_P}{visibility:visible!important}html.${P_ISO_B} .${P_ISO_P} * {visibility:visible!important}`; const style = document.createElement('style'); style.id = `${P_ID}-global-style`; style.textContent = css; document.head.appendChild(style); }, injectStylesIntoShadowRoots() { const styleContent = `.${P_HL}{outline:2px dotted #ff453a!important;outline-offset:2px;box-shadow:0 0 0 9999px rgba(0,0,0,.4)!important;}`; document.querySelectorAll('*').forEach(el => { if (el.shadowRoot && !el.shadowRoot.getElementById(`${P_ID}-hl-style`)) { const style = document.createElement('style'); style.id = `${P_ID}-hl-style`; style.textContent = styleContent; el.shadowRoot.appendChild(style); } }); }, build() { this.injectGlobalStyles(); let host = document.getElementById(P_HOST); if (!host) { host = document.createElement('div'); host.id = P_HOST; document.documentElement.appendChild(host); } this.ui.host = host; const shadow = host.attachShadow({ mode: 'open' }); this.ui.shadow = shadow; const style = document.createElement('style'); style.textContent = this.getToolCss(); shadow.appendChild(style); this.ui.tool = document.createElement('div'); this.ui.tool.id = P_ID; this.ui.tool.className = this.st.pos; shadow.appendChild(this.ui.tool); this.ui.shield = document.createElement('div'); this.ui.shield.id = P_SHIELD; shadow.appendChild(this.ui.shield); this.ui.tool.addEventListener('click', this.act.bind(this)); this.draw(); setTimeout(() => this.ui.tool.classList.add('visible'), 50); this.observer = new MutationObserver(() => { if (!document.documentElement.contains(this.ui.host)) { document.documentElement.appendChild(this.ui.host); } }); this.observer.observe(document.documentElement, { childList: true }); }, draw() { const tool = this.ui.tool; if (!tool) return; tool.classList.toggle('minimized', this.st.min); tool.classList.toggle('minimal', !this.st.min && this.st.size === 'minimal'); tool.classList.remove('full'); if (!this.st.min && this.st.size === 'full') tool.classList.add('full'); this.ui.shield.style.display = (this.st.view === 'initial' || this.st.view === 'selected') && !this.st.min ? 'block' : 'none'; let content = ''; if (this.st.min) { content = ``; } else if (this.st.size === 'minimal') { content = `
${this.viewMin()}
`; } else { content = `
${this.viewFull()}
`; } tool.innerHTML = content; if (this.st.view === 'selected') { this.bindEls(); this.upd8(); } }, viewFull() { switch(this.st.view) { case 'selected': return this.viewSel(); case 'settings': return this.viewSet(); default: return `
요소 선택기
페이지에서 요소를 탭/클릭하세요...
`; } }, viewSel() { const s = this.getSliderCfg(); const slider = `
`; return `
요소 선택됨
CSS 선택자
${slider}
`; }, viewMin() { return ``; }, viewSet() { const c = this.st.cfg; const manual = c.intelligentMode ? 'style="display:none;"' : ''; return `
설정
복사 후 자동 닫기
선택자 생성 규칙
지능형 모드
ID 사용 (#id)
클래스 사용 (.class)
순서 사용 (:nth-of-type)
고급 기능
Shadow DOM 호환성
광고 차단 관리
개발자 도구 및 UI
`; }, bindEls() { this.ui.disp = this.ui.tool.querySelector('.picky-selector-display'); this.ui.match = this.ui.tool.querySelector('.picky-match-count'); this.ui.slider = this.ui.tool.querySelector('#picky-nav-slider'); if(this.ui.slider) this.ui.slider.addEventListener('input', this.onSlide.bind(this)); }, buildPath(el) { this.st.path = []; let curr = el; while (curr && curr.tagName !== 'BODY') { this.st.path.unshift(curr); curr = this.getParent(curr); } }, getSliderCfg() { const p = this.st.path; if (!p.length) return { min: 0, max: 0, val: 0 }; const rootIdx = p.indexOf(this.st.rootEl); const children = this.getChildren(this.st.rootEl); const currIdx = this.st.el === this.st.rootEl ? rootIdx : (p.includes(this.st.el) ? p.indexOf(this.st.el) : rootIdx + 1 + children.indexOf(this.st.el)); return { min: 0, max: rootIdx + children.length, val: currIdx }; }, onSlide(e) { const val = parseInt(e.target.value, 10); const p = this.st.path; const rootIdx = p.indexOf(this.st.rootEl); let newEl = val <= rootIdx ? p[val] : this.getChildren(this.st.rootEl)[val - rootIdx - 1]; if (newEl && newEl !== this.st.el) { this.unhl(this.st.el); this.st.el = newEl; this.hl(this.st.el); this.upd8(); } }, onPick(e) { if (this.st.min) return; const cp = e.composedPath(); if (cp[0] === this.ui.host || cp.includes(this.ui.tool) || cp.includes(this.Modal.el)) return; const realTarget = cp[0] === this.ui.shield ? this.getRealTarget(e) : cp[0]; if (!realTarget) return; e.preventDefault(); e.stopImmediatePropagation(); if (this.st.view === 'initial' || this.st.view === 'selected') { this.unhl(this.st.el); this.st.el = realTarget; this.st.rootEl = realTarget; this.buildPath(realTarget); this.hl(this.st.el); this.st.view = 'selected'; if (this.st.cfg.shadowDomSupport) { this.injectStylesIntoShadowRoots(); } this.draw(); } }, getRealTarget(e) { const touch = e.touches?.[0] || e.changedTouches?.[0] || e; const prevDisplay = this.ui.shield.style.display; this.ui.shield.style.display = 'none'; const target = this.st.cfg.shadowDomSupport ? this.getElementFromPointDeep(touch.clientX, touch.clientY) : document.elementFromPoint(touch.clientX, touch.clientY); this.ui.shield.style.display = prevDisplay; return target; }, onSelStart(e) { if (e.composedPath().includes(this.ui.tool)) return; initialTouchedEl = this.getRealTarget(e); touchMoved = false; }, onSelMove(e) { if (touchMoved || !initialTouchedEl) return; const t = e.touches[0]; const rect = initialTouchedEl.getBoundingClientRect(); const dx = t.clientX - rect.left; const dy = t.clientY - rect.top; if (Math.sqrt(dx*dx + dy*dy) > MOVE_THRESHOLD) touchMoved = true; }, onSelEnd(e) { if (this.st.min) return; if (touchMoved || e.composedPath().includes(this.ui.tool)) return; const realTarget = this.getRealTarget(e); this.onPick({ target: realTarget, composedPath: () => [realTarget], preventDefault: e.preventDefault, stopImmediatePropagation: e.stopImmediatePropagation }); }, act(e) { const t = e.target, aT = t.closest('[data-action]'), cT = t.closest('[data-cfg-key]'); if (cT) { const k = cT.dataset.cfgKey; if (typeof this.st.cfg[k] === 'boolean') { this.st.cfg[k] = cT.checked; } if (k === 'shadowDomSupport' && cT.checked) this.injectStylesIntoShadowRoots(); if (k === 'intelligentMode') this.ui.tool.querySelector('.picky-manual-settings').style.display = cT.checked ? 'none' : 'block'; this.upd8(); return; } if (!aT) return; const act = aT.dataset.action, type = aT.dataset.type; const actions = { close: () => this.end(false), cycleSize: () => { if (this.st.min) { this.st.min = false; this.st.size = 'full'; } else if (this.st.size === 'full') { this.st.size = 'minimal'; } else { this.st.min = true; } this.draw(); }, showSettings: () => { this.st.view = 'settings'; this.draw(); }, showSelected: () => { this.st.view = 'selected'; this.draw(); }, reset: () => { this.cleanup(); this.unhl(this.st.el); this.st.el = null; this.st.rootEl = null; this.st.path = []; this.st.view = 'initial'; this.st.size = 'full'; this.st.min = false; this.draw(); }, selParent: () => { this.cleanup(); const p = this.getParent(this.st.el); if (p && p.tagName?.toLowerCase() !== 'body' && p.tagName?.toLowerCase() !== 'html') { this.unhl(this.st.el); this.st.el = p; this.hl(this.st.el); if (!this.st.path.includes(p)) this.buildPath(this.st.rootEl); this.upd8(); this.draw(); } }, selChild: () => this.showChildSel(), selSimilar: () => { const selInfo = this.css(this.st.el); const s = selInfo.selector.replace(/:nth-of-type\(\d+\)/g, ''); if (this.ui.disp) this.ui.disp.textContent = s + (selInfo.root instanceof ShadowRoot ? ' (in Shadow DOM)' : ''); this.upd8(); }, toggleHide: () => { const { selector, root } = this.st.selInfo; if (!selector) return; if (this.st.hidden) this.restoreHidden(); else this.applyHide(selector, root); this.draw(); }, permanentBlock: () => { const { selector } = this.st.selInfo; if (selector) { if (confirm(`다음 선택자를 영구적으로 차단(숨김)하시겠습니까?\n\n${selector}\n\n* "설정" 메뉴에서 차단을 해제할 수 있습니다.`)) { this.Blocker.add(selector); this.act({ target: { closest: () => ({ dataset: { action: 'reset' } }) } }); } } else { alert('선택할 수 없는 요소입니다.'); } }, showBlockRules: () => { const renderList = () => { const rules = this.Blocker.getRules(); if (rules.length === 0) return '
저장된 차단 규칙이 없습니다.
'; const items = rules.map(r => `
  • ${r}
  • `).join(''); return ``; }; this.Modal.show('현재 차단 규칙 관리', renderList(), true); this.Modal.el.querySelector('.picky-modal-body').addEventListener('click', (e) => { const btn = e.target.closest('button[data-rule]'); if (!btn) return; const rule = btn.dataset.rule; if (confirm(`이 규칙을 삭제하시겠습니까?\n\n${rule}`)) { this.Blocker.remove(rule); this.Modal.el.querySelector('.picky-modal-body').innerHTML = renderList(); } }); }, resetBlocks: () => { if (confirm('현재 사이트(' + window.location.hostname + ')에 설정된 모든 차단 규칙을 삭제하시겠습니까?')) { this.Blocker.reset(); } }, toggleIsolate: () => this.toggleIso(), copyCSS: () => this.copy(false), copyRule: () => this.copy(true), toggleAutoClose: () => { this.st.autoClose = t.checked; }, moveTop: () => this.move('top'), moveBottom: () => this.move('bottom'), extractUrl: () => this.getUrl(), extractAttr: () => this.getAttr(), inspectCode: () => this.showCodeInspector(), showSource: () => this.showSrc(type), showCookies: () => this.showCookies(), showFp: () => this.showFp(), }; if (actions[act]) actions[act](); }, hl(el) { el?.classList.add(P_HL); }, unhl(el) { el?.classList.remove(P_HL); }, cleanup() { this.restoreHidden(); if(this.st.isolate) this.toggleIso(true); }, applyHide(selector, root) { try { this.st.hiddenEls = Array.from(root.querySelectorAll(selector)); this.st.hiddenEls.forEach(el => { if (!this.st.origDisp.has(el)) this.st.origDisp.set(el, el.style.display || ''); el.style.display = 'none'; }); this.st.hidden = true; } catch(e) {} }, restoreHidden() { this.st.hiddenEls.forEach(el => { if (this.st.origDisp.has(el)) el.style.display = this.st.origDisp.get(el); }); this.st.hiddenEls = []; this.st.origDisp.clear(); this.st.hidden = false; }, toggleIso(forceOff = false) { this.st.isolate = forceOff ? false : !this.st.isolate; document.querySelectorAll(`.${P_ISO_P}`).forEach(el => el.classList.remove(P_ISO_P)); if (this.st.isolate && this.st.el) { let current = this.st.el; while(current) { current.classList.add(P_ISO_P); current = this.getParent(current); } document.documentElement.classList.add(P_ISO_B); } else { document.documentElement.classList.remove(P_ISO_B); } this.draw(); }, copy(asRule = false) { const { selector, root } = this.st.selInfo; if (!selector) return; if (this.st.cfg.shadowDomSupport && root instanceof ShadowRoot) { alert("Shadow DOM 내부의 선택자는 복사되지만, 전역 스타일시트나 광고 차단 규칙에서는 작동하지 않을 수 있습니다. 개발자 도구의 해당 컴포넌트 내부에서 사용해야 합니다."); } let text = asRule ? `${window.location.hostname}##${selector}` : selector; navigator.clipboard.writeText(text).then(() => { const b = this.ui.tool.querySelector(asRule ? '[data-action="copyRule"]' : '[data-action="copyCSS"]'); if (!b) return; const o = b.innerHTML; b.textContent = '복사 완료!'; b.classList.add('copied'); setTimeout(() => { if (this.st.autoClose) this.end(false); else { b.innerHTML = o; b.classList.remove('copied'); } }, 1200); }).catch(() => { prompt('복사 실패:', text); if (this.st.autoClose) this.end(false); }); }, move(pos) { this.st.pos = pos; this.ui.tool.className = `${pos} visible`; }, getUrl() { let el = this.st.el, url = null; for (let i = 0; i < 5 && el; i++) { url = el.getAttribute('href') || el.getAttribute('src') || el.getAttribute('data-src') || el.getAttribute('data-original'); if (url) break; const bg = window.getComputedStyle(el).backgroundImage; if (bg?.includes('url')) { url = bg.match(/url\(['"]?(.*?)['"]?\)/)[1]; if(url) break; } el = this.getParent(el); } if (url) prompt("추출된 URL:", new URL(url, window.location.href).href); else alert("URL을 찾을 수 없습니다."); }, getAttr() { const a = prompt('추출할 속성 이름 입력 (예: data-id, alt):'); if (!a) return; const v = this.st.el?.getAttribute(a); if (v !== null) prompt(`'${a}' 속성 값:`, v); else alert(`'${a}' 속성을 찾을 수 없습니다.`); }, showChildSel() { const children = this.getChildren(this.st.el); if (!children || children.length === 0) { alert('하위 요소가 없습니다.'); return; } const items = children.map((c, i) => { const t = c.tagName.toLowerCase(), id = c.id ? `#${c.id}` : '', cls = c.className ? `.${String(c.className).split(' ').filter(Boolean).join('.')}` : ''; return `
  • ${t}${id}${cls}
  • `; }).join(''); this.Modal.show('하위 요소 선택', ``, true); this.Modal.el.querySelector('.picky-child-list').addEventListener('click', (e) => { const t = e.target.closest('li[data-idx]'); if (!t) return; const i = parseInt(t.dataset.idx, 10), n = children[i]; if (n) { this.unhl(this.st.el); this.st.el = n; this.hl(this.st.el); this.upd8(); } this.Modal.hide(); }); }, showCodeInspector() { if (!this.st.el) return; const el = this.st.el; const toolClasses = [P_HL, P_ISO_P]; const getRelatedHTML = () => { const cleanEl = el.cloneNode(true); cleanEl.classList.remove(...toolClasses); cleanEl.querySelectorAll(toolClasses.map(c => `.${c}`).join(', ')).forEach(child => child.classList.remove(...toolClasses)); let formattedHtml = cleanEl.outerHTML; let indentedHtml = ''; let indentLevel = 0; const indentSize = 2; formattedHtml.split(/(?=<)/).forEach(line => { const trimmedLine = line.trim(); if (!trimmedLine) return; if (trimmedLine.startsWith('')) indentLevel++; }); return indentedHtml.trim(); }; const getRelatedCSS = () => { let cssText = `/* --- 인라인 스타일 --- */\n`; cssText += el.style.cssText ? `${this.css(el).selector} {\n ${el.style.cssText.replace(/; /g, ';\n ')}\n}\n\n` : '없음\n\n'; cssText += `/* --- 계산된 스타일 (기본값 제외) --- */\n`; let computedStylesText = ''; try { const computed = window.getComputedStyle(el); const defaultStyles = window.getComputedStyle(document.createElement(el.tagName)); const props = new Set(); for(let i=0; i props.add(p)); for (const prop of Array.from(props).sort()) { const value = computed.getPropertyValue(prop); if (value && value !== defaultStyles.getPropertyValue(prop)) { if (prop.startsWith('-') || ['width', 'height', 'top', 'left', 'right', 'bottom'].some(s => prop.includes(s))) continue; computedStylesText += ` ${prop}: ${value};\n`; } } } catch(e) { } return cssText + (computedStylesText ? `${this.css(el).selector} {\n${computedStylesText}}\n` : '추가적인 계산된 스타일 없음\n'); }; const getRelatedJS = () => { let jsText = `/* --- 인라인 이벤트 핸들러 --- */\n`; let hasInline = false; for (const attr of el.attributes) { if (attr.name.startsWith('on')) { jsText += `${attr.name}="${attr.value}"\n`; hasInline = true; } } if (!hasInline) jsText += '없음\n'; jsText += `\n/* --- 인라인 스크립트 연관 코드 (ID/클래스 기반 검색) --- */\n`; let foundScripts = ''; const searchTerms = [el.id, ...Array.from(el.classList).filter(c => !toolClasses.includes(c))].filter(Boolean); if (searchTerms.length > 0) { const regex = new RegExp(searchTerms.map(t => CSS.escape(t)).join('|'), 'i'); document.querySelectorAll('script:not([src])').forEach((script, i) => { if (regex.test(script.innerHTML)) { foundScripts += `\n// 인라인 스크립트 #${i+1} 에서 발견:\n${script.innerHTML.substring(0, 1000).trim()}...\n`; } }); } jsText += (foundScripts || '없음\n'); jsText += `\n/* 외부 스크립트나 동적 이벤트 리스너는 개발자 도구에서 확인해야 합니다. */`; return jsText; }; const content = `
    HTML
    CSS
    JS
    ${getRelatedHTML().replace(/
    ${getRelatedCSS().replace(/
    ${getRelatedJS().replace(/
    `; this.Modal.show('연관 코드 검사기', content, true); const modal = this.Modal.el; modal.querySelectorAll('.picky-code-tab').forEach(tab => { tab.addEventListener('click', () => { modal.querySelector('.picky-code-tab.active').classList.remove('active'); modal.querySelector('.picky-code-panel.active').classList.remove('active'); tab.classList.add('active'); modal.querySelector(`.picky-code-panel[data-panel="${tab.dataset.tab}"]`).classList.add('active'); }); }); }, showSrc(type) { let t = '', c = ''; switch(type) { case 'html': t = 'HTML (현재 DOM)'; c = document.documentElement.outerHTML; break; case 'css': t = 'CSS (내부 스타일)'; c = `/* 동일 출처 스타일시트와 인라인 스타일만 표시됩니다. */\n\n`; Array.from(document.styleSheets).forEach(s => { try { if (!s.href || s.href.startsWith(location.origin)) { c += `/* --- ${s.href || 'Inline'} --- */\n`; Array.from(s.cssRules).forEach(r => c += r.cssText + '\n'); } } catch (e) {} }); break; case 'js': t = 'JavaScript'; c = `/* 페이지에 로드된 스크립트 목록입니다. */\n\n`; Array.from(document.scripts).forEach((s, i) => { c += s.src ? `\n\n\n` : `\n\n\n`; }); break; } this.Modal.show(t, c); }, showCookies() { const getCookies = () => document.cookie.split(';').filter(Boolean).map(c => { const parts = c.trim().split('='); return { name: parts[0], value: decodeURIComponent(parts.slice(1).join('=')) }; }); const render = () => { const cookies = getCookies(); if (cookies.length === 0) { return '표시할 쿠키가 없습니다 (HttpOnly 쿠키는 접근 불가).'; } const rows = cookies.map(c => `${c.name}${c.value}`).join(''); return `

    HttpOnly 플래그가 설정된 쿠키는 보안 정책상 표시되지 않습니다.

    ${rows}`; }; this.Modal.show('쿠키 정보', render(), true); this.Modal.el.querySelector('.picky-modal-body').addEventListener('click', e => { const btn = e.target.closest('button[data-cookie-name]'); if (!btn) return; const name = btn.dataset.cookieName, action = btn.dataset.action; if (action === 'editCookie') { const current = getCookies().find(c => c.name === name)?.value || ''; const newValue = prompt(`'${name}' 쿠키의 새 값을 입력하세요:`, current); if (newValue !== null) { document.cookie = `${name}=${encodeURIComponent(newValue)};path=/;max-age=31536000`; this.Modal.el.querySelector('.picky-modal-body').innerHTML = render(); } } else if (action === 'deleteCookie') { if (confirm(`'${name}' 쿠키를 삭제하시겠습니까?`)) { document.cookie = `${name}=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT`; this.Modal.el.querySelector('.picky-modal-body').innerHTML = render(); } } }); }, showFp() { let c = "--- 브라우저/시스템 ---\n"; try { c += `User Agent: ${navigator.userAgent}\n언어: ${navigator.language}\n시간대: ${Intl.DateTimeFormat().resolvedOptions().timeZone}\n스레드 수: ${navigator.hardwareConcurrency || 'N/A'}\n메모리(GB): ${navigator.deviceMemory || 'N/A'}\n\n--- 화면 ---\n`; c += `해상도: ${screen.width}x${screen.height}\n사용 가능: ${screen.availWidth}x${screen.availHeight}\n색상 깊이: ${screen.colorDepth}\n픽셀 비율: ${devicePixelRatio}\n\n--- 렌더링 ---\n`; const gl = document.createElement('canvas').getContext('webgl'); const dbg = gl.getExtension('WEBGL_debug_renderer_info'); c += `WebGL 벤더: ${gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL)}\nWebGL 렌더러: ${gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL)}\n\n`; } catch (e) {} c += "--- 네트워크 (Performance API) ---\n"; const r = performance.getEntriesByType('resource'); c += `${r.length}개 리소스 요청됨.\n\n`; r.slice(0, 20).forEach(res => { c += `[${res.initiatorType}] ${res.name} (${Math.round(res.duration)}ms)\n`; }); this.Modal.show('핑거프린팅 정보', c); }, run() { if (!document.documentElement) return; this.Blocker.init(); this.b = { onPick: this.onPick.bind(this), onSelStart: this.onSelStart.bind(this), onSelMove: this.onSelMove.bind(this), onSelEnd: this.onSelEnd.bind(this) }; this.build(); document.addEventListener('click', this.b.onPick, { capture: true }); document.addEventListener('touchstart', this.b.onSelStart, { capture: true, passive: true }); document.addEventListener('touchmove', this.b.onSelMove, { capture: true, passive: true }); document.addEventListener('touchend', this.b.onSelEnd, { capture: true }); window.Picky = this; }, end(restore = true) { if (restore) this.cleanup(); document.removeEventListener('click', this.b.onPick, { capture: true }); document.removeEventListener('touchstart', this.b.onSelStart, { capture: true }); document.removeEventListener('touchmove', this.b.onSelMove, { capture: true }); document.removeEventListener('touchend', this.b.onSelEnd, { capture: true }); this.ui.tool?.classList.remove('visible'); this.Modal.hide(); this.observer?.disconnect(); setTimeout(() => { this.ui.host?.remove(); document.getElementById(`${P_ID}-global-style`)?.remove(); document.querySelectorAll('*').forEach(el => { if (el.shadowRoot) { const shadowStyle = el.shadowRoot.getElementById(`${P_ID}-hl-style`); if (shadowStyle) shadowStyle.remove(); } }); this.unhl(document.querySelector(`.${P_HL}`)); }, 400); delete window.Picky; }, }; P.run(); })();