PageSourceSearch

https://huphailab.com/assets/copy-editor-BjKOYxRi.js

js huphailab.com collected 2026-10-08 23:19:51 UTC 17,947 bytes, 52 lines download raw bytes

1const h="copy-editor-panel";let A='[data-slot="topnav-shell"], header, [role="banner"]';const q='a, button, [role="button"], [role="link"], summary',K=new Set(["SCRIPT","STYLE","NOSCRIPT","SVG","PATH","IMG","VIDEO","AUDIO","IFRAME","CANVAS","INPUT","TEXTAREA","SELECT","OPTION","HEAD","META","LINK","TITLE"]);let R="copydeck",k="copydeck-copy-edits";const u=new Map;let T=!1,L,Y=0;function f(e,t,o=0){return`${e}
2${t}
3#${o}`}function X(){try{const e=localStorage.getItem(k);if(!e)return;for(const t of JSON.parse(e))u.set(f(t.page,t.old,t.dup??0),t)}catch{}}function C(){try{localStorage.setItem(k,JSON.stringify(Array.from(u.values())))}catch{}}let I="copydeck-copy-memos";const m=new Map;function Q(){try{const e=localStorage.getItem(I);if(!e)return;for(const t of JSON.parse(e))m.set(f(t.page,t.anchorText),t)}catch{}}function z(){try{localStorage.setItem(I,JSON.stringify(Array.from(m.values())))}catch{}}const Z="section, main, header, nav, footer, body";function P(e){let t=e.parentElement,o=null,n=0;for(;t&&n<7&&!(t===document.body||t.matches(Z));){const r=getComputedStyle(t),c=parseFloat(r.borderTopLeftRadius)>0,y=r.backgroundColor!=="rgba(0, 0, 0, 0)"&&r.backgroundColor!=="transparent"||r.backgroundImage!=="none",G=parseFloat(r.borderTopWidth)>0,W=!!r.boxShadow&&r.boxShadow!=="none";(t.tagName==="ARTICLE"||t.tagName==="LI"||t.tagName==="FIGURE"||c&&(y||G||W))&&(o=t),t=t.parentElement,n++}return o||e}function D(e){const t=e.getAttribute("data-ce-orig");if(t)return t;const o=e.querySelector("h1,h2,h3,h4,h5,h6");if(o&&(o.textContent||"").trim())return(o.textContent||"").trim();const n=e.querySelector("[data-ce-orig]");return n?n.getAttribute("data-ce-orig")||"":(e.textContent||"").trim().slice(0,80)}function ee(){document.querySelectorAll(".ce-has-memo").forEach(e=>e.classList.remove("ce-has-memo"));for(const e of Array.from(m.values())){if(e.page!==location.pathname)continue;let t=null;document.querySelectorAll("[data-ce-orig]").forEach(o=>{!t&&o.getAttribute("data-ce-orig")===e.anchorText&&(t=o)}),t||document.querySelectorAll("h1,h2,h3,h4,h5,h6").forEach(o=>{!t&&(o.textContent||"").trim()===e.anchorText&&(t=o)}),t&&P(t).classList.add("ce-has-memo")}}function te(e){if(!(e instanceof HTMLElement)||e.getClientRects().length===0)return!1;const t=getComputedStyle(e);return t.visibility!=="hidden"&&t.display!=="none"}function oe(e){let t=e;for(;t;){let o=t;for(;o;){if(/^H[1-6]$/.test(o.tagName))return(o.textContent||"").trim().slice(0,60);o=o.previousElementSibling}t=t.parentElement}return""}function ne(e){if(K.has(e.tagName)||e.id===h||e.closest(`#${h}`)||e.closest("#ce-memo-bar, #ce-memo-pop, #ce-memo-backdrop")||e.closest(A)||e.closest("[data-ce-id]")||!te(e)||!(e.textContent||"").replace(/\s+/g," ").trim())return!1;const o=H(e);for(const n of Array.from(e.children)){if(n.tagName==="BR"||n.tagName==="WBR")continue;if(n instanceof SVGElement||n.tagName==="IMG"||n.tagName==="VIDEO"||n.tagName==="CANVAS"||n.tagName==="IFRAME"){if(o)continue;return!1}const c=getComputedStyle(n).display;if(!c.startsWith("inline")&&c!=="contents")return!1}return!0}function H(e){return!!e.closest(q)}function re(e){const t=`ce-${Y++}`,o=(e.innerText||"").trim(),n=Array.from(document.querySelectorAll("[data-ce-orig]")).filter(c=>c.getAttribute("data-ce-orig")===o).length;e.setAttribute("data-ce-id",t),e.setAttribute("data-ce-orig",o),e.setAttribute("data-ce-dup",String(n)),e.setAttribute("data-ce-heading",oe(e)),e.contentEditable="true",e.spellcheck=!1,e.classList.add("ce-editable"),H(e)&&e.classList.add("ce-interactive");const r=u.get(f(location.pathname,o,n));r&&r.new!==o&&(e.innerText=r.new,e.classList.add("ce-changed"),r.new===""&&e.classList.add("ce-emptied"))}function j(e){const t=e.target;t?.closest&&(t.closest(`#${h}`)||t.closest("#ce-memo-bar, #ce-memo-pop, #ce-memo-backdrop")||t.closest(A)||t.closest(q)&&(e.preventDefault(),e.stopPropagation()))}function ae(){try{const e=new URL(location.href);e.searchParams.has("edit")||(e.searchParams.set("edit","1"),history.replaceState(history.state,"",e.toString()))}catch{}}function O(e=document.body){if(!T)return;ae(),e.querySelectorAll("*").forEach(o=>{o.hasAttribute("data-ce-id")||ne(o)&&re(o)}),ie(),ee(),v()}function ie(){document.querySelectorAll("[data-ce-id]").forEach(e
3=>{e.closest(A)&&(e.removeAttribute("data-ce-id"),e.removeAttribute("data-ce-orig"),e.removeAttribute("data-ce-dup"),e.removeAttribute("data-ce-heading"),e.contentEditable="inherit",e.classList.remove("ce-editable","ce-interactive","ce-changed"))})}function B(e){const t=e.target?.closest?.("[data-ce-id]");if(!t)return;const o=t.getAttribute("data-ce-orig")||"",n=Number(t.getAttribute("data-ce-dup")||0),r=(t.innerText||"").trim(),c=f(location.pathname,o,n);r===o?(u.delete(c),t.classList.remove("ce-changed","ce-emptied")):(u.set(c,{old:o,new:r,page:location.pathname,nearHeading:t.getAttribute("data-ce-heading")||"",tag:t.tagName,ts:Date.now(),dup:n}),t.classList.add("ce-changed"),t.classList.toggle("ce-emptied",r==="")),C(),v()}function $(e){if(!e.target?.closest?.("[data-ce-id]"))return;e.preventDefault();const o=e.clipboardData?.getData("text/plain")??"";document.execCommand("insertText",!1,o)}function ce(){const e=Array.from(u.values()).sort((o,n)=>o.page.localeCompare(n.page)||n.ts-o.ts).map(({old:o,new:n,page:r,nearHeading:c,tag:y})=>({old:o,new:n,page:r,nearHeading:c,tag:y})),t=Array.from(m.values()).sort((o,n)=>o.page.localeCompare(n.page)||n.ts-o.ts).map(({anchorText:o,note:n,page:r,nearHeading:c,tag:y})=>({anchorText:o,note:n,page:r,nearHeading:c,tag:y}));return JSON.stringify({_tool:"livecopy",_howto:"웹사이트 카피(문구) 검수 변경분입니다. 반영 방법: 해당 프로젝트 repo 에서 `npx livecopy apply <이 JSON 파일>` 실행 → edits 는 옛 문구를 새 문구로 자동 치환하고, git diff 로 검토 후 커밋하세요. memos 는 자동 반영하지 말고 개발자가 처리할 요청 목록입니다(위치 후보 파일이 함께 출력됨). livecopy 가 설치돼 있지 않으면 edits 의 old→new 를 소스에서 찾아 치환하면 됩니다.",_docs:"https://github.com/goodsoo/livecopy",edits:e,memos:t},null,2)}function se(){const e=ce(),t=new Blob([e],{type:"application/json"}),o=URL.createObjectURL(t),n=document.createElement("a"),r=new Date().toISOString().slice(0,10);n.href=o,n.download=`${R}-copy-changes-${r}.json`,n.click(),URL.revokeObjectURL(o)}let s=null,E=null;function v(){if(!E)return;E.textContent=String(u.size);const e=s?.querySelector("[data-ce-memocount]");e&&(e.textContent=String(m.size));const t=u.size+m.size,o=s?.querySelector("[data-ce-dl]");o&&(o.disabled=t===0);const n=s?.querySelector("[data-ce-reset]");n&&(n.disabled=t===0)}function de(){s=document.createElement("div"),s.id=h,s.innerHTML=`
4    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
5      <span style="font-weight:600;font-size:14px;">✏️ 문구 편집</span>
6      <button data-ce-exit title="편집 모드 끄기" style="
7        border:0;border-radius:6px;cursor:pointer;background:#3f3f46;color:#e4e4e7;
8        font-size:11px;padding:4px 8px;">편집 종료 ✕</button>
9    </div>
10    <ul style="margin:0 0 10px;padding-left:16px;font-size:12px;line-height:1.7;color:#d4d4d8;">
11      <li>글자·버튼 클릭해 수정 (지우면 삭제)</li>
12      <li>추가·요청은 글 위 <b>💬</b> 눌러 메모</li>
13      <li>이동은 <b>상단 메뉴</b>로 · 자동 저장</li>
14    </ul>
15    <div style="font-size:12px;margin-bottom:8px;">
16      수정 <span data-ce-count style="font-weight:700;color:#fde047;">0</span>건 ·
17      메모 <span data-ce-memocount style="font-weight:700;color:#5eead4;">0</span>건
18    </div>
19    <button data-ce-dl disabled style="
20      width:100%;padding:9px 12px;border:0;border-radius:6px;cursor:pointer;
21      background:#fde047;color:#111;font-weight:600;font-size:13px;
22    ">JSON 다운로드</button>
23    <button data-ce-reset disabled style="
24      width:100%;margin-top:6px;padding:6px 12px;border:0;border-radius:6px;cursor:pointer;
25      background:transparent;color:#a1a1aa;font-size:11px;text-decoration:underline;
26    ">전체 초기화</button>
27  `,Object.assign(s.style,{position:"fixed",right:"16px",bottom:"16px",width:"220px",padding:"14px",background:"#18181b",color:"#fff",borderRadius:"10px",boxShadow:"0 8px 30px rgba(0,0,0,.35)",zIndex:"2147483647",fontFamily:"system-ui, sans-serif"}),document.body.appendChild(s),E=s.querySelector("[data-ce-count]"),s.querySelector("[data-ce-exit]").addEventListener("click",be),s.querySelector("[data-ce-dl]").addEventListener("click",
27se),s.querySelector("[data-ce-reset]").addEventListener("click",()=>{window.confirm("저장된 모든 수정·메모를 지울까요? 되돌릴 수 없습니다.")&&(u.clear(),m.clear(),C(),z(),document.querySelectorAll(".ce-changed").forEach(e=>{const t=e.getAttribute("data-ce-orig");t!=null&&(e.innerText=t),e.classList.remove("ce-changed","ce-emptied")}),document.querySelectorAll(".ce-has-memo").forEach(e=>e.classList.remove("ce-has-memo")),x(),v())})}let i=null,p=null,l=null,a=null,d=null,g=null,w=null,S,M="";const le='<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',ue='<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/></svg>';function U(e){return e.hasAttribute("data-ce-id")?[e]:Array.from(e.querySelectorAll("[data-ce-id]"))}function me(e){return U(e).some(t=>{const o=t.getAttribute("data-ce-orig");return o!=null&&u.has(f(location.pathname,o))})}function pe(e){let t=!1;for(const o of U(e)){const n=o.getAttribute("data-ce-orig");n!=null&&u.delete(f(location.pathname,n))&&(o.innerText=n,o.classList.remove("ce-changed","ce-emptied"),t=!0)}t&&(C(),v())}function _(e){if(!i)return;l&&(l.style.display=me(e)?"inline-flex":"none"),i.style.display="inline-flex";const t=i.offsetWidth||120,o=i.offsetHeight||26,n=e.getBoundingClientRect();let r=n.top-o-4;r<4&&(r=n.bottom+4);const c=Math.min(window.innerWidth-t-4,Math.max(4,n.right-t));i.style.top=`${r}px`,i.style.left=`${c}px`}function V(e){const t=e.target;if(!t?.closest||d?.style.display==="block")return;if(i?.contains(t)||a?.contains(t)){window.clearTimeout(S);return}const o=t.closest(".ce-editable");o&&(window.clearTimeout(S),g=P(o),_(g))}function F(){window.clearTimeout(S),S=window.setTimeout(()=>{i&&(i.style.display="none")},350)}function fe(e){if(!a||!d)return;w=e;const t=D(e),o=m.get(f(location.pathname,t));a.querySelector("[data-ce-anchor]").textContent=t.slice(0,60)+(t.length>60?"…":"");const n=a.querySelector("textarea");n.value=o?.note||"",a.querySelector("[data-ce-memo-del]").style.display=o?"block":"none",d.style.display="block",a.style.display="block",i&&(i.style.display="none"),M=document.body.style.overflow,document.body.style.overflow="hidden",n.focus()}function x(){a&&(a.style.display="none"),d&&(d.style.display="none"),document.body.style.overflow=M,w=null}function N(){if(!w||!a)return;const e=w,t=D(e),o=f(location.pathname,t),n=a.querySelector("textarea").value.trim();n?(m.set(o,{page:location.pathname,anchorText:t,note:n,tag:e.tagName,nearHeading:e.getAttribute("data-ce-heading")||t,ts:Date.now()}),e.classList.add("ce-has-memo")):(m.delete(o),e.classList.remove("ce-has-memo")),z(),v(),x()}function ge(){i=document.createElement("div"),i.id="ce-memo-bar",Object.assign(i.style,{position:"fixed",display:"none",gap:"4px",zIndex:"2147483646",fontFamily:"system-ui, sans-serif"});const e={display:"inline-flex",alignItems:"center",justifyContent:"center",gap:"4px",height:"24px",padding:"0 8px",border:"0",borderRadius:"6px",color:"#fff",fontSize:"11px",fontWeight:"600",lineHeight:"1",cursor:"pointer",boxShadow:"0 2px 8px rgba(0,0,0,.3)"};p=document.createElement("button"),p.id="ce-memo-btn",p.type="button",p.title="이 카드/글에 메모·요청 남기기",p.innerHTML=`${le}<span>메모</span>`,Object.assign(p.style,{...e,background:"#0f766e"}),p.addEventListener("click",t=>{t.preventDefault(),t.stopPropagation(),g&&fe(g)}),l=document.createElement("button"),l.id="ce-undo-btn",l.type="button",l.title="이 카드/글의 수정을 원문으로 되돌리기",l.innerHTML=`${ue}<span>되돌리기</span>`,Object.assign(l.style,{...e,background:"#57534e",display:"none"}),l.addEventListener("click",t=>{t.preventDefault(),t.stopPropagation(),g&&(pe(g),_(g))}),i.appendChild(p),i.appendChild(l),document.body.appendChild(i),d=document.createElement("div"),d.id="ce-memo-backdrop",Object.assign(d.style,{position:"fixed",inset:"0",display:"none",background:"rgba(0,0,0,.45)",zIndex:"2147483646"}),d.addEventListener("click",x),d.addEventListener("wheel",t=>t.preventDefault(),{passive:!1}),document.body.appendChild(d),a=document.createElement("div"),a.id="ce-memo-pop",a.innerHTML=`
28    <div style="font-weight:600;font-size:14px;margin-bottom:8px;">💬 메모 / 요청</div>
29    <div style="font-size:11px;color:#a1a1aa;margin-bottom:8px;">위치: “<span data-ce-anchor></span>”</div>
30    <textarea placeholder="예: 이 카드 아래에 도입 사례 섹션 추가해주세요" style="
31      width:100%;height:90px;resize:vertical;box-sizing:border-box;padding:9px;
32      border:1px solid #3f3f46;border-radius:6px;background:#27272a;color:#fff;
33      font-size:13px;font-family:inherit;"></textarea>
34    <div style="display:flex;gap:6px;margin-top:10px;">
35      <button data-ce-memo-save style="flex:1;padding:8px;border:0;border-radius:6px;cursor:pointer;background:#14b8a6;color:#052e2b;font-weight:600;font-size:13px;">저장</button>
36      <button data-ce-memo-del style="padding:8px 10px;border:0;border-radius:6px;cursor:pointer;background:#3f3f46;color:#fca5a5;font-size:13px;display:none;">삭제</button>
37      <button data-ce-memo-close style="padding:8px 10px;border:0;border-radius:6px;cursor:pointer;background:transparent;color:#a1a1aa;font-size:13px;">닫기</button>
38    </div>
39  `,Object.assign(a.style,{position:"fixed",display:"none",left:"50%",top:"50%",transform:"translate(-50%, -50%)",width:"340px",maxWidth:"calc(100vw - 32px)",padding:"16px",background:"#18181b",color:"#fff",borderRadius:"12px",border:"1px solid #14b8a6",boxShadow:"0 12px 40px rgba(0,0,0,.5)",zIndex:"2147483647",fontFamily:"system-ui, sans-serif"}),document.body.appendChild(a),a.querySelector("[data-ce-memo-save]").addEventListener("click",N),a.querySelector("[data-ce-memo-close]").addEventListener("click",x),a.querySelector("[data-ce-memo-del]").addEventListener("click",()=>{a.querySelector("textarea").value="",N()}),document.addEventListener("mouseover",V,!0),document.addEventListener("mouseout",F,!0),document.addEventListener("keydown",J)}
39function J(e){e.key==="Escape"&&d?.style.display==="block"&&x()}let b=null;function be(){T=!1,window.clearTimeout(L),b?.disconnect(),b=null,document.removeEventListener("input",B,!0),document.removeEventListener("paste",$,!0),document.removeEventListener("click",j,!0),document.removeEventListener("mouseover",V,!0),document.removeEventListener("mouseout",F,!0),document.removeEventListener("keydown",J),document.querySelectorAll("[data-ce-id]").forEach(e=>{const t=e.getAttribute("data-ce-orig");t!=null&&(e.innerText=t),e.removeAttribute("data-ce-id"),e.removeAttribute("data-ce-orig"),e.removeAttribute("data-ce-dup"),e.removeAttribute("data-ce-heading"),e.contentEditable="inherit",e.classList.remove("ce-editable","ce-interactive","ce-changed","ce-emptied","ce-has-memo")}),s?.remove(),i?.remove(),a?.remove(),d?.remove(),document.getElementById("ce-styles")?.remove(),document.body.style.overflow=M,s=i=a=d=null,E=null,p=l=null;try{const e=new URL(location.href);e.searchParams.delete("edit"),history.replaceState(null,"",e.toString())}catch{}console.info("[copy-editor] 편집 모드 종료")}function he(){const e=document.createElement("style");e.id="ce-styles",e.textContent=`
40    .ce-editable:hover { outline: 1px dashed rgba(59,130,246,.7); outline-offset: 2px; cursor: text; }
41    .ce-editable.ce-interactive:hover { outline-color: rgba(168,85,247,.8); } /* 본문 버튼·링크는 보라 힌트 */
42    .ce-editable:focus { outline: 2px solid #3b82f6; outline-offset: 2px; }
43    .ce-changed { background: rgba(253,224,71,.35) !important; box-shadow: 0 0 0 2px rgba(253,224,71,.5); border-radius: 2px; }
44    /* 완전히 지운 박스: 원문을 취소선으로 흐리게 표시해 크기를 유지 → 클릭·hover 가능 */
45    .ce-emptied { display: inline-block; min-width: 40px; min-height: 1em; }
46    .ce-emptied::after { content: attr(data-ce-orig); opacity: .15; text-decoration: line-through; pointer-events: none; }
47    /* 메모 달린 카드/박스 = 청록 링 (outline 이라 카드 자체 그림자를 안 덮음) */
48    .ce-has-memo { outline: 2px solid rgba(20,184,166,.8); outline-offset: 2px; }
49    #ce-memo-btn:hover { background: #0d5f58 !important; }
50    #ce-undo-btn:hover { background: #44403c !important; }
51    #${h} button:disabled { opacity: .5; cursor: default; }
52  `,document.head.appendChild(e)}function xe(e={}){document.getElementById(h)||(e.headerSelector&&(A=e.headerSelector),e.storagePrefix&&(R=e.storagePrefix,k=`${e.storagePrefix}-copy-edits`,I=`${e.storagePrefix}-copy-memos`),T=!0,X(),Q(),he(),de(),ge(),document.addEventListener("input",B,!0),document.addEventListener("paste",$,!0),document.addEventListener("click",j,!0),setTimeout(()=>O(),800),b=new MutationObserver(()=>{window.clearTimeout(L),L=window.setTimeout(()=>{b?.disconnect(),O(),b?.observe(document.body,{childList:!0,subtree:!0})},400)}),b.observe(document.body,{childList:!0,subtree:!0}),console.info("[copy-editor] 문구 편집 모드 활성화됨"))}export{xe as initCopyEditor};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.