import React, {useEffect, useRef, useState} from "react"; import { getDocument, GlobalWorkerOptions, PageViewport, PDFDocumentProxy, PDFPageProxy, renderTextLayer } from "pdfjs-dist"; import "./PDFViewer.css" import {Button, Dropdown, Input, MenuProps, Modal, Progress, Space, Tooltip} from "antd"; import { CaretLeftOutlined, CaretRightOutlined, DownloadOutlined, FullscreenOutlined, PercentageOutlined, PrinterOutlined, ZoomInOutlined, ZoomOutOutlined } from "@ant-design/icons"; import html2canvas from "html2canvas"; import {PDFDocument, PDFPage} from "pdf-lib"; const currentUrl = getCurrentUrl(); console.log(currentUrl); GlobalWorkerOptions.workerSrc = `${currentUrl}/pdf.worker.js`; function getCurrentUrl(): string { // window.location 객체를 통해 현재 URL 정보에 접근 const { protocol, hostname, port } = window.location; // 포트가 80이 아닌 경우에만 포트를 포함한 URL 반환 const portSuffix = (port === '80' || port === '443') ? '' : `:${port}`; // 조합된 URL 반환 return `${protocol}//${hostname}${portSuffix}/pdfwiz/pdfjs`; } interface PdfViewerProps { fileUrl: string; fileName?:string; downloadFileUrl?:string; scale?:number; renderText?:boolean; waterMarkText?:string; } async function loadPage(pdf:PDFDocumentProxy, pageNum:number, scale:number, rotate:number, container:HTMLDivElement, isRenderText:boolean, waterMarkText:string|undefined) { const pageLayer = document.createElement("div"); const canvas = document.createElement("canvas"); const cavasLayer = document.createElement("div"); const loadingLayer = document.createElement("div"); const drawLayer = document.createElement("div"); const loadingImage = document.createElement("img"); loadingImage.setAttribute("src", "pdfwiz/image/loading.svg"); const textLayer = isRenderText ? document.createElement("div") : null; if (textLayer) { pageLayer.appendChild(textLayer); textLayer.className = "textLayer"; } pageLayer.className = "pageLayer"; cavasLayer.className = "loading_canvasWrapper"; pageLayer.setAttribute("data-pageIdx", `${pageNum}`); pageLayer.setAttribute("id", `page-${pageNum}`); pageLayer.setAttribute("data-pageLoad", `false`); drawLayer.className = "drawLayer"; drawLayer.setAttribute("data-pageIdx", `${pageNum}`); drawLayer.setAttribute("id", `draw-${pageNum}`); if (waterMarkText) { const waterMarkLayer = document.createElement("div"); waterMarkLayer.className = "waterMarkLayer"; drawLayer.appendChild(waterMarkLayer); } pageLayer.appendChild(drawLayer); container.appendChild(pageLayer); cavasLayer.appendChild(canvas); pageLayer.appendChild(cavasLayer); loadingLayer.appendChild(loadingImage); loadingLayer.className = "loading"; cavasLayer.appendChild(loadingLayer); const page = await pdf.getPage(pageNum); const viewport = page.getViewport({ scale: scale ,rotation:rotate|0 }) canvas.width = viewport.width; canvas.height = viewport.height; const context = canvas.getContext("2d"); if (!context) return; if(pageNum <= 3) { const isLoad = pageLayer.getAttribute("data-pageLoad") === "true" ? true : false; if(!isLoad) { pageLayer.setAttribute("data-pageLoad", `true`); await renderPage(page, context, viewport, textLayer).finally(() => { cavasLayer.className = "canvasWrapper"; if (loadingImage && cavasLayer.contains(loadingLayer)) { cavasLayer.removeChild(loadingLayer); } }); } } } async function renderPage(page:PDFPageProxy, context:CanvasRenderingContext2D, viewport:PageViewport, textLayer:HTMLDivElement|null) { const renderContext = { canvasContext: context, viewport: viewport, }; await page.render(renderContext).promise; if (textLayer) { page.getTextContent().then(textContent => { const txtLayer = renderTextLayer( { textContentSource: textContent, container: textLayer, viewport: viewport, isOffscreenCanvasSupported:true, } ).promise; }).finally(()=>{ const parent = textLayer.parentElement; if (parent != null) { const drawLayer = parent.querySelector(".drawLayer"); const pageStyle = textLayer.getAttribute("style"); if (drawLayer && pageStyle) { drawLayer.setAttribute("style", pageStyle); } } }); } } export const PDFViewer = (props: PdfViewerProps) => { const { fileUrl , scale, renderText, downloadFileUrl , waterMarkText, fileName} = props; const [modalVisible, setModalVisible] = useState(false); //const downloadUrl = downloadFileUrl || fileUrl; //const fileTitle= fileName||fileUrl; const [threshold, setThreshold] = useState(0.7); const [scaleValue, setScaleValue] = useState(scale || 1.5); const [currentPage, setCurrentPage] = useState(1); const [totalPage, setTotalPage] = useState(0); const rightClickCountRef = useRef(0); const leftClickCountRef = useRef(0); const [observer, setObserver] = useState(); const [calcScaleValue, setCalcScaleValue] = useState(scaleValue); const timerRef = useRef(null); const timerClickRef = useRef(null); const [pdfDocument, setPdfDocument] = useState(); const [modalTitle, setModalTitle] = useState(); const [progressVisible, setProgressVisible] = useState(false); const [procPercent, setProcPercent] = useState(0); const [passwordVisible, setPasswordVisible] = useState(false); const [pdfPassword, setPdfPassword] = useState(""); const [pdfErrorMsg, setPdfErrorMsg] = useState(""); const waterMarkCanvasRef = useRef(null); const isRenderText = renderText||false; const containerRef = useRef(null); const items:MenuProps['items'] = [ { key: 'ACTUAL_SIZE', label: '실제크기', }, { key: 'WIDTH_FIT', label: '가로에 맞추기', }, { key: 'HEIGHT_FIT', label: '세로에 맞추기', }, { key: '0.5', label: '50%', }, { key: '0.75', label: '70%', }, { key: '1.0', label: '100%', }, { key: '1.25', label: '125%', }, { key: '1.5', label: '150%', }, { key: '2.0', label: '200%', }, { key: '3.0', label: '300%', }, { key: '4.0', label: '400%', }, ]; const handleRatioMenuClick: MenuProps['onClick'] = (e) => { const key:string = e.key; switch (key) { case 'ACTUAL_SIZE' : ZoomCtrl(1.0); break; case 'WIDTH_FIT' : ZoomCtrl(getWidthFitScale()); break; case 'HEIGHT_FIT' : ZoomCtrl(getHeightFitScale()); break; case '0.5' : ZoomCtrl(0.5); break; case '0.75' : ZoomCtrl(0.75); break; case '1.0' : ZoomCtrl(1.0); break; case '1.25' : ZoomCtrl(1.25); break; case '1.5' : ZoomCtrl(1.5); break; case '2.0' : ZoomCtrl(2.0); break; case '3.0' : ZoomCtrl(3.0); break; case '4.0' : ZoomCtrl(4.0); break; default: break; } }; function calcThreshold(stdScaleValue:number) :number { if (0.5 >= stdScaleValue ) { return 1.0; } else if (0.75 >= stdScaleValue && 1.0 < stdScaleValue) { return 0.9; } else if (1.0 >= stdScaleValue && 1.25 < stdScaleValue) { return 0.8; } else if (1.25 >= stdScaleValue && 1.5 < stdScaleValue) { return 0.7; } else if (1.5 >= stdScaleValue && 1.75 < stdScaleValue) { return 0.6; } else if (1.75 >= stdScaleValue && 2.0 < stdScaleValue) { return 0.5; } else if (2.0 >= stdScaleValue && 3.0 < stdScaleValue) { return 0.4; } else if (4.0 >= stdScaleValue ) { return 0.3; } else { return 0.65 } } const raioMenuProps = { items, onClick: handleRatioMenuClick, }; function enterFullScreen(elementId: string): void { const element = document.getElementById(elementId) as HTMLElement & { requestFullscreen?: () => Promise; webkitRequestFullscreen?: () => Promise; mozRequestFullScreen?: () => Promise; msRequestFullscreen?: () => Promise; }; if (!element) { console.error(`Element with id ${elementId} not found.`); return; } const onFullScreenEntered = () => { // 여기에 원하는 로직을 추가하세요. console.log("전체 화면 모드에 진입하였습니다."); ZoomCtrl(getWindowHeightFitScale()); // 이벤트 리스너 제거 document.removeEventListener('fullscreenchange', onFullScreenEntered); }; // 이벤트 리스너 추가 document.addEventListener('fullscreenchange', onFullScreenEntered); if (element.requestFullscreen) { element.requestFullscreen(); } else if (element.webkitRequestFullscreen) { element.webkitRequestFullscreen(); } else if (element.mozRequestFullScreen) { element.mozRequestFullScreen(); } else if (element.msRequestFullscreen) { element.msRequestFullscreen(); } else { console.error("Fullscreen API is not supported."); } } function scrollToDiv(pageNum:number) { const divId = `page-${pageNum}`; const element = document.getElementById(divId); if (element) { element.scrollIntoView({ behavior: 'smooth' }); } else { console.error(`Element with id ${divId} not found.`); } } function movePage(pageNum:number) { if(pageNum < 1) { pageNum = 1; } else if(pageNum > totalPage) { pageNum = totalPage; } scrollToDiv(pageNum); return pageNum; } function getWidthFitScale() { const parentObj = document.getElementById("pdf_content"); if(parentObj) { const parentWidth = parentObj.offsetWidth; console.log("부모 : " + parentWidth); const pageLayer = parentObj.querySelector(`.pageLayer[data-pageidx="${currentPage}"]`); if (pageLayer) { const canvas = pageLayer.querySelector('canvas'); if (canvas) { console.log("자식 : " + (canvas.width / calcScaleValue) ); return parentWidth / ((canvas.width / calcScaleValue) + 15) ; } } } return 0; } function getHeightFitScale() { const parentObj = document.getElementById("pdf_content"); if(parentObj) { const parentHeight = parentObj.offsetHeight; const pageLayer = parentObj.querySelector(`.pageLayer[data-pageidx="${currentPage}"]`); if (pageLayer) { const canvas = pageLayer.querySelector('canvas'); if (canvas) { return parentHeight / ((canvas.height / calcScaleValue) + 30); } } } return 0; } function getWindowHeightFitScale() { const parentObj = document.getElementById("pdf_content"); if(parentObj) { const pageLayer = parentObj.querySelector(`.pageLayer[data-pageidx="${currentPage}"]`); if (pageLayer) { const canvas = pageLayer.querySelector('canvas'); if (canvas) { return window.screen.height / ((canvas.height / calcScaleValue) + 15); } } } return 0; } function zoomIn(stdValue:number) { const maxZoom:number = 4.0; let calsZoom:number = stdValue + 0.25; if (calsZoom > maxZoom) { calsZoom = maxZoom; } return calsZoom; } function zoomOut(stdValue:number) { const minZoom:number = 0.5; let calsZoom:number = stdValue - 0.25; if (calsZoom < minZoom) { calsZoom = minZoom; } return calsZoom; } function drawLoadingPage(pageLayer:Element, isLoadingIcon:boolean) { pageLayer.setAttribute("data-pageLoad", "false"); const cavasLayer = pageLayer.querySelector('.canvasWrapper') as HTMLDivElement; if (cavasLayer) { cavasLayer.className = "loading_canvasWrapper"; if (isLoadingIcon) { const loadingLayer = document.createElement("div"); const loadingImage = document.createElement("img"); loadingImage.setAttribute("src", "pdf-loading.svg"); loadingLayer.appendChild(loadingImage); loadingLayer.className = "loading"; cavasLayer.appendChild(loadingLayer); } } } function drawLayerShowNHide(isShow:boolean) { if (containerRef.current) { const drawLayers = containerRef.current.querySelectorAll(".drawLayer"); drawLayers.forEach((layer) => { if (!isShow) { (layer as HTMLElement).style.display = 'none'; } else { (layer as HTMLElement).style.display = 'block'; } }); } } const ZoomCtrl = (calcZoom:number) => { if(calcZoom === calcScaleValue) { return; } if (observer) { observer.disconnect(); } console.log("cal : " + calcZoom); console.log("calsc : " + calcScaleValue); console.log("cur : " + currentPage); const targetPageIdx = currentPage; const startIdx = Math.max(1, targetPageIdx - 3); const endIdx = targetPageIdx + 3; if (containerRef.current) { drawLayerShowNHide(false); setCalcScaleValue(calcZoom); const pdfViewer = containerRef.current; for (let i = 1; i <= totalPage; i++) { const pageLayer = pdfViewer.querySelector(`.pageLayer[data-pageidx="${i}"]`); if (pageLayer) { const canvas = pageLayer.querySelector('canvas'); if (canvas) { if ( i >= startIdx && i <= endIdx) { const tempCanvas = document.createElement('canvas'); const tempCtx = tempCanvas.getContext('2d'); if (!tempCtx) { console.error("Failed to get 2d context from tempCanvas."); return; } tempCanvas.width = canvas.width; tempCanvas.height = canvas.height; tempCtx.drawImage(canvas, 0, 0); canvas.width = (canvas.width / calcScaleValue) * calcZoom; canvas.height = (canvas.height / calcScaleValue) * calcZoom; const ctx = canvas.getContext('2d')!; ctx.drawImage(tempCanvas, 0, 0, tempCanvas.width, tempCanvas.height, 0, 0, canvas.width, canvas.height); } else { drawLoadingPage(pageLayer, true); canvas.width = (canvas.width / calcScaleValue) * calcZoom; canvas.height = (canvas.height / calcScaleValue) * calcZoom; } console.log("can : " + canvas.width); } } } } if (timerRef.current) { clearTimeout(timerRef.current); } timerRef.current = setTimeout(() => { if (containerRef.current) { setCalcScaleValue(calcZoom); const pdfViewer = containerRef.current; for (let i = startIdx; i <= endIdx; i++) { const pageLayer = pdfViewer.querySelector(`.pageLayer[data-pageidx="${i}"]`); if (pageLayer) { drawLoadingPage(pageLayer, false); } } } const container = containerRef.current; container!.style.setProperty('--scale-factor', `${calcZoom}`); // 원하는 값으로 설정 setScaleValue(calcZoom); setThreshold(calcThreshold(calcZoom)); console.log("타임 : " + calcZoom); }, 500); } const observerOptions = { root: null, rootMargin: '0px 0px 30px 0px', threshold: threshold, } function createObserver(pdfDocument:PDFDocumentProxy) { if (observer) { observer.disconnect(); } const newObserver = new IntersectionObserver((entries, observer) => { entries.forEach(async entry => { // 관찰 대상이 viewport 안에 들어온 경우 image 로드 if (entry.isIntersecting) { const pageLayer = entry.target; const pageIdx: number = Number(pageLayer.getAttribute("data-pageidx")); setCurrentPage(pageIdx); await drawPdfPage(pdfDocument, pageLayer); } }) }, observerOptions); setObserver(newObserver); const pdfPages = document.querySelectorAll('.pageLayer'); pdfPages.forEach((pdfPage) => { newObserver.observe(pdfPage); }) return () => { newObserver.disconnect(); }; } async function drawPdfPage(pdfDocument:PDFDocumentProxy, pageLayer: Element) { console.log(scaleValue); if (pdfDocument) { const pageIdx: number = Number(pageLayer.getAttribute("data-pageidx")); const cavasLayer = pageLayer.querySelector('.loading_canvasWrapper') as HTMLDivElement; const isLoad = pageLayer.getAttribute("data-pageLoad") === "true" ? true : false; if (cavasLayer && !isLoad) { pageLayer.setAttribute("data-pageLoad", `true`); const page = await pdfDocument.getPage(pageIdx); const textLayer = pageLayer.querySelector('.textLayer') as HTMLDivElement; const canvas = pageLayer.querySelector('canvas'); const loadingLayer = pageLayer.querySelector('.loading'); const context = canvas!.getContext("2d"); if (!context) return; const viewport = page.getViewport({ scale: scaleValue , rotation: 0 }) await renderPage(page, context, viewport, textLayer).finally(() => { cavasLayer.className = "canvasWrapper"; if (loadingLayer && cavasLayer.contains(loadingLayer)) { cavasLayer.removeChild(loadingLayer); } }); } } } async function runZoom() { if (containerRef.current && pdfDocument) { const pdfViewer = containerRef.current; const targetPageIdx = currentPage; const startIdx = Math.max(1, targetPageIdx - 3); const endIdx = targetPageIdx + 3; for (let i = startIdx; i <= endIdx; i++) { const pageLayer = pdfViewer.querySelector(`.pageLayer[data-pageidx="${i}"]`); if (pageLayer) { await drawPdfPage(pdfDocument, pageLayer); } } createObserver(pdfDocument); } } async function loadPdfFromUrl() { const response = await fetch(fileUrl); if (!response.ok) { throw new Error(`Failed to fetch PDF from ${fileUrl}. Status: ${response.statusText}`); } const pdfBytes = new Uint8Array(await response.arrayBuffer()); return pdfBytes; } async function getPdfBlob() { const warterMark = document.querySelector("#draw-1"); if (warterMark != null && pdfDocument) { const waterMarkCanvas = await html2canvas(warterMark as HTMLElement, { backgroundColor: null, }); //패스워드가 있으면 이미지를 만들고 없으면 기존거에 워터마크만 추가한다. const isSecurity = pdfPassword.length > 0; let pdfDoc:PDFDocument = isSecurity ? await PDFDocument.create() : await PDFDocument.load(await pdfDocument.getData()); const waterMarkData = waterMarkCanvas.toDataURL("image/png"); const waterMarkImage = pdfDoc.embedPng(waterMarkData); for (let i=1; i<=totalPage; i++) { setProcPercent(calculateProgress(i, totalPage, 100)); let pdfPage:PDFPage|null = null; if (isSecurity) { const page = await pdfDocument.getPage(i); const viewport = page.getViewport({ scale: 2.0 }); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.height = viewport.height; canvas.width = viewport.width; if (context && viewport) { const renderContext = { canvasContext: context, viewport: viewport }; await page.render(renderContext).promise; pdfPage = pdfDoc.addPage([viewport.width, viewport.height]); const pdfPageIamge = pdfDoc.embedJpg(canvas.toDataURL("image/jpeg")); pdfPage.drawImage(await pdfPageIamge, { x: 0, y: 0, width: viewport.width, height: viewport.height }); } } else { pdfPage = pdfDoc.getPages()[i-1]; } //워터마크 삽입 if (pdfPage && waterMarkImage) { pdfPage.drawImage(await waterMarkImage, { x: 0, y: 0, width: pdfPage.getWidth(), height: pdfPage.getHeight(), }); } } const modifiedPdfBytes = await pdfDoc.save(); setProcPercent(100); const blob = new Blob([modifiedPdfBytes], {type: 'application/pdf'}); return blob; } return null; } const downloadPdfDocument = async () => { const blob = await getPdfBlob(); if (blob) { const pdfUrl = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = pdfUrl; link.download = 'overlayed.pdf'; link.click(); URL.revokeObjectURL(pdfUrl); hideModal(); } }; const printPdfDocument = async () => { const blob = await getPdfBlob(); if (blob) { const pdfUrl = URL.createObjectURL(blob); const printIframe = document.getElementById('printFrame') as HTMLIFrameElement; printIframe.src = pdfUrl; const printWindow = printIframe.contentWindow; if (printWindow) { printIframe.onload = function () { setTimeout(function () { printWindow.print(); hideModal(); URL.revokeObjectURL(pdfUrl); }, 500); }; } } }; function calculateProgress(currentPage:number, totalPage:number, totalPercent:number) { const progress = (currentPage / totalPage) * totalPercent; console.log(progress); return Math.min(Math.round(progress), totalPercent); // 진행률이 100%를 초과하지 않도록 합니다. } function showModal(revModalTitle:string, isProgress:boolean) { if (isProgress) { setProgressVisible(true); } setProcPercent(0); setModalTitle(revModalTitle); setModalVisible(true); } function hideModal() { setPdfErrorMsg(""); setModalVisible(false); setModalTitle(""); setProcPercent(0); setProgressVisible(false); setPasswordVisible(false); } function PrevPage() { const movePageNum = currentPage-1; setCurrentPage(movePage(movePageNum < 1 ? 1: movePageNum )); } function NextPage() { const movePageNum = currentPage+1; setCurrentPage(movePage(movePageNum > totalPage ? totalPage : movePageNum )); } const handleDoubleClick = (event:React.MouseEvent) => { event.preventDefault(); leftClickCountRef.current += 1; if (leftClickCountRef.current === 1) { timerClickRef.current = setTimeout(() => { leftClickCountRef.current = 0; // 클릭 횟수 초기화 }, 250); } else if (leftClickCountRef.current === 2) { if (timerClickRef.current) { clearTimeout(timerClickRef.current); } ZoomCtrl( zoomIn( calcScaleValue || scaleValue ) ); leftClickCountRef.current = 0; // 클릭 횟수 초기화 } }; const handleRightClick = (event:React.MouseEvent) => { event.preventDefault(); rightClickCountRef.current += 1; if (rightClickCountRef.current === 1) { timerClickRef.current = setTimeout(() => { rightClickCountRef.current = 0; // 클릭 횟수 초기화 }, 250); } else if (rightClickCountRef.current === 2) { if (timerClickRef.current) { clearTimeout(timerClickRef.current); } ZoomCtrl( zoomOut( calcScaleValue || scaleValue ) ); rightClickCountRef.current = 0; // 클릭 횟수 초기화 } }; const handlePasswordSubmit = () => { loadPdf(pdfPassword).then(r => { }); }; useEffect(() => { if (waterMarkText && waterMarkCanvasRef.current) { const canvas = waterMarkCanvasRef.current; const ctx = canvas.getContext('2d'); const fontSize = 70 * scaleValue; const padding = fontSize; if (ctx) { ctx.font = `${fontSize}px Arial`; const textWidth = ctx.measureText(waterMarkText).width; const textHeight = fontSize; // 이 값은 폰트 크기에 따라 조절하시길 바랍니다. canvas.width = textWidth + padding; canvas.height = textHeight + padding * 2; ctx.font = `${fontSize}px Arial`; // 캔버스 크기가 변경되면 폰트 설정도 초기화되므로 다시 설정해야 합니다. ctx.fillStyle = 'rgba(0, 0, 0, 0.10)'; ctx.fillText(waterMarkText, 0, textHeight - 5); // y 좌표를 조절하여 텍스트가 캔버스 내에 잘 들어가도록 합니다. //setWaterMarkImgURl(canvas.toDataURL('image/png')); const waterMarkImgURl = canvas.toDataURL('image/png'); if(waterMarkImgURl) { const style = document.createElement('style'); style.innerHTML = ` .pdfViewer .waterMarkLayer::before { content: ""; position: absolute; top: 0px; left: 0px; width: 200%; height: 200%; background-image: url(${waterMarkImgURl}); background-repeat: repeat; transform: rotate(-45deg); background-color: transparent; }`; document.head.appendChild(style); } } } }, [waterMarkText, scaleValue]); const loadPdf = async (password: string | null | undefined) => { console.log("PDFViewer component rendered"); const container = containerRef.current; if (!container) return; // Clear the container's contents whenever fileUrl changes container.innerHTML = ""; const loadOption: any = { url: fileUrl, cMapUrl: `${currentUrl}/cmaps/`, cMapPacked: true, enableXfa: true, }; if (password) { loadOption.password = password; } let isError = false; try { const pdfDocumentProxy: PDFDocumentProxy = await getDocument(loadOption).promise; setPdfDocument(pdfDocumentProxy); setTotalPage(pdfDocumentProxy.numPages); container.style.setProperty('--scale-factor', `${scaleValue}`); // 원하는 값으로 설정 for (let pageNum = 1; pageNum <= pdfDocumentProxy.numPages; pageNum++) { await loadPage(pdfDocumentProxy, pageNum, scaleValue, 0, container, isRenderText, waterMarkText); } createObserver(pdfDocumentProxy); } catch (error:any) { isError = true if (error.name === 'PasswordException' && !password) { showModal("보안 PDF", false); setPasswordVisible(true); } else if (error.name === 'PasswordException' && error.message === 'Incorrect Password' && password) { setPdfErrorMsg("PDF 암호가 잘못되었습니다."); showModal("보안 PDF", false); setPasswordVisible(true); } else { console.error(error); } } finally { if (!isError && password) hideModal(); } }; useEffect(() => { setTotalPage(0); setCurrentPage(1); setPdfErrorMsg(""); setPdfPassword(""); loadPdf(null); }, [fileUrl]); useEffect(() => { runZoom().then().finally( () =>{ drawLayerShowNHide(true); }); }, [ scaleValue ]); useEffect(() => { const handleKeyDown = (event:KeyboardEvent) => { switch (event.key) { case 'PageUp': PrevPage(); console.log("up"); event.preventDefault(); // 기본 동작 방지 break; case 'PageDown': NextPage(); console.log("down"); event.preventDefault(); // 기본 동작 방지 break; case '+': ZoomCtrl( zoomIn( calcScaleValue || scaleValue ) ); break; case '-': ZoomCtrl( zoomOut( calcScaleValue || scaleValue ) ); break; default: break; } }; window.addEventListener('keydown', handleKeyDown); return () => { window.removeEventListener('keydown', handleKeyDown); }; }, [currentPage, scaleValue, calcScaleValue, totalPage]); // currentPage가 변경될 때마다 이벤트 리스너 갱신 return (