/* * Copyright (c) FOX EDU CO., LTD ALL RIGHT RESERVED. * by flashkid * 25.SEP.2024(WED) */ export class colouredPenciles { constructor(btnInit, objArr, coloursArr, coloursSelect = 1, brushMin = 0.5, brushMax = 10, saveDataURL = '', loadDataURL = '', deleteDataURL = '', dataParam = {}) { var _a; this.svgNS = "http://www.w3.org/2000/svg"; this.ctrlCon = null; this.targetCon = null; this.targetArr = []; this.ifrObj = null; this.ifrDoc = null; this.ifrContents = null; this.ifrContentsType = "etc"; this.svg = null; this.eraseObj = null; this.previewObj = null; this.loadedDrawingData = false; this.drawingGroup = null; this.maskingGroup = null; this.drawPathElement = null; this.eraserPathElement = null; this.undoStack = []; this.redoStack = []; this.currentPath = []; this.drawing = false; this.isErasing = false; this.detectTargetObj = () => { let tobj, i = 0; while (!tobj && i <= this.targetArr.length) { tobj = document.querySelector(`.${this.targetArr[i]}`); i++; } if (tobj) { this.targetCon = tobj; this.targetCon.classList.add('painter-target'); } }; // Function to handle the scroll event this.onScroll = () => { var _a; if (['img', 'iframe', 'etc', 'unknown'].includes(this.ifrContentsType) && ((_a = this.ifrObj) === null || _a === void 0 ? void 0 : _a.contentWindow) && this.targetCon) { this.ifrObj.contentWindow.scrollTo(this.targetCon.scrollLeft, this.targetCon.scrollTop); } else if (this.ifrContents && this.targetCon) { this.ifrContents.scrollTo(this.targetCon.scrollLeft, this.targetCon.scrollTop); } }; // 아이프레임 온로드 this.onIfrLoad = () => { var _a, _b, _c; if (this.btnInit.classList.contains('is-active')) { this.closeDrawing(); this.onResize(); } const ifrDoc = ((_b = (_a = this.ifrObj) === null || _a === void 0 ? void 0 : _a.contentWindow) === null || _b === void 0 ? void 0 : _b.document) || ((_c = this.ifrObj) === null || _c === void 0 ? void 0 : _c.contentDocument); if (!ifrDoc) return; this.ifrDoc = ifrDoc; // ifrContents 선언 this.ifrContents = this.ifrDoc.querySelector('#contents2') || this.ifrDoc.querySelector('.viewer .viewer-body .viewer-cont .quiz-viewer .quiz-viewer-body') || null; if (this.ifrContents.classList.contains('quiz-viewer-body')) { this.ifrContentsType = 'quiz'; this.setBaseSize({ w: this.ifrContents.querySelector('.quiz-viewer-cont').clientWidth, h: this.ifrContents.scrollHeight }); this.setBaseSize({ x: -this.ifrContents.querySelector('.quiz-viewer-cont').clientWidth / 2, y: 0 }); } else if (this.ifrContents.id === 'contents2') { const video = this.ifrContents.querySelector('video'); const img = this.ifrContents.querySelector('img'); const iframe = this.ifrContents.querySelector('iframe'); const youtube = this.ifrContents.querySelector('input#tempCntntsUrl'); const whiteboard = this.ifrContents.querySelector('.whiteboard'); if (video) { // 비디오 태그가 있을 경우 this.ifrContentsType = 'video'; this.setBaseSize({ x: -video.videoWidth / 2, y: -video.videoHeight / 2, w: video.videoWidth, h: video.videoHeight }); } else if (img) { // 이미지 태그가 있을 경우 this.ifrContentsType = 'img'; this.setBaseSize({ x: -img.naturalWidth / 2, y: -img.naturalHeight / 2, w: img.naturalWidth, h: img.naturalHeight }); } else if (iframe) { // 아이프레임이 있을 경우 this.ifrContentsType = 'iframe'; this.setBaseSize({ x: -iframe.clientWidth / 2, y: 0, w: iframe.clientWidth, h: iframe.scrollHeight }); } else if (youtube) { // 유튜브 프레임이 들어온 경우 this.ifrContentsType = 'youtube'; this.setBaseSize({ x: -960, y: -540, w: 1920, h: 1080 }); } else if (whiteboard) { // 화이트보드가 있을 경우 this.ifrContentsType = 'whiteboard'; this.setBaseSize({ x: -whiteboard.clientWidth / 2, y: -whiteboard.clientHeight / 2, w: whiteboard.clientWidth, h: whiteboard.clientHeight }); } else { this.ifrContentsType = 'etc'; this.setBaseSize({ w: this.ifrContents.clientWidth, h: this.ifrContents.scrollHeight }); this.setBaseSize({ x: -this.ifrContents.clientWidth / 2, y: 0 }); } } else { this.ifrContentsType = 'unknown'; this.setBaseSize({ w: this.ifrContents.clientWidth, h: this.ifrContents.scrollHeight }); this.setBaseSize({ x: -this.ifrContents.clientWidth / 2, y: 0 }); } }; this.onResize = () => { if (!this.ifrDoc || !this.svg || !this.ifrContents) return; const hasYScroll = this.ifrContents.scrollHeight > this.ifrContents.clientHeight; if (hasYScroll) { switch (this.ifrContentsType) { case 'img': // this.svg.style.height = `${this.ifrContents.scrollHeight}px`; // this.setViewBox(this.baseSize); // break; case 'quiz': this.svg.style.height = `${this.ifrContents.scrollHeight}px`; this.setViewBox(this.baseSize); break; case 'iframe': case 'unknown': case 'etc': this.setViewBox({ x: -this.ifrContents.clientWidth / 2, w: this.ifrContents.clientWidth, h: this.ifrContents.scrollHeight }); break; case 'video': case 'youtube': case 'whiteboard': default: this.setViewBox(this.baseSize); break; } } else { this.svg.style.height = ``; switch (this.ifrContentsType) { case 'quiz': this.svg.style.height = `${this.ifrContents.scrollHeight}px`; this.setViewBox({ h: this.ifrContents.scrollHeight }); break; case 'iframe': case 'unknown': case 'etc': this.setViewBox({ x: -this.ifrContents.clientWidth / 2, w: this.ifrContents.clientWidth, h: this.ifrContents.scrollHeight }); break; case 'img': case 'video': case 'youtube': case 'whiteboard': default: this.setViewBox(this.baseSize); break; } } }; this.setBaseSize = (newBaseSize) => { this.baseSize = Object.assign(Object.assign({}, this.baseSize), newBaseSize); // Merge with the existing baseSize }; this.setViewBox = (newSize) => { const newBaseSize = Object.assign(Object.assign({}, this.baseSize), newSize); if (!this.svg) return; this.svg.setAttribute("viewBox", `${newBaseSize.x} ${newBaseSize.y} ${newBaseSize.w} ${newBaseSize.h}`); }; this.createDrawingGroup = () => { var _a; const drawingGroup = document.createElementNS(this.svgNS, 'g'); (_a = this.svg) === null || _a === void 0 ? void 0 : _a.append(drawingGroup); this.drawingGroup = drawingGroup; }; this.createMaskingGroup = () => { var _a, _b; const maskingGroup = document.createElementNS(this.svgNS, 'mask'); maskingGroup.id = 'eraser-mask'; (_a = this.svg) === null || _a === void 0 ? void 0 : _a.append(maskingGroup); this.maskingGroup = maskingGroup; (_b = this.drawingGroup) === null || _b === void 0 ? void 0 : _b.setAttribute('mask', 'url(#eraser-mask)'); // 흰색 배경 (지우개 영역은 투명) const maskRect = document.createElementNS(this.svgNS, "rect"); maskRect.setAttribute("fill", "white"); maskRect.setAttribute("x", `${this.baseSize.x * 100}`); maskRect.setAttribute("y", `${this.baseSize.y * 100}`); maskRect.setAttribute("width", `${this.baseSize.w * 100}`); maskRect.setAttribute("height", `${this.baseSize.h * 100}`); this.maskingGroup.appendChild(maskRect); }; // 콘트롤 패널 드래그 앤 드롭 기능 추가 // this.enableDrag = (element) => { // let offsetX = 0; // let offsetY = 0; // let isDragging = false; // const onMouseDown = (e) => { // var _a; // e.preventDefault(); // isDragging = true; // // 현재 요소의 fixed 위치 계산 // const rect = element.getBoundingClientRect(); // const targetConRect = (_a = this.targetCon) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect(); // if (!targetConRect) // return; // // 마우스 포인터와 요소의 **오른쪽** 및 **위쪽** 경계와의 거리 계산 // // offsetX = rect.right - e.clientX; // // offsetY = e.clientY - rect.top; // // // 요소의 스타일을 고정 위치에 맞추어 설정 // // element.style.right = `${window.innerWidth - rect.right}px`; // right 기준으로 위치 지정 // // element.style.top = `${rect.top}px`; // // element.style.left = 'auto'; // left 속성 제거 // // element.style.transform = 'none'; // 기존 translateY 제거 // // element.style.transition = 'none'; // }; // const onMouseMove = (e) => { // var _a; // if (!isDragging) // return; // e.preventDefault(); // // painter-target 영역의 크기 가져오기 // const containerRect = (_a = this.targetCon) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect(); // if (!containerRect) // return; // // 새로운 위치 계산 // // let right = window.innerWidth - e.clientX - offsetX; // // let y = e.clientY - offsetY; // // // 요소의 x, y 위치가 painter-target 영역을 넘지 않도록 제한 // // right = Math.max(0, Math.min(right, window.innerWidth - containerRect.left - element.offsetWidth)); // // y = Math.max(containerRect.top, Math.min(y, containerRect.bottom - element.offsetHeight)); // // // 요소의 위치 업데이트 (오른쪽 기준) // // element.style.right = `${right}px`; // // element.style.top = `${y}px`; // }; // const onMouseUp = () => { // isDragging = false; // element.style.transition = ''; // }; // // 이벤트 리스너 등록 // element.addEventListener('mousedown', onMouseDown); // document.addEventListener('mousemove', onMouseMove); // document.addEventListener('mouseup', onMouseUp); // }; this.uiUpdate = () => { if (!this.targetCon) return; // targetCon이 null인지 확인 this.btnInit.classList.add('is-active'); this.targetCon.classList.add('is-active'); this.ctrlCon = document.createElement('aside'); this.ctrlCon.classList.add('painterbar'); this.targetCon.append(this.ctrlCon); // 드래그 기능 활성화 //this.enableDrag(this.ctrlCon); const minBtn = document.createElement('button'); minBtn.type = "button"; minBtn.classList.add('btn-painterbar-size'); minBtn.textContent = '색연필 최소화'; this.ctrlCon.append(minBtn); minBtn.onclick = () => { var _a; (_a = this.ctrlCon) === null || _a === void 0 ? void 0 : _a.classList.toggle('min'); // null 체크 }; const previewCon = document.createElement('div'); previewCon.classList.add('pencil-ctrl-options', 'pencil-preview'); previewCon.style.width = `${this.sizeRange['max'] * 0.1 + 0.2}rem`; previewCon.style.height = `${this.sizeRange['max'] * 0.1 + 0.2}rem`; this.ctrlCon.append(previewCon); this.previewObj = document.createElement('div'); previewCon.append(this.previewObj); const sizeCon = document.createElement('div'); sizeCon.classList.add('pencil-ctrl-options', 'painterbar-stroke'); this.ctrlCon.append(sizeCon); const range = document.createElement('input'); range.type = "range"; range.min = this.sizeRange['min'].toString(); range.max = this.sizeRange['max'].toString(); range.step = "0.1"; range.setAttribute("orient", "vertical"); range.value = this.size.toString(); sizeCon.append(range); // range input 이벤트에서 드래그 앤 드롭과 충돌 방지 range.addEventListener('mousedown', (e) => e.stopPropagation()); range.addEventListener('mousemove', (e) => e.stopPropagation()); range.addEventListener('mouseup', (e) => e.stopPropagation()); range.oninput = () => this.changeSize(Number(range.value)); // 값 변환 const fncCon = document.createElement('div'); fncCon.classList.add('pencil-ctrl-options', 'painterbar-fnc'); this.ctrlCon.append(fncCon); const undo = document.createElement('button'); undo.type = "button"; undo.classList.add('undo'); undo.textContent = "undo"; fncCon.append(undo); undo.addEventListener('click', this.undo); const redo = document.createElement('button'); redo.type = "button"; redo.classList.add('redo'); redo.textContent = "redo"; fncCon.append(redo); redo.addEventListener('click', this.redo); this.eraseObj = document.createElement('input'); this.eraseObj.type = "checkbox"; this.eraseObj.id = "colouredPencils_eraser"; this.eraseObj.classList.add('clear'); this.eraseObj.checked = this.isErasing; fncCon.append(this.eraseObj); const eraserLB = document.createElement('label'); eraserLB.setAttribute("for", this.eraseObj.id); eraserLB.textContent = "지우개"; fncCon.append(eraserLB); this.eraseObj.onchange = () => { this.switchEraser(); }; const reset = document.createElement('button'); reset.type = 'button'; reset.classList.add('reset'); reset.textContent = 'reset'; fncCon.append(reset); reset.onclick = () => this.reset(); // const save = document.createElement('button'); // save.type = 'button'; // save.classList.add('save'); // save.textContent = 'save'; // fncCon.append(save); // save.onclick = () => this.save(); const colorDiv = document.createElement('div'); colorDiv.classList.add('pencil-ctrl-options', 'painterbar-color'); colorDiv.setAttribute('data-opt', this.coloursArr[this.coloursSelect]); this.ctrlCon.append(colorDiv); this.coloursArr.forEach((el, i) => { const radio = document.createElement('input'); radio.type = "radio"; radio.name = "colouredPencils_colour"; radio.id = "colouredPencils_colour_" + i; radio.value = i.toString(); if (i === this.coloursSelect) radio.checked = true; colorDiv.append(radio); const label = document.createElement('label'); label.setAttribute("for", radio.id); label.textContent = el; label.style.backgroundColor = el; if (el === '#ffffff') label.classList.add('white'); colorDiv.append(label); radio.onchange = () => { this.changeColour(i); }; }); const close = document.createElement('button'); close.type = "button"; close.classList.add('btn-viewer-painter-close'); close.textContent = "색연필 닫기"; this.ctrlCon.append(close); close.onclick = () => { this.save(); this.closeDrawing(); }; this.updatePreview(); }; this.undo = () => { if (this.undoStack.length === 0) return; const paths = this.undoStack.pop(); if (!paths) return; Array.isArray(paths) ? paths.forEach(path => path.remove()) : {}; this.redoStack.push(paths); }; this.redo = () => { if (this.redoStack.length === 0) return; const paths = this.redoStack.pop(); if (!paths) return; // Check if drawingGroup and maskingGroup are not null if (!this.drawingGroup || !this.maskingGroup) return; Array.isArray(paths) ? paths.forEach(path => { if (path.dataset.use === 'draw') { this.drawingGroup.append(path); // Add null check with "!" to ensure not null } else { this.maskingGroup.append(path); // Add null check } }) : {}; this.undoStack.push(paths); }; this.switchEraser = () => { this.isErasing = !this.isErasing; if (this.eraseObj) { this.eraseObj.checked = this.isErasing; } this.updatePreview(); }; this.reset = () => { this.undoStack = []; this.redoStack = []; if (this.maskingGroup) { const maskPaths = this.maskingGroup.querySelectorAll('path'); maskPaths.forEach(path => path.remove()); } if (this.drawingGroup) { const drawPaths = this.drawingGroup.querySelectorAll('path'); drawPaths.forEach(path => path.remove()); } }; this.save = () => { if (!this.drawingGroup) return; const paths = this.drawingGroup.querySelectorAll('path'); const drawingData = []; paths.forEach(path => { const pathData = [ path.getAttribute('stroke'), parseFloat(path.getAttribute('stroke-width') || '0'), path.getAttribute('d'), ]; drawingData.push(pathData); }); if (drawingData.length === 0) { if (this.loadedDrawingData) { // 그린 데이터가 없는데 로드된 데이터가 있으면 삭제 this.deleteDrawingData(); } return; } if (this.maskingGroup) { const maskPaths = this.maskingGroup.querySelectorAll('path'); const maskData = []; maskPaths.forEach(path => { const maskPathData = [ path.getAttribute('stroke'), parseFloat(path.getAttribute('stroke-width') || '0'), path.getAttribute('d'), ]; maskData.push(maskPathData); }); this.saveDrawingData({ drawingData, maskData }); } }; this.changeSize = (size) => { this.size = Number(size); // Ensure the size is converted to a number this.updatePreview(); }; this.changeColour = (key) => { this.coloursSelect = key; if (this.isErasing) { this.switchEraser(); } this.updatePreview(); }; this.updatePreview = () => { if (this.previewObj) { this.previewObj.style.width = `${this.size * 0.1}rem`; this.previewObj.style.height = `${this.size * 0.1}rem`; if (this.coloursArr[this.coloursSelect] === '#ffffff') { this.previewObj.classList.add('white'); } else { this.previewObj.classList.remove('white'); } if (this.isErasing) { this.previewObj.classList.add('erase'); } else { this.previewObj.classList.remove('erase'); this.previewObj.style.backgroundColor = `${this.coloursArr[this.coloursSelect]}`; } } }; this.closeDrawing = () => { var _a; if (this.ctrlCon) { this.ctrlCon.remove(); this.ctrlCon = null; } if (this.drawingGroup) this.drawingGroup = null; if (this.maskingGroup) this.maskingGroup = null; if (this.svg && this.targetCon) { this.targetCon.removeChild(this.svg); this.svg = null; } this.isErasing = false; this.btnInit.classList.remove('is-active'); (_a = this.targetCon) === null || _a === void 0 ? void 0 : _a.classList.remove('is-active'); // resize 이벤트 리스너 제거 window.removeEventListener('resize', this.onResize); // beforeunload 이벤트 리스너 제거 // window.removeEventListener('beforeunload', this.beforeUnloadHandler); }; this.createDrawEvents = () => { if (!this.svg) return; this.svg.addEventListener("mousedown", this.pressEventHandler); this.svg.addEventListener("mousemove", this.dragEventHandler); this.svg.addEventListener("mouseup", this.releaseEventHandler); this.svg.addEventListener("mouseout", this.cancelEventHandler); this.svg.addEventListener("touchstart", this.pressEventHandler); this.svg.addEventListener("touchmove", this.dragEventHandler); this.svg.addEventListener("touchend", this.releaseEventHandler); this.svg.addEventListener("touchcancel", this.cancelEventHandler); }; // line 대신 path 사용으로 메모리 사용량 최적화 this.releaseEventHandler = () => { this.drawing = false; if (this.drawPathElement) { this.currentPath.push(this.drawPathElement); } if (this.eraserPathElement) { this.currentPath.push(this.eraserPathElement); } if (this.currentPath.length > 0) { this.undoStack.push(this.currentPath); this.redoStack = []; } this.drawPathElement = null; // 현재 그리던 path 초기화 this.eraserPathElement = null; // 현재 그리던 path 초기화 this.currentPath = []; // 드로잉이 끝나면 다시 기본 동작을 허용 this.preventPageInteractions(true); // 드로잉이 끝나면 다시 painterbar의 pointer-events 활성 this.disablePointerEvents(this.ctrlCon, false); }; this.cancelEventHandler = () => { this.releaseEventHandler(); }; this.pressEventHandler = (e) => { var _a; this.drawing = true; const newPoint = this.getMousePosition(e); // 드로잉 중에는 페이지에서 텍스트 선택 및 드래그가 되지 않도록 차단 this.preventPageInteractions(false); // 드로잉 중에는 painterbar의 pointer-events 비활성 this.disablePointerEvents(this.ctrlCon, true); // 드로잉 또는 지우기 동작 처리 const strokeColor = this.isErasing ? 'black' : this.coloursArr[this.coloursSelect]; this.startDrawing(strokeColor, this.size, this.isErasing, newPoint); // 마스킹 그룹에 path 가 있을 경우, 마스킹 그룹에도 path 추가 if (!this.isErasing && ((_a = this.maskingGroup) === null || _a === void 0 ? void 0 : _a.querySelector('path'))) { this.startDrawing('white', this.size, true, newPoint); } }; this.dragEventHandler = (e) => { if (!this.drawing) return; const newPoint = this.getMousePosition(e); // 새로운 path 요소 그리기 if (this.drawPathElement) { this.drawPath(`L ${newPoint.x} ${newPoint.y}`); } if (this.eraserPathElement) { this.drawPath(`L ${newPoint.x} ${newPoint.y}`, true); } e.preventDefault(); }; // 새로운 path를 생성하고 시작 지점을 그리는 함수 this.startDrawing = (strokeColor, strokeWidth, eraser, point) => { this.createNewPath(strokeColor, strokeWidth, eraser); this.drawPath(`M ${point.x} ${point.y} L ${point.x + 0.01} ${point.y}`, eraser); }; this.createNewPath = (strokeColor, strokeWidth, eraser = false) => { var _a, _b; // 새로운 path 요소 생성 const pathElement = document.createElementNS(this.svgNS, 'path'); pathElement.setAttribute('fill', 'none'); pathElement.setAttribute('stroke', strokeColor); pathElement.setAttribute('stroke-width', `${strokeWidth}`); pathElement.setAttribute('stroke-linecap', 'round'); pathElement.setAttribute('d', ''); if (!eraser) { pathElement.setAttribute('data-use', 'draw'); (_a = this.drawingGroup) === null || _a === void 0 ? void 0 : _a.append(pathElement); this.drawPathElement = pathElement; } else { (_b = this.maskingGroup) === null || _b === void 0 ? void 0 : _b.append(pathElement); this.eraserPathElement = pathElement; } }; this.drawPath = (d, eraser = false) => { const pathElement = eraser ? this.eraserPathElement : this.drawPathElement; // 새로운 path 요소 그리기 const od = pathElement.getAttribute('d'); pathElement.setAttribute('d', `${od} ${d}`); }; this.getMousePosition = (e) => { var _a; if (!this.svg) return new DOMPoint(0, 0); // Add a fallback value in case svg is null const point = this.svg.createSVGPoint(); point.x = e.changedTouches ? e.changedTouches[0].clientX : e.clientX; point.y = e.changedTouches ? e.changedTouches[0].clientY : e.clientY; return point.matrixTransform((_a = this.svg.getScreenCTM()) === null || _a === void 0 ? void 0 : _a.inverse()); // Convert screen coordinates to SVG coordinates }; // 페이지의 기본 선택 및 드래그 동작을 활성/비활성화하는 함수 this.preventPageInteractions = (state) => { if (state) { document.onselectstart = null; // 텍스트 선택 차단 해제 document.ondragstart = null; // 드래그 차단 해제 } else { document.onselectstart = () => false; // 텍스트 선택 차단 document.ondragstart = () => false; // 드래그 차단 } }; // pointer-events를 활성/비활성화하는 함수 this.disablePointerEvents = (target, state) => { if (!target) return; target.style.pointerEvents = state ? 'none' : ''; }; // 데이터 가져오기 this.loadDrawingData = () => { if (this.loadDataUrl === '') { // 세션스토리지에서 데이터 가져오기 const userKey = this.dataParam.userKey; const contentsKey = this.dataParam.contentsKey; if (userKey && contentsKey) { const storedData = sessionStorage.getItem(`drawingData-${userKey}-${contentsKey}`); if (storedData) { const parsedData = JSON.parse(storedData); const { drawingData, maskData } = parsedData; // 드로잉 데이터를 복원 if (drawingData && this.drawingGroup) { drawingData.forEach((line) => { this.createNewPath(line[0], line[1]); this.drawPath(line[2]); }); } // 마스크 데이터를 복원 if (maskData && this.maskingGroup) { maskData.forEach((line) => { this.createNewPath(line[0], line[1], true); this.drawPath(line[2], true); }); } this.loadedDrawingData = true; console.log('Data loaded from session storage'); } else { this.loadedDrawingData = false; console.log('No data found in session storage'); } } else { console.error('userKey or contentsKey is missing in dataParam'); } return; } // fetch(this.loadDataUrl, { // method: 'GET', // headers: { // 'Content-Type': 'application/json', // } // }) fetch(this.loadDataUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(this.dataParam), }) .then(response => response.json()) .then(data => { const { drawingData, maskData } = data; if (!drawingData || drawingData && drawingData.length === 0) { this.loadedDrawingData = false; return; } if (drawingData && this.drawingGroup) { drawingData.forEach((line) => { this.createNewPath(line[0], line[1]); this.drawPath(line[2]); }); } if (maskData && this.maskingGroup) { maskData.forEach((line) => { this.createNewPath(line[0], line[1], true); this.drawPath(line[2], true); }); } this.loadedDrawingData = true; }) .catch(error => console.error('Error loading drawing data:', error)); }; // 데이터 저장 this.saveDrawingData = (data) => { const params = Object.assign(Object.assign({}, this.dataParam), data); console.log('save data :', params); console.log(this.dataParam); // saveDataUrl이 비어 있으면 세션스토리지에 저장 if (this.saveDataUrl === '') { if ('userKey' in params && 'contentsKey' in params) { sessionStorage.setItem(`drawingData-${params['userKey']}-${params['contentsKey']}`, JSON.stringify(params)); console.log('Data saved to session storage'); } else { console.error('userKey or contentsKey is missing in params'); } return; } fetch(this.saveDataUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(params), }).then(response => this.reset()) .catch(error => console.error('Error saving drawing data:', error)); }; // 저장된 데이터 삭제 this.deleteDrawingData = () => { const params = Object.assign(Object.assign({}, this.dataParam)); // deleteDataUrl이 비어 있으면 세션스토리지를 확인하여 삭제 if (this.deleteDataUrl === '') { if ('userKey' in params && 'contentsKey' in params) { sessionStorage.removeItem(`drawingData-${params['userKey']}-${params['contentsKey']}`); console.log('Data deleted to session storage'); } else { console.error('userKey or contentsKey is missing in params'); } return; } fetch(this.deleteDataUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(this.dataParam), }) .then(response => { // 먼저 text로 받아서 확인 return response.text().then(text => { console.log("서버 응답 내용:\n", text); try { return JSON.parse(text); } catch (err) { console.error("⚠ JSON 파싱 실패: ", err); throw new Error("서버에서 JSON이 아닌 HTML 응답을 받음"); } }); }) .then(data => { console.log("정상적으로 파싱된 JSON:", data); }) .catch(error => { console.error("Fetch 처리 중 에러:", error); }); }; // 창 닫기 이벤트에서 save() 호출 this.beforeUnloadHandler = (event) => { this.save(); event.preventDefault(); // // 경고 메시지 표시 (선택 사항) // const confirmationMessage = '색연필로 작성한 내용이 저장되지 않을 수 있습니다.'; // event.returnValue = confirmationMessage; // return confirmationMessage; }; this.btnInit = btnInit; this.targetArr = objArr; this.coloursArr = coloursArr.map(c => c.toLowerCase()); this.coloursSelect = coloursSelect - 1; this.baseSize = { x: -800, y: -450, w: 1600, h: 900 }; brushMax = brushMax > 60 ? 60 : brushMax; this.sizeRange = { min: brushMin, max: brushMax }; this.size = Math.round((brushMin + brushMax) / 3); this.saveDataUrl = saveDataURL; this.loadDataUrl = loadDataURL; this.deleteDataUrl = deleteDataURL; this.dataParam = dataParam; this.btnInit.onclick = () => { var _a, _b; if (this.btnInit.classList.contains('is-active')) { this.save(); this.closeDrawing(); } else { this.detectTargetObj(); const svg = document.createElementNS(this.svgNS, 'svg'); this.svg = svg; this.svg.id = 'painterSvg'; this.svg.setAttribute("preserveAspectRatio", "xMidYMid meet"); (_a = this.targetCon) === null || _a === void 0 ? void 0 : _a.append(this.svg); this.createDrawingGroup(); // 드로잉 그룹 생성 this.createMaskingGroup(); // 마스크 생성 this.uiUpdate(); this.createDrawEvents(); this.loadDrawingData(); this.onResize(); // Listen for scroll event (_b = this.targetCon) === null || _b === void 0 ? void 0 : _b.addEventListener('scroll', this.onScroll); } }; const ifrObj = document.querySelector('.iframe-area iframe'); this.ifrObj = ifrObj; (_a = this.ifrObj) === null || _a === void 0 ? void 0 : _a.addEventListener('load', this.onIfrLoad); window.addEventListener('resize', this.onResize); // 창을 닫거나 새로고침할 때 save() 호출 // window.addEventListener('beforeunload', this.beforeUnloadHandler); //window.onbeforeunload = this.beforeUnloadHandler; document.addEventListener("visibilitychange", (event) => { if (document.visibilityState === 'hidden') { this.beforeUnloadHandler(event); // 사용자가 페이지를 떠나면 저장 } }); } // Getter for dataParam getDataParam() { return this.dataParam; } // Setter for dataParam setDataParam(newDataParam) { this.dataParam = Object.assign(Object.assign({}, this.dataParam), newDataParam); // Merge with the existing dataParam } saveDrawing() { this.save(); } } // Create the instance and attach it to the window object export function initColouredPenciles() { window.addEventListener('DOMContentLoaded', () => { const btnInit = document.querySelector('.btn-painter-toggle'); // 색연필 활성 버튼 if (!btnInit) { // console.error('.btn-painter-toggle 요소를 찾을 수 없습니다.'); return; } const objArr = ['viewer-painter']; // 색연필 콘테이너 클래스명 - 여러개 지정 가능, 앞에 위치한 클래스명의 엘리먼트 우선 const colorArray = ["#222222", "#fd4418", "#ffce00", "#00d56a", "#2f77ff", "#ffffff"]; // 색상 팔레트 6색 const defaultColor = 5; // 기본 선 굵기 const brushMin = 0.5, brushMax = 43; // 최소, 최대 선 굵기 const saveDataUrl = '/classroom/insertClassLessonWrtng.json'; // save api url const loadDataURL = '/classroom/selectClassLessonWrtng.json'; // load api url const deleteDataURL = '/classroom/deleteClassLessonWrtng.json'; // delete api url // const loadDataURL = 'lineData.json'; // test load api url /** dataParam은 외부에서 설정, 사용 환경에 따라 key, value 자유 설정 가능 */ const dataParam = {}; // 인스턴스 생성 후 전역 window 객체에 저장 window.cpInstance = new colouredPenciles(btnInit, objArr, colorArray, defaultColor, brushMin, brushMax, saveDataUrl, loadDataURL, deleteDataURL, dataParam); }); } initColouredPenciles();