1/* 2 PDF 미리보기 ì´ëí° 3 4 ì´ íì¼ì´ PDF ë·°ì´ êµ¬íì ê°ì¶ë ì ì¼í ê²½ê³ë¤. 5 í¸ì¶ë¶(íì¼ ì»´í¬ëí¸Â·ê³ì½ì 보기·ê³ì½ íë¡ì¸ì¤)ë ìë í¨ì íëë§ ìë©´ ëë¤. 6 7 buildPdfPreviewHtml(fileUrl, height) -> PDF 미리보기 HTML 문ìì´ 8 9 구íì ê°ìë¼ì¸ ëë ì´ íì¼ë§ êµì²´íë¤. ê°ì ìê·¸ëì²ë¡ HTML 문ìì´ë§ ëë ¤ì£¼ë©´ 10 í¸ì¶ë¶ë ìëì§ ììë ëë¤. (ë¸ë¼ì°ì 기본 iframe ì¼ë¡ ëëë¦¬ë ¤ë©´ USE_PDF_VIEWER = false) 11 12 íì¬ êµ¬í: EmbedPDF 2.15.0 â PDFium(WASM) ìì§, íì´ì§ì ì§ì ë ë 13 - ìì° ìì¹: /assets/lib/embedpdf (ì본 무ìì vendoring, ë²ì ì VERSION.txt 참조) 14 - ìí¸ë¦¬ê° ES 모ëì´ë¼ ëì import ë¡ ë¶ë¬ì¨ë¤. ìµì´ PDF 미리보기 ìì ìë§ ë´ë ¤ë°ëë¤. 15 - ìì ì°ë pdfjs-viewer-element(PDF.js)ë iOS ìì 못 ì¼ë¤. Safari 18 미ë§ì ë¡ë ì¤í¨, 16 ìµì iOS ë 첫 ì ì¤ì² ë¤ ì¡°ìì´ ë©ìë¤. EmbedPDF ë 구í iOS í¬í¨ ì ì ëìì íì¸íë¤. 17 - wasmUrl ì origin ì ë¶ì¸ ì ë URL ë¡ ì¤ë¤. 루í¸ìëê²½ë¡ë ì커ìì 못 íê³ , ìì 빼면 ì¸ë¶ CDN ì ì´ë¤. 18 19 usage: element.innerHTML = buildPdfPreviewHtml(fileUrl, "534px"); 20*/ 21const PDF_VIEWER_BASE = "/assets/lib/embedpdf"; 22const USE_PDF_VIEWER = true; // â 롤백 ì¤ìì¹ (false ë©´ ë¸ë¼ì°ì 기본 iframe) 23 24// 미리보기ì ë¶íìí 기ë¥ì UIÂ·ëª ë ¹ ì쪽ìì ì°¨ë¨íë¤. 25// ì ì¥ ê²½ë¡ë íë©´ì 기존 ë¤ì´ë¡ë ë²í¼ì¼ë¡ ì¼ìííë¯ë¡ ë´ë³´ë´ê¸°Â·ì¸ìë ëºë¤. 26const PDF_VIEWER_DISABLED = [ 27 "annotation", "redaction", "form", "signature", "stamp", "insert", "capture", "history", 28 "document-export", "document-print", "document-open", "document-close", 29 "mode-annotate", "mode-shapes", "mode-redact", "mode-insert", "mode-form", 30 "panel-comment", "panel-annotation-style", "panel-redaction", 31 "document-menu" // ë³´ì·í면캡ì²Â·ì ì²´íë©´ì´ ë ë©ë´ (ì ì²´íë©´ì 쪽 ì¤ì ì ë¨ëë¤) 32]; 33 34// ë·°ì´ë shadow DOM ì´ë¼ ë°ê¹¥ CSS ê° ë¿ì§ ìëë¤. ì´ê¸°í í ì¤íì¼ì ì§ì ë£ëë¤. 35// 문ì ë©ë´ë¥¼ ê»ëë°ë ê·¸ ë²í¼ ì리(í 0)ì ë¤ë°ë¥´ë 구ë¶ì ì´ ë¨ì í´ë° 맨 ìì ì¸ë¡ì ì´ ì긴ë¤. 36const PDF_VIEWER_SHADOW_CSS = [ 37 // 문ì ë©ë´ë¥¼ ê»ëë°ë ê·¸ ë²í¼ ì리(í 0)ì ë¤ë°ë¥´ë 구ë¶ì ì´ ë¨ì í´ë° 맨 ìì ì¸ë¡ì ì´ ìê¸´ë¤ 38 ".flex.items-center.gap-2.justify-start > div:nth-child(-n+2) { display: none; }", 39 // ì¢ì íìì ì íë ë°°ì¨ ë²í¼ ìì´ì½ì ë보기+ ë¡ ë°ê¾¼ë¤. 40 // ë´ì¥ ìì´ì½ì registerIcons ë¡ ë®ì´ì¸ ì ìì´ ë§ì¤í¬ë¡ ê·¸ë¦°ë¤ (ìì currentColor 를 ë°ë¥¸ë¤) 41 // ë¹ ìí(문ì ë«ì ë¤) íë©´ì "문ì ì´ê¸°" ë²í¼Â·ì§ìíì 문구 ì¨ê¹ â 미리보기ìì ìì íì¼ ì´ê¸° ì°¨ë¨. 42 // ì¹´í ê³ ë¦¬ ìì±ì´ ìë ë²í¼ì´ë¼ disabledCategories ë¡ë ì ë§íë¤. ë¹ ìí ì¹´ëë§ h2 ì 목ì ì´ë¤ 43 ".max-w-sm:has(> h2) > button, .max-w-sm:has(> h2) > p.text-fg-muted { display: none !important; }", 44 '[data-epdf-i="zoom-menu-button"] button svg { display: none; }', 45 '[data-epdf-i="zoom-menu-button"] button::before {' + 46 'content: ""; width: 20px; height: 20px; background-color: currentColor;' + 47 '-webkit-mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27black%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Ccircle%20cx%3D%2711%27%20cy%3D%2711%27%20r%3D%277%27%2F%3E%3Cpath%20d%3D%27M20.5%2020.5l-4.7-4.7%27%2F%3E%3Cpath%20d%3D%27M11%208v6%27%2F%3E%3Cpath%20d%3D%27M8%2011h6%27%2F%3E%3C%2Fsvg%3E") center / contain no-repeat;' + 48 'mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27black%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Ccircle%20cx%3D%2711%27%20cy%3D%2711%27%20r%3D%277%27%2F%3E%3Cpath%20d%3D%27M20.5%2020.5l-4.7-4.7%27%2F%3E%3Cpath%20d%3D%27M11%208v6%27%2F%3E%3Cpath%20d%3D%27M8%2011h6%27%2F%3E%3C%2Fsvg%3E") center / contain no-repeat; }' 49].join(" "); 50 51// ë°°ì¨ ìì´ì½ êµì²´ (기본ì ì ìì +/-). ì¢ì íìì ì íë ë°°ì¨ ë©ë´ë zoomIn ìì´ì½ì ì´ë¤. 52// path ë°ì´í°ë§ ë°ë êµ¬ì¡°ë¼ SVG ë§í¬ì ì ë£ì ì ìë¤. 53const PDF_VIEWER_ICONS = { 54 zoomIn: { 55 strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", 56 paths: [ 57 { d: "M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z", stroke: "currentColor" }, 58 { d: "M21 21l-4.35-4.35", stroke: "currentColor" }, 59 { d: "M11 8v6", stroke: "currentColor" }, 60 { d: "M8 11h6", stroke: "currentColor" } 61 ] 62 }, 63 zoomOut: { 64 strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", 65 paths: [ 66 { d: "M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z", stroke: "currentColor" }, 67 { d: "M21 21l-4.35-4.35", stroke: "currentColor" }, 68 { d: "M8 11h6", stroke: "currentColor" } 69 ] 70 } 71}; 72 73let pdfViewerLoading = null; 74let pdfViewerLocale = null; 75let pdfViewerObserved = false;
76 77/** 78 * PDF ë·°ì´ ëª¨ë + íêµì´ ë¡ì¼ì¼ ì§ì° ë¡ë (ì¤ë³µ í¸ì¶í´ë 1íë§ ë¡ë) 79 */ 80function ensurePdfViewer() { 81 if (pdfViewerLoading == null) { 82 pdfViewerLoading = Promise.all([ 83 import(`${PDF_VIEWER_BASE}/embedpdf.js`), 84 import("/assets/js/components/pdf-viewer-locale-ko.js") 85 ]).then(([module, locale]) => { 86 pdfViewerLocale = { code: "ko", name: "íêµì´", translations: locale.default }; 87 // ë°°ì¨ ìì´ì½ êµì²´ë init ì¤ì ì´ ìëë¼ registry ë±ë¡ì¼ë¡ ì ì©ëë¤ 88 if (typeof module.registerIcons === "function") { 89 module.registerIcons(PDF_VIEWER_ICONS); 90 } 91 observePdfViewerHosts(); 92 return module; 93 }).catch((error) => { 94 // ë·°ì´ë¥¼ 못 ì°ë íê²½ì´ë©´ ë¸ë¼ì°ì 기본 ë¯¸ë¦¬ë³´ê¸°ë¡ ëëë¦°ë¤ 95 console.warn("PDF ë·°ì´ ë¡ë ì¤í¨, 기본 ë¯¸ë¦¬ë³´ê¸°ë¡ ëì²´", error); 96 document.querySelectorAll(".pdf-preview-host").forEach(fallbackPdfPreview); 97 throw error; 98 }); 99 } 100 return pdfViewerLoading; 101} 102 103/** 104 * 미리보기 ì리ë í¸ì¶ë¶ë§ë¤ ì½ì ë°©ìì´ ë¬ë¼(문ìì´ innerHTML) í ê³³ìì ê´ì°°í´ ì´ê¸°ííë¤. 105 * ê´ì°° ëìì´ .pdf-preview-host íëë¿ì´ë¼ 기존 ìì·íì´ë°ìë ìí¥ì´ ìë¤. 106 */ 107function observePdfViewerHosts() { 108 if (pdfViewerObserved) { 109 return; 110 } 111 pdfViewerObserved = true; 112 113 initPdfViewerHosts(document); 114 115 new MutationObserver((mutations) => { 116 mutations.forEach((mutation) => { 117 mutation.addedNodes.forEach((node) => { 118 if (node.nodeType === Node.ELEMENT_NODE) { 119 initPdfViewerHosts(node); 120 } 121 }); 122 }); 123 }).observe(document.documentElement, { childList: true, subtree: true }); 124} 125 126function initPdfViewerHosts(root) { 127 let hosts = []; 128 129 if (root.matches != null && root.matches(".pdf-preview-host")) { 130 hosts.push(root); 131 } 132 if (root.querySelectorAll != null) { 133 hosts = hosts.concat(Array.from(root.querySelectorAll(".pdf-preview-host"))); 134 } 135 136 hosts.forEach((host) => { 137 if (host.dataset.ready != null) { 138 return; 139 } 140 host.dataset.ready = "1"; 141 142 ensurePdfViewer().then((module) => { 143 module.default.init({ 144 type: "container", 145 target: host, 146 src: host.dataset.pdfSrc, 147 worker: true, 148 // wasm ê²½ë¡ë origin ì ë¶ì¸ë¤. ì커 ìììë 루í¸ìëê²½ë¡ê° íë¦¬ì§ ìì 149 // ì§ì ì 빼면 ì¸ë¶ CDN(jsDelivr)ìì ë°ìì¨ë¤ 150 wasmUrl: `${location.origin}${PDF_VIEWER_BASE}/pdfium.wasm`, 151 // íì´ì§ ë°°ê²½ì´ í°ìì´ë¼ ë·°ì´ë ë¤í¬ë¡ ëë¹ë¥¼ ì¤ë¤ (ë¼ì´í¸ë¡ ëëë¦¬ë ¤ë©´ preference: "light") 152 theme: { preference: "dark" }, 153 icons: PDF_VIEWER_ICONS, 154 tabBar: "never", 155 disabledCategories: PDF_VIEWER_DISABLED, 156 fonts: { ui: null, signature: null }, // ì¸ë¶(구ê¸) í°í¸ ìì² ì°¨ë¨ 157 fontFallback: null, // ëì²´í°í¸ 미ì¬ì© (í본 íì¸ í ê²°ì ) 158 stamp: { manifests: [] }, // 기본 ì¤í¬í ë¼ì´ë¸ë¬ë¦¬(ì¸ë¶ CDN) 미ì¬ì© 159 i18n: { defaultLocale: "ko", locales: [pdfViewerLocale] }, 160 pan: { defaultMode: "mobile" }, 161 zoom: { defaultZoomLevel: "fit-width" } // ì´ìë§ì íì ë§ì¶ë¤ 162 }); 163 injectPdfViewerShadowStyles(host); 164 }).catch(() => { /* ensurePdfViewer ìì ì´ë¯¸ ëì²´ ì²ë¦¬ */ }); 165 }); 166} 167 168/** shadow DOM ìì ì¤íì¼ì ë£ëë¤ (컨í ì´ëê° ë¶ì ëê¹ì§ ì ê¹ ê¸°ë¤ë¦°ë¤) */ 169function injectPdfViewerShadowStyles(host, tries = 0) { 170 const container = host.querySelector("embedpdf-container"); 171 172 if (container == null || container.shadowRoot == null) { 173 if (tries < 40) { 174 requestAnimationFrame(() => injectPdfViewerShadowStyles(host, tries + 1)); 175 } 176 return; 177 } 178 if (container.shadowRoot.querySelector("style[data-app]") != null) { 179 return; 180 } 181 182 const style = document.createElement("style"); 183 style.dataset.app = "1"; 184 style.textContent = PDF_VIEWER_SHADOW_CSS; 185 container.shadowRoot.appendChild(style); 186} 187 188/** ë·°ì´ë¥¼ 못 ì¸ ë ë¸ë¼ì°ì 기본 ë¯¸ë¦¬ë³´ê¸°ë¡ ëëë¦°ë¤ */ 189function fallbackPdfPreview(host) { 190 const height = host.style.height || "534px"; 191 host.innerHTML = `<iframe src="${host.dataset.pdfSrc}" width="100%" height="${height}"></iframe>`; 192} 193 194/**
195 * PDF 미리보기 HTML 문ìì´ ë°í (í¸ì¶ë¶ë ë기 ì ì§) 196 * 197 * @param {string} fileUrl ìë² íì¼ URL ëë blob URL 198 * @param {string} height ë·°ì´ ëì´ (기본 534px) 199 */ 200function buildPdfPreviewHtml(fileUrl, height = "534px") { 201 if (!USE_PDF_VIEWER) { 202 return `<iframe src="${fileUrl}" width="100%" height="${height}"></iframe>`; 203 } 204 205 ensurePdfViewer(); // ì§ì° ë¡ë í¸ë¦¬ê±° (ì½ì í ìµì ë²ê° ì´ê¸°ííë¯ë¡ ë기 ë¶íì) 206 207 return `<div class="pdf-preview-host" data-pdf-src="${fileUrl}" 208 style="display: block; width: 100%; height: ${height};"></div>`; 209}
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.